/* ==========================================================================
   Stripi design system — trial adoption layer
   Source of truth: /Users/bwertheim/DESIGN.md

   This file is loaded LAST in <head>, after index.css and SiteLayout.css, so it
   re-points the compiled Tailwind theme at the Stripi tokens instead of forking
   the generated stylesheets. The page markup is untouched: every component in
   components/ already speaks the theme's variable vocabulary (--color-fern,
   --surface, --radius-box, .font-display, …), so remapping those variables
   repaints the whole site.

   Remove the single <link> in app/layout.tsx to back the whole trial out.

   Layout:
     1. Fonts
     2. Tokens          (colours, radii, spacing)
     3. Global type     (ss01, light weight, negative tracking)
     4. Display tier
     5. Buttons         (4px, measured against the live site)
     6. Cards & surfaces
     7. Inputs
     8. Navigation & footer
     9. Gradient mesh
    10. Tabular figures
    11. Responsive
   ========================================================================== */

/* --------------------------------------------------------------------------
   1. Fonts

   Sohne is proprietary; DESIGN.md nominates Inter as the sanctioned substitute
   ("Inter at weight 300 with ss01 and negative tracking approximates the rhythm
   closely"). The repo already self-hosts Inter, but only as static 400/500/700
   cuts — the brand's defining weight 300 is missing. These two faces add the
   variable cut (100–900) covering latin and latin-ext, self-hosted alongside the
   existing files so the trial adds no third-party request.
   -------------------------------------------------------------------------- */
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 100 900;
  font-display: swap;
  src: url('/fonts/inter-var-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;
}

@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 100 900;
  font-display: swap;
  src: url('/fonts/inter-var-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;
}

/* --------------------------------------------------------------------------
   2. Tokens
   -------------------------------------------------------------------------- */
:root,
:host {
  /* --- Raw palette, verbatim from DESIGN.md ---------------------------- */
  --ds-primary: #533afd;
  --ds-primary-deep: #4434d4;
  --ds-primary-press: #2e2b8c;
  --ds-primary-soft: #665efd;
  --ds-primary-subdued: #b9b9f9;
  --ds-brand-dark-900: #1c1e54;
  --ds-ink: #0d253d;
  --ds-ink-secondary: #273951;
  --ds-ink-mute: #64748d;
  --ds-ink-mute-2: #61718a;
  --ds-on-primary: #ffffff;
  --ds-canvas: #ffffff;
  --ds-canvas-soft: #f6f9fc;
  --ds-canvas-cream: #f5e9d4;
  --ds-hairline: #e3e8ee;
  --ds-hairline-input: #a8c3de;
  --ds-ruby: #ea2261;
  --ds-magenta: #f96bee;
  --ds-lemon: #9b6829;
  --ds-shadow-blue: #003770;

  /* Elevation ladder (DESIGN.md § Elevation & Depth). */
  --ds-elevation-1: rgba(0, 55, 112, 0.08) 0 1px 3px;
  --ds-elevation-2: rgba(0, 55, 112, 0.08) 0 8px 24px, rgba(0, 55, 112, 0.04) 0 2px 6px;

  /* Radius scale. */
  --ds-radius-xs: 4px;
  --ds-radius-sm: 6px;
  --ds-radius-md: 8px;
  --ds-radius-lg: 12px;
  --ds-radius-xl: 16px;
  --ds-radius-pill: 9999px;

  /* --- Corrections measured against the live stripe.com (2026-08-11) --------
     DESIGN.md's colour and type scales check out exactly against the real site
     (h1 48/300/-0.96px, h2 56/300/-1.4px, h3 26/300/-0.26px, body 16/300/1.4,
     primary #533afd, ink-mute #64748d — all confirmed). Two things in it do not.

     1. Button geometry. The spec mandates `{rounded.pill}` 9999px and explicitly
        forbids rounded-rectangles. The live site's buttons are 4px. The spec is
        wrong; the site wins. */
  --ds-radius-button: 4px;

  /*  2. The warm gradient stop. The spec lists `lemon #9b6829`, a dark ochre that
        cannot produce the sherbet in the real ribbon. Measured closer to: */
  --ds-sherbet: #ff8a2b;

  /*  3. `{colors.canvas-cream}` #f5e9d4 and the `card-cream-band` component are
        not on the live site — a sweep for any light warm fill over 300x120px
        returns nothing at all. Almost certainly the same bad warm sample as
        `lemon`. What the site does have, and the spec omits, is a full-bleed
        dark band (`section.hds-mode--dark`) filled: */
  --ds-band-dark: #0d1738;

  /*  And one substitution artefact, not a spec error: the spec is right that the
     display tier is weight 300 — but that is 300 *in Sohne*, which is optically
     far denser than Inter 300. Rendering the substitute at a literal 300 makes
     the page read anaemic next to Stripe. Inter 400 carries Sohne 300's colour,
     so the display and body tiers below use this rather than the literal value. */
  --ds-weight-display: 400;
  --ds-weight-body: 400;

  /* Spacing scale. */
  --ds-space-xxs: 2px;
  --ds-space-xs: 4px;
  --ds-space-sm: 8px;
  --ds-space-md: 12px;
  --ds-space-lg: 16px;
  --ds-space-xl: 24px;
  --ds-space-xxl: 32px;
  --ds-space-huge: 64px;

  /* --- Theme remap: the compiled Tailwind vocabulary --------------------
     Brand greens become the indigo CTA ramp; the greys become the navy ink
     ramp. Every downstream utility (.bg-fern, .text-fern-60, .text-reef,
     .bg-graphite, .border-deep-teal, …) follows from these five lines. */
  --color-fern: var(--ds-primary);
  --color-fern-60: var(--ds-primary-deep);
  /* `reef` is the accent used on dark surfaces — pale indigo carries the
     brand there, where primary-soft would fall below contrast on navy. */
  --color-reef: var(--ds-primary-subdued);
  --color-mist: var(--ds-on-primary);
  --color-deep-teal: var(--ds-brand-dark-900);
  --color-graphite: var(--ds-ink);

  /* Type stacks. `--font-display` was a serif (Season Mix); the brand's display
     tier is the same Sohne/Inter sans as body, differentiated by weight and
     tracking rather than by family. */
  --font-sans: 'Inter', 'SF Pro Display', system-ui, -apple-system, sans-serif;
  --font-display: 'Inter', 'SF Pro Display', system-ui, -apple-system, sans-serif;
  --font-serif: 'Inter', 'SF Pro Display', system-ui, -apple-system, sans-serif;

  /* .font-display / .font-serif read these two. Light weight + negative tracking
     is the brand's typographic signature. */
  --font-serif-weight: var(--ds-weight-display);
  --font-serif-letter-spacing-adjustment: -0.02em;

  /* Tailwind radius tokens. */
  --radius-box: var(--ds-radius-md);
  --radius-sm: var(--ds-radius-sm);
  --radius-md: var(--ds-radius-md);
  --radius-lg: var(--ds-radius-lg);
  --radius-xl: var(--ds-radius-xl);
  --radius-2xl: var(--ds-radius-xl);

  /* Tracking. */
  --tracking-tight: -0.025em;
}

/* Light track — canvas white page, canvas-soft feature surfaces. */
:root {
  --background: var(--ds-canvas);
  --surface: var(--ds-canvas-soft);
  --surface-hover: color-mix(in srgb, var(--ds-canvas-soft) 90%, var(--ds-primary) 10%);
  --footer-background: var(--ds-canvas);
  --foreground: var(--ds-ink);
  --muted-foreground: var(--ds-ink-mute);
  --border: var(--ds-hairline);
  --content-accent: var(--ds-primary);
  --site-header-active-underline: var(--ds-primary);

  /* text-input. Kept as its own token trio so the field follows the active
     track instead of being pinned to the light canvas. */
  --ds-input-bg: var(--ds-canvas);
  --ds-input-border: var(--ds-hairline-input);
  --ds-input-fg: var(--ds-ink);
  --ds-input-placeholder: var(--ds-ink-mute);
}

/* --- Light only -----------------------------------------------------------
   stripe.com has no dark mode. Not "a subtle one" — zero `prefers-color-scheme:
   dark` rules across its stylesheets, and identical computed styles under a dark
   OS preference. Checked 2026-08-11.

   DESIGN.md agrees, and reads differently once you look closely: its "dashboard
   track flips polarity to a familiar dark-app shell" is about
   dashboard.stripe.com, a separate surface listed among its own sources. On the
   marketing page the spec is explicit that "the lower portion of the page
   returns to white" and that the dark thing is the *mockups* — "deep navy
   product UI mockups sit composited above the white canvas". Navy is a
   component fill (product mockups, the featured pricing tier), not a theme.

   The dark track on this page is inherited from the page's original design, not from
   the design system. Under the system it has no counterpart, and it was the
   source of most of the defects found along the way — navy on navy, blank white
   panels. So the trial pins the page light and the dark surfaces stay exactly
   where the spec puts them: the terminal, the featured card, the mockups.

   Three separate switches have to be held, because the inherited CSS flips in
   three different ways:
     1. `data-dev-theme="light"` on <html> (set in app/layout.tsx) disables the
        rules in index.css guarded by `:root:not([data-dev-theme=light])`.
     2. SiteLayout.css flips its tokens on a *bare* `:root` inside the media
        query, which no attribute can reach — so the light values are restated
        here, in the same media query, from a sheet that loads later.
     3. The how-it-works bundle reads `prefers-color-scheme` in its own JS and
        sets `data-theme="alfred-dark"` on its scope, so that scope is pinned too.

   To restore dark mode: delete this block and the `data-dev-theme` attribute in
   app/layout.tsx. The per-track values it replaced are in the branch history. */
:root {
  color-scheme: light;
}

@media (prefers-color-scheme: dark) {
  :root {
    color-scheme: light;
    --background: var(--ds-canvas);
    --surface: var(--ds-canvas-soft);
    --surface-hover: color-mix(in srgb, var(--ds-canvas-soft) 90%, var(--ds-primary) 10%);
    --footer-background: var(--ds-canvas);
    --foreground: var(--ds-ink);
    --muted-foreground: var(--ds-ink-mute);
    --border: var(--ds-hairline);
    --content-accent: var(--ds-primary);
    --site-header-active-underline: var(--ds-primary);

    --ds-input-bg: var(--ds-canvas);
    --ds-input-border: var(--ds-hairline-input);
    --ds-input-fg: var(--ds-ink);
    --ds-input-placeholder: var(--ds-ink-mute);
  }
}

/* The demo bundle sets its own scope theme from `prefers-color-scheme` in JS,
   so the attribute it writes is neutralised rather than the media query. */
[data-hiw-scope][data-theme='alfred-dark'],
[data-hiw-scope]:has(input.theme-controller[value='alfred-dark']:checked) {
  --color-accent: var(--ds-primary);
  --color-accent-content: var(--ds-on-primary);
  --background: var(--ds-canvas);
  --foreground: var(--ds-ink);
  --card-foreground: var(--ds-ink);
  --popover-foreground: var(--ds-ink);
  --muted-foreground: var(--ds-ink-mute);
  --border-color: var(--ds-hairline);
  --border-focus: var(--ds-primary);
}

/* --- The "How it works" demo bundle ------------------------------------
   /how-it-works/demo-*.css is injected into <head> at runtime and carries its
   own copy of the theme, scoped to [data-hiw-scope] rather than to :root — so
   the page-level remap above cannot reach inside the frame and the demo would
   otherwise keep rendering the old green.

   Every declaration in that bundle is wrapped in :where(), i.e. specificity
   zero, and the palette half of it sits in `@layer theme`. A plain attribute
   selector therefore wins on both counts, whatever order the sheets land in.

   The fern/reef ramps are mixed off the primary rather than picked by eye, so
   the demo introduces no colour the spec doesn't document. */
[data-hiw-scope] {
  --color-fern: var(--ds-primary);
  --color-fern-10: color-mix(in srgb, var(--ds-primary) 12%, #ffffff);
  --color-fern-20: color-mix(in srgb, var(--ds-primary) 22%, #ffffff);
  --color-fern-30: color-mix(in srgb, var(--ds-primary) 34%, #ffffff);
  --color-fern-40: color-mix(in srgb, var(--ds-primary) 48%, #ffffff);
  --color-fern-50: color-mix(in srgb, var(--ds-primary) 70%, #ffffff);
  --color-fern-60: var(--ds-primary-deep);
  --color-fern-70: var(--ds-primary-press);
  --color-fern-80: color-mix(in srgb, var(--ds-primary-press) 72%, var(--ds-ink));
  --color-fern-90: color-mix(in srgb, var(--ds-primary-press) 48%, var(--ds-ink));
  --color-fern-100: color-mix(in srgb, var(--ds-primary-press) 28%, var(--ds-ink));

  --color-reef: var(--ds-primary-subdued);
  --color-reef-60: color-mix(in srgb, var(--ds-primary-subdued) 78%, var(--ds-brand-dark-900));
  --color-reef-70: color-mix(in srgb, var(--ds-primary-subdued) 58%, var(--ds-brand-dark-900));
  --color-reef-80: color-mix(in srgb, var(--ds-primary-subdued) 38%, var(--ds-brand-dark-900));
  --color-reef-100: color-mix(in srgb, var(--ds-primary-subdued) 18%, var(--ds-brand-dark-900));

  --color-mist: var(--ds-on-primary);
  --color-deep-teal: var(--ds-brand-dark-900);
  --color-graphite: var(--ds-ink);

  --font-sans: 'Inter', 'SF Pro Display', system-ui, -apple-system, sans-serif;
  --font-serif: 'Inter', 'SF Pro Display', system-ui, -apple-system, sans-serif;
  /* The bundle keeps its own --font-display alongside --font-serif; the demo's
     headings read that one. */
  --font-display: 'Inter', 'SF Pro Display', system-ui, -apple-system, sans-serif;
  --font-serif-weight: var(--ds-weight-display);
  --font-serif-letter-spacing-adjustment: -0.02em;

  --radius-box: var(--ds-radius-md);

  /* Light track (the bundle's `alfred-light`). */
  --color-accent: var(--ds-primary);
  --color-accent-content: var(--ds-on-primary);
  --background: var(--ds-canvas);
  --foreground: var(--ds-ink);
  --card-foreground: var(--ds-ink);
  --popover-foreground: var(--ds-ink);
  --muted-foreground: var(--ds-ink-mute);
  --border-color: var(--ds-hairline);
  --border-focus: var(--ds-primary);
}

/* --- The other half of the bundle's theme --------------------------------
   The block above remaps the *ported brand* vocabulary — fern, reef, mist,
   deep-teal, graphite — and stops there. But the bundle ships a second,
   independent palette underneath it: a DaisyUI theme, declared in the same
   `:where([data-hiw-scope][data-theme='alfred-light'])` rule, with its own
   `base-*` surface ramp and its own `primary`/`secondary`/`success`/`error`.
   Nothing above reaches it, so until now the demo was a Stripi frame around a
   Tasklet interior:

     --color-primary   #4e814e   the old green — this is why the visitor's
                                 message bubble (`bg-primary/10`) and the
                                 "Trabajando…" dots (`bg-primary`) were green
                                 on an indigo page
     --color-base-200  #eaeaea   the sidebar rail, the document panel and the
                                 panel header — flat neutral grey, which is what
                                 made that whole right-hand column read dead
                                 against the bloom outside the shell
     --color-base-300  #dfdfdf   the seams between them
     --color-base-content #121215 the near-black the demo tints ~260 surfaces
                                 and borders off, at every alpha from /10 to /90

   None of these is precautionary: every token below is referenced by the
   bundle's own stylesheet, `base-content` 262 times and `primary` 85. The
   surface ramp keeps its three steps and its contrast, it just moves onto the
   house's: #ffffff → #f6f9fc → #e3e8ee, where it was #f5f5f5 → #eaeaea →
   #dfdfdf. Because the demo tints from `base-content`, moving it to `--ds-ink`
   also swings dozens of derived fills from neutral grey to the navy ink ramp,
   which is the whole point.

   Two mappings are judgement calls rather than translations:

     · **success** has no counterpart. The house palette has no green at all —
       the page's own "done" and "registered" signals are indigo (`#audit`'s
       shield is `text-fern-60`). So it lands on the deep primary, and the demo
       introduces no colour the spec does not document.
     · **warning** goes to `--ds-lemon` and not to `--ds-sherbet`. Sherbet is
       the gradient stop, tuned to be seen at 40% over a ramp; as a solid behind
       `warning-content` white it fails contrast. Lemon is the dark ochre the
       spec lists, and it is legible.

   Specificity is the same free win as the block above: the bundle wraps every
   one of these in `:where()`, so a bare attribute selector outranks it in any
   load order. */
[data-hiw-scope] {
  /* Surface ramp and the ink it tints from. */
  --color-base-100: var(--ds-canvas);
  --color-base-200: var(--ds-canvas-soft);
  --color-base-300: var(--ds-hairline);
  --color-base-content: var(--ds-ink);
  --muted: var(--ds-canvas-soft);

  /* The two sidebar states, mixed toward the primary the same way
     `--surface-hover` is at the page level rather than picked as greys. */
  --sidebar-hover: color-mix(in srgb, var(--ds-canvas-soft) 88%, var(--ds-primary) 12%);
  --sidebar-active: color-mix(in srgb, var(--ds-canvas-soft) 78%, var(--ds-primary) 22%);

  /* Semantic slots. */
  --color-primary: var(--ds-primary);
  --color-primary-content: var(--ds-on-primary);
  --color-secondary: var(--ds-primary-deep);
  --color-secondary-content: var(--ds-on-primary);
  --color-neutral: var(--ds-brand-dark-900);
  --color-neutral-content: var(--ds-canvas);
  --color-info: var(--ds-primary-deep);
  --color-info-content: var(--ds-on-primary);
  --color-success: var(--ds-primary-deep);
  --color-success-content: var(--ds-on-primary);
  --color-warning: var(--ds-lemon);
  --color-warning-content: var(--ds-canvas);
  --color-error: var(--ds-ruby);
  --color-error-content: var(--ds-on-primary);

  /* The two leftover ported ramps the bundle still reaches for. Portage was a
     muted blue-violet standing in for the accent; coral was the warm stop. */
  --portage: var(--ds-primary-deep);
  --color-portage-20: color-mix(in srgb, var(--ds-primary-subdued) 46%, #ffffff);
  --color-portage-40: var(--ds-primary-subdued);
  --color-portage-60: var(--ds-primary-soft);
  --color-portage-70: var(--ds-primary-deep);
  --color-portage-80: var(--ds-primary-press);

  --color-coral: var(--ds-sherbet);
  --color-coral-10: color-mix(in srgb, var(--ds-sherbet) 16%, #ffffff);
  --color-coral-30: color-mix(in srgb, var(--ds-sherbet) 42%, #ffffff);
  --color-coral-60: var(--ds-lemon);
  --color-coral-70: color-mix(in srgb, var(--ds-lemon) 82%, var(--ds-ink));
  --color-coral-80: color-mix(in srgb, var(--ds-lemon) 64%, var(--ds-ink));
}

/* The one green no token remap can reach. `_astro/index.BRimgEk0.css` writes the
   composer's active-automation button as

     border-color: var(--color-fern);   color: var(--color-fern);
     background: #4e814e1a;

   — two thirds through the variable, the fill hardcoded. So the button came out
   with an indigo border and an indigo glyph on a green wash, which is worse than
   being uniformly wrong. `--ds-fill-indigo` is the house's documented pale fill
   for exactly this (chips and soft buttons) and lands where fern-at-10%-over-
   white did. Only visible in the last chapter, when the weekly trigger is live.

   Two more hardcoded greens survive in that file, both on
   `.landing-page-how-it-works-frame` under `data-dev-theme=dark` — dead while
   `app/layout.tsx` pins the page light, and already noted in § 20. */
.landing-how-it-works-input button[data-active] {
  background: var(--ds-fill-indigo);
}

/* The demo renders the wordmark as <img src="/logo.svg">, which can't inherit
   currentColor the way the header's inline mark does. Swapping the replaced
   element's content is the only way to reach it from a stylesheet. */
[data-hiw-scope] .landing-how-it-works-sidebar-logo,
[data-hiw-scope] .landing-how-it-works-home-logo {
  content: url('/stripe/logo.svg');
}

/* The bundle's headings carry their own bold; the display tier runs light
   everywhere, inside the product mockup included. */
[data-hiw-scope] :is(h1, h2, h3) {
  font-weight: var(--ds-weight-display);
  letter-spacing: -0.26px;
}

/* --------------------------------------------------------------------------
   3. Global type

   `ss01` globally on the body is an explicit rule in DESIGN.md; body defaults to
   body-md (15px / 1.4), at the substitution-corrected weight.
   -------------------------------------------------------------------------- */
body {
  font-family: var(--font-sans);
  font-size: 15px;
  font-weight: var(--ds-weight-body);
  line-height: 1.4;
  font-feature-settings: 'ss01';
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

/* The brand runs light. `font-semibold` (600) and `font-bold` (700) in the
   ported markup are pulled back to the 400 "regular" role so the editorial air
   survives; the display tier is handled separately in § 4. */
.landing-page .font-semibold,
.landing-page .font-medium,
footer .font-semibold,
footer .font-medium,
[data-site-header] .font-semibold,
[data-site-header] .font-medium {
  --tw-font-weight: 400;
  font-weight: 400;
}

/* Body copy tiers. */
.landing-page p.text-lg,
.landing-page p.text-xl {
  font-size: 1rem; /* body-lg */
  line-height: 1.4;
  font-weight: var(--ds-weight-body);
  letter-spacing: 0;
}

.landing-page p.text-base {
  font-size: 0.9375rem; /* body-md */
  line-height: 1.4;
  font-weight: var(--ds-weight-body);
}

/* Eyebrows — micro-cap: 10px / 400 / +0.1px, and always in the accent indigo. */
.landing-page .text-xs.uppercase,
footer h2.text-xs.uppercase {
  font-size: 0.625rem; /* micro-cap, 10px */
  line-height: 1.15;
  font-weight: 400;
  letter-spacing: 0.1em;
}

/* Fine print. */
.landing-page .text-\[0\.625rem\],
.landing-page .text-\[0\.6875rem\] {
  font-weight: var(--ds-weight-display);
}

/* --------------------------------------------------------------------------
   4. Display tier

   Sizes stair-step 56 -> 48 -> 32 -> 26 -> 22px with tracking scaling from
   -1.4px down to -0.22px. Values are expressed in px to match the spec exactly
   and clamped so the mobile end lands on the documented 36px floor.
   -------------------------------------------------------------------------- */
.landing-page .font-display,
footer .font-display {
  font-family: var(--font-display);
  font-weight: var(--ds-weight-display);
}

/* display-xxl — hero headline. 56px desktop, 36px mobile. */
.landing-page h1.font-display {
  font-size: clamp(36px, 4.6vw, 56px);
  line-height: 1.03;
  letter-spacing: -1.4px;
  font-weight: var(--ds-weight-display);
}

/* display-xl — section opener. */
.landing-page h2.font-display {
  font-size: clamp(32px, 3.6vw, 48px);
  line-height: 1.15;
  letter-spacing: -0.96px;
  font-weight: var(--ds-weight-display);
}

/* display-lg / display-md — card and sub-section titles. */
.landing-page h3.font-display,
.landing-page .font-display.text-xl,
.landing-page .font-display.text-2xl {
  font-size: 26px;
  line-height: 1.12;
  letter-spacing: -0.26px;
  font-weight: var(--ds-weight-display);
}

/* The hero's accent span is the one place indigo carries display type — it is
   emphasis on a headline, not body text, which the spec permits. */
.landing-page h1.font-display .text-fern,
.landing-page h1.font-display .text-reef {
  color: var(--ds-primary);
}

/* --------------------------------------------------------------------------
   5. Buttons — pill geometry

   "Don't replace the pill shape with rounded-rectangles for buttons." Every CTA,
   nav affordance and tag in the ported markup is matched below and given
   `{rounded.pill}`; padding floors at the documented 8px 16px.
   -------------------------------------------------------------------------- */

/* button-primary-pill — the filled indigo CTA. */
.landing-page a.rounded.border.border-fern.bg-fern,
[data-site-header] a.rounded.border.border-fern.bg-fern,
.landing-page-product-input-submit {
  background-color: var(--ds-primary);
  border-color: var(--ds-primary);
  color: var(--ds-on-primary);
  border-radius: var(--ds-radius-button);
  padding: var(--ds-space-sm) var(--ds-space-lg);
  font-size: 16px;
  font-weight: 400;
  line-height: 1;
  letter-spacing: 0;
}

.landing-page a.rounded.border.border-fern.bg-fern:hover,
[data-site-header] a.rounded.border.border-fern.bg-fern:hover,
.landing-page-product-input-submit:hover {
  background-color: var(--ds-primary-deep);
  border-color: var(--ds-primary-deep);
}

.landing-page a.rounded.border.border-fern.bg-fern:active,
[data-site-header] a.rounded.border.border-fern.bg-fern:active,
.landing-page-product-input-submit:active {
  background-color: var(--ds-primary-press);
  border-color: var(--ds-primary-press);
}

/* The hero submit keeps its own fixed height from index.css; restate it so the
   pill reads as a pill rather than a stadium-cornered slab. */
.landing-page-product-input-submit {
  height: auto;
  min-height: 40px;
  padding-inline: var(--ds-space-lg);
  transform: none;
}

.landing-page-product-input-submit:hover {
  transform: none;
}

/* button-secondary — outline pill, indigo on canvas. */
.landing-page a.rounded.border.border-border.px-5.py-3,
.landing-page a.rounded.border.border-border.px-4.py-2 {
  border-radius: var(--ds-radius-button);
  border-color: var(--content-accent);
  color: var(--content-accent);
  background-color: var(--color-background);
  padding: var(--ds-space-sm) var(--ds-space-lg);
  font-size: 16px;
  font-weight: 400;
  line-height: 1;
}

.landing-page a.rounded.border.border-border.px-5.py-3:hover,
.landing-page a.rounded.border.border-border.px-4.py-2:hover {
  border-color: var(--content-accent);
  color: var(--content-accent);
  background-color: color-mix(in srgb, var(--color-background) 92%, var(--ds-primary));
}

/* button-on-dark — sits inside the deep-navy band. */
.landing-page a.rounded.border.border-reef.bg-reef {
  border-radius: var(--ds-radius-button);
  background-color: var(--ds-primary);
  border-color: var(--ds-primary);
  color: var(--ds-on-primary);
  padding: var(--ds-space-sm) var(--ds-space-lg);
  font-size: 16px;
  font-weight: 400;
  line-height: 1;
}

.landing-page a.rounded.border.border-reef.bg-reef:hover {
  background-color: var(--ds-primary-soft);
  border-color: var(--ds-primary-soft);
  color: var(--ds-on-primary);
}

/* Plain text-link buttons in the header ("Log in") and mobile menu. */
[data-site-header] a.rounded.px-3.py-2,
[data-site-header] a.rounded-md,
[data-site-header] .site-header-menu summary,
.landing-page a.rounded.px-3.py-2,
.landing-page-scroll-cue {
  border-radius: var(--ds-radius-button);
}

/* link-on-light — inline links carry the primary, no underline. */
.landing-page a.text-fern-60,
.landing-page a:not([class*='bg-']):not(.rounded):hover {
  text-decoration: none;
}

/* --------------------------------------------------------------------------
   6. Cards & surfaces
   -------------------------------------------------------------------------- */

/* card-feature-light / card-pricing — 12px, hairline border, level-1 lift. */
.landing-page .rounded.border.border-border.bg-surface,
.landing-page .rounded.border.border-border.bg-background,
.landing-page .rounded.border.border-dashed.border-border,
.landing-page .rounded.border.border-border.bg-surface\/50 {
  border-radius: var(--ds-radius-lg);
  border-color: var(--ds-hairline);
  box-shadow: var(--ds-elevation-1);
}

/* card-pricing-featured / card-dashboard-mockup — the inverted navy surfaces.
   Level 2 elevation, 12px chrome. */
.landing-page .rounded.border.border-deep-teal.bg-deep-teal,
.landing-page .rounded.bg-graphite,
.landing-page .rounded.border.border-border.bg-graphite,
.landing-page .overflow-hidden.rounded.border.border-border.bg-background {
  border-radius: var(--ds-radius-lg);
  box-shadow: var(--ds-elevation-2);
}

/* Product-UI mockup chrome sits one step rounder. */
.landing-page-how-it-works-frame {
  border-radius: var(--ds-radius-xl);
  border-color: var(--ds-hairline);
  background: var(--ds-canvas-soft);
  box-shadow: var(--ds-elevation-2);
}

/* Icon tiles / avatars — compact 8px, not the card 12px. */
.landing-page .size-9.rounded.border,
.landing-page .size-10.rounded.border,
.landing-page .size-14.rounded.border,
.landing-page .size-20.rounded.border {
  border-radius: var(--ds-radius-md);
  border-color: var(--ds-hairline);
}

/* pill-tag-soft — subdued indigo chips. */
.landing-page .rounded.border.border-fern\/30.bg-fern\/10,
.landing-page .rounded.border.border-reef\/20.bg-reef\/10,
.landing-page .rounded.border.border-mist\/20.bg-mist\/10 {
  border-radius: var(--ds-radius-button);
  padding: var(--ds-space-xs) var(--ds-space-sm);
  font-size: 10px;
  line-height: 1.15;
  font-weight: 400;
  letter-spacing: 0.1px;
  text-transform: uppercase;
}

.landing-page .rounded.border.border-fern\/30.bg-fern\/10 {
  background-color: var(--ds-primary-subdued);
  border-color: var(--ds-primary-subdued);
  color: var(--ds-primary-deep);
}

/* Status dots and chart bars read as true circles / soft bars. Dots are the one
   place the pill radius survives the 4px correction — they are circles, not
   buttons. */
.landing-page .size-1\.5.rounded,
.landing-page .size-2.rounded,
.landing-page .size-3.rounded {
  border-radius: var(--ds-radius-pill);
}

.landing-page .flex-1.rounded[class*='h-\['] {
  border-radius: var(--ds-radius-xs);
}

/* The Security band — corrected from `card-cream-band` --------------------
   This was built as the spec's warm interlude: `{colors.canvas-cream}` #f5e9d4
   filling the band with the navy card floating on it. Measured against the live
   site, that is wrong twice over.

   There are **no warm surfaces on stripe.com at all** — a sweep of every element
   over 300x120px for a light fill with red meaningfully above blue returns
   nothing. `canvas-cream` is very probably from the same bad warm sample that
   produced `lemon #9b6829`, which § 2 already corrects.

   What the live site does have, and the spec never mentions, is a **full-bleed
   dark band**: `section.hds-mode--dark`, 1440x2341, filled #0d1738 — a deep blue
   navy, not the purple-leaning brand-dark-900. Multi-tone headline in white and
   slate, filled indigo CTA, chips a step lighter than the field.

   So the section becomes the band rather than a card sitting on one: the fill
   moves to the section, and the card inside goes transparent and loses its
   padding, since the section already carries the vertical rhythm. */
.landing-page > section#security {
  background-color: var(--ds-band-dark);
  background-image: radial-gradient(
    92% 70% at 50% 0%,
    color-mix(in srgb, var(--ds-primary) 16%, transparent),
    var(--ds-primary-0) 72%
  );
  border-top-color: transparent;
  color: var(--ds-canvas);

  /* A fixed dark surface, so it holds the dark end of the ramp regardless of
     what the page around it is doing — same treatment as the terminal. */
  --ds-tone-strong: var(--ds-canvas);
  --ds-tone-body: color-mix(in srgb, var(--ds-canvas) 70%, var(--ds-band-dark));
  --ds-tone-caption: color-mix(in srgb, var(--ds-canvas) 52%, var(--ds-band-dark));
  --ds-section-accent: var(--ds-primary-subdued);
  --color-foreground: var(--ds-canvas);
  --color-muted-foreground: color-mix(in srgb, var(--ds-canvas) 66%, var(--ds-band-dark));
}

/* The band's right half was empty — on the live site that space always carries a
   graphic. This is a sibling to the hero ribbon rather than a repeat: same fan
   geometry radiating from a focal point off the right edge, drawn as fine
   line-work over a colour glow instead of blurred bands. It bookends the page,
   opening and closing on the same device in two registers.

   Bled past the section's right and vertical edges so it reads as a crop of
   something larger, and masked back on its left so it never reaches the copy. */
.landing-page > section#security {
  position: relative;
  overflow: hidden;
  isolation: isolate;
}

/* The fan turns continuously about the point its rays radiate from, which takes two layers
   rather than one.

   `::before` is the fan, and it carries no mask. Its `transform-origin` is the generator's
   focal point: `tools/generate-security-fan.mjs` puts the focus at fx=1010, fy=470 in a
   900x900 viewBox — off the right edge, so the burst opens leftward into the band — which is
   112% 52% of that box, the same coordinate `#sf-reach` encodes as cx="1.12" cy="0.52".
   Rotating about the element's own centre instead would swing the whole graphic across the
   band rather than turning the rays in place.

   `::after` is the reason the mask moved off the fan. `mask-image` transforms with its
   element, so a rotating fan carries its own left-edge fade around with it and stops holding
   the rays off the copy within a few degrees. Instead `::after` paints an exact copy of the
   band's own background — same colour, same glow, same box — over the fan's left portion and
   fades out to the right. Because what it paints *is* the backdrop, it is invisible as an
   overlay and the composite is identical to the old single-layer version; it just does not
   rotate. Both pseudo-elements sit at z-index -1, so they paint in document order and
   `::after` lands over `::before`.

   Note for `.ds-band-dark--fan`, the reusable version further down: it is still the
   single-layer static fan. This id-keyed pair is the reference implementation if it ever
   needs to move too. */
.landing-page > section#security::before {
  content: '';
  position: absolute;
  z-index: -1;
  pointer-events: none;

  /* A square centred on the pivot, not a rectangle cropped to the band. The generator now
     puts the focal point at the centre of the SVG (see the note at the top of
     tools/generate-security-fan.mjs), so centring the element on the pivot makes a rotation
     about it map the square onto itself — every direction stays painted through the whole
     revolution, and `transform-origin` is an exact 50% 50% instead of a percentage corrected
     for `background-size: cover`.

     The pivot is 114% across and half way down: just off the section's right edge, which is
     what makes the burst read as opening leftward across the band. Side is 170% of the
     section's width; `aspect-ratio` keeps it square, and because the height then equals 170%
     of the *width*, `margin-top: -85%` — also resolved against the width — is exactly half
     of it. Both margins are therefore exact, with no `translate` to compose with the
     animation's `transform`. */
  left: 114%;
  top: 50%;
  width: 170%;
  aspect-ratio: 1;
  margin-left: -85%;
  margin-top: -85%;

  background: url('/stripe/security-fan.svg') no-repeat center;
  background-size: 100% 100%;
  opacity: 0.85;
  transform-origin: 50% 50%;
  /* One revolution every three minutes. */
  animation: ds-security-fan-spin 180s linear infinite;
  will-change: transform;
}

@keyframes ds-security-fan-spin {
  to {
    transform: rotate(360deg);
  }
}

.landing-page > section#security::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background-color: var(--ds-band-dark);
  background-image: radial-gradient(
    92% 70% at 50% 0%,
    color-mix(in srgb, var(--ds-primary) 16%, transparent),
    var(--ds-primary-0) 72%
  );
  /* The old fade in section coordinates rather than fan-element ones, and inverted, since
     this layer hides where the fan used to be transparent. The fan starts 42% across the
     section and is 64% of it wide, so the old stops at 26%/54%/74% of the fan land at
     59%/77%/89% of the section. */
  mask-image: linear-gradient(
    to right,
    #000 0%,
    #000 42%,
    rgba(0, 0, 0, 0.7) 59%,
    rgba(0, 0, 0, 0.15) 77%,
    transparent 89%
  );
}

/* Same contract as the scroll reveals in landing-page.js: motion is decoration here, and the
   band has to read identically without it. A permanently animating layer is also a permanent
   compositor cost, so this is the opt-out that actually matters. */
@media (prefers-reduced-motion: reduce) {
  .landing-page > section#security::before {
    animation: none;
    will-change: auto;
  }
}

/* Below the two-column breakpoint the copy runs full width, so the fan retreats
   to the top-right corner instead of sitting behind the text. */
@media (max-width: 1023px) {
  /* The pivot moves up to the corner and the square shrinks; on a band this tall the square's
     own size is what keeps the burst in the upper portion, so the old explicit `height: 48%`
     is no longer needed. */
  .landing-page > section#security::before {
    left: 112%;
    top: 14%;
    width: 130%;
    margin-left: -65%;
    margin-top: -65%;
    opacity: 0.6;
  }

  /* The corner crop starts 34% across and is 76% of the section wide, so this breakpoint's
     stops at 34%/68% of the fan land at 60%/86% of the section. The overlay still spans the
     whole band — it only has to be transparent wherever the fan is meant to show. */
  .landing-page > section#security::after {
    mask-image: linear-gradient(
      to right,
      #000 0%,
      #000 34%,
      rgba(0, 0, 0, 0.6) 60%,
      transparent 86%
    );
  }
}

/* The card is now the band, so it stops being a card. */
.landing-page #security .rounded.border.border-deep-teal.bg-deep-teal {
  background-color: transparent;
  border-color: transparent;
  box-shadow: none;
  padding: 0;
}

.landing-page #security .text-mist\/70 {
  color: var(--ds-tone-body);
}

/* Chips sit a step lighter than the field rather than as a translucent white. */
.landing-page #security .rounded.border.border-mist\/20.bg-mist\/10 {
  background-color: color-mix(in srgb, var(--ds-canvas) 10%, var(--ds-band-dark));
  border-color: color-mix(in srgb, var(--ds-canvas) 20%, var(--ds-band-dark));
  color: var(--ds-canvas);
}

/* Small inline code/label chips. */
.landing-page .rounded.bg-surface.px-1\.5 {
  border-radius: var(--ds-radius-xs);
}

/* --------------------------------------------------------------------------
   7. Inputs
   -------------------------------------------------------------------------- */
.landing-page-product-input-shell {
  border-radius: var(--ds-radius-sm);
  border-color: var(--ds-input-border);
  background: var(--ds-input-bg);
  padding: var(--ds-space-md);
  box-shadow: var(--ds-elevation-1);
}

.landing-page-product-input-shell:focus-within {
  border-color: var(--ds-primary);
  box-shadow: var(--ds-elevation-1), 0 0 0 3px color-mix(in srgb, var(--ds-primary) 14%, transparent);
}

.landing-page-product-input-textarea,
.landing-page-product-input-animation {
  font-size: 15px;
  font-weight: var(--ds-weight-display);
  line-height: 1.4;
  color: var(--ds-input-fg);
}

.landing-page-product-input-textarea::placeholder {
  color: var(--ds-input-placeholder);
}

.landing-page-product-input-caret {
  background: var(--ds-primary);
}

/* --------------------------------------------------------------------------
   8. Navigation & footer
   -------------------------------------------------------------------------- */

/* nav-bar-on-mesh — canvas bar floating over the mesh, 16px/24px padding. */
[data-site-header] > div {
  padding-block: var(--ds-space-lg);
}

[data-site-header] a,
[data-site-header] nav a {
  font-size: 15px;
  font-weight: 400;
  line-height: 1.4;
}

[data-site-header] .text-xl.font-semibold {
  font-size: 20px;
  font-weight: 400;
  letter-spacing: -0.2px;
}

/* Header dropdowns pick up the card treatment. */
[data-site-header] .rounded.border.border-border.bg-surface {
  border-radius: var(--ds-radius-md);
  border-color: var(--color-border);
  background: var(--color-background);
  box-shadow: var(--ds-elevation-2);
}

/* The brand logo is a fixed asset and is left alone; only its wordmark follows
   the type system. */

/* footer-light — canvas ground, ink-mute caption type, 64px/24px padding. */
/* On the dark track the same rule resolves to the navy footer surface. */
footer {
  background: var(--color-footer-background);
  border-top-color: var(--color-border);
  padding: var(--ds-space-huge) var(--ds-space-xl);
  color: var(--color-muted-foreground);
}

footer .text-sm,
footer a.text-sm {
  font-size: 13px;
  line-height: 1.4;
  font-weight: 400;
  letter-spacing: -0.39px;
}

footer a.text-sm:hover {
  color: var(--ds-primary);
}

footer .text-lg.font-bold {
  font-size: 20px;
  font-weight: 400;
  letter-spacing: -0.2px;
}

/* --------------------------------------------------------------------------
   9. Gradient mesh

   "The gradient mesh is non-negotiable on marketing heroes." The existing
   atmosphere element is repurposed: its two blurred CSS-gradient pseudo-elements
   are replaced by the SVG mesh, washed across the upper third and faded into the
   white canvas below. The mesh is inline in the document (`components/HeroMesh.tsx`)
   rather than a background image, so the bands are addressable and can undulate.
   -------------------------------------------------------------------------- */
.landing-page-hero-atmosphere::before,
.landing-page-hero-atmosphere::after {
  /* The mesh is the depth medium; the original blurred blobs and conic ring,
     which these two pseudo-elements painted, would fight it. */
  content: none;
}

.ds-hero-mesh {
  position: absolute;
  overflow: hidden;
  /* Anchored to the right, the way the live hero is — the ribbon bleeds off the
     top-right corner rather than sitting as a band across the top. The box is
     clipped to the right ~56% as well as masked inside the SVG: with the copy
     centred rather than left-aligned, the mask alone is not enough headroom to
     keep the headline on near-canvas. */
  inset: -10% -4% -6% 34%;
  /* The dissolve has to happen in page space, not in the SVG's own box: clipping
     the box narrow enough to clear centred copy crops the art's internal fade off
     and leaves a hard vertical seam. So the box stays wide and this mask does the
     fading, angled to follow the ribbon rather than cutting straight down it. */
  mask-image: linear-gradient(
    100deg,
    transparent 0%,
    rgba(0, 0, 0, 0.18) 20%,
    rgba(0, 0, 0, 0.62) 40%,
    rgba(0, 0, 0, 0.92) 58%,
    #000 72%
  );
  animation: ds-mesh-drift 30s ease-in-out infinite alternate;
}

.ds-hero-mesh-art {
  display: block;
  width: 100%;
  height: 100%;
}

@keyframes ds-mesh-drift {
  0% {
    transform: translate3d(1%, 0, 0) scale(1.02);
  }
  100% {
    transform: translate3d(-1%, -1.5%, 0) scale(1.06);
  }
}

/* The swell. One keyframe set for every band, offset in time band by band, so the
   crest travels down the fan instead of the whole ribbon pumping at once — the
   negative delays below are the phase, not a start-up stagger.

   Translation is in the rotated group's own space, i.e. across the band's width;
   the small counter-rotation is what keeps the far end of a 1000-unit-long band
   from moving in lockstep with the near end, which is what makes it read as a
   wave rather than a slide. `fill-box` puts that rotation on the band's own
   centre — the default `view-box` would swing all seven around the same point.

   Cost: the bands sit inside the two feGaussianBlur groups, so a moving band
   re-runs its blur every frame. On a GPU that is free — measured at 1440x900, the
   hero holds the display's full refresh with the swell running, the same as with
   it disabled. Software rasterisation is the case that pays: ~15fps against ~60
   static. Hence `prefers-reduced-motion` below.

   Amplitude is set against what the eye can actually resolve here, and the first
   pass — 11 units over 13s — was under it. The art scales at ~1.18px per unit, so
   that was 13px of travel spread over 6.5s: ~4px/s, on edges the 17-unit blur
   turns into 60px ramps, underneath a container drift of larger amplitude than
   the swell itself. It animated and nobody could see it. 30 units at 9s is ~8x
   the rate and lands above the threshold.

   The ceiling is the gap between bands, 46-66 units. Neighbours run an eighth of
   a cycle apart, so their relative displacement is 2A·sin(22.5°) ≈ 0.77A — at
   A=30 the gaps breathe by ~23 units, half their width, without any two bands
   crossing. That squeeze-and-spread between neighbours is most of what reads as a
   wave; the scaleY is the same idea inside one band. */
@keyframes ds-mesh-swell {
  0%,
  100% {
    transform: translate(0, 30px) rotate(1.2deg) scaleY(1.08);
  }
  50% {
    transform: translate(0, -30px) rotate(-1.2deg) scaleY(0.94);
  }
}

.ds-mesh-band {
  transform-box: fill-box;
  transform-origin: 50% 50%;
  animation: ds-mesh-swell 9s ease-in-out infinite;
}

/* 9s / 8 ≈ 1.125s between bands: an eighth of a cycle each, so the seven sharp
   bands span three quarters of the wave and the crest is always visible crossing
   the fan. */
.ds-mesh-band-1 { animation-delay: 0s; }
.ds-mesh-band-2 { animation-delay: -1.125s; }
.ds-mesh-band-3 { animation-delay: -2.25s; }
.ds-mesh-band-4 { animation-delay: -3.375s; }
.ds-mesh-band-5 { animation-delay: -4.5s; }
.ds-mesh-band-6 { animation-delay: -5.625s; }
.ds-mesh-band-7 { animation-delay: -6.75s; }

/* The two washes are the fan's atmosphere rather than its structure: heavier blur
   and nothing to collide with, so they swing wider and slower. */
.ds-mesh-wash-1,
.ds-mesh-wash-2 {
  animation-name: ds-mesh-swell-wash;
  animation-duration: 15s;
}

.ds-mesh-wash-1 { animation-delay: -1.5s; }
.ds-mesh-wash-2 { animation-delay: -8.5s; }

@keyframes ds-mesh-swell-wash {
  0%,
  100% {
    transform: translate(0, 46px) rotate(1deg) scaleY(1.06);
  }
  50% {
    transform: translate(0, -46px) rotate(-1deg) scaleY(0.95);
  }
}

/* The closing band's copy of the fan. The page's last screen was the only body
   band with no graphic at all: a headline and a button on flat white, with the
   closing wash — centred at 118% of the band's height — sitting almost entirely
   below the fold of its own section. The same ribbon that opens the page closes
   it, turned through 180° so it enters from the bottom left instead of the top
   right; #security's line-work fan already bookends the page in a second
   register, and this is the first one.

   Three things differ from the hero, all forced by the band being a fifth of the
   height and the copy being centred in it rather than sitting above it:

     - The rotation is on this box and folded into its own drift keyframes, not
       put on the `<svg>`. An outer `<svg>` takes `transform-box: view-box`, so
       `transform-origin` resolves from the viewport's origin rather than from
       the element's centre the way it would on a div: `rotate(180deg)` there
       swings the art a full box up and to the left instead of turning it in
       place. Same trap `.ds-mesh-band` sidesteps with `transform-box: fill-box`.
       The declared `transform` below is the reduced-motion state — with the
       animation off it is the only thing holding the fan upside down.
     - The mask is written in the box's own coordinates, which the rotation has
       already turned over: `mask-image` is painted in the element's local space
       and rides its transform, so `to left` here dissolves the ribbon towards
       the *right* of the screen and the second layer's `to bottom` fades its
       top. Same reason the security fan had to move its mask onto a second
       pseudo-element once the fan started rotating. Read the two layers upside
       down and they are the hero's: hold the leading edge, dissolve along the
       ribbon, and ease both ends so no section boundary cuts the art — the top
       one is Models' hairline, the bottom one the footer's.
     - Two layers, `intersect`. The default `add` would union them, and a union
       of two fades is the weaker of the two rather than the stronger. Where
       `mask-composite` is unsupported the layers union and the result is the
       horizontal fade alone: a heavier ribbon, not a broken mask. */
.ds-closing-mesh {
  position: absolute;
  overflow: hidden;
  /* Bled past three edges. What lands inside the band is the fan's leading
     third; the rest of the sweep resolves off-canvas, the way the hero's does
     past the top-right corner. */
  inset: -6% 30% -10% -6%;
  opacity: 0.9;
  /* The fade finishes at 48% and not at the 66% the composition looked best at
     on a 1440: `slice` sizes the art from the box's aspect ratio, so a narrower
     window fits more of the fan into the same box and the ribbon gains chroma as
     the viewport loses width. Tuned at 1024, where it was crowding the headline;
     at 1440 the difference is a slightly shorter tail. */
  mask-image:
    linear-gradient(
      to left,
      #000 0%,
      rgba(0, 0, 0, 0.5) 18%,
      rgba(0, 0, 0, 0.14) 34%,
      transparent 48%
    ),
    linear-gradient(
      to bottom,
      rgba(0, 0, 0, 0.32) 0%,
      #000 26%,
      #000 58%,
      rgba(0, 0, 0, 0.28) 86%,
      rgba(0, 0, 0, 0.08) 100%
    );
  -webkit-mask-composite: source-in;
  mask-composite: intersect;
  transform: rotate(180deg) scale(1.04);
  animation: ds-closing-mesh-drift 30s ease-in-out infinite alternate;
}

/* The hero's drift with the flip carried through every keyframe: an
   animation-set transform replaces the declared one outright, so leaving the
   rotation out here would stand the fan back up on the first frame. */
@keyframes ds-closing-mesh-drift {
  0% {
    transform: rotate(180deg) translate3d(1%, 0, 0) scale(1.02);
  }
  100% {
    transform: rotate(180deg) translate3d(-1%, -1.5%, 0) scale(1.06);
  }
}

/* Below the lg breakpoint the band is half as wide and the copy runs edge to
   edge, so the ribbon's dissolve no longer clears it: it gives up a step of
   weight and dissolves in half the distance — present as colour, not as
   structure.

   The right edge is bled instead of clipped, and the horizontal fade retuned to
   finish inside the narrower box. Cropping it at 54% instead left a two-pixel
   warm hairline down the band where the box cut a blurred band that the mask had
   not finished dissolving. Invisible at desktop widths only because the fade
   there has proportionally further to run before it meets the same edge. */
@media (max-width: 1023px) {
  .ds-closing-mesh {
    inset: -6% -10% -10% -16%;
    opacity: 0.85;
    mask-image:
      linear-gradient(
        to left,
        #000 0%,
        rgba(0, 0, 0, 0.5) 18%,
        rgba(0, 0, 0, 0.16) 34%,
        transparent 50%
      ),
      linear-gradient(
        to bottom,
        rgba(0, 0, 0, 0.32) 0%,
        #000 26%,
        #000 58%,
        rgba(0, 0, 0, 0.28) 86%,
        rgba(0, 0, 0, 0.08) 100%
      );
  }
}

@media (prefers-reduced-motion: reduce) {
  .ds-hero-mesh,
  .ds-closing-mesh,
  .ds-mesh-band {
    animation: none;
  }
}

/* --------------------------------------------------------------------------
   10. Tabular figures

   "Any cell rendering currency, transaction amounts, or numeric counts uses
   tnum plus a tightening tracking." On this page the numerics live inside the
   composited product-UI mockups and the stat chips.
   -------------------------------------------------------------------------- */
.ds-tabular,
.landing-page .rounded.bg-graphite,
.landing-page .rounded.border.border-border.bg-graphite,
.landing-page .rounded.border.border-deep-teal.bg-deep-teal,
.landing-page .font-mono,
.landing-page time,
.landing-page .landing-page-how-it-works-frame {
  font-feature-settings: 'tnum';
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.42px;
}

.landing-page .rounded.bg-graphite,
.landing-page .rounded.border.border-border.bg-graphite,
.landing-page .rounded.border.border-deep-teal.bg-deep-teal {
  font-size: 14px;
  line-height: 1.4;
  font-weight: var(--ds-weight-display);
}

/* Headings inside those panels stay on the display tier, not the tabular one. */
.landing-page .rounded.bg-graphite .font-display,
.landing-page .rounded.border.border-deep-teal.bg-deep-teal .font-display {
  font-feature-settings: 'ss01';
  font-variant-numeric: normal;
}

/* --------------------------------------------------------------------------
   11. Responsive

   Display tiers stair-step through the breakpoints (§ Responsive Behavior); the
   clamp()s in § 4 cover the continuum. What remains is the touch-target floor
   and the mobile mesh re-tile.
   -------------------------------------------------------------------------- */
@media (max-width: 767px) {
  /* "Gradient mesh re-tiles on mobile to preserve the wash without
     disappearing." At 390px the copy spans the full width, so the ribbon is
     pushed to the top-right corner and eased off rather than crossing the
     headline — the wash survives, the text stays on canvas. */
  .ds-hero-mesh {
    inset: -6% -10% auto 30%;
    height: 52%;
    opacity: 0.8;
    mask-image: linear-gradient(
      to right,
      transparent 0%,
      rgba(0, 0, 0, 0.35) 30%,
      #000 62%
    );
  }

  /* The box crops the art rather than fitting it: scaled past its own frame and
     anchored top-right, what lands in the corner is the ribbon's tail, which is
     the faint end. Fit it instead and the saturated middle of the fan sits on the
     headline. (This is the `background-size: auto 128%` the background version
     used, restated for an element that scales by `slice`.) */
  .ds-hero-mesh-art {
    transform: scale(1.28);
    transform-origin: right top;
  }

  /* Pill buttons size up to 44x44 to hold WCAG AAA. */
  .landing-page a.rounded.border.border-fern.bg-fern,
  [data-site-header] a.rounded.border.border-fern.bg-fern,
  .landing-page a.rounded.border.border-border.px-5.py-3,
  .landing-page a.rounded.border.border-border.px-4.py-2,
  .landing-page a.rounded.border.border-reef.bg-reef,
  .landing-page-product-input-submit {
    min-height: 44px;
    padding-block: var(--ds-space-md);
  }

  /* Form fields stay at 40px minimum height. */
  .landing-page-product-input-textarea {
    min-height: 40px;
  }
}

/* --------------------------------------------------------------------------
   12. Body parity with the live site

   The hero landed, the body did not. Reading the two side by side, the gap is
   not colour or type — those already match. It is four structural habits the
   live site has and this page did not:

     a. Cards carry no border. Measured on the live site: `border-radius: 6px`,
        `border: none`, and a large soft lift — `rgba(23,23,23,.08) 0 15px 35px`
        plus `rgba(23,23,23,.06) 0 3px 6px`. Hairline-bordered cards read
        utilitarian; borderless cards floating on a tint read premium. This page
        had a 1px hairline on every card and a 1px-blur shadow.
     b. Sections are not flat. Each carries a very soft radial wash rather than a
        single flat fill, so the page never goes inert between bands.
     c. Numbers are display-sized. Stripe promotes stats to 56px; this page had
        them at body scale.

   What this section cannot fix is (d): stripe.com's body is overwhelmingly
   product-UI composites — a rendered dashboard, table or checkout fills most of
   every card, with the copy demoted to a corner. That is a content and asset
   problem, not a styling one. See DESIGN-SYSTEM.md.
   -------------------------------------------------------------------------- */

:root,
:host {
  /* Measured off the live site, replacing the spec's much tighter level-1/2. */
  --ds-elevation-card: rgba(23, 23, 23, 0.06) 0 15px 35px, rgba(23, 23, 23, 0.05) 0 3px 6px;
  --ds-elevation-card-hover: rgba(23, 23, 23, 0.1) 0 22px 48px, rgba(23, 23, 23, 0.06) 0 4px 8px;
  --ds-radius-card: 6px;
}

/* --- a. Cards: borderless, 6px, big soft lift ----------------------------
   The border is kept at 1px transparent rather than removed so nothing reflows,
   and so the interactive card's `hover:border-fern/40` still has a border to
   colour. */
.landing-page .rounded.border.border-border.bg-background.p-5,
.landing-page .rounded.border.border-border.bg-background.p-6,
.landing-page .rounded.border.border-border.bg-surface.p-5,
.landing-page .overflow-hidden.rounded.border.border-border.bg-background,
.landing-page .group.relative.grid.overflow-hidden.rounded.border.border-border {
  border-radius: var(--ds-radius-card);
  border-color: transparent;
  box-shadow: var(--ds-elevation-card);
  background-color: var(--color-background);
}

.landing-page .group.relative.grid.overflow-hidden.rounded.border.border-border:hover {
  box-shadow: var(--ds-elevation-card-hover);
}

/* Cards that sit on the white bands take the cool tint instead, so a card is
   never white-on-white. */
.landing-page section:not(.bg-surface) > * .rounded.border.border-border.bg-background.p-5,
.landing-page section:not(.bg-surface) > * .rounded.border.border-border.bg-background.p-6 {
  background-color: var(--ds-canvas-soft);
}

/* Inner panels — chart wells, stat tiles — stay hairlined but drop to the card
   radius so nothing inside is rounder than its container. */
.landing-page .rounded.border.border-border.bg-surface.p-3,
.landing-page .rounded.border.border-border.bg-surface.p-4,
.landing-page .size-9.rounded.border,
.landing-page .size-10.rounded.border,
.landing-page .size-14.rounded.border,
.landing-page .size-20.rounded.border {
  border-radius: var(--ds-radius-card);
  box-shadow: none;
}

/* The dark product mockups keep their lift but match the new geometry. */
.landing-page .rounded.border.border-deep-teal.bg-deep-teal,
.landing-page .rounded.bg-graphite,
.landing-page .rounded.border.border-border.bg-graphite,
.landing-page-how-it-works-frame {
  border-radius: var(--ds-radius-card);
  border-color: transparent;
  box-shadow: var(--ds-elevation-card);
}

/* --- b. Section atmosphere ------------------------------------------------
   A very soft indigo wash off the top edge of each band, so the page never goes
   inert between sections. Excluded: the hero, which already carries the ribbon,
   and the cream interlude, where a cool wash would muddy the warm fill. */
.landing-page > section:not(#top):not(#security) {
  background-image: radial-gradient(
    120% 58% at 50% 0%,
    color-mix(in srgb, var(--ds-primary) 4.5%, transparent),
    transparent 68%
  );
}

/* --------------------------------------------------------------------------
   13. Gradient planes inside product cards

   The hero ribbon is not the only place stripe.com puts saturated colour — it
   is just the biggest. Through the body, the pattern repeats at card scale: the
   card's own background *is* a vivid gradient plane, and the product UI floats
   on top of it as white panels with their own lift. The billing card is violet
   through pink into orange with two white panels over it; the payments card
   bleeds orange from one corner and violet from the other.

   That is a different device from § 12's section wash. The wash is atmosphere
   you are not meant to notice; these are graphic colour events you are.

   Reproduced here on the two cards whose structure already matches — an outer
   container with self-contained panels inside it. The plane goes on the
   container, the panels go white and lift off it.
   -------------------------------------------------------------------------- */

:root,
:host {
  /* Violet -> pink -> orange, weighted to the lower-left. Stops are the
     documented gradient palette plus the corrected sherbet. */
  --ds-plane-violet:
    var(--ds-grain),
    radial-gradient(88% 62% at 88% 6%, color-mix(in srgb, var(--ds-sherbet) 82%, transparent), var(--ds-sherbet-0) 58%),
    radial-gradient(78% 70% at 12% 96%, var(--ds-primary), color-mix(in srgb, var(--ds-primary-deep) 70%, transparent) 46%, var(--ds-primary-deep-0) 78%),
    radial-gradient(86% 78% at 58% 64%, color-mix(in srgb, var(--ds-magenta) 78%, transparent), var(--ds-magenta-0) 62%),
    linear-gradient(158deg, #ffffff 0%, var(--ds-canvas-soft) 22%, var(--ds-primary-subdued) 58%, color-mix(in srgb, var(--ds-primary) 68%, #ffffff) 100%);

  /* Warmer sibling, bleeding sherbet from the top-right and ruby from below. */
  --ds-plane-ember:
    var(--ds-grain),
    radial-gradient(70% 58% at 92% 4%, color-mix(in srgb, var(--ds-sherbet) 90%, transparent), var(--ds-sherbet-0) 62%),
    radial-gradient(84% 72% at 8% 92%, color-mix(in srgb, var(--ds-ruby) 62%, transparent), var(--ds-ruby-0) 66%),
    radial-gradient(90% 80% at 52% 58%, color-mix(in srgb, var(--ds-primary-subdued) 85%, transparent), var(--ds-primary-subdued-0) 70%),
    linear-gradient(150deg, #ffffff 0%, var(--ds-canvas-soft) 26%, color-mix(in srgb, var(--ds-magenta) 34%, #ffffff) 66%, color-mix(in srgb, var(--ds-primary) 52%, #ffffff) 100%);

  /* Panels floating on a plane need a crisper lift than cards on canvas. */
  --ds-elevation-panel: rgba(23, 23, 23, 0.1) 0 8px 20px, rgba(23, 23, 23, 0.06) 0 2px 4px;

  /* Zero-alpha companions to the palette. A gradient stop of bare `transparent`
     is rgba(0,0,0,0), so the ramp interpolates toward transparent *black* and
     leaves a muddy, hard-edged fringe where it lands. Fading to the same hue at
     alpha 0 keeps the ramp in-hue and the edge invisible. These must track the
     hexes in § 2 by hand — there is no CSS way to derive them. */
  --ds-primary-0: rgba(83, 58, 253, 0);
  --ds-primary-deep-0: rgba(68, 52, 212, 0);
  --ds-primary-subdued-0: rgba(185, 185, 249, 0);
  --ds-magenta-0: rgba(249, 107, 238, 0);
  --ds-ruby-0: rgba(234, 34, 97, 0);
  --ds-sherbet-0: rgba(255, 138, 43, 0);
  --ds-cream-0: rgba(245, 233, 212, 0);

  /* Fine grain, as an SVG turbulence data URI. Eight-bit sRGB cannot hold a
     gradient this wide without stepping; a few percent of noise over the top
     dithers the steps away. Cheaper and sharper than blurring the ramp. */
  --ds-grain: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='g'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23g)' opacity='0.6'/%3E%3C/svg%3E");
}

/* --- Always-on: the automations panel over the ember plane ----------------
   Unlike Apps this card has no padding of its own — its children run edge to
   edge — so the plane needs the inset added and the children rounded off it. */
.landing-page #always .overflow-hidden.rounded.border.border-border.bg-background {
  background-color: transparent;
  background-image: var(--ds-plane-ember);
  /* Both fixes for the seam the planes showed along their top and left edges.

     `background-origin: border-box` — the cards keep a 1px transparent border
     (§ 12 keeps it so nothing reflows), and by default a background image is
     *positioned* from the padding box while being *painted* to the border box.
     The gradient therefore gets clamped across that 1px ring and reads as a
     hard stripe. Originating it from the border box makes the ramp cover the
     ring properly.

     The grain layer is the top background layer and must tile at its intrinsic
     140px while the gradients below stay full-bleed, so the two get explicit
     per-layer `background-size` / `-repeat` values. */
  background-origin: border-box;
  background-repeat: repeat, no-repeat, no-repeat, no-repeat, no-repeat;
  background-size: 140px 140px, auto, auto, auto, auto;
  /* Blended rather than laid on flat. Composited `normal`, mid-grey noise over
     the near-white end of the ramp reads as dirt; `soft-light` leaves a 50%-grey
     pixel a no-op, so the grain modulates the midtones where the banding
     actually is and leaves the white corner clean. */
  background-blend-mode: soft-light, normal, normal, normal, normal;
  padding: 30px 30px 58px;
  overflow: visible;
}

.landing-page #always .overflow-hidden.rounded.border.border-border.bg-background > * {
  background-color: var(--ds-canvas);
}

.landing-page #always .overflow-hidden.rounded.border.border-border.bg-background > :first-child {
  border-radius: var(--ds-radius-card) var(--ds-radius-card) 0 0;
  box-shadow: var(--ds-elevation-panel);
}

.landing-page #always .overflow-hidden.rounded.border.border-border.bg-background > :last-child {
  border-radius: 0 0 var(--ds-radius-card) var(--ds-radius-card);
  box-shadow: var(--ds-elevation-panel);
}

/* The panel's own last row keeps a flat bottom edge inside the rounded corner. */
.landing-page #always .overflow-hidden.rounded.border.border-border.bg-background > :last-child > :last-child {
  border-bottom: 0;
}

/* --- The night plane ------------------------------------------------------
   Same device as the planes above, inverted. On the live site the vivid planes
   sit behind dark product surfaces as readily as light ones — the card-issuing
   and crypto cards both do it — so a dark panel gets one weighted toward the
   deep end of the palette instead of the pastel end.

   This was built in place for #computer's terminal. That band is #permissions
   now and its device is a light panel, so the four id-keyed rules that painted
   the terminal are gone; what stays is the variable, which is the whole of what
   `.ds-plane--night` in § 15 needs. Nothing on the page uses it at the moment —
   it is kept because it is a device, not a leftover, and § 15 exposes it. */
:root,
:host {
  --ds-plane-night:
    var(--ds-grain),
    radial-gradient(76% 60% at 90% 6%, color-mix(in srgb, var(--ds-ruby) 76%, transparent), var(--ds-ruby-0) 60%),
    radial-gradient(84% 74% at 10% 94%, var(--ds-primary), var(--ds-primary-0) 68%),
    radial-gradient(70% 60% at 62% 52%, color-mix(in srgb, var(--ds-magenta) 52%, transparent), var(--ds-magenta-0) 64%),
    linear-gradient(152deg, var(--ds-brand-dark-900) 0%, color-mix(in srgb, var(--ds-primary-deep) 70%, var(--ds-brand-dark-900)) 46%, color-mix(in srgb, var(--ds-primary) 60%, var(--ds-brand-dark-900)) 100%);
}

/* --- Connections: a colour band under the logo marquee --------------------
   The live site never lets a bare logo strip sit on flat canvas; there is
   always a graphic behind it. This is the cheapest honest version of that —
   a wide, low wash the icons ride on. */
.landing-page #connections .landing-page-marquee-window {
  position: relative;
  padding-block: var(--ds-space-xl);
}

.landing-page #connections .landing-page-marquee-window::before {
  content: '';
  position: absolute;
  inset: 0 -8%;
  z-index: -1;
  border-radius: var(--ds-radius-card);
  /* Same in-hue fades as the planes — at these alphas a bare `transparent` stop
     leaves a visible grey fringe where each blob lands. */
  background-image:
    radial-gradient(46% 120% at 8% 50%, color-mix(in srgb, var(--ds-sherbet) 26%, transparent), var(--ds-sherbet-0) 70%),
    radial-gradient(42% 120% at 40% 50%, color-mix(in srgb, var(--ds-magenta) 24%, transparent), var(--ds-magenta-0) 72%),
    radial-gradient(52% 130% at 78% 50%, color-mix(in srgb, var(--ds-primary) 30%, transparent), var(--ds-primary-0) 72%);
  mask-image: linear-gradient(to right, transparent, #000 14%, #000 86%, transparent);
}

/* --- Pricing: the closing band gets the strongest wash on the page --------
   The last thing before the footer is the one place the live site lets colour
   come back up after a run of white. */
.landing-page > section#pricing {
  background-image: radial-gradient(
    70% 130% at 50% 118%,
    color-mix(in srgb, var(--ds-primary) 20%, transparent),
    color-mix(in srgb, var(--ds-magenta) 8%, transparent) 44%,
    var(--ds-magenta-0) 72%
  );
  background-repeat: no-repeat;
  background-position: center bottom;
}

/* --- Panels on a plane are a fixed light surface -------------------------
   The planes are graphics, not themed surfaces, so the panels floating on them
   stay white on both tracks — same reasoning as the cream interlude. Their type
   therefore has to be pinned too: left inheriting `--foreground` it flips to
   white on the dark track and the panels render blank. */
.landing-page #always .overflow-hidden.rounded.border.border-border.bg-background > * {
  color: var(--ds-ink);
}

.landing-page #always .overflow-hidden.rounded.border.border-border.bg-background .text-muted-foreground,
.landing-page #always .overflow-hidden.rounded.border.border-border.bg-background .text-mist\/60 {
  color: var(--ds-ink-mute);
}

/* Accents inside the panels resolve to the deep indigo rather than to the pale
   dark-track accent, which would fall below contrast on white. */
.landing-page #always .overflow-hidden.rounded.border.border-border.bg-background .text-fern-60,
.landing-page #always .overflow-hidden.rounded.border.border-border.bg-background .dark\:text-reef {
  color: var(--ds-primary-deep);
}

/* Hairlines dividing rows inside the panels likewise stay light. */
.landing-page #always .overflow-hidden.rounded.border.border-border.bg-background .border-b,
.landing-page #always .overflow-hidden.rounded.border.border-border.bg-background .border-border {
  border-color: var(--ds-hairline);
}

.landing-page #always .overflow-hidden.rounded.border.border-border.bg-background .bg-surface {
  background-color: var(--ds-canvas-soft);
}

/* --- Planes at narrow widths ----------------------------------------------
   A plane's inset costs width a 390px viewport cannot spare.

   This block used to carry #apps and #computer's terminal as well, plus a
   companion rule dropping display-scale figures to 20px so a 3-up stat grid
   would not collide at 390px. Both bands are gone, and their replacements
   (#permissions, #context) build their planes from `.ds-plane` in § 15, which
   carries its own narrow-width inset. What is left is the one ported card that
   still needs the override. */
@media (max-width: 640px) {
  .landing-page #always .overflow-hidden.rounded.border.border-border.bg-background {
    padding: 14px 14px 34px;
  }
}

/* ==========================================================================
   14. Accent range

   The page reads flat next to stripe.com for a reason that is neither layout
   nor colour saturation: it uses *one* ink and *one* accent for everything.
   Every eyebrow, icon, dot, bar and link is the same indigo, and all secondary
   type is the same muted grey.

   Sampled off the live site, the real structure is:

     Ink ramp — six steps, not two. Most-used by a wide margin is #50617a
     (169 occurrences), a mid slate-blue this page does not have at all.
     Then #061b31 ink, #273951, #3c4f69, #64748d, #7d8ba4. The multi-tone
     headline — dark for the first clause, slate for the rest — comes out of
     this ramp, and an <em> in the hero headline is #2d2564, a violet-ink.

     Accents — not one. #533afd primary, #6b59fe lighter indigo, #7f7dfc
     lavender (31 uses, chart fills), #ff6118 vivid orange (10 uses), plus pale
     fills #e2e4ff / #e8e9ff / #e3ecf7.

   So: widen the ink ramp, and give each section its own accent drawn from the
   gradient palette instead of repeating the primary nine times. Filled CTAs
   stay indigo throughout — the spec's "one filled button per band, always
   primary" rule is about buttons, and it still holds.
   ========================================================================== */

:root,
:host {
  /* Ink ramp, measured. `ink` and `ink-mute` were already in DESIGN.md; the
     three between them were missing, which is why everything secondary on this
     page collapsed onto a single grey. */
  --ds-ink-2: #1a2c44;
  --ds-ink-3: #273951;
  --ds-ink-body: #50617a;
  --ds-ink-4: #3c4f69;
  --ds-ink-caption: #7d8ba4;
  --ds-ink-violet: #2d2564;

  /* Accent set. Text-safe variants are deepened against ink where the raw
     gradient stop is too light to carry small type on white. */
  --ds-accent-indigo: var(--ds-primary);
  --ds-accent-violet: var(--ds-primary-deep);
  --ds-accent-lavender: #7f7dfc;
  --ds-accent-orange: #ff6118;
  --ds-accent-orange-text: color-mix(in srgb, #ff6118 74%, var(--ds-ink));
  --ds-accent-ruby: var(--ds-ruby);
  --ds-accent-magenta-text: color-mix(in srgb, var(--ds-magenta) 58%, var(--ds-ink));

  /* Pale fills for chips and soft buttons. */
  --ds-fill-indigo: #e8e9ff;
  --ds-fill-blue: #e3ecf7;

  /* Default, overridden per section below. */
  --ds-section-accent: var(--ds-accent-indigo);

  /* Track-aware aliases. Light track: steps up from ink. */
  --ds-tone-strong: var(--ds-ink-2);
  --ds-tone-body: var(--ds-ink-body);
  --ds-tone-caption: var(--ds-ink-caption);
  --ds-tone-warm: var(--ds-accent-orange-text);
  --ds-tone-magenta: var(--ds-accent-magenta-text);
}

/* --- Ink ramp applied ----------------------------------------------------
   Through the track-aware `--ds-tone-*` aliases, not the raw ink values: the
   ramp is a set of *relationships* (strong / body / caption), and on the dark
   shell those have to resolve to steps down from canvas rather than up from
   ink. Pinning the raw values here paints navy on navy.

   Lead paragraphs move off the single muted grey onto the mid slate that
   carries most of the live site's secondary type; captions drop a further
   step. Three distinct weights of secondary text where there was one. */
.landing-page p.text-lg.text-muted-foreground,
.landing-page p.text-xl.text-muted-foreground {
  color: var(--ds-tone-body);
}

.landing-page .text-sm.text-muted-foreground,
.landing-page .text-xs.text-muted-foreground {
  color: var(--ds-tone-caption);
}

.landing-page .text-sm.font-semibold,
.landing-page .text-base.font-semibold {
  color: var(--ds-tone-strong);
}

/* --- The hero headline goes two-tone -------------------------------------
   On the live site the headline is not one colour with one accent span; it
   steps down the ink ramp across the sentence and picks up violet-ink on the
   emphasised clause. */
.landing-page h1.font-display {
  color: var(--ds-tone-strong);
}

.landing-page h1.font-display .text-fern,
.landing-page h1.font-display .text-reef {
  color: var(--ds-primary);
}

/* --- Per-section accents --------------------------------------------------
   Each band takes one stop from the gradient palette rather than repeating the
   primary. This is the thing that stops the page reading as a single-hue
   template. */
.landing-page #how-it-works { --ds-section-accent: var(--ds-accent-indigo); }
.landing-page #customers    { --ds-section-accent: var(--ds-tone-warm); }
.landing-page #connections  { --ds-section-accent: var(--ds-accent-violet); }
.landing-page #always       { --ds-section-accent: var(--ds-tone-magenta); }
/* #computer is #permissions now, and it carries `.ds-accent--indigo` in its markup
   rather than getting the accent from an id-keyed rule here — § 15 is the shape new
   work should use. */
.landing-page #models       { --ds-section-accent: var(--ds-tone-warm); }

/* Eyebrows and the small icons beside them carry the section accent. */
.landing-page > section .text-xs.uppercase.text-fern-60,
.landing-page > section p.text-xs.uppercase,
.landing-page > section .text-xs.uppercase .dark\:text-reef {
  color: var(--ds-section-accent);
}

.landing-page > section > * .size-4.text-fern-60,
.landing-page > section > * .size-5.text-fern-60 {
  color: var(--ds-section-accent);
}

/* Status dots follow too — they are the smallest carrier of the accent and the
   easiest place for a single hue to give the whole page away. */
.landing-page > section .landing-page-status-pulse {
  background-color: var(--ds-section-accent);
}

/* The hero release badge takes the warm accent, so the first colour the eye
   meets after the ribbon is not another indigo. */
.landing-page #top .text-xs.uppercase.text-fern-60,
.landing-page #top a[data-latest-release-note] .text-fern-60 {
  color: var(--ds-tone-warm);
}

.landing-page #top a[data-latest-release-note] svg {
  color: var(--ds-tone-warm);
}

/* Soft chips take the pale indigo fill rather than a tint of the primary. */
.landing-page .rounded.border.border-fern\/30.bg-fern\/10 {
  background-color: var(--ds-fill-indigo);
  border-color: transparent;
  color: var(--ds-primary-deep);
}

/* --- Panels on a plane re-declare the tone ramp ---------------------------
   § 13 pinned the panels' `color` directly, which worked until § 14 introduced
   tone-ramp rules at higher specificity and the type went white-on-white on the
   dark track again.

   Chasing each of those rules with a more specific override is the wrong shape.
   The panels are a fixed light surface, so the right fix is to re-declare the
   ramp *on the panel itself* — every tone rule inside then resolves to its
   light-track value with no per-rule overrides at all. Same for the section
   accent, which needs its deep variant to carry on white.

   Now that the page is pinned light this is a no-op for the tone values; it is
   kept because the section accent override is still live, and because it is the
   thing that would have to exist again the moment a dark track returns. */
.landing-page #always .overflow-hidden.rounded.border.border-border.bg-background > * {
  --ds-tone-strong: var(--ds-ink-2);
  --ds-tone-body: var(--ds-ink-body);
  --ds-tone-caption: var(--ds-ink-caption);
  --ds-section-accent: var(--ds-primary-deep);
  --color-foreground: var(--ds-ink);
  --color-muted-foreground: var(--ds-ink-mute);
}

/* #computer's terminal was the inverse case — a fixed *dark* surface holding the
   dark-track ramp whatever the page did — and had its own block here. #permissions
   replaced it with a light panel, and `.ds-panel--dark` in § 15 is where that
   inversion lives now for anything that needs it. */

/* ==========================================================================
   15. Reusable device hooks

   § 12–14 build the page's visual devices — the gradient planes, the panels
   floating on them, the dark band, the per-section accent — but they build them
   *in place*. Every rule is keyed to a section id (`#always`, `#connections`)
   and to the literal Tailwind class string that section's markup
   happens to carry (`.rounded.border.border-border.bg-background.p-5`).

   That is the right shape for a port whose markup was fixed and finite. It is
   the wrong shape for new work: a section with a new id matches none of those
   selectors, so anything written after the port renders as hairlined cards on
   flat canvas — the devices are invisible to it. Which is exactly what a new
   section looks like today.

   This section re-exposes the same devices as classes. Nothing above changes:
   the ported sections keep their id-keyed rules, and these hooks are additive.

   Deliberately *not* scoped under `.landing-page`, so pages under app/learn/
   can use them too — unlike § 12–14, which they currently miss entirely.

   Two rules for using them:
     - Put a hook on a plain element. Do not also give it the card class string
       (`rounded border border-border bg-background p-5`) — § 12 matches that at
       higher specificity and will repaint the background out from under it.
     - Hooks compose with the Tailwind utilities for layout and type; they only
       own the things the compiled sheet has no vocabulary for.

   See SECTION-PLAYBOOK.md for which device to reach for and the markup that
   goes with each.
   ========================================================================== */

/* --- Per-section accent ---------------------------------------------------
   Goes on the <section>. Replaces the per-id block in § 14 for new sections.
   Pick a different one from the band above and below: a page where every
   eyebrow is the same indigo is the single clearest tell of a template. */
.ds-accent--indigo   { --ds-section-accent: var(--ds-accent-indigo); }
.ds-accent--violet   { --ds-section-accent: var(--ds-accent-violet); }
.ds-accent--lavender { --ds-section-accent: var(--ds-accent-lavender); }
.ds-accent--orange   { --ds-section-accent: var(--ds-tone-warm); }
.ds-accent--ruby     { --ds-section-accent: var(--ds-accent-ruby); }
.ds-accent--magenta  { --ds-section-accent: var(--ds-tone-magenta); }

/* The eyebrow, and any icon sitting beside it, carry the accent. § 14 reaches
   these through the ported class strings; new markup names them instead, which
   also works outside `.landing-page`. */
.ds-eyebrow {
  color: var(--ds-section-accent);
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.16em;
  text-transform: uppercase;
}

.ds-eyebrow svg,
.ds-accent-mark {
  color: var(--ds-section-accent);
}

/* --- The section wash -----------------------------------------------------
   § 12b gives every ported band a soft radial wash off its top edge so the page
   never goes inert. It is keyed to `.landing-page > section`, so a section
   nested any deeper — or on another page — misses it. */
.ds-wash {
  background-image: radial-gradient(
    120% 58% at 50% 0%,
    color-mix(in srgb, var(--ds-primary) 4.5%, transparent),
    transparent 68%
  );
}

/* The closing-band wash: colour coming back up before the footer. Strongest on
   the page, and the only one that reads from the bottom. */
.ds-wash--closing {
  background-image: radial-gradient(
    70% 130% at 50% 118%,
    color-mix(in srgb, var(--ds-primary) 20%, transparent),
    color-mix(in srgb, var(--ds-magenta) 8%, transparent) 44%,
    var(--ds-magenta-0) 72%
  );
  background-repeat: no-repeat;
  background-position: center bottom;
}

/* --- Gradient planes ------------------------------------------------------
   The card's own background *is* a vivid gradient plane, and the product UI
   floats on top of it as panels with their own lift. § 13 has the full argument
   for the three incantations below; they are the difference between a plane
   that renders clean and one that shows a 1px seam and visible banding:

     background-origin: border-box  — the ported cards keep a 1px transparent
       border, and a background image is positioned from the padding box while
       painted to the border box; the ramp gets clamped across that ring and
       reads as a stripe.
     per-layer size/repeat          — the grain layer tiles at its intrinsic
       140px while the gradients below stay full-bleed.
     background-blend-mode          — composited `normal`, mid-grey noise over
       the near-white end reads as dirt; `soft-light` makes a 50%-grey pixel a
       no-op, so the grain dithers the midtones and leaves white clean.

   The inset is generous and deliberately uneven. A uniform thin padding turns
   the plane into a frame around the panels; the device only works when the
   colour reads as a field they sit *on*. */
.ds-plane {
  background-color: transparent;
  background-origin: border-box;
  background-repeat: repeat, no-repeat, no-repeat, no-repeat, no-repeat;
  background-size: 140px 140px, auto, auto, auto, auto;
  background-blend-mode: soft-light, normal, normal, normal, normal;
  border: 1px solid transparent;
  border-radius: var(--ds-radius-card);
  box-shadow: var(--ds-elevation-card);
  padding: 30px 30px 58px;
}

.ds-plane--violet { background-image: var(--ds-plane-violet); }
.ds-plane--ember  { background-image: var(--ds-plane-ember); }
.ds-plane--night  { background-image: var(--ds-plane-night); }

/* Tighter inset for a plane behind a dark surface — the night plane carries
   less contrast against its panel, so it needs less room to read. */
.ds-plane--night { padding: 26px 26px 48px; }

/* --- Panels floating on a plane -------------------------------------------
   A fixed light surface regardless of the track, so it re-declares the tone
   ramp on itself rather than letting each rule inside be chased with an
   override. Same reasoning as § 14's panel block. */
.ds-panel {
  background-color: var(--ds-canvas);
  border: 1px solid transparent;
  border-radius: var(--ds-radius-card);
  box-shadow: var(--ds-elevation-panel);
  color: var(--ds-ink);

  --ds-tone-strong: var(--ds-ink-2);
  --ds-tone-body: var(--ds-ink-body);
  --ds-tone-caption: var(--ds-ink-caption);
  --ds-section-accent: var(--ds-primary-deep);
  --color-foreground: var(--ds-ink);
  --color-muted-foreground: var(--ds-ink-mute);
  --background: var(--ds-canvas);
  --surface: var(--ds-canvas-soft);
  --border: var(--ds-hairline);
}

/* The inverse: a fixed dark surface holding the dark end of the ramp, for a
   terminal or any product mockup with dark chrome. */
.ds-panel--dark {
  background-color: var(--ds-ink);
  color: var(--ds-canvas);

  --ds-tone-strong: var(--ds-canvas);
  --ds-tone-body: color-mix(in srgb, var(--ds-canvas) 72%, var(--ds-ink));
  --ds-tone-caption: color-mix(in srgb, var(--ds-canvas) 54%, var(--ds-ink));
  --ds-section-accent: var(--ds-primary-subdued);
  --color-foreground: var(--ds-canvas);
  --color-muted-foreground: color-mix(in srgb, var(--ds-canvas) 66%, var(--ds-ink));
  --background: var(--ds-ink);
  --border: color-mix(in srgb, var(--ds-canvas) 12%, var(--ds-ink));
}

/* A panel whose children run edge to edge — a chrome bar, a list of rows, a
   status strip. Geometry only: it clips the rows to the panel's corners and
   keeps the seams between them flat, and deliberately paints nothing, so it
   composes with `--dark` instead of repainting it back to canvas.

   For the other shape — several panels floating separately on one plane, with
   the colour showing between them — put `.ds-panel` on each child instead. */
.ds-panel-stack {
  overflow: hidden;
}

.ds-panel-stack > * {
  border-radius: 0;
}

/* The last row's own bottom hairline would double up with the panel edge. */
.ds-panel-stack > :last-child {
  border-bottom: 0;
}

/* --- The full-bleed dark band ---------------------------------------------
   The device § 6 found on the live site and the spec never mentions: a whole
   section filled deep navy, holding the dark ramp regardless of the page around
   it. One per page at most — it is the page's punctuation, not a surface. */
.ds-band-dark {
  position: relative;
  isolation: isolate;
  overflow: hidden;
  background-color: var(--ds-band-dark);
  background-image: radial-gradient(
    92% 70% at 50% 0%,
    color-mix(in srgb, var(--ds-primary) 16%, transparent),
    var(--ds-primary-0) 72%
  );
  border-top-color: transparent;
  color: var(--ds-canvas);

  --ds-tone-strong: var(--ds-canvas);
  --ds-tone-body: color-mix(in srgb, var(--ds-canvas) 70%, var(--ds-band-dark));
  --ds-tone-caption: color-mix(in srgb, var(--ds-canvas) 52%, var(--ds-band-dark));
  --ds-section-accent: var(--ds-primary-subdued);
  --color-foreground: var(--ds-canvas);
  --color-muted-foreground: color-mix(in srgb, var(--ds-canvas) 66%, var(--ds-band-dark));
  --border: color-mix(in srgb, var(--ds-canvas) 20%, var(--ds-band-dark));
}

/* Chips on the band sit a step lighter than the field rather than as a
   translucent white, which greys out against the wash. */
.ds-band-dark .ds-chip {
  display: inline-flex;
  align-items: center;
  gap: var(--ds-space-sm);
  border: 1px solid color-mix(in srgb, var(--ds-canvas) 20%, var(--ds-band-dark));
  border-radius: var(--ds-radius-xs);
  background-color: color-mix(in srgb, var(--ds-canvas) 10%, var(--ds-band-dark));
  padding: var(--ds-space-sm) var(--ds-space-lg);
  color: var(--ds-canvas);
  font-size: 14px;
  font-weight: 500;
}

/* The radiating fan, bled past the band's right and vertical edges so it reads
   as a crop of something larger, and masked back on its left so it never
   reaches the copy. Generated by `npm run generate:fan`. */
.ds-band-dark--fan::after {
  content: '';
  position: absolute;
  inset: -12% -6% -12% 42%;
  z-index: -1;
  pointer-events: none;
  background: url('/stripe/security-fan.svg') no-repeat right center;
  background-size: cover;
  opacity: 0.85;
  mask-image: linear-gradient(
    to right,
    transparent 0%,
    rgba(0, 0, 0, 0.3) 26%,
    rgba(0, 0, 0, 0.85) 54%,
    #000 74%
  );
}

/* Below the two-column breakpoint the copy runs full width, so the fan retreats
   to the top-right corner instead of sitting behind the text. */
@media (max-width: 1023px) {
  .ds-band-dark--fan::after {
    inset: -10% -10% auto 34%;
    height: 48%;
    opacity: 0.6;
    mask-image: linear-gradient(to right, transparent 0%, rgba(0, 0, 0, 0.4) 34%, #000 68%);
  }
}

/* --- Figures at display scale ---------------------------------------------
   Stripe promotes stats to display size; at body scale a number reads as label
   text and the tile it sits in reads as a list item. */
.ds-figure {
  font-family: var(--font-display);
  font-size: 32px;
  font-weight: 600;
  line-height: 1.1;
  letter-spacing: -0.64px;
  font-variant-numeric: tabular-nums;
}

/* --- Chart bars -----------------------------------------------------------
   The lavender the live site uses for exactly this, with the peak bar in full
   primary. Heights are inline styles: the compiled sheet has no `h-[…]` rule
   for a value the landing page never used. */
.ds-bar {
  flex: 1;
  border-radius: var(--ds-radius-xs);
  background-color: color-mix(in srgb, var(--ds-accent-lavender) 72%, #ffffff);
}

.ds-bar--peak {
  background-color: var(--ds-primary);
}

/* --- Narrow widths --------------------------------------------------------
   A plane's inset costs width a 390px viewport cannot spare, and a 32px figure
   in a 3-up grid collides with its neighbours. */
@media (max-width: 640px) {
  .ds-plane,
  .ds-plane--night {
    padding: 14px 14px 34px;
  }

  .ds-figure {
    font-size: 20px;
    letter-spacing: -0.2px;
  }
}

/* --------------------------------------------------------------------------
   16. Connections marquee — pause on hover

   Each tile carries a native `title`, which the browser reveals only after about
   a second of hover, and the strip does not stop for the cursor. A tile is 80px
   wide, so it slides out from under a stationary pointer before its own tooltip
   appears — and because the tooltip follows the pointer rather than the tile, it
   ends up naming a logo that has already moved on. Pausing makes the label
   legible, and lets a visitor stop on the integration they actually care about.

   Scoped to the window rather than the strip: the strip is three lists wide and
   translating, the window is the box the pointer can actually be inside.

   Deliberately not wrapped in a media query. `prefers-reduced-motion: reduce`
   already sets `animation: none` on this element in index.css, and a paused
   non-animation is still no animation.
   -------------------------------------------------------------------------- */

.landing-page-marquee-window:hover .landing-page-marquee {
  animation-play-state: paused;
}

/* --------------------------------------------------------------------------
   17. Connections marquee — tooltip

   Replaces the browser's native `title` on each tile, which arrives about a
   second late, is unstyled, and follows the pointer instead of the tile. The
   name now lives in `data-label` and is drawn here, so it appears immediately
   and belongs to the tile it names.

   It sits INSIDE the tile on purpose. The marquee window clips with
   `overflow: hidden`, so anything escaping above or below the strip is cut off,
   and `overflow-x: hidden` with a visible y-axis is not a thing CSS allows —
   the y-axis computes to auto. Horizontal escape is fine, though: a long name
   like "Microsoft Teams" simply runs over its neighbours, which is exactly what
   a tooltip should do. Hence `white-space: nowrap` and the bottom anchoring.

   The pause rule in section 16 is what makes this readable at all: on a moving
   strip the tile would leave before the eye arrived.
   -------------------------------------------------------------------------- */

.landing-page-marquee li::after {
  content: attr(data-label);
  position: absolute;
  bottom: 0.25rem;
  left: 50%;
  z-index: 1;
  transform: translateX(-50%) translateY(0.25rem);
  white-space: nowrap;
  padding: 0.25rem 0.4rem;
  border-radius: var(--ds-radius-sm, 4px);
  background-color: var(--ds-ink);
  color: var(--ds-canvas);
  font-size: 11px;
  line-height: 1;
  letter-spacing: 0;
  opacity: 0;
  pointer-events: none;
  transition: opacity 120ms ease, transform 120ms ease;
}

.landing-page-marquee li:hover::after {
  opacity: 1;
  transform: translateX(-50%) translateY(0);
}

/* The tooltip of a hovered tile has to clear the tiles that come after it. */
.landing-page-marquee li:hover {
  z-index: 1;
}

@media (prefers-reduced-motion: reduce) {
  .landing-page-marquee li::after {
    transition: none;
  }
}

/* ==========================================================================
   18. The access switch

   #permissions' device is four switches, and the band's whole claim is that
   throwing one is something you do yourself, in a second. A switch drawn at
   rest asserts that; a switch caught in the act shows it. So the three granted
   rows start off and flip on in sequence the moment the panel scrolls in.

   Banco never flips. It is the row that says nobody gave the agent the ability
   to move money, and animating it on — even for the second before it went back
   off — would put "Doroteo puede mover fondos" on screen on a page whose entire
   job is the opposite. The off switch is the argument; it does not perform.

   Geometry lives here rather than in the markup because the knob has to travel,
   and a transform is not something one of the compiled utility classes can
   carry. It also retires the inline `width` / `padding` the switches shipped
   with — `w-9` and `p-0.5` have no rule behind them in the compiled sheet.

   Every rule's *resting* state is the correct final state, so a visitor with
   JavaScript off (no `is-visible`, so no animation) or reduced motion on sees
   the finished panel and misses only the movement.
   ========================================================================== */

.landing-page-access-switch {
  box-sizing: border-box;
  display: inline-flex;
  flex-shrink: 0;
  align-items: center;
  width: 36px;
  height: 20px;
  padding: 2px;
  border: 1px solid var(--ds-hairline);
  border-radius: 999px;
  background-color: var(--ds-canvas-soft);
}

/* The knob is a pseudo-element so the markup is one empty span per switch.
   36 × 20 less 1px of border and 2px of padding on each side leaves an inner
   track of 30 × 14, so the knob is 14px square and travels exactly 16px. Sizing
   it 16px — the obvious guess — makes it a pixel taller than the track it sits
   in, and the switch reads as a bulge rather than as a knob. */
.landing-page-access-switch::after {
  content: '';
  width: 14px;
  height: 14px;
  border-radius: 999px;
  background-color: #ffffff;
  box-shadow: 0 1px 2px rgb(13 23 56 / 0.16);
  transform: translateX(0);
}

.landing-page-access-switch--on {
  border-color: var(--ds-primary);
  background-color: var(--ds-primary);
}

.landing-page-access-switch--on::after {
  transform: translateX(16px);
}

/* `.is-visible` is what landing-page.js adds when a `[data-reveal]` column
   intersects, and `.landing-page-reveal-ready` only exists once that script has
   run — so both halves of the selector are the no-JS guard.

   The delays start at 600ms because the reveal itself is a 600ms fade: flipping
   a switch while its panel is still arriving reads as a rendering artefact
   rather than as something happening. Each row carries its own delay as an
   inline custom property, which the ::after inherits — a pseudo-element cannot
   be reached by an inline style directly. */
.landing-page-reveal-ready [data-reveal].is-visible .landing-page-access-switch--on {
  animation: landing-page-access-switch-on 260ms ease-out both;
  animation-delay: var(--ds-switch-delay, 0ms);
}

.landing-page-reveal-ready [data-reveal].is-visible .landing-page-access-switch--on::after {
  /* A small overshoot on the knob only — enough to read as thrown rather than
     cross-faded, and kept under a pixel past the track so it never pokes out of
     the pill at the peak. */
  animation: landing-page-access-knob-on 260ms cubic-bezier(0.34, 1.15, 0.64, 1) both;
  animation-delay: var(--ds-switch-delay, 0ms);
}

@keyframes landing-page-access-switch-on {
  from {
    border-color: var(--ds-hairline);
    background-color: var(--ds-canvas-soft);
  }
  to {
    border-color: var(--ds-primary);
    background-color: var(--ds-primary);
  }
}

@keyframes landing-page-access-knob-on {
  from { transform: translateX(0); }
  to   { transform: translateX(16px); }
}

@media (prefers-reduced-motion: reduce) {
  .landing-page-reveal-ready [data-reveal].is-visible .landing-page-access-switch--on,
  .landing-page-reveal-ready [data-reveal].is-visible .landing-page-access-switch--on::after {
    animation: none;
  }
}

/* ==========================================================================
   19. The context matrix

   #context's device is a coverage table: one column for what a private chat
   can reach, one for what Doroteo can. The grid lives here rather than as an
   inline style because it needs a narrow-width variant — two 7rem columns leave
   a 390px panel about 150px for the row label, which wraps every one of them to
   four lines.

   An earlier version had four columns headed VS / CF / MB / PV. Initials in a
   table header read as category codes rather than as people, and labelling them
   properly costs more width than the fragmentation they were buying.

   During the same round this number briefly held a two-slide crossfade, for the
   version of the band that showed "sin Doroteo" and "con Doroteo" in turn — grid
   stack, keyframes, dots, and no-JS and reduced-motion fallbacks. A comparison
   you can see beats one you have to remember, so it went when the table arrived.
   It never reached a commit, so it is not recoverable from history: rebuilding
   it means rebuilding it. Worth knowing before assuming otherwise.
   ========================================================================== */

.landing-page-context-matrix {
  display: grid;
  grid-template-columns: 1fr 7rem 7rem;
  align-items: center;
  gap: 8px;
}

@media (max-width: 640px) {
  .landing-page-context-matrix {
    grid-template-columns: 1fr 4.5rem 5rem;
    gap: 6px;
  }
}

/* ==========================================================================
   20. The "How it works" frame as a lit surface

   The frame was doing nothing. `--ds-canvas-soft` on a band that is itself
   near-white meant the demo read as a white rectangle on white, with the
   frame's own edge invisible — so the page's largest device was the one place
   it reached for none of its devices.

   The fix is § 15's plane, applied to a composite that was already the right
   shape for it: an outer container holding a self-contained panel (the agent
   shell) that carries its own lift. Same three incantations § 13 argues for —
   `background-origin: border-box` against the 1px seam, per-layer size/repeat
   so the grain tiles at 140px while the ramps stay full-bleed, and `soft-light`
   so mid-grey noise over the near-white end does not read as dirt.

   Three things are specific to this instance and are the reason it is its own
   number rather than a `.ds-plane` class in the markup:

     · **The inset is horizontally biased.** § 15's plane is 30px on all sides
       of a ~600px card — 5% of its width. Five percent of this frame is 70px,
       and it only has room for that at the sides: the height is pinned by
       `min-height: 51rem` in `_astro/index.BRimgEk0.css`, and past a point every
       pixel added top and bottom grows an already-800px section. So the sides
       take roughly 3.4vw and the vertical stays modest: at 834 and 390 the
       whole composite still fits under `min-height` and the frame does not grow
       at all, and at 1440 the vertical inset resolves to 27 + 39px, which is
       15px more than the 51rem floor. That is the one width where this costs
       height, and 15px on 818 was judged worth the bloom having somewhere to
       land. Measure it *after* the bundle mounts — before that the frame is an
       empty skeleton sitting at exactly `min-height`, which reports no change
       at any width and is how this was briefly mis-measured.

     · **The tab strip is left alone.** It is `background-color` at 90% over a
       `backdrop-filter`, so the plane shows through it as a faint tint rather
       than being cut off by a hard white bar — which is what an opaque strip
       would look like clipped to the frame's top radius. Restyling the strip is
       its own change; this one has to survive it either way.

     · **A corner bloom, not § 15's ramp.** The first attempt reused
       `--ds-plane-violet` directly and it failed for a reason worth recording:
       a plane's ramp runs corner to corner, and when the panel covers all but
       70px of it, the only part still visible is a uniform hard-edged ring —
       which reads as a coloured border, the exact thing § 15 says a plane must
       not become. Proportion is the constraint, and this frame has no room to
       fix it by inset alone. So the colour concentrates in the corners and
       falls off before it reaches the middle of any edge: no continuous ring,
       no hard edge, and the light comes from somewhere rather than framing
       something. The stock ramp's sherbet layer went with it — bled through
       the tab strip's 90% white as a peach smear that read as a rendering
       fault rather than as a decision.

   `.landing-how-it-works-stage` and `.landing-how-it-works-agent-shell` are
   declared only in `_astro/index.BRimgEk0.css`, and the frame only there and in
   §§ 6 and 12 above; this sheet loads after all of them at equal specificity,
   so nothing here needs to patch the demo bundle or the stylesheet the bundle
   injects at runtime. The one rule that still outranks these is
   `:root[data-dev-theme="dark"] .landing-page-how-it-works-frame` in that same
   file — dead on this page, which `app/layout.tsx` pins to light, but it is why
   a dark-mode experiment would find the plane missing.
   ========================================================================== */

:root,
:host {
  /* Three corner blooms over a near-white base: indigo bottom-left, magenta
     bottom-right, lavender top-right. Each is tight (≤44% of the box) and
     centred on or just outside its corner, so no two of them meet along an edge
     — which is what keeps this a bloom and not the ring the first attempt was.
     The top-left corner is deliberately left empty: it is the one the eye
     reaches first, coming off the headline, and it is where the tab strip's
     active tab sits. The base ramp runs white to a quarter-strength lavender
     and does the rest. */
  --ds-how-it-works-bloom:
    var(--ds-grain),
    radial-gradient(44% 46% at 2% 100%, color-mix(in srgb, var(--ds-primary) 66%, transparent), var(--ds-primary-0) 72%),
    radial-gradient(40% 42% at 98% 96%, color-mix(in srgb, var(--ds-magenta) 40%, transparent), var(--ds-magenta-0) 70%),
    radial-gradient(34% 40% at 100% 2%, color-mix(in srgb, var(--ds-primary-subdued) 78%, transparent), var(--ds-primary-subdued-0) 74%),
    linear-gradient(168deg, #ffffff 0%, var(--ds-canvas-soft) 46%, color-mix(in srgb, var(--ds-primary-subdued) 26%, #ffffff) 100%);
}

.landing-page-how-it-works-frame {
  background-color: transparent;
  background-image: var(--ds-how-it-works-bloom);
  background-origin: border-box;
  background-repeat: repeat, no-repeat, no-repeat, no-repeat, no-repeat;
  background-size: 140px 140px, auto, auto, auto, auto;
  background-blend-mode: soft-light, normal, normal, normal, normal;
  border: 1px solid transparent;
}

/* The field the shell sits on. Wide at the sides, bottom-heavier than the top,
   which is the asymmetry § 15's plane uses and the reason it reads as a surface
   rather than as a mount. */
.landing-how-it-works-stage {
  padding: clamp(1rem, 1.9vw, 1.75rem) clamp(1.25rem, 3.4vw, 3.25rem)
    clamp(1.5rem, 2.7vw, 2.5rem);
}

/* The agent shell becomes the panel: no hairline (the plane behind it is the
   separation), card geometry, and the crisper lift panels take over cards. */
.landing-how-it-works-agent-shell {
  border-color: transparent;
  border-radius: var(--ds-radius-card);
  box-shadow: var(--ds-elevation-panel);
}

/* Narrow widths: the frame already goes edge to edge and loses its radius, so
   the plane has no corners to fill and the inset is all that is left of it.
   Matching `_astro`'s own 1rem here rather than letting the clamp resolve to
   something wider, because at 390px every pixel of inset is one the shell
   does not have. */
@media (max-width: 47.99rem) {
  .landing-how-it-works-stage {
    padding: 0.75rem 1rem 1.25rem;
  }
}


/* ==========================================================================
   21. The approval footer in the demo's result panel

   `tools/cobranza-hiw-demo.mjs` hangs this off `.landing-how-it-works-document`,
   which is already a flex column with a `flex:1; overflow:auto` body — so it
   lands as a fixed footer under the scrolling ledger with no layout change.

   It exists because the panel was answering the wrong question. The chapter is
   called "Revisas y apruebas" and the scenario turns on a person authorising a
   write to Defontana; a ledger on its own says what was found, not what you are
   looking at it for. This is the authorisation.

   Nothing here is a real control, and that is deliberate. The shell kills
   pointer events on everything inside it, so a `<button>` would look pressable
   and refuse to be pressed — the trap #permissions' access switch was
   redesigned to avoid. What carries the argument instead is that the visitor
   *watches the approval happen*: the primary action holds both labels and
   crossfades from "Aprobar y conciliar las 9" to "Aprobado · 11:07" partway
   through the chapter, with a ring that expands out of it at the moment of the
   swap — the same `landing-page-how-it-works-click` keyframes the demo's own
   fake cursor uses when it clicks something. A replay that shows a click is
   honest in a way an inert button is not.

   The timing is tied to the chapter, not guessed: `result` runs 8.2s and the
   panel stays mounted into `recurring` until it closes, so the swap sits at
   3.4s — late enough to read the pending state, early enough that the resolved
   state is what most of the chapter shows.
   ========================================================================== */

.landing-how-it-works-approve {
  flex: none;
  border-top: 1px solid var(--ds-hairline);
  background: var(--ds-canvas);
  padding: 14px 18px 16px;
  display: flex;
  flex-direction: column;
  gap: 10px;
}

/* The attachment. Lives here rather than in the markdown because the panel
   renders only headings, paragraphs, strong and lists. */
.landing-how-it-works-approve-file {
  display: flex;
  align-items: center;
  gap: 8px;
  border: 1px solid var(--ds-hairline);
  border-radius: var(--ds-radius-card);
  background: var(--ds-canvas-soft);
  padding: 8px 10px;
  font-size: 12px;
  color: var(--ds-ink-body);
}

.landing-how-it-works-approve-file svg {
  flex: none;
  color: var(--ds-ink-mute);
}

.landing-how-it-works-approve-file small {
  margin-left: auto;
  color: var(--ds-ink-mute);
  font-size: 11px;
}

.landing-how-it-works-approve-bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 10px;
}

.landing-how-it-works-approve-meta {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}

.landing-how-it-works-approve-meta strong {
  font-size: 15px;
  color: var(--ds-ink);
  font-variant-numeric: tabular-nums;
}

.landing-how-it-works-approve-meta span {
  font-size: 11.5px;
  color: var(--ds-ink-mute);
}

.landing-how-it-works-approve-actions {
  display: flex;
  align-items: center;
  gap: 8px;
  flex: none;
}

.landing-how-it-works-approve-secondary {
  border: 1px solid var(--ds-hairline);
  border-radius: var(--ds-radius-button);
  padding: 8px 12px;
  font-size: 12.5px;
  font-weight: 600;
  color: var(--ds-ink-body);
  white-space: nowrap;
}

/* Both labels are stacked in one box so the swap cannot reflow the bar: the
   grid puts them on the same cell and the widest one sets the width. */
.landing-how-it-works-approve-primary {
  position: relative;
  display: grid;
  border-radius: var(--ds-radius-button);
  padding: 8px 14px;
  font-size: 12.5px;
  font-weight: 600;
  white-space: nowrap;
  background: var(--ds-primary);
  color: var(--ds-on-primary);
  box-shadow: var(--ds-elevation-1);
  animation: landing-page-hiw-approve-box 8s linear forwards;
}

.landing-how-it-works-approve-primary > em,
.landing-how-it-works-approve-primary > b {
  grid-area: 1 / 1;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  font-style: normal;
  font-weight: 600;
}

.landing-how-it-works-approve-primary > em {
  animation: landing-page-hiw-approve-out 8s linear forwards;
}

.landing-how-it-works-approve-primary > b {
  opacity: 0;
  animation: landing-page-hiw-approve-in 8s linear forwards;
}

/* The ring. Same keyframes the demo's own cursor uses for a click, so the
   gesture reads as the one the rest of the replay already taught. */
.landing-how-it-works-approve-primary::after {
  content: '';
  position: absolute;
  inset: -6px;
  border-radius: calc(var(--ds-radius-button) + 6px);
  border: 1px solid var(--ds-primary);
  opacity: 0;
  animation: landing-page-hiw-approve-ring 8s linear forwards;
  pointer-events: none;
}

/* Nine cells, one per reconciliation. Empty outlines while the approval is
   pending, which already says "there are nine of these"; filled left to right
   after it, which says the write is happening. `--i` comes in as an inline style on
   each cell and staggers the start by 90ms, so the whole sweep takes about
   1.7s — long enough to read as a sequence, short enough to finish well inside
   the chapter. */
.landing-how-it-works-approve-batch {
  display: flex;
  gap: 3px;
  height: 6px;
}

.landing-how-it-works-approve-batch span {
  flex: 1;
  border-radius: 2px;
  background-color: var(--ds-hairline);
  animation: landing-page-hiw-batch-fill 8s linear forwards;
  animation-delay: calc(var(--i) * 150ms);
}

/* Three states in one line, stacked on a grid cell so the swaps cannot reflow
   the footer: waiting for you, writing, written and logged. The last one is the
   only place the demo says out loud whose name the write is recorded under. */
.landing-how-it-works-approve-note {
  margin: 0;
  display: grid;
  font-size: 11.5px;
  line-height: 1.45;
  color: var(--ds-ink-mute);
}

.landing-how-it-works-approve-note > span {
  grid-area: 1 / 1;
  display: inline-flex;
  align-items: center;
  gap: 5px;
  opacity: 0;
}

.landing-how-it-works-approve-note > span:nth-child(1) {
  opacity: 1;
  animation: landing-page-hiw-note-1 8s linear forwards;
}

.landing-how-it-works-approve-note > span:nth-child(2) {
  animation: landing-page-hiw-note-2 8s linear forwards;
}

.landing-how-it-works-approve-note > span:nth-child(3) {
  animation: landing-page-hiw-note-3 8s linear forwards;
  color: var(--ds-primary-deep);
}

/* The spinner on "Enviando…". A ring with one quadrant open, which is the same
   shape the demo's own tool steps spin while they are pending. */
.landing-how-it-works-approve-note i {
  width: 10px;
  height: 10px;
  flex: none;
  border-radius: var(--ds-radius-pill);
  border: 1.5px solid var(--ds-hairline);
  border-top-color: var(--ds-primary);
  animation: landing-page-hiw-note-spin 0.7s linear infinite;
}

@keyframes landing-page-hiw-batch-fill {
  0%, 43% { background-color: var(--ds-hairline); transform: scaleY(1); }
  45% { background-color: var(--ds-primary); transform: scaleY(1.8); }
  49%, 100% { background-color: var(--ds-primary); transform: scaleY(1); }
}

@keyframes landing-page-hiw-note-1 {
  0%, 40% { opacity: 1; }
  44%, 100% { opacity: 0; }
}

@keyframes landing-page-hiw-note-2 {
  0%, 42% { opacity: 0; }
  46%, 66% { opacity: 1; }
  70%, 100% { opacity: 0; }
}

@keyframes landing-page-hiw-note-3 {
  0%, 68% { opacity: 0; }
  72%, 100% { opacity: 1; }
}

@keyframes landing-page-hiw-note-spin {
  to { transform: rotate(360deg); }
}

/* 3.4s of 8s = 42.5%. The swap is a hard cut with a short crossfade either
   side, so it reads as a state change rather than as a dissolve. */
@keyframes landing-page-hiw-approve-out {
  0%, 40% { opacity: 1; }
  45%, 100% { opacity: 0; }
}

@keyframes landing-page-hiw-approve-in {
  0%, 42% { opacity: 0; }
  47%, 100% { opacity: 1; }
}

@keyframes landing-page-hiw-approve-box {
  0%, 40% {
    background-color: var(--ds-primary);
    color: var(--ds-on-primary);
    box-shadow: var(--ds-elevation-1);
  }
  45%, 100% {
    background-color: var(--ds-canvas);
    color: var(--ds-primary-deep);
    box-shadow: inset 0 0 0 1px var(--ds-hairline);
  }
}

@keyframes landing-page-hiw-approve-ring {
  0%, 38% { opacity: 0; transform: scale(0.9); }
  41% { opacity: 0.85; transform: scale(0.94); }
  50%, 100% { opacity: 0; transform: scale(1.12); }
}

/* Narrow: the bar stacks and the actions go full width, because 390px has no
   room for a two-column footer under the sheet. */
@media (max-width: 47.99rem) {
  .landing-how-it-works-approve {
    padding: 12px 14px 14px;
  }

  .landing-how-it-works-approve-actions {
    width: 100%;
  }

  .landing-how-it-works-approve-primary {
    flex: 1;
  }
}

/* Reduced motion keeps the resolved state: the point is that an approval
   happened and is timestamped, and that survives without the transition. */
@media (prefers-reduced-motion: reduce) {
  .landing-how-it-works-approve-primary,
  .landing-how-it-works-approve-primary > em,
  .landing-how-it-works-approve-primary > b,
  .landing-how-it-works-approve-primary::after {
    animation: none;
  }

  .landing-how-it-works-approve-primary {
    background: var(--ds-canvas);
    color: var(--ds-primary-deep);
    box-shadow: inset 0 0 0 1px var(--ds-hairline);
  }

  .landing-how-it-works-approve-primary > em { opacity: 0; }
  .landing-how-it-works-approve-primary > b { opacity: 1; }

  .landing-how-it-works-approve-batch span,
  .landing-how-it-works-approve-note > span,
  .landing-how-it-works-approve-note i {
    animation: none;
  }

  .landing-how-it-works-approve-batch span {
    background-color: var(--ds-primary);
  }

  .landing-how-it-works-approve-note > span:nth-child(1),
  .landing-how-it-works-approve-note > span:nth-child(2) {
    opacity: 0;
  }

  .landing-how-it-works-approve-note > span:nth-child(3) {
    opacity: 1;
  }
}

/* ==========================================================================
   22. The spreadsheet in the demo's result panel

   `tools/cobranza-hiw-demo.mjs` builds this as JSX and drops it into
   `.landing-how-it-works-document-body`. The panel has been a brief, a
   letter and a compose window before this, and each of those asked a
   visitor to read before they knew what they were looking at. Column
   letters, row numbers and a sheet tab do not: that is recognised
   pre-verbally, and what it is recognised as — a ledger of overdue
   invoices — is what the chapter is about.

   The `estado` column is the part that earns its place. The reply says
   "de 34 facturas vencidas, 9 ya estaban pagadas", and until now that was
   a sentence to be taken on trust. Here it is a column you can see: Impaga
   rows, a Pagada row where a transfer landed and nobody matched it, two
   Convenio rows the policy excludes. So the pills get real colour — this
   is the one place on the page where colour identifies rather than
   emphasises, and washing them out would cost the whole point of showing
   a table instead of a paragraph.

   Everything is on the 24-grid sans and `tabular-nums`, because a ledger
   whose digits do not line up column-to-column is not a ledger. That is
   also why `monto` and `días` are right-aligned and `razón social` is not.

   Sizes are set on cells, never on bare `p` or `li`, so none of this has to
   out-specify `.landing-how-it-works-document-body p, li { font-size:
   .78rem }` — it simply never collides with it.
   ========================================================================== */

.landing-how-it-works-sheet {
  display: flex;
  flex-direction: column;
  border: 1px solid var(--ds-hairline);
  border-radius: var(--ds-radius-card);
  background: var(--ds-canvas);
  box-shadow: var(--ds-elevation-1);
  overflow: hidden;
}

/* --- the file bar --------------------------------------------------------- */

.landing-how-it-works-sheet-bar {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.5rem 0.7rem;
  border-bottom: 1px solid var(--ds-hairline);
  background: color-mix(in oklab, var(--ds-canvas-soft) 55%, transparent);
}

/* The type badge, in the green every spreadsheet application uses for one.
   It is the only colour on the page that is not from the palette, and it is
   here because "XLS" in indigo does not read as a spreadsheet — the colour
   is doing the identifying, exactly like the estado pills below. */
.landing-how-it-works-sheet-bar > em {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.75rem;
  height: 1.75rem;
  border-radius: var(--ds-radius-xs);
  background: #107c41;
  color: #fff;
  font-style: normal;
  font-size: 0.5625rem;
  font-weight: 700;
  letter-spacing: 0.02em;
}

.landing-how-it-works-sheet-bar > strong {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: var(--ds-ink);
  font-size: 0.8125rem;
  font-weight: 600;
}

.landing-how-it-works-sheet-bar-actions {
  display: flex;
  align-items: center;
  gap: 0.45rem;
  margin-left: auto;
  color: var(--ds-ink-mute);
}

/* --- the name box and formula bar ----------------------------------------- */

.landing-how-it-works-sheet-formula {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.35rem 0.7rem;
  border-bottom: 1px solid var(--ds-hairline);
  color: var(--ds-ink-body);
  font-size: 0.71875rem;
}

.landing-how-it-works-sheet-namebox {
  flex: none;
  min-width: 2.75rem;
  padding: 0.1rem 0.35rem;
  border: 1px solid var(--ds-hairline);
  border-radius: var(--ds-radius-xs);
  background: var(--ds-canvas);
  color: var(--ds-ink);
  font-variant-numeric: tabular-nums;
}

.landing-how-it-works-sheet-formula > s {
  flex: none;
  width: 1px;
  height: 0.9rem;
  background: var(--ds-hairline);
}

.landing-how-it-works-sheet-formula > i {
  flex: none;
  color: var(--ds-ink-mute);
  font-style: italic;
  font-family: var(--font-display);
}

/* --- the grid -------------------------------------------------------------- */

.landing-how-it-works-sheet-grid {
  overflow-x: auto;
}

.landing-how-it-works-sheet-table {
  width: 100%;
  border-collapse: collapse;
  font-variant-numeric: tabular-nums;
}

.landing-how-it-works-sheet-table th,
.landing-how-it-works-sheet-table td {
  padding: 0.34rem 0.55rem;
  border-right: 1px solid color-mix(in oklab, var(--ds-hairline) 60%, transparent);
  border-bottom: 1px solid color-mix(in oklab, var(--ds-hairline) 60%, transparent);
  font-size: 0.75rem;
  font-weight: 400;
  text-align: left;
  white-space: nowrap;
}

/* Column letters and row numbers are chrome: same tint, same muted ink, and
   centred the way a spreadsheet centres them. */
.landing-how-it-works-sheet-cols th,
.landing-how-it-works-sheet-rownum {
  background: color-mix(in oklab, var(--ds-canvas-soft) 70%, transparent);
  color: var(--ds-ink-mute);
  font-size: 0.6875rem;
  text-align: center;
}

.landing-how-it-works-sheet-cols th {
  width: auto;
}

.landing-how-it-works-sheet-cols th:first-child,
.landing-how-it-works-sheet-rownum {
  width: 2rem;
  min-width: 2rem;
}

/* Row 1 holds the field names — the sheet's own header, not the viewer's. */
.landing-how-it-works-sheet-heads th:not(.landing-how-it-works-sheet-rownum) {
  color: var(--ds-ink);
  font-weight: 600;
}

.landing-how-it-works-sheet-name {
  max-width: 13rem;
  overflow: hidden;
  text-overflow: ellipsis;
  color: var(--ds-ink-body);
}

.landing-how-it-works-sheet-num {
  text-align: right;
  color: var(--ds-ink);
}

/* The selected cell, matching the A2 in the name box above. The two agreeing
   is most of what makes this read as an open application rather than as a
   picture of a table. */
.landing-how-it-works-sheet-selected {
  outline: 2px solid var(--ds-primary);
  outline-offset: -2px;
  color: var(--ds-ink);
  font-weight: 500;
}

/* --- the estado pills ------------------------------------------------------ */

.landing-how-it-works-sheet-estado em {
  display: inline-block;
  padding: 0.08rem 0.4rem;
  border-radius: var(--ds-radius-pill);
  font-style: normal;
  font-size: 0.6875rem;
  font-weight: 600;
  letter-spacing: 0.01em;
}

.landing-how-it-works-sheet-estado em[data-estado='impaga'] {
  background: color-mix(in oklab, var(--ds-ruby) 12%, transparent);
  color: color-mix(in srgb, var(--ds-ruby) 76%, var(--ds-ink));
}

.landing-how-it-works-sheet-estado em[data-estado='pagada'] {
  background: color-mix(in oklab, #107c41 14%, transparent);
  color: color-mix(in srgb, #107c41 82%, var(--ds-ink));
}

.landing-how-it-works-sheet-estado em[data-estado='convenio'] {
  background: color-mix(in oklab, var(--ds-sherbet) 18%, transparent);
  color: color-mix(in srgb, var(--ds-sherbet) 62%, var(--ds-ink));
}

/* --- the tail row ---------------------------------------------------------- */

/* A sheet that stopped at six rows without saying so would be claiming the
   ledger is six rows long, which contradicts every total on screen. */
.landing-how-it-works-sheet-rest td {
  color: var(--ds-ink-mute);
  font-size: 0.6875rem;
  font-style: italic;
}

/* --- the sheet tabs -------------------------------------------------------- */

.landing-how-it-works-sheet-tabs {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  padding: 0.35rem 0.7rem;
  border-top: 1px solid var(--ds-hairline);
  background: color-mix(in oklab, var(--ds-canvas-soft) 55%, transparent);
}

/* The active tab. White against the tinted strip with the top edge picked out
   in the primary — the shape every spreadsheet uses to say which sheet you
   are on, and the reason one tab is enough to read as a tab bar. */
.landing-how-it-works-sheet-tab {
  padding: 0.2rem 0.6rem;
  border: 1px solid var(--ds-hairline);
  border-top: 2px solid var(--ds-primary);
  border-radius: 0 0 var(--ds-radius-xs) var(--ds-radius-xs);
  background: var(--ds-canvas);
  color: var(--ds-ink);
  font-size: 0.71875rem;
  font-weight: 600;
}

.landing-how-it-works-sheet-add {
  display: inline-flex;
  align-items: center;
  color: var(--ds-ink-mute);
}

.landing-how-it-works-sheet-tabs > small {
  margin-left: auto;
  color: var(--ds-ink-mute);
  font-size: 0.6875rem;
  font-variant-numeric: tabular-nums;
}

/* At the narrow breakpoint the panel takes the whole shell. The grid already
   scrolls horizontally, so the columns keep their widths and the chrome just
   tightens; the formula bar goes, because a name box and an `fx` at 390px is
   the first thing a real application drops too. */
@media (max-width: 47.99rem) {
  .landing-how-it-works-sheet-bar,
  .landing-how-it-works-sheet-tabs {
    padding-inline: 0.55rem;
  }

  .landing-how-it-works-sheet-formula {
    display: none;
  }

  .landing-how-it-works-sheet-name {
    max-width: 9rem;
  }
}


/* ==========================================================================
   23. The cursor's fourth target

   The demo flies a fake cursor at three things: the composer and its submit
   button in `describe`, and Cerrar panel in `recurring`. `result` had none —
   the ledger panel simply appeared, with nothing connecting it to the
   artefact card sitting in the chat beside it.

   `tools/cobranza-hiw-demo.mjs` adds a `card` target and a
   `target-card` → `click-card` → `open` sequence on the result phase.
   `_astro/index.BRimgEk0.css` knows nothing about either, and it is
   precompiled, so the two rules it would have needed live here instead:
   where the cursor goes, and the ring it leaves when it clicks.

   Both mirror that sheet exactly rather than inventing anything — same
   custom-property convention for the position, same 1px fern ring at the
   same size and offset, same `landing-page-how-it-works-click` keyframes.
   A fourth target that ripples differently from the other three would read
   as a different kind of event.
   ========================================================================== */

.landing-how-it-works-preview[data-cursor-target='card'] .landing-how-it-works-cursor {
  --cursor-x: var(--card-target-x);
  --cursor-y: var(--card-target-y);
  opacity: 1;
}

.landing-how-it-works-preview[data-result-phase='click-card'] .landing-how-it-works-cursor::after {
  content: '';
  position: absolute;
  top: -0.5rem;
  left: -0.5rem;
  width: 1.6rem;
  height: 1.6rem;
  border: 1px solid var(--color-fern);
  border-radius: 9999px;
  animation: 0.52s ease-out both landing-page-how-it-works-click;
}

/* `_astro`'s reduced-motion block lists the cursor and the ripple selectors
   it knows about; this one has to opt out on its own or it would be the only
   ring still expanding when everything else has stopped. */
@media (prefers-reduced-motion: reduce) {
  .landing-how-it-works-preview[data-result-phase='click-card'] .landing-how-it-works-cursor::after {
    animation: none;
  }
}
