/* The design system's MARKETING layer for server-rendered pages, outside the React app bundle.
   ================================================================================

   Tokens are `--mkt-*`, classes `.rd-mkt-*`. The prefix is the enforcement, not a convention:
   `website/tests/test_marketing_layer.py` fails an unprefixed name declared here, and a marketing
   name used under `frontend/`.

   It is a separate file because `frontend/vite.config.ts` sets `cssCodeSplit: false` — anything a
   Vite entry imports lands in the app's single stylesheet, whichever entry pulled it in. This
   reaches the browser through a `<link>` in a server-rendered template instead, which is also why
   the landing page ships no JavaScript framework.

   Resolves against `static/css/tokens.css`, the one palette, and declares no `--sand-*` or `--n-*`
   name of its own.

   What it adds and why the base system had no answer: `design-system/docs/guides/marketing.md`. */

/* ---------------------------------------------------------------- Tokens ----

   Mobile first. Values are the mobile artboard's; the two media queries below step them at the
   design system's own breakpoints. Media queries cannot read `var()`, so the literals are copied
   with the token they stand for cited beside them. */
:root {
  /* Type. Sizes are literals rather than scale steps because the marketing scale is not the app's
     1.2 ramp — a landing page sets its own rhythm against a much larger measure. */
  --mkt-font-size-display: 34px; /* the hero headline, once per page */
  --mkt-font-size-title: 28px; /* section headers, and the closing line */
  --mkt-font-size-subtitle: 21px; /* the phase headings inside a section */
  --mkt-font-size-lead: 17px; /* the hero standfirst — sans, not serif */
  --mkt-font-size-read: 18px; /* editorial body. Serif: this is the page's reading voice */
  --mkt-font-size-control: 17px; /* nav links and CTA labels */
  --mkt-font-size-wordmark: 21px; /* the wordmark, which is a mark and steps with nothing */
  --mkt-font-size-figure: 32px; /* a price */
  --mkt-font-size-plan: 17px; /* a plan name */
  --mkt-font-size-cell: 16px; /* a pricing table cell */
  --mkt-font-size-annotation: 14px; /* the unit beside a price; a card's mono value */
  --mkt-font-size-rate: 13px; /* the mono rate column in the usage table */
  --mkt-font-size-badge: 11px; /* the SOON tag */
  --mkt-font-size-micro: 11px; /* the footer's mono rule */

  /* Rhythm. `--mkt-space-9` continues the app's 4px scale past its last step (`--space-8`, 64px),
     because a landing page's band gaps are larger than anything a session view has ever needed.
     Marketing-prefixed rather than appended to the core scale for that reason: a 96px gap inside
     the product would be a mistake, not a choice. */
  --mkt-space-9: 96px;

  --mkt-gutter: 20px; /* the page's side padding */
  --mkt-band: 64px; /* vertical air above a section */

  /* The pinned track's rhythm. Desktop-only: it is the gap between the sections that scroll past
     the docked card, and it has no meaning in the stacked layout. */
  --mkt-track-gap: 200px;

  /* The composition cutoff. NOT a breakpoint — `--breakpoint-mobile` (640) and
     `--breakpoint-desktop` (1024) remain the only two the system has. This is the width below
     which the scroll-pinned desktop composition cannot fit its docked card and text track side by
     side, so the band beneath it falls back to the stacked layout rather than getting a third
     design of its own. Declared for documentation; media queries copy the literal.

     It is the shell, not the track arithmetic. The open card is `left: 30px` plus a 1200px width,
     so its right edge is at 1230 and the composition needs a viewport at least that wide. Gated at
     1200 — the track's own 795 + 405 — the card ran 30px past the viewport from 1200 to 1229 and
     `overflow-x: clip` swallowed it, so nothing overflowed and a slice of the hero was simply gone.
     `--mkt-shell` clears 1230 with a margin and is a number the page already means something by. */
  --mkt-compose: 1260px;

  /* CTA metrics. 48px is `--space-7`, the system's touch floor — the marketing CTA keeps it and
     spends its extra size on horizontal padding instead of height. */
  --mkt-cta-height: 48px;
  --mkt-cta-padding: 24px;

  /* The hero backdrop: an atmospheric field behind the first viewport, a photograph in both
     themes.

     The wash is what makes it a backdrop and not a picture: it holds the field back off the hero
     text, and it is stronger on dark because a luminous streak carries further against a near-black
     ground than a wet-in-wet bloom does against cream. The fade is the field's bottom edge,
     dissolving into the page so the band needs no border to end on. */
  --mkt-backdrop-height: 100vh;
  --mkt-backdrop-fade: 34vh;

  /* The bar is sticky, so it is read over the backdrop at rest and over page ground once it
     sticks. A flat opaque stripe is what the old in-flow bar was written to avoid; glass is
     what lets one ground work over both fields. The height is a token because the anchor
     offset below has to clear the bar exactly, and two numbers that must agree should be one. */
  --mkt-nav-h: 64px;
  --mkt-nav-ground: color-mix(in oklab, var(--sand-1) 82%, transparent);
  --mkt-backdrop-wash: light-dark(
    color-mix(in srgb, var(--sand-1) 22%, transparent),
    color-mix(in srgb, var(--sand-1) 64%, transparent)
  );

  /* How far down the field is held back so the bar has a quiet ground. The bar is 64px — the CTA
     height plus its padding either side — and this is a little over twice that, which is what it
     takes for the gradient to still be decaying once it is past the links. Without it the bar's
     accent hover lands on the field's warmest band at 3.8:1; the resting ink clears either way. */
  --mkt-backdrop-clear: 150px;

  /* The field is a photograph in both themes: a watercolour sky on light (1600x1000, 118KB) and a
     spectral smear on dark (1400x788, 167KB). An earlier build drew the light one out of the ramps
     instead, which got the palette right and still read as an airbrush — paint is irregular in a
     way a gradient cannot be.

     This is the page's one `prefers-color-scheme` query, and it is here rather than in a
     `light-dark()` because that function takes colours, not images. Every alternative that keeps
     the single switch — a cover layer over one raster, a mask over a second box — still leaves both
     images in the used declaration, so every visitor downloads both and throws one away. Only the
     winning custom property is substituted, so the query is what makes it one fetch.

     The `[data-theme]` rules are what keep the two switches from disagreeing: base.css says only
     the app shell's script stamps that attribute, so on this page the media query decides alone —
     but a toggle arriving on a public page is the exact failure the single-switch rule exists to
     prevent, and three lines close it now rather than leaving a raster behind. */
  --mkt-backdrop-image: url("../images/hero-watercolour.2c293d3779e7.webp");

  /* Accent ink for the two links that are read over the backdrop. `--n-accent-text` is --orange-11,
     which clears a bare --sand-1 by 5.4:1 — enough on the flat surfaces the app puts it on, and not
     enough for anything with a tint under it. The step below it is the answer in light. In dark it
     is not, --orange-10 there being a fill step (#833719), which as ink is the 2.3:1 bug this class
     already had once — so dark keeps --orange-11. */
  --mkt-accent-ink: light-dark(var(--orange-10), var(--orange-11));

  /* Motion. The track entrance is the page's one time-based transition, and it wants double
     `--duration-slow` — a step the base scale has no name for. Collapsed to 1ms under reduced
     motion below, exactly as tokens.css collapses its own four; 0s would never fire
     transitionend. */
  --mkt-duration-entrance: 0.5s;

  /* The card's cross-dissolve. The four panes share an app shell — same rail, same header, same
     composer — so only the content inside changes at a swap, and a cut there reads as the frame
     glitching rather than the subject changing. It happens in peripheral vision: the reader is in
     the text column, not on the card. Long enough to register as a dissolve, short enough that a
     fast scroll never stacks two of them. */
  --mkt-duration-swap: 0.32s;

  /* The scrolly stage. The card the hero media docks into, and the column the text track runs in,
     both measured inside `--mkt-shell` at the desktop composition's design width. */
  --mkt-shell: 1260px;
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --mkt-backdrop-image: url("../images/hero-spectral-dark.61f2faa2ddfd.webp");
  }
}

:root[data-theme="dark"] {
  --mkt-backdrop-image: url("../images/hero-spectral-dark.61f2faa2ddfd.webp");
}

/* 640 is --breakpoint-mobile. */
@media (min-width: 640px) {
  :root {
    --mkt-font-size-display: 44px;
    --mkt-font-size-title: 32px;
    --mkt-font-size-subtitle: 22px;
    --mkt-font-size-lead: 19px;
    --mkt-font-size-read: 21px;
    --mkt-font-size-wordmark: 22px;
    --mkt-font-size-figure: 26px;
    --mkt-font-size-plan: 20px;
    --mkt-font-size-cell: 17px;
    --mkt-font-size-rate: 16px;

    --mkt-gutter: 48px;
    --mkt-band: var(--mkt-space-9);
  }
}

/* 1260 is --mkt-compose, the composition cutoff above. The desktop composition runs its headings
   and body inside a 405px track column, so its type steps DOWN from the tablet layout's — that
   column is a third of the width the tablet sets the same paragraph in. */
@media (min-width: 1260px) {
  /* The artboard's 0.75 zoom, rounded onto whole pixels — the hierarchy and its ordering are the
     artboard's, the quarter-pixels were residue. Badge and micro do not step down at all: they
     are already the system's smallest text-safe voice at 11px, and a uniform factor applied to a
     floor lands below it (8.25px and 9px were the two sub-legible sizes on the page). */
  :root {
    --mkt-font-size-display: 45px;
    --mkt-font-size-title: 30px;
    --mkt-font-size-subtitle: 20px;
    --mkt-font-size-lead: 17px;
    --mkt-font-size-read: 19px;
    --mkt-font-size-control: 16px;
    --mkt-font-size-figure: 26px;
    --mkt-font-size-plan: 15px;
    --mkt-font-size-cell: 14px;
    --mkt-font-size-rate: 13px;
    --mkt-font-size-badge: 11px;
    --mkt-font-size-micro: 11px;

    --mkt-band: 82.5px;
  }
}

/* ------------------------------------------------------------------ Page ---- */

body {
  background: var(--sand-1);
  color: var(--n-text);
  font-family: var(--font-family-sans);
}

.rd-mkt-page {
  min-height: 100vh;
  background: var(--sand-1);
  color: var(--n-text);
  font-family: var(--font-family-sans);
  /* The containing block for the backdrop below. The shell inside keeps its own `relative`, and
     stays the nearer ancestor for everything the stage positions. */
  position: relative;
}

/* The hero backdrop. Full-bleed rather than shell-width — it is the weather the page sits in, and a
   field that stopped at 1260px would read as a banner.

   On the modifier, not on `.rd-mkt-page`, because the field belongs to a hero rather than to the
   shell: the blog carries the same bar, bands, type and footer, and a full-viewport photograph
   behind an essay's title is weather for a page that has no hero to set.

   A pseudo-element, not the three divs the artboard draws, because the layers are backgrounds
   rather than content and CSS stacks those on one box. Order below is paint order, topmost first:
   the fade, then the top scrim, then the wash, then the hero's own ground, then the photograph.

   `z-index: 0` puts it on the same layer as the shell, which follows it in tree order and therefore
   paints over it. Without the index it would sit in the positioned layer above the shell's in-flow
   content and cover the hero. */
.rd-mkt-page--backdrop::before {
  content: "";
  position: absolute;
  inset: 0 0 auto;
  height: var(--mkt-backdrop-height);
  z-index: 0;
  pointer-events: none;
  background:
    linear-gradient(
        180deg,
        transparent 0%,
        color-mix(in srgb, var(--sand-1) 55%, transparent) 55%,
        var(--sand-1) 94%
      )
      0 100% / 100% var(--mkt-backdrop-fade) no-repeat,
    linear-gradient(180deg, color-mix(in srgb, var(--sand-1) 66%, transparent) 0%, transparent 100%)
      0 0 / 100% var(--mkt-backdrop-clear) no-repeat,
    linear-gradient(var(--mkt-backdrop-wash), var(--mkt-backdrop-wash)),
    /* The hero's own ground, in both themes — `--sand-1` is near-white in light and near-black in
       dark, so one layer moves the ground away from the ink either way. It is load-bearing at both
       ends: the watercolour's grey cloud sits under "Watch the demo" at 3.5:1 without it, and the
       spectral smear's bright streaks put the same link at 3.3:1. */
    radial-gradient(
      34% 30% at 50% 26%,
      light-dark(
          color-mix(in srgb, var(--sand-1) 70%, transparent),
          color-mix(in srgb, var(--sand-1) 74%, transparent)
        )
        0%,
      transparent 100%
    ),
    var(--mkt-backdrop-image) 50% 30% / cover no-repeat;
}

/* The marketing pages carry no theme toggle, so `color-scheme` from base.css is the only switch
   and every token below resolves through `light-dark()`. */

/* `hidden` is how the looping session and the docked card's panes say "not this frame", and both
   sit on elements this file also gives a `display` to — where the UA's `[hidden]` rule loses on
   specificity and the element stays painted. Scoped to the marketing page rather than global, so
   it cannot reach a stylesheet that did not ask for it. */
.rd-mkt-page [hidden] {
  display: none !important;
}

/* Composed-only; the stacked layout carries the hero's own controls in flow. */
.rd-mkt-shell {
  max-width: var(--mkt-shell);
  margin: 0 auto;
  position: relative;
}

/* A section's vertical rhythm and side gutter. Bands stack with no gap of their own: each owns
   the air above it, which keeps the last one from adding trailing space the footer then repeats. */
.rd-mkt-band {
  padding: var(--mkt-band) var(--mkt-gutter) 0;
}

/* Band air is carried on the top edge only, so each gap belongs to the section below it and the
   last one on the page has no one to inherit from. Without this the pricing panel's border sits on
   the footer rule. */
.rd-mkt-band:last-child {
  padding-bottom: var(--mkt-band);
}

/* An anchored band is a jump destination, and the sticky bar does overlay it — so the offset is the
   bar's own height plus a breath, or the heading lands underneath it. */
.rd-mkt-band[id] {
  scroll-margin-top: calc(var(--mkt-nav-h) + var(--space-4));
}

/* The closing band. `.rd-mkt-band` gives it its top air and the `:last-child` rule above gives it
   its bottom, so all this adds is the centring and the contact line. */
.rd-mkt-band--close {
  text-align: center;
}

.rd-mkt-band--close .rd-mkt-ctas {
  justify-content: center;
}

/* On a phone both closing controls take the column, like every other control at this width. The row
   has to become a column first: `.rd-mkt-ctas` does not wrap, so two full-width buttons in it would
   otherwise overflow rather than stack. */
@media (max-width: 639px) {
  .rd-mkt-band--close .rd-mkt-ctas {
    flex-direction: column;
    gap: var(--space-3);
  }

  .rd-mkt-band--close .rd-mkt-cta {
    width: 100%;
  }
}

/* ------------------------------------------------------------------ Type ---- */

.rd-mkt-display {
  margin: 0;
  font: 500 var(--mkt-font-size-display) / 1.16 var(--font-family-sans);
  letter-spacing: -0.02em;
  text-wrap: balance;
}

.rd-mkt-title {
  margin: 0;
  font: 500 var(--mkt-font-size-title) / 1.2 var(--font-family-sans);
  letter-spacing: -0.015em;
  text-wrap: pretty;
}

.rd-mkt-subtitle {
  margin: 0;
  font: 500 var(--mkt-font-size-subtitle) var(--font-family-sans);
}

.rd-mkt-lead {
  margin: 0;
  font: 400 var(--mkt-font-size-lead) / 1.5 var(--font-family-sans);
}

/* The editorial voice. Serif via the token — the design system's `--font-family-serif` already
   resolves to Newsreader, so naming the family here would be a second source of truth for it. */
/* ★ A measure cap, not a layout width: without one the stacked band runs the page's editorial
   voice at 77ch by 1024 and 92ch by 1199 — measured against the actual `0` advance of Newsreader
   at 21px, where 65-75 is the comfortable band. It bites only between `--breakpoint-desktop` and
   the composition; below that the column is already narrower, and above it the composed track
   sets the same text in 405px (41ch) by design. */
.rd-mkt-read {
  max-width: 70ch;
  margin: 0;
  font: 400 var(--mkt-font-size-read) / 1.65 var(--font-family-serif);
  text-wrap: pretty;
}

.rd-mkt-read--strong {
  font-weight: 500;
}

.rd-mkt-read + .rd-mkt-read {
  margin-top: var(--space-4);
}

/* The Micro-Label Recipe, at marketing's wider tracking: the page sets these against far more
   whitespace than a panel header does, and 0.06em closes up at that scale. */
.rd-mkt-kicker {
  font: 500 var(--font-size-caption) var(--font-family-mono);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--n-text-muted);
}

.rd-mkt-kicker--accent {
  color: var(--n-accent-text);
}

/* ★ Composed only, and only these. In the stacked layout every kicker sits below the backdrop on a
   flat ground and the muted step clears AA at 5.97:1. Inside the pinned track they are read over the
   photograph, and the muted step measures **3.16:1 (THE PROBLEM) and 2.98:1 (OUR BELIEF)** against
   the worst pixel beneath them — worse than the 4.18:1 that moved `.rd-mkt-nav__link` off the same
   step. The accent variant already clears at 5.43 because `--mkt-accent-ink` exists for this exact
   reason, so it takes that here rather than `--n-accent-text`.

   Scoped rather than applied to the base class: the same selector is the `<dt>` in the pricing
   cards, which is on a flat ground and must stay muted. Re-measure at three scroll positions
   INSIDE the pin, not at the top — at scroll 0 the track is still `opacity: 0`. */
[data-compose="stage"] .rd-mkt-stage__track .rd-mkt-kicker {
  color: var(--n-text);
}

[data-compose="stage"] .rd-mkt-stage__track .rd-mkt-kicker--accent {
  color: var(--mkt-accent-ink);
}

/* A heading that sits on a rule rather than in a box — the marketing counterpart of PanelHeader,
   for a section that has no panel to head. */
.rd-mkt-rule {
  padding-bottom: var(--space-4);
  border-bottom: 1px solid var(--n-border);
}

.rd-mkt-rule + .rd-mkt-read {
  margin-top: var(--space-4);
}

/* ------------------------------------------------------------------- CTA ----

   Square by intent: `--radius-none` is the design system's way of saying "deliberately not
   rounded", and a marketing CTA that meets its column's edge is not a card. */
/* ★ `gap`, not a space in the markup. The arrow is wrapped in an `aria-hidden` span so a screen
   reader does not announce "Try rubberduck right arrow" — but this box is `inline-flex`, so the
   label and that span are two flex items, and leading whitespace at the start of a flex item is
   stripped. The space was in the DOM and rendered at 0px. A label with no span has a single item
   and no gap to apply, so this is inert everywhere else. */
.rd-mkt-cta {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-1);
  box-sizing: border-box;
  height: var(--mkt-cta-height);
  padding: 0 var(--mkt-cta-padding);
  border: 1px solid transparent;
  border-radius: var(--radius-none);
  font: 500 var(--mkt-font-size-control) var(--font-family-sans);
  white-space: nowrap;
  cursor: pointer;
  text-decoration: none;
  transition: background var(--duration-fast) var(--ease-out);
}

.rd-mkt-cta--primary {
  background: var(--n-accent);
  color: var(--n-accent-contrast);
}

.rd-mkt-cta--secondary {
  border-color: var(--n-border);
  color: var(--n-text);
}

/* `white-space: nowrap` on `.rd-mkt-cta` keeps the hero button's arrow on its label's line, and
   there it is right: that button is only as wide as its text, so a break would strand the arrow.
   A block CTA is already as wide as its card, so nowrap buys it nothing and costs it the card's
   min-width — the button's unbreakable label becomes the widest thing in the column. At 320px
   (400% zoom, the width WCAG 1.4.10 asks a page to reflow at) today's longest label sits about
   eleven characters from pushing the pricing card past the viewport, which is one ordinary copy
   edit. Wrapping lets the button grow downward instead, which a card absorbs and a viewport does
   not. Every shipped label is short enough to stay on one line, so nothing moves today. */
.rd-mkt-cta--block {
  width: 100%;
  height: auto;
  min-height: var(--mkt-cta-height);
  white-space: normal;
}

@media (hover: hover) {
  .rd-mkt-cta--primary:hover {
    background: var(--n-accent-hover);
  }

  .rd-mkt-cta--secondary:hover {
    background: var(--sand-3);
  }
}

.rd-mkt-cta:focus-visible {
  outline: var(--focus-ring);
  outline-offset: 2px;
}

/* Every keyboard stop takes the system ring, not only the CTAs — a UA-default ring reads as the
   one part of the page nobody designed. */
.rd-mkt-nav__brand:focus-visible,
.rd-mkt-nav__link:focus-visible,
.rd-mkt-nav__menu:focus-visible,
.rd-mkt-footer__links a:focus-visible,
.rd-mkt-textlink:focus-visible {
  outline: var(--focus-ring);
  outline-offset: 2px;
}

/* A panel link is full-bleed — `x: 0, width: 100%` inside an absolutely positioned panel — which is
   exactly the case tokens.css defines the inset ring for. An outward 2px offset plus a 2px stroke
   put 4px of ring past each edge, and `overflow-x: clip` deleted it silently rather than scrolling.
   The panel's own CTA is inset by a gutter and keeps the outset ring, which is the other half of
   that rule: a ring on a fill has to sit on the surface around it. */
.rd-mkt-nav__panel-link:focus-visible {
  outline: var(--focus-ring);
  outline-offset: var(--focus-ring-offset);
}

/* The secondary affordance beside a CTA: an accent word on a 40% edge, not a button. The
   border-bottom is the underline, so the touch box grows through the overlay pseudo-element
   below rather than padding, which would detach it. */
.rd-mkt-textlink {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  align-self: center;
  padding-bottom: 2px;
  border-bottom: 1px solid color-mix(in srgb, var(--mkt-accent-ink) 40%, transparent);
  color: var(--mkt-accent-ink);
  font: 500 var(--mkt-font-size-control) var(--font-family-sans);
  text-decoration: none;
  white-space: nowrap;
  cursor: pointer;
}

/* Off-screen until focused, then a real control in the corner it would occupy anyway. The page is
   ~5,400px with sixteen chrome stops before the first content, and above `--mkt-compose` the first
   two viewport-heights are a pinned stage — so the bypass matters more here than the link count
   alone suggests. */
.rd-mkt-skip {
  position: absolute;
  left: var(--mkt-gutter);
  top: var(--space-2);
  /* One over the bar, not on its layer: `.rd-mkt-nav` is also `--z-sticky` and follows in tree
     order, so at the same value the wordmark paints over the focused link. */
  z-index: calc(var(--z-sticky) + 1);
  transform: translateY(-200%);
  display: inline-flex;
  align-items: center;
  height: var(--mkt-cta-height);
  padding: 0 var(--mkt-cta-padding);
  background: var(--n-accent);
  color: var(--n-accent-contrast);
  font: 500 var(--mkt-font-size-control) var(--font-family-sans);
  text-decoration: none;
}

.rd-mkt-skip:focus-visible {
  transform: none;
  outline: var(--focus-ring);
  outline-offset: 2px;
}

/* The wordmark paints a 24px line and is the bar's only control besides the menu button on a
   phone. It was the one link on the page with no overlay at all. */
.rd-mkt-nav__brand::before {
  content: "";
  position: absolute;
  inset: -12px -8px;
}

/* Invisible hit-area overlays: these links paint a text line ~24px tall, but "Watch the demo"
   is the only route to the demo, the footer links are the no-JS mobile navigation, and the bar
   links serve the 640–1023 touch band — The 48 Rule applies to all three. The overlay reaches
   the floor without painting a pixel; horizontal reach stays under half each context's gap so
   adjacent boxes never overlap. Vertical reach is not free, though — an overlay that extends past
   its container's last pixel grows the document, so the footer's is biased upward below. */
.rd-mkt-textlink::before,
.rd-mkt-nav__link::before,
.rd-mkt-footer__links a::before {
  content: "";
  position: absolute;
  inset: -12px -8px;
}

/* ★ A symmetric inset is the wrong instrument for a vertical target: it has to be tuned against a
   line box, and every one of these line boxes moves — with a font-size step at 1260, or with a
   border the padding box does not count. Both link families below take a fixed centred height
   instead, which cannot drift. Only the footer keeps an inset, because its row has no font step
   and the height it needs is asymmetric (see there).

   Horizontal reach is still per-family, and its budget is the distance to the neighbouring target,
   not the CSS `gap` — the footer's separators are their own flex items, which buys it three times
   the room the gap alone suggests. */
/* Anchored to the link's vertical centre at a fixed height rather than by a symmetric inset. An
   inset has to be tuned against the line box, and `--mkt-font-size-control` steps 17px -> 16px at
   1260 — which silently took these overlays to 47px on exactly the widths the composed layout runs
   at, while they still measured 48 at 768 where they were checked. Height set here cannot drift
   with a font step. The textlink needs it for a second reason: its `::before` containing block is
   the padding box, which excludes its 1px underline, so an inset tuned against the visible line is
   always a pixel short. */
.rd-mkt-textlink::before,
.rd-mkt-nav__link::before {
  top: 50%;
  right: -8px;
  bottom: auto;
  left: -8px;
  height: 48px;
  transform: translateY(-50%);
}


.rd-mkt-nav__link,
.rd-mkt-footer__links a {
  position: relative;
}

/* Biased upward rather than centred on the text line. The footer leaves 16px below this row, so a
   centred 48px box (-17px each side) reaches 1px past the page's last pixel and grows the document
   — which the visual baselines caught as a 1px height change at every stacked width. The target is
   still 48px tall; it just sits 6px higher, over footer padding that holds nothing else.

   The horizontal reach is 8.25px, not the half-a-gap this file's shared rule above assumes. The
   budget here is not the 8px `gap`: a `·` separator sits between each pair as its own flex item,
   so adjacent links are 23.9px apart and each may take 11.9px before two overlays touch. At the
   old ±4px the narrowest label, "Blog", made a 39.5px-wide target — under the 44px floor and well
   under the 48 this file claims. 8.25px brings it to exactly 48 and still leaves 3.6px of slack. */
.rd-mkt-footer__links a::before {
  inset: -20px -8.25px -14px;
}

/* The edge comes up to full strength and the ink does not move. `--n-accent-hover` is the step a
   filled control darkens its *background* to, and as ink in dark it is #833719 — 2.27:1 on the page
   and worse over the backdrop. Holding the accent text step and strengthening the underline is the
   same gesture `.rd-prose a:hover` makes in components.css, which thickens its line rather than
   recolouring it. */
@media (hover: hover) {
  .rd-mkt-textlink:hover {
    border-bottom-color: var(--mkt-accent-ink);
    text-decoration: none;
  }
}

/* Wraps between the controls rather than inside one. Without it the row stayed a single line and
   crushed the textlink instead: 136px at 390 and 66px at 320, which broke "Watch the demo ↗" over
   two and three lines with the arrow orphaned and the border-bottom underlining the empty space
   beside it. 360 is the page's own stated minimum, so this was the hero, broken, at the floor. */
.rd-mkt-ctas {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-5);
  align-items: center;
  margin-top: var(--space-5);
}

/* ------------------------------------------------------------------- Nav ---- */

/* The bar sticks, so it is the page's standing offer: the stage runs 3,706px with the hero's
   controls gone, and a bar that scrolled away left that stretch with nothing to click.

   At rest it is read over the backdrop alone and carries no ground — a flat stripe of sand-1 laid
   across a tinted field is a bar that looks forgotten rather than drawn. The ground arrives only
   with `data-stuck`, once real content is passing underneath and the bar has to separate from it.

   `z-index` is stacking as well as position here: the backdrop is a pseudo-element of the same page
   box, and without this the bar would paint under it. */
.rd-mkt-nav {
  position: sticky;
  top: 0;
  /* Held, not derived: the CTA inside it is the tallest thing in the bar, so letting the box follow
     its content means the whole page jumps 24px every time the offer retires and comes back. */
  min-height: var(--mkt-nav-h);
  z-index: var(--z-sticky);
  transition: background-color var(--mkt-duration-swap) var(--ease-out);
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: var(--space-2) var(--mkt-gutter);
}

/* `site-nav.js` sets this once the sentinel above the bar has left the viewport. Glass rather than
   a solid fill so the one ground reads over both the backdrop and the page. */
.rd-mkt-nav[data-stuck] {
  background-color: var(--mkt-nav-ground);
  backdrop-filter: blur(12px);
  box-shadow: inset 0 -1px 0 0 var(--n-border-subtle);
}

/* Set by `site-nav.js` while any usable primary control is on screen. Hidden rather than faded:
   the bar has three items and reserving 155px for an invisible one reads as a hole for most of the
   page's length. */
.rd-mkt-nav[data-offer-elsewhere] .rd-mkt-nav__actions .rd-mkt-cta--primary {
  display: none;
}

/* The thing that leaves the viewport, so the bar's stuck state is an intersection rather than a
   scroll listener on the page's hottest path.

   Absolutely positioned so it costs no layout, and given real area because a zero-area box is a
   degenerate IntersectionObserver target that browsers disagree about. */
.rd-mkt-nav-sentinel {
  position: absolute;
  top: 0;
  width: 1px;
  height: 1px;
}

.rd-mkt-nav__brand {
  position: relative;
  display: flex;
  align-items: center;
  gap: var(--space-2);
  color: var(--n-text);
  text-decoration: none;
}

.rd-mkt-nav__mark {
  display: inline-flex;
  align-items: center;
}

/* The wordmark is set in the editorial face, at a size of its own: it is a mark, not a heading,
   so it does not step with the display scale. */
.rd-mkt-nav__word {
  font: 500 var(--mkt-font-size-wordmark) var(--font-family-serif);
  letter-spacing: -0.01em;
}

.rd-mkt-nav__links,
.rd-mkt-nav__actions {
  display: flex;
  align-items: center;
  gap: var(--space-5);
}

.rd-mkt-nav__actions {
  gap: var(--space-4);
}

/* Full-strength ink, not the muted step. For most of the first viewport the bar's ground is the
   hero backdrop rather than the page, and `--n-text-muted` is the neutral ramp's text step — it
   clears AA on the three neutral surfaces with little to spare, which a tinted ground spends.
   Measured against the backdrop's warmest band it lands at 4.18:1. */
.rd-mkt-nav__link {
  font: 400 var(--mkt-font-size-control) var(--font-family-sans);
  color: var(--n-text);
  text-decoration: none;
}

@media (hover: hover) {
  .rd-mkt-nav__link:hover {
    color: var(--n-accent-text);
  }
}

/* "You are here", spelled with a rule under the word rather than by demoting every other link.
   The bar is shared, and on the landing page its links are read over the backdrop — where
   `--n-text-muted` measures 4.18:1, which is the whole reason `.rd-mkt-nav__link` holds full ink.
   `text-decoration` rather than a border: the bar centres its items, and 3px of extra box on one
   of them lifts that label off the line the others sit on. */
.rd-mkt-nav__link[aria-current="page"],
.rd-mkt-nav__panel-link[aria-current="page"] {
  text-decoration: underline;
  text-decoration-color: var(--mkt-accent-ink);
  text-decoration-thickness: 1px;
  text-underline-offset: 6px;
}

/* Below 640 the links and the sign-in fold into the panel behind the menu button, which is the
   only nav control left in the bar. The button is a full 48px box around a 20px glyph — The 48
   Rule, and the reason the bar's own padding is only `--space-2`. */
.rd-mkt-nav__menu {
  display: none;
  width: 48px;
  height: 48px;
  align-items: center;
  justify-content: center;
  padding: 0;
  border: none;
  background: none;
  color: var(--n-text);
  cursor: pointer;
}

.rd-mkt-nav__panel {
  display: none;
  position: absolute;
  left: 0;
  right: 0;
  top: 100%;
  background: var(--n-bg);
  border-bottom: 1px solid var(--n-border);
  box-shadow: var(--n-shadow-md);
}

/* The panel's own offer, off by default. The bar carries the standing offer at every width here,
   and a second live copy inside the fold would put two on one phone screen. A surface that quiets
   the bar's CTA below 640 turns this on instead, so the offer stays one tap from every scroll
   position rather than disappearing. */
.rd-mkt-nav__panel-cta {
  display: none;
  padding: var(--space-4) var(--space-5);
}

.rd-mkt-nav__panel-link {
  display: flex;
  align-items: center;
  min-height: 48px;
  padding: 0 var(--space-5);
  font: 400 var(--mkt-font-size-control) var(--font-family-sans);
  color: var(--n-text);
  text-decoration: none;
  border-bottom: 1px solid var(--n-border-subtle);
}

@media (max-width: 639px) {
  .rd-mkt-nav__links {
    display: none;
  }

  /* The CTA stays in the bar; only the sign-in link beside it folds, and the panel and footer both
     still carry that. A standing offer is the whole reason the bar sticks, and a bar that carries
     none on a phone leaves the stacked layout with the dead stretch the composition just lost. */
  .rd-mkt-nav__actions .rd-mkt-nav__link {
    display: none;
  }

  /* Revealed by the script that makes it work. Without scripting the panel can never open, so a
     rendered button here is a focus stop that does nothing when pressed — `site-nav.js` sets
     `data-nav-enhanced` on the bar, and this is what that attribute buys. Every link the panel
     holds is repeated in the footer, so the no-JS path loses no navigation. */
  [data-nav-enhanced] .rd-mkt-nav__menu {
    display: inline-flex;
    /* Never the item that gives: it is a 48px target, and flex was shrinking it to 20px at 320
       once the bar had to hold the CTA as well. */
    flex: none;
  }

  /* Below 380 the bar cannot hold the wordmark, the CTA and the button with any air between them —
     measured with the rule off: the gaps are 10px at 380, 5px at 370 and 0 from 360 down, where the
     wordmark and the CTA touch. The mark alone still identifies the page, and the standing offer is
     what the bar became sticky for. */
  @media (max-width: 379px) {
    .rd-mkt-nav__word {
      display: none;
    }
  }

  /* Scoped to the width its toggle exists at. Unscoped, crossing 640 with the panel open drew the
     whole nav twice — once in the bar, once in a panel covering the hero — with the button that
     dismisses it display:none. `initNav` closes it on the same boundary so `aria-expanded` cannot
     be left true on a button nobody can see. */
  .rd-mkt-nav__panel[data-state="open"] {
    display: block;
  }
}

/* The bar with a ground of its own, for the painting shell (auth, the Stripe return pages, the
   error pages). Static rather than sticky: those pages are one viewport tall and have nothing to
   scroll past, so the bar sits in flow on a solid `--sand-1` with the hairline the stuck state
   draws — the shell's deliberate departure from the wash-and-glass composition above. A right-hand
   label (a status code, a page name) rides in `.rd-mkt-nav__label`, in the MonoLabel voice. */
.rd-mkt-nav--solid {
  position: static;
  background: var(--sand-1);
  box-shadow: inset 0 -1px 0 0 var(--n-border-subtle);
}

.rd-mkt-nav__label {
  font-family: var(--font-family-mono);
  font-size: var(--font-size-caption);
  font-weight: 500;
  letter-spacing: 0.06em;
  color: var(--n-text);
  white-space: nowrap;
}

/* A block CTA that is a button rather than a link can be held — the resend and retry buttons
   during their cooldown. Dimmed, not recoloured, so it reads as the same control waiting. */
.rd-mkt-cta:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

/* ---------------------------------------------------------------- Claims ----

   The problem / belief / answer block. A stack of hairline-topped rows on the stacked layouts;
   the desktop composition runs the same markup inside the pinned track. */
.rd-mkt-claims {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  margin-top: var(--space-5);
}

.rd-mkt-claim {
  border-top: 1px solid var(--n-border-subtle);
  padding-top: var(--space-3);
}

.rd-mkt-claim .rd-mkt-read {
  margin-top: var(--space-2);
}

/* ----------------------------------------------------------------- Phase ----

   One of the three phases under "How it works": a heading, its prose, and a picture of the
   product in that phase. */
.rd-mkt-phase {
  margin-top: var(--space-7);
}

.rd-mkt-phase__copy {
  margin-bottom: var(--space-3);
}

.rd-mkt-phase .rd-mkt-read {
  margin-top: var(--space-3);
}

.rd-mkt-phase .rd-mkt-subtitle + .rd-mkt-read {
  margin-top: 0;
}

/* The picture of the phase, below the prose that describes it. */
.rd-mkt-phase .rd-mkt-figure {
  margin-top: var(--space-5);
}

/* ---------------------------------------------------------------- Figure ----

   A picture of the product, drawn at its own fixed width and scaled to whatever column it lands
   in. `static/js/landing_page.js` measures the drawing and sets both the scale and this box's height,
   because a scaled child no longer contributes its painted size to layout.

   The aspect-ratio and transform are the same fit in CSS, so the box holds its fitted height
   before the script runs — and entirely without it. The drawings' sizes are pinned by this
   stylesheet (1080×708 for the session, 1000×655 for a screenshot), which is what lets static
   ratios stand in for the measurement. A browser that cannot divide lengths in calc() drops the
   transform and shows an unscaled crop inside the same box. */
.rd-mkt-figure {
  position: relative;
  width: 100%;
  overflow: hidden;
  container-type: inline-size;
}

/* Both scoped off hoisted panes: the stage sizes those through `inset: 0`, and absolute
   positioning resolves an aspect-ratio in preference to the bottom inset. The cap is for boxes
   wider than their drawing, where the ratio would letterbox below it. */
.rd-mkt-figure:not([data-hoisted]) {
  aspect-ratio: 1000 / 655;
  max-height: 655px;
}

.rd-mkt-figure > .rd-mkt-app {
  position: absolute;
  left: 0;
  top: 0;
  transform-origin: top left;
  transform: scale(min(1, calc(100cqw / 1000px)));
}

.rd-mkt-figure--center:not([data-hoisted]) {
  aspect-ratio: 1080 / 708;
  max-height: 708px;
}

.rd-mkt-figure--center > .rd-mkt-app {
  left: 50%;
  transform-origin: top center;
  margin-left: -540px;
  transform: scale(min(1, calc(100cqw / 1080px)));
}

/* ----------------------------------------------------------------- Price ---- */

/* Three cards, one layout at every width. This replaced a four-column comparison grid, which could
   not survive a narrow page: each of its rows was a grid of its own, so the widest row — the one
   carrying the three buttons — stretched alone while the rest stayed put, and `overflow-x: clip`
   ate the overrun rather than producing a scrollbar. Cards reflow by stacking, which needs no fit
   line and no second rendering of the same three plans. */
.rd-mkt-price {
  display: grid;
  gap: var(--space-5);
  align-items: stretch;
  margin-top: var(--space-5);
}

/* 1024 is --breakpoint-desktop. One column below it, three above, and never two — the band's whole
   argument is that the three plans are read across in one line, and two columns destroy that while
   leaving the third card alone beside half a row of empty page. An intrinsic `auto-fit` grid was
   tried first and is what produced that orphan, between 780 and 1151.

   Three columns are clean down to a 293px card, measured: the widest pair wants 347px laid out with
   no wrapping anywhere, but the value breaks internally long before the pair stops fitting. So what
   fails below 1024 is the page's room for three, not the card's room for its content. */
@media (min-width: 1024px) {
  .rd-mkt-price {
    grid-template-columns: repeat(3, 1fr);
  }
}

.rd-mkt-price__card {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  padding: var(--space-5);
  border: 1px solid var(--n-border);
  border-radius: var(--radius-sm);
}

/* The recommended plan. A full-strength edge rather than a ribbon or a tint: the band is already a
   row of bordered boxes, so the weight of the border is the one difference that reads without
   adding a third thing to the composition. */
.rd-mkt-price__card--lead {
  border-color: var(--n-text);
}

.rd-mkt-price__plan {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  margin: 0;
  font: 500 var(--mkt-font-size-plan) var(--font-family-sans);
}

.rd-mkt-price__figure {
  font: 500 var(--mkt-font-size-figure) / 1 var(--font-family-sans);
  letter-spacing: -0.02em;
}

.rd-mkt-price__unit {
  font: 400 var(--mkt-font-size-annotation) var(--font-family-sans);
  color: var(--n-text-muted);
}

/* The Badge pending recipe — gold 3/6/11 — spelled out here because marketing carries no React.
   The step-3 fill is what makes gold-11 legal; do not lift the fill and keep the text step. */
.rd-mkt-price__soon {
  font: 500 var(--mkt-font-size-badge) var(--font-family-mono);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--gold-11);
  background: var(--gold-3);
  border: 1px solid var(--gold-6);
  border-radius: var(--radius-none);
  padding: 2px 7px;
}

.rd-mkt-price__card-amount {
  display: flex;
  align-items: baseline;
  gap: var(--space-1);
}

.rd-mkt-price__card-rows {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  margin: 0;
  padding-top: var(--space-4);
  border-top: 1px solid var(--n-border-subtle);
}

/* The `<div>` between `<dl>` and its pairs is what lets a term and its value be one flex line. */
.rd-mkt-price__card-row {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: var(--space-4);
}

.rd-mkt-price__card-value {
  margin: 0;
  font: 400 var(--mkt-font-size-cell) var(--font-family-sans);
  text-align: right;
}

/* A term this plan does not carry. Muted rather than omitted: every card states the same four in
   the same order, which is what lets the three be read across. */
.rd-mkt-price__card-value--none {
  color: var(--n-text-muted);
}

/* The cards are stretched to a common height, and `auto` is what keeps the buttons on one line
   once their plans stop carrying the same number of rows. Today they all carry four, so this
   changes nothing on screen — which is why it is a declaration and not a test. */
.rd-mkt-price__card-action {
  margin-top: auto;
  padding-top: var(--space-2);
}

/* ----------------------------------------------------------------- Usage ---- */

/* Rates are not a fourth plan, and reading as one was the problem: set on the page ground under a
   sentence-long heading, the block looked like the table continuing. Recessed and labelled, it
   reads as the footnote to all three plans that it is. The bottom padding is short because the
   last row keeps no rule under it.

   `--sand-3`, not `--n-bg-alt`: that token is the recess from `--n-bg`, and this page's ground is
   `--sand-1` instead, which in dark is the colour `--n-bg-alt` resolves to — an invisible panel
   drawn only by its border. Step 3 is one off the ground in both themes; that it recedes in light
   and lifts in dark is how the ramp is meant to read. */
.rd-mkt-usage {
  margin-top: var(--space-5);
  padding: var(--space-5) var(--space-5) var(--space-2);
  background: var(--sand-3);
  border: 1px solid var(--n-border-subtle);
  border-radius: var(--radius-sm);
}

.rd-mkt-usage__head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  column-gap: var(--space-4);
  row-gap: var(--space-1);
  padding-bottom: var(--space-3);
  border-bottom: 1px solid var(--n-border);
}

.rd-mkt-usage__eyebrow {
  margin: 0;
  font: 500 var(--font-size-caption) var(--font-family-mono);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--n-text-muted);
}

/* Slanted, not italic, for the reason `.rd-mkt-app em` is: Newsreader-Italic is a 240KB face and
   this is the only italic on the page — one 39-character line. It is also the one face no <link
   rel=preload> covers, so requesting it puts 26% of the page's weight at the tail of the waterfall
   for a gloss. `display: inline-block` is what makes the transform apply. */
.rd-mkt-usage__note {
  margin: 0;
  display: inline-block;
  font: 400 var(--mkt-font-size-cell) var(--font-family-serif);
  transform: skewX(-9deg);
  color: var(--n-text-muted);
}

/* Uniform by construction at every width, which took two goes to get right. A three-column grid
   everywhere leaves the description 162px at 834 and breaks "Billed per second." beside the rate.
   A flex line everywhere is worse in a subtler way: flex decides each row's wrap from that row's
   own max-content, and the three rates are 280px, 112px and 72px — so from 834 to 1100 the compute
   row dropped its rate to a line of its own while the two either side kept theirs on the line. One
   bordered panel, three rows, two shapes, which reads as a bug rather than as a reflow.

   So: the grid from `--breakpoint-desktop`, where the description still clears 350px, and below it
   the rate takes a line of its own on every row rather than on whichever row happens to be widest.
   No fit line of its own, and no arrangement where two rows disagree. */
.rd-mkt-usage__row {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  column-gap: var(--space-4);
  row-gap: var(--space-1);
  padding: var(--space-3) 0;
  border-bottom: 1px solid var(--n-border-subtle);
}

.rd-mkt-usage__row:last-child {
  border-bottom: none;
}

.rd-mkt-usage__label {
  font: 500 var(--mkt-font-size-cell) var(--font-family-sans);
  white-space: nowrap;
}

/* The rate breaks between its figures, never inside one. Held rigid it was the widest thing in the
   panel at ~280px, and since each row decides its own wrap, the compute row dropped its rate to a
   line of its own from 834 to 1100 while the two either side of it kept theirs on the line — one
   bordered panel, three rows, two different shapes. Each figure keeps its own `nowrap` below. */
.rd-mkt-usage__rate {
  flex-basis: 100%;
  font: 500 var(--mkt-font-size-rate) / 1.5 var(--font-family-mono);
}

.rd-mkt-usage__rate > span {
  white-space: nowrap;
}

/* Two rates on one line need the gap BETWEEN them to beat the gaps inside them, or the run reads as
   one undifferentiated string. It was the other way round: two spaces inside each figure (around
   the slash) against one between them, so the eye grouped `hr $0.0162` more tightly than either
   rate held together. The slash now binds its own pair and this is the only space in the run. */
.rd-mkt-usage__rate > span + span {
  margin-left: var(--space-5);
}

.rd-mkt-usage__row .rd-mkt-read {
  /* flex-shrink 1, not 0: the 440px basis is wider than this column ever gets on a phone — at
     320px, the width WCAG 1.4.10 asks a page to reflow at, it measures 246px — and a rigid basis
     would clip unrecoverably behind the page's overflow-x: clip, which eats the overrun without
     leaving a scrollbar to reveal it. Ignored in the grid below, which is what `min-width` is
     still doing there. */
  flex: 1 1 440px;
  min-width: 0;
}

@media (max-width: 639px) {
  .rd-mkt-usage {
    padding: var(--space-4) var(--space-4) var(--space-2);
  }

  .rd-mkt-usage__row .rd-mkt-read {
    font-size: var(--mkt-font-size-cell);
    line-height: 1.55;
  }
}

/* 1024 is --breakpoint-desktop. */
@media (min-width: 1024px) {
  .rd-mkt-usage__row {
    display: grid;
    grid-template-columns: 200px 1fr auto;
    gap: var(--space-5);
  }

  .rd-mkt-usage__rate {
    text-align: right;
  }
}

/* 1260 is --mkt-compose. */
@media (min-width: 1260px) {
  .rd-mkt-usage__row {
    gap: var(--space-6);
  }
}

/* 1260 is --mkt-compose. The composed band carries a step more air between the cards and above
   them, as every other section of the page does at this width. */
@media (min-width: 1260px) {
  .rd-mkt-price {
    gap: var(--space-6);
    margin-top: var(--space-6);
  }
}

/* ---------------------------------------------------------------- Footer ---- */

.rd-mkt-footer {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  align-items: center;
  padding: var(--space-4) var(--mkt-gutter);
  border-top: 1px solid var(--n-border-subtle);
  font: 400 var(--mkt-font-size-micro) var(--font-family-mono);
  letter-spacing: 0.08em;
  /* --n-text-muted, not the --sand-9 the artboard drew: a step-9 grey is a solid-fill step, and at
     this size it lands at 3.3:1 against the page. Step 11 is the ramp's text step and clears AA. */
  color: var(--n-text-muted);
}

.rd-mkt-footer__links {
  display: flex;
  gap: var(--space-2);
  /* The caps live here rather than in the source text so the brand line beside this row can render
     lowercase. Note what this does NOT buy: `text-transform` is applied before the accessible name
     is computed, so the browser's own accessibility tree reports PRICING / BLOG / SIGN IN / GET
     STARTED in Chromium, Firefox and WebKit alike — verified. An earlier version of this comment
     claimed the CSS placement kept the announced name sentence-case. It does not. The names are
     ordinary words rather than acronyms, so they are read as words and this is left as it is. */
  text-transform: uppercase;
}

.rd-mkt-footer__links a {
  color: inherit;
  text-decoration: none;
}

@media (hover: hover) {
  .rd-mkt-footer__links a:hover {
    color: var(--n-accent-text);
  }
}

@media (min-width: 640px) {
  .rd-mkt-footer {
    flex-direction: row;
    justify-content: space-between;
  }
}

@media (min-width: 1260px) {
  .rd-mkt-footer {
    padding: var(--space-5) var(--mkt-gutter);
  }
}

/* ------------------------------------------------- Desktop scrolly stage ----

   At 1260 and above the hero media docks to the left and a column of text scrolls past it. Below that —
   and with scripting off at ANY width — the same markup is a plain stacked page: the wrappers go
   `display: contents`, the hero and the four sections become ordinary bands, and each screenshot
   sits inside the phase it illustrates.

   The composition is gated on `[data-compose="stage"]`, which `static/js/landing_page.js` sets, and
   not on the media query alone. Every rule below needs geometry the script computes each frame, so
   a media query that fired without it would leave a sticky viewport with nothing positioned in it.

   Motion here is scroll-linked: the dock and the track's travel are functions of scroll position,
   not of time, so they take no `--duration-*` token — the marketing guide's exemption. The track's
   ENTRANCE is time-based and does take one. Under reduced motion the entrance collapses; the
   scroll-driven positions do not, because a position frozen mid-progress would hide content. */

.rd-mkt-stage,
.rd-mkt-stage__sticky,
.rd-mkt-stage__track,
.rd-mkt-stage__track-inner {
  display: contents;
}

.rd-mkt-stage__media {
  display: none;
}

.rd-mkt-stage__hero {
  padding: 40px var(--mkt-gutter) 0;
  text-align: center;
}

.rd-mkt-stage__hero .rd-mkt-lead {
  max-width: 640px;
  margin: var(--space-4) auto 0;
}

.rd-mkt-stage__hero .rd-mkt-ctas {
  justify-content: center;
}

/* In the stack the drawing sits between the standfirst and the CTAs and needs its own air. In the
   composed stage it has left the hero entirely, so this never applies there. */
.rd-mkt-stage__hero .rd-mkt-figure {
  margin-top: var(--space-6);
}

.rd-mkt-stage__section {
  padding: var(--mkt-band) var(--mkt-gutter) 0;
}

@media (min-width: 640px) {
  .rd-mkt-stage__hero {
    padding-top: var(--space-8);
  }
}

@media (min-width: 1260px) {
  /* Height is the script's: it is the fold plus the scroll the track sections need to pass the
     docked card at `--mkt-track-gap` apart, which is a measurement of the track, not a multiple of
     the viewport. `setComposed` and the first frame run in the same task, so this is never painted
     without one. */
  [data-compose="stage"] .rd-mkt-stage {
    display: block;
    position: relative;
  }

  [data-compose="stage"] .rd-mkt-stage__sticky {
    display: block;
    position: sticky;
    top: 0;
    height: 100vh;
    overflow: hidden;
  }

  /* Absolutely positioned and translated up by scroll progress: the hero text leaves upward while
     the media card below it docks left. */
  [data-compose="stage"] .rd-mkt-stage__hero {
    position: absolute;
    top: 52.5px;
    left: 0;
    right: 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    padding: 0 var(--mkt-gutter);
  }

  [data-compose="stage"] .rd-mkt-stage__hero .rd-mkt-lead {
    width: 480px;
    max-width: 100%;
  }

  [data-compose="stage"] .rd-mkt-stage__hero .rd-mkt-ctas {
    gap: var(--space-6);
    margin-top: 21px;
  }

  /* The frame the media lives in. Left, top, width and height are driven by the script from scroll
     progress: it opens full-bleed under the hero and lands as the docked card on the left. */
  /* The travelling CTA. `top` and `width` follow the docked card from the frame loop, because the
     card's box is JS-driven; both are written through the same compare-then-write guard, and the
     card's geometry is constant once docked so neither is rewritten while scrolling. Hidden by
     default so the stacked layout — which has the hero's own pair in flow — never sees it. */
  [data-compose="stage"] .rd-mkt-stage__media {
    display: block;
    position: absolute;
    left: 30px;
    border-radius: var(--radius-lg);
    overflow: hidden;
  }

  /* Each pane is one state of the docked card — the live session, then the three screenshots. The
     script moves them here from their stacked homes and shows one at a time.

     The swap cross-dissolves rather than cuts. `display` is kept in the transition with
     `allow-discrete` so the outgoing pane holds its box until its opacity reaches zero, and
     `@starting-style` gives the incoming one somewhere to fade up from — without it a pane
     arriving from `display: none` has no previous opacity to animate and snaps in. This keeps the
     idle cost of `display: none`: three of the four panes are neither laid out nor painted except
     during the 0.32s a swap is running, which happens three times across the whole pinned scroll.

     Chromium and WebKit cross-dissolve — measured with both panes co-visible at intermediate
     opacities. Firefox drops the outgoing pane immediately and only fades the incoming one up,
     over the page ground, because the media box is transparent. That reads as a fade rather than a
     flash and is left as the degradation: making all three identical means giving up `display:
     none` for `visibility`, which lays out all four app subtrees for the whole scroll to buy one
     engine a softer 320ms. */
  [data-compose="stage"] .rd-mkt-stage__media > [data-shot] {
    position: absolute;
    inset: 0;
    padding: 0;
  }

  [data-compose="stage"] .rd-mkt-stage__media[data-swap="armed"] > [data-shot] {
    opacity: 1;
    transition:
      opacity var(--mkt-duration-swap) var(--ease-out),
      display var(--mkt-duration-swap) allow-discrete;
  }

  /* Beats `.rd-mkt-page [hidden]`'s `display: none !important` on opacity only — the `!important`
     still decides `display`, and `allow-discrete` still defers when it takes effect. */
  [data-compose="stage"] .rd-mkt-stage__media[data-swap="armed"] > [data-shot][hidden] {
    opacity: 0;
  }

  @starting-style {
    [data-compose="stage"] .rd-mkt-stage__media[data-swap="armed"] > [data-shot] {
      opacity: 0;
    }
  }

  [data-compose="stage"] .rd-mkt-stage__media .rd-mkt-app {
    position: absolute;
    left: 50%;
    transform-origin: top center;
  }

  /* The pinned text column. Its right edge lands at 1200px inside the 1260px shell — the
     measurement `--mkt-compose` is named for. */
  [data-compose="stage"] .rd-mkt-stage__track {
    display: block;
    position: absolute;
    left: 795px;
    width: 405px;
    top: 0;
    bottom: 0;
    overflow: hidden;
    opacity: 0;
    transform: translateY(27px);
  }

  [data-compose="stage"] .rd-mkt-stage__track[data-entered="true"] {
    opacity: 1;
    transform: translateY(0);
    transition:
      opacity var(--mkt-duration-entrance) var(--ease-out),
      transform var(--mkt-duration-entrance) var(--ease-out);
  }

  [data-compose="stage"] .rd-mkt-stage__track-inner {
    display: flex;
    flex-direction: column;
    gap: var(--mkt-track-gap);
  }

  [data-compose="stage"] .rd-mkt-stage__section {
    flex: none;
    padding: 0;
  }

  /* Pulled up over the stage's empty tail so pricing sits one track gap below the last section
     rather than a blank viewport below it. The offset depends on viewport height, so the script
     sets it. */
  [data-compose="stage"] .rd-mkt-stage-after {
    position: relative;
    z-index: 1;
  }
}

/* ------------------------------------------------- A drawing of the product ----

   The hero's looping session and the three phase screenshots are the same drawing in different
   states: a picture of the app, not the app. It is decoration everywhere it appears, named by its figure's `role="img"` rather than read,
   which is what lets it use the product's own vocabulary without owing the page an accessible
   name for every simulated control.

   Two things it deliberately does NOT do. It does not import the design system's component sheet
   (see the header: that sheet reaches the browser through the app bundle, and the app bundle is
   one stylesheet). And it does not invent surfaces: every border, ground and ink below is the
   recipe `design-system/src/components.css` draws for the same part, cited where it is not
   obvious. `website/tests/test_marketing_layer.py` pins the three Collapsible skins against that
   file so the copy cannot drift silently.

   Sizes inside the drawing are its own, not the page's: it is authored at a fixed width (1080 for
   the live session, 1000 for a screenshot) and then scaled to whatever column it lands in, so a
   responsive page token here would be scaled twice. Off-scale paddings are the same story — they
   are pixels in a picture, not spacing in a layout. */
:root {
  --mkt-app-font-micro: 10px; /* a screenshot's mono labels */
  --mkt-app-font-caption: 11.1px; /* = --font-size-caption at a 16px root */
  --mkt-app-font-small: 13.3px; /* = --font-size-small at a 16px root */
  --mkt-app-font-prose: 16px; /* = --font-size-body at a 16px root */
  --mkt-app-font-mono: 10.5px; /* a screenshot's tool rows */
  --mkt-app-font-code: 11.5px; /* a screenshot's inline code */
  --mkt-app-font-chrome: 12px; /* a screenshot's block headers */
  --mkt-app-font-body: 13px; /* a screenshot's prose */
  --mkt-app-font-h3: 18px; /* a screenshot's document subheading */
  --mkt-app-font-h2: 24px; /* a screenshot's document heading */
}

.rd-mkt-app {
  position: relative;
  /* Pinned, not inherited: in the stacked layouts the hero pane renders inside the centered
     hero band, and a drawing of the left-aligned product must not center its conversation
     with the marketing text around it. (Composed mode masked this by hoisting the pane out.) */
  text-align: left;
  width: 1080px;
  background: var(--n-bg);
  border: 1px solid var(--n-border-subtle);
  border-radius: var(--radius-lg);
  box-shadow: var(--n-shadow-lg);
  overflow: hidden;
}

.rd-mkt-app--shot {
  width: 1000px;
  border-color: var(--n-border);
  border-radius: var(--radius-sm);
}

.rd-mkt-app__body {
  display: flex;
  height: 706px;
}

.rd-mkt-app--shot .rd-mkt-app__body {
  height: 653px;
}

/* The overlay rail, at its collapsed width. */
.rd-mkt-app__rail {
  flex: none;
  width: 50px;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  align-items: center;
  padding: var(--space-4) 0 20px;
  background: var(--n-surface-raised);
  border-right: 1px solid var(--n-border-subtle);
}

.rd-mkt-app--shot .rd-mkt-app__rail {
  width: 44px;
  padding: var(--space-3) 0;
}

.rd-mkt-app__rail-group {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-4);
}

.rd-mkt-app__rail-group--foot {
  color: var(--n-text-muted);
}

.rd-mkt-app__glyph {
  display: inline-flex;
  color: var(--n-text-muted);
}

.rd-mkt-app__avatar {
  width: 24px;
  height: 24px;
  border-radius: var(--radius-full);
  background: var(--n-accent);
  color: var(--n-accent-contrast);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font: 600 var(--mkt-app-font-caption) var(--font-family-sans);
}

.rd-mkt-app--shot .rd-mkt-app__avatar {
  width: 20px;
  height: 20px;
  font-size: var(--mkt-app-font-micro);
}

.rd-mkt-app__main {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
}

.rd-mkt-app__head {
  flex: none;
  display: flex;
  align-items: center;
  gap: var(--space-3);
  height: 40px;
  padding: 0 var(--space-5);
  border-bottom: 1px solid var(--n-border-subtle);
}

/* A screenshot's header spans two panes, so it stretches rather than centres and carries the
   pane divider itself. */
.rd-mkt-app__head--split {
  align-items: stretch;
  gap: 0;
  padding: 0;
}

.rd-mkt-app__head-pane {
  flex: none;
  width: 58%;
  box-sizing: border-box;
  min-width: 0;
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: 0 var(--space-4);
  border-right: 1px solid var(--n-border-subtle);
}

.rd-mkt-app__head-pane--chat {
  flex: 1;
  width: auto;
  border-right: none;
}

.rd-mkt-app__crumb {
  display: flex;
  flex: 1;
  align-items: baseline;
  gap: var(--space-3);
  min-width: 0;
}

.rd-mkt-app__crumb-root {
  font: 500 var(--mkt-app-font-caption) var(--font-family-mono);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--n-text-muted);
}

.rd-mkt-app__crumb-sep {
  color: var(--n-border);
  font-size: var(--mkt-app-font-caption);
}

.rd-mkt-app__crumb-title {
  font: 400 var(--mkt-app-font-small) var(--font-family-sans);
  color: var(--n-text);
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}

.rd-mkt-app--shot .rd-mkt-app__crumb-root,
.rd-mkt-app--shot .rd-mkt-app__crumb-sep {
  font-size: var(--mkt-app-font-micro);
  letter-spacing: 0.08em;
}

.rd-mkt-app--shot .rd-mkt-app__crumb-title {
  font-size: var(--mkt-app-font-chrome);
}

/* StatusIndicator's dot at its documented 6px, with the ramp step for the state. */
.rd-mkt-app__dot {
  flex: none;
  width: 6px;
  height: 6px;
  border-radius: var(--radius-full);
  background: var(--grass-9);
}

.rd-mkt-app__tabs {
  margin-left: auto;
  display: inline-flex;
  gap: var(--space-4);
  height: 100%;
}

.rd-mkt-app__tab {
  position: relative;
  height: 100%;
  display: inline-flex;
  align-items: center;
  font: 500 var(--mkt-app-font-micro) var(--font-family-mono);
  letter-spacing: 0.08em;
  color: var(--n-text);
}

/* Tabs' 2px indicator, sitting on the header's own bottom hairline. */
.rd-mkt-app__tab--current {
  color: var(--n-accent-text);
}

.rd-mkt-app__tab--current::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: -1px;
  height: 2px;
  background: var(--n-accent);
}

/* ---- The conversation ---- */

.rd-mkt-app__thread {
  flex: 1;
  min-height: 0;
  overflow: hidden;
  padding: 24px 24px 12px;
}

.rd-mkt-app__thread-inner {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  max-width: 600px;
  margin: 0 auto;
}

.rd-mkt-app__steps {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  min-width: 0;
}

.rd-mkt-app__turn {
  display: flex;
  gap: var(--space-3);
  min-width: 0;
}

/* The user's gutter: an accent edge, the one place in the thread that carries the brand solid. */
.rd-mkt-app__meta {
  flex: none;
  display: flex;
  flex-direction: column;
  gap: 2px;
  width: 64px;
  padding: 3px var(--space-2) 0 0;
  border-right: 1px solid var(--n-accent);
  text-align: right;
}

.rd-mkt-app--shot .rd-mkt-app__meta {
  width: 52px;
}

.rd-mkt-app__who {
  font: 500 var(--mkt-app-font-caption) var(--font-family-mono);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--n-accent-text);
}

/* Same substitution as the footer's: the artboard drew the timestamp in --sand-9, which is a fill
   step and fails AA as text. The drawing is decorative, but a contrast rule is a visual rule and
   axe checks it whatever the element is labelled. */
.rd-mkt-app__when {
  font: 400 var(--mkt-app-font-caption) var(--font-family-mono);
  color: var(--n-text-muted);
}

.rd-mkt-app--shot .rd-mkt-app__who,
.rd-mkt-app--shot .rd-mkt-app__when {
  font-size: var(--mkt-app-font-micro);
}

/* The Voice Rule: the conversation reads serif, user and agent turns alike. */
.rd-mkt-app__says {
  flex: 1;
  min-width: 0;
  font: 400 var(--mkt-app-font-prose) / var(--leading-loose) var(--font-family-serif);
  color: var(--n-text);
}

.rd-mkt-app__p {
  margin: 0;
  font: 400 var(--mkt-app-font-prose) / var(--leading-normal) var(--font-family-serif);
  color: var(--n-text);
}

/* Emphasis inside a drawing slants the roman face instead of requesting Newsreader-Italic: the
   true face is 240KB and this page's only italics are two words inside a drawing, which is read
   as one described image rather than as text.
   The app keeps the real italic for live prose; at drawing size the skew is indistinguishable. */
.rd-mkt-app em {
  font-style: normal;
  display: inline-block;
  transform: skewX(-9deg);
}

.rd-mkt-app__list {
  margin: 0;
  padding-left: var(--space-5);
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  font: 400 var(--mkt-app-font-prose) / var(--leading-normal) var(--font-family-serif);
  color: var(--n-text);
}

.rd-mkt-app--shot .rd-mkt-app__says,
.rd-mkt-app--shot .rd-mkt-app__p {
  font-size: var(--mkt-app-font-body);
  line-height: 1.6;
}

.rd-mkt-app__code {
  font: 400 var(--mkt-app-font-small) var(--font-family-mono);
  color: var(--n-code-text);
  background: color-mix(in srgb, var(--n-accent) 8%, transparent);
  padding: 1px 5px;
  border-radius: var(--radius-sm);
}

.rd-mkt-app--shot .rd-mkt-app__code {
  font-size: var(--mkt-app-font-code);
  padding: 1px 4px;
}

/* ---- Collapsible, three skins ----

   Copied from `.rd-collapsible*` in design-system/src/components.css, compact density. */

.rd-mkt-app__block {
  border-radius: var(--radius-md);
}

.rd-mkt-app__block--default {
  border: 1px solid var(--n-border);
}

.rd-mkt-app__block--thinking {
  background: color-mix(in srgb, var(--n-accent) 3%, transparent);
  border: 1px solid color-mix(in srgb, var(--n-accent) 12%, transparent);
}

.rd-mkt-app__block--subagent {
  border: 1px solid var(--n-border);
  border-left-width: 2px;
}

.rd-mkt-app__block-head {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  width: 100%;
  padding: var(--space-1) var(--space-3);
  color: var(--n-text);
  font-family: var(--font-family-sans);
  font-size: var(--mkt-app-font-small);
  line-height: var(--leading-normal);
  text-align: left;
}

.rd-mkt-app__block--thinking .rd-mkt-app__block-head {
  color: var(--n-accent-text);
}

.rd-mkt-app__block--subagent .rd-mkt-app__block-head {
  color: var(--n-text-muted);
}

.rd-mkt-app__block-chevron {
  flex: none;
  display: inline-flex;
}

/* The chevron takes no colour of its own — it reads the head's, which is how the design system
   draws it. The three skins whose head is NOT already muted mute it here: on a plain tool card the
   glyph is machinery beside a label that is content. */
.rd-mkt-app__block--default > .rd-mkt-app__block-head > .rd-mkt-app__block-chevron,
.rd-mkt-app__block--tool > .rd-mkt-app__block-head > .rd-mkt-app__block-chevron,
.rd-mkt-app__block--implementer > .rd-mkt-app__block-head > .rd-mkt-app__block-chevron {
  color: var(--n-text-muted);
}

/* A row, because a label may carry a kind beside a brief — the subagent card's "EXPLORE" and
   what it is exploring. A single text label is simply the only item in it. */
.rd-mkt-app__block-label {
  flex: 1;
  min-width: 0;
  display: flex;
  align-items: baseline;
  gap: var(--space-3);
}

/* Continuous, so deliberately outside the duration scale — the same exemption the spinners and the
   caret take. It is the only signal that a step is still running. */
.rd-mkt-app__block-label--streaming {
  animation: rd-mkt-text-pulse 1.5s ease-in-out infinite;
}

@keyframes rd-mkt-text-pulse {
  0%,
  100% {
    opacity: 1;
  }
  50% {
    opacity: 0.5;
  }
}

.rd-mkt-app__block-trailing {
  flex: none;
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  font-family: var(--font-family-mono);
  font-size: var(--mkt-app-font-caption);
  color: var(--n-text-muted);
}

.rd-mkt-app__block-body {
  padding: var(--space-3) var(--space-3) var(--space-3) var(--space-6);
}

.rd-mkt-app__block--default > .rd-mkt-app__block-body {
  border-top: 1px solid var(--n-border-subtle);
}

.rd-mkt-app__block[data-state="closed"] > .rd-mkt-app__block-body {
  display: none;
}

/* A screenshot's blocks are drawn at its own density, and its subagent card carries the
   implementer's 3px rail. */
.rd-mkt-app--shot .rd-mkt-app__block {
  border-radius: var(--radius-sm);
}

.rd-mkt-app--shot .rd-mkt-app__block-head {
  padding: 7px var(--space-3);
  font-size: var(--mkt-app-font-chrome);
  font-weight: 400;
}

.rd-mkt-app--shot .rd-mkt-app__block-body {
  padding: 0 var(--space-3) var(--space-2) 26px;
}

.rd-mkt-app--shot .rd-mkt-app__block--default {
  border-color: var(--n-border-subtle);
}

.rd-mkt-app--shot .rd-mkt-app__block--default > .rd-mkt-app__block-body {
  border-top: none;
}

.rd-mkt-app__block--tool {
  border: 1px solid var(--n-border-subtle);
}

/* The Collapsible subagent's rail carries the subagent's TYPE, and `components.css` solves the
   implementer's to --blue-11 at 2px. The artboard drew a 3px neutral one; the product's own recipe
   is both thinner and the one a visitor would recognise, so the drawing follows the product. */
.rd-mkt-app__block--implementer {
  border: 1px solid var(--n-border);
  border-left: 2px solid var(--blue-11);
}

.rd-mkt-app__block-kind {
  flex: none;
  font: 500 var(--mkt-app-font-micro) var(--font-family-mono);
  letter-spacing: 0.06em;
  color: var(--n-text);
}

.rd-mkt-app__block-brief {
  flex: 1;
  min-width: 0;
  font: 400 var(--mkt-app-font-small) var(--font-family-sans);
  color: var(--n-text-muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.rd-mkt-app--shot .rd-mkt-app__block-brief {
  font-size: var(--mkt-app-font-chrome);
}

/* ---- Tool rows ---- */

.rd-mkt-app__tool {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  min-height: 26px;
  font: 400 var(--mkt-app-font-caption) var(--font-family-mono);
  color: var(--n-text-muted);
}

.rd-mkt-app--shot .rd-mkt-app__tool {
  min-height: 24px;
  font-size: var(--mkt-app-font-mono);
}

.rd-mkt-app__tool-verb {
  flex: none;
  width: 44px;
  font-weight: 500;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--n-text);
}

.rd-mkt-app--shot .rd-mkt-app__tool-verb {
  width: 40px;
}

.rd-mkt-app__tool-target {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}

.rd-mkt-app__tool-mark {
  flex: none;
  display: inline-flex;
  align-items: center;
  color: var(--grass-11);
}

.rd-mkt-app__tool-ms {
  flex: none;
  width: 48px;
  text-align: right;
}

.rd-mkt-app--shot .rd-mkt-app__tool-ms {
  width: auto;
}

/* The two states a step's trailing label reports. The mono voice and the caption size come from
   `.rd-mkt-app__block-trailing`; these set only what the state changes. */
.rd-mkt-app__running {
  font-weight: 500;
  color: var(--gold-11);
}

.rd-mkt-app__completed {
  font-weight: 500;
  color: var(--grass-11);
}

/* The executing status, pulsing. Continuous, so outside the duration scale. */
.rd-mkt-app__pulse {
  display: inline-block;
  width: 6px;
  height: 6px;
  border-radius: var(--radius-full);
  background: var(--gold-9);
  color: var(--gold-9);
  animation: rd-mkt-pulse 2.2s ease-out infinite;
}

@keyframes rd-mkt-pulse {
  0% {
    box-shadow: 0 0 0 0 color-mix(in srgb, currentcolor 45%, transparent);
  }
  70% {
    box-shadow: 0 0 0 7px transparent;
  }
  100% {
    box-shadow: 0 0 0 0 transparent;
  }
}

/* ---- The composer ---- */

.rd-mkt-app__composer {
  flex: none;
  padding: 0 24px 16px;
}

/* Full-bleed against its column, so `--radius-none`: the field meets the edges it grounds to. */
.rd-mkt-app__composer-field {
  max-width: 600px;
  margin: 0 auto;
  background: var(--n-surface-raised);
  border: 1px solid var(--n-border);
  border-radius: var(--radius-none);
  padding: var(--space-3) var(--space-4);
  font: 400 var(--mkt-app-font-prose) var(--font-family-serif);
  color: var(--n-text-muted);
}

.rd-mkt-app__composer-field--busy {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  background: var(--n-bg-alt);
  border-color: var(--n-border-subtle);
  border-radius: var(--radius-sm);
}

.rd-mkt-app__composer-draft {
  color: var(--n-text);
}

.rd-mkt-app__composer-busy-label {
  flex: 1;
}

.rd-mkt-app__spinner {
  flex: none;
  display: inline-flex;
  color: var(--n-accent-text);
  animation: rd-mkt-spin 1s linear infinite;
}

@keyframes rd-mkt-spin {
  to {
    transform: rotate(360deg);
  }
}

/* Continuous, and a step function rather than a fade — a caret that eases is a glow. */
.rd-mkt-app__caret {
  display: inline-block;
  width: 2px;
  height: 14px;
  margin-left: 1px;
  vertical-align: -2px;
  background: var(--n-accent);
  animation: rd-mkt-caret 1s step-end infinite;
}

@keyframes rd-mkt-caret {
  0%,
  49% {
    opacity: 1;
  }
  50%,
  100% {
    opacity: 0;
  }
}

.rd-mkt-app--shot .rd-mkt-app__composer {
  padding: 0 var(--space-5) var(--space-5);
}

.rd-mkt-app--shot .rd-mkt-app__composer-field {
  max-width: none;
  border-radius: var(--radius-sm);
  font-size: var(--mkt-app-font-body);
}

/* ---- Two panes: a document beside the conversation ---- */

.rd-mkt-app__panes {
  flex: 1;
  min-height: 0;
  display: flex;
}

.rd-mkt-app__pane {
  flex: none;
  width: 58%;
  box-sizing: border-box;
  min-width: 0;
  position: relative;
  overflow: hidden;
  border-right: 1px solid var(--n-border-subtle);
  padding: var(--space-6) 44px;
}

.rd-mkt-app__pane--chat {
  flex: 1;
  width: auto;
  padding: 0;
  border-right: none;
  display: flex;
  flex-direction: column;
}

/* The document runs past the pane; it is cut with the page's own ground rather than a hard edge. */
.rd-mkt-app__fade {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 72px;
  background: linear-gradient(to bottom, transparent, var(--n-bg));
}

.rd-mkt-app__chat {
  flex: 1;
  min-height: 0;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  gap: var(--space-4);
  padding: var(--space-5);
}

.rd-mkt-app__doc-h2 {
  margin: 0;
  font: 500 var(--mkt-app-font-h2) var(--font-family-serif);
  color: var(--n-text);
}

.rd-mkt-app__doc-h3 {
  margin: var(--space-5) 0 0;
  font: 500 var(--mkt-app-font-h3) var(--font-family-serif);
  color: var(--n-text);
}

.rd-mkt-app__doc-h4 {
  margin: var(--space-4) 0 0;
  font: 600 var(--mkt-app-font-body) var(--font-family-sans);
  color: var(--n-text);
}

.rd-mkt-app__doc-p {
  margin: var(--space-3) 0 0;
  font: 400 var(--mkt-app-font-body) / 1.7 var(--font-family-serif);
  color: var(--n-text);
}

.rd-mkt-app__doc-list {
  margin: var(--space-3) 0 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

.rd-mkt-app__doc-item {
  display: flex;
  gap: var(--space-3);
  font: 400 var(--mkt-app-font-body) / 1.65 var(--font-family-serif);
  color: var(--n-text);
}

.rd-mkt-app__doc-item::before {
  content: "\2022";
  flex: none;
  padding-top: 1px;
}

.rd-mkt-app__doc-h2 + .rd-mkt-app__doc-h2 {
  margin-top: var(--space-6);
}

/* ---- The plan: a task graph, then the stack ---- */

/* Dashed, because the canvas is a work surface rather than a panel — it is the one place in the
   product where a border says "you can move things here". */
.rd-mkt-app__graph {
  position: relative;
  box-sizing: border-box;
  height: 268px;
  padding: var(--space-4);
  border: 1px dashed var(--n-border);
  overflow: hidden;
}

.rd-mkt-app__graph-canvas {
  position: relative;
  width: 432px;
  height: 228px;
}

.rd-mkt-app__graph-canvas svg {
  position: absolute;
  left: 0;
  top: 0;
  overflow: visible;
}

.rd-mkt-app__node {
  position: absolute;
  width: 132px;
  min-height: 44px;
  box-sizing: border-box;
  display: flex;
  align-items: center;
  justify-content: center;
  text-align: center;
  padding: 4px 8px;
  background: var(--sand-3);
  border: 1px solid var(--n-border);
  font: 400 var(--mkt-app-font-mono) / 1.3 var(--font-family-sans);
  color: var(--n-text);
}

.rd-mkt-app__node--done {
  border-color: var(--grass-7);
}

.rd-mkt-app__node--running {
  border-color: var(--gold-7);
}

.rd-mkt-app__zoom {
  position: absolute;
  right: var(--space-3);
  bottom: var(--space-3);
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 3px;
}

.rd-mkt-app__zoom-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 16px;
  height: 16px;
  background: var(--sand-11);
  color: var(--sand-1);
  border-radius: var(--radius-sm);
  font: 600 var(--mkt-app-font-micro) var(--font-family-mono);
}

.rd-mkt-app__zoom-btn--wide {
  width: auto;
  padding: 2px 6px;
}

.rd-mkt-app__tasks {
  margin-top: var(--space-4);
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

/* Border-defined, like the Collapsible skins it sits beside: this card's ground IS `--n-bg`, so a
   `--n-bg` fill here painted the colour already behind it. Measured in both themes. */
.rd-mkt-app__task {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: 5px var(--space-3);
  border: 1px solid var(--n-border-subtle);
  border-radius: var(--radius-sm);
}

.rd-mkt-app__task-title {
  flex: 1;
  min-width: 0;
  font: 400 var(--mkt-app-font-body) var(--font-family-serif);
  color: var(--n-text);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.rd-mkt-app__task-status {
  font: 500 var(--mkt-app-font-micro) var(--font-family-mono);
  letter-spacing: 0.06em;
}

.rd-mkt-app__task-status--done {
  color: var(--grass-11);
}

.rd-mkt-app__task-status--running {
  color: var(--gold-11);
}

/* The phase change, drawn as a rule with its name in it. */
.rd-mkt-app__divider {
  display: flex;
  align-items: center;
  gap: var(--space-3);
}

.rd-mkt-app__divider::before,
.rd-mkt-app__divider::after {
  content: "";
  flex: 1;
  height: 1px;
  background: var(--n-border-subtle);
}

.rd-mkt-app__divider span {
  font: 500 var(--mkt-app-font-micro) var(--font-family-mono);
  letter-spacing: 0.1em;
  color: var(--n-text-muted);
}

/* Forced colors replaces authored colors with the user's own palette, but it does not reach
   backgrounds painted by a pseudo-element from custom properties — the hero backdrop's photograph
   and its four wash layers paint straight through, and the browser falls back to drawing white
   backplates behind every line of hero text to keep it legible. Backplates are a rescue, not a
   design. A user in this mode has asked for their palette to win, so the field is dropped and the
   hero sits on their ground. The washes go with it: they exist to hold the photograph back off the
   ink, and with no photograph they are four gradients tinting a surface the user chose. */
@media (forced-colors: active) {
  .rd-mkt-page::before {
    background: none;
  }
}

/* Under reduced motion the looping session holds still. The steps stay visible — the timeline's
   final frame is the one with every step in it — so nothing is hidden behind an animation that
   no longer runs. */
@media (prefers-reduced-motion: reduce) {
  :root {
    --mkt-duration-entrance: 1ms;
    --mkt-duration-swap: 1ms;
  }

  .rd-mkt-app__pulse,
  .rd-mkt-app__caret,
  .rd-mkt-app__spinner,
  .rd-mkt-app__block-label--streaming {
    animation: none;
  }
}
