/* Component styles for the EastRouter design system.
 *
 * Hand-authored (unlike tokens.css), but every value is a token — there are no
 * literal colours, sizes or radii below. Specs come from the landing comps in
 * the .fig; see tokens/comps/*.json.
 *
 * Layout that must change between the 1440 and 402 comps lives here as media
 * queries rather than in the React components, so a component never has to
 * know the viewport.
 */

/* ── Reset ────────────────────────────────────────────────────────────────── */
/* Without this, any element combining `width: 100%` with padding overflows its
   parent by exactly the padding. That is what put a horizontal scrollbar on the
   phone layout: .er-container was 100% + 20px each side = viewport + 40px.
   Every consuming repo already sets this; the design system must not assume it. */
*, *::before, *::after { box-sizing: border-box; }

/* ── Page shell ───────────────────────────────────────────────────────────── */
/* The desktop comp lays content on a 1328px column inside a 1440 frame;
   the mobile comp uses 362 inside 402. Both are the frame minus a gutter. */
.er-container {
  box-sizing: border-box;
  width: 100%;
  max-width: 1328px;
  margin-inline: auto;
  padding-inline: var(--er-space-56);
}
@media (max-width: 900px) {
  .er-container { padding-inline: var(--er-space-20); }
}

.er-section {
  display: flex;
  flex-direction: column;
  gap: var(--er-space-40);
  padding-block: var(--er-space-80);
}
@media (max-width: 900px) {
  .er-section { gap: var(--er-space-24); padding-block: var(--er-space-40); }
}
.er-section--tight { padding-block: var(--er-space-40); }

/* ── cURL's mark, in the dark ─────────────────────────────────────────────── */
/* Every other mark in the SDK row either carries a colour that reads on both
   surfaces or is monochrome and tokenised. cURL is neither: it is two-tone, and
   both tones are very dark — #073551 navy and #0C544C green — which on a
   near-black card came out all but invisible.

   These are those two colours with their LIGHTNESS inverted and their hue and
   saturation left alone: 202.7deg at 17.3% lightness becomes 202.7deg at 82.7%,
   and 173.3deg at 18.8% becomes 173.3deg at 81.2%. Inverting the channels
   instead — the literal reading — turns navy and green into #F8CAAE peach and
   #F3ABB3 pink, which is a different logo.

   Declared here rather than in the component because the value has to change
   with the theme, which JSX cannot do; and here rather than in tokens.css
   because these are one vendor's brand colours, not design tokens of ours. The
   three-state pattern matches tokens.css exactly, so the theme toggle wins in
   both directions. */
:root {
  --er-vendor-curl-ink: #073551;
  --er-vendor-curl-accent: #0C544C;
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --er-vendor-curl-ink: #AEDCF8;
    --er-vendor-curl-accent: #ABF3EB;
  }
}
:root[data-theme="dark"] {
  --er-vendor-curl-ink: #AEDCF8;
  --er-vendor-curl-accent: #ABF3EB;
}

/* ── Testimonial row ──────────────────────────────────────────────────────── */
/* The comp runs these wider than the screen on purpose: Frame 218 is 1500 wide
   at x=-30 on a 1440 canvas, and inside it the four cards plus their 40px gaps
   total 1825.33 — so a card and a half hangs off each edge, and two gradient
   rectangles (Rectangle 5 and 6, 104 wide) fade what overhangs.

   We were rendering a 4-column grid inside the container: 286px cards, 24px
   gaps, 1216 across. Four cards, all fully visible, none of the overhang.

   The row scrolls rather than merely clipping. In the comp the outer two
   quotes are decoration; on a real page they are two of four testimonials, and
   a reader should be able to get to them.

   The fade is a mask on the WRAPPER, not on the scroller. A percentage in a
   mask gradient resolves against the mask box, and for a scrolling element
   that box is its SCROLL width — 1825 here, not the ~1330 on screen — so
   `calc(100% - 104px)` on the track itself would land far off the visible edge.
   The wrapper does not scroll, so its 100% is the width you can actually see.
   Same trap as the tab strip's trailing fade; different way out of it. */
@media (min-width: 901px) {
  .er-quoterow {
    /* Out to the container's own padding, so the cards clip nearer the screen
       edge as the comp has them. */
    margin-inline: calc(var(--er-space-56) * -1);
    /* The mask itself is .er-dragrow's; this only says how wide. Written as a
       two-class selector below, because .er-dragrow's own default is declared
       later in this file and would otherwise win on source order. */
  }
  /* Fading a 426px card, not a line of 12px text, so the comp's 28 is far too
     tight here. */
  .er-quoterow.er-dragrow { --er-dragrow-fade: var(--er-space-96); }
  .er-quoterow__track {
    display: flex;
    gap: var(--er-space-40);
    /* `safe`, so a viewport too narrow for the row leaves the first card
       reachable instead of stranding it before the scroll origin. */
    justify-content: safe center;

    /* No scroll-snap here, deliberately. With `scroll-snap-align: center` on
       426px cards the only snap positions in a 1328 viewport are "card 2
       centred" (scrollLeft 15) and "card 3 centred" (481) — the row's own
       centre, 248, is not one of them, so the engine pulled the opening view
       back to 15 and left card four entirely off screen. The comp's framing is
       the row centred, not a card centred. The phone scroller below 901 keeps
       its snapping, where snapping to a card is the point. */
  }
  .er-quoterow__track > * {
    /* The comp's four are 433.33, 429.33, 421.33 and 421.33 — hugging their
       own text. Equal width is the right call for a row of cards; 426 is the
       mean, so the row totals the comp's 1824 against its 1825.33. */
    flex: 0 0 426px;
  }
}

/* Room for the cards' shadow, at every width — the row scrolls on phones too.
   `overflow-x: auto` forces overflow-y to auto as well, so anything painted
   outside the track's box is clipped, and a box-shadow is painted outside by
   definition. At the 12 this had, the shadow was cut and left a hard line
   across the row.

   20 and 44 are measured, not derived: a card with shadow-2 (0 12px 36px) was
   rendered on the page background and the pixels scanned until they stopped
   differing from it. Reasoning about the blur radius gave 6 and 30, which is
   why the first attempt at this still clipped. Rounded up to the ladder's 24
   and 48.

   The extra is pulled straight back out with negative margins, so the row sits
   exactly where it did and nothing above or below it moves. */
.er-quoterow__track {
  padding-block: var(--er-space-24) var(--er-space-48);
  margin-block:
    calc(var(--er-space-12) * -1)
    calc((var(--er-space-48) - var(--er-space-12)) * -1);
}

/* ── Full-visibility band ─────────────────────────────────────────────────── */
/* The comp ends this section BEFORE the screenshot does, and the cut is the
   point: Frame 319 is 1440x930 with the shot at (345, 418) running to 943, and
   the Android export of the same frame says clipToOutline. A dashboard sliding
   out of frame reads as a window onto a live product; the whole picture, fully
   contained with space under it, reads as a photograph of one.

   So the band takes no bottom padding and the dashboard overhangs it.

   This used to be desktop-only, because the PNG was 750px wide there and a
   fixed -13px overhang only means 13px at one size. The mock overhangs by a
   share of itself instead — .er-dashmock reserves 752 of the frame's 791, and
   the 39px difference scales with everything else — so the cut is now the same
   cut at every width and the media query is gone. */
.er-fullvis { overflow: hidden; padding-bottom: 0; }

/* Present to assistive tech, invisible on screen. The clip-path pair is the
   modern form; the 1px box and the negative clip are what older engines honour.
   NOT `display: none` or `visibility: hidden` — both take the text out of the
   accessibility tree, which is the opposite of the point. */
.er-visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  white-space: nowrap;
  border: 0;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
}

/* ── Hero's cycling second line ───────────────────────────────────────────── */
/* Each step fades up rather than cutting, so the line reads as one phrase
   changing rather than six phrases flickering. inline-block because a transform
   does nothing to an inline box.

   The phrases are different widths and the hero is centred, so the line
   re-centres on every step. That is the point of a centred cycle; it is also
   why the fade matters, since a hard swap at a new width reads as a glitch. */
.er-hero__cycle {
  display: inline-block;
  animation: er-cycle-in 380ms cubic-bezier(0.2, 0, 0.2, 1) both;
}
@keyframes er-cycle-in {
  from { opacity: 0; transform: translateY(0.14em); }
  to { opacity: 1; transform: none; }
}
/* The hook already pins the line, so this is belt and braces for the frame
   between mount and the effect running. */
@media (prefers-reduced-motion: reduce) {
  .er-hero__cycle { animation: none; }
}

/* ── Section watermark ────────────────────────────────────────────────────── */
/* The comp puts a very large, very faint mark behind two sections. Frame 324 is
   1440x1294 and holds the mark's layer as a 1445x1290 frame at (-5, 77), which
   the section clips — the Android export of the same frame says clipToOutline,
   and the designer's SVG comes out 1440x1217, exactly the clipped region.
   Inside that, the artwork's own bounds are 1344.3 x 1375.6 at (39.4, 105.8)
   measured from the section's top-left.

   Every offset below is a percentage of the section's WIDTH, so the mark scales
   with the viewport and keeps the comp's proportions at any width. That is also
   why the vertical offset is margin-top rather than top: a percentage `top`
   resolves against the containing block's HEIGHT, and our section is not the
   comp's 1294 tall — it would drift as content changed. Percentage margins
   resolve against width, which is the behaviour wanted here.

     left   39.4 / 1440 =  2.736%
     width  1344.3 / 1440 = 93.354%
     top    105.8 / 1440 =  7.347% */
.er-section--watermark { position: relative; overflow: hidden; }
/* Content over the mark. The watermark is painted first, so this only needs to
   establish a stacking context, not out-index anything. */
.er-section--watermark > .er-container { position: relative; }
.er-watermark {
  position: absolute;
  top: 0;
  left: var(--er-watermark-left, 2.736%);
  width: var(--er-watermark-width, 93.354%);
  height: auto;
  /* margin-top, not top: a percentage `top` resolves against the containing
     block's HEIGHT, which changes with the content; a percentage margin
     resolves against its WIDTH, so the mark keeps the comp's proportions. */
  margin-top: var(--er-watermark-top, 7.347%);
  opacity: var(--er-watermark-opacity, 0.15);
  pointer-events: none;
}
/* `tight` covers two different jobs, and only one of them is "half the normal
   step". The hero and the fused demo are two frames of ONE comp section
   (Frame 353 contains both), so 40/40 BETWEEN them is their internal spacing
   and is right. What is not right is using the same 40 at the section's outer
   edges, where the comp is much more generous:

     - the hero's lead: comp runs the top bar's bottom (72) to the first
       headline (214) = 142; at tight we render 86. The whole page inherits
       this, which is why every heading below it started 27px high.
     - the closing CTA: comp puts 104 before its frame and 122 inside it, so
       the pricing band's bottom to the CTA headline is 226; we render 178.

   So the two outer edges get the normal step back and the inner boundary keeps
   the half step. Each is a separate class rather than a change to `tight`,
   which the hero/demo boundary still needs as it is. */
@media (min-width: 901px) {
  .er-section--tight.er-section--opening { padding-top: var(--er-space-80); }
  .er-section--tight.er-section--closing { padding-top: var(--er-space-80); }
  /* Some comp frames ABUT exactly rather than being separated: the specialty
     block and the category tabs share an edge at y1722, the same way the hero
     and the fused demo do inside Frame 353. They read as one unit, so the
     section that runs into the next one drops its trailing step and lets the
     follower's own leading step be the whole separation. We were inserting
     80 + 40 where the comp has 0. */
  .er-section--runs-on { padding-bottom: 0; }
}
.er-section--inverse {
  background: var(--er-bg-prime-inverse);
  /* Not text-on-color: that token is #FFFFFF in BOTH themes, while
     bg-prime-inverse flips to white in dark. See text-prime-inverse. */
  color: var(--er-text-prime-inverse);
}

/* ── The dark blue band ───────────────────────────────────────────────────── */
/* The comps paint two surfaces in the secondary brand's darkest steps: the
   closing CTA panel and the "Buy credit once" band. Both are the same deep blue
   gradient and both must stay dark in BOTH themes, which is why the stops are
   named directly rather than going through bg-prime-inverse (that token flips
   to white in dark mode). #0A305C in Figma sits one value off sec-900; not
   worth a token of its own.

   .er-dark is the shared treatment. .er-cta adds the panel's radius, padding
   and ellipse; .er-section--dark is the full-bleed band version.

   ⚠️ Text here is text-on-color (#FFFFFF in BOTH themes), NOT
   text-prime-inverse — which is the opposite of the usual advice, and
   deliberately so. text-prime-inverse exists for surfaces that FLIP
   (bg-prime-inverse: near-black in light, white in dark); it is near-black in
   dark mode, which on a surface that stays deep blue renders the headline
   almost invisible. Measured in a dark-mode screenshot, not assumed. Anything
   placed on .er-dark inherits this and must not name text-prime-inverse. */
.er-dark {
  color: var(--er-text-on-color);
  background: linear-gradient(135deg, var(--er-sec-950), var(--er-sec-900));
}
/* A dark band gets more air above it than a section on the page ground does.
   Measured as gap-before + the band's own internal top padding, the comp gives
   its three dark surfaces 316 (credit), 193 (pricing) and 226 (the CTA panel) —
   never less than 193 — against ~104-208 for the tinted bands and ~113-166 for
   plain sections. We render a flat 160 everywhere. The scatter is too wide to
   fit exactly, so this takes the conservative end of it: one extra step on the
   leading edge only, which is 200. */
/* ⚠️ Desktop only. Mobile rhythm is owned elsewhere and its comp is a different
   drawing; these were measured against the 1440 frame and must not leak to 402. */
@media (min-width: 901px) {
  /* 80 + 80. The comp's three dark surfaces get 316 (credit), 193 (pricing) and
     226 (the CTA panel) of separation before them, measured as gap-before plus
     the band's own internal top padding — mean 245. One extra step (200) was
     the first pass and left every one of them short; two lands at 240, inside
     the scatter rather than under all of it. */
  .er-section--dark { padding-top: calc(var(--er-space-80) + var(--er-space-80)); }
}

/* Gold on the dark band. The comps set the payoff clause of both dark-band
   headlines in this colour (#E8B05A in Figma, closest built step is prime-400).
   Named once so the two stay in step; .er-cta__accent is kept as the older
   name so the closing panel's markup does not have to change. */
/* Gold on a band that is navy in BOTH themes. prime-400 was a step darker than
   the comps draw it; text-prime-brand-on-dark is the #E8B05A they actually use,
   which had no Variable behind it in Figma and so was never extracted (see
   extraPrimitives in tokens/overrides.json). */
.er-accent-gold, .er-cta__accent, .er-dark__accent { color: var(--er-text-prime-brand-on-dark); }
/* .er-em declares its own colour further down this file, so on equal
   specificity it wins and an <Em class="er-accent-gold"> came out brand blue.
   Two classes beat one. */
.er-em.er-accent-gold { color: var(--er-text-prime-brand-on-dark); }

/* The muted blue-grey lede that sits under a dark-band headline. */
.er-dark__lede, .er-cta__lede { color: var(--er-sec-grey-300); max-width: 54ch; }

/* ── Type ─────────────────────────────────────────────────────────────────── */
/* The display scale is set for a 1440 frame. On mobile the comp drops roughly
   one step, which is expressed here so no component carries a breakpoint. */
@media (max-width: 900px) {
  /* The hero: 84/68 on desktop, 40/44 on mobile — both measured from the comps. */
  .er-dhero, .er-dihero { font-size: 40px; line-height: 44px; }
  /* Measured across the mobile comp: the hero is the ONLY 40px display line.
     Every section headline — 13 of them — is 32px. dh1/dih1 were 40 here, which
     wrapped the section headings a line or two further than the comp. */
  .er-dh1, .er-dih1 { font-size: 32px; line-height: 40px; }
  .er-dh2, .er-dih2 { font-size: 32px; line-height: 40px; }
  .er-h2 { font-size: 24px; line-height: 32px; }
  .er-h3 { font-size: 20px; line-height: 28px; }
  .er-p1 { font-size: 18px; line-height: 24px; }
  /* Every long body paragraph in the mobile comp is 16/24 — all eleven of them,
     with none at 18, against a desktop comp that is predominantly 18/24. So the
     lede drops one step on phones, the same way the display scale does. */
  .er-p2 { font-size: 16px; line-height: 24px; }
}

/* The comps' recurring editorial device: a headline's second clause set italic
   in a brand colour. 26 headlines across the two comps do this. */
.er-em {
  font-style: italic;
  color: var(--er-text-sec-brand);
}
.er-em--gold { color: var(--er-text-prime-brand); }
.er-em--plain { color: inherit; }

.er-lede {
  color: var(--er-text-prime-subtle);
  max-width: 62ch;
}

.er-eyebrow {
  display: inline-flex;
  align-items: center;
  gap: var(--er-space-16);
  color: var(--er-text-prime-subtle);
  /* It renders as a <p>, so without this it carries the UA's 1em block margin —
     14px top and bottom at this size. In a gap-based column that margin ADDS to
     the gap, so every eyebrow was sitting 38px above its headline where the
     comps set 24, on all seven of them. Text zeroes its own margin; Eyebrow
     builds its <p> directly and did not. */
  margin: 0;
}
/* The flanking rules. `rules="start"` (default) draws one before the label;
   "both" adds one after, which is how the comps set centred eyebrows;
   "none" drops them. */
.er-eyebrow::before,
.er-eyebrow--rules-both::after {
  content: '';
  width: 40px;
  height: 2px;
  background: currentColor;
  flex: none;
}
.er-eyebrow--rules-none::before { display: none; }

/* Gold eyebrow on a dark band: the usual subtle grey has too little contrast
   there. Defined after .er-eyebrow so it wins on order, not weight. */
/* Same gold as the accents above — the comps set every dark-band eyebrow at
   #E8B05A, not the prime-400 this used. */
.er-dark__eyebrow { color: var(--er-text-prime-brand-on-dark); }
.er-dark__eyebrow::before, .er-dark__eyebrow::after { background: var(--er-text-prime-brand-on-dark); }
.er-eyebrow--center { justify-content: center; }

/* ── Buttons ──────────────────────────────────────────────────────────────── */
.er-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--er-space-8);
  border: 1px solid transparent;
  border-radius: var(--er-radius-sm);
  cursor: pointer;
  text-decoration: none;
  white-space: nowrap;
  transition: background-color 140ms ease, border-color 140ms ease, color 140ms ease;
}
.er-btn:focus-visible {
  outline: 2px solid var(--er-border-sec-brand);
  outline-offset: 2px;
}
.er-btn[disabled], .er-btn[aria-disabled='true'] {
  cursor: default;
  opacity: 0.5;
}

/* Sizes — the comp uses 16/80 for hero CTAs, 16/32 for inline, 8/24 in the nav. */
.er-btn--lg { padding: var(--er-space-16) var(--er-space-80); }
.er-btn--md { padding: var(--er-space-16) var(--er-space-32); }
.er-btn--sm { padding: var(--er-space-8) var(--er-space-24); }
@media (max-width: 900px) {
  /* The mobile comp keeps the 16/80 padding and stretches the button across the
     column instead — the padding just centres a short label. Labels wrap rather
     than force the page wider. */
  .er-btn--lg, .er-btn--md { width: 100%; }
  .er-btn { white-space: normal; }
}

/* Variants. `primary` is the blue the comps use for every hero CTA — which is
   bg-sec-brand, because the Variables call the blue Brand/Secondary. See the
   brandRoles note in tokens/overrides.json. */
.er-btn--primary {
  background: var(--er-bg-sec-brand);
  color: var(--er-text-on-color);
}
.er-btn--primary:hover { background: var(--er-bg-sec-brand-inverse); }

.er-btn--secondary {
  background: var(--er-bg-prime-base);
  color: var(--er-text-prime-strong);
  border-color: var(--er-border-prime-base);
}
.er-btn--secondary:hover { border-color: var(--er-border-prime-strong); }

.er-btn--inverse {
  background: var(--er-bg-prime-inverse);
  color: var(--er-text-prime-inverse);
}
/* bg-prime-inverse flips between themes (near-black in light, white in dark),
   so a hover that names a fixed surface inverts its own contrast in one theme.
   Dimming works from either direction. */
.er-btn--inverse:hover { opacity: 0.88; }

/* Sits on a dark band — inverts against the section, not the page. */
/* Sits on a band that stays deep blue in BOTH themes, so it cannot use tokens
   that flip: bg-prime-page-body is near-black in dark mode, which turned this
   into a dark button on a dark band. Named steps, like the band itself. */
.er-btn--onDark {
  background: var(--er-prime-grey-50);
  color: var(--er-prime-grey-950);
}

.er-btn--quiet {
  background: none;
  padding-inline: 0;
  color: var(--er-text-prime-strong);
}
.er-btn--quiet:hover { color: var(--er-text-sec-brand); }

/* Text link with a trailing arrow — the comps' standard "read more" affordance. */
.er-textlink {
  display: inline-flex;
  align-items: center;
  gap: var(--er-space-4);
  color: var(--er-text-sec-brand);
  text-decoration: none;
}
.er-textlink svg { flex: none; transition: transform 140ms ease; }
.er-textlink:hover svg { transform: translateX(3px); }
.er-textlink:focus-visible { outline: 2px solid var(--er-border-sec-brand); outline-offset: 3px; border-radius: 2px; }
@media (prefers-reduced-motion: reduce) { .er-textlink svg { transition: none; } }

/* Nothing may exceed the viewport. Flex and grid children default to
   min-width:auto, so a wide child (a code block, an unbreakable string) pushes
   the whole page sideways instead of scrolling inside its own box. */
.er-card, .er-heading, .er-meter, .er-quote { min-width: 0; }
.er-card > *, .er-heading > * { min-width: 0; max-width: 100%; }
img, svg, video { max-width: 100%; }

/* ── Surfaces ─────────────────────────────────────────────────────────────── */
.er-card {
  box-sizing: border-box;
  display: flex;
  flex-direction: column;
  gap: var(--er-space-16);
  padding: var(--er-space-24);
  border-radius: var(--er-radius-lg);
  border: 1px solid transparent;
  background: var(--er-bg-prime-base);
}
.er-card--base { border-color: var(--er-border-prime-base); }
.er-card--subtle {
  background: var(--er-bg-prime-subtle);
  border-color: var(--er-border-prime-base);
}
.er-card--page {
  background: var(--er-bg-prime-page-body);
  border-color: var(--er-border-prime-base);
}
.er-card--raised {
  box-shadow: var(--er-shadow-2);
  padding: var(--er-space-32);
  gap: var(--er-space-32);
}
.er-card--flat { border-color: transparent; }
.er-card--pad-lg { padding: var(--er-space-32); }
@media (max-width: 900px) {
  .er-card--raised, .er-card--pad-lg { padding: var(--er-space-24); gap: var(--er-space-24); }
}

.er-divider {
  border: 0;
  border-top: 1px solid var(--er-border-prime-base);
  margin: 0;
  width: 100%;
}
.er-divider--strong { border-top-color: var(--er-border-prime-strong); }

/* ── Pills ────────────────────────────────────────────────────────────────── */
.er-pill {
  display: inline-flex;
  align-items: center;
  gap: var(--er-space-8);
  padding: var(--er-space-4) var(--er-space-12);
  border-radius: var(--er-radius-pill);
  background: var(--er-bg-prime-subtle);
  color: var(--er-text-prime-base);
  white-space: nowrap;
  /* Not one pill in either comp is uppercased or tracked — "Fused Result",
     "2 models combined", "Best single model for this task", "Top Result",
     "+25% bonus" are all set as written. Both come from .er-cap1 / .er-cap2,
     which apply them to every caption on the page; a pill opts out. */
  text-transform: none;
  letter-spacing: normal;
}
/* The chip's text box is 16 tall against a 12px face — 2px of leading above and
   below — which is the whole of "make chip text smaller and add padding 1-2px
   top and bottom" from the designer's Figma comments. Frame 348 is 94x24: 4
   padding, a 16 line box, 4 padding.

   It has to be set here because the cap2 TOKEN carries line-height 12. Figma
   has two text styles both named cap2: 12/16 with no tracking, which is what 54
   of the comp's 67 instances use, and 12/12 with 0.05em, which the other 13
   use ("Human Preference"). The extractor took the minority form. Correcting
   the token itself would move 39 other elements on this page, so the chip
   states its own line box and the token is left alone — see the note in
   docs/landing-open-questions.md. */
.er-pill.er-cap2 { line-height: 16px; }
.er-pill--brand { background: var(--er-bg-sec-brand-subtle-inverse); color: var(--er-text-sec-brand); }
.er-pill--success { background: var(--er-bg-success-subtle); color: var(--er-text-success); }
/* Frame 348 sets #211B10 on #F2EEE6 — text-prime-base, not the subtle grey
   this used, which was a step too light against the same background. */
.er-pill--neutral { background: var(--er-bg-prime-subtle); color: var(--er-text-prime-base); }

/* ── Section heading ──────────────────────────────────────────────────────── */
.er-heading {
  display: flex;
  flex-direction: column;
  /* 24, not 16. Every heading block in the desktop comp that has an eyebrow
     sets 24 between its parts — six of them, with no exceptions (Frame 392,
     265, 144, 365, 171). At 16 each of those sections came out ~24-34px short
     and the whole page below inherited it. */
  gap: var(--er-space-24);
  max-width: 68ch;
}
.er-heading--center {
  align-items: center;
  text-align: center;
  margin-inline: auto;
  /* The comps set centred headings 800px wide. 68ch is right for a left-aligned
     column of body text but forces the display line to wrap four times here. */
  max-width: 800px;
}

.er-heading__row {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: var(--er-space-24);
  max-width: none;
}
@media (max-width: 900px) {
  .er-heading--center {
  align-items: center;
  text-align: center;
  margin-inline: auto;
}

.er-heading__row { flex-direction: column; align-items: flex-start; gap: var(--er-space-16); }
}

/* ── Top bar ──────────────────────────────────────────────────────────────── */
.er-topbar {
  padding-block: var(--er-space-32);
  background: var(--er-bg-prime-page-body);
}
.er-navlink {
  text-decoration: none;
  transition: color 140ms ease;
}
/* "Menu items must turn black on hover (text-prime-base)" — designer, Figma.
   Was the brand blue. */
.er-navlink:hover { color: var(--er-text-prime-base); text-decoration: underline; }
.er-navlink:focus-visible {
  outline: 2px solid var(--er-border-sec-brand);
  outline-offset: 3px;
  border-radius: 2px;
}
/* The mobile comp replaces the link row with a menu button. Until that exists,
   hide the row rather than let it wrap into the wordmark. */
@media (max-width: 1100px) { .er-topbar__nav { display: none; } }
/* Mobile navigation. The comp shows the wordmark and a menu button; this is
   that, on <details>, so it opens without JavaScript. */
.er-navmenu { position: relative; display: none; }
.er-navmenu > summary {
  list-style: none;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 40px;
  height: 40px;
  margin-right: calc(var(--er-space-8) * -1);
  color: var(--er-icon-prime-base);
}
.er-navmenu > summary::-webkit-details-marker { display: none; }
.er-navmenu > summary:focus-visible { outline: 2px solid var(--er-border-sec-brand); border-radius: var(--er-radius-sm); }
.er-navmenu__panel {
  position: absolute;
  right: 0;
  top: calc(100% + var(--er-space-8));
  z-index: 50;
  min-width: 220px;
  display: flex;
  flex-direction: column;
  gap: var(--er-space-16);
  padding: var(--er-space-24);
  border-radius: var(--er-radius-lg);
  border: 1px solid var(--er-border-prime-base);
  background: var(--er-bg-prime-base);
  box-shadow: var(--er-shadow-2);
}
/* Layout lives here rather than on a Stack, so the breakpoint below can hide it
   — see the note in navigation.jsx. */
.er-topbar__actions {
  display: flex;
  align-items: center;
  gap: var(--er-space-24);
  min-width: 0;
}
@media (max-width: 1100px) {
  .er-navmenu { display: block; }
  .er-topbar__actions { display: none; }
  .er-topbar { padding-block: var(--er-space-20); }
}

/* ── Closing CTA panel ────────────────────────────────────────────────────── */
/* Dark rounded panel with a large brand ellipse bleeding out of the bottom. */
/* Surface and text colour come from .er-dark, which the band shares. This adds
   only what makes it a floating panel rather than a full-bleed band. */
.er-cta {
  box-sizing: border-box;
  position: relative;
  overflow: hidden;
  isolation: isolate;
  border-radius: var(--er-radius-xl);
  padding: var(--er-space-56);
}
.er-cta::before {
  content: '';
  position: absolute;
  z-index: -1;
  left: 50%;
  top: 62%;
  width: 780px;
  height: 780px;
  transform: translateX(-50%);
  border-radius: 50%;
  background: var(--er-sec-900);
  /* A lighter disc against the gradient, as in the comp — not a translucent
     brand wash, which read as a blob sitting on top of the panel. */
  opacity: .55;
}
/* The CTA panel carries the mark too, off its right edge. Frame 116 is 521x521
   at (806, -21) inside the 1070x480 panel, so it hangs off the right by 257 and
   past the top and bottom — the Android export of this frame says
   clipToOutline, and .er-cta already clips.

   The mark's artwork is larger than the frame that holds it, by the ratio
   measured on the other watermark: 1.2166x the frame's width, centred on the
   frame's centre give or take 0.8%. That puts the artwork at 633.8 x 648.6 at
   (745.4, -89) — which checks out, because 633.8/648.6 is 0.97724, the same
   aspect MarkWatermark's own viewBox has.

     left   745.4 / 1070 = 69.66%
     width  633.8 / 1070 = 59.24%
     top    -89   / 1070 = -8.318%

   Fixed white rather than a theme token: this panel is navy in BOTH themes, so
   a token that flips would turn the mark near-black here in dark mode — the
   same trap the Mark's own onDark tone had to avoid. */
.er-cta .er-watermark {
  --er-watermark-left: 69.66%;
  --er-watermark-width: 59.24%;
  --er-watermark-top: -8.318%;
  --er-watermark-opacity: 0.07;
  fill: var(--er-icon-on-color);
}

/* The comp's button spans a little over half the panel (566 of 1070) rather
   than shrinking to its label — at the end of the page it is the one thing left
   to do, and a label-width button reads as incidental. min-width rather than a
   fixed width so a longer label still fits. */
.er-cta .er-btn { min-width: 53%; }

@media (max-width: 900px) {
  /* The phone comp runs this edge to edge with square corners — it is the last
     thing on the page and reads as the page ending, not as a card floating on
     it. Bleeding by the container's own gutter is what makes it reach the
     screen edge without widening the document. */
  .er-cta {
    border-radius: 0;
    padding: var(--er-space-64) var(--er-space-24);
    margin-inline: calc(var(--er-space-20) * -1);
    width: auto;
  }
  .er-cta::before { width: 460px; height: 460px; }
  /* The comp drops the supporting line on the phone: the headline and the
     button are the whole message, and keeping the lede pushed the panel from
     the comp's 368 to 522. */
  .er-cta .er-dark__lede, .er-cta .er-cta__lede { display: none; }
  /* Full-width button, as the comp's 355-into-355 shows. */
  .er-cta .er-btn { width: 100%; }
}

/* ── Feature list ─────────────────────────────────────────────────────────── */
/* A list whose marker is the brand gold diamond, not a bullet. The comps use it
   for both pricing cards' feature lists. The diamond is a rotated square rather
   than a ◆ glyph so it does not depend on a font having one, and it is a
   ::before on the <li> so the text wraps to a hanging indent. */
.er-features { display: flex; flex-direction: column; gap: var(--er-space-16); margin: 0; padding: 0; list-style: none; }
.er-features__item {
  display: flex;
  align-items: baseline;
  gap: var(--er-space-8);
}
.er-features__item::before {
  content: '';
  flex: none;
  width: 8px;
  height: 8px;
  /* Nudged off the baseline so the diamond sits mid-cap-height, as in the comp. */
  transform: translateY(-2px) rotate(45deg);
  background: var(--er-bg-prime-brand);
}

/* ── Slider ───────────────────────────────────────────────────────────────── */
/* A real <input type="range">, so it is focusable and arrow-key operable. The
   comps show a resting position only — nothing computes from it yet — but a
   picture of a slider would be unusable, so this is the control.

   The track gradient is painted on the input's background rather than on the
   ::-*-track pseudo-elements, because the filled portion has to follow the
   value and only the element itself can carry a custom property that a caller
   sets. --er-slider-pct is that value, 0–100. */
.er-slider { display: flex; flex-direction: column; gap: var(--er-space-8); }
.er-slider__head { display: flex; align-items: center; justify-content: space-between; gap: var(--er-space-16); }
.er-slider__input {
  -webkit-appearance: none;
  appearance: none;
  display: block;
  width: 100%;
  height: 24px; /* the knob's diameter: the input's box is the hit target */
  margin: 0;
  background: transparent;
  cursor: pointer;
}
.er-slider__input::-webkit-slider-runnable-track {
  height: 12px;
  border-radius: var(--er-radius-sm);
  background: linear-gradient(
    to right,
    var(--er-bg-sec-brand) 0 var(--er-slider-pct, 30%),
    var(--er-bg-sec-subtle) var(--er-slider-pct, 30%) 100%
  );
}
.er-slider__input::-moz-range-track {
  height: 12px;
  border-radius: var(--er-radius-sm);
  background: var(--er-bg-sec-subtle);
}
.er-slider__input::-moz-range-progress {
  height: 12px;
  border-radius: var(--er-radius-sm);
  background: var(--er-bg-sec-brand);
}
.er-slider__input::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  width: 24px;
  height: 24px;
  border-radius: var(--er-radius-round);
  /* Stays white in both themes: it sits on the blue track, not on the page. */
  background: var(--er-prime-grey-50);
  border: 2px solid var(--er-bg-sec-brand);
  margin-top: -6px; /* centre the 24px knob on the 12px track */
}
.er-slider__input::-moz-range-thumb {
  width: 20px;
  height: 20px;
  border-radius: var(--er-radius-round);
  background: var(--er-prime-grey-50);
  border: 2px solid var(--er-bg-sec-brand);
}
.er-slider__input:focus-visible { outline: 2px solid var(--er-border-sec-brand); outline-offset: 4px; }
.er-slider__ends { display: flex; align-items: center; justify-content: space-between; gap: var(--er-space-16); }

/* ── Callout ──────────────────────────────────────────────────────────────── */
/* A pale tinted strip inside a card, with a label/figure on the left and a pill
   on the right — the comp's "You get $400 / +25% bonus" panel. */
.er-callout {
  display: flex;
  align-items: center;
  gap: var(--er-space-8);
  padding: var(--er-space-16);
  border-radius: var(--er-radius-sm);
  /* bg-blue-subtle, not bg-sec-brand-subtle-inverse: the latter is the right
     colour in light (#E4EBF5, near the comp's #EBF1FA) but resolves to
     warning-900 — an amber — in dark, which paints this panel brown. Checked in
     both themes. bg-blue-subtle is #E6EFFA / #103059: blue on both sides. */
  background: var(--er-bg-blue-subtle);
}
.er-callout__body { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: var(--er-space-4); }
/* The comp separates the badge from this panel by one step of blue. There is no
   built token for "one step darker than bg-blue-subtle" — bg-sec-brand-subtle-
   inverse is that step in light but an amber in dark — and a token cannot be
   added without the .fig, which is stale. So the separation is drawn rather
   than filled: a brand hairline, which reads correctly in both themes and needs
   no new token. Replace with the tint when the step exists upstream. */
.er-callout .er-pill { border: 1px solid var(--er-border-sec-brand); }

/* ── Meter ────────────────────────────────────────────────────────────────── */
/* Labelled score bar. Ten of these in the benchmark card: a track, a fill, and
   a value, with the fill in the brand colour when it is the subject. */
.er-meter { display: flex; flex-direction: column; gap: var(--er-space-4); }
.er-meter__row { display: flex; align-items: center; gap: var(--er-space-8); }
.er-meter__track {
  flex: 1;
  /* Frame 398's bar is 12 tall in a 21 row, not the 16 this was. */
  height: 12px;
  min-width: 0;
  border-radius: var(--er-radius-sm);
  /* Not bg-sec-subtle: on the cream page card that is a 1% difference from the
     card itself, so the unfilled part of the bar vanishes and every score reads
     as a floating stub with no scale behind it. See bg-sec-muted in
     tokens/overrides.json. */
  background: var(--er-bg-sec-muted);
  overflow: hidden;
}
.er-meter__fill {
  height: 100%;
  border-radius: var(--er-radius-sm);
  background: var(--er-bg-sec-brand);
}
.er-meter--muted .er-meter__fill { background: var(--er-border-sec-strong); }
/* The comps paint the runner-up's preference bar in the brand gold, not grey —
   see the hero comparison, where the single-model score sits beside the fused
   one. `muted` stays for bars that should recede entirely. */
.er-meter--brand .er-meter__fill { background: var(--er-bg-prime-brand); }
/* The comps put the value immediately after the bar with an 8px gap, not pushed
   to the far edge of the row — `min-width: 2.2em; text-align: right` made a
   two-digit number sit further from its bar than a three-digit one, which reads
   as misalignment across a row of five. Left-aligned and unpadded, every value
   sits the same distance from its own track. */
.er-meter__value { flex: none; text-align: left; }
/* Frame 398 sets the subject's name at 12/700 and its comparisons at 12/600,
   and the same one-step split on the values (14/600 against 14/500). The scale
   has no 12/700 or 14/500 style, so each borrows the weight from the style
   where this family defines it — h7 is the 700, p3 the 500 — rather than
   typing a number the tokens would not know about. */
.er-meter:not(.er-meter--muted) .er-meter__label { font-weight: var(--er-type-h7-weight); }
.er-meter--muted .er-meter__value { font-weight: var(--er-type-p3-weight); }

/* ── Tabs ─────────────────────────────────────────────────────────────────── */
/* Top-rounded tabs that sit on the card below them, so the active one reads as
   continuous with the panel. */
.er-tabs { display: flex; gap: 0; overflow-x: auto; scrollbar-width: none; }
/* The comp centres the folder strip on the card and sets the tabs 24px apart:
   Frame 388 is 854 wide at x=293 on a 1440 canvas, so its centre is 720 — half
   the canvas — and its five children sit at 0, 148, 385, 546 and 720 against
   widths of 122, 213, 137, 150 and 134, which is a 24px gap four times over.
   `safe` matters: below 900 the strip is wider than the screen, and a plain
   `center` puts the leading overflow out of scroll reach. It falls back to
   flex-start exactly when that would happen. */
.er-tabs--folder { gap: var(--er-space-24); justify-content: safe center; }
.er-tabs::-webkit-scrollbar { display: none; }
/* Below 900 the strip is wider than the screen (measured: 788 into 362), so it
   scrolls. Above it the tabs fit and a fade would dim the last one for no
   reason, which is why the hint is scoped to the breakpoint rather than always
   on. Snapping makes a flick land on a tab boundary instead of mid-word. */
@media (max-width: 900px) {
  /* The comp's phone tab is 52 tall: 16 padding, a 20px line, 16 padding. Ours
     was 61 because the folder tab carries DH5 at 24px. The comp keeps the
     display serif here (Minion Pro 20/600) and only steps the size down, so
     this sets size and line-height and leaves the family alone. */
  .er-tabs--folder .er-tab {
    font-size: var(--er-type-dh6-size);
    line-height: var(--er-type-dh6-line-height);
  }
  .er-tabs {
    scroll-snap-type: x proximity;
    /* The mask crops the folder tabs' top corners by a hair otherwise. */
    padding-top: 1px;
  }
  .er-tabs .er-tab { scroll-snap-align: start; }
  /* Fade the trailing edge so the strip reads as scrollable rather than as a
     row that happens to be cut off at the screen edge. mask-image leaves the
     tabs themselves untouched — a gradient overlay would need a background to
     sit on and the tabs are two different colours. */
  /* A fade at the trailing edge, so the strip reads as scrollable rather than
     as a row cut off at the screen edge.

     NOT mask-image on .er-tabs: a percentage in a mask gradient resolves
     against the mask box, which for a scrolling row is its SCROLL width (788px
     here, not the visible 362) — `calc(100% - 40px)` landed at 48px and faded
     almost the whole strip. Measured, not guessed. A sticky child rides the
     scroll instead and needs no percentage. */
  .er-tabs::after {
    content: '';
    position: sticky;
    right: 0;
    flex: none;
    align-self: stretch;
    width: var(--er-space-40);
    margin-left: calc(var(--er-space-40) * -1);
    pointer-events: none;
    background: linear-gradient(to right, transparent, var(--er-bg-prime-page-body));
  }
}

/* Shared by both variants. Everything a variant differs on is set below, so
   the two never fight over source order. */
.er-tab {
  appearance: none;
  cursor: pointer;
  white-space: nowrap;
  transition: background-color 140ms ease, color 140ms ease, border-color 140ms ease;
}
.er-tab:focus-visible { outline: 2px solid var(--er-border-sec-brand); outline-offset: -2px; }

/* variant="folder" (the default): raised tabs sitting on the panel below. */
.er-tabs--folder .er-tab {
  border: 1px solid transparent;
  border-bottom: 0;
  border-radius: var(--er-radius-lg) var(--er-radius-lg) 0 0;
  padding: var(--er-space-16) var(--er-space-24) var(--er-space-12);
  background: var(--er-bg-prime-subtle);
  color: var(--er-text-prime-subtle);
}
.er-tabs--folder .er-tab[aria-selected='true'] {
  background: var(--er-bg-prime-base);
  border-color: var(--er-border-prime-base);
  color: var(--er-text-prime-strong);
}

/* variant="underline": one shared rule under the whole strip, the active tab
   claiming its own segment of it. Cells share the width equally, as the comp's
   five 120px cells do, and shrink together rather than scrolling. */
.er-tabs--underline .er-tab {
  flex: 1 1 0;
  min-width: 0;
  /* The base .er-tab sets nowrap so a folder tab never breaks mid-label. These
     share the width equally instead, so a long label has to be allowed to wrap
     — without this they overlap each other at narrow card widths. */
  white-space: normal;
  border: 0;
  border-bottom: 1px solid var(--er-border-prime-base);
  border-radius: 0;
  background: none;
  padding: var(--er-space-16);
  text-align: center;
  color: var(--er-text-prime-subtle);
}
.er-tabs--underline .er-tab[aria-selected='true'] {
  border-bottom-color: var(--er-border-sec-brand);
  color: var(--er-text-sec-brand);
}

/* ── Accordion ────────────────────────────────────────────────────────────── */
/* Built on <details> so it works with no JavaScript and stays keyboard- and
   screen-reader-navigable for free. */
.er-accordion { display: flex; flex-direction: column; }
/* The comps' width for questions under a centred heading. */
.er-accordion--narrow { max-width: 782px; margin-inline: auto; width: 100%; }

/* The comp rules BELOW every row, including the last — there is no rule above
   the first, which would read as a lid on a list that has a heading over it
   already. */
.er-accordion__item { border-bottom: 1px solid var(--er-border-prime-strong); }
.er-accordion__item > summary {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--er-space-24);
  /* 32 top and bottom gives the comp's 89px collapsed row (24 line + 2x32). */
  padding-block: var(--er-space-32);
  cursor: pointer;
  list-style: none;
}
.er-accordion__item > summary::-webkit-details-marker { display: none; }
.er-accordion__item > summary:focus-visible { outline: 2px solid var(--er-border-sec-brand); outline-offset: 2px; }
/* Gold, and 2px on a 14px glyph inside a 24px box — measured off the comp's
   plus-01 instance rather than eyeballed. text-prime-brand rather than a named
   step so it lightens in dark mode along with everything else gold. */
.er-accordion__sign { flex: none; position: relative; width: 24px; height: 24px; }
.er-accordion__sign::before,
.er-accordion__sign::after {
  content: '';
  position: absolute;
  inset: 50% 5px auto 5px;
  height: 2px;
  border-radius: 1px;
  background: var(--er-text-prime-brand);
  transition: transform 160ms ease;
}
.er-accordion__sign::before { transform: translateY(-50%); }
.er-accordion__sign::after { transform: translateY(-50%) rotate(90deg); }
/* Open: the vertical stroke rotates flat onto the horizontal one, leaving a
   minus. Both keep the same translate or the pair drifts off centre. */
.er-accordion__item[open] .er-accordion__sign::after { transform: translateY(-50%) rotate(0deg); }
/* The answer sits under the question with the comp's 16px gap, and the row's
   own 32px bottom padding closes it out. */
/* No max-width: the row is already held to a readable measure by the column
   itself (782 in the comps), and a 76ch cap on top of that rewrapped the answer
   narrower than the question above it, which read as a mistake. */
.er-accordion__body { padding-bottom: var(--er-space-32); margin-top: calc(var(--er-space-16) - var(--er-space-32)); }
@media (prefers-reduced-motion: reduce) {
  .er-accordion__sign::before, .er-accordion__sign::after { transition: none; }
}
/* The phone comp tightens the rows to 73px (20px line + 2x24) and drops the
   question a step to 16px. */
@media (max-width: 900px) {
  .er-accordion__item > summary { padding-block: var(--er-space-24); gap: var(--er-space-16); }
  .er-accordion__body { padding-bottom: var(--er-space-24); }
  /* The phone comp steps the question down from B1 (18) to B2 (16). Setting the
     style here rather than swapping the Text variant keeps the component free
     of any knowledge of width. */
  .er-accordion__q {
    font-size: var(--er-type-b2-size);
    line-height: var(--er-type-b2-line-height);
  }
}

/* ── Data table ───────────────────────────────────────────────────────────── */
/* Two layouts from one <table>. Wide: a bordered card with a tinted header row
   and a navy identity column. Narrow (<= 900px): one card per row, because a
   four-column table on a 402px phone hides half its data behind a sideways
   scroll. Everything that differs between the two is here — DataTable itself
   never reads the viewport. */
.er-tablewrap {
  overflow-x: auto;
  border: 1px solid var(--er-border-prime-base);
  border-radius: var(--er-radius-lg);
  background: var(--er-bg-prime-base);
}
/* The header cells paint their own background, so the wrapper's radius would
   be squared off by them. Clipping restores it without a second radius that
   could fall out of step. */
.er-tablewrap { overflow: hidden auto; }
/* The comps' width for a table under a centred heading. */
.er-tablewrap--narrow { max-width: 960px; margin-inline: auto; }
.er-table { width: 100%; border-collapse: collapse; table-layout: fixed; }
/* The identity column is a label, not data: the comps give it a fixed narrow
   share and split what is left equally between the rest. table-layout:fixed
   above is what makes those shares hold rather than being renegotiated by the
   longest string in each column. */
.er-table th:first-child { width: 19.4%; }
.er-table th {
  text-align: left;
  padding: var(--er-space-24);
  background: var(--er-bg-prime-subtle);
  color: var(--er-text-prime-subtle);
  border-bottom: 1px solid var(--er-border-prime-base);
  border-right: 1px solid var(--er-border-prime-base);
  white-space: nowrap;
  vertical-align: middle;
}
.er-table th:last-child { border-right: 0; }
.er-table td {
  padding: var(--er-space-24);
  border-bottom: 1px solid var(--er-border-prime-base);
  border-right: 1px solid var(--er-border-prime-base);
  vertical-align: top;
}
.er-table td:last-child { border-right: 0; }
.er-table tr:last-child td { border-bottom: 0; }
.er-table .er-num { font-variant-numeric: tabular-nums; white-space: nowrap; }

/* The identity column. Named steps, not bg-prime-inverse: this block is navy in
   BOTH themes in the comps, and the inverse token flips to white in dark mode
   (the trap CONTRIBUTING.md describes), which would put white on white. */
.er-table td.er-td--key {
  background: var(--er-sec-900);
  vertical-align: middle;
}
.er-table td.er-td--key,
.er-table td.er-td--key * { color: var(--er-prime-grey-0); }

/* The per-cell label a card shows above its value. Present in the markup at
   every width so the switch is purely a display change; hidden while the table
   is a table. */
.er-td__label { display: none; }

@media (max-width: 900px) {
  /* Cards, not a scrolling table. The wrapper stops being the card — each row
     is one — so it drops its own border and background. */
  .er-tablewrap {
    overflow: visible;
    border: 0;
    border-radius: 0;
    background: none;
  }
  /* A <table> cannot be laid out as a stack of cards while it is still a table,
     so its parts become blocks. The element stays a <table> in the markup, so
     assistive technology still reads a table. */
  .er-table, .er-table tbody, .er-table tr, .er-table td { display: block; width: 100%; }
  /* The header row's labels reappear inside each cell instead. */
  .er-table thead { display: none; }
  .er-table tr {
    border: 1px solid var(--er-border-prime-base);
    border-radius: var(--er-radius-md);
    background: var(--er-bg-prime-base);
    overflow: hidden;
    margin-bottom: var(--er-space-24);
  }
  .er-table tr:last-child { margin-bottom: 0; }
  .er-table td {
    border: 0;
    padding: var(--er-space-16);
  }
  /* The identity cell becomes the card's header strip: full bleed, no radius of
     its own — the row clips it. */
  .er-table td.er-td--key {
    padding: var(--er-space-10) var(--er-space-16);
  }
  /* Metric cells sit side by side under the prose, each in its own tinted
     block, as the mobile comp shows. Floating them is the only way to get two
     block-level <td>s onto one line without changing their display. */
  .er-table td.er-td--metric {
    float: left;
    /* Both blocks the same height whichever label wraps, so the pair reads as
       one comparison rather than two unrelated boxes. */
    min-height: 64px;
    /* Two blocks and three 16px gutters (outer, middle, outer) across the
       card, so each is half of what is left. */
    width: calc(50% - var(--er-space-16) - (var(--er-space-16) / 2));
    margin: 0 0 var(--er-space-16) var(--er-space-16);
    padding: var(--er-space-10) var(--er-space-12);
    border-radius: var(--er-radius-md);
    background: var(--er-bg-prime-subtle);
  }
  /* bg-blue-subtle (#E6EFFA / #103059), the same token the callout panel uses
     and for the same reason: it is blue in BOTH themes. sec-100 named directly
     would leave a light block on a dark card, bg-sec-brand-subtle-inverse
     resolves to a warning brown in dark, and bg-sec-subtle is so close to white
     the block disappears. Checked in both themes. */
  .er-table td.er-td--metric.er-td--brand { background: var(--er-bg-blue-subtle); }
  /* Clear the float so the next row's card does not wrap around it. */
  .er-table tr::after { content: ''; display: block; clear: both; }
  /* Labels appear on the two metric blocks, which is where the comp needs them
     — the prose cell reads for itself and the identity strip IS the label. */
  .er-table td.er-td--metric .er-td__label {
    display: block;
    color: var(--er-text-prime-subtle);
    margin-bottom: var(--er-space-4);
    text-transform: uppercase;
  }
  .er-table td.er-td--metric.er-td--brand .er-td__label { color: var(--er-text-sec-brand); }
  /* Whatever a metric cell shows beside its number on the wide table — a delta
     pill, a per-task cost — is detail the card has no room for. The comp drops
     it rather than shrinking it. */
  .er-table td.er-td--metric .er-td__detail { display: none; }
}

/* ── Quote ────────────────────────────────────────────────────────────────── */
/* The mark is a vector now (QuoteMarkIcon), sized 30x22 by the comp. It was a
   64px &ldquo; in the display face, which measured 220 x 38.4 — a block-level
   glyph seven times the comp's width, at whatever size the font gave it. */
.er-quote__mark { display: block; flex: none; }
.er-avatar {
  width: 64px; height: 64px;
  flex: none;
  border-radius: var(--er-radius-round);
  background: var(--er-bg-prime-subtle);
  border: 1px solid var(--er-border-prime-base);
  /* Photographs are rarely square; cover crops rather than distorting. */
  object-fit: cover;
}

/* ── Grid ─────────────────────────────────────────────────────────────────── */
.er-grid {
  display: grid;
  gap: var(--er-space-24);
  grid-template-columns: repeat(var(--er-grid-cols, 3), minmax(0, 1fr));
}
@media (max-width: 1100px) { .er-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 900px) { .er-grid { grid-template-columns: minmax(0, 1fr); } }

/* ── Motion ───────────────────────────────────────────────────────────────── */
@media (prefers-reduced-motion: reduce) {
  .er-btn { transition: none; }
}

/* ── Code panel ───────────────────────────────────────────────────────────── */
/* Dark panel with a line-number gutter, as the hero comparison uses. The comps
   set the body in CD4 (mono 10/12) on prime-grey-950 with an 8px radius. */
.er-code {
  box-sizing: border-box;
  margin: 0;
  padding: var(--er-space-16);
  border-radius: var(--er-radius-sm);
  font-family: var(--er-type-cd4-family);
  font-size: var(--er-type-cd4-size);
  font-weight: var(--er-type-cd4-weight);
  line-height: var(--er-type-cd4-line-height);
  overflow-x: auto;
}
/* tone="dark": a terminal, dark in BOTH themes. bg-prime-inverse flips to white
   in dark mode, which put white text on white here (the exact trap
   CONTRIBUTING.md describes), so the steps are named directly. */
.er-code--dark { background: var(--er-prime-grey-950); }
.er-code--dark .er-code__text { color: var(--er-prime-grey-50); }
.er-code--dark .er-code__kw { color: var(--er-sec-300); }
.er-code--dark .er-code__str { color: var(--er-prime-300); }

/* tone="subtle": the quickstart's panel. It sits inside a card on the page's own
   surface, so unlike the terminal it follows the theme and uses semantic
   tokens throughout. */
/* The quickstart panel sets its code at CD3 (mono 14/20) with a wider gutter,
   which is what the comp measures — the hero's terminal is CD4 because it has
   half the width to work in. */
.er-code--subtle {
  background: var(--er-bg-prime-subtle);
  padding: var(--er-space-16);
  font-size: var(--er-type-cd3-size);
  line-height: var(--er-type-cd3-line-height);
}
.er-code--subtle .er-code__line { gap: var(--er-space-24); }
.er-code--subtle .er-code__num { width: 20px; }
.er-code--subtle .er-code__text { color: var(--er-text-prime-base); }
.er-code--subtle .er-code__kw { color: var(--er-text-sec-brand); }
.er-code--subtle .er-code__str { color: var(--er-text-prime-brand); }

.er-code code { display: block; }
.er-code__line { display: flex; gap: var(--er-space-16); }
/* The gutter is decorative, so it is aria-hidden in the markup and must not be
   selectable — copying the block should yield runnable code, not line numbers. */
.er-code__num {
  flex: none;
  width: 1.5em;
  text-align: right;
  color: var(--er-text-prime-subtle);
  user-select: none;
}
/* Wrap rather than scroll: the comp shows wrapped continuations, and a
   horizontal scrollbar inside the hero reads as a defect. The hanging indent
   keeps a wrapped line clear of the gutter. */
.er-code__text {
  flex: 1;
  min-width: 0;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

/* Prompt row: a mono label on a tinted cell, then the prompt itself. One
   bordered row in the comps, so the two halves share an outline rather than
   each carrying their own. */
.er-prompt {
  border: 1px solid var(--er-border-prime-base);
  border-radius: var(--er-radius-md);
  overflow: hidden;
}
/* Frame 330 is 811 wide at x=193 in a 1197 card — inset by the same 193 on both
   sides, not run to the card's padding as ours was (1150 of 1216).
   Desktop only. On a phone the card drops its own side padding and hands the
   prompt a 16px margin instead (see the rule near .er-scrollrow--fused), and a
   width of 100% sits OUTSIDE those margins — so the row came out 348 wide with
   32 of margin around it inside a 348 box, and ran off the screen. */
@media (min-width: 901px) {
  .er-prompt {
    max-width: 811px;
    margin-inline: auto;
    width: 100%;
  }
}
.er-prompt__label {
  flex: none;
  align-self: stretch;
  display: flex;
  align-items: center;
  padding: var(--er-space-16);
  background: var(--er-bg-prime-subtle);
  border-right: 1px solid var(--er-border-prime-base);
}
/* The padding sits on the TRACK, not on this wrapper. On the wrapper the fade
   would spend its 28px on the 24px of right padding and barely touch the text;
   on the track it scrolls with the content, so the line still starts and ends
   clear of the edges. */
.er-prompt__value { min-width: 0; }
.er-prompt__text {
  padding: var(--er-space-16) var(--er-space-24);
  /* One line whatever the prompt is. Coding fits; Creative Writing did not, and
     wrapped to two — which moved everything below it on every tab change. */
  white-space: nowrap;
}
/* The phone comp sets the prompt at 14/20 in a 12px cell, not 18/21.6 in a 24px
   one — at the wider type it wrapped to four lines and the row measured 164
   against the comp's 86. Measured from Frame 329/364. */
@media (max-width: 900px) {
  .er-prompt__label { padding: var(--er-space-16); }
  .er-prompt__text {
    padding: var(--er-space-12);
    font-size: var(--er-type-cd3-size);
    line-height: var(--er-type-cd3-line-height);
  }
}

/* ── Footer ───────────────────────────────────────────────────────────────── */
/* Brand column then three link columns. The comp sets the brand column wider
   than the rest (240 vs 240 but with the tagline wrapping under a 177px lockup),
   so it takes the remaining space and the link columns are fixed. */
.er-footer { padding-block: var(--er-space-56) var(--er-space-24); }
.er-footer__grid {
  display: grid;
  grid-template-columns: minmax(0, 1.4fr) repeat(3, minmax(0, 1fr));
  gap: var(--er-space-40);
  padding-block: var(--er-space-40);
}
.er-footer__links { list-style: none; margin: 0; padding: 0; }
.er-footer__base { padding-block: var(--er-space-16); }
/* Frame 433 draws the tile as a filled rounded square — 40 at radius 8, fill
   #F2EEE6, no stroke — where this had a border and no fill. radius-sm is
   already 8. */
.er-footer__social {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 40px;
  height: 40px;
  border-radius: var(--er-radius-sm);
  background: var(--er-bg-prime-subtle);
  color: var(--er-icon-prime-subtle);
}
/* Dim on hover rather than naming a surface: a fixed hover background inverts
   its own contrast in one of the two themes. */
.er-footer__social:hover { opacity: 0.7; }

@media (max-width: 900px) {
  .er-footer__grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 560px) {
  /* Two columns, not one: three stacked link lists put the copyright ~940px
     below the CTA. The mobile comp has no footer at all (the designer added it
     to the desktop frame only), so this follows the desktop footer's structure
     at phone width rather than a comp. */
  .er-footer__grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: var(--er-space-32);
  }
  .er-footer__brand { grid-column: 1 / -1; }
  .er-footer__links { gap: var(--er-space-12); }
  .er-footer { padding-block: var(--er-space-40) var(--er-space-24); }
}

/* A section on the tinted container surface — used where the comps set a section
   background a step off the page (the visibility and pricing bands). */
/* #F2EEE6 in the comps, which is bg-prime-subtle (prime-grey-100) — NOT
   bg-prime-main-container, which is grey-0 and renders white. */
.er-section--tinted { background: var(--er-bg-prime-subtle); }
/* Same argument as .er-section--dark, one notch smaller. The comp's tinted
   bands get 194 (benchmark) and 208 (how it works) of separation before them,
   against our flat 160 — a change of ground wants more air than a change of
   subject does. The visibility band is the exception at 119, and it is the one
   whose gap-before in Figma is a stray 37; the two clean cases agree on ~200.
   ⚠️ Only the LEADING edge. Adding it to both would double at the how-it-works
   -> works-with-your-stack boundary, where the comp has the bands abutting. */
@media (min-width: 901px) {
  .er-section--tinted { padding-top: calc(var(--er-space-80) + var(--er-space-64)); }
}

/* Product screenshot: rounded, shadowed, centred and never wider than its
   column. Width comes from the caller so one component covers the comp's
   different sizes. */
.er-shot {
  display: block;
  margin-inline: auto;
  max-width: 100%;
  height: auto;
  border-radius: var(--er-radius-lg);
  box-shadow: var(--er-shadow-1);
  /* A caller can set --er-shot-aspect to crop rather than letterbox. Figma's
     image fills default to Fill, which crops; a shot whose frame in the comp
     does not match the source's own aspect is being cropped there, and
     `height: auto` here quietly undid that. dashboard-visibility.png is
     2086x1592 (1.310) shown in a 750x525 frame (1.429), so ~8% of its height
     is meant to be off. object-fit only bites once an aspect is set. */
  aspect-ratio: var(--er-shot-aspect, auto);
  object-fit: cover;
}

/* ── Dashboard mock ───────────────────────────────────────────────────────── */
/* A picture of the product, drawn rather than photographed. The PNG this
   replaces was captured in light mode, so on the dark page it sat as a white
   slab; markup on tokens follows the theme like the real dashboard does.

   AUTHORED AT 1:1, SCALED HERE. Divide any number in the comp's Figma CSS by
   0.656132 and it lands on a design-system value (20.9962→32, 9.18584→14,
   15.7472→24, 0.656132→1px borders), which says the designer drew the real
   dashboard full size and scaled the frame to sit on the page. So every length
   below is a full-size token value and --er-dashmock-scale is the only thing
   that knows this is a landing page. Re-specifying 40 sizes at 65.6% would put
   all of them off the scale instead. */
.er-dashmock {
  --er-dashmock-w: 1336;
  /* The visible height, NOT the frame's, which measures 791. The last 39px
     run past it, so the table header strip is cut mid-way and the section
     below covers the rest — what the comp does with the request rows it draws
     and never shows. The cut itself is .er-fullvis's overflow, below. */
  --er-dashmock-h: 752;
  position: relative;
  container-type: inline-size;
  width: 100%;
  /* Never bigger than the comp draws it: 1336 × 0.656132. */
  max-width: 876px;
  aspect-ratio: var(--er-dashmock-w) / var(--er-dashmock-h);
  margin-inline: auto;
}

.er-dashmock__frame {
  /* Out of flow, so the 1336px width never reaches the page's layout — the
     wrapper's aspect-ratio reserves the scaled box instead.

     Centring a box WIDER than its containing block is the fiddly part.
     `left: 0; right: 0; margin-inline: auto` does not do it: that is
     over-constrained, so the margins resolve to zero and the frame sits at the
     left edge, then scales about its own centre and ends up 230px off. Scale
     THEN translate, about the top-left corner, with the left edge on the
     wrapper's centre line: local x maps to s·(x − 668), so the frame spans
     ±668s about that line at any scale. Order matters — `translateX(-50%)
     scale()` is the same two operations composed the other way and does not
     centre. */
  position: absolute;
  top: 0;
  left: 50%;
  width: calc(var(--er-dashmock-w) * 1px);
  transform: scale(var(--er-dashmock-scale)) translateX(-50%);
  transform-origin: top left;

  box-sizing: border-box;
  display: flex;
  flex-direction: column;
  gap: var(--er-space-24);
  padding: var(--er-space-24);
  /* The comp's 24px radius is the one number it does NOT scale — it is set on
     the page-level frame, not on the dashboard inside it. radius-xl is 24. */
  border-radius: var(--er-radius-xl);
  background: var(--er-bg-prime-page-body);
  box-shadow: var(--er-shadow-1);
}

/* Scale. The stepped values are a floor for engines that cannot divide a
   length by a length in calc(); each one is the largest that still fits the
   narrowest viewport in its band (content is 100vw-112 above 900px and
   100vw-40 below). Everything since Chrome 133 / Safari 18.4 / Firefox 137
   takes the @supports branch and scales to the column exactly. */
.er-dashmock__frame { --er-dashmock-scale: 0.6562; }
@media (max-width: 1100px) { .er-dashmock__frame { --er-dashmock-scale: 0.59; } }
@media (max-width: 900px)  { .er-dashmock__frame { --er-dashmock-scale: 0.45; } }
@media (max-width: 640px)  { .er-dashmock__frame { --er-dashmock-scale: 0.33; } }
@media (max-width: 480px)  { .er-dashmock__frame { --er-dashmock-scale: 0.21; } }
@supports (transform: scale(calc(1px / 1px))) {
  .er-dashmock__frame {
    /* cqw resolves against .er-dashmock, which is capped at the comp's size —
       so this is "fill the column, but never exceed 0.656132". */
    --er-dashmock-scale: calc(100cqw / (var(--er-dashmock-w) * 1px));
  }
}

/* Heading row: title left, range picker right. */
.er-dashmock__heading {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: var(--er-space-24);
}

/* The range picker is inert — a picture of a segmented control. */
.er-dashmock__range {
  box-sizing: border-box;
  display: flex;
  align-items: center;
  gap: var(--er-space-4);
  padding: var(--er-space-4);
  border: 1px solid var(--er-border-prime-base);
  border-radius: var(--er-radius-sm);
  background: var(--er-bg-prime-subtle);
}
.er-dashmock__range-opt {
  padding: var(--er-space-8) var(--er-space-16);
  border: 1px solid transparent;
  border-radius: var(--er-radius-sm);
  color: var(--er-text-prime-subtle);
  white-space: nowrap;
}
.er-dashmock__range-opt.is-active {
  border-color: var(--er-border-prime-strong);
  background: var(--er-bg-prime-base);
  color: var(--er-text-sec-strong);
}

/* Three equal stat widgets. */
.er-dashmock__stats {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--er-space-32);
}
.er-dashmock__stat { gap: var(--er-space-12); }

/* ── The spend chart ─────────────────────────────────────────────────────── */
.er-dashmock__chart { padding: var(--er-space-32); }
.er-dashmock__plot {
  --er-dashmock-span: 220px;  /* what $45, the axis maximum, is worth */
  --er-dashmock-gutter: 56px; /* the tick column left of the bars */
  /* Day label (20) + its gap (12): the bars' baseline, and the $0 gridline. */
  --er-dashmock-floor: 32px;
  position: relative;
}
.er-dashmock__gridline {
  position: absolute;
  left: var(--er-dashmock-gutter);
  right: 0;
  bottom: calc(var(--er-dashmock-floor) + var(--er-dashmock-tick) * var(--er-dashmock-span));
  border-top: 1px dashed var(--er-border-sec-base);
}
.er-dashmock__tick {
  position: absolute;
  right: calc(100% + var(--er-space-16));
  transform: translateY(-50%);
  white-space: nowrap;
  /* The faintest text the system has: an axis should not compete with the
     figures on it. The comp's sec-grey-400 has no semantic token. */
  color: var(--er-text-prime-inactive);
}
.er-dashmock__bars {
  /* Positioned so the bars paint OVER the gridlines. DOM order alone does not
     do it: the gridlines are absolutely positioned and the bars were not, and
     a positioned box paints in a later layer than its static siblings whatever
     the source order — so every dashed line ran straight across the bars. */
  position: relative;
  display: flex;
  align-items: flex-end;
  margin-left: var(--er-dashmock-gutter);
}
.er-dashmock__col {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--er-space-12);
}
.er-dashmock__value,
.er-dashmock__day { color: var(--er-text-sec-subtle); }
.er-dashmock__col.is-today .er-dashmock__value,
.er-dashmock__col.is-today .er-dashmock__day { color: var(--er-text-sec-strong); }
.er-dashmock__bar {
  width: 64px;
  height: calc(var(--er-dashmock-bar) * var(--er-dashmock-span));
  /* 4px, off the radius scale — the comp's 2.62453 ÷ 0.656132. radius-sm (8)
     on a 64px bar reads as a lozenge. Same call as the 1px and 2px radii
     elsewhere in this file. */
  border-radius: 4px 4px 0 0;
  /* The quiet bars are the brand blue held back against the card, so the
     relationship to the highlighted bar survives the theme flip. A literal
     sec-200 would not: in dark mode the pale bars would outshine the
     highlight and invert the emphasis. Wants a real bg-sec-brand-muted —
     the same gap bg-sec-muted was added for; see tokens/overrides.json. */
  background: color-mix(in srgb, var(--er-bg-sec-brand) 30%, var(--er-bg-prime-base));
}
.er-dashmock__col.is-today .er-dashmock__bar { background: var(--er-bg-sec-brand); }

/* ── Recent requests: the header strip, and nothing under it ─────────────── */
.er-dashmock__thead {
  box-sizing: border-box;
  display: flex;
  align-items: center;
  gap: var(--er-space-32);
  padding: var(--er-space-24) var(--er-space-32);
  border: 1px solid var(--er-border-prime-base);
  border-radius: var(--er-radius-lg) var(--er-radius-lg) 0 0;
  background: var(--er-bg-prime-subtle);
  color: var(--er-text-prime-subtle);
}
/* Column widths measured off the comp's header row. Time and Model take the
   slack; the rest are fixed, as they are in the real table. */
.er-dashmock__th { flex: 0 0 auto; white-space: nowrap; }
.er-dashmock__th:nth-child(1),
.er-dashmock__th:nth-child(2) { flex: 1 1 0; }
.er-dashmock__th:nth-child(3) { flex-basis: 149px; }
.er-dashmock__th:nth-child(4) { flex-basis: 113px; }
.er-dashmock__th:nth-child(5) { flex-basis: 107px; }
.er-dashmock__th:nth-child(6) { flex-basis: 96px; }

/* The frame is a fixed-width PICTURE of a 1336px dashboard, so it must render
   its desktop form at every viewport — the scale is the only thing allowed to
   change. But the design system's phone rules key off the VIEWPORT, not off
   the frame, so at 820px they reach inside and restyle a mock that is still
   "1336px" internally: the frame lost 56px of internal height that way, which
   the wrapper's aspect-ratio had already reserved, and the overhang the crop
   depends on turned into a gap.

   Two rules bite today. Both are single-class, so naming the frame is enough
   to outrank them wherever they sit in the file. check-layout.mjs asserts the
   frame measures the same at every viewport, so a third one cannot land
   quietly. */
.er-dashmock__frame .er-card { padding: var(--er-space-24); }
.er-dashmock__frame .er-dashmock__chart { padding: var(--er-space-32); }
.er-dashmock__frame .er-h3 { font-size: 28px; line-height: 36px; }

/* Quote cards in a row: the attribution should sit on one line across the row
   regardless of quote length, so the figure fills the card and the rule before
   the caption takes the slack. Without this the captions stagger. */
.er-card > figure { flex: 1; display: flex; flex-direction: column; }
.er-card > figure > .er-divider { margin-top: auto !important; }
/* The attribution block is two lines of text beside a 64px avatar. A name that
   wraps ('Marvin McKinney') makes its block taller than its neighbours', which
   floats that card's avatar and rule out of line with the rest of the row.
   Reserving the two-line height keeps every avatar and rule on one line across
   the row, whatever the names are — and the names are placeholders, so this
   must not depend on them. */
/* Two lines of body text (2 x 27px) plus the avatar's overhang. min-height was
   not enough — a wrapped name pushes past it, so the block is fixed and the name
   clamps. The names are placeholders and will change; the row must not
   re-align itself every time one of them gets longer. */
.er-card > figure > figcaption { height: 90px; display: flex; align-items: center; }
.er-card > figure > figcaption .er-b1 {
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

/* Pills carry short labels but not always short enough for a 402px card; let one
   wrap its own text rather than forcing its row wider than the viewport. */
.er-pill { max-width: 100%; }

/* ── Step card ────────────────────────────────────────────────────────────── */
/* The comps: 246×252 cards, 24px padding, a 64px tinted icon tile, and the step
   number in the top-right corner. The number is positioned rather than in flow
   so it stays put whatever the title wraps to. */
.er-step {
  box-sizing: border-box;
  position: relative;
  display: flex;
  flex-direction: column;
  gap: var(--er-space-16);
  padding: var(--er-space-24);
  border-radius: var(--er-radius-lg);
  border: 1px solid var(--er-border-prime-base);
  background: var(--er-bg-prime-base);
}
.er-step__tile {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 64px;
  height: 64px;
  border-radius: var(--er-radius-md);
  /* Set per tone below; both halves must move together. */
  background: var(--er-step-tile, var(--er-bg-prime-subtle));
  color: var(--er-step-ink, var(--er-text-prime-base));
}
.er-step__tile > svg { width: 48px; height: 48px; }
.er-step__n {
  position: absolute;
  top: var(--er-space-24);
  right: var(--er-space-24);
  font-family: var(--er-type-h5-family);
  font-size: var(--er-type-h5-size);
  font-weight: var(--er-type-h5-weight);
  line-height: 1;
  color: var(--er-text-prime-inactive);
}
/* Each step's hue. The 100 step tints the tile, the 900 step inks the glyph —
   the pairing the comps use, and the reason both are set together. */
.er-step--yellow  { --er-step-tile: var(--er-yellow-100);  --er-step-ink: var(--er-yellow-900); }
.er-step--cyan    { --er-step-tile: var(--er-cyan-100);    --er-step-ink: var(--er-cyan-900); }
.er-step--blue    { --er-step-tile: var(--er-blue-100);    --er-step-ink: var(--er-blue-900); }
.er-step--purple  { --er-step-tile: var(--er-purple-100);  --er-step-ink: var(--er-purple-900); }
.er-step--magenta { --er-step-tile: var(--er-magenta-100); --er-step-ink: var(--er-magenta-900); }
.er-step--green   { --er-step-tile: var(--er-green-100);   --er-step-ink: var(--er-green-900); }
.er-step--orange  { --er-step-tile: var(--er-orange-100);  --er-step-ink: var(--er-orange-900); }

/* On phones the comp turns the step card on its side: the tile moves beside the
   text rather than above it, which takes a 252px card down to 120. Same padding,
   same gap — only the axis changes, so it is a media query and the component
   stays width-agnostic. */
@media (max-width: 900px) {
  .er-step {
    flex-direction: row;
    align-items: flex-start;
    gap: var(--er-space-16);
  }
  /* The number would otherwise sit over the text it shares a row with. */
  .er-step__n { top: var(--er-space-16); right: var(--er-space-16); }
  .er-step > .er-step__tile { margin-top: var(--er-space-4); }
  /* Leave room for the number in the corner. */
  .er-step > *:last-child { padding-right: var(--er-space-32); }
}

/* ── Split section ────────────────────────────────────────────────────────── */
/* Heading on the left, content on the right. The comps use it for Works with
   your stack (544px heading beside a 696px tile grid). */
.er-split {
  display: grid;
  grid-template-columns: minmax(0, 544fr) minmax(0, 696fr);
  gap: var(--er-space-56);
  align-items: center;
}
@media (max-width: 900px) {
  .er-split { grid-template-columns: minmax(0, 1fr); gap: var(--er-space-32); }
  /* The comp centres this heading once it is no longer beside the tiles. */
  .er-split .er-heading { align-items: center; text-align: center; }
  .er-split .er-heading__row { align-items: center; }
}

/* A logo tile: mark above a name, on the subtle surface. */
.er-logotile {
  box-sizing: border-box;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--er-space-16);
  padding: var(--er-space-24);
  min-height: 132px;
  border-radius: var(--er-radius-lg);
  background: var(--er-bg-prime-subtle);
  text-align: center;
}
.er-logotile > svg { flex: none; }

/* Phones: the comp keeps two columns and lays each tile out on its side, so six
   tiles take three short rows instead of six tall ones. */
@media (max-width: 900px) {
  /* Override the resolved property, not --er-grid-cols: Grid sets that variable
     as an inline style, which a class rule cannot beat. */
  .er-split > .er-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .er-logotile {
    flex-direction: row;
    justify-content: flex-start;
    gap: var(--er-space-8);
    min-height: 0;
    padding: var(--er-space-16);
    text-align: left;
  }
  .er-logotile > svg { width: 24px; height: 24px; }
  /* Names sit on one line beside the mark in the comp; the display step wraps
     'Codex CLI' and 'Custom agents' onto two and doubles the tile height. */
  .er-logotile > .er-dh5 { font-size: var(--er-type-b1-size); line-height: var(--er-type-b1-line-height); }
}

/* Inside a split column the heading has ~544px, not the full container. Let the
   display line use all of it, and stack the lede's action underneath rather than
   squeezing both onto one row. */
.er-split .er-heading { max-width: none; }
.er-split .er-heading__row {
  flex-direction: column;
  align-items: flex-start;
  gap: var(--er-space-16);
}

/* The closing panel's headline breaks over two lines with the second half in
   the brand gold; the lede is a muted blue-grey, not white at reduced opacity. */
.er-cta__title { max-width: 20ch; }
/* __accent and __lede are defined with .er-dark, up with the shared band. */

/* ── Credit band ──────────────────────────────────────────────────────────── */
/* The two prepaid cards. In the comp they are a centred row of two 528px cards
   32 apart, and because Bulk buy is taller (614 vs 466) centring is what lifts
   it above Pay as you go. So the offset is not a nudge — it falls out of
   align-items, and it degrades correctly when the cards stack.

   The comp's 1088px content is narrower than the 1328 container, so the row is
   capped and centred rather than filling the column. */
.er-credit__cards {
  display: flex;
  align-items: center;
  gap: var(--er-space-32);
  width: 100%;
  max-width: 1088px;
  margin-inline: auto;
}
.er-credit__card {
  flex: 1 1 0;
  min-width: 0;
  /* The comp sets radius 12 and a 32 gap between the card's blocks; padding is
     the default card's 24, so it is not restated here. */
  gap: var(--er-space-32);
  border-radius: var(--er-radius-md);
}
/* The heading sits on the band, so it is centred and given the comp's measure
   rather than the container's full width. */
/* The comp gives the headline a 624px measure inside the 1088 content column,
   which is what breaks it after the first clause. Expressed in ch so it tracks
   the type scale rather than the frame width. */
.er-credit__title { max-width: 22ch; margin-inline: auto; }
/* Same 624px measure for the lede, at body size. */
.er-section--dark .er-dark__lede { max-width: 62ch; margin-inline: auto; }

@media (max-width: 900px) {
  /* Stacked, full width, no offset — the mobile comp puts Pay as you go first
     and gives both cards the whole column. */
  .er-credit__cards {
    flex-direction: column;
    align-items: stretch;
    gap: var(--er-space-24);
  }
  .er-credit__card { gap: var(--er-space-24); }
  /* The mobile comp drops the card titles a step, to 20/28. */
  .er-credit__cardtitle { font-size: 20px; line-height: 28px; }
  .er-credit__title { max-width: none; }
}

/* ── Scroll row ───────────────────────────────────────────────────────────── */
/* A grid that becomes a swipeable row on phones. The comps do this for the hero
   comparison: at 402 the two panes stay side by side and scroll, rather than
   stacking into a very tall column. Each pane keeps a readable width and snaps.
   Kept as its own class so a Grid that SHOULD stack still does. */
@media (max-width: 900px) {
  .er-scrollrow {
    display: flex;
    grid-template-columns: none;
    overflow-x: auto;
    scroll-snap-type: x mandatory;
    /* Bleeds to the screen edge as the comp does, while the first pane stays
       aligned with the container. `width: auto` is load-bearing: without it the
       negative margins add to a 100% width and push the document wider than the
       viewport. */
    /* Bleed by exactly the container's own inline padding, whatever that is —
       a fixed -24px overshoots when the row sits directly in a Section rather
       than inside a Card, and pushes the document past the viewport. */
    --er-bleed: var(--er-space-24);
    margin-inline: calc(var(--er-bleed) * -1);
    width: auto;
    padding-inline: var(--er-bleed);
    scrollbar-width: none;
  }
  /* Directly inside a section container there is no card padding to give back. */
  .er-container > .er-scrollrow { --er-bleed: 0px; }
  /* The testimonial track sits one level deeper (inside .er-quoterow, which
     carries the desktop fade), so the rule above no longer reaches it and it
     would bleed by the default 24 on each side — straight past the viewport. */
  .er-container > .er-quoterow > .er-scrollrow { --er-bleed: 0px; }
  /* Inside a card the bleed is that card's OWN padding, which is 16 at this
     width, not the 24 the default assumed. Overshooting by 8 shrank every pane
     and was what wrapped the fused pane's title and chip rows onto second
     lines — 64px of the section's 260px excess. */
  .er-card > .er-scrollrow { --er-bleed: var(--er-space-16); }
  .er-scrollrow::-webkit-scrollbar { display: none; }
  .er-scrollrow > * {
    /* The comp's pane is 361 of a 402 screen — nearly full width, with the next
       one just showing at the edge. Measured from Frame 340. */
    flex: 0 0 min(100%, 420px);
    scroll-snap-align: start;
  }
}

/* ── Doc card ─────────────────────────────────────────────────────────────── */
/* Icon tile beside the title and body, a rule, then the link pinned to the
   bottom so the links align across a row of uneven cards. Tones reuse the step
   card's tile variables, so one rule set serves both. */
.er-doccard {
  box-sizing: border-box;
  display: flex;
  flex-direction: column;
  gap: var(--er-space-24);
  padding: var(--er-space-24);
  border-radius: var(--er-radius-lg);
  border: 1px solid var(--er-border-prime-base);
  background: var(--er-bg-prime-base);
}
.er-doccard__head { display: flex; gap: var(--er-space-16); align-items: flex-start; }
.er-doccard__tile {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 56px;
  height: 56px;
  border-radius: var(--er-radius-md);
  background: var(--er-step-tile, var(--er-bg-prime-subtle));
  color: var(--er-step-ink, var(--er-text-prime-base));
}
.er-doccard__foot { margin-top: auto; padding-top: var(--er-space-24); border-top: 1px solid var(--er-border-prime-base); }
/* A Section lays its children out in flow; the grid and the SDK panel below it
   need the section's own gap, not zero. */
.er-section > .er-container > * + * { margin-top: var(--er-space-40); }
/* The comp sets 64 between a centred headline and the first question, not the
   section's default 40. This has to sit here, at the same specificity as the
   rule above and after it — written beside .er-accordion--narrow it lost
   silently, which is the source-order trap and not a specificity one. */
.er-section > .er-container > * + .er-accordion--narrow { margin-top: var(--er-space-64); }

/* ── Step row ─────────────────────────────────────────────────────────────── */
/* Round icon tile beside a title and one line of body — the quickstart's four
   steps. The tile size follows the step-card tiles' precedent of a literal px
   value: there is no size token for icon tiles, and inventing one for a single
   pair of values would be worse than matching what is already here. */
.er-steprow {
  display: flex;
  align-items: center;
  gap: var(--er-space-16);
}
.er-steprow__tile {
  display: inline-flex;
  flex: none;
  align-items: center;
  justify-content: center;
  width: 64px;
  height: 64px;
  border-radius: var(--er-radius-round);
  background: var(--er-bg-prime-subtle);
  border: 1px solid var(--er-border-prime-base);
  color: var(--er-icon-prime-subtle);
}
.er-steprow__tile > svg { width: 32px; height: 32px; }
@media (max-width: 900px) {
  .er-steprow__tile { width: 56px; height: 56px; }
}

/* ── Code panel header ────────────────────────────────────────────────────── */
/* Title and subtitle on the left, an action on the right — the bar above the
   quickstart's code panel. It wraps rather than squeezing the action, which is
   what the phone comp does with the same content. */
.er-codehead {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--er-space-16);
  flex-wrap: wrap;
}

/* ── Quickstart ───────────────────────────────────────────────────────────── */
/* The comp's panel is as tall as the steps beside it, with the code filling
   whatever is left under the header and tabs rather than leaving a gap at the
   bottom of the card. Cards are already flex columns, so the code block only
   has to be told to grow. */
.er-quickstart__panel { gap: var(--er-space-24); }
.er-quickstart__panel .er-code { flex: 1; }
/* The phone comp drops the code panel and stacks the two buttons full width.
   Both are width decisions, so both live here rather than in the page. */
@media (max-width: 900px) {
  .er-quickstart__panel { display: none; }
  .er-quickstart__cta {
    /* Stack writes display:flex inline, so a class cannot re-lay it out — but
       it can still change direction and alignment, which is all that is needed
       here. See the warning in CONTRIBUTING.md. */
    flex-direction: column;
    align-items: stretch;
    gap: var(--er-space-16);
  }
  .er-quickstart__cta .er-btn { width: 100%; }
}

/* ── Hero ─────────────────────────────────────────────────────────────────── */
/* The comp's phone hero is 430px against 1160 on desktop, and most of that is
   spacing rather than type. Stack writes its gap inline, so these have to be
   !important to reach it — the alternative is a prop that reads the viewport,
   which components must not do. */
@media (max-width: 900px) {
  .er-hero { gap: var(--er-space-32) !important; }
  .er-hero > * { gap: var(--er-space-24) !important; }
  /* The two headline lines are one block in the comp, not two spaced lines. */
  .er-hero > * > * { gap: var(--er-space-8) !important; }
}

/* On phones the comp puts a meter's label on the same line as its track rather
   than above it — five paired meters is a lot of vertical space otherwise. */
@media (max-width: 900px) {
  .er-meter { flex-direction: row; align-items: center; gap: var(--er-space-8); }
  /* A fixed label column, so every bar in a stack starts at the same x and is
     the same length. Without it "EastRouter 5" (69px) and "GPT 5.6" (44px)
     pushed their tracks to 77 and 52 and made one bar 25px longer than the
     other, which reads as a difference in the score rather than in the name.

     This rule existed and had stopped matching: it selected .er-cap2, and the
     label moved to p5 when the comps turned out to set it in sentence case
     rather than uppercase. It has its own class now so the two cannot drift
     apart again. */
  .er-meter > .er-meter__label { flex: none; width: 8.5em; }
  .er-meter__row { flex: 1; min-width: 0; }
}
/* The comps left-align the category name over its column at BOTH widths (16/700
   in Frame 313); it had been centred on desktop. */

/* Phones: the comp drops the doc card's rule and link, leaving the tile, title
   and body — the whole card is the tap target at that size, so a separate link
   row is a second control for the same destination and a lot of height across
   six cards. */
@media (max-width: 900px) {
  /* The phone comp's card is 132 tall with 24 padding — hiding the footer link
     already took ~50px out of these, so 16 undershot it. Measured from Frame
     120 in the mobile comp, not tuned by eye. */
  .er-doccard { gap: var(--er-space-16); padding: var(--er-space-24); }
  .er-doccard__foot { display: none; }
  .er-doccard__tile { width: 48px; height: 48px; }
  .er-doccard__tile > svg { width: 24px; height: 24px; }
}
/* The card may be an anchor; it must not take the browser's link styling. */
a.er-doccard { text-decoration: none; color: inherit; display: flex; }

/* The comp leads the hero comparison with the fused result on phones — that is
   the pane worth seeing first when only one fits. Desktop keeps the single
   model on the left, so this is order, not markup. */
@media (max-width: 900px) {
  .er-scrollrow--fused > :first-child { order: 2; }
  /* Frame 344 is 401 wide on a 402 screen — the card runs to both edges rather
     than sitting in the page gutter, so the panes are not squeezed into the
     middle with the page showing either side of them. Bleeding by the
     container's own gutter is what reaches the edge without widening the
     document. */
  .er-card:has(> .er-scrollrow--fused) {
    padding-inline: 0;
    margin-inline: calc(var(--er-space-20) * -1);
    width: auto;
    border-radius: 0;
    border-inline: 0;
  }
  .er-card:has(> .er-scrollrow--fused) > .er-prompt { margin-inline: var(--er-space-16); }
  /* Inset on BOTH sides. With padding only on the left, whichever pane you
     scrolled to ended up flush against a screen edge — the fused one at x=0 on
     arrival, the Claude one at the right edge once scrolled. Each now keeps the
     prompt row's own 16px inset at whichever end it lands.

     scroll-padding matters as much as padding here: snap alignment measures
     from the scrollport, which is the BORDER box, so `scroll-snap-align: start`
     was parking the pane against the container's edge and cancelling the left
     padding outright — the row rested at scrollLeft 16, exactly the padding it
     was meant to leave. */
  .er-card > .er-scrollrow--fused {
    --er-bleed: 0px;
    padding-inline: var(--er-space-16);
    scroll-padding-inline: var(--er-space-16);
  }
  /* 88% leaves the comp's sliver of the next pane visible at 402. */
  .er-scrollrow--fused > * { flex-basis: min(88%, 420px); }
  /* The comp sets each pane's title at 14px on a phone, not H6's 18 — at 18 the
     title and its pill summed to 368 into 326 and wrapped onto two lines,
     costing 20px per pane. Measured from Frame 349. */
  .er-resultpane__head .er-h6 {
    font-size: var(--er-type-cd3-size);
    line-height: var(--er-type-cd3-line-height);
  }
}

/* On phones the comp shows the top of the snippet in a fixed-height panel
   rather than the whole file — 263px against the ~380 sixteen wrapped lines
   need. The panel scrolls, so nothing is lost. */
@media (max-width: 900px) {
  .er-code { max-height: 264px; overflow-y: auto; }
}

/* Phone panes are ~380px wide: at 10px the snippet wraps every few tokens and
   the chip rows stack. The comp keeps both compact. */
@media (max-width: 900px) {
  /* A narrower gutter and tighter padding buy back most of the wrapping without
     going below the comp's 10px, which is already the smallest legible step. */
  .er-code { padding: var(--er-space-12); }
  .er-code__line { gap: var(--er-space-8); }
  .er-code__num { width: 1.2em; }
  /* The comp runs 'Fused result · 2 models combined' as one line. */
  .er-scrollrow .er-pill { font-size: 10px; padding-inline: var(--er-space-8); }
}

/* Phones: a Grid's gap is an inline style, so a stacked two-column split keeps
   its 64px desktop gutter as vertical space between the halves. The comps use
   the section's own rhythm instead. */
@media (max-width: 900px) {
  .er-grid { gap: var(--er-space-24) !important; }
  /* Cards carry desktop padding at 402 too; the comps drop a step. */
  .er-card { padding: var(--er-space-16); }
  .er-card--pad-lg { padding: var(--er-space-24); }
}

/* ── Phone-only omissions ─────────────────────────────────────────────────── */
/* The mobile comp does not simply reflow every section — in several it drops
   supporting copy that the desktop layout has room for. The visibility section
   is the clearest case: at 1440 it is eyebrow + headline + lede + link over the
   screenshot; at 402 the comp shows the headline, the screenshot and the link,
   and nothing else. Reflowing what the comp deletes cost that section 249px.
   ⚠️ display:none on a CHILD is safe even inside a Stack — the trap is that a
   class cannot re-lay-out the Stack ITSELF, whose display is written inline. */
@media (max-width: 900px) {
  .er-hide-phone { display: none; }
  /* The comp opens the centred heading block up a step on the phone (24 between
     eyebrow and headline, not 16) and then sets the content much closer under
     it — 16, against the section's default 40. */
  .er-heading--center { gap: var(--er-space-24); }
  .er-section > .er-container > * + .er-tablewrap { margin-top: var(--er-space-16); }
}

/* ── Section: tinted on desktop only ──────────────────────────────────────── */
/* The comps paint the visibility section's band #F2EEE6 at 1440 but leave it on
   the page body (#FAF8F2) at 402 — sampled from both comp renders, not assumed.
   A band that runs the full width of a phone reads as a box around the section
   rather than as a change of ground, which is presumably why. */
@media (max-width: 900px) {
  .er-section--tinted-wide { background: none; }
}

/* ── Phone-only reordering ────────────────────────────────────────────────── */
/* The visibility section puts its "Explore the dashboard" link ABOVE the
   screenshot at 1440 and BELOW it at 402 — measured in both comps, where the
   link sits at y6649 (above the 6715 image) on desktop and y7245 (below the
   6968 image) on mobile.

   The link sits in DESKTOP order in the DOM (before the screenshot), and this
   pushes it last on phones. `order` on a flex child avoids writing the link
   into the markup twice, which would put two identical links in the
   accessibility tree with one of them always hidden.
   Safe inside .er-section: its display:flex comes from a CLASS, not from
   Stack's inline style, so its children can be reordered.
   ⚠️ Reordering visually without reordering the DOM breaks the correspondence
   between reading order and tab order. It is defensible for exactly two
   siblings, one of which is a decorative image; do not grow this into a
   general-purpose reordering utility. */
/* ⚠️ `order` only does anything inside a flex or grid container, and Section
   wraps its children in .er-container, which is a plain BLOCK. Setting order on
   a Section child therefore RESOLVES — computed style reads order:1 — and
   changes nothing at all. Found by reading the parent's computed display, not
   by looking at the page, which showed no clue. The column has to opt into
   being the flex context, which is what .er-flow is for.

   It also has to take over the spacing: sections space their children with
   `> * + *` margins (below), and a margin on the adjacent SIBLING is wrong the
   moment anything is reordered — the visually-first child would carry a top
   margin. A flex `gap` has no such directionality, so .er-flow zeroes the
   margins and owns the spacing itself, matching the section's own steps. */
.er-flow {
  display: flex;
  flex-direction: column;
  gap: var(--er-space-40);
}
.er-section > .er-flow > * + * { margin-top: 0; }
/* The link is a Section child (it has to be, to move relative to the image), so
   it would otherwise take the section's 40px step on both sides. The comps put
   24 between the lede and the link — the same step as inside the heading block
   it visually belongs to — and keep the section's own step below it. */
.er-flow > .er-order-phone-last { margin-top: calc(var(--er-space-24) - var(--er-space-40)); }
@media (max-width: 900px) {
  .er-flow { gap: var(--er-space-24); }
  /* Matched to the desktop rule's specificity (0,2,0). As a bare class this
     lost to it and never applied, so on a phone the link kept the desktop's
     -16 and sat 8px under the screenshot. `order` was unaffected, which is why
     the reorder worked and the spacing did not.

     25 between the shot and the link, per the design; the column's own gap is
     24, so this is the last pixel of it rather than a number of its own. */
  .er-flow > .er-order-phone-last {
    order: 1;
    margin-top: calc(25px - var(--er-space-24));
  }
}


/* The phone comp draws no rule inside a testimonial card — but the space it
   holds is still there, so the line is hidden rather than removed. Taking the
   element out, or zeroing its margin, would close a 32px gap the design keeps. */
@media (max-width: 900px) {
  .er-quoterow .er-card > figure > .er-divider { border-top-color: transparent; }
}

/* ── Centred section measure ──────────────────────────────────────────────── */
/* The comps set these centred sections on a 632px column inside the 1440 frame,
   not the full 1328 container. The cap is what wraps their ledes to two lines
   instead of one; without it the section runs ~70px short of the comp.

   ⚠️ 700, not the comp's 632. The comp is set in Minion Pro, which is not a
   webfont — the page substitutes Source Serif 4, which is wider. "Every model
   has a specialty." measures 684px here against 632 in Figma, so the comp's own
   box would wrap a headline the comp sets on ONE line. 700 clears both display
   lines in this section (684 and 663, measured in the browser) and so preserves
   the comp's line structure, which is the thing worth matching. Re-measure if
   the display face ever changes.
   At 402 the column is narrower than the cap, so it simply stops applying. */
.er-centred-measure { max-width: 700px; margin-inline: auto; width: 100%; }

/* The hero comparison's pane header and chip row each sit on ONE line in the
   mobile comp — the header with a single merged pill, the chips clipped at the
   pane edge. Wrapping them costs ~150px across the two panes. */
/* "Make text one line and auto scroll to the left and to the right" — designer,
   Figma. This is the row he meant: Frame 34 is 535 x 20 and holds 575 of
   content, so it overflows by 40 and carries a 28px gradient (Rectangle 26) at
   its right edge. One line, and a fade saying the line continues.

   This treatment used to be scoped to phones. On desktop the fused pane's row
   wrapped to two lines — measured 513 x 32 against the comp's 20 — which is the
   thing the note was about.

   28px, from the comp, rather than a percentage. On a masked element a
   percentage resolves against the mask box, and that is fine here because the
   row clips rather than scrolls; but a px stop stays right if it ever does
   scroll, and matches the drawn rectangle either way. */
/* ── Drag row ─────────────────────────────────────────────────────────────── */
/* A line of content that stays one line and can be dragged sideways, fading at
   whichever edge still has something behind it. Two places on this page need it
   and the comp draws both the same way: the result pane's summary line (Frame
   34, 535 wide holding 575) and the demo card's prompt (Frame 329, a fixed 54
   tall — 16, a 20 line box, 16 — however long the prompt is).

   Always a WRAPPER around a TRACK. The wrapper carries the fade and does not
   scroll, so its 100% is the width you can see; on the scrolling track a mask
   percentage would resolve against the SCROLL width instead and land far past
   the visible edge. The track scrolls and keeps its own layout — the chips are
   a flex row, the prompt a single nowrap line — so the track rule sets scrolling
   and nothing else. */
.er-dragrow {
  /* How wide this row's fade is; each row sets its own. The chip and prompt
     rows take the comp's 28 (Rectangle 26); the testimonial row takes 96,
     because it is fading a 426px card rather than a line of 12px text. */
  --er-dragrow-fade: 28px;
  --er-dragrow-fade-l: 0px;
  --er-dragrow-fade-r: 0px;
  -webkit-mask-image: linear-gradient(to right,
    transparent 0, #000 var(--er-dragrow-fade-l),
    #000 calc(100% - var(--er-dragrow-fade-r)), transparent 100%);
          mask-image: linear-gradient(to right,
    transparent 0, #000 var(--er-dragrow-fade-l),
    #000 calc(100% - var(--er-dragrow-fade-r)), transparent 100%);
}
/* useDragScroll sets the attributes; the width stays in CSS, so a row changes
   its fade by setting one custom property rather than by telling JS a pixel
   count. */
.er-dragrow[data-fade-l] { --er-dragrow-fade-l: var(--er-dragrow-fade); }
.er-dragrow[data-fade-r] { --er-dragrow-fade-r: var(--er-dragrow-fade); }
.er-dragrow__track {
  overflow-x: auto;
  /* No scrollbar: the fades are the affordance, and a 12px-tall row with a
     scrollbar under it is mostly scrollbar. */
  scrollbar-width: none;
  /* Let a trackpad swipe leave the row once it reaches an end rather than
     trapping the page's own scroll. */
  overscroll-behavior-x: contain;
}
.er-dragrow__track::-webkit-scrollbar { display: none; }
/* Only while the row actually has somewhere to go — grab on a row that fits is
   a promise it cannot keep. Set by useDragScroll alongside the fades. */
.er-dragrow[data-scrollable] .er-dragrow__track { cursor: grab; }
.er-dragrow[data-dragging] .er-dragrow__track {
  cursor: grabbing;
  /* Otherwise the drag selects the text instead of moving the row. */
  user-select: none;
}

.er-resultpane__chips { flex-wrap: nowrap; }
/* Reserve the Top-result chip's height whether or not the pane has one. See the
   note at the markup: without it the chipless pane's bar sat 10px high. */
.er-resultpane__score { min-height: 24px; }
/* Each label keeps its own width instead of being squeezed to fit — squeezing
   is what puts a line break inside "Strict types across every path". */
.er-resultpane__chips > * { flex: none; }
/* The pane's HEADER row is deliberately not nowrap: forcing it collided the
   title with the pills at 402. The comp merges its two pills into one there
   instead, so that row fits without being told to. */

/* The pill-merging rules that were here are gone with "2 models combined".
   They existed so the pane header could show two pills at 1440 and one merged
   phrase at 402; with a single "Fused result" chip there is nothing to merge,
   and it fits at both widths. */

/* The wordmark beside the mark in the top-left.
   dh5 is a HEADING style and carries the heading weight, 600. Set on a logo,
   that is what "the font looks fat" turned out to mean.

   The brand weight is Minion Pro SEMIBOLD — the designer's, 2026-09-01, and the
   weight he holds the Adobe Fonts licence for. That is nominally 600, the same
   number we were already using, which is the whole trap: Source Serif 4 runs
   visibly heavier than Minion at the same number, so matching the number does
   not match the weight.

   So 500 is a STAND-IN, not the target. Measured against his lockup, Source
   Serif 4 at 500 sits about where Minion Semibold does. When Minion is served
   from Adobe Fonts this should go back to 600 and the substitution rule can go
   with it — see tokens/fonts.json. dh1's 500 is borrowed by name rather than
   typed, because the scale has no 400. */
.er-logo > span { font-weight: var(--er-type-dh1-weight); }

/* Head to head — Frame 398. Five suites side by side, ruled apart.
   The card's own gap is 24, not the Card default's 16. */
.er-h2h { gap: var(--er-space-24); }

/* The comp lays this out as nine flex children — five 212 columns and four
   Line elements — 24 apart, so a column's content sits 24 from the rule on
   either side. Here the rule is a pseudo-element centred in the column gap,
   which gives the same 24 + rule + 24 while leaving all five columns exactly
   equal. Doing it as `border-left` on the columns instead, which is what the
   previous five-across did, takes the border and its padding out of four of
   the five column widths and makes the first bar in the row longer than the
   rest.

   The gap is a variable because the rule has to sit at half of it and because
   .er-grid forces its own gap back with !important at phone width — one place
   to change, and the rule cannot end up drawn at a column's edge. */
.er-h2h__grid {
  --er-h2h-gap: var(--er-space-48);
  column-gap: var(--er-h2h-gap);
}
.er-h2h__col { position: relative; }
.er-h2h__col + .er-h2h__col::before {
  content: '';
  position: absolute;
  left: calc(var(--er-h2h-gap) / -2);
  top: 0;
  bottom: 0;
  border-left: 1px solid var(--er-border-prime-base);
}

/* Below the comp's width the five columns get too narrow to hold a model name,
   a bar and a score on one line, so they wrap — but not straight to one, which
   at tablet width stretches a 74.0 bar across 700px and reads as a progress
   indicator rather than a comparison. Three, then two, then one.

   At each step the rule is dropped from whichever column now starts a row,
   where it would draw against the grid's left edge instead of between two
   columns. */
@media (max-width: 1100px) {
  .er-h2h__grid { grid-template-columns: repeat(3, minmax(0, 1fr)); row-gap: var(--er-space-32); }
  .er-h2h__col + .er-h2h__col:nth-child(3n + 1)::before { content: none; }
}
@media (max-width: 900px) {
  .er-h2h__grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    /* Matches the gap .er-grid re-asserts with !important at this width. */
    --er-h2h-gap: var(--er-space-24);
  }
  .er-h2h__col + .er-h2h__col:nth-child(3n + 1)::before { content: ''; }
  .er-h2h__col + .er-h2h__col:nth-child(odd)::before { content: none; }
}
@media (max-width: 640px) {
  .er-h2h__grid { grid-template-columns: minmax(0, 1fr); }
  /* :nth-child(n) matches everything and only exists to carry the same
     specificity as the two rules above, which would otherwise outrank it. */
  .er-h2h__col + .er-h2h__col:nth-child(n)::before { content: none; }
}

/* SDK row: a mark above a sentence-case name, as the comp arranges it. */
/* Just the 32 tile the comp gives each mark. The bordered, tinted chip that was
   here existed to make a two-letter monogram look deliberate; the real logos
   sit bare on the card, as Frame 234 has them, and a border around the yellow
   JavaScript tile or the blue TypeScript one reads as a mistake. */
.er-sdk__mark {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
}

/* The SDK card is a row, not two stacked bands: Frame 125 is 1071 x 142 with
   24 padding and a 48 gap, holding a 386-wide intro column and then the marks.
   386 + 48 + 587 = 1021 against the 1023 of content the padding leaves, so the
   intro is fixed and the marks take the remainder. */
.er-sdkcard__intro { flex: 0 0 386px; }
.er-sdks { flex: 1 1 auto; }
/* Six fixed tiles of the comp's own 77.83, rounded to 78, with the row centred
   in whatever space is left — 6 x 78 + 5 x 24 = 588 against the comp's 587.
   NOT `flex: 1 1 0`: equal shares of the remainder spread the marks from one
   edge of the card to the other, which is a different picture from six evenly
   spaced marks sitting together. The widest label needs 56, measured, so
   nothing is squeezed by the fixed width. */
.er-sdks > .er-sdk { flex: 0 0 78px; }
/* Between 901 and roughly 1100 the intro's 386 plus six tiles will not fit on
   one line. Wrapping puts the marks under the intro rather than crushing the
   tiles until the labels break mid-word. */
@media (max-width: 1100px) {
  .er-sdkcard__row { flex-wrap: wrap; }
  .er-sdkcard__intro { flex: 1 1 100%; }
}

/* ── Interaction states ───────────────────────────────────────────────────── */
/* "Add hover and press states" appears four times in the designer's Figma
   comments, and there were no :active states at all.
 *
 * Press is expressed as a 1px depression plus a slight darkening rather than by
 * naming a darker surface token: a named surface inverts its own contrast in
 * one theme, the same trap that produced white-on-white earlier. brightness()
 * darkens correctly from either direction. */
.er-btn:active,
.er-tab:active,
.er-textlink:active,
.er-doccard:active {
  transform: translateY(1px);
}
.er-btn:active { filter: brightness(0.94); }

/* "Underline text on hover" — designer, Figma. */
.er-textlink:hover { text-decoration: underline; }

.er-footer__links a:hover {
  color: var(--er-text-prime-base);
  text-decoration: underline;
}

/* Tabs had no hover at all, so a pointer over an inactive tab gave no signal
   that it could be clicked. */
.er-tab:hover:not([aria-selected='true']) {
  background: var(--er-bg-prime-active);
  color: var(--er-text-prime-base);
}

/* Cards that are themselves links need to say so on hover. */
.er-doccard { transition: border-color 140ms ease, box-shadow 140ms ease; }
.er-doccard:hover {
  border-color: var(--er-border-prime-strong);
  box-shadow: var(--er-shadow-1);
}

@media (prefers-reduced-motion: reduce) {
  .er-doccard { transition: none; }
}

/* "Move logo up 2-4px so it look centered related to other content in a line"
   — designer, Figma. The wordmark's optical centre sits below its box centre
   because the display face has deep descenders. */
.er-topbar .er-logo { transform: translateY(-2px); }

/* A result pane whose output is a render rather than a listing. Same radius as
   the code panel it stands in for, and the intrinsic ratio is honoured so the
   two panes stay the same height as each other whatever the pane width is. */
.er-resultpane__shot {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 528 / 332;
  object-fit: cover;
  border-radius: var(--er-radius-md);
  background: var(--er-bg-prime-subtle);
}

/* ── Breakdown page ───────────────────────────────────────────────────────── */
/* The article column: Slide - 17 sets every block at 675 of its 1440, centred.
   Narrower than the site container because this page is prose, and prose wants
   a measure rather than a width. */
.er-doc {
  width: 100%;
  max-width: 675px;
  margin-inline: auto;
}

/* The comp mirrors the arrow to point back at the page you came from. */
.er-backlink {
  display: inline-flex;
  align-items: center;
  gap: var(--er-space-4);
  color: var(--er-text-sec-brand);
  text-decoration: none;
}
.er-backlink > svg { transform: scaleX(-1); }
.er-backlink:hover { text-decoration: underline; }

/* Frame 392: the tinted aside, 32 padding and a 32 gap, with a page-toned note
   card inside it at 24. */
.er-doc__panel { gap: var(--er-space-32); }
.er-doc__note { gap: var(--er-space-16); }

/* Frame 166: the four-column trial record. Model on the left, three figures
   right-aligned in fixed columns so the costs line up down the page rather than
   drifting with the length of the model name. */
.er-trials { gap: var(--er-space-24); }
.er-trial {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--er-space-32);
}
.er-trial__figures {
  display: grid;
  /* The comp's 120 / 76 / 120, widened to fit the substituted face: measured,
     the headers need 56, 80 and 125, so "Attempts" and "Cost / attempt" both
     wrapped at the comp's widths. Fixed columns rather than auto, because each
     row is its own grid and auto columns would let the figures drift out of
     line down the table. */
  grid-template-columns: 120px 84px 128px;
  align-items: center;
  gap: var(--er-space-16);
  text-align: right;
}
.er-trial__figures > * { white-space: nowrap; }

/* Frame 467: the five verification steps. A numbered disc, then the step.
   NOT .er-step / .er-step__n — those already belong to StepCard, whose number
   is absolutely positioned in a card's top corner. Reusing the names inherited
   that card's border and pinned the disc to the right of this row, on top of
   the text. */
.er-verify {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--er-space-32);
  width: 100%;
}
.er-verify__item {
  display: flex;
  align-items: flex-start;
  gap: var(--er-space-16);
}
.er-verify__n {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 48px;
  height: 48px;
  border-radius: var(--er-radius-pill);
  background: var(--er-bg-prime-subtle);
  border: 1px solid var(--er-border-prime-base);
  color: var(--er-text-prime-subtle);
  font-family: var(--er-type-h5-family);
  font-size: var(--er-type-h5-size);
  font-weight: var(--er-type-h5-weight);
  line-height: var(--er-type-h5-line-height);
}
.er-verify__body {
  display: flex;
  flex-direction: column;
  gap: var(--er-space-16);
  /* Without this the code block's own width forces the row wider than the
     column: a pre does not shrink below its content on its own. */
  min-width: 0;
  flex: 1;
}
/* The commands are long and must not reflow into something unrunnable, so the
   block scrolls rather than wrapping. */
.er-verify__body .er-code { overflow-x: auto; }

@media (max-width: 900px) {
  /* The disc costs 64 of a 350 column, which the commands cannot spare. */
  .er-verify__item { gap: var(--er-space-12); }
  .er-verify__n { width: 36px; height: 36px; font-size: var(--er-type-p3-size); }
  /* Three fixed figure columns do not fit beside a model name at this width. */
  .er-trial { flex-direction: column; align-items: flex-start; gap: var(--er-space-4); }
  .er-trial__figures {
    grid-template-columns: auto auto auto;
    justify-content: start;
    gap: var(--er-space-12);
    text-align: left;
  }
}

/* ── Independent benchmark panel ──────────────────────────────────────────── */
/* Frame 432: 1197 x 488, a row with 56 padding and a 40 gap, the CTA's navy
   gradient at radius 24. A 566 intro column on the left, a 448 card on the
   right, both vertically centred. */
.er-bench {
  position: relative;
  overflow: hidden;
  isolation: isolate;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--er-space-40);
  padding: var(--er-space-56);
  border-radius: var(--er-radius-xl);
}
/* Ellipse 7 — a 780 disc blurred 102, inset 145 from each side and centred.
   The same device as the CTA panel's glow: a lighter pool behind the content
   rather than a wash over it. The inset is a percentage (145/1197) so it holds
   its proportion instead of inverting on a narrow screen. */
.er-bench::before {
  content: '';
  position: absolute;
  z-index: -1;
  inset-inline: 12.11%;
  top: 50%;
  height: 780px;
  transform: translateY(-50%);
  background: var(--er-sec-900);
  filter: blur(102px);
}
.er-bench__intro {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--er-space-40);
  /* 566 of the comp's 1085 of content; it may shrink but not grow. */
  flex: 0 1 566px;
  min-width: 0;
}
.er-bench__card {
  /* 448 in the comp, whose rows read just "0 passed". They carry an attempt
     count now, and at 448 the longest row wrapped its cost to a second line and
     split "Claude Opus 5 [max]" across two. Grows to fit that string, no
     further. Reverted once with the copy and re-applied with it. */
  flex: 0 1 528px;
  min-width: 0;
  /* The comp's card gaps its blocks by 24 rather than the card default's 16. */
  gap: var(--er-space-24);
}
/* Model on the left, result on the right. */
.er-bench__row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--er-space-32);
}
@media (max-width: 900px) {
  /* The card's inner width on a phone is 252, and the longest pair --
     "Claude Opus 5 [max]" at 153 beside "0 passed x $5.63" at 126 -- needs 287
     however small the gap. Measured, not guessed. So each entry stacks: the
     model, then its result under it.

     The head row keeps its shape but drops "Model", which labels a column that
     no longer exists here; "Result • Cost / attempt" stays, because it is what
     explains "0 passed x $5.63". */
  .er-bench__row {
    flex-direction: column;
    align-items: flex-start;
    gap: var(--er-space-4);
  }
  .er-bench__row--head > :first-child { display: none; }
  /* A 566 column and a 448 card do not sit side by side on a phone, and the
     card is the payload — it goes under the claim rather than beside it. */
  .er-bench {
    flex-direction: column;
    align-items: stretch;
    padding: var(--er-space-32) var(--er-space-24);
    gap: var(--er-space-32);
  }
  .er-bench__intro, .er-bench__card { flex: 1 1 auto; }
  .er-bench__card { padding: var(--er-space-24); }
}

/* ── Demo card under the tab strip ────────────────────────────────────────── */
/* No squared corner here. That was right while the strip was left-aligned and
   the selected tab sat over the card's top-left corner; now the strip is
   centred, no tab reaches the corner, and the comp's card (Rectangle 15) is a
   uniform 16 radius. Squaring it would just look like a chip taken out. */

/* "Code viewport must be the same height for both blocks make content
   scrollable" — designer, Figma. The two samples are different lengths, so the
   panes otherwise disagree on where everything below the code sits. Measured
   at 1440: 224 vs 236 before this. */
.er-scrollrow--fused .er-code {
  height: 236px;
  overflow: auto;
}

/* The demo card's measured tab shows two real answers, which run longer than the comp's
   hand-written sixteen lines (29 and 52 for the prompt on the page). The comp's panel height is
   what the section was laid out around, so the panel keeps it and scrolls — the same treatment
   the phone breakpoint already gives .er-code, and for the same reason: nothing is trimmed, so
   the listing on the page still matches the transcript line for line.

   Both panes get the identical height whatever their content, so neither side reads as the
   longer answer because of its box. */
.er-demo__card .er-code {
  max-height: 264px;
  overflow-y: auto;
}

/* ── Docs page ────────────────────────────────────────────────────────────── */
/* Frame 482 / Frame 475. The comp puts a 240 sidebar at the container's left
   edge and the article at 675, leaving the right third empty — it is a reading
   column with a nav beside it, not a three-column shell, so the space on the
   right is deliberate and the measure stays at .er-doc's 675. */
.er-docslayout {
  display: grid;
  grid-template-columns: 240px minmax(0, 675px);
  gap: 88px;
  justify-content: start;
  align-items: start;
}

/* Sticky rather than the comp's absolute: the article is several screens long
   and a nav that scrolls away is a nav you have to scroll back for. Cleared by
   the topbar's height plus the section's own top padding. */
.er-docsnav {
  position: sticky;
  top: var(--er-space-32);
  display: flex;
  flex-direction: column;
  gap: var(--er-space-32);
}
.er-docsnav__links { list-style: none; margin: 0; padding: 0; }

/* Frame 474. The comp sets the field at 80% opacity over the page body; that is
   a white fill let down towards the cream behind it, which colour-mix states
   directly instead of making the whole control — text and icon included —
   translucent. */
.er-docsnav__search {
  box-sizing: border-box;
  display: flex;
  align-items: center;
  gap: var(--er-space-8);
  padding: var(--er-space-8) var(--er-space-12);
  background: color-mix(in srgb, var(--er-bg-prime-base) 80%, var(--er-bg-prime-page-body));
  border: 1px solid var(--er-border-prime-base);
  border-radius: var(--er-radius-sm);
}
.er-docsnav__searchicon { flex: none; color: var(--er-text-prime-subtle); }
.er-docsnav__input {
  flex: 1;
  min-width: 0;
  margin: 0;
  padding: 0;
  border: 0;
  background: none;
  color: var(--er-text-prime-base);
  font: inherit;
}
.er-docsnav__input::placeholder { color: var(--er-text-prime-inactive); opacity: 1; }
/* The ring belongs on the field, not on the bare input inside it, or it draws
   inside the border and reads as a second box. */
.er-docsnav__input:focus { outline: none; }
.er-docsnav__search:focus-within {
  outline: 2px solid var(--er-border-sec-brand);
  outline-offset: 2px;
}
/* WebKit's own clear button is a grey cross that ignores the token palette;
   type="search" still gets Escape-to-clear without it. */
.er-docsnav__input::-webkit-search-cancel-button { appearance: none; }

/* Frame 481: the aside beside the prose. */
.er-note {
  box-sizing: border-box;
  display: flex;
  align-items: center;
  gap: var(--er-space-12);
  padding: var(--er-space-12);
  background: var(--er-bg-prime-subtle);
  border: 1px solid var(--er-border-prime-base);
  border-radius: var(--er-radius-sm);
}
/* text-prime-brand, not an icon-* token: the Icons family has no brand entry
   (base, strong, subtle, on-color and the hues only), and inventing one to hold
   a colour that already exists would be a token for its own sake. It resolves
   to prime-500 in light and steps to prime-300 in dark, which is what this
   tinted surface needs. */
.er-note__mark { flex: none; color: var(--er-text-prime-brand); }
.er-note__body { flex: 1; min-width: 0; }
/* A Note holding more than a couple of lines reads badly with the mark
   vertically centred against a tall block, so it rises to the first line. */
.er-note--top { align-items: flex-start; }

/* One column below the table breakpoint, nav first — it is the page's index,
   and burying it under six screens of prose makes it unreachable. It also stops
   being sticky there: at phone height a pinned nav eats the viewport. */
@media (max-width: 900px) {
  .er-docslayout { grid-template-columns: minmax(0, 1fr); gap: var(--er-space-40); }
  .er-docsnav { position: static; }
}

/* Inline code inside a sentence. Set one step below the surrounding body text
   rather than at CD3's 14px: mono faces run large for their nominal size, and
   at a matched size a chip sits taller than the line it is in. */
.er-ic {
  font-family: var(--er-type-cd3-family);
  font-size: 0.875em;
  font-weight: var(--er-type-cd3-weight);
  color: var(--er-text-prime-base);
  background: var(--er-bg-prime-subtle);
  border: 1px solid var(--er-border-prime-base);
  border-radius: 4px;
  padding: 1px 5px;
  /* Endpoint paths and key prefixes are long single tokens with no space to
     break at, and one of them is what pushes a table column past its share. */
  overflow-wrap: anywhere;
}
/* Inside a table cell the chip is the cell's whole content, so its own box
   fights the cell padding and the row's rhythm. */
.er-table .er-ic { border: 0; background: none; padding: 0; }

/* A link in running prose. TextLink is the "read more" affordance and always
   carries an arrow, which is wrong mid-sentence. */
.er-ilink {
  color: var(--er-text-sec-brand);
  text-decoration: underline;
  text-underline-offset: 2px;
}
.er-ilink:hover { color: var(--er-text-prime-base); }
.er-ilink:focus-visible {
  outline: 2px solid var(--er-border-sec-brand);
  outline-offset: 2px;
  border-radius: 2px;
}

/* The docs tables have no identity column — a method-and-path or a field name
   is content, not a row label — so they drop the benchmark table's fixed 19.4%
   first column and let the columns share the width. */
.er-docs .er-table th:first-child { width: auto; }
/* 24 all round is the benchmark table's rhythm, drawn at 1216. These sit in a
   675 column, where it leaves the cells almost no measure. */
.er-docs .er-table th,
.er-docs .er-table td { padding: var(--er-space-12) var(--er-space-16); }

/* The article's own rhythm. A section is a Stack at 32; a heading that follows
   prose needs more air above it than the block it introduces needs below. */
.er-docs__section > .er-dh5 { margin-top: var(--er-space-16); }

/* A blank line inside a code block. .er-code__line is a flex row whose only
   child is an empty span, so it collapsed to zero height and the paragraph
   breaks in every snippet disappeared — the landing's samples have none, which
   is why this survived until the docs page. 1lh is the element's own computed
   line height, so it follows whichever code size the panel is set at; the em
   fallback covers browsers without the unit. */
.er-code__line { min-height: 1.2em; min-height: 1lh; }

/* The docs comp sets its panels one step up from the landing's dense hero
   panes — CD3.5 rather than CD4 — because these are snippets you read and
   paste, not a wall of text shown at a glance. */
.er-docs .er-code {
  font-size: var(--er-type-cd35-size);
  line-height: var(--er-type-cd35-line-height);
}

/* Table cells carry raw strings here rather than a wrapped <Text>, so without
   this they fall through to the browser's default serif — the shell sets no
   font on <body> (tokens own type, element by element). The comp sets these
   cells as P4: Metropolis 14/20. */
.er-docs .er-table td {
  font-family: var(--er-type-p4-family);
  font-size: var(--er-type-p4-size);
  font-weight: var(--er-type-p4-weight);
  line-height: var(--er-type-p4-line-height);
  color: var(--er-text-sec-subtle);
}

/* Docs snippets are not capped on phones. The landing's rule above shows the
   top of a long listing in a fixed 264px pane, which suits a sample you glance
   at beside a claim; these are configuration you copy, and a nested scroll
   region that hides the second half of a settings.json is how someone pastes a
   file with no closing brace. The page scrolls instead. */
@media (max-width: 900px) {
  .er-docs .er-code { max-height: none; overflow-y: visible; }
}

/* .er-code--dark is deliberately near-black in BOTH themes, and in dark mode
   bg-prime-page-body resolves to that same prime-grey-950 — so on this page,
   where a panel sits straight on the page rather than inside a card, every
   snippet dissolved into the background. A rule the panel only wears in dark;
   the light comp draws none, and a visible one there would be a change to the
   design rather than a fix. Same three-state shape as the tokens: the media
   query yields to an explicit data-theme, which is why it carries the :not().
   Declared on :root so the value follows the theme without JS. */
:root { --er-docs-code-border: transparent; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) { --er-docs-code-border: var(--er-border-prime-base); }
}
:root[data-theme="dark"] { --er-docs-code-border: var(--er-border-prime-base); }
.er-docs .er-code--dark { border: 1px solid var(--er-docs-code-border); }

/* A jumped-to section lands flush against the viewport edge otherwise. The
   previous version of this page set the same 36px; the topbar is static, so
   this is breathing room rather than clearance for fixed chrome. */
.er-docs__section, .er-docs__hero { scroll-margin-top: var(--er-space-40); }
