@font-face {
  font-family: "Geist";
  src: url("../fonts/geist-latin-variable.woff2") format("woff2");
  font-style: normal;
  font-weight: 100 900;
  font-display: swap;
}
@font-face {
  font-family: "IBM Plex Mono";
  src: url("../fonts/ibm-plex-mono-400.woff2") format("woff2");
  font-style: normal;
  font-weight: 400;
  font-display: swap;
}
@font-face {
  font-family: "IBM Plex Mono";
  src: url("../fonts/ibm-plex-mono-600.woff2") format("woff2");
  font-style: normal;
  font-weight: 600;
  font-display: swap;
}

:root {
  --font-sans: "Geist", Arial, sans-serif;
  --font-data: "IBM Plex Mono", monospace;
  /* Two greens, two jobs. --brand-core is the logo green and carries identity
     and interface states, where the extra depth reads better on white.
     --brand-bright stays with the illustrative layer: glows, orbits and the
     connection lines in the data visuals, which the 3D artwork already uses. */
  --brand-core: #2fa844;
  --brand-bright: #4acd4e;
  /* Descriptions run a step lighter than the interface around them. Geist is
     one variable file carrying 100-900, so this costs nothing to load. IBM
     Plex Mono ships only 400 and 600 and font-synthesis is off, so every mono
     line — eyebrows, kickers, the match rows — keeps its weight regardless. */
  --weight-body: 300;
  --weight-title: 500;
  --brand-label: #24752e;
  /* --brand-label is tuned for light surfaces and only reaches 3.0-3.5:1 on the
     dark bands, so green text there uses its own value. 13.4:1 on --inverse-primary. */
  --brand-on-dark: #8bea79;
  /* The focus ring. It used to be --brand-label at 0.35 alpha, which measured
     1.67:1 on --base and 1.37:1 on --inverse-primary: invisible on the closing
     CTA bands, and under the 3:1 that a focus indicator has to reach. Solid,
     --brand-label runs 5.4:1 on --base, and the dark bands take the same
     --brand-on-dark they already use for green text.

     It is drawn 2px wide at 2px offset, down from 3 and 3. Contrast was never
     the problem — 5.4:1 is nearly twice what is asked — so the weight came off
     the ring rather than out of its colour: 2px is still over the 2px minimum
     perimeter a focus indicator is measured on, and the shorter offset stops
     the ring reading as a second box floating around the field. On a white
     card the colour goes up as the weight comes down; see .request-form. */
  --focus-ring: var(--brand-label);
  /* Link ink and the step it takes on hover. The hover is the next stop down
     the same ramp rather than a new colour: --glow-2 is --brand-label itself,
     so --glow-1 is one step darker and needs no invention. On the inverses it
     moves the other way, because there contrast comes from going paler.
     Measured on the ground each lands on: 5.47:1 to 10.22:1 on --base,
     5.12:1 to 9.56:1 on --sunken, 13.42:1 to 14.92:1 on --inverse-primary. */
  --link: var(--brand-label);
  --link-hover: var(--glow-1);
  /* Three steps of green tint, not one. --brand-soft is the middle; the two
     either side of it existed as raw values and nearly did not survive being
     named, which is worth writing down because it is the one case in this
     pass where folding a near-duplicate onto its closest token was wrong.

     --tint-lift is four points off --base and eight off white, so on its own
     it looks exactly like the drift this pass was cleaning up. It is not. Its
     job is to be *greener than the ground*, and --base cannot do that job by
     definition: pointing the orbit's inner disc at --base deleted the disc,
     and pointing its 1%-wide edge at --brand-soft halved what was left. The
     two values were chosen against each other, and the distance between them
     was the whole design. Measured on a row through the specimen: the disc
     edge fell from a 7-step to a 4-step, and the disc's own body became
     indistinguishable from the section behind it.

     The same lift is what marks a checked .region-pick and .region-card. That
     is deliberately a whisper — selection is carried by the --brand-core
     border and inset ring, and this only warms the interior — so it is named
     here rather than promoted to --brand-soft, which would be a design change
     dressed up as a cleanup. Worth a second look as a design question: at
     four points off the unselected ground it is very nearly nothing. */
  --tint-lift: #f9fff8;
  --brand-soft: #f1f8f0;
  --tint-edge: #e9f5e8;
  /* Glow ramp for the primary button. #afeeb5, #5be565 and #1d4628 are the
     values the hero package briefing sampled off the 3D renders for their own
     glow; #4acd4e and #24752e are the palette's existing greens, so those two
     steps name them rather than repeating the value.

     --glow-4 is the one step nothing currently reaches for: the beam uses 2, 3
     and 5, and --link-hover takes 1. It stays for the same reason the three
     radius tokens stay at zero — a ramp with a hole in it stops being legible
     as a ramp — and the token check reports it every run so the choice has to
     be made again rather than forgotten. Do not fill the gap by pointing
     something at it that wants a different colour. */
  --glow-1: #1d4628;
  --glow-2: var(--brand-label);
  --glow-3: var(--brand-bright);
  --glow-4: #5be565;
  --glow-5: #afeeb5;
  /* --line on white measures 1.31:1. The dark dividers this replaces ran
     1.64-2.27:1, all heavier than their light counterpart. 0.14 lands at
     ~1.55:1 — nearer the light feel without disappearing on a dark ground.
     0.10 would match the maths exactly; 0.18 is the old look. */
  --line-dark: color-mix(in srgb, var(--text-on-dark) 14%, transparent);
  --text-heading: #171a19;
  /* Four ink steps, not three. This one was already in use and had no name:
     the nav links ran #303633, both callouts #35413c and the form labels
     #343c38 — three values within 25 points of each other, none of them a
     token, each typed by whoever needed something firmer than body and
     quieter than a heading. Nobody drifted off the scale here; four separate
     moments each re-invented the step the scale was missing.

     #313a36 is the centre of those three, so no site moves more than 17
     points and none of them moves visibly. 11.19:1 on --base, between the
     16.71:1 a heading reads at and the 7.97:1 of body copy. */
  --text-strong: #313a36;
  --text-body: #46504c;
  --text-muted: #5f6a65;
  /* The neutral pair, for anything procedural rather than editorial. --slate
     was declared long ago and then used by nothing at all: the quiet callout,
     its only consumer, spelled #54606b instead — thirteen points away and
     indistinguishable. The token did not fail to exist, it failed to be
     reached for, and it stayed dead because a rule that names no token will
     not turn up in a search for the token.

     --slate-soft is the ground that goes with it, on the same terms as
     --brand-soft under --brand-label. Cool where --sunken is green, so a
     procedural note reads as a different kind of remark rather than a
     slightly different shade of the same one. 6.32:1. */
  --slate: #545866;
  --slate-soft: #f0f2f3;
  /* The same scale on the other side. Every value above is a light-surface
     one, so the dark bands could not name any of them and spelled their two
     tones in raw hex instead: white for full strength, #c9cfcb for recessive,
     fifteen declarations across bands, the code panel, the coverage cart and
     the coverage matrix. That is what --brand-on-dark already fixed for green,
     and the same reasoning applies to the ink beside it. 17.35:1 and 10.96:1
     on --inverse-secondary, 19.93:1 and 12.59:1 on --inverse-primary.

     The ground decides, not the widget. Anything sitting on --inverse-primary
     or --inverse-secondary takes these, a band, the coverage cart, a primary
     button, the skip link, the comparison mark. What is not one of these is
     the label reversed out of a fill that is not a dark ground at all: the
     play disc and the region tick on --brand-core, the error badge on --warn.
     Those follow their own fill, and tuning the ink for a dark band should not
     repaint them.

     That distinction was the right one and it was written down here, but the
     three rules holding it said `white`, so it existed only in this comment.
     --text-on-fill is the same value under its own name, which is what lets
     the two cases be told apart by reading the rule instead of reading this
     paragraph. Same value, three declarations, no rendered change. */
  --text-on-dark: #fff;
  --text-on-fill: #fff;
  --text-muted-on-dark: #c9cfcb;
  /* The alert family. --warn was called "the one alert colour", and as a
     border and a fill it is: 7.6:1 on white, and it carries --text-on-fill on
     the error badge. But a red band needs three things, not one, and the other
     two were unnamed — #48231e for the ink inside a failed submission,
     #fbf1ef for the ground under it, plus a second ground at #fcf5f4 on the
     sections-page alert.

     That second ground is the interesting one. The rule that introduced it
     carries a comment saying it "reuses the colour the form errors use rather
     than introducing a second", and then introduces a second: ten points off,
     invisible, and a straight contradiction of its own note. One token makes
     the claim true. --warn-soft takes #fbf1ef, the form-error value, because
     that is the one the comment was pointing at.

     --warn-ink is 12.33:1 on --warn-soft. --warn itself would be 6.81:1 there,
     which passes, but error copy reading a step lighter than body copy is the
     wrong direction for the one message a reader must not miss. */
  --warn: #9c2b20;
  --warn-ink: #48231e;
  --warn-soft: #fbf1ef;
  --line: #dce2de;
  /* The frame a form control wears, and the one line on the site that is not
     --line. A divider separates two things that are already there; a control's
     border is the control — it is the whole of what says where the box you can
     type in begins and ends — so it carries more weight than a rule between
     rows. 1.86:1 on --raised against --line's 1.31:1. */
  --line-control: #b8c0ba;
  /* ------------------------------------------------------------------
     The four section grounds. Every band on the site sits on one of
     these and nothing else.

       base               (no class)                    98.0% B
       sunken             .section--sunken              95.3% B
       inverse secondary  .section--inverse-secondary   #181b1a
       inverse primary    .section--inverse-primary     #08090a

     --raised is the fifth surface and not a fifth band: nothing carries
     it as a section, and a section never sits on it. It is what a card,
     an input, a code panel or the sticky filter bar paints to lift off
     whichever band it is on, which is the job the paragraph below
     already describes base as being two points off pure white for. The
     surface was in use eleven times and named none of them — it said
     `white` — so the list said four grounds while five surfaces were
     being painted. --sunken sits below base and --raised above it, so
     the scale reads in one direction and the name says which way.

     Six used to exist. Three were light greys within two brightness
     points of each other — the old --mist, the hero canvas, and a raw
     #f5f4f2 on the flat heroes — and two more gradients in this file
     were unreachable, overridden on every page that could have shown
     them. The heroes take sunken, so the light side is one value.

     The two inverses are a pair, not a duplication: secondary is the
     dark band a section can sit on, primary is the terminal ground the
     closing CTA uses and the fill the primary button carries. They
     share one treatment set, written against both, so a band moves
     between them by swapping one class.

     base is off pure white by two points, so a card, an input or a code
     panel that paints white reads as lifted off the page rather than
     flush with it. sunken came down to match: at 96.9% against a 98%
     base the step between the two light grounds was 1.026, which is not
     a step. 95.3% puts it back to 1.069, near the 1.077 that white
     against the old grey gave.

     Contrast is unaffected in any way that matters. On base the inks
     run 16.71:1 (heading), 7.97:1 (body), 5.36:1 (muted) and 5.47:1
     (--brand-label); the weakest was 5.62:1 on white before. */
  /* The ground the region renders bake into their own pixels. They carry no
     alpha, so whatever sits behind them has to match this exactly or a square
     shows. It is pure white because the renders are, not because the surface
     scale wants pure white, and it moves only if the artwork is re-exported.
     Kept apart from --raised for that reason: --raised is free to soften. */
  --render-ground: #fff;
  --raised: #fff;
  --base: #f8faf8;
  --sunken: #eff3ef;
  --inverse-secondary: #181b1a;
  --inverse-primary: #08090a;
  /* Square corners throughout. The three tokens stay so the decision has one
     place to live, and so the scale is still legible if it is ever reversed. */
  --radius-sm: 0;
  --radius-card: 0;
  --radius-large: 0;
  /* One ink for everything that darkens what is behind it. --shadow already
     carried this colour and kept it to itself, so the three other shadows in
     the file spelled it again at their own alphas, and the two overlay scrims
     spelled two different colours for the same job: #14231d at 0.42 behind the
     mobile drawer, #121614 at 0.62 behind the hero dialog. Neither reads as a
     hue at those alphas, which is exactly why they were free to disagree.
     Both take this one now; the dialog moves 24 points at 62% opacity, which
     is nothing you can see. */
  --shadow-ink: #14231d;
  --shadow: 0 24px 70px color-mix(in srgb, var(--shadow-ink) 10%, transparent);
  --max-width: 1240px;

  /* The text measure, as a share of the section rather than a width of its own.
     It was 820px on .section-heading, and a fixed number has to be re-picked
     every time the container moves: it read as 66% of the 1240px container on a
     wide screen and as 84% of it at 1024, so the right margin the cap exists to
     leave kept shrinking exactly where the line was getting harder to track
     back. 60% holds that margin at every width the container takes.

     The floor is what keeps a proportion from becoming a column. Below about a
     1130px container 60% stops being a measure and starts being a sliver, so the
     cap widens back to 680px — roughly 72 characters of body copy, the top of
     the readable range — and then stops binding once the container is narrower
     than that, which is the tablet and phone case. max() of the two is
     continuous: there is no width at which a block jumps.

     In :root because two unrelated blocks answer to it: the section heading and
     the text half of the final CTA. The percentage resolves at the point of use,
     against whichever box the capped element sits in, and both of those are the
     section container. */
  --measure: max(60%, 680px);

  /* ------------------------------------------------------------------
     Two ramps, four steps each, and they are the same idea pointed in
     opposite directions: how sure the system is about something.

       --level-*     .match-levels     grey to green, precision rising
       --evidence-*  .evidence-stack   green to grey, support falling

     They used to be the same four light-surface values twice over, and
     the case for merging them was that their grey ends agreed to within
     seven points while their greens did not agree at all. That case is
     gone: the evidence rows are dark panels now, and a ramp is only
     legible against the ground it is drawn on. The two ramps answer to
     different grounds, so they are two ramps, and the elements page —
     which renders both — now shows why rather than showing a
     near-duplicate.

     The evidence ramp had to be restated rather than reused, because a
     light ramp read backwards on a dark panel. #2fa844 to #bdc5bd falls
     from 6.1:1 to 1.8:1 on white, which is support draining away; the
     same four values on --inverse-secondary climb from 5.6:1 to 9.8:1,
     so the step that means "review only" ended up the loudest mark on
     the block. On a dark ground the fade has to be in saturation
     instead: --brand-on-dark is the green a dark band already uses, and
     --text-muted-on-dark is the neutral it already recedes to, so the
     ramp is the distance between two tokens rather than four new
     values. Every step stays above 10:1, which is right — a mark that
     says how sure the system is has to be readable at every step; what
     changes along it is how much green is left, not how visible it is.

     Step 4 of the precision ramp is --brand-label exactly, and step 1
     of the evidence ramp is --brand-on-dark, so both say so. */
  --level-1: #bfcac2;
  --level-2: #9ddba0;
  --level-3: #62c767;
  --level-4: var(--brand-label);
  --evidence-1: var(--brand-on-dark);
  --evidence-2: color-mix(in srgb, var(--brand-on-dark) 66%, var(--text-muted-on-dark));
  --evidence-3: color-mix(in srgb, var(--brand-on-dark) 33%, var(--text-muted-on-dark));
  --evidence-4: var(--text-muted-on-dark);

  /* Shared rhythm. Section padding is redefined per breakpoint further down,
     so pages never need their own vertical spacing values. */
  --space-section: clamp(88px, 6.2vw, 104px);
  --space-section-tight: clamp(64px, 4.8vw, 76px);
  /* Was 28px flat, then 32px under 1024 and 28px again under 768: it went up as
     the section title came down. A clamp instead, so it tracks the title the
     way --space-section already tracks the band. 28px holds to 1273px and
     reaches its 20px floor at 909px, which is 0.56 of a 36px title against the
     0.58 the desktop pair reads at. */
  --space-heading: clamp(20px, 2.2vw, 28px);
  --header-height: 78px;
  --scroll-offset: 96px;
  --icon-size: 32px;

  /* Hero art. The renders are 1536x1024 cutouts on transparency, so the layer
     below them is drawn in CSS and the subjects in the file begin around 24-35%
     into the frame. That empty left third is the text-safe area: the layer
     width is what decides whether the artwork clears the copy column. */
  /* Sized off the container, not the viewport. The frame is wider than the
     drawing inside it — the ink starts about a third of the way in — so 0.62
     of the container puts roughly 40% of it under the artwork. */
  --hero-art-width: calc(min(100vw - 48px, var(--max-width)) * 0.62);
  --hero-art-ratio: 1.5;
  /* The canvas and the warm end of the gradient across it. Every hero sets
     --hero-canvas to its own value to match its render, which is the pattern
     working as intended rather than nine loose greys; the warm stop is shared
     by all of them and so belongs here. The cool end is --sunken, which it was
     already within four points of. */
  --hero-canvas: #f7f6f4;
  --hero-canvas-warm: #fbfaf8;
  /* The Medimapper atmosphere: warm off-white across the copy, drifting to a
     pale sage on the artwork side, with a soft green bloom behind the render.
     Both stops fade to zero well inside the element so there is no edge to see.
     Defined once and resolved wherever it is used, so the per-page glow values
     below and the specimen on the elements page draw the same thing. */
  --hero-glow-x: 74%;
  --hero-glow-y: 50%;
  --hero-glow-size: 58% 74%;
  --hero-glow-strength: 0.1;
  --hero-atmosphere:
    radial-gradient(
      var(--hero-glow-size) at var(--hero-glow-x) var(--hero-glow-y),
      color-mix(in srgb, var(--brand-core) calc(var(--hero-glow-strength) * 100%), transparent) 0%,
      color-mix(in srgb, var(--brand-core) calc(var(--hero-glow-strength) * 36%), transparent) 45%,
      transparent 74%
    ),
    linear-gradient(104deg, var(--hero-canvas-warm) 0%, var(--hero-canvas) 42%, var(--sunken) 100%);
  /* For the diagrams that still ship with an opaque backdrop, so their edges
     dissolve into the section instead of ending on a straight line. */
  --art-edge-fade-x: linear-gradient(to right, transparent 0%, black 7%, black 93%, transparent 100%);
  --art-edge-fade-y: linear-gradient(to bottom, transparent 0%, black 6%, black 94%, transparent 100%);

  color-scheme: light;
  font-family: var(--font-sans);
  font-synthesis: none;
}

* {
  box-sizing: border-box;
}
/* The UA hides [hidden] with a rule any author display beats, so every
   component that sets display and is toggled by script has to opt back in.
   Three did it one at a time — the hero player's waiting state, the
   progressively revealed form fields, and the coverage filter's empty state,
   which lost: .callout set display: block, so the "no country matches this
   filter" message was on the page under a full table of sixty-six countries.
   Two of those were found and patched by name; the third was shipped.

   One rule instead, and !important is the right tool exactly here: the guard
   has to beat whatever display the component sets, and the component has no
   business winning. This is the second use of it in the file and, like the
   reduced-motion block, it is a promise being kept rather than a fight being
   won. Toggle with el.hidden and this holds, whatever the component is. */
[hidden] {
  display: none !important;
}
html {
  scroll-behavior: smooth;
  background: var(--base);
}
body {
  margin: 0;
  color: var(--text-heading);
  background: var(--base);
  font-size: 17px;
  line-height: 1.62;
}
/* Every description on the site is one of these five. Anything that is not a
   description already declares its own weight, and a class beats an element,
   so those are untouched. */
p,
li,
dd,
td,
figcaption {
  font-weight: var(--weight-body);
}

/* And every title inside a component is the other. Thirteen of them inherited
   the browser's bold and five declared 550, 600 or 620, which is how a card
   title at 17px came to out-shout the section title above it at 46. Medium is
   one step up from the descriptions it sits on and one step down from the 550
   the section titles run at, and that is the whole hierarchy a card needs.
   Element selectors, like the rule above: every h3 on the site is a component
   title, and the three that are not headings are named. A component that wants
   its own weight — .final-cta, .section-title — still carries a class and
   still wins. */
h3,
h4,
.callout strong,
.region-card__name,
.disclosure summary {
  font-weight: var(--weight-title);
}

a {
  color: inherit;
  text-decoration: none;
}
img,
svg {
  display: block;
  max-width: 100%;
}
button,
input,
select,
textarea {
  font: inherit;
}
/* .is-focus and .is-hover carry no declarations of their own. They hang off the
   real state rules so the specimens on /en/elements/ cannot drift away from
   live behaviour. They are used on that page only. */
:focus-visible,
.is-focus {
  outline: 2px solid var(--focus-ring);
  outline-offset: 2px;
}
.skip-link {
  position: fixed;
  top: 8px;
  left: 8px;
  z-index: 100;
  padding: 10px 14px;
  color: var(--text-on-dark);
  background: var(--inverse-primary);
  transform: translateY(-150%);
  border-radius: 0;
}
.skip-link:focus {
  transform: none;
}
.container {
  width: min(calc(100% - 48px), var(--max-width));
  margin-inline: auto;
}
.eyebrow,
.data-kicker {
  font-family: var(--font-data);
  font-size: 0.75rem;
  font-weight: 600;
  line-height: 1.3;
  letter-spacing: 0.1em;
  text-transform: uppercase;
}
.eyebrow {
  margin: 0 0 10px;
  color: var(--brand-label);
}

/* Ordinals. Six components number their items — the feature cards, the match
   levels, the model flow, the step rows, the workflow accordion and the data
   chain. Seven, until the three-stage band turned out to be a feature grid
   with its own class. They were four sizes, two weights and two
   letter-spacings between them. One of each now, and always the brand green:
   a number that marks a position should look the same wherever it does it.
   Only the position is left to the component.

   What is left is only what a number is true of. Four sets counted things that
   have no order — three jobs on Home, three audiences on About, five contact
   reasons, four API capabilities — and a reader who sees 01 02 03 looks for
   the sequence. Those carry an icon in the same slot now; see .icon below. */
.feature-index,
.match-levels__index,
.three-stage span,
.model-flow small,
.step-rows > * > span,
.disclosure summary span,
.data-chain li::before {
  color: var(--brand-label);
  font-family: var(--font-data);
  font-size: 0.9rem;
  font-weight: 600;
  line-height: 1.3;
  letter-spacing: 0.1em;
}

.body-copy {
  max-width: 62ch;
  /* Primary, not --text-body. The section title above this is now a label for
     the section rather than its argument, so the prose is what the reader is
     meant to land on and it carries the darkest ink on the page. */
  color: var(--text-heading);
  font-size: 1.0625rem;
  line-height: 1.62;
}

.button {
  display: inline-flex;
  min-height: 44px;
  align-items: center;
  justify-content: center;
  padding: 10px 18px;
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  font-size: 0.9375rem;
  font-weight: 600;
  transition:
    transform 0.18s ease,
    border-color 0.18s ease,
    color 0.18s ease;
}
.button:hover,
.button.is-hover {
  transform: translateY(-1px);
}
.button-primary {
  position: relative;
  color: var(--text-on-dark);
  background: var(--inverse-primary);
}
/* The beam belongs to the filled button, and on the dark bands the filled
   button is .button with no modifier — the fill comes from the ground there,
   further down. So every rule in the beam block below names both, and both
   exclude .button-outline, which is the bordered variant and matches .button
   too. Written out rather than hooked on a shared class because the dark
   bands deliberately carry no .button-primary in the markup: the ground
   decides which button is primary, not the element.

   The gradient needs no second version. Its dim stops are --brand-bright at
   16%, which reads as a faint trace over --base and over --inverse-primary
   alike, and the bright arc is the same green either way. */
:is(.section--inverse-secondary, .section--inverse-primary)
  .button:not(.button-outline) {
  position: relative;
}
/* Hover only, and nothing but a ring. A rotating linear gradient clipped to a
   1.5px ring sitting 4px outside the button: two mask layers, one content-box
   and one full, composited to exclude, so only that ring is ever painted. It
   floats clear of the edge rather than being the button's own border, and
   nothing sits behind the button.

   The angle is a registered custom property because a plain one is not an
   animatable type: it would step from 0 to 360 and never move. It lives on
   the pseudo-element that animates it, so inherits is false. */
@property --beam-angle {
  syntax: "<angle>";
  inherits: false;
  initial-value: 0deg;
}
/* Without mask-composite the gradient would fill the whole button and cover
   the label, so the beam only exists where the clip does. */
@supports (mask-composite: exclude) or (-webkit-mask-composite: xor) {
  .button-primary::before,
  :is(.section--inverse-secondary, .section--inverse-primary)
    .button:not(.button-outline)::before {
    content: "";
    position: absolute;
    inset: -4px;
    padding: 1.5px;
    /* The four dim stops are one value, so it is named once here rather than
       four times below, or a sixth time in :root for something only this
       gradient uses. */
    --beam-dim: color-mix(in srgb, var(--brand-bright) 16%, transparent);
    background: linear-gradient(
      var(--beam-angle),
      var(--beam-dim) 0%,
      var(--beam-dim) 38%,
      var(--glow-2) 46%,
      var(--glow-3) 52%,
      var(--glow-5) 58%,
      var(--glow-3) 64%,
      var(--beam-dim) 72%,
      var(--beam-dim) 100%
    );
    -webkit-mask:
      linear-gradient(white 0 0) content-box,
      linear-gradient(white 0 0);
    -webkit-mask-composite: xor;
    mask:
      linear-gradient(white 0 0) content-box,
      linear-gradient(white 0 0);
    mask-composite: exclude;
    opacity: 0;
    transition: opacity 0.22s ease;
  }
}
.button-primary:hover,
.button-primary:focus-visible,
.button-primary.is-hover {
  color: var(--brand-on-dark);
}
/* The same green step, on the fill it actually lands on. The two filled
   buttons are inversions of each other, so their labels turn green off
   opposite ends of the ramp: --brand-on-dark is the value tuned for a dark
   fill and measures 1.48:1 on this white one, which is why the mirror is
   --brand-label, the light-surface green, at 5.74:1 on --raised.

   Specificity, since this is declared well above the rule that fills the
   button: (0,4,0) here against the (0,3,0) of the ground's own colour, so it
   wins on weight rather than on order. */
:is(.section--inverse-secondary, .section--inverse-primary)
  .button:not(.button-outline):is(:hover, :focus-visible, .is-hover) {
  color: var(--brand-label);
}
/* Hover only, not :focus-visible. The focus outline reaches 4px out — 2px of
   offset and 2px of ring — which is exactly where this ring is drawn, so
   running both would lay one green ring over the other. Keyboard keeps the
   outline, which is the stronger indicator, plus the green label. */
.button-primary:hover::before,
.button-primary.is-hover::before,
:is(.section--inverse-secondary, .section--inverse-primary)
  .button:not(.button-outline):is(:hover, .is-hover)::before {
  opacity: 1;
  animation: border-beam 3s linear infinite;
}
@keyframes border-beam {
  to {
    --beam-angle: 360deg;
  }
}
/* The beam still appears on hover, it just holds still. */
@media (prefers-reduced-motion: reduce) {
  .button-primary:hover::before,
  .button-primary.is-hover::before,
  :is(.section--inverse-secondary, .section--inverse-primary)
    .button:not(.button-outline):is(:hover, .is-hover)::before {
    animation: none;
  }
}
/* A disabled control must not light up. The label and border are already
   handled by .button:disabled:hover, which outranks the hover rule above on
   both specificity and order. */
.button-primary:disabled::before,
.button-primary[aria-disabled="true"]::before,
:is(.section--inverse-secondary, .section--inverse-primary)
  .button:not(.button-outline):is(:disabled, [aria-disabled="true"])::before {
  display: none;
}
/* An outline, not a panel. The near-white fill it used to carry read as a
   second filled button beside the primary one, which is the one thing the
   secondary is there not to be. Transparent is also the only fill that works
   on all four grounds it lands on: the hero gradient, the flat panel, the
   header and the mobile menu. The border stays explicit so it survives on
   <button> and <summary>, where the UA supplies a background of its own. */
.button-secondary {
  border-color: var(--line);
  background: transparent;
}
/* Disabled controls. Both variants set fill and label colour explicitly, so
   without this the browser default cannot show through and a disabled button
   looks live. Placed before the coverage-cart override further down, which has
   the same specificity and needs to win on the dark cart surface. */
.button:disabled,
.button[aria-disabled="true"] {
  border-color: var(--line);
  color: var(--text-muted);
  background: var(--sunken);
  cursor: not-allowed;
}
.button:disabled:hover,
.button[aria-disabled="true"]:hover {
  transform: none;
  border-color: var(--line);
  color: var(--text-muted);
}
/* The border picks up the label rather than both turning green.
   currentColor rather than a token, so it follows the label in any context
   the button lands in. */
.button-secondary:hover,
.button-secondary.is-hover {
  border-color: currentColor;
}
.site-header {
  position: sticky;
  top: 0;
  z-index: 50;
  border-bottom: 1px solid color-mix(in srgb, var(--line) 80%, transparent);
}
/* The tinted blur is a layer inside the header rather than a property on it.
   backdrop-filter makes an element the containing block for every fixed
   descendant, and the drawer below is one: on the header itself it anchored
   the drawer to the header's box, which happens to sit at the top of the
   screen only for as long as the sticky position holds. Scrolled down and
   opened, the drawer went wherever the header's own coordinates had gone. A
   pseudo-element has no descendants to capture, so the same blur costs
   nothing here. */
.site-header::before {
  content: "";
  position: absolute;
  z-index: -1;
  inset: 0;
  background: color-mix(in srgb, var(--raised) 94%, transparent);
  backdrop-filter: blur(16px);
}
.nav-wrap {
  min-height: var(--header-height);
  display: flex;
  align-items: center;
  gap: 28px;
}
/* The V2 artwork is cropped to the ink, so the mark is only about 23px tall.
   The link keeps its own 44px minimum rather than inheriting a target size from
   the whitespace that used to be baked into the image. */
.brand {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
}
/* 184px, not the 210px the old file used. The PNG carried roughly 13% empty
   width and 63% empty height around the artwork; measured, its mark sat at
   2084x268 inside a 2388x724 canvas. The new SVG is that artwork cropped to
   2083x267, so this width renders it at exactly the size it renders today. */
.brand img {
  display: block;
  width: 190px;
  height: auto;
}

/* The nav reads as a continuation of the brand, so it sits beside the logo
   and .nav-actions takes over the push to the right edge. Stretching to the
   full header height is what lets the active marker reach the bottom border. */
.desktop-nav {
  display: flex;
  align-self: stretch;
  align-items: stretch;
  gap: 10px;
}
.desktop-nav .nav-link {
  display: inline-flex;
  align-items: center;
}
/* The 404's nav nests its links in <details>, so that has to stretch too. */
.desktop-nav .nav-group {
  display: flex;
  align-items: stretch;
}
.nav-link,
summary.nav-link {
  padding: 11px 9px;
  color: var(--text-strong);
  font-size: 0.88rem;
  font-weight: 450;
  line-height: 1;
  cursor: pointer;
  list-style: none;
}
.nav-link:hover,
.nav-group[open] summary {
  color: var(--brand-label);
}
/* "You are here". The underline carries the meaning as well as the colour, so
   it survives a colour-vision difference or a greyscale print. */
.nav-group {
  position: relative;
}
.nav-group summary::-webkit-details-marker {
  display: none;
}
.dropdown {
  position: absolute;
  top: calc(100% + 12px);
  left: 0;
  min-width: 250px;
  padding: 8px;
  border: 1px solid var(--line);
  border-radius: 0;
  background: var(--raised);
  box-shadow: var(--shadow);
}
.dropdown a:not(.button) {
  display: block;
  padding: 11px 12px;
  border-radius: 0;
  font-size: 0.9375rem;
}
.dropdown a:not(.button):hover {
  color: var(--brand-label);
  background: var(--brand-soft);
}
.nav-actions {
  display: flex;
  align-items: center;
  gap: 9px;
  margin-left: auto;
}
.language {
  padding: 9px;
  color: var(--text-muted);
  font-family: var(--font-data);
  font-size: 0.75rem;
}
.mobile-toggle {
  display: none;
  border: 0;
  background: transparent;
  padding: 8px;
}
/* The menu button. Icon only, so the name lives in aria-label and the mark is
   a mask rather than an <img>: the same trick the accordion markers use, which
   is what lets the bars take the button's own colour and follow it green when
   .nav-group[open] sets the open state. Square at the button's own 44px, so
   there is no padding left to pull the mark off centre. */
.menu-button {
  width: 44px;
  padding: 0;
  cursor: pointer;
  list-style: none;
}
.menu-button::-webkit-details-marker {
  display: none;
}
.menu-button::after {
  content: "";
  width: 22px;
  height: 22px;
  background: currentColor;
  -webkit-mask: url("/assets/icons/list.svg") center / contain no-repeat;
  mask: url("/assets/icons/list.svg") center / contain no-repeat;
}
.mobile-toggle[open] .menu-button::after {
  -webkit-mask-image: url("/assets/icons/x.svg");
  mask-image: url("/assets/icons/x.svg");
}

/* The mobile menu. Destinations first, then the two controls the header itself
   drops at this width. They stay controls rather than becoming two more rows
   in the list: logging in and requesting a trial are the reasons the menu gets
   opened, and a list of eleven identical rows hides both. */
.mobile-menu {
  padding: 0;
}
.mobile-menu__links {
  padding: 8px;
}
.mobile-menu__actions {
  display: grid;
  gap: 8px;
  padding: 14px;
  border-top: 1px solid var(--line);
  background: var(--sunken);
}
.mobile-menu__actions .button {
  width: 100%;
}
/* "You are here" on the login and trial pages, which are the two destinations
   that left the list. The filled button already reads as its own page, so only
   the outline one needs the cue. */
.mobile-menu__actions .button-secondary[aria-current="page"] {
  border-color: currentColor;
  color: var(--brand-label);
}

/* Active navigation. The underline uses --brand-label rather than --brand:
   at 2px on white the lighter green measures 2.07:1, below the 3:1 that a
   non-text state indicator needs. Both cues therefore clear contrast on
   their own. */
/* bottom: -1px so the 2px bar sits over the header's 1px bottom border and
   its lower edge lines up with it: the tab owns that segment of the rule
   rather than drawing a second line above it. */
.nav-link {
  position: relative;
}
.nav-link[aria-current="page"] {
  color: var(--brand-label);
}
.nav-link[aria-current="page"]::after {
  content: "";
  position: absolute;
  right: 0;
  bottom: -1px;
  left: 0;
  height: 2px;
  background: var(--brand-label);
}
.dropdown a:not(.button)[aria-current="page"] {
  color: var(--brand-label);
  background: var(--brand-soft);
}

/* Every hero sits on sunken, art and flat alike. The band is the page's
   opening statement and the first thing under a near-white header, so it takes
   the recessed ground rather than the same base the body sections default to.
   One value, not a per-page canvas: see the note on --hero-canvas below. */
.hero {
  position: relative;
  overflow: hidden;
  padding: 124px 0 116px;
  background: var(--sunken);
}
/* The decorative ring belongs to the heroes without artwork. On an art hero the
   same pseudo-element carries the gradient instead, so it is excluded here
   rather than switched off again further down. */
.hero:not(.hero--art)::before {
  content: "";
  position: absolute;
  width: 620px;
  height: 620px;
  right: -210px;
  top: -330px;
  border: 1px solid color-mix(in srgb, var(--brand-label) 14%, transparent);
  border-radius: 50%;
  box-shadow:
    0 0 0 86px color-mix(in srgb, var(--brand-bright) 3.5%, transparent),
    0 0 0 170px color-mix(in srgb, var(--brand-bright) 2.5%, transparent);
}
.hero-grid {
  position: relative;
  z-index: 1;
  display: grid;
  grid-template-columns: minmax(0, 0.93fr) minmax(520px, 1.07fr);
  gap: 74px;
  align-items: center;
}
.hero h1,
.spec-h1 {
  /* No measure of its own, and no balancing. A cap of 13ch held the title
     inside about 600px of a 680px column, so lines broke while space was still
     left beside them: Home opened on "Compare" alone with a line twice its
     length under it. text-wrap: balance then evened what was left, moving words
     down for symmetry rather than because the line was full.

     The title now fills .hero__copy and wraps where it runs out of column. The
     column is the measure, one number instead of two, and it is the same number
     the paragraph under the title already answers to. */
  margin: 0;
  /* Parked with the inversion on .section-title: the headline named the page
     and the copy beneath it made the case, but that reading only worked while
     the lead held the claim at primary. Restore both together. */
  color: var(--text-heading);
  font-size: clamp(3rem, 5.2vw, 5.25rem);
  font-weight: 550;
  line-height: 0.98;
  letter-spacing: -0.045em;
}
/* The gap under the heading belongs to the heading, and it is in em so it
   tracks whatever size the clamp above has resolved to. It was 30px flat, which
   held at 72px on a wide screen and then stayed 30px while the heading came
   down to 48px, so the space under the title grew by half against the type it
   was separating. 0.42em is that same 30px at the top of the clamp and 20px at
   the bottom. Not on .spec-h1, which shares the size rule but sits in a
   specimen frame that spaces its own children. */
.hero h1 {
  margin-bottom: 0.42em;
}
.hero-copy {
  max-width: 56ch;
  margin: 0;
  color: var(--text-heading);
  font-size: 1.1875rem;
  line-height: 1.58;
}
.hero-copy p {
  margin: 0 0 10px;
}
.hero-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  margin-top: 34px;
}

/* Art hero -----------------------------------------------------------------
   Three layers that know nothing about each other: a CSS gradient (0), the
   transparent render (1) and the copy (2). The atmosphere used to be baked into
   the render, which meant a picture with its own background had to be faded out
   at the edges to hide the rectangle it sat in. Drawing it here instead keeps it
   inside the section, so it stops cleanly at the boundary and every page can set
   its own tone.

   The custom properties below are the per-page dials. --hero-ink-* describe
   where the artwork actually sits inside its 1536x1024 frame; the stacked layout
   uses them to frame the render to its own subject instead of to a box that is
   a third empty. */
.hero--art {
  --hero-art-scale: 1;
  --hero-art-x: 0px;
  --hero-art-y: 0px;
  --hero-ink-left: 0.3;
  --hero-ink-right: 0.98;
  --hero-ink-top: 0.05;
  --hero-ink-bottom: 0.95;
  --hero-art-size: calc(var(--hero-art-width) * var(--hero-art-scale));

  position: relative;
  /* The section has to be at least as tall as the artwork, otherwise clipping it
     to the boundary would cut the render instead of the spill it is there to
     stop. */
  min-height: max(
    min(820px, calc(100svh - var(--header-height))),
    calc(var(--hero-art-size) / var(--hero-art-ratio) + 40px)
  );
  padding: 0;
  overflow: clip;
  isolation: isolate;
  background: var(--sunken);
}
/* The atmosphere used to be painted here, on a ::before covering the section.
   It is parked, not deleted: --hero-atmosphere, --hero-canvas and the per-page
   --hero-glow-* values below all stay defined, and the specimen on the elements
   page still draws it. When it comes back it belongs on the body, so one wash
   can cover a run of light bands instead of stopping at each hero's boundary.
   Restoring it here is this rule again and nothing else:

     .hero--art::before {
       content: ""; position: absolute; z-index: 0; inset: 0;
       background-image: var(--hero-atmosphere);
     }

   overflow: clip and isolation: isolate stay either way — they clip the render
   to the section, which is a separate job from the wash. */
/* Anchored to the container's right edge, not the section's. Pinned to the
   section it kept going right on a wide screen, so past about 1500px the
   artwork sat out on its own with the whole page ending well to its left. The
   offset is the container's own inset: it is centred at min(100% - 48px,
   --max-width), so its right edge is exactly this far in. */
.hero__art {
  position: absolute;
  z-index: 1;
  top: 50%;
  right: max(24px, calc((100% - var(--max-width)) / 2));
  display: block;
  width: var(--hero-art-size);
  translate: var(--hero-art-x) calc(-50% + var(--hero-art-y));
  pointer-events: none;
}
.hero__art > picture {
  display: block;
  width: 100%;
}
.hero__art img {
  display: block;
  width: 100%;
  height: auto;
}
.hero--art > .hero-grid {
  position: relative;
  z-index: 2;
  min-height: inherit;
  padding-block: clamp(72px, 8vh, 112px);
  grid-template-columns: minmax(0, 0.6fr) minmax(0, 0.4fr);
  pointer-events: none;
}
.hero__copy {
  position: relative;
  z-index: 3;
  pointer-events: auto;
  /* This is the hero's measure, and since the h1 lost its own cap it is the
     only one: the title fills this column and wraps at its edge. 590 was 34px
     short of holding "Compare medicine" on one line at the top of the clamp,
     which is what opened Home on a single word. Nothing else in here comes
     close — the paragraph stops at 56ch. */
  max-width: 680px;
}

/* Opaque stills, and the home test of the same layout. Copy takes 52% of
   the site column. The visual takes the remaining 48% and sits flush on
   the column's right edge. The atmosphere gradient is off: the section is
   a flat field. */
.hero--still::before {
  display: none;
}
.hero--still {
  --hero-art-scale: 1;
  --hero-art-width: calc(min(100vw - 48px, var(--max-width)) * 0.48);
}
.hero--still > .hero-grid {
  grid-template-columns: minmax(0, 0.52fr) minmax(0, 0.48fr);
  gap: 0;
}
.hero--still .hero__copy {
  /* The column takes the whole 52%, so the measure here is the column and the
     copy fills it. The gutter is what keeps that from meaning "up against the
     render": the still visual sits flush on this column's right edge, and with
     the h1 free to use the full width About came within 2px of the artwork.
     48px is the slack the old 13ch cap used to leave by accident, now declared
     where it belongs — on the column, not on one heading inside it. */
  max-width: none;
  padding-right: clamp(28px, 3.4vw, 48px);
}

/* Per-page visual configuration. The ink box of each render is measured from its
   alpha channel, padded outwards a little so a soft glow is never clipped.

   The --hero-canvas values below are inert while the atmosphere is parked:
   every hero paints --sunken and no background image. They matter again the
   moment the gradient is restored, and there are nine of them spanning 6.12
   lightness points, a ratio of 1.067 from lightest to darkest. The whole
   deliberate step from --base to --sunken is 1.069, so nine values currently
   sit inside the space of one intended step, and about at 86.91% against
   contact at 87.05% is a difference nobody can see.

   This is the same problem the four grounds replaced, waiting to switch back
   on. Collapse them to two or three deliberate values before restoring the
   wash, or drop them entirely, which is what putting one wash on the body
   implies anyway.

   They were not arbitrary, which is worth knowing before dropping them. Ten of
   the eleven hero renders are opaque PNGs with a light matte baked in, and each
   value here is that page's matte sampled. That is what they were for, hiding
   the rectangle an opaque render would otherwise draw on its band.

   That is now history: the stills have been re-exported on one matte, #eff3ef,
   which is --sunken itself. Eight of the nine sit on their band exactly and
   about is one point off per channel, a ratio of 1.0096 where the old spread
   ran to 1.099. The api render these values described worst is the one that
   gained most. So they no longer record anything a render does, and dropping
   them costs nothing. See docs/decisions.md. */
.hero--home {
  --hero-art-ratio: calc(1280 / 1080);
  --hero-ink-left: 0;
  --hero-ink-right: 1;
  --hero-ink-top: 0;
  --hero-ink-bottom: 1;
  --hero-canvas: #eef3ec;
}
.hero--platform {
  --hero-art-ratio: calc(1280 / 1080);
  --hero-ink-left: 0;
  --hero-ink-right: 1;
  --hero-ink-top: 0;
  --hero-ink-bottom: 1;
  --hero-canvas: #eef2eb;
}
.hero--solutions {
  --hero-art-ratio: calc(1280 / 1080);
  --hero-ink-left: 0;
  --hero-ink-right: 1;
  --hero-ink-top: 0;
  --hero-ink-bottom: 1;
  --hero-canvas: #edf0e7;
}
.hero--data {
  --hero-art-ratio: calc(1280 / 1080);
  --hero-ink-left: 0;
  --hero-ink-right: 1;
  --hero-ink-top: 0;
  --hero-ink-bottom: 1;
  --hero-canvas: #f4f6f1;
}
.hero--trade-landscape {
  --hero-ink-left: 0.307;
  --hero-ink-right: 0.972;
  --hero-ink-top: 0.125;
  --hero-ink-bottom: 0.864;
}
.hero--api {
  --hero-art-ratio: calc(1280 / 1080);
  --hero-ink-left: 0;
  --hero-ink-right: 1;
  --hero-ink-top: 0;
  --hero-ink-bottom: 1;
  --hero-canvas: var(--brand-soft);
}
.hero--pricing,
.hero--coverage,
.hero--legal {
  --hero-art-ratio: calc(1280 / 1080);
  --hero-ink-left: 0;
  --hero-ink-right: 1;
  --hero-ink-top: 0;
  --hero-ink-bottom: 1;
  --hero-canvas: #f2f7f1;
}
.hero--trial,
.hero--contact {
  --hero-art-ratio: calc(1280 / 1080);
  --hero-ink-left: 0;
  --hero-ink-right: 1;
  --hero-ink-top: 0;
  --hero-ink-bottom: 1;
  --hero-canvas: #ecf2e6;
}
.hero--about {
  --hero-art-ratio: calc(1280 / 1080);
  --hero-ink-left: 0;
  --hero-ink-right: 1;
  --hero-ink-top: 0;
  --hero-ink-bottom: 1;
  --hero-canvas: #eef1e8;
}

/* The player ---------------------------------------------------------------
   Home's How it works section: a 16:9 film in a frame as wide as the
   container, with the film's first frame as its poster until it plays.
   Everything over the video is in how-it-works.css, which the boot script
   loads when the visitor presses play and not before. */
.player {
  margin: 0;
}
.player__frame {
  position: relative;
  aspect-ratio: 16 / 9;
  overflow: hidden;
  border-radius: var(--radius-card);
  /* The stills are composited on this colour, so the poster's own ground and
     the frame's are the same one and the crop has no edge. */
  background: var(--sunken);
}
.player__poster {
  position: absolute;
  z-index: 2;
  inset: 0;
}
.player__poster img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}
/* The poster leaves once the film is playing. It is the film's first frame,
   so the fade only covers the moment the video takes over. */
.player.is-ready .player__poster {
  opacity: 0;
  transition: opacity 0.45s ease;
  pointer-events: none;
}
.player__play {
  position: absolute;
  z-index: 3;
  left: 50%;
  top: 50%;
  display: inline-flex;
  flex-direction: column;
  align-items: center;
  gap: 12px;
  padding: 0;
  border: 0;
  background: transparent;
  color: var(--text-heading);
  cursor: pointer;
  translate: -50% -50%;
}
.player__disc {
  display: grid;
  place-items: center;
  width: 76px;
  height: 76px;
  border-radius: 50%;
  background: var(--brand-core);
  box-shadow: 0 12px 28px color-mix(in srgb, var(--inverse-primary) 22%, transparent);
  transition: background 0.18s ease, transform 0.18s ease;
}
/* The glyph is drawn through a mask, not served as an <img>, because the icon
   files carry the icon colour and this one has to be the ink on a filled disc.
   No optical nudge: phosphor already sets the triangle off-centre in its own
   256 grid, and the mark's mass lands on the middle of the disc. */
.player__disc::after {
  content: "";
  width: 30px;
  height: 30px;
  background: var(--text-on-fill);
  -webkit-mask: url("/assets/icons/play.svg") center / contain no-repeat;
  mask: url("/assets/icons/play.svg") center / contain no-repeat;
}
.player__play:hover .player__disc,
.player__play:focus-visible .player__disc {
  background: var(--inverse-primary);
  transform: scale(1.04);
}
.player__label {
  padding: 4px 10px;
  border-radius: var(--radius-sm);
  background: color-mix(in srgb, var(--raised) 86%, transparent);
  font-size: 0.8125rem;
  font-weight: 600;
  letter-spacing: 0.01em;
  backdrop-filter: blur(8px);
}
.player.is-loading .player__play,
.player.is-ready .player__play {
  display: none;
}
@media (prefers-reduced-motion: reduce) {
  .player__play {
    display: none;
  }
}
/* Below 1025px the whole section goes, not just the play control. The overlay
   type keeps px floors so it stays legible, and on a narrower frame the cards
   cover the globe and each other. A section that cannot do what it says is
   better absent: the page below still carries the process, in the chain the
   next section animates. */
@media (max-width: 1024px) {
  .section--how {
    display: none;
  }
}
.player__waiting {
  position: absolute;
  z-index: 3;
  inset: 0;
  display: grid;
  place-items: center;
  margin: 0;
  color: var(--text-muted);
  font-size: 0.9375rem;
  font-weight: 600;
}
/* Top right of the frame, where the scene has sky. It is only there once the
   scene is: a pause control over a still has nothing to pause. */
.player .motion-controls {
  position: absolute;
  z-index: 4;
  top: 14px;
  right: 14px;
  display: none;
}
.player.is-ready .motion-controls {
  display: block;
}
.player__caption {
  margin-top: 14px;
  color: var(--text-muted);
  font-size: 0.875rem;
  text-align: center;
}
/* The film and its overlay, laid out from the moment the visitor asks for
   them and shown once the film is playing. */
.player__stage {
  position: absolute;
  z-index: 1;
  inset: 0;
  display: none;
  opacity: 0;
  pointer-events: none;
  container: film-stage / inline-size;
}
.player.is-loading .player__stage,
.player.is-ready .player__stage {
  display: block;
}
.player.is-ready .player__stage {
  opacity: 1;
}

.data-kicker {
  color: var(--text-muted);
  font-size: 0.65rem;
}

.section {
  padding-block: var(--space-section);
}
h1[id],
h2[id],
h3[id],
section[id],
[id]:target {
  scroll-margin-top: var(--scroll-offset);
}
.section--sunken {
  background: var(--sunken);
}
.section-heading {
  /* --measure, not a width of its own. The text column inside
     .section-heading--linked cannot inherit this cap — the flex row it sits in
     has to reach the container edge so the link can finish against it — so it
     names the same custom property rather than repeating the value. */
  max-width: var(--measure);
  margin-bottom: var(--space-heading);
}
/* The one heading on the site that is centred, over the player on Home. The
   measure still caps it: centred text that runs the full container is harder
   to read back to, not easier. */
.section-heading--centred {
  margin-inline: auto;
  text-align: center;
}
/* Every paragraph in a section is capped at 68ch, and the eyebrow is one: in
   the data font at 12px that cap is 490px inside a 744px block, so a centred
   line was centred on the cap's middle rather than the block's — 127px left of
   the title under it. Centring the boxes as well as the text is what makes the
   two agree, whatever any of them is capped to. */
.section-heading--centred > * {
  margin-inline: auto;
}
.section-title {
  /* The 880px on .section-heading is the measure. A 24ch cap on top of it bound
     first at every size the clamp produces, so section titles broke a word or
     two before the block they sit in ended and the leftover space read as a
     mistake. Balancing compounded it: with the cap gone it would still pull
     words down to even the lines out. One measure, filled, then wrapped. */
  margin: 0;
  font-size: clamp(2.125rem, 3.2vw, 3rem);
  font-weight: 550;
  line-height: 1.06;
  letter-spacing: -0.035em;
}
/* Inverted hierarchy: parked, not deleted. The title names the section; the
   body under it makes the point, so reading the title as tertiary and the
   prose as primary put the emphasis where the substance already was. That only
   held because the lead pulled the claim back to primary inside the heading.
   Tertiary alone leaves the page without a first read. The copy rewrite
   replaced the sentences the leads were marked on, so the marking has to be
   redone against the new copy before the inversion can come back.

   Restoring it is this one rule plus the leads: set --text-muted here again
   and mark .lead on each heading. The .lead rules below stay defined and
   inert in the meantime, so nothing else has to move. */
.section-title {
  color: var(--text-heading);
}
/* The emphasised opening of a heading. The inversion is not one colour per
   heading but two inside it: the lead states the claim in primary ink, the
   remainder qualifies it in tertiary. That is the whole reason the title could
   drop to --text-muted without the page losing its first read.

   Where a heading is two sentences the first one is the lead. The rest were
   marked by hand: splitting on punctuation put the break in the wrong place
   often enough to be worthless — list commas are not clause commas, and a lead
   that ends on "is" or "are" strands the verb away from what it governs. The
   lead carries the claim, the remainder qualifies it. Twenty-two headings are
   too short to divide and take no lead at all; below about five words the
   remainder is a fragment and the split reads as a mistake. */
.section-title .lead,
.hero h1 .lead {
  color: var(--text-heading);
}
:is(.section--inverse-secondary, .section--inverse-primary) .section-title .lead {
  color: var(--text-on-dark);
}
.section--statement,
.section--tight {
  padding-block: var(--space-section-tight);
}
.section--statement .section-heading {
  margin-bottom: 0;
}
.section--statement .section-title {
  font-size: clamp(2.5rem, 4vw, 4rem);
}
.editorial-body {
  max-width: 68ch;
  margin-top: 18px;
}
.editorial-body p {
  margin-block: 0 16px;
}
.editorial-body p:last-child {
  margin-bottom: 0;
}
.editorial-body .check-list {
  margin-block: 0 16px;
}
.editorial-body .check-list:last-child {
  margin-bottom: 0;
}

/* An icon or an ordinal, a heading and a line of body, three or four across.
   Ruled rather than boxed. The border round each cell said nothing the gaps
   were not already saying, and equal-height cells left the short ones ending
   in dead space. These are the same rules .match-levels and .audience-bands
   draw, which on the home page is what they now sit between. A box is for
   something you click, and .region-card on Pricing is the one that is left. */
.feature-grid {
  display: grid;
  grid-template-columns: repeat(var(--feature-columns, 3), minmax(0, 1fr));
  /* Half the measure either side of the spine, so the text columns sit 56px
     apart with the rule down the middle — what .match-levels reaches by
     putting 28px of padding on each neighbour. */
  column-gap: 56px;
  /* Both rules are drawn 28px wider than the cell on each side, so column one
     starts and the last column ends outside this box; clipping here is what
     stops the spine landing on the container edge and lets the head rules of
     neighbouring cells meet across the gap instead of breaking at it. The
     alternative names the column count in an :nth-child selector, and would
     have to name it again at each breakpoint that drops these to two columns
     and then to one. This way the track list is the only place it is written. */
  overflow: hidden;
}
.feature-grid--two {
  --feature-columns: 2;
}
.feature-grid--three {
  --feature-columns: 3;
}
.feature-grid--four {
  --feature-columns: 4;
}
.feature-card {
  position: relative;
  padding: 26px 0 30px;
}
/* Head rule and spine on one box, half the gap proud of the cell on each side:
   the spine then sits in the gutter rather than on either neighbour, and stays
   there whether a cell is first, last or alone in its row. Contact shares it —
   same geometry, and it would otherwise be written twice. */
.feature-card::before,
.contact-card::before {
  content: "";
  position: absolute;
  top: 0;
  right: -28px;
  bottom: 0;
  left: -28px;
  /* Two borders, but the box carrying them covers the whole cell, and a
     positioned box paints over content that is not positioned itself. It was
     therefore the top hit target across every card on the site: it took the
     mousedown that starts a selection and the click that follows a link, so no
     card's copy could be selected and no .section-link inside one worked. It is
     decoration and has nothing to hit. */
  pointer-events: none;
  border-top: 1px solid var(--line);
  border-left: 1px solid var(--line);
}
.feature-index,
.feature-card > .icon {
  display: block;
  margin-bottom: 18px;
}
.feature-card h3 {
  margin: 0 0 12px;
  font-size: clamp(1.25rem, 2vw, 1.5rem);
  line-height: 1.2;
  letter-spacing: -0.02em;
}
.feature-card p {
  max-width: 48ch;
  margin: 0 0 10px;
  color: var(--text-body);
  font-size: 1.02rem;
  line-height: 1.62;
}
/* Explanatory diagram: a supporting visual only gets space when it explains a
   process. The compact artwork is wide, so narrow viewports get the taller
   variant instead of hiding the diagram altogether. */
.diagram {
  display: block;
  width: 100%;
  max-width: 1100px;
  margin: 32px 0 0;
}
.diagram img {
  display: block;
  width: 100%;
  height: auto;
  object-fit: contain;
}
/* The six-stage chain reads better across the full measure, and its backdrop is
   opaque, so the edges fade where they meet the section background. */
.diagram--connections {
  max-width: none;
  margin-top: 36px;
}
.diagram--connections img {
  aspect-ratio: 1923 / 417;
  -webkit-mask-image: var(--art-edge-fade-x), var(--art-edge-fade-y);
  -webkit-mask-composite: source-in;
  mask-image: var(--art-edge-fade-x), var(--art-edge-fade-y);
  mask-composite: intersect;
}
.diagram--boundary {
  max-width: 380px;
  margin: 0;
}

/* The icon system. Phosphor at the light weight, recoloured to brand-label by
   input/icons/build-icons.sh and otherwise left alone. Phosphor draws every
   weight as a filled outline rather than a stroke, so the weight is baked
   into the icon and choosing it is the whole decision; weight= in
   input/icons/icons.txt swaps the set. Lucide is supported the same way, and
   previous sets are in the archive beside the manifest, including the four
   hand drawn originals.

   The colour is baked into each file rather than driven by tokens here. It
   could come through a CSS mask, but the duotone weight could not, and one
   pipeline that works for every weight beats two. The trade is deliberate: a
   colour change is a script rerun, not a stylesheet edit.

   Every icon is decorative. The heading beside it does the naming. */
.icon {
  display: block;
  width: var(--icon-size);
  height: var(--icon-size);
  flex: 0 0 auto;
}
/* Office, contact and company details. Ruled like .feature-grid and on the
   same 56px measure; the cells stretch now rather than sitting at their own
   heights, because what has to line up is the spine between them and not the
   foot of the shortest column. */
.contact-panel {
  display: grid;
  grid-template-columns: 1.2fr 1fr 1fr;
  column-gap: 56px;
  overflow: hidden;
}
.contact-card {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 8px;
  padding: 26px 0 30px;
  color: var(--text-body);
  font-style: normal;
}
.contact-card .data-kicker {
  margin-bottom: 24px;
  color: var(--brand-label);
}
.contact-card strong {
  color: var(--text-heading);
  font-size: 1.15rem;
}
.contact-card a {
  color: var(--text-heading);
  font-weight: 600;
}
/* The company name is a link to the parent company, so it keeps the weight of
   the surrounding strong rather than dropping to the lighter link weight. The
   underline is what marks it: inside an all-bold address line the weight says
   nothing, and it measured identical to the text around it, so the only way to
   find the link was to hover something that gave no sign of being one. Green
   would be louder than an address block wants. */
.contact-card strong a {
  font-weight: inherit;
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 3px;
}
.contact-card a:hover {
  color: var(--link-hover);
}

.check-list {
  display: grid;
  gap: 8px;
  padding: 0;
  margin: 20px 0 0;
  list-style: none;
}
.check-list li {
  position: relative;
  padding-left: 26px;
  color: var(--text-body);
}
.check-list li::before {
  content: "";
  position: absolute;
  /* Centred on the first line, not dropped a fixed distance into it. 0.7em put
     the mark's centre about 2px under the line's, which is the amount that
     makes a square read as sitting on the baseline rather than beside the
     words — and it was measured against one size, so every other size it was
     used at was wrong by a different margin. Half a line less half the mark
     holds at any size and any line-height.

     The 0.02em is the optical correction on top of the geometric one. A line
     box is centred on the em box, but a font's lowercase mass sits a little
     below that: the x-height band runs from the baseline up, and its middle
     lands under the middle of the em. A square centred by arithmetic still
     reads high, so it goes down by that much.

     The px fallback is the same sum with the inherited 1.65 line-height
     resolved by hand, for engines without the lh unit; the calc below it is
     dropped whole where lh does not parse. */
  top: calc(0.845em - 4.5px);
  top: calc((1lh - 9px) / 2 + 0.02em);
  left: 0;
  width: 9px;
  height: 9px;
  background: var(--brand-core);
}

.coverage-stat strong {
  display: block;
  font-family: var(--font-data);
  font-size: clamp(4rem, 6.5vw, 6rem);
  font-weight: 400;
  line-height: 0.85;
  letter-spacing: -0.08em;
}
.coverage-stat strong span {
  color: var(--brand-on-dark);
}
.coverage-stat p {
  max-width: 29ch;
}
.region-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  align-content: center;
}
.region-grid span {
  display: flex;
  gap: 18px;
  padding: 16px 8px;
  border-bottom: 1px solid var(--line-dark);
}
.region-grid b {
  color: var(--brand-on-dark);
  font-family: var(--font-data);
  font-size: 0.68rem;
}

/* Two dark grounds, one treatment set. --deep is the terminal band: the
   closing CTA, and the near-black the primary button is filled with. It was
   reachable only by being a .final-cta, which made it a ground the section
   vocabulary could not name. Everything a dark ground implies is written once
   against both, so a band can move between them by swapping one class.
   :is() takes the specificity of its most specific argument, so each of these
   still weighs exactly what .section--inverse-secondary weighed on its own. */
.section--inverse-secondary {
  background: var(--inverse-secondary);
}
.section--inverse-primary {
  background: var(--inverse-primary);
}
.section--inverse-secondary,
.section--inverse-primary {
  color: var(--text-on-dark);
}
:is(.section--inverse-secondary, .section--inverse-primary) .eyebrow {
  color: var(--brand-on-dark);
}
/* The focus ring takes the same swap the eyebrow and the links take. Solid
   --brand-label is 5.4:1 on --base but only 1.37:1 on --inverse-primary, so a
   keyboard user could not see focus at all on the closing CTA bands. Redefining
   the token rather than the outline means every rule that already reads
   var(--focus-ring) follows the ground it lands on. */
:is(.section--inverse-secondary, .section--inverse-primary) {
  --focus-ring: var(--brand-on-dark);
  --link: var(--brand-on-dark);
  --link-hover: var(--glow-5);
}
/* .token-code was missed when green text moved to --brand-on-dark on the dark
   bands: --brand-label measured 3.02:1 there, under the 4.5:1 body text needs. */
:is(.section--inverse-secondary, .section--inverse-primary) .token-code {
  color: var(--brand-on-dark);
}
/* The same inversion on dark ground, and parked on the same terms as the light
   one above. White and #c9cfcb swap roles here: the title would take the
   recessive value and the prose the full-strength one, because --text-muted
   cannot cross over, it is a light-surface value.

   That hex is why this rule outlived the park. The light twin was returned to
   --text-heading by name, and this one names no token, so it did not read as
   part of the same decision and kept the inversion running on one ground:
   18 headings across 13 pages were still showing two tones inside a title
   while the other 110 leads on the site were inert.

   Restoring it is this one value, back to #c9cfcb, at the same time as the
   light rule goes back to --text-muted. Not before: they are one decision. */
:is(.section--inverse-secondary, .section--inverse-primary) .section-title {
  color: var(--text-on-dark);
}
:is(.section--inverse-secondary, .section--inverse-primary) .body-copy {
  color: var(--text-on-dark);
}
:is(.section--inverse-secondary, .section--inverse-primary) p,
:is(.section--inverse-secondary, .section--inverse-primary) li {
  color: var(--text-muted-on-dark);
}

/* ==========================================================================
   Merged layout vocabulary
   Thirteen two-column classes, five band classes, four callouts and three
   disclosure lists used to be spelled separately. They are one set each now.
   The rule throughout: this block owns layout, and a class that survives
   alongside it owns only its skin.
   ========================================================================== */

/* Two columns, top-aligned. Every two-column section on the site is this plus
   at most a ratio, and the ratio is a token rather than a number picked per
   page. Four ratios cover thirteen former classes. */
.split {
  --split-ratio: 1fr 1fr;
  --split-gap: 56px;
  display: grid;
  grid-template-columns: var(--split-ratio);
  gap: var(--split-gap);
  align-items: start;
}
/* A column that ends in the way out of its section places it at its own foot,
   level with the rule the row closes on, rather than 22px under whatever line
   the paragraph happened to finish on. Ten splits end this way and each was
   reaching that arrangement — or not reaching it — on its own; this is the one
   recipe, and the same one .feature-card--linked already uses: the auto margin
   does the pushing, the padding keeps the gap when there is no height to push
   through. .channel-lead was the hand-rolled version of it, on API and Pricing.
   It bottomed the paragraph instead of the link, which put the prose in the
   middle of its column on the one use that had no way out under it; both go
   through this now and the class is gone.

   Scoped with :has() rather than declared on every column, because the other
   thirteen splits hold lists, tables, forms and accordions, and making those
   flex containers would change how their own children space themselves for no
   gain. The column has to be a flex container for an auto margin to have
   anything to absorb, so the test is exactly the condition that needs it. */
.split > *:has(> .section-link:last-child) {
  display: flex;
  flex-direction: column;
}
.split > * > .section-link:last-child {
  margin: auto 0 0;
  padding-top: 22px;
}
/* And where the column has no way out, the prose itself is what closes it. A
   ruled split stretches both columns to the spine, so a paragraph answering a
   three-line title sat at the top of a column whose foot rule was 100px under
   it, while the title it answers ended on that rule. Bottomed, the two columns
   finish on the same line and the row reads as one band.

   This is what .channel-lead was reaching for and missing: it bottomed the
   paragraph with an auto margin above and below, which centres rather than
   bottoms. Here the column does the placing and the paragraph is only its last
   child. Held to a lone .body-copy answering a .section-heading — the pairing
   that has a title to finish level with — so a column holding a note, a list or
   a form is untouched. */
.split--ruled > .section-heading + *:has(> .body-copy:only-child) {
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
}
/* A slightly narrower first column: a heading introducing the column beside it. */
.split--lead {
  --split-ratio: 0.9fr 1.1fr;
}
/* A wide first column with a supporting card or action beside it. */
.split--aside {
  --split-ratio: 1.35fr 0.65fr;
}
/* Columns that read as one thing rather than two: they want to sit close, not
   across a 56px gap. */
.split--tight {
  --split-gap: 24px;
}
.split--center {
  align-items: center;
}
.split--stretch {
  align-items: stretch;
}
/* A hairline between the columns instead of a gap, for splits that read as one
   ruled block rather than two separate things. */
.split--ruled {
  --split-gap: 0;
  /* The spine divides the whole block, so both columns run its full height
     whatever they hold. Without this the rule stops wherever the column
     carrying it runs out, which is only ever right by accident. */
  align-items: stretch;
  border-top: 1px solid var(--line);
}
.split--ruled > * {
  padding: 34px 34px 24px 0;
}
.split--ruled > * + * {
  padding-left: 34px;
  border-left: 1px solid var(--line);
}
/* Closes a ruled split at the foot as well as the head, so it reads as a band
   in the page rather than as the top edge of whatever follows it. */
.split--closed {
  border-bottom: 1px solid var(--line);
}
/* Its counterpart. --closed adds the foot rule, --open drops the head one, so
   a ruled split can carry a rule on either edge, both, or only the spine. */
.split--open {
  border-top: 0;
}
/* Which column the heading takes is a property of the layout, not of the
   source order: the heading stays first in the markup, so it is still read and
   announced first, and only the columns swap. */
.split--reverse > *:first-child {
  order: 2;
}
/* The spine belongs between the columns whichever way round they are, so the
   padding and the rule move with them. */
.split--ruled.split--reverse > *:first-child {
  padding: 34px 0 24px 34px;
  border-left: 1px solid var(--line);
}
.split--ruled.split--reverse > *:last-child {
  padding: 34px 34px 24px 0;
  border-left: 0;
}
/* A column whose own rows are ruled lets those rules meet the spine instead of
   stopping short of it, so the horizontals and the vertical make one grid. The
   column drops the padding on the spine side and the rows take it, which keeps
   the text off the rule without shortening it. */
.split--ruled.split--reverse > .step-rows {
  padding-right: 0;
}
.split--ruled.split--reverse > .step-rows > * {
  padding-right: 34px;
}
:is(.section--inverse-secondary, .section--inverse-primary) .split--ruled,
:is(.section--inverse-secondary, .section--inverse-primary) .split--ruled > * + * {
  border-color: var(--line-dark);
}
/* Skins that survive the merge, holding no layout of their own. What helps-split
   used to hold was a border round the pair and a grey fill on the second
   column, which boxed a split that .split--ruled had already divided; the
   crosses down the negative column say which side it is without the tint. */
.helps-split h3 {
  font-size: 1.5rem;
}
/* A list of things the platform does not do. The mark is the whole point, so
   it is asked for by name rather than inferred from which column it landed in. */
.check-list--negative li::before {
  content: "×";
  top: 0;
  width: auto;
  height: auto;
  color: var(--warn);
  background: transparent;
  font-weight: 700;
}
/* What is left of a column that used to be a box. The border went first: it
   was drawing an outline round a list that reads perfectly well on the page
   ground. The padding went with the callout beside it — it existed to hold
   this column's heading level with that one's, hand-matched, and the ruled
   split the pair sits in now pads both columns from one rule. A skin holding
   nothing but its heading size is close to not being a class, and it stays one
   only because that size is a match to something on the other side of a spine.

   Matched to .margin-note__lead, which is itself matched to .callout h3: the
   three are the same line in three treatments, and this column and the note
   beside it used to sit two pixels out of line with each other. */
.regulated-role h3 {
  margin: 0 0 12px;
  font-size: 1.35rem;
}
/* A dark panel inside a light section: the coverage figure on Data, the trial
   band on Pricing. It carries .section--inverse-secondary alongside, which is where the
   ground already lives, and with it everything the ground implies — paragraph
   colour, link colour, the eyebrow, the rules a ruled split draws. What is
   left here is the inset. */
.dark-panel {
  padding: 44px;
  border-radius: var(--radius-large);
}
.api-teaser .split > div:last-child {
  padding-top: 8px;
}
.api-teaser .split .button {
  margin-top: 20px;
}
/* The API request and response sample. Lifted off .technical-grid so the code
   panel is a thing you can put anywhere, not a side effect of a layout.

   Two variants, and the dark one is the base. Its three values were raw:
   #0f1110 for the ground, #3b423e for the border, #d9dedb for the code. The
   ground is the one worth naming, because #0f1110 is eight channel points off
   --inverse-primary and that is the same near-duplicate the four grounds exist
   to prevent. It is not a fifth dark surface; it is --inverse-primary, which a
   panel recessed into the --inverse-secondary band should take anyway. The
   other two follow the tokens that now exist for a dark ground. */
.code-panel {
  overflow-x: auto;
  margin: 0;
  padding: 24px 26px;
  border: 1px solid var(--line-dark);
  background: var(--inverse-primary);
  font-family: var(--font-data);
  font-size: 0.74rem;
  line-height: 1.55;
}
/* The code declares no colour on either ground, because it does not need one.
   It is the panel's whole contents, so it is primary ink, and primary ink is
   what it inherits: --text-on-dark down from the band on the dark variant,
   --text-heading from the panel itself on the light one. Two declarations used
   to say this and disagreed about the rank, the dark one recessive at
   --text-muted-on-dark and the light one primary. Deleting both is what makes
   them agree, and it is also the only way inheritance can reach either: a
   direct declaration on .code-panel code beats a colour inherited from the
   panel, so while one variant declared it the other had to as well.

   em keeps its font-style and drops its colour for the same reason. On the dark
   panel the colour repeated what it already inherited; on the light one it
   would have painted white on white the day someone used it. */
.code-panel code em {
  font-style: normal;
}
/* b is the exception, and a real one: a green highlight is not the ink around
   it. It takes the green of the ground it lands on, which is the pair
   .code-note beside it already uses. */
.code-panel code b {
  color: var(--brand-on-dark);
  font-weight: 400;
}
/* white, not --base, and deliberately: a panel that paints pure white reads as
   lifted off a ground that is two points under it, which is the reason --base
   is not white. This is the one colour the light variant has to state, and the
   code inside inherits it. */
.api-code-light .code-panel {
  border-color: var(--line);
  color: var(--text-heading);
  background: var(--raised);
}
.api-code-light .code-panel code b {
  color: var(--brand-label);
}

/* Three equal bands naming the audiences the product serves. Home states them,
   Platform mentions them in passing, so the difference is type size only. */
.audience-bands {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  border-block: 1px solid var(--line);
}
.audience-bands article {
  padding: 22px 30px 22px 0;
}
.audience-bands article + article {
  padding-left: 30px;
  border-left: 1px solid var(--line);
}
/* The bands never numbered anything, so here the icon is an addition rather
   than a replacement: it sits above the name, on the band's own top margin. */
.audience-bands .icon {
  margin-bottom: 16px;
}
.audience-bands h3 {
  margin: 0 0 10px;
  font-size: 1.3rem;
}
.audience-bands p {
  margin: 0;
  color: var(--text-body);
}
/* The compact band is the same component a size down, so the icon comes down
   with the type rather than sitting over it at full height. */
.audience-bands--compact {
  --icon-size: 26px;
}
.audience-bands--compact .icon {
  margin-bottom: 12px;
}
.audience-bands--compact h3 {
  margin-bottom: 6px;
  font-size: 1.05rem;
}
.audience-bands--compact p {
  font-size: 0.9rem;
}

/* Numbered rows: an ordinal column, then a heading and a line of body. Used on
   white for what you can build with the API and on dark for the trade process.
   The ordinal column is one width now instead of 70px in one place and 52px in
   the other. */
.step-rows {
  --ordinal: 64px;
  max-width: 1040px;
  padding: 0;
  margin: 0;
  list-style: none;
}
/* A rule between the rows, not around them, so the sequence reads open at both
   ends. Same as .model-flow and .match-levels. */
.step-rows > * {
  display: grid;
  grid-template-columns: var(--ordinal) 1fr;
  gap: 22px;
  padding: 16px 0;
}
.step-rows > * + * {
  border-top: 1px solid var(--line);
}
.step-rows > * > span {
  padding-top: 5px;
}
/* The ordinal column is 64px and the icon is 32, so it sits at the head of the
   column rather than filling it. The nudge lines its optical centre up with
   the cap height of the h3 beside it, which 5px of padding does for a line of
   mono and does not for a square. */
.step-rows > * > .icon {
  margin-top: 2px;
}
.step-rows h3 {
  margin: 0;
  font-size: 1.35rem;
}
.step-rows p {
  margin: 7px 0 0;
  color: var(--text-body);
}
:is(.section--inverse-secondary, .section--inverse-primary) .step-rows > * + * {
  border-color: var(--line-dark);
}
:is(.section--inverse-secondary, .section--inverse-primary) .step-rows > * > span {
  color: var(--brand-on-dark);
}
:is(.section--inverse-secondary, .section--inverse-primary) .step-rows p {
  color: var(--text-muted-on-dark);
}

/* Icons beside a heading and a line of copy. Three across on the API page,
   four on Pricing; the count is the only difference and it is a modifier. */
.icon-grid {
  display: grid;
  grid-template-columns: repeat(var(--icon-grid-columns, 3), minmax(0, 1fr));
  /* The row gap was 24px against a card that puts 20px between its own icon and
     its own copy: a card in the second row sat as close to the card above it as
     that card's heading sits to its icon, so five cards read as one column of
     ten things. The space between two cards has to beat the space inside one,
     and by enough to be read as a different kind of gap. The columns keep 28px,
     where the space between them is only ever between cards. */
  gap: 48px 28px;
  padding: 0;
  margin: 0;
  list-style: none;
}
.icon-grid--three {
  --icon-grid-columns: 3;
}
.icon-grid--four {
  --icon-grid-columns: 4;
}

/* Stacked disclosure: the platform workflow, the trade pathways and the
   pricing FAQ. The plus-to-minus marker is why these are one component and not
   two — a reader should not have to guess what expands. */
.disclosure {
  display: grid;
  max-width: 960px;
  gap: 0;
  border-bottom: 1px solid var(--line);
}
.disclosure details {
  border-top: 1px solid var(--line);
}
.disclosure summary {
  display: flex;
  gap: 20px;
  align-items: center;
  padding: 15px 4px;
  cursor: pointer;
  font-size: 1.15rem;
  list-style: none;
}
/* display: flex on a <summary> suppresses the native triangle, so these rows
   need a marker of their own. The same pair of icon files the region cards on
   Pricing use, drawn through a mask so the mark takes the row's own colour.
   The row is a flex container, so the marker is centred by align-items and
   needs no baseline nudge of its own. */
.disclosure summary::-webkit-details-marker {
  display: none;
}
.disclosure summary::after {
  content: "";
  width: 16px;
  height: 16px;
  margin-left: auto;
  background: var(--brand-label);
  -webkit-mask: url("/assets/icons/plus.svg") center / contain no-repeat;
  mask: url("/assets/icons/plus.svg") center / contain no-repeat;
}
.disclosure details[open] summary::after {
  -webkit-mask-image: url("/assets/icons/minus.svg");
  mask-image: url("/assets/icons/minus.svg");
}
.disclosure summary:hover {
  color: var(--brand-label);
}
.disclosure summary:focus-visible {
  outline: 2px solid var(--focus-ring);
  outline-offset: 2px;
}
.disclosure details p {
  max-width: 68ch;
  margin: 0 0 18px 44px;
  color: var(--text-body);
}

/* All three accordions put their heading in the left column rather than above
   the rows: the question on one side, the answers on the other. The heading is
   a column here, not a stacked block, so the gap does its spacing and the cap
   that keeps a full-width title from running long is off. */
.split > .section-heading {
  max-width: none;
  margin-bottom: 0;
}
.split > .disclosure {
  max-width: none;
}

/* A heading with its lead paragraph beside it rather than under the content it
   introduces — the same .split the problem statement on Home already uses, for
   a paragraph that qualifies a whole section instead of trailing one part of
   it. Two differences from that one, both below. */
.section-lead {
  /* Full height, and each column places its own content. This aligned the feet
     before, on the reasoning that a heading of two display lines against three
     of body should land on the rule below rather than leave the paragraph
     floating. What it did in practice was the reverse: the paragraph column is
     the taller one in sixteen of the seventeen uses, so it was the eyebrow and
     title that got pushed down the page, beside a paragraph that started at the
     top, and the two blocks no longer read as starting together. Tops, then —
     and the feet still meet the rule, because the column below places its way
     out at its own foot rather than the row doing it for both. */
  align-items: stretch;
  /* .split zeroes the heading's own bottom margin, on the assumption that the
     section ends where the columns do. Here the chain and its illustration
     follow, so the pair carries that spacing itself. */
  margin-bottom: var(--space-heading);
}
/* The seventeenth use, and the exception the rule above describes rather than a
   change of mind about it. Tops are right when the paragraph is the taller
   column, because a lead that starts level with the eyebrow is what makes the
   two read as one block. Over the normalisation table the heading is the taller
   one — three display lines against two of body — so aligning tops leaves the
   lead hanging in the middle of the row with the table's own edge below it.
   Bottoms, and the two blocks finish together on the table they introduce. */
.section-lead--foot > *:last-child {
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
}
/* .body-copy sets a measure, a colour and a size and no margins, so a paragraph
   sitting loose in a section keeps the browser's own 1em above and below — a
   sensible default in flowing prose, and 17px of dead space under the last line
   when that line is supposed to be the foot of the column. The column's last
   child closes on the edge; anything above it keeps its rhythm. */
.section-lead--foot > *:last-child > :last-child {
  margin-bottom: 0;
}

/* An aside that qualifies the section it sits in — almost always a limit on
   what the data proves. One recipe, two tones and a boxed variant for the ones
   that sit beside content rather than under it. */
.callout {
  display: block;
  max-width: 900px;
  margin: 26px 0 0;
  padding: 20px 24px;
  border-left: 4px solid var(--brand-label);
  color: var(--text-strong);
  background: var(--brand-soft);
  font-size: 0.98rem;
}
/* Procedural or regulatory, rather than a caveat about the data itself. */
.callout--quiet {
  border-left-color: var(--slate);
  background: var(--slate-soft);
}
.callout--boxed {
  max-width: none;
  padding: 26px;
  border: 1px solid var(--line);
  border-left: 4px solid var(--brand-label);
}
.callout--boxed.callout--quiet {
  border-left-color: var(--slate);
}
.callout h3 {
  margin: 0 0 12px;
  font-size: 1.35rem;
}
.callout p {
  margin-bottom: 0;
  color: var(--text-body);
}
.callout p:first-of-type {
  margin-top: 0;
}
/* The top margin is for a callout that follows running copy. As a column of a
   split it follows nothing, and the margin was pushing its heading 26px below
   the one in the column beside it. */
.split > .callout {
  margin-top: 0;
}

/* A remark in the margin rather than a card in the column. The .callout above
   is the section raising its voice — tinted ground, green edge, ink a step
   firmer than body — which is right for a limit stated inside the argument and
   wrong for one stated beside it: two of those side by side and the page has a
   box for every paragraph. This says aside instead, on the terms .hero-qualifier
   already set: tertiary ink, italic, no fill and no border of its own. The
   spine of the ruled split is what puts it beside the column, so the note draws
   nothing and the layout carries the separation.

   One ink for the whole note. The reference has the opening line a shade
   firmer than the sentence under it, which is a fifth step on a scale that
   deliberately has four; size, weight and the mark already say which line is
   the claim, so both take --text-muted and the scale stays closed. */
.margin-note {
  display: flex;
  flex-direction: column;
  /* For a note that follows the content it qualifies. As a column of a split it
     follows nothing and the split's own padding places it, so the margin comes
     off there — the same arrangement .callout and .split > .callout already
     have between them. */
  margin-top: 30px;
  /* One gap for the note's own lines, so a note that runs to two paragraphs of
     small print spaces them the same way it spaces its mark from its first
     line. The lead adds to this rather than replacing it. */
  gap: 14px;
  color: var(--text-muted);
  font-style: italic;
  /* font-synthesis is none at the root for weight, and Geist ships no italic
     file, so style is re-enabled here alone — the same exception, for the same
     reason, as .hero-qualifier. */
  font-synthesis: style;
}
/* :where() so this stays at one class and the two named lines below still win
   on source order. It reaches nested paragraphs because a note that lays its
   small print out in columns holds them one level down, inside the .split doing
   the laying out. */
.margin-note :where(p),
.margin-note > * {
  margin: 0;
  font-size: 1rem;
  line-height: 1.6;
}
/* An asterism, the mark a printed page uses for a break in the argument. It is
   punctuation, not a sentence, so it leaves the reading order; the spacing
   between the three is tracking rather than typed spaces, so it stays one
   token to a screen reader that reaches it anyway and one string to translate.

   The rule above resets the note's children as children rather than as
   paragraphs, so the two lines that do want space below them can ask for it
   under their own name and win on source order. That matters beyond the
   margins: the lead is a paragraph where the note qualifies a section and a
   heading where it answers a column that has one, and neither the reset nor
   the two rules under it should care which. */
.margin-note__mark {
  letter-spacing: 0.4em;
  line-height: 1;
}
/* Matched to .callout h3, because it is the same line doing the same job in the
   other treatment: the claim the paragraph under it qualifies. */
.margin-note__lead {
  margin-bottom: 8px;
  font-size: 1.35rem;
  font-weight: var(--weight-title);
  line-height: 1.35;
  letter-spacing: -0.01em;
}
/* Where the note answers a whole argument rather than a paragraph, it belongs
   at the foot of it — level with the rule the row closes on, where that
   argument finishes, rather than starting level with its eyebrow. This is a
   property of the pairing and not of the note: beside a single paragraph the
   two read as a pair and both start at the top, which is the default above.
   Stacked below on a phone it does nothing either way, which is correct —
   there is no column left to answer. */
.margin-note--foot {
  justify-content: flex-end;
}
.split > .margin-note {
  margin-top: 0;
}
/* Standing under the content it qualifies rather than beside it, the note has
   no spine to be on the other side of, so it takes a rule of its own — the
   footnote rule a printed page draws above its small print. Beside a column it
   would be a second line parallel to the spine, which is why this is asked for
   by name rather than given to every note. */
.margin-note--ruled {
  padding-top: 26px;
  border-top: 1px solid var(--line);
}
/* Under a block that already rules its own rows, the note's rule is that
   block's foot and not a line floating below it: the last row's padding is the
   space, so the note's own margin comes off. On .match-levels that lands the
   foot rule 26px under the last level, the same 26px its top rule sits above
   the first — the list closes the way it opens. .feature-grid is the same
   arrangement one page later: head rules on every cell, spines between them,
   and 30px of padding under the last line of a card. */
:is(.match-levels, .feature-grid) + .margin-note--ruled {
  margin-top: 0;
}
/* A line of small print under a full-width block has no column to be the
   measure of it, and the rule above it runs the whole container. So the note's
   own paragraphs take the measure running copy takes everywhere else. Direct
   children only: a note that lays its small print out in columns is already
   measured by them, and capping the layout would squeeze both into one. */
.margin-note--ruled > p {
  max-width: 68ch;
}
/* Beside a note, the heading shares its column with the copy and the buttons
   instead of being the column, so it is not the .split child that already
   drops the measure. The column is the width the title should run to. */
.split:has(> .margin-note) > *:first-child > .section-heading {
  max-width: none;
}
/* The ground and its text colour come from .section--inverse-primary now; what is left
   here is the CTA's own rhythm and headline. */
.final-cta {
  padding-block: clamp(72px, 6vw, 92px);
}
.final-cta-inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 50px;
}
/* The same measure the section headings answer to, on the band that was the
   last one setting its own. The heading here is the largest type on the site
   after the hero — clamp tops out at 3.6rem — and with nothing holding it the
   text half took whatever it wanted and the buttons were left the remainder.

   The two rules are one mechanism. The cap alone still lets the flex algorithm
   take width off the buttons first, because a text block whose content is wider
   than its cap has more to give up on paper; flex: 0 0 auto says the pair of
   buttons is not the thing that yields. Below about a 1130px container the two
   no longer both fit at full size and the deficit goes to the text, which drops
   under 60% rather than making the buttons stack. Which is the point: the
   buttons were stacking at every width, because the heading had pushed the
   actions column narrower than one button. */
.final-cta-inner > :first-child {
  max-width: var(--measure);
}
.final-cta .hero-actions {
  flex: 0 0 auto;
}
.final-cta :is(h2,
h3) {
  margin: 0;
  font-size: clamp(2.25rem, 4vw, 3.6rem);
  font-weight: 550;
  line-height: 1.04;
  letter-spacing: -0.04em;
}
/* The same two tones inside a heading that .section-title carries, on the one
   heading that is not a .section-title, and parked with it. It applies only
   when a lead is actually marked: the short CTA headings take none, and
   dropping those to the recessive value would leave the band with nothing at
   full strength. No CTA heading carries a lead today, so this is inert either
   way, and it is parked rather than left at #c9cfcb so that marking one later
   cannot bring the inversion back on this heading alone.

   Restoring it is this one value, back to #c9cfcb, with the two rules above. */
.final-cta :is(h2, h3):has(.lead) {
  color: var(--text-on-dark);
}
.final-cta :is(h2, h3) .lead {
  color: var(--text-on-dark);
}
/* The fill belongs to the dark grounds, not to the one section that happened
   to have it first. .button-outline is declared further down and still wins on
   source order, so an outline button on dark stays an outline. */
:is(.section--inverse-secondary, .section--inverse-primary) .button {
  color: var(--inverse-primary);
  background: var(--raised);
}

.site-footer {
  padding: 62px 0 28px;
  border-top: 1px solid var(--line);
}
.footer-grid {
  display: grid;
  grid-template-columns: 1.5fr repeat(4, 1fr);
  gap: 38px;
}
/* Spacing that the padded PNG used to supply on its own. */
.footer-intro .brand {
  margin-bottom: 34px;
}
.footer-intro p {
  max-width: 38ch;
  margin-top: 0;
  color: var(--text-muted);
  font-size: 0.98rem;
}
.footer-group h2 {
  margin: 4px 0 18px;
  font-size: 0.95rem;
}
.footer-group a {
  display: block;
  margin: 10px 0;
  color: var(--text-muted);
  font-size: 0.93rem;
}
.footer-group a:hover {
  color: var(--brand-label);
}
.footer-bottom {
  display: flex;
  justify-content: space-between;
  gap: 30px;
  margin-top: 42px;
  padding-top: 26px;
  border-top: 1px solid var(--line);
  color: var(--text-muted);
  font-size: 0.82rem;
}
.footer-bottom p {
  max-width: 760px;
  margin: 0;
}

.gateway {
  min-height: 100vh;
  display: grid;
  place-items: center;
  background: var(--sunken);
}
.gateway-card {
  width: min(640px, calc(100% - 32px));
  padding: 54px;
  text-align: center;
  border: 1px solid var(--line);
  border-radius: var(--radius-large);
  background: var(--raised);
  box-shadow: var(--shadow);
}
.gateway-card h1 {
  font-size: clamp(2.5rem, 6vw, 4.4rem);
  line-height: 1;
  letter-spacing: -0.045em;
}
.language-list {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 10px;
  padding: 0;
  list-style: none;
  text-align: left;
}
.language-list li {
  padding: 13px;
  border: 1px solid var(--line);
  border-radius: 0;
  color: var(--text-muted);
}
.language-list a {
  color: var(--brand-label);
  font-weight: 600;
}

@media (max-width: 1024px) {

  .icon-grid--three {
    --icon-grid-columns: 2;
  }
  :root {
    --space-section: clamp(64px, 7vw, 80px);
    --space-section-tight: clamp(56px, 6vw, 68px);
  }
}

@media (max-width: 768px) {

  .split {
    --split-ratio: 1fr;
    /* row-gap, not --split-gap. A 40px --split-gap used to sit here and never
       ran: the 1120px block further down restates --split-gap, and at a phone
       width both queries match, so the later one won and the stacked gap was
       46px. row-gap is set after the shorthand that reads the variable, so it
       holds. One column, so this is the only axis that is left.

       20px, which is --space-heading at every width this block covers, and so
       the gap the split below already takes when its first column is the
       section heading. Stacked, no split is two columns any more: it is a run
       of blocks in one column, and the space between two of them is the space
       between two blocks of prose rather than the space between two halves of a
       section. 32px was the second of those, and with the margins below still
       falling into it the reader was seeing 66. */
    row-gap: 20px;
  }
  /* The margins the columns bring to the gap, which side by side are invisible
     — every column starts and ends on the same line, so nothing was ever
     written to take them off. Stacked they land in the row gap and add to it,
     and a paragraph carries 1em at both ends: 17 + 32 + 17 between the two
     halves of Home's chain reading, which is a section break's worth of air
     between two sentences that finish each other's thought.

     The heading-led split had this fixed one relationship at a time — the lead
     paragraph's leading margin, three wrappers deep — because there the gap has
     a name and a token. This is the same double count everywhere else, taken
     off at both ends, so the row gap above is the whole gap and the number in
     the stylesheet is the number on the screen. */
  .split > * > :is(.body-copy, .editorial-body):first-child {
    margin-top: 0;
  }
  .split > * > :is(.body-copy, .editorial-body):last-child {
    margin-bottom: 0;
  }
  /* Stacked, there is nothing left for the spine to divide, so the ruled split
     is a plain stacked split and takes the same row gap as every other one.
     --split-gap still has to be zeroed: the 1120px block restates it and both
     queries match at a phone width. */
  .split--ruled {
    --split-gap: 0;
  }
  /* Only once the split has stacked. Side by side that margin is optical
     alignment, and a good one: it drops the paragraph off the eyebrow and onto
     the title beside it. Stacked there is no column to align to, so the same
     28px lands in the row gap and adds to it, which is what put 74px between a
     section title and the paragraph answering it.

     The second selector is the same paragraph one level down. Where the column
     carries a way out under the prose it is a wrapper holding both, so the
     .editorial-body inside it is not a child of the split and kept the margin
     the child selector was written to remove — Home's lead sat 28px lower than
     Data's, stacked, for no reason a reader could see. */
  .split > .editorial-body,
  .section-lead > * > .editorial-body {
    margin-top: 0;
  }
  /* Stacked, a split whose first column is the section heading is not two
     columns any more: it is a title and the lead answering it — the pair a
     section already makes when the lead is the heading's own sibling, and that
     pair is --space-heading everywhere else on the site.

     Here it was the row gap plus whatever the lead's wrapper contributed, and
     the wrapper differs three ways: 32px where .editorial-body is the column
     itself and the rule above zeroes it, 49px through a bare div, whose 1em
     cannot collapse out of a grid item, and 60px where the column is the flex
     container .split > *:has(> .section-link:last-child) makes of it and
     .editorial-body's own 28px cannot collapse either. One relationship, three
     numbers, none of them the token that names it.

     The row gap carries the pair now, so the lead's leading margin comes off
     whichever of the three wrappers it arrives in. This also settles the
     heading that introduces .step-rows rather than a lead: 20px on Trial, where
     the rows are the heading's sibling, and 32px on API and Trade landscape,
     where they are the second column of the same split. */
  .split:has(> .section-heading:first-child) {
    row-gap: var(--space-heading);
  }
  .split > .section-heading + * > :is(.body-copy, .editorial-body):first-child {
    margin-top: 0;
  }
  /* Stacked, there is one column, so a reversed split reads in source order
     again — the heading before the rows it introduces — and the spine is a
     rule between the two blocks rather than beside them. */
  .split--reverse > *:first-child {
    order: 0;
  }
  /* A spine is a property of two columns. Stacked it used to be rewritten as a
     rule between the halves plus 24px of side padding, and both were wrong on a
     phone: the rule divided a section title from the one sentence explaining
     it, and the padding pushed that pair 24px further in than the cards and
     callouts under it, so the section had two left edges. Neither the band
     edges nor the spine survive the stack — the section grounds already say
     where a band starts and ends — and the block sits on the container edge
     with the rest of the page.

     Rules the columns own rather than the split are untouched: .step-rows still
     rules between its rows, because there the horizontal separates one item
     from the next rather than a heading from its own lead. */
  .split--ruled,
  .split--ruled.split--closed {
    border-top: 0;
    border-bottom: 0;
  }
  .split--ruled > *,
  .split--ruled > * + *,
  .split--ruled.split--reverse > *:first-child,
  .split--ruled.split--reverse > *:last-child {
    padding: 0;
    border-top: 0;
    border-left: 0;
  }
  /* Stacked there is no spine, so the rows keep only the column's own padding. */
  .split--ruled.split--reverse > .step-rows > * {
    padding-right: 0;
  }
  /* The one exception, and the reason it is one: a margin note is small print
     qualifying the argument, not a second half of it. Beside a column the spine
     already set it apart; under one it has nothing, and small print that simply
     follows the copy reads as more copy. So it takes the footnote rule a
     printed page draws above its small print — the treatment .margin-note--ruled
     already spells for a note that stands alone under content, applied here to
     every note, because stacked every note stands alone under content. */
  .split > .margin-note {
    padding-top: 26px;
    border-top: 1px solid var(--line);
  }
  :is(.section--inverse-secondary, .section--inverse-primary) .split > .margin-note {
    border-top-color: var(--line-dark);
  }
  .dark-panel {
    padding: 34px 26px;
  }
  .icon-grid {
    --icon-grid-columns: 1;
  }
  .audience-bands {
    grid-template-columns: 1fr;
  }
  .audience-bands article,
  .audience-bands article + article {
    padding: 22px 0;
    border-left: 0;
  }
  .audience-bands article + article {
    border-top: 1px solid var(--line);
  }
  :root {
    --space-section: clamp(48px, 9vw, 64px);
    --space-section-tight: clamp(44px, 8vw, 56px);
    --scroll-offset: 84px;
  }
}

@media (max-width: 1120px) {

  .split {
    --split-gap: 46px;
  }
  /* Both re-asserted, because the line above would otherwise widen them. */
  .split--tight {
    --split-gap: 24px;
  }
  .split--ruled {
    --split-gap: 0;
  }
  .desktop-nav {
    display: none;
  }
  .mobile-toggle {
    display: block;
    margin-left: auto;
  }
  /* A drawer, at both widths the button appears at. Fixed to the right edge
     of the screen and the full height under the header, so the logo and the
     button that closes it again stay where they were. The width caps at 380px
     but always leaves a 44px strip of scrim beside it, which is what keeps it
     a drawer on a narrow phone rather than a full-screen takeover with no
     visible way back. */
  .mobile-toggle .dropdown {
    position: fixed;
    top: var(--header-height);
    right: 0;
    left: auto;
    display: flex;
    width: min(380px, calc(100% - 44px));
    height: calc(100dvh - var(--header-height));
    flex-direction: column;
    border-top: 0;
    border-right: 0;
    border-bottom: 0;
    transition: translate 0.26s ease;
    translate: 0 0;
  }
  /* It slides in from the edge it is anchored to. @starting-style supplies the
     frame to move from; where it is unsupported the drawer simply appears,
     which is all it did before. */
  @starting-style {
    .mobile-toggle[open] .dropdown {
      translate: 100% 0;
    }
  }
  /* The list takes the slack and scrolls on its own, which leaves the two
     actions parked on the bottom edge within thumb reach. */
  .mobile-menu__links {
    overflow-y: auto;
    flex: 1;
    padding: 12px 0 24px;
    overscroll-behavior: contain;
  }
  /* Rows go the full width of the drawer, so the tap target is the panel and
     not a word inside it. */
  .mobile-menu__links a:not(.button) {
    display: flex;
    min-height: 48px;
    align-items: center;
    padding: 12px 20px;
    font-size: 1.0625rem;
  }
  .mobile-menu__actions {
    padding: 16px 20px;
  }
  /* The scrim. Drawn on body rather than inside the drawer, so a tap on it
     lands outside the <details> and the outside-click close in site.js already
     covers it. :has() rather than the class site.js sets, so the scrim is
     there without JavaScript as well. Above the page, below the header. */
  html:has(.site-header .mobile-toggle[open]) body::after {
    content: "";
    position: fixed;
    z-index: 40;
    inset: var(--header-height) 0 0;
    background: color-mix(in srgb, var(--shadow-ink) 42%, transparent);
  }
  /* The page must not scroll behind the drawer. clip rather than hidden, and
     the difference is the whole bug this replaces: hidden makes the root a
     scroll container, which leaves the sticky header resolving against a
     container that no longer scrolls, so it dropped back to its place in the
     document — 900px above the screen if that is how far down the reader
     was. clip suppresses the scrolling without creating that container, so
     the header stays where it is stuck. Where clip is not supported the
     declaration is simply dropped and the page scrolls behind the drawer,
     which is the harmless half of the two failures. */
  .menu-open {
    overflow: clip;
  }
  /* The whole action group goes, not just the two quieter members of it. The
     trial request used to survive here, and with .nav-actions and the toggle
     both pushing right it landed a header's width away from the button that
     opens the menu holding the same link. One control on this edge, and the
     request is the first thing under it. */
  .nav-actions {
    display: none;
  }
  .hero-grid {
    grid-template-columns: 1fr;
  }

  .feature-grid--three,
  .feature-grid--four {
    grid-template-columns: repeat(2, 1fr);
  }
  .footer-grid {
    grid-template-columns: repeat(2, 1fr);
  }
  .footer-intro {
    grid-column: 1/-1;
  }
}

/* Between roughly 1025px and 1280px the copy column and the artwork compete for
   the same space, and a media query here used to narrow the reading measure.
   Three things it did, none of which it needs to do now. It shrank the art
   layer, which sizing the layer off its container already does in step. It
   narrowed .hero__copy, the whole column, which caught the h1 in a cap meant
   for the paragraph and broke "Compare medicines" apart across the band. What
   was left after those two came out was a 500px measure on .hero-copy with
   .hero--still opting back out of it, and neither half of that ran: the 500px
   lost to the 68ch the launch-page block sets below this point, and once it
   never applied the opt-out had nothing left to cancel. Measured before
   removing the last two lines: the paragraph is column-bound across this whole
   range on every hero, so they came out with no change on screen. */

/* Large tablet. The two columns hold, but the artwork gives up ground before the
   copy does. Scaling is the only lever that helps here: the layer is anchored
   right and its subject already runs to that edge, so nudging it sideways would
   push artwork out of the section rather than away from the copy. */
@media (min-width: 1025px) and (max-width: 1180px) {
  .hero--art {
    --hero-art-scale: 0.94;
    --hero-glow-x: 78%;
  }
  .hero--still {
    --hero-art-scale: 1;
  }
  .hero--still > .hero-grid {
    gap: 0;
  }
  .hero--art .hero-grid {
    gap: 34px;
  }
}

/* Stacked. From 1024px down the hero grid is a single column, so the render
   leaves its absolute layer and simply follows the copy. It is framed to its own
   ink box here: the transparent margin that keeps the artwork off the text on
   desktop would only be empty height underneath it. The frame narrows with the
   viewport, so the illustration shrinks rather than being cropped. */
@media (max-width: 1024px) {
  .hero--art {
    --hero-ink-width: calc(var(--hero-ink-right) - var(--hero-ink-left));
    --hero-ink-height: calc(var(--hero-ink-bottom) - var(--hero-ink-top));
    --hero-glow-x: 50%;
    --hero-glow-y: 74%;
    --hero-glow-size: 92% 44%;

    min-height: 0;
  }
  .hero--art > .hero-grid {
    grid-template-columns: 1fr;
    padding-block: 56px 44px;
  }
  .hero__art {
    position: relative;
    z-index: 1;
    top: auto;
    right: auto;
    width: min(100%, clamp(340px, 62vw, 640px));
    margin: 34px auto 0;
    translate: none;
    aspect-ratio: calc(1536 * var(--hero-ink-width) / (1024 * var(--hero-ink-height)));
    overflow: hidden;
  }
  .hero__art > picture {
    position: absolute;
    inset: 0;
  }
  .hero--platform .hero__art,
  .hero--solutions .hero__art,
  .hero--data .hero__art,
  .hero--api .hero__art,
  .hero--pricing .hero__art,
  .hero--legal .hero__art,
  .hero--coverage .hero__art,
  .hero--trial .hero__art,
  .hero--contact .hero__art,
  .hero--about .hero__art,
  .hero--home .hero__art {
    width: min(calc(100% - 48px), var(--max-width));
    margin: 34px auto 0;
    aspect-ratio: 1280 / 1080;
  }
  .hero--still .hero__art img {
    -webkit-mask-image: none;
    mask-image: none;
  }
  .hero__art img {
    position: absolute;
    top: calc(-100% * var(--hero-ink-top) / var(--hero-ink-height));
    left: calc(-100% * var(--hero-ink-left) / var(--hero-ink-width));
    /* The render is deliberately wider than its frame here, so the global
       max-width on images has to step aside or it squashes the artwork. */
    width: calc(100% / var(--hero-ink-width));
    max-width: none;
    height: calc(100% / var(--hero-ink-height));
  }
}

@media (max-width: 768px) {
  body {
    font-size: 16px;
  }
  .container {
    width: min(calc(100% - 30px), var(--max-width));
  }
  .hero--platform .hero__art,
  .hero--solutions .hero__art,
  .hero--data .hero__art,
  .hero--api .hero__art,
  .hero--pricing .hero__art,
  .hero--legal .hero__art,
  .hero--coverage .hero__art,
  .hero--trial .hero__art,
  .hero--contact .hero__art,
  .hero--about .hero__art,
  .hero--home .hero__art {
    width: min(calc(100% - 30px), var(--max-width));
  }
  .brand img {
    width: 165px;
  }
  .hero:not(.hero--art) {
    padding: 82px 0 76px;
  }
  .hero-grid {
    gap: 50px;
  }
  /* Font-size deliberately absent: the launch-page block below this media
     query restates .hero h1 without a media query of its own, so any size set
     here loses to it at every width. The line-height does survive. */
  .hero h1 {
    line-height: 1;
  }
  .hero-copy {
    font-size: 1.125rem;
    line-height: 1.55;
  }

  /* Same as .hero h1 above: a size here cannot win against the launch-page
     block, the line-height can. */
  .section-title {
    line-height: 1.08;
  }
  .editorial-body {
    margin-top: 28px;
  }
  .feature-grid,
  .feature-grid--three,
  .feature-grid--four {
    grid-template-columns: 1fr;
  }
  .contact-panel {
    grid-template-columns: 1fr;
  }
  .region-grid {
    grid-template-columns: 1fr;
  }
  .final-cta-inner {
    align-items: flex-start;
    flex-direction: column;
  }
  .footer-grid {
    grid-template-columns: 1fr 1fr;
    gap: 28px;
  }
  .footer-bottom {
    flex-direction: column;
  }
  .gateway-card {
    padding: 38px 24px;
  }
  .language-list {
    grid-template-columns: 1fr;
  }
}

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    scroll-behavior: auto !important;
    transition: none !important;
  }
}

/* Revised English launch pages */
body {
  min-width: 320px;
  /* clip, not hidden: hidden turns the body into a scroll container and breaks
     position: sticky for the coverage summary. */
  overflow-x: clip;
  font-size: 18px;
  line-height: 1.65;
}
.hero h1,
.spec-h1 {
  font-size: clamp(3rem, calc(5vw - 0.25rem), 4.5rem);
}
.section-title {
  font-size: clamp(2.25rem, 3.2vw, 3rem);
}
.body-copy,
.hero-copy,
.feature-card p,
.section p {
  max-width: 68ch;
}
/* page-hero is the inner-page hero. Six of them carry artwork and get their
   surface from .hero--art; the rest (about, contact, trial, login, legal,
   elements) used to sit on a raw #f5f4f2, a third light grey a point off
   --sunken and a point off the hero canvas. They take --base from .hero now,
   like every other band. */
.page-hero:not(.hero--art) {
  min-height: min(820px, calc(100svh - var(--header-height)));
  isolation: isolate;
}
.page-hero:not(.hero--art)::before {
  display: none;
}
.page-hero:not(.hero--art) .hero-grid {
  position: static;
  grid-template-columns: minmax(0, 1.05fr) minmax(360px, 0.95fr);
}
/* The hero's three facts. Tertiary rather than green — they qualify the claim
   above them, they are not a third call to action — and divided by a rule
   rather than by dots, which were two more marks on a line that only holds
   three things.

   The rule is drawn half a gap to each item's left and the line clips its
   overflow, so an item that begins a wrapped line has its rule outside the box
   and loses it. Same mechanism as .feature-card's spine, and it is what makes
   this safe to wrap: on a phone the third fact drops to its own line and does
   not start with a divider. */
.hero-stat {
  display: flex;
  flex-wrap: wrap;
  gap: 2px 32px;
  overflow: hidden;
  margin: 0 0 22px;
  color: var(--text-muted);
  /* The buttons below it, to the pixel. These three facts and those two labels
     are the same rank — short, fixed, read at a glance — and at body size the
     line was competing with the paragraph above instead of following it. */
  font-size: 0.9375rem;
}
.hero-stat span {
  position: relative;
}
.hero-stat span::before {
  content: "";
  position: absolute;
  top: 0.2em;
  bottom: 0.2em;
  left: -16px;
  border-left: 1px solid var(--line);
}
/* A note, not a badge. The fill and the border made a chip of a line that is
   only ever read once, and a chip on the hero competes with the two buttons
   above it for the same attention. What is left says aside: tertiary, italic
   and an icon.

   font-synthesis is none at the root, which is deliberate for weight — Plex
   Mono ships 400 and 600 and nothing between. Style is the one axis this line
   wants and the face has no italic file, so it is re-enabled here alone.

   Declared once. It was two rules eleven lines apart, the second turning the
   note into a flex container: every text run between the link and the full
   stop became its own flex item with an 8px gap, so the sentence came apart.
   The icon is inline instead, and the copy is just copy. */
.hero-qualifier {
  display: inline-block;
  max-width: 620px;
  margin-top: 12px;
  color: var(--text-muted);
  font-family: var(--font-data);
  font-size: 0.76rem;
  font-style: italic;
  font-synthesis: style;
  line-height: 1.7;
}
/* The icon is a mask rather than an <img>, which is the one thing that lets it
   take the colour of the line it sits on. The generated file is filled
   --brand-label like every other icon; through a mask only its shape is used
   and currentColor supplies the ink. */
.hero-qualifier::before {
  content: "";
  display: inline-block;
  width: 15px;
  height: 15px;
  margin-right: 7px;
  vertical-align: -2px;
  background: currentColor;
  -webkit-mask: url("/assets/icons/info.svg") center / contain no-repeat;
  mask: url("/assets/icons/info.svg") center / contain no-repeat;
}
.hero-orbit {
  position: relative;
  min-height: 390px;
  border: 1px solid color-mix(in srgb, var(--brand-label) 18%, transparent);
  border-radius: 50%;
  background:
    radial-gradient(circle at 42% 36%, color-mix(in srgb, var(--brand-bright) 30%, transparent), transparent 30%),
    radial-gradient(circle, var(--tint-lift) 0 38%, var(--tint-edge) 39% 40%, transparent 41%);
  box-shadow: 0 0 0 52px color-mix(in srgb, var(--brand-bright) 2.5%, transparent);
}
.hero-orbit span,
.hero-orbit i,
.hero-orbit b {
  position: absolute;
  border-radius: 50%;
  background: var(--brand-bright);
  box-shadow: 0 0 0 8px color-mix(in srgb, var(--brand-bright) 13%, transparent);
}
.hero-orbit span {
  top: 20%;
  left: 24%;
  width: 10px;
  height: 10px;
}
.hero-orbit i {
  right: 18%;
  bottom: 30%;
  width: 8px;
  height: 8px;
}
.hero-orbit b {
  bottom: 13%;
  left: 38%;
  width: 6px;
  height: 6px;
}
.section--jobs {
  padding-bottom: var(--space-section-tight);
}
.section--jobs + .section--statement,
.page-hero + .section {
  padding-top: var(--space-section-tight);
}
.split--closed + .icon-grid {
  margin-top: 34px;
}
.section-link {
  margin-top: 22px;
  font-weight: 650;
}
/* The way out of a section, on the heading row rather than under the content.
   Where a section has a paragraph beside its title the link goes in that
   column with it, which is what API and About do; this is for the sections
   whose title has nothing beside it, where the right half of the row is empty
   and the link is what fills it.

   Two boxes rather than the eyebrow, the title and the link as three grid
   items: the link has to sit against the foot of the whole text block, and a
   grid item can only span rows that exist, so `1 / -1` addresses the explicit
   grid and collapses to one row. The wrapper makes the block one box, which
   aligns whether or not there is an eyebrow above the title.

   The whole text block is now what it says: the section's lead paragraph goes
   inside the wrapper rather than after the row. That is the same claim the
   markup was already making — the paragraph qualifies the section, it does not
   belong to the grid of cards under it — and it is what makes `last baseline`
   land where this comment always said it would. On the row, the block's last
   baseline was the title's, so the link sat up beside the title with the
   paragraph running on underneath it and the rule below that; inside, the last
   baseline is the paragraph's, and the link finishes the block just above the
   rule.

   Lifting the cap is for the row, not for the text in it. It used to be for
   both: the title carried its own 24ch and kept its measure wherever the block
   was put, so `max-width: none` here only bought the link its trip to the
   container edge. That cap came off .section-title — it bound before the block
   did and broke titles a word early — and this rule was left handing the title
   the full 1240px container, which is a 46-character line at 48px on Home. The
   row stays uncapped; the wrapper inside it takes the measure back. */
.section-heading--linked {
  display: flex;
  align-items: last baseline;
  justify-content: space-between;
  gap: 16px 40px;
  flex-wrap: wrap;
  max-width: none;
}
/* The measure, back on the only thing in the row that is text. The percentage
   resolves against the flex container, which is the section's container, so the
   text column here lands on the same edge as the block in an unlinked heading —
   the link is not part of the sum. */
.section-heading--linked > :not(.section-link) {
  max-width: var(--measure);
}
/* A control on the heading row, not a way out of the section. It ends the row
   rather than sitting on its last baseline, which is the difference between a
   button and a link: the link is a line of text and lines up as one, the button
   is a box and lines up by its edge. Bottom, not top, so it finishes level with
   the copy above the inputs and reads as near the fields it clears. This is
   where the coverage filters' reset went when it stopped being a fourth track
   in the input grid — it had no label of its own there, so it borrowed the
   inputs' baseline and read as one of them. */
.section-heading--linked > .button {
  align-self: end;
}
/* One measure per block, and it belongs to the block. Every child of a heading
   used to carry a second cap of its own — .body-copy at 68ch, and .section p at
   68ch behind it — so the lead paragraph stopped 100px short of the title above
   it and the block had no edge. The wrapper is the only thing that decides how
   wide the text runs; the text fills it. Caps outside a heading are untouched:
   prose that sits loose in a section still needs its own measure. */
.section-heading > *,
.section-heading--linked > :not(.section-link) > * {
  max-width: none;
}
/* Both margins, not only the top one. The link is a <p> and nothing resets a
   paragraph's margins here, so its 1em bottom margin was left hanging below the
   baseline it aligns to and became the flex line's depth — 17px of nothing
   between the block and whatever the block introduces. */
.section-heading--linked > .section-link {
  flex: 0 0 auto;
  margin: 0;
}
/* Half the space the block keeps below itself, so the lead reads as attached to
   the title and the section's own rhythm is what separates the pair from the
   content. It was the other way round: --space-heading above the paragraph and
   the browser's 1em below it, which put more air between a title and its own
   lead than between that lead and the cards it introduced. Both margins are
   named here because a flex item does not collapse its children's margins out,
   so a bottom one would be dead height under the last baseline the link aligns
   to. */
.section-heading--linked .body-copy {
  margin: calc(var(--space-heading) / 2) 0 0;
}
/* On the disclaimer the heading row is the whole section, so the margin that
   separates a heading from its content has no content to separate it from. */
.section-heading--linked:last-child {
  margin-bottom: 0;
}
/* The global reset strips colour and underline from every <a>, so a link
   dropped into a paragraph would be invisible. Running-copy containers opt
   back in, which means a link added later is discoverable without anyone
   remembering the class.

   The list is the whole mechanism, so a container missing from it is a link
   nobody can see. Six were: .callout, .coverage-note, .spec__meta, .steps,
   .check-control and the address block, which between them hid the consent
   link on both forms, the way out of an empty coverage filter and the licence
   note under the matrix. They are named below.

   This used to be two blocks with the same selector list, the second setting
   the offset the first had just set. One block, one value.

   Every entry carries :not(.button) for a reason worth keeping: the dark-band
   version of this rule was once written as `.split a`, which weighed (0,2,1)
   and outranked .section--inverse-secondary .button-outline at (0,2,0), so the
   API teaser's button took the link colour and lost its white label. The
   dark-band version is gone now, because --link is redefined on the inverses
   and one rule covers both, but the guard stays. */
.section-link a,
.inline-link,
.body-copy a:not(.button),
.hero-copy a:not(.button),
.editorial-body a:not(.button),
.comparison-note a,
.hero-qualifier a,
.callout a:not(.button),
.margin-note a:not(.button),
.coverage-empty a:not(.button),
.coverage-note a:not(.button),
.spec__meta a:not(.button),
.steps a:not(.button),
.check-control a:not(.button) {
  color: var(--link);
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 3px;
  transition: color 0.18s ease;
}
/* Hover. A link that gives no response to the pointer is only half a link, and
   69 of the 376 non-button links on the site gave none: the whole running-copy
   set above, every .section-link, both logos, and the drawer. It is one step
   along the green ramp, not a new colour, and it follows the ground because
   --link-hover is redefined on the inverses.

   The colour carries it on its own. The underline is already there in running
   copy and adding a second signal on hover moves the text, so the shift is the
   ink and nothing else. */
.section-link a:hover,
.inline-link:hover,
.body-copy a:not(.button):hover,
.hero-copy a:not(.button):hover,
.editorial-body a:not(.button):hover,
.comparison-note a:hover,
.hero-qualifier a:hover,
.callout a:not(.button):hover,
.margin-note a:not(.button):hover,
.coverage-empty a:not(.button):hover,
.coverage-note a:not(.button):hover,
.spec__meta a:not(.button):hover,
.steps a:not(.button):hover,
.check-control a:not(.button):hover {
  color: var(--link-hover);
}
/* The component links that carry their own colour. They had the colour and no
   response; this is the same step the running copy takes. */
.coverage-recap > p a:hover,
.login-card > a:hover,
.form-feedback a:hover,
.language-list a:hover,
.mobile-menu__links a:not(.button):hover {
  color: var(--link-hover);
}
/* "You are here" was the one nav state with no hover, because the rule above it
   moves the link to the colour this one already has. */
.nav-link[aria-current="page"]:hover {
  color: var(--link-hover);
}
/* The logo is artwork, not text, so it has no ink to shift. A short fade is the
   only honest signal that it is a link at all. */
.brand {
  transition: opacity 0.18s ease;
}
.brand:hover {
  opacity: 0.72;
}
.data-chain {
  /* The step count is a property, not a hard-coded track count: the Data page
     draws five stages and the Home page six, and the six-track version left an
     empty column on one of them. */
  --chain-steps: 6;
  display: grid;
  grid-template-columns: repeat(var(--chain-steps), minmax(0, 1fr));
  gap: 0;
  padding: 0;
  margin: 0;
  list-style: none;
  counter-reset: chain;
}
.data-chain--five {
  --chain-steps: 5;
}
.data-chain li {
  position: relative;
  padding: 40px 16px 16px;
  border-top: 1px solid var(--line);
  border-bottom: 1px solid var(--line);
  color: var(--text-body);
  font-size: 0.88rem;
  line-height: 1.4;
  counter-increment: chain;
}
.data-chain li::before {
  content: counter(chain, decimal-leading-zero);
  position: absolute;
  top: 15px;
}
/* The chain is a closed band of ruled cells, so prose set directly under it
   reads as a sixth row rather than as a reading of the five above. The space
   belongs to what follows rather than to the chain: on Data a .diagram follows
   and brings its own 32px, and a bottom margin here would land on top of it. */
.data-chain + .split,
.chain-layout + .split {
  margin-top: 48px;
}
/* Tertiary, like the arrow on .approach-line. The ordinal is the green in
   these rows; an arrow only says which way to read them. */
.data-chain li:not(:last-child)::after {
  content: "→";
  position: absolute;
  z-index: 2;
  top: 15px;
  right: -7px;
  color: var(--text-muted);
}
/* Match levels: four steps of increasing precision. The difference between the
   levels is carried by the number, the title and the filled meter segments,
   not by artwork. Shared by the home, platform, data and API pages. */
.match-levels {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 0;
  padding: 0;
  margin: 0;
  list-style: none;
  border-top: 1px solid var(--line);
}
.match-levels li {
  --level: var(--level-1);
  position: relative;
  display: flex;
  flex-direction: column;
  padding: 26px 28px 26px 0;
}
.match-levels li + li {
  padding-left: 28px;
  border-left: 1px solid var(--line);
}
.match-levels li:nth-child(2) {
  --level: var(--level-2);
}
.match-levels li:nth-child(3) {
  --level: var(--level-3);
}
.match-levels li:nth-child(4) {
  --level: var(--level-4);
}
.match-levels__index {
  margin: 0 0 14px;
}
.match-levels h3 {
  margin: 0 0 10px;
  font-size: 1.3rem;
  line-height: 1.2;
  letter-spacing: -0.02em;
}
/* The description, which is the one paragraph in the card without a name. As
   `.match-levels p` this outranked both .match-levels__index and
   .match-levels__scope — they are paragraphs too — and quietly took the
   ordinal's colour, size and margin with it. */
.match-levels p:not([class]) {
  margin: 0;
  color: var(--text-body);
  font-size: 0.96rem;
}
/* No rule above this: the space is the separation. 34px is what the 18px
   margin and the 16px padding either side of the old line added up to, so the
   scope sits exactly where it did — as padding now, because the margin above
   it is auto: the columns are equal height, so the auto margin pushes every
   scope to the foot of its card and the meters read as one row across the
   four. Cards whose description runs long keep the 34px and nothing moves. */
.match-levels__scope {
  display: flex;
  flex-direction: column;
  gap: 9px;
  margin-top: auto;
  padding-top: 34px;
  color: var(--text-heading);
  font-family: var(--font-data);
  font-size: 0.72rem;
  line-height: 1.5;
}
.match-levels__meter {
  display: flex;
  gap: 4px;
}
.match-levels__meter i {
  width: 20px;
  height: 5px;
  border-radius: 0;
  background: var(--line);
}
.match-levels li:nth-child(1) .match-levels__meter i:nth-child(-n + 1),
.match-levels li:nth-child(2) .match-levels__meter i:nth-child(-n + 2),
.match-levels li:nth-child(3) .match-levels__meter i:nth-child(-n + 3),
.match-levels li:nth-child(4) .match-levels__meter i:nth-child(-n + 4) {
  background: var(--level);
}
/* The worked example. The verdict used to sit as chips along the top of a
   white shell and a sentence underneath it; it is a column beside the two
   records now, which is where a reader looking for the answer goes first.
   Losing the shell also loses a box: three panels on the section ground say
   the same thing the shell-around-panels version said. */
.comparison-lead {
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 24px;
}
.comparison-lead .section-heading {
  margin-bottom: 0;
}
.comparison-status {
  display: flex;
  flex-direction: column;
  gap: 16px;
}
/* The two match lines read as one verdict, so they sit on a line together
   with a rule between them rather than stacked like two separate facts. */
/* Three verdicts rather than two once the worked example states the substance
   result as well, and this column is narrow, so they wrap. The divider is drawn
   half a gap to each chip's left and the row clips its overflow — the same
   mechanism as .hero-stat — so a chip that begins a wrapped line has its rule
   outside the box and loses it instead of opening the line with a stray mark. */
.comparison-match {
  display: flex;
  flex-wrap: wrap;
  overflow: hidden;
  gap: 10px 24px;
}
.comparison-match span {
  position: relative;
}
.comparison-match span::before {
  content: "";
  position: absolute;
  top: 0.1em;
  bottom: 0.1em;
  left: -12px;
  border-left: 1px solid var(--line);
}
.comparison-status span {
  display: flex;
  gap: 16px;
  color: var(--text-muted);
  font-family: var(--font-data);
  font-size: 0.72rem;
}
.comparison-status b {
  color: var(--text-heading);
}
.comparison-note {
  margin: 0;
  color: var(--text-body);
  font-size: 0.95rem;
}
/* A hairline between the records rather than a gap: they are one comparison,
   and the 24px beside them is what separates them from the verdict. */
.comparison-products {
  display: grid;
  position: relative;
  grid-template-columns: 1fr 1fr;
  gap: 2px;
}
.product-record {
  padding: 26px;
  border: 1px solid var(--line);
  border-radius: 0;
  background: var(--base);
}
.product-record small {
  color: var(--brand-label);
  font-family: var(--font-data);
  font-size: 0.68rem;
  text-transform: uppercase;
}
.product-record h3 {
  margin: 12px 0 18px;
  font-size: 1.5rem;
}
.product-record dl {
  margin: 0;
}
.product-record dl div {
  display: flex;
  justify-content: space-between;
  gap: 20px;
  padding: 9px 0;
  border-top: 1px solid var(--line);
  font-size: 0.9rem;
}
.product-record dt {
  color: var(--text-muted);
}
.product-record dd {
  margin: 0;
  font-weight: 650;
}
/* The mark sits on the seam rather than in a track of its own, so the two
   records stay the same width as each other whatever the mark is. */
.comparison-mark {
  display: grid;
  position: absolute;
  z-index: 2;
  top: 50%;
  left: 50%;
  width: 42px;
  height: 42px;
  place-items: center;
  transform: translate(-50%, -50%);
  border-radius: 50%;
  box-shadow: 0 0 0 4px var(--base);
  color: var(--text-on-dark);
  background: var(--inverse-secondary);
  font-family: var(--font-data);
}
/* Was .final-cta only, which left the same button on the API teaser with a
   transparent border and no colour — a button that read as a link. Both dark
   grounds get it now.

   The border runs the same two steps .button-secondary runs on light: the
   ground's own rule at rest, currentColor on hover. It used to sit at 50%
   white and never move, which made it the loudest edge on the band and gave
   it nothing to answer a pointer with — a heavier border than the light
   outline draws, holding still while the filled button beside it lights up.
   --line-dark is the counterpart of the --line the light one takes, tuned
   against it (1.55:1 to its 1.31:1), so the pair now reads the same weight on
   either ground. currentColor rather than --text-on-dark for the same reason
   it is currentColor on light: the border follows the label wherever the
   button lands. */
:is(.section--inverse-secondary, .section--inverse-primary) .button-outline {
  border-color: var(--line-dark);
  color: var(--text-on-dark);
  background: transparent;
}
:is(.section--inverse-secondary, .section--inverse-primary)
  .button-outline:is(:hover, .is-hover) {
  border-color: currentColor;
}
/* Four kinds of evidence, each one a step further from proof, drawn as a
   staircase of dark panels descending across a light band. The rows were white
   cards with a green edge, which put four boxes on a page that had already
   decided a box is for something a reader must not miss; inverted, the block
   reads as one object with four treads rather than four cards that happen to
   be indented, and the band it sits on carries it the way .dark-panel is
   already carried by a light section.

   The panel is --inverse-secondary, not --inverse-primary: secondary is the
   dark a light band holds, primary is the terminal ground the closing CTA
   ends the page on. */
.evidence-stack {
  display: grid;
  margin-bottom: 32px;
  gap: 8px;
}
.evidence-stack div {
  display: grid;
  grid-template-columns: 5px 260px 1fr;
  align-items: center;
  gap: 0 26px;
  padding: 18px 24px;
  color: var(--text-on-dark);
  background: var(--inverse-secondary);
}
/* The mark, inset rather than flush. As a border it was the panel's own edge,
   which on a dark row would read as a rule holding the block together; held
   off the edge it is a mark on the row, which is what it has always meant.
   Square, because all three radius tokens are zero and a rounded cap here
   would be the only curve on the page. */
.evidence-stack div::before {
  content: "";
  align-self: stretch;
  background: var(--evidence-1);
}
.evidence-stack div:nth-child(2) {
  margin-left: 35px;
}
.evidence-stack div:nth-child(3) {
  margin-left: 70px;
}
.evidence-stack div:nth-child(4) {
  margin-left: 105px;
}
.evidence-stack div:nth-child(2)::before {
  background: var(--evidence-2);
}
.evidence-stack div:nth-child(3)::before {
  background: var(--evidence-3);
}
.evidence-stack div:nth-child(4)::before {
  background: var(--evidence-4);
}
.evidence-stack span {
  color: var(--text-muted-on-dark);
}
.diagram--boundary img {
  aspect-ratio: 1;
}
.model-flow {
  display: grid;
  grid-template-columns: repeat(5, 1fr);
  gap: 0;
}
/* A rule between the stages, not around them. The row is a sequence, so it
   reads open at both ends — the same way .match-levels draws its dividers. */
.model-flow article {
  padding: 20px 20px 22px;
  border-top: 1px solid var(--line);
}
.model-flow article + article {
  border-left: 1px solid var(--line);
}
.model-flow h3 {
  margin: 12px 0 6px;
  font-size: 1.15rem;
}
.model-flow p {
  color: var(--text-body);
  font-size: 0.9rem;
}
.icon-card {
  display: grid;
  gap: 20px;
  align-items: start;
}
.icon-card h3 {
  margin: 0;
  font-size: 1.1rem;
  letter-spacing: -0.02em;
}
.icon-card p {
  margin: 6px 0 0;
  color: var(--text-body);
  font-size: 0.93rem;
  line-height: 1.5;
}
/* The panel's caption, and the same story: the light variant below was written
   against tokens and the dark one against raw values, #445049 and #99d991,
   which are --line-dark and --brand-on-dark under other names. */
.code-note {
  margin-top: 28px;
  padding: 14px 16px;
  border: 1px solid var(--line-dark);
  color: var(--brand-on-dark);
  font-family: var(--font-data);
  font-size: 0.72rem;
}
.api-code-light .code-note {
  border-color: var(--line);
  color: var(--brand-label);
  background: color-mix(in srgb, var(--raised) 70%, transparent);
}
.approach-line {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  max-width: 1000px;
  margin-bottom: 44px;
}
.approach-line span {
  position: relative;
  padding: 25px 15px;
  border-top: 2px solid var(--brand-core);
  color: var(--text-heading);
  font-family: var(--font-data);
  font-size: 0.78rem;
}
.approach-line span:not(:last-child)::after {
  content: "→";
  position: absolute;
  right: 0;
  color: var(--text-muted);
}
/* The one place the ring changes colour on a light ground. --brand-label is
   the value that survives every band on the site, --sunken included, where
   --brand-core measures 2.76:1 and is not usable; but these two cards are
   painted --raised, and on white --brand-core measures 3.09:1 and clears the
   bar. A form is where focus is followed field by field rather than glanced
   at once, so it is worth the brighter, more obviously live green there and
   nowhere the ground is not white. */
.request-form,
.login-card {
  --focus-ring: var(--brand-core);
  max-width: 900px;
  padding: 42px;
  border: 1px solid var(--line);
  border-radius: var(--radius-card);
  background: var(--raised);
  box-shadow: 0 15px 50px color-mix(in srgb, var(--shadow-ink) 6%, transparent);
}
/* Column gap only. The row rhythm is the field's own margin, so that a field
   inside this grid and one below it are spaced the same way — see .form-field.
   With a row-gap here as well the two were added together and the grid ran at
   40px while the fields under it ran at 20px, on the same form. */
.form-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  column-gap: 20px;
}
.form-field--wide {
  grid-column: 1 / -1;
}
.choice-set {
  max-height: 280px;
  overflow: auto;
  padding: 8px 12px 12px;
  border: 1px solid var(--line-control);
  background: var(--raised);
}
.choice-set__group {
  margin: 14px 0 8px;
  color: var(--text-muted);
  font-family: var(--font-data);
  font-size: 0.68rem;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}
.choice-set__group:first-child {
  margin-top: 6px;
}
.choice-set .check-control {
  max-width: none;
  margin: 0;
  padding: 6px 4px;
}
.region-picks {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 10px;
}
@media (min-width: 720px) {
  .region-picks {
    grid-template-columns: repeat(4, minmax(0, 1fr));
  }
}
.region-pick {
  position: relative;
  display: flex;
  align-items: center;
  gap: 10px;
  min-height: 64px;
  padding: 8px 12px 8px 8px;
  border: 1px solid var(--line);
  background: var(--raised);
  cursor: pointer;
  transition:
    border-color 0.18s ease,
    background-color 0.18s ease,
    box-shadow 0.18s ease;
}
.region-pick:hover {
  border-color: color-mix(in srgb, var(--brand-core) 45%, transparent);
}
.region-pick:has(:checked) {
  border-color: var(--brand-core);
  background: var(--tint-lift);
  box-shadow: inset 0 0 0 1px var(--brand-core);
}
.region-pick:has(:focus-visible) {
  outline: 2px solid var(--focus-ring);
  outline-offset: 2px;
}
.form-field .region-pick__input {
  position: absolute;
  width: 1px;
  height: 1px;
  min-height: 0;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  border: 0;
}
/* The trial rows take the same white ground on the same terms, as a square
   plate rather than a band: the row runs horizontally, so there is no card
   edge for a band to reach. */
.region-pick img {
  --globe: 48px;
  --plate-pad: 8px;
  box-sizing: border-box;
  flex: none;
  width: calc(var(--globe) + var(--plate-pad) * 2);
  height: calc(var(--globe) + var(--plate-pad) * 2);
  padding: var(--plate-pad);
  object-fit: contain;
  background: var(--render-ground);
  border: 1px solid var(--line);
}
.region-pick__name {
  /* Stated here rather than inherited. This used to come from .form-field
     label, which every .region-pick is, and that rule is a field label's now
     that field labels are regular. A card's name is a title and keeps 650. */
  font-weight: 650;
  /* min-width: 0 because flex items default to min-width: auto, and
     "Australasia" is one unbreakable 80px word: without this the name refuses
     to shrink and pushes the checkmark 23px outside the card at 390px, into
     the neighbouring one. */
  flex: 1;
  min-width: 0;
  color: var(--text-heading);
  font-size: 0.95rem;
  letter-spacing: -0.02em;
}
/* The same pair the pricing cards carry, on the same terms: plus-circle at rest
   and check-circle at fill when it is on, both whole Phosphor icons through a
   mask rather than a glyph centred in a CSS circle. It used to be a 1px ring
   with a typed ✓ in it, which is a hand-built approximation of two drawings
   that already exist and already agree with each other — the two states used
   different constructions, and the tick sat where the type metrics put it
   rather than where the disc's centre is.

   The mask is what lets one file carry two colours: --brand-label at rest and
   the brighter --brand-core when selected, and on the inverse bands the green
   the rest of the dark type takes.

   28px, the pricing card's size, and not the 22px that would have reproduced
   the old ring's 18px circle exactly. A Phosphor disc is 80% of its own square,
   so 22px draws a 17.6px circle, and on a display at one device pixel per CSS
   pixel that is where a hard-edged circle starts to show its steps: there is
   about one pixel of antialiasing to carry the whole curve, and the mark reads
   as chunky beside type the operating system has hinted. Nothing about the
   drawing or the mask is at fault — measured, a mask, a background-image and an
   <img> of this file are pixel-identical, and at two device pixels every size
   from 18px up is clean. The only thing that helps at one is more pixels on the
   curve. */
.region-pick__mark {
  flex: none;
  width: 28px;
  height: 28px;
  background: var(--brand-label);
  -webkit-mask: url("/assets/icons/plus-circle.svg") center / contain no-repeat;
  mask: url("/assets/icons/plus-circle.svg") center / contain no-repeat;
}
:is(.section--inverse-secondary, .section--inverse-primary) .region-pick__mark {
  background: var(--brand-on-dark);
}
.region-pick:has(:checked) .region-pick__mark {
  background: var(--brand-core);
  -webkit-mask-image: url("/assets/icons/check-circle.svg");
  mask-image: url("/assets/icons/check-circle.svg");
}
/* Coverage carried over from the pricing page. */
.coverage-recap {
  max-width: 900px;
  margin-bottom: 22px;
  padding: 26px 30px;
  border: 1px solid var(--line);
  border-left: 4px solid var(--brand-label);
  border-radius: var(--radius-card);
  background: var(--brand-soft);
}
.coverage-recap h3 {
  margin: 0 0 16px;
  font-size: 1.2rem;
}
.coverage-recap dl {
  margin: 0;
}
.coverage-recap dl div {
  display: flex;
  justify-content: space-between;
  gap: 20px;
  padding: 9px 0;
  border-top: 1px solid color-mix(in srgb, var(--brand-label) 16%, transparent);
  font-size: 0.94rem;
}
.coverage-recap dt {
  color: var(--text-body);
}
.coverage-recap dd {
  margin: 0;
  font-weight: 650;
  text-align: right;
}
.coverage-recap > p {
  margin: 16px 0 0;
  color: var(--text-body);
  font-size: 0.9rem;
}
.coverage-recap > p a {
  color: var(--brand-label);
  font-weight: 600;
  text-decoration: underline;
}
/* align-content, because a .form-field is itself a grid item inside
   .form-grid and stretches to the tallest field in its row. Auto rows in a
   stretched grid share the extra height out between them, so the field beside
   a two-line .field-help grew its own control to match: "Country of
   registration" measured 60px tall next to a 54px "Company number", on the
   same row, with the same rule behind both. The rows hold their own heights
   now and the extra space falls to the bottom of the field, where nothing is
   drawn. */
/* One number for the space under a field, wherever the field is. It used to be
   20px here and a 20px row-gap on .form-grid, which are added when a field is
   in the grid and not when it is not: measured on the trial form, every row of
   the grid sat 40px from the next, and the two textareas below the grid sat
   20px from it and from each other. Nothing declared the difference — it was
   the sum of two rules that each looked right on their own. The gap is off the
   grid now and the margin carries the whole distance, at the 40px the form was
   already reading at for most of its length. */
.form-field {
  display: grid;
  align-content: start;
  margin-bottom: 40px;
}
/* The 8px between a label and what it names is a margin rather than the grid's
   gap, and the two notes carry the gap's share in their own margins, because a
   gap is charged per child and the children are not all ours. A password
   manager puts its icon holder in the field when the control takes focus —
   measured, a node of zero height — and as a grid item that node bought a full
   8px gutter and pushed the help text down the moment anyone clicked in. The
   field looks identical and an injected node now costs exactly its own height,
   which is nothing.

   The child combinator on the label, because .region-picks holds seven more
   labels inside the field and this margin is not theirs. */
.form-field > label {
  margin-bottom: 8px;
}
.form-field label {
  color: var(--text-strong);
  font-size: 0.9rem;
  font-weight: 400;
}
/* One box for all three. The height used to be left to each control's own
   idea of a line, and the three do not agree: at the desktop body scale an
   input measured 54px and the select beside it 50px, so no two fields on a
   row were ever the same size. Stating the type size and the line makes the
   height a sum instead — 24px of line, 26px of padding, 2px of border, 52px —
   and every control reaches it the same way rather than through whatever the
   platform draws. min-height is the floor under that, for a control holding
   no text at all.

   1rem, not the inherited body size. What someone types is answering a label,
   not reading prose, and at the 18px body scale it sat a step above the label
   asking for it. It also settles a difference the form never meant to have:
   the body drops to 16px under 768px, so a control was 55px on a laptop and
   52px on a phone. One size, one height, everywhere. Not below 1rem either —
   iOS Safari zooms the page when a control it focuses is under 16px. */
.form-field input,
.form-field select,
.form-field textarea {
  width: 100%;
  min-height: 52px;
  padding: 13px 14px;
  border: 1px solid var(--line-control);
  border-radius: 0;
  color: var(--text-heading);
  background: var(--raised);
  font-size: 1rem;
  line-height: 1.5;
}
/* The select's mark is ours, not the platform's. A native one is painted hard
   against the right frame with no room around it, at a different size and
   angle on every browser, and it cannot be moved without appearance: none —
   which is also what makes a select the same height as an input, since the
   native box carries padding of its own underneath ours. The caret is the same
   Phosphor light set as every other icon on the site and is set in 16px from
   the edge, with the padding to keep the longest option clear of it. */
.form-field select {
  appearance: none;
  padding-right: 46px;
  background-image: url("/assets/icons/caret-down.svg");
  background-repeat: no-repeat;
  background-position: right 16px center;
  background-size: 16px 16px;
}
.form-field textarea {
  min-height: 140px;
  resize: vertical;
}
/* The ring sits *in* the field rather than off it. Everywhere else on the site
   it is held 2px clear, which is right for a link or a card, where the thing
   being marked has no frame of its own for the ring to be mistaken for. A
   control has one, so the ring lands on that frame and reads as it waking up
   rather than as a second box drawn around it.

   The offset is negative, not zero, and the difference is a third of the edge.
   At 0 the ring is drawn just outside the border and the grey 1px stays
   visible inside it, so a focused field wears 3px of frame. At -1px the ring
   covers the border instead of adding to it: 2px of green, no grey line under
   it, and the indicator is still the 2px a focus ring is measured on. Taking
   it to 1px was the other way to lose the weight and gives that measurement
   away for the same look.

   A frame going green is what a focused border-color would give, and the
   reason this is still an outline: border-color is already the invalid state's,
   and a focused invalid field has to be able to show both. It also goes on
   working for the eight other focusable things on the site that have no border
   to swap. */
.form-field :is(input, select, textarea):focus-visible {
  outline-offset: -1px;
}
.form-field input:disabled,
.form-field select:disabled,
.form-field textarea:disabled {
  border-color: var(--line);
  color: var(--text-muted);
  background: var(--sunken);
  cursor: not-allowed;
}
/* The caret's colour is baked into the file, so a disabled select cannot take
   the lighter border the rest of the field takes. Dropping the mark would read
   as broken and a filter would drag the text and the border down with it, so
   the ground is laid over the caret instead: a background layer sits above the
   image and below the text, so only the mark is washed out. 77% is not a taste
   value — it is what carries the caret's #9fa6a3 over --sunken to within a
   point of --line, which is the border the disabled field is wearing. */
.form-field select:disabled {
  background-image:
    linear-gradient(color-mix(in srgb, var(--sunken) 77%, transparent) 0 0),
    url("/assets/icons/caret-down.svg");
  background-repeat: no-repeat;
  background-position:
    0 0,
    right 16px center;
  background-size:
    auto,
    16px 16px;
}
/* :user-invalid, not :invalid: a required field must not turn red before
   someone has had a chance to fill it in. */
.form-field input:user-invalid,
.form-field select:user-invalid,
.form-field textarea:user-invalid,
.form-field [aria-invalid="true"] {
  border-color: var(--warn);
}
.field-note {
  margin-left: 7px;
  color: var(--text-muted);
  font-family: var(--font-data);
  font-size: 0.62rem;
  font-weight: 600;
  letter-spacing: 0.1em;
  text-transform: uppercase;
}
.form-note {
  margin: 0 0 24px;
  color: var(--text-muted);
  font-size: 0.9rem;
}
.field-help {
  margin: 7px 0 0;
  color: var(--text-muted);
  font-size: 0.85rem;
  line-height: 1.5;
}
/* 7px of its own plus the 8px the field's gap used to add. Scoped, because
   .field-help also captions a .check-list on Data and Coverage, where there is
   no gap to make up and 7px is the whole distance. */
.form-field > .field-help {
  margin-top: 15px;
}
.field-error {
  display: flex;
  gap: 9px;
  margin: 9px 0 0;
  color: var(--warn);
  font-size: 0.85rem;
  font-weight: 500;
  line-height: 1.5;
}
/* The same make-up as .field-help, one step further off the control because an
   error is a heavier line than a hint. */
.form-field > .field-error {
  margin-top: 17px;
}
/* A badge, not just a colour, so the message does not depend on hue. */
.field-error::before {
  content: "!";
  display: grid;
  flex: none;
  place-items: center;
  width: 17px;
  height: 17px;
  margin-top: 2px;
  border-radius: 50%;
  color: var(--text-on-fill);
  background: var(--warn);
  font-family: var(--font-data);
  font-size: 0.68rem;
  font-weight: 600;
}
.check-control {
  display: flex;
  max-width: 70ch;
  gap: 11px;
  align-items: flex-start;
  margin: 8px 0 25px;
  color: var(--text-body);
  font-size: 0.9rem;
}
/* A drawn box rather than the platform's. A native checkbox takes the operating
   system's accent — the blue in every screenshot of these forms — which is the
   one control on the site that was never ours, and it is drawn at a size the
   platform picks. appearance: none takes away the paint and nothing else: this
   is still a real checkbox, so it still tabs, still toggles on space, and still
   announces itself as a checkbox.

   The pair is square and check-square, not the circles the region cards use.
   A grid of circles reads as a multiple choice, which those are; one circle on
   its own reads as a radio, and the site has none. 24px against a 23.8px line
   box, so it sits on the first line of the sentence without a nudge.

   A background image, and the one mark on the site that is not a mask. Every
   other one is a mask so a single file can take its colour from the rule, and
   this was written that way first — but a mask clips the element's own
   rendering, the outline included, and this is a real checkbox that carries its
   own focus ring. Masked, tabbing to it drew nothing at all: the ring was
   painted and then clipped away with everything else. So the two states are two
   files with their colours baked, which is what the icon manifest's colour= is
   for. The region marks keep their masks, because their ring is drawn by the
   card around them and never touches the mark. */
.check-control input {
  appearance: none;
  flex: none;
  width: 24px;
  height: 24px;
  margin: 0;
  background: url("/assets/icons/square.svg") center / contain no-repeat;
  cursor: pointer;
}
.check-control input:checked {
  background-image: url("/assets/icons/check-square.svg");
}
/* The site's other drawn marks are decoration — the menu glyph, a card's add
   mark — and forced colours dropping them costs a picture. This one *is* the
   state: without it there is no way to see whether you have agreed to anything.
   So this control, and only this one, hands the drawing back to the platform
   when the platform is the one being asked to draw. */
@media (forced-colors: active) {
  .check-control input {
    appearance: auto;
    background-image: none;
  }
}
.form-status {
  margin: 18px 0 0;
  color: var(--text-muted);
  font-size: 0.82rem;
}
.trial-note {
  margin-bottom: var(--space-section);
}
.login-hero .login-card {
  max-width: 500px;
}
.login-card .button {
  width: 100%;
  margin-bottom: 16px;
}
.login-card > a {
  color: var(--brand-label);
  font-size: 0.9rem;
  text-decoration: underline;
}
.compact-form {
  max-width: 600px;
}
.legal-section {
  padding-block: var(--space-section-tight);
  border-top: 1px solid var(--line);
}
.legal-section .section-heading {
  margin-bottom: 28px;
}
.legal-section .section-title {
  font-size: clamp(2rem, 3vw, 2.65rem);
}
.compact-list {
  display: grid;
  gap: 0;
  padding: 0;
  margin: 0;
  list-style: none;
}
.compact-list li {
  position: relative;
  padding: 11px 16px 11px 30px;
  color: var(--text-body);
}
.compact-list li::before {
  content: "";
  position: absolute;
  /* The same centring as .check-list, plus this list's own top padding, which
     is what the 1.45em was mostly spelling out. Same defect underneath it: the
     mark sat about 4px below the middle of the line it marks. */
  top: calc(11px + 0.845em - 4px);
  top: calc(11px + (1lh - 8px) / 2 + 0.02em);
  left: 4px;
  width: 8px;
  height: 8px;
  background: var(--brand-core);
}
.api-teaser {
  border-bottom: 1px solid var(--line-dark);
}
.redirect-page {
  min-height: 100svh;
  display: grid;
  place-items: center;
  padding: 32px;
  background: var(--sunken);
}
.redirect-card {
  width: min(100%, 620px);
  padding: 48px;
  border: 1px solid var(--line);
  border-radius: var(--radius-large);
  background: var(--raised);
  box-shadow: var(--shadow);
}
.redirect-card .brand {
  margin-bottom: 64px;
}
.redirect-card h1 {
  margin: 0;
  font-size: clamp(2.5rem, 7vw, 4rem);
  line-height: 1;
  letter-spacing: -0.04em;
}
.redirect-card p:not(.eyebrow) {
  margin-block: 24px 30px;
  color: var(--text-body);
}

/* Pricing — regional coverage */
/* .sr-only is one class, so .form-field input (0,2,1) outranks it and the
   hidden mirror input on the trial form computed to 1440x50px absolute. It was
   invisible and unclickable thanks to clip-path, but it still made the page
   1582px wide against a 1440px viewport, which only overflow-x: clip was
   hiding. Matching the container's specificity keeps the utility working
   wherever it is used inside a field. */
.sr-only,
.form-field .sr-only,
.form-field input.sr-only,
.form-field select.sr-only,
.form-field textarea.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  min-height: 0;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  border: 0;
  clip-path: inset(50%);
  white-space: nowrap;
}
.price-flag {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 8px 18px;
  margin: 30px 0 0;
}
/* No fill and no padding: this sits directly above two real buttons, and the
   one thing here that cannot be clicked must not look like the main one. */
.price-flag__amount {
  color: var(--text-heading);
  font-size: 1.5rem;
  font-weight: 600;
  letter-spacing: -0.025em;
}
.coverage-builder {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 340px;
  gap: 30px;
  align-items: start;
}
.region-select {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 200px), 1fr));
  gap: 18px;
  align-items: start;
  margin: 0;
  padding: 0;
  border: 0;
}
.region-card {
  position: relative;
  display: flex;
  flex-direction: column;
  border: 1px solid var(--line);
  border-radius: var(--radius-card);
  background: var(--base);
  transition:
    transform 0.18s ease,
    border-color 0.18s ease,
    box-shadow 0.18s ease;
}
.region-card:hover {
  transform: translateY(-2px);
  box-shadow: 0 16px 34px color-mix(in srgb, var(--shadow-ink) 9%, transparent);
}
/* The selected edge is two pixels of ring, a border plus an inset shadow. An
   inset shadow paints under in-flow children, so the band's own background
   would cover one of them and the ring would read 1px beside the image and
   2px beside the body. Drawing it in an overlay puts it above every child and
   keeps one weight the whole way round, edge to edge intact. */
.region-card:has(:checked) {
  border-color: var(--brand-core);
  background: var(--tint-lift);
}

.region-card:has(:checked)::after {
  content: "";
  position: absolute;
  inset: 0;
  box-shadow: inset 0 0 0 1px var(--brand-core);
  pointer-events: none;
  z-index: 2;
}

/* The band's divider follows the selected state on the terms the countries
   divider already set, so the ring is not interrupted by a grey line. */
.region-card:has(:checked) .region-card__media {
  border-bottom-color: color-mix(in srgb, var(--brand-core) 35%, transparent);
}
.region-card:has(:focus-visible) {
  outline: 2px solid var(--focus-ring);
  outline-offset: 2px;
}
.region-card__control {
  display: block;
  padding: 0;
  cursor: pointer;
}
.region-card__input {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
}
/* The renders bake a white ground and carry no alpha, so rather than fight it
   the image gets a band of its own: edge to edge, white, closed with a bottom
   border on the card's own line. White here belongs to the asset rather than
   to the surface scale, which is why it is a literal and not var(--raised).
   The band ends where the body's padding begins, so the card reads as an
   image over text rather than a picture floating in a padded box. */
.region-card__media {
  display: block;
  background: var(--render-ground);
  border-bottom: 1px solid var(--line);
}

.region-card__media img {
  --band: 156px;
  display: block;
  width: 100%;
  height: var(--band);
  margin: 0;
  object-fit: contain;
}

.region-card__body {
  display: block;
  padding: 20px 20px 6px;
}
.region-card__name {
  display: block;
  font-size: 1.15rem;
  letter-spacing: -0.02em;
}
.region-card__price {
  display: block;
  margin-top: 5px;
  font-weight: 600;
}
.region-card__price small {
  color: var(--text-muted);
  font-size: 0.8rem;
  font-weight: 400;
}
.region-card__meta {
  display: block;
  margin-top: 8px;
  color: var(--text-muted);
  font-size: 0.78rem;
  line-height: 1.4;
}
/* A text button, so it is set like one: the label carries .button's size and
   weight in the sans, not the mono label treatment the eyebrows and the price
   use. Those name things; this one is the thing you click. Accent green rather
   than a fill, because it sits inside the card it acts on. */
.region-card__action {
  display: inline-flex;
  align-items: center;
  /* 7px against a 28px box, not the 10px it would take against the ring: the
     drawn circle is 80% of the icon's own square, so the file already carries
     about 3px of air on the side facing the label. */
  gap: 7px;
  margin-top: 15px;
  color: var(--brand-label);
  font-size: 0.9375rem;
  font-weight: 600;
}
/* One mark, ring included: both states are a whole Phosphor icon rather than a
   glyph centred in a CSS circle. Which is also what makes the pair read as one
   control — plus-circle and check-circle are drawn to the same construction,
   which a hand-built ring can only approximate.

   Through a mask, because the generated files carry one baked colour and the
   selected state needs the brighter --brand-core. The check needs no second
   colour: at fill weight it is knocked out of the disc, so what shows through
   is the card behind it. */
.region-card__action::before {
  content: "";
  flex: none;
  width: 28px;
  height: 28px;
  background: currentColor;
  -webkit-mask: url("/assets/icons/plus-circle.svg") center / contain no-repeat;
  mask: url("/assets/icons/plus-circle.svg") center / contain no-repeat;
}
.region-card:has(:checked) .region-card__action::before {
  background: var(--brand-core);
  -webkit-mask-image: url("/assets/icons/check-circle.svg");
  mask-image: url("/assets/icons/check-circle.svg");
}
.region-card:has(:checked) .region-card__action-add,
.region-card__action-on {
  display: none;
}
.region-card:has(:checked) .region-card__action-on {
  display: inline;
}
.region-card__countries {
  margin-top: auto;
  border-top: 1px solid var(--line);
}
.region-card:has(:checked) .region-card__countries {
  border-color: color-mix(in srgb, var(--brand-core) 35%, transparent);
}
.region-card__countries summary {
  padding: 13px 20px;
  color: var(--text-muted);
  font-size: 0.82rem;
  cursor: pointer;
  list-style: none;
}
.region-card__countries summary::-webkit-details-marker {
  display: none;
}
/* Inline after the label rather than a flex item, so this one is baseline set:
   -2px drops an 11px box from sitting on the baseline to centring on the
   x-height, where the typed "+" it replaces already sat. currentColor, because
   the whole row turns green on hover and open and the mark goes with it. */
.region-card__countries summary::after {
  content: "";
  display: inline-block;
  width: 11px;
  height: 11px;
  margin-left: 6px;
  vertical-align: -2px;
  background: currentColor;
  -webkit-mask: url("/assets/icons/plus.svg") center / contain no-repeat;
  mask: url("/assets/icons/plus.svg") center / contain no-repeat;
}
.region-card__countries[open] summary::after {
  -webkit-mask-image: url("/assets/icons/minus.svg");
  mask-image: url("/assets/icons/minus.svg");
}
.region-card__countries summary:hover,
.region-card__countries[open] summary {
  color: var(--brand-label);
}
/* Country lists expand in full: no inner scroll area, so nothing is clipped
   and no keyboard user gets trapped in a scroll container. One column at every
   width, because a region card is only about 200px wide on desktop and a second
   column leaves too little room for names like Bosnia and Herzegovina. */
.region-card__countries ul {
  margin: 0;
  padding: 0 20px 16px;
  list-style: none;
}
.region-card__countries li {
  color: var(--text-body);
  font-size: 0.82rem;
  line-height: 1.55;
}
.coverage-cart {
  position: sticky;
  top: calc(var(--header-height) + 20px);
  max-height: calc(100svh - var(--header-height) - 40px);
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: 28px;
  border-radius: var(--radius-large);
  color: var(--text-on-dark);
  background: var(--inverse-primary);
}
.coverage-cart .eyebrow {
  margin-bottom: 14px;
  color: var(--brand-on-dark);
}
.coverage-cart__count {
  margin: 0;
  font-size: 1.6rem;
  font-weight: 550;
  line-height: 1.1;
  letter-spacing: -0.03em;
}
.coverage-cart__regions {
  min-height: 76px;
  margin: 14px 0 24px;
  padding: 0;
  color: var(--text-muted-on-dark);
  font-size: 0.9rem;
  line-height: 1.7;
  list-style: none;
}
.coverage-cart__rows {
  margin: 0;
}
.coverage-cart__rows div {
  display: flex;
  justify-content: space-between;
  gap: 16px;
  padding: 12px 0;
  border-top: 1px solid var(--line-dark);
  font-size: 0.92rem;
}
.coverage-cart__rows dt,
.coverage-cart__rows dd {
  margin: 0;
}
.coverage-cart__total {
  margin: 0 0 22px;
  padding: 18px 0 0;
  border-top: 1px solid var(--line-dark);
  font-size: 1.55rem;
  font-weight: 600;
  letter-spacing: -0.03em;
}
/* Commitment pricing leaves nothing to report as a saving, but a total still
   needs saying what it covers — and in the four-plus state, why there is no
   figure above it at all. That is a sentence rather than a mono badge. */
.coverage-cart__note {
  margin: -8px 0 20px;
  color: var(--text-muted-on-dark);
  font-size: 0.86rem;
  line-height: 1.5;
}
.coverage-cart__cta {
  width: 100%;
  color: var(--inverse-primary);
  background: var(--brand-core);
}
/* An empty subscription must not be submittable. The label changes as well,
   so the state is never communicated by dimming alone. */
.coverage-cart__cta[aria-disabled="true"] {
  border-color: color-mix(in srgb, var(--text-on-dark) 28%, transparent);
  color: color-mix(in srgb, var(--text-on-dark) 72%, transparent);
  background: transparent;
  cursor: not-allowed;
}
.coverage-cart__cta[aria-disabled="true"]:hover {
  transform: none;
}
.coverage-cart__vat {
  margin: 14px 0 0;
  color: var(--text-muted-on-dark);
  font-family: var(--font-data);
  font-size: 0.68rem;
  letter-spacing: 0.1em;
  text-align: center;
  text-transform: uppercase;
}
.coverage-note {
  margin: 30px 0 0;
  color: var(--text-muted);
  font-size: 0.9rem;
}
/* One table for the site, framed. There were two: .data-table, an unframed set
   of hairlines that API, Pricing and the coverage matrix used, and
   .normalization-table, three divs deep in role="table" that drew a border, a
   header band and a green result column round the same idea. The second was the
   better-dressed of the two and the worse-built — a table that has to be told
   it is a table is one an assistive technology can lose — so the frame moves on
   to the real element and the divs are gone.

   The frame is what makes a table an object on the page rather than a set of
   ruled lines in the prose. Both grounds are already named: --raised is what a
   card, an input or a code panel paints to lift off whichever band it is on,
   and --sunken is the band the header borrows to separate the labels from the
   data under them. */
.data-table {
  width: 100%;
  border-collapse: collapse;
  border: 1px solid var(--line);
  background: var(--raised);
  text-align: left;
}
.data-table th,
.data-table td {
  padding: 15px 16px;
  border-bottom: 1px solid var(--line);
  font-size: 0.95rem;
  font-weight: 400;
}
/* The last row closes on the frame and does not draw a second line inside it. */
.data-table tbody tr:last-child > * {
  border-bottom: 0;
}
/* Column names are labels for the data, not part of it — the same job the
   eyebrow does for a section, in the same mono. Tertiary, not --brand-label:
   green on the furniture left nothing for the one column that is an answer. */
.data-table thead th {
  color: var(--text-muted);
  background: var(--sunken);
  font-family: var(--font-data);
  font-size: 0.7rem;
  font-weight: 600;
  letter-spacing: 0.1em;
  text-transform: uppercase;
}
/* The row's own name. Sans and firm rather than mono: mono is for identifiers
   and labels, and a row header is the subject of its row — it is read, not
   scanned. This is the one place the merge changes an existing table, and it
   changes it towards the treatment the normalisation rows already had. */
/* No width. The unframed table set the row-header column to 9rem so the value
   column would dominate; framed and padded, 9rem is 108px of text and the API
   table's longest row header — "Permitted use and redistribution terms" — came
   apart over four lines beside a single line of value. The column takes what
   its longest name needs and the value column takes the rest, which is the
   arrangement every one of these tables wants. Where a table needs a shape of
   its own it says so on its modifier, as --result does below. */
.data-table tbody th {
  color: var(--text-heading);
  font-weight: var(--weight-title);
}
.data-table tbody td:last-child {
  color: var(--text-body);
}
/* A table whose last column is the answer it exists to produce, beside the
   source it was produced from. Green and firm, so the outcome can be read down
   the column without reading the working beside it. A modifier, because the
   other three tables end on prose and an accent there would be decoration. */
.data-table--result tbody td:last-child {
  color: var(--brand-label);
  font-weight: 600;
}
/* The two value columns sit together, so a row can be read across — this was
   the source, this is what it became — instead of the eye crossing an empty
   half-table between them. The row header takes the slack, which is why the
   width lives with the modifier: it is the shape this kind of table has, not a
   number for one page. */
.data-table--result thead th:first-child,
.data-table--result tbody th {
  width: 45%;
}
/* The action column of a dark panel: the button and the one line that
   qualifies it. The line takes its colour from the ground rather than setting
   --text-muted, which is a value for light grounds and was reading as unlit
   type on this one. */
.panel-action {
  display: grid;
  gap: 12px;
  justify-items: start;
}
.panel-action p {
  margin: 0;
  font-size: 0.88rem;
}
.global-card {
  padding: 30px;
  border: 1px solid var(--line);
  border-radius: var(--radius-large);
  background: var(--brand-soft);
}
.global-card h3 {
  margin: 0;
  font-size: 1.7rem;
  letter-spacing: -0.03em;
}
.global-card p {
  margin: 14px 0 26px;
  color: var(--text-body);
  font-size: 1rem;
}
/* Held at (0,2,0) on purpose. The shared dark-ground prose rule is
   :is(.section--inverse-secondary, .section--inverse-primary) p, which weighs (0,1,1) and would
   otherwise take this paragraph to #c9cfcb the moment the CTA became a
   .section--inverse-primary. The two values are within a few points of each other and
   could reasonably be merged one day; that is a decision, not a side effect. */
.final-cta .final-cta__copy {
  /* Was 48ch. One measure per block, and the block above now has one: a second
     cap in here only made the paragraph stop short of the heading it sits under.
     See .section-heading > * for the same call on every other heading. */
  max-width: none;
  margin: 20px 0 0;
  color: color-mix(in srgb, var(--text-on-dark) 74%, transparent);
  font-size: 1rem;
}

/* ==========================================================================
   Commercial workflow pages — Solutions, Data coverage, Guided evaluation
   Every rule below extends a component that already exists. Each one is here
   because the existing component could not carry the new content, not because
   a page wanted a look of its own.
   ========================================================================== */

/* A feature card that ends in a link. .feature-card p sets the margin of every
   paragraph in the card and is a heavier selector than .section-link, so a link
   dropped in landed 10px under the copy rather than at the foot of the cell.
   Making the card a column is also what puts the links of a row on one line
   when the copy above them runs to different lengths. */
.feature-card--linked {
  display: flex;
  flex-direction: column;
}
.feature-card--linked > .section-link {
  margin: auto 0 0;
  padding-top: 20px;
}
.feature-card--unavailable {
  display: flex;
  flex-direction: column;
}

/* Coverage explorer. The filters are ordinary .form-field controls, so this
   only lays them out, on the same auto-fit track list the region cards use. */
.coverage-filters {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 230px), 1fr));
  gap: 0 24px;
  margin: 0 0 28px;
  padding: 0;
  border: 0;
}
.coverage-filters .form-field {
  margin-bottom: 18px;
}
/* The coverage matrix is the one table on the site that cannot drop a column:
   a country row is only useful beside its region and its source. It scrolls
   inside its own box rather than shrinking the type below the body scale, and
   the box is a labelled, focusable region so the scroll is reachable from the
   keyboard as well as by touch. */
.table-scroll {
  overflow-x: auto;
  overscroll-behavior-x: contain;
  /* The frame belongs to the box you look through, not to the content sliding
     behind it. A table wider than its container carries its own border off to
     the right, where nobody sees it until they scroll to the end — so the
     matrix read as an open edge on every screen narrower than 1680px, which is
     every screen. The border moves out here and the table drops its own. */
  border: 1px solid var(--line);
}
.table-scroll > .data-table {
  border: 0;
}
.table-scroll:focus-visible {
  outline: 2px solid var(--focus-ring);
  outline-offset: 3px;
}
.table-scroll .data-table {
  min-width: 620px;
}
.table-scroll .coverage-matrix {
  min-width: 1680px;
}
/* These are sticky, so the background is load bearing: without it the rows
   scroll through the header. Each takes the ground its own band of the table
   has now that the table is framed — the header keeps --sunken, and the country
   column takes the --raised the rows around it are painted on. It used to be
   --sunken for both, which was right when the table had no ground of its own
   and is a stripe down the first column now that it has one. */
.coverage-matrix thead th {
  background: var(--sunken);
}
.coverage-matrix tbody th {
  background: var(--raised);
}
.coverage-matrix thead th:first-child,
.coverage-matrix tbody th {
  position: sticky;
  left: 0;
  z-index: 1;
  padding-right: 18px;
  box-shadow: 1px 0 0 var(--line);
}
.coverage-matrix thead th:first-child {
  z-index: 2;
}
.coverage-matrix .coverage-flag {
  color: inherit;
  font-family: var(--font-data);
  font-size: 0.7rem;
  font-weight: 600;
  letter-spacing: 0.04em;
  white-space: nowrap;
}
.coverage-matrix .coverage-flag--available {
  color: var(--brand-label);
}
.coverage-matrix .coverage-flag--partial {
  color: var(--text-heading);
}
.coverage-matrix .coverage-flag--unavailable,
.coverage-matrix .coverage-flag--planned {
  color: var(--text-muted);
  font-weight: 400;
}
/* All three states are written for a light ground, which is where the matrix
   sits today. On an inverse band --text-heading #171a19 disappears into
   #181b1a entirely, and --text-muted is a light-surface value by definition.
   The flags carry their own words, so the state survives either way, but the
   text has to stay readable if the table ever moves. */
:is(.section--inverse-secondary, .section--inverse-primary)
  .coverage-matrix .coverage-flag--available {
  color: var(--brand-on-dark);
}
:is(.section--inverse-secondary, .section--inverse-primary)
  .coverage-matrix .coverage-flag--partial {
  color: var(--text-on-dark);
}
:is(.section--inverse-secondary, .section--inverse-primary)
  .coverage-matrix :is(.coverage-flag--unavailable, .coverage-flag--planned) {
  color: var(--text-muted-on-dark);
}
/* A row's state is metadata about the row, not part of its content, so it takes
   the same mono the table heads use. Deliberately uncoloured: the only state
   this page can currently evidence is "on request", and a green badge would
   read as availability the project has no maintained source for. */
.coverage-state {
  color: var(--text-muted);
  font-family: var(--font-data);
  font-size: 0.74rem;
}
.coverage-result {
  margin: 22px 0 0;
  color: var(--text-muted);
  font-size: 0.9rem;
}
/* The filter reporting on itself, which is not the section speaking. It wore
   .callout — the green edge and tinted ground the site keeps for a limit on
   what the data proves — so a filter that matched nothing looked like a
   caveat about medicines. The neutral pair is what the palette keeps for
   anything procedural, and it says interface rather than argument. No edge:
   the fill is enough, and a left border here would only rebuild the callout. */
.coverage-empty {
  margin: 22px 0 0;
  padding: 20px 24px;
  color: var(--slate);
  background: var(--slate-soft);
  font-size: 0.95rem;
}
:is(.section--inverse-secondary, .section--inverse-primary) .coverage-result {
  color: var(--text-muted-on-dark);
}

/* Form result. .form-status is the quiet line under a submit button; a failed
   or completed submission is neither quiet nor a line, so it borrows the
   callout treatment the rest of the site uses for anything a reader must not
   miss. Rendered above the fields, where focus is sent after a submit. */
.form-feedback {
  margin: 0 0 26px;
  padding: 20px 24px;
  border-left: 4px solid var(--brand-label);
  color: var(--text-strong);
  background: var(--brand-soft);
  font-size: 0.98rem;
}
.form-feedback--error {
  border-left-color: var(--warn);
  color: var(--warn-ink);
  background: var(--warn-soft);
}
.form-feedback h3 {
  margin: 0 0 8px;
  font-size: 1.12rem;
}
.form-feedback p {
  margin: 0;
}
.form-feedback ul {
  margin: 10px 0 0;
  padding-left: 20px;
}
.form-feedback a {
  color: inherit;
  font-weight: 600;
  text-decoration: underline;
}
/* A submit button mid-request keeps its size and its label so the row does not
   jump; only the pointer and the ARIA state change. Scoped to the form, because
   the pricing CTA uses the same attribute to mean "nothing selected yet". */
.request-form .button[aria-disabled="true"] {
  cursor: progress;
  opacity: 0.72;
}
.request-form .button[aria-disabled="true"]:hover {
  transform: none;
}

@media (max-width: 1180px) {

  /* Three splits hold something that stops working well before the phone
     breakpoint: the discount table on Pricing, the trial band under it, and
     the two records in the worked example — four label/value rows each, which
     start wrapping once a record drops under about 260px. */
  .split--early {
    --split-ratio: 1fr;
    --split-gap: 40px;
  }
  .icon-grid--four {
    --icon-grid-columns: 2;
  }
  .coverage-builder {
    grid-template-columns: 1fr;
  }
  .coverage-cart {
    position: static;
  }
}

@media (max-width: 1120px) {
  .data-chain {
    --chain-steps: 3;
  }
  /* The animated process chain stands up into a column at this width, and it
     stands beside its own labels rather than above them: five nodes down the
     left, the five stages they are down the right, node 01 level with 01. Side
     by side there was a row of pictures and a row of names under it and the
     reader had to count across to pair them; here the pulse lights a label a
     centimetre from the node it just reached.

     Both columns are five equal rows of one box, so the pairing is the layout
     rather than a coincidence of two heights: .process-nodes and .data-chain
     both take repeat(5, 1fr), and the rule between two labels lands on the same
     line as the bead between two nodes.

     Here rather than in chain-animation.css because that sheet is a fetch, and
     a block that changed shape when it landed would move the section under the
     reader. .is-machine-pending is set by chain-animation-boot.js before the
     first paint and only when the machine is actually coming, so reduced motion
     and data-saver keep the strip above the stacked list, which is the page
     this file has always described. */
  .chain-layout.is-machine-pending {
    display: grid;
    grid-template-columns: var(--machine-column) minmax(0, 1fr);
    gap: 18px;
    max-width: 520px;
    margin-top: 36px;
    margin-inline: auto;
    --machine-column: 168px;
  }
  /* The strip is the wrong picture for this shape — 1923/417 in a 148px column
     is a 32px thread — and it is about to be replaced anyway. */
  .chain-layout.is-machine-pending .diagram--connections {
    display: none;
  }
  .chain-layout.is-machine-pending .chain-visual {
    min-height: 520px;
  }
  .chain-layout.is-machine-pending .data-chain {
    grid-template-columns: minmax(0, 1fr);
    grid-template-rows: repeat(5, 1fr);
  }
  /* The ordinal was placed 15px down from the top of a cell it shared with
     nothing. In a row half as wide and paired with a picture it sits in the
     flow, directly over the words, and the pair is centred on the node beside
     it rather than hung from the rule above it. */
  .chain-layout.is-machine-pending .data-chain li {
    display: flex;
    flex-direction: column;
    justify-content: center;
    gap: 4px;
    padding: 10px 0;
    border-bottom: 0;
  }
  .chain-layout.is-machine-pending .data-chain li:first-child {
    border-top: 0;
  }
  .chain-layout.is-machine-pending .data-chain li::before {
    position: static;
  }
  /* The arrow said which way to read a row of cells. There is a line down the
     next column doing that now, with a moving light on it. */
  .chain-layout.is-machine-pending .data-chain li::after {
    content: none;
  }
  .match-levels {
    grid-template-columns: repeat(2, 1fr);
  }
  .match-levels li:nth-child(3) {
    border-left: 0;
    padding-left: 0;
  }
  .model-flow {
    grid-template-columns: repeat(3, 1fr);
  }
  .model-flow article:nth-child(3n + 1) {
    border-left: 0;
  }
}

/* Below 1025px there is no longer room for the artwork beside a readable
   measure, so the hero stacks and the art gets the full column width. */
@media (max-width: 1024px) {
  .page-hero {
    min-height: auto;
  }
  /* The :not() is not decoration. The desktop rule for a hero with no artwork
     is .page-hero:not(.hero--art) .hero-grid, which is a class heavier than
     this one, so at (0,2,0) this never reached the pages that needed it most:
     the orbit column held its 360px minimum all the way down and the copy
     column resolved to nothing. Contact, Trial, About, the disclaimer and Log
     in were all stacking one word per line below about 800px. */
  .page-hero .hero-grid,
  .page-hero:not(.hero--art) .hero-grid {
    grid-template-columns: 1fr;
  }
  /* Stacked, so there is no render beside the copy to keep clear of. */
  .hero--still .hero__copy {
    padding-right: 0;
  }
  .page-hero .hero-orbit {
    min-height: 280px;
  }
}

@media (max-width: 768px) {
  body {
    font-size: 16px;
  }
  /* No heading font-size here on purpose. The desktop clamps bottom out on
     their floors at about 1024px and hold them the rest of the way down, so a
     second clamp at this breakpoint only has to disagree to be visible. The
     pair that used to sit here did: they opened on ceilings of 3.75rem and
     2.5rem, above the 3rem and 2.25rem the desktop curves had already reached,
     so crossing 768 made the h1 jump from 48px to 60px and the section title
     from 36px to 40px. Type that grows as the screen shrinks. Re-adding a rule
     here means matching the floor above it, not picking a size. */
  .data-chain {
    --chain-steps: 1;
  }
  .match-levels,
  .three-stage,
  .model-flow,
  .form-grid,
  .approach-line {
    grid-template-columns: 1fr;
  }
  /* Side by side the top rule runs across all five cells at once: one line over
     a row, open at both ends, the way .match-levels and .step-rows draw. In one
     column the same declaration draws a rule above every cell, including the
     first, which closes the sequence at the head while it stays open at the
     foot. A rule between the stages, then, at any width — and the cell keeps
     only its vertical padding, so the ordinals start on the container edge
     where .step-rows and .disclosure already put theirs. */
  .model-flow article {
    padding-inline: 0;
  }
  .model-flow article:first-child {
    border-top: 0;
  }
  .model-flow article + article {
    border-left: 0;
  }
  /* The same rule for the same reason, and one more: the chain cell carries a
     border on both edges, which side by side is the closed band the row is
     meant to read as. Stacked, the cells meet, so every rule between two rows
     was being drawn twice — a 2px ladder with a 1px rule at each end of it.
     One border, between the rows only, and the ordinals on the container edge. */
  .data-chain li {
    padding-inline: 0;
    border-bottom: 0;
  }
  .data-chain li:first-child {
    border-top: 0;
  }
  .data-chain li:not(:last-child)::after,
  .approach-line span:not(:last-child)::after {
    content: "↓";
    right: 16px;
  }
  .match-levels li,
  .match-levels li + li {
    padding: 22px 0;
    border-left: 0;
  }
  /* Stacked, the spine between the levels goes and nothing replaces it — the
     ordinal and the meter say where each level starts. That leaves the block's
     top rule as the one line on it, drawn above the first level and matched by
     nothing at the foot. It went with the spine. */
  .match-levels {
    border-top: 0;
  }
  /* One column: nothing to align the meters with, so the auto margin has no
     room to work and the gap is the padding alone. */
  .match-levels__scope {
    padding-top: 26px;
  }
  .diagram--boundary {
    max-width: 320px;
    margin-inline: auto;
  }
  .comparison-products {
    grid-template-columns: 1fr;
    gap: 12px;
  }
  .comparison-mark {
    position: static;
    margin-inline: auto;
    transform: rotate(90deg);
    box-shadow: none;
  }
  .product-record {
    padding: 24px;
  }
  /* Stacked, the label sits above its description and the staircase flattens —
     one column of rows, since an indent that has run out of width to indent
     into is just a narrower row. The mark keeps its own column and spans both
     lines, so the row still carries its step of the ramp. */
  .evidence-stack div,
  .evidence-stack div:nth-child(n) {
    grid-template-columns: 5px 1fr;
    gap: 5px 18px;
    margin-left: 0;
  }
  .evidence-stack div::before {
    grid-row: span 2;
  }
  .request-form,
  .login-card {
    padding: 28px 22px;
  }
  .redirect-card {
    padding: 36px 24px;
  }
  .price-flag__amount {
    font-size: 1.25rem;
  }
  .region-select {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 12px;
  }
  .region-card__control {
    padding: 0;
  }
  .region-card__media img {
    --band: 124px;
  }

  .region-card__body {
    padding: 16px 14px 4px;
  }
  .region-card__name {
    font-size: 1rem;
  }
  .region-card__price {
    font-size: 0.95rem;
  }
  .region-card__action {
    margin-top: 12px;
    font-size: 0.875rem;
  }
  .region-card__meta {
    font-size: 0.72rem;
  }
  .region-card__countries summary {
    padding: 11px 14px;
    font-size: 0.76rem;
  }
  .region-card__countries ul {
    padding: 0 14px 14px;
  }
  .region-card__countries li {
    font-size: 0.78rem;
  }
  .coverage-cart {
    padding: 26px 22px;
  }
  .coverage-cart__regions {
    min-height: 0;
  }
  /* The table keeps its columns on mobile: eight rows read faster as one
     compact grid than as eight stacked blocks. Keeping them only works if
     every cell is free to wrap, which is what the rules below are now about.

     They were written for the unframed table — a set of hairlines ruled across
     the prose column, where the first cell opened on the prose's own left edge
     and the last one closed on its right. Both of those edges were a zero, and
     the frame changed what a zero there means: a cell that opens on the prose
     edge opens on the border once there is a border. Every row header on the
     site was touching it, and every value on the far side of the table was
     touching the other one. Padding on all four sides, inside the frame. */
  .data-table th,
  .data-table td {
    padding: 11px 12px;
    font-size: 0.88rem;
  }
  /* Two rules are gone rather than changed, both of them the same mistake in a
     different property. The row header held one line, and one line of
     "Permitted use and redistribution terms" is 410px — wider than the phone
     the rule was written for. It set the table's own minimum width, so the
     value column was pushed off the right edge and the reader was left with a
     framed list of labels and no answers beside them; wrapping, that table is
     193px at its narrowest. And the last column was set right, to put the
     answer on the far prose edge with nothing between it and the label. Inside
     a frame the two columns are already adjacent, and the selector reached past
     the two-column table it was written for to the pricing table's third
     column, setting three lines of sentence ragged left. Every column reads
     from the same edge now, the way it does on every other width.

     .table-scroll gives what it wraps a 620px floor, so a narrow table cannot
     squash to nothing in the desktop split. On a phone that floor is the whole
     problem: 620px inside a 350px container is a table that always scrolls, and
     the first thing to scroll out of sight is the column the table exists to
     show. The two tables that fit on a phone are let fit; the coverage matrix
     keeps its own floor below, because it is the one that never can. */
  .table-scroll > .data-table:not(.coverage-matrix) {
    min-width: 0;
  }
  .global-card {
    padding: 26px 22px;
  }
  .footer-grid {
    grid-template-columns: 1fr;
  }
}

/* ==========================================================================
   Internal reference pages — /en/elements/ and /en/sections/
   Everything below is scaffolding: preview frames, labels and specimen
   sizing. The components themselves are rendered with their production
   classes and are not restyled here. The two pages share this shell rather
   than each carrying a copy of it, which is the argument /en/sections/ makes
   about the rest of the site.
   ========================================================================== */
.reference-page section[id] {
  /* The section index below the header is sticky too, so the default
     --scroll-offset would drop anchor targets underneath it. */
  scroll-margin-top: calc(var(--header-height) + 58px);
}
.reference-hero {
  min-height: auto;
  padding: 92px 0 60px;
}
.reference-hero .hero-grid {
  display: block;
}
.reference-hero .hero__copy {
  max-width: 640px;
}
.reference-hero .hero-qualifier {
  margin-top: 26px;
}

.reference-nav {
  position: sticky;
  z-index: 20;
  top: var(--header-height);
  border-bottom: 1px solid var(--line);
  background: color-mix(in srgb, var(--raised) 93%, transparent);
  backdrop-filter: blur(8px);
}
.reference-nav ul {
  display: flex;
  flex-wrap: wrap;
  gap: 0 22px;
  margin: 0;
  padding: 9px 0;
  list-style: none;
}
.reference-nav a {
  display: block;
  padding: 5px 0;
  color: var(--text-muted);
  font-family: var(--font-data);
  font-size: 0.7rem;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}
.reference-nav a:hover {
  color: var(--brand-label);
}

.spec {
  margin-top: 36px;
}
.spec__label {
  margin: 0 0 12px;
  color: var(--text-muted);
  font-family: var(--font-data);
  font-size: 0.7rem;
  font-weight: 600;
  letter-spacing: 0.1em;
  text-transform: uppercase;
}
.spec__meta {
  max-width: 80ch;
  margin: 14px 0 0;
  color: var(--text-muted);
  font-size: 0.875rem;
  line-height: 1.55;
}
.spec-frame {
  /* A specimen wider than the frame scrolls inside it. The reference pages show
     components at their real width, and some of them — the tables, the wider
     specimens — are built for a desktop column, so without this they
     push the page sideways instead of the frame. */
  overflow-x: auto;
  padding: 26px;
  border: 1px solid var(--line);
  border-radius: var(--radius-card);
  background: var(--base);
}
.spec-frame > * + * {
  margin-top: 18px;
}
.spec-frame--flush {
  padding: 0;
  overflow: hidden;
}
/* For components that already carry their own card border, so the specimen is
   not framed twice. */
.spec-frame--bare {
  padding: 0;
  border: 0;
  background: none;
}
.reference-page .data-table tbody th {
  width: auto;
  white-space: nowrap;
}
.spec-frame .diagram {
  margin-top: 0;
}
.spec-frame .section-heading {
  margin-bottom: 0;
}

.spec-row {
  display: flex;
  flex-wrap: wrap;
  gap: 16px;
  align-items: center;
}
.spec-row--wrap {
  gap: 24px 30px;
  align-items: flex-start;
}
.spec-row--split {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 18px;
  align-items: start;
}

.swatch-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
  gap: 18px;
  margin: 0;
  padding: 0;
  list-style: none;
}
.swatch {
  display: grid;
  gap: 2px;
}
.swatch .swatch__chip {
  height: 68px;
  margin-bottom: 10px;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
}
.swatch .swatch__chip--outline {
  border-width: 2px;
}
.swatch b {
  font-family: var(--font-data);
  font-size: 0.8rem;
  font-weight: 600;
}
.swatch span {
  color: var(--text-muted);
  font-family: var(--font-data);
  font-size: 0.75rem;
}
.swatch small {
  margin-top: 4px;
  color: var(--text-muted);
  font-size: 0.8rem;
  line-height: 1.45;
}

/* The generated token reference on /en/elements/. Everything here styles what
   assets/js/token-inventory.js draws; the swatch itself is the same component
   the hand-written specimens beside it use. */
.token-inventory__group {
  margin-top: 30px;
}
.token-inventory__group:first-child {
  margin-top: 0;
}
.token-inventory__flags {
  color: var(--text-muted);
}
/* A token nobody has explained should be uncomfortable to look at, or the
   inventory becomes a list that is complete and says nothing. */
.token-inventory__missing {
  margin-top: 4px;
  padding: 4px 7px;
  border-left: 2px solid var(--warn);
  color: var(--warn-ink);
  background: var(--warn-soft);
  font-size: 0.8rem;
  line-height: 1.45;
}

.type-sample {
  margin: 0;
  color: var(--text-heading);
  font-size: clamp(1.6rem, 2.4vw, 2.1rem);
  font-weight: 550;
  letter-spacing: -0.03em;
}
.type-sample--mono {
  font-family: var(--font-data);
  font-size: 1.05rem;
  font-weight: 600;
  letter-spacing: 0;
}
/* The specimen shows the h1 wrapping the way it wraps on a page, so it stands
   in the same 680px .hero__copy gives it. Scoped to the frame because .section p
   would otherwise win the measure on specificity: the specimen is a <p>. */
.spec-frame .spec-h1 {
  max-width: 680px;
}

.ruler-list {
  display: grid;
  gap: 14px;
  margin: 0;
  padding: 0;
  list-style: none;
}
.ruler-list li {
  display: flex;
  flex-wrap: wrap;
  gap: 6px 16px;
  align-items: center;
}
.ruler {
  display: block;
  flex: none;
  height: 12px;
  border: 1px solid var(--brand-core);
  border-radius: 0;
  background: var(--brand-soft);
}
.ruler-list b {
  font-family: var(--font-data);
  font-size: 0.8rem;
}
.ruler-list li > span:last-child {
  color: var(--text-muted);
  font-size: 0.85rem;
}

.token-demo {
  display: grid;
  gap: 3px;
  width: 158px;
}
.token-demo__box {
  width: 72px;
  height: 72px;
  margin-bottom: 10px;
  border: 1px solid var(--line);
  background: var(--sunken);
}
.token-demo__box--shadow {
  border-radius: var(--radius-card);
  background: var(--base);
  box-shadow: var(--shadow);
}
.token-demo .icon {
  margin-bottom: 10px;
}
.token-demo b {
  font-family: var(--font-data);
  font-size: 0.78rem;
}
.token-demo span {
  color: var(--text-muted);
  font-size: 0.8rem;
  line-height: 1.45;
}
/* A numbered procedure. The numbers carry order that the reader needs, which
   is the only reason to number anything. Internal reference pages only. */
.steps {
  padding: 0 0 0 22px;
  margin: 0;
  color: var(--text-body);
  font-size: 0.93rem;
  line-height: 1.6;
}
.steps li {
  padding-left: 4px;
  margin-bottom: 8px;
}
.steps li::marker {
  color: var(--brand-label);
  font-family: var(--font-data);
  font-size: 0.8rem;
}
.token-code {
  color: var(--brand-label);
  font-family: var(--font-data);
  font-size: 0.88em;
  overflow-wrap: anywhere;
}

.state-demo {
  display: grid;
  gap: 9px;
  justify-items: start;
}
.state-demo__dark {
  padding: 16px;
  border-radius: var(--radius-sm);
  background: var(--inverse-primary);
}
.state-demo > span {
  color: var(--text-muted);
  font-family: var(--font-data);
  font-size: 0.68rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}
/* The dark-band specimens caption their states on --inverse-primary, where
   --text-muted runs under 3:1. The recessive value for that ground already
   exists, so the caption takes it rather than the band being captioned in a
   colour tuned for white. */
:is(.section--inverse-secondary, .section--inverse-primary) .state-demo > span {
  color: var(--text-muted-on-dark);
}

/* Sections page — /en/sections/
   The findings are marked with the site's own .callout rather than a bespoke
   box, because a reference arguing against duplicate components should not
   ship one to make the argument. */
.sections-page .callout .data-kicker {
  display: block;
  margin-bottom: 8px;
  color: var(--brand-label);
}
/* The findings on that page are three different asks and the reader should be
   able to tell them apart before reading a word: green proposes a merge, grey
   says leave it alone, red is something already broken. Green and grey are the
   .callout tones the site already has; only red is added here, and it reuses
   the colour the form errors use rather than introducing a second. */
.callout--alert {
  border-left-color: var(--warn);
  background: var(--warn-soft);
}
.sections-page .callout--alert .data-kicker {
  color: var(--warn);
}
/* The four ground specimens run at a smaller title than a real section, so all
   four fit in one frame without each reading as a page of its own. */
.sections-page .spec-ground .section-title {
  font-size: 1.6rem;
}
/* A specimen of a component that only exists on a dark section. The preview
   frame is declared 2000 lines after .section--inverse-secondary and would win
   the background on source order alone, leaving white text on a light ground.

   Scoped to .sections-page until the elements page grew a dark specimen of its
   own and hit exactly the failure this comment describes. Both reference pages
   now, which is every page that frames a preview: .spec-frame appears nowhere
   else. */
.spec-frame.section--inverse-secondary {
  border-color: var(--inverse-secondary);
  background: var(--inverse-secondary);
}
/* Hero specimens are shown at their real width but not their real height: a
   full-height hero inside a preview frame would push every other specimen off
   the page. */
.sections-page .spec-frame .hero {
  min-height: auto;
  padding: 44px 0;
}

.reference-page [aria-invalid="true"] {
  border-color: var(--warn);
}

.skip-link-demo {
  position: static;
  transform: none;
}

.elements-toggle {
  display: block;
  position: relative;
  padding: 0;
}
/* The specimen shows the panel open in place, on a page of any width, so it
   opts out of the fixed drawer the real header switches to below 1120px. */
.elements-toggle .dropdown {
  position: static;
  display: block;
  max-width: 280px;
  min-width: 0;
  height: auto;
  margin-top: 12px;
}

.region-grid-demo {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 240px), 1fr));
  gap: 18px;
  align-items: start;
}
.cart-demo .coverage-cart {
  position: static;
  max-width: 400px;
  max-height: none;
}
.imagery-sample {
  width: 100%;
  max-width: 560px;
  height: auto;
  border-radius: var(--radius-card);
}
/* The hero renders are cutouts, so the specimen has to supply a layer behind
   them or a page of white objects lands on a near-white ground. With the atmosphere
   parked off .hero--art this is the only place it is still drawn, which is
   deliberate: it keeps the wash visible while it waits to go on the body. */
.imagery-sample--atmosphere {
  --hero-glow-y: 46%;

  background-image: var(--hero-atmosphere);
}
.imagery-item {
  display: grid;
  grid-template-rows: 122px auto;
  gap: 10px;
  align-items: center;
  justify-items: center;
  width: 132px;
  text-align: center;
}
.imagery-item span {
  color: var(--text-muted);
  font-family: var(--font-data);
  font-size: 0.7rem;
  line-height: 1.4;
}
.globe-sample {
  width: 116px;
  height: 116px;
}

.notes-list {
  display: grid;
  gap: 15px;
  margin: 0;
  padding-left: 1.3em;
  color: var(--text-body);
  font-size: 1.0625rem;
  line-height: 1.62;
}
/* The chip for a token that only exists on a dark ground: the ground is the
   chip and the token is the letters on it, which is the only way to show an
   ink value honestly. --swatch-ink lets each swatch name its own, defaulting
   to the green this pattern was built for. */
.swatch .swatch__on-dark {
  display: grid;
  height: 100%;
  place-items: center;
  color: var(--swatch-ink, var(--brand-on-dark));
  font-family: var(--font-data);
  font-size: 0.9rem;
  font-weight: 600;
}
.notes-list b {
  color: var(--text-heading);
  font-weight: 600;
}

@media (max-width: 900px) {
  .spec-row--split {
    grid-template-columns: 1fr;
  }
}
@media (max-width: 720px) {
  .reference-hero {
    padding: 60px 0 44px;
  }
  .spec {
    margin-top: 28px;
  }
  .spec-frame {
    padding: 20px 18px;
  }
  /* Three wrapped rows of links plus the sticky header would take a fifth of a
     phone viewport, so the index scrolls away here. */
  .reference-nav {
    position: static;
  }
  .reference-nav ul {
    gap: 0 16px;
  }
  .reference-page section[id] {
    scroll-margin-top: calc(var(--header-height) + 12px);
  }
  .cart-demo .coverage-cart {
    max-width: none;
  }
}
@media (max-width: 560px) {
  .swatch-grid {
    grid-template-columns: repeat(auto-fill, minmax(142px, 1fr));
  }
  .token-demo {
    width: 136px;
  }
  .imagery-item {
    width: 106px;
  }
}
