/* ---------------------------------------------------------------------------
   landing.css — the marketing site's composite layer: the pieces that make up
   a page, built out of the primitives in components.css.

   Load order is brand.css → layout.css → components.css → landing.css, so a
   rule here wins over a primitive, and a primitive wins over the shared token
   defaults. Nothing in this file spells a colour or a measurement of its own.

   Figma file 76n6btvsYu0t6AG5VZ353t.
   --------------------------------------------------------------------------- */

html {
  /* An in-page anchor would otherwise land underneath the fixed pill. Derived
     from --nav-clear rather than restating the pill's numbers: the pill now
     sizes itself against the window's height, so a literal here would be
     correct at one window height and wrong at every other. */
  scroll-padding-top: calc(var(--nav-clear) + 24px);
  scroll-behavior: smooth;
}

body {
  /* brand.css defaults every web surface to --surface; the marketing frames
     sit on the lighter --neutral-95 with the washes painted over it. */
  background: var(--surface-lift);
}

/* ── Nav ───────────────────────────────────────────────────────────────────
   Figma: Container — node 1414:205352. A 601x72 glass pill floating 24px from
   the top of the hero, centred. Its width is content-driven in the design too
   — 8 + 184 + 32 + links + 32 + button + 32 comes to the 601 the frame reports
   — so it is left to size itself rather than pinned, which is also what lets
   the same markup shrink to a logo and a toggle on a phone. */
.site-nav {
  position: fixed;
  top: var(--nav-top);
  left: 50%;
  z-index: 50;
  translate: -50%;
  max-width: calc(100% - 2 * var(--page-padding));
}

.site-nav__bar {
  position: relative;
  display: flex;
  align-items: center;
  gap: var(--nav-gap);
  padding: 8px var(--nav-pad-end) 8px 8px;
  border-radius: var(--radius-full);
  background: var(--glass);
  backdrop-filter: blur(20px);
  -webkit-backdrop-filter: blur(20px);
  transition: box-shadow 0.2s ease;
}

/* Once the page has scrolled the pill is over arbitrary content rather than
   the hero wash, and 60% white with nothing behind it stops reading as a
   surface. The shadow is the separation; the glass is left alone. */
.site-nav.is-scrolled .site-nav__bar { box-shadow: var(--shadow-card); }

/* Without a backdrop-filter the pill is 60% white over whatever it happens to
   overlap, which is unreadable rather than merely different. */
@supports not ((backdrop-filter: blur(20px)) or (-webkit-backdrop-filter: blur(20px))) {
  .site-nav__bar { background: var(--card); }
}

/* Figma: logo — node 1437:209103. 184x56: a 56px sphere, 4px, a 124x45
   wordmark.

   The wordmark is written as a ratio of the sphere rather than as its own
   pair of numbers, because --nav-logo is now a clamp on the window's height
   and there is no single value to write. 124/56 and 45/56 resolve to exactly
   the design's 124x45 at the top of that clamp. */
.site-nav__logo {
  display: flex;
  align-items: center;
  gap: 4px;
  flex-shrink: 0;
  text-decoration: none;
}

/* `display: block` is load-bearing, not tidiness. The sphere is an <img>
   inside a <picture>, and a <picture> is an inline box — so an inline <img>
   in it sits on a baseline and the line box adds four pixels of descender
   underneath. That made the pill 76px tall against the design's 72, and it
   made --nav-h wrong by the same four: the scroll offset an anchor lands at
   and the floor of the hero's top padding are both derived from it. The
   wordmark never had the problem because it is a direct child of the flex
   container, which blockifies it. */
.logo-sphere,
.logo-wordmark { display: block; }

.site-nav__logo .logo-sphere {
  width:  var(--nav-logo);
  height: var(--nav-logo);
}

.site-nav__logo .logo-wordmark {
  width:  calc(var(--nav-logo) * 124 / 56);
  height: calc(var(--nav-logo) * 45 / 56);
}

.site-nav__links {
  display: flex;
  align-items: center;
  gap: 32px;
}

.site-nav__links > a:not(.btn) {
  font-family: var(--font-body);
  font-weight: 400;
  font-size: var(--text-md);
  line-height: 1.5;
  color: var(--neutral-mid);
  text-decoration: none;
  white-space: nowrap;
  transition: color 0.15s ease;
}

.site-nav__links > a:not(.btn):hover { color: var(--ink); }

/* Drawn in CSS rather than exported, because it is not in the design: the
   Figma file is four 1440-wide frames and has no phone breakpoint, so the
   collapsed nav is a build decision. An icon that exists in Figma is always
   exported instead of redrawn. */
.site-nav__toggle {
  display: none;
  position: relative;
  width: 44px;
  height: 44px;
  padding: 0;
  border: 0;
  border-radius: var(--radius-12);
  background: transparent;
  cursor: pointer;
}

.site-nav__toggle::before,
.site-nav__toggle::after { content: ""; }

.site-nav__toggle::before,
.site-nav__toggle::after,
.site-nav__toggle > span {
  position: absolute;
  left: 50%;
  width: 20px;
  height: 2px;
  margin-left: -10px;
  border-radius: var(--radius-full);
  background: var(--ink);
  transition: transform 0.2s ease, opacity 0.2s ease;
}

.site-nav__toggle::before  { top: 15px; }
.site-nav__toggle > span   { top: 21px; }
.site-nav__toggle::after   { top: 27px; }

.site-nav.is-open .site-nav__toggle::before { transform: translateY(6px) rotate(45deg); }
.site-nav.is-open .site-nav__toggle > span  { opacity: 0; }
.site-nav.is-open .site-nav__toggle::after  { transform: translateY(-6px) rotate(-45deg); }

/* ── Hero ──────────────────────────────────────────────────────────────────
   Figma: Header / 1 / — node 1414:204994. 1440x883: pt180 (the pill floats
   over it) and pb112, two columns with an 80px gap.

   The two washes are Figma radial fills whose gradientTransform is a skewed
   ellipse; CSS radial-gradient is axis-aligned, so each is placed at the
   bounding ellipse of the mapped circle. The amber one drops its middle stop
   — a 4%-alpha brown that would need a token of its own to darken a wash
   almost nobody will see. The washes' 40% layer opacity is already baked into
   --wash-violet / --wash-amber, which is why they are used undivided here and
   in the footer. */
.hero {
  /* ── The vertical budget ─────────────────────────────────────────────────
     The design's hero is 883px tall, which is taller than the window it is
     read in: a 1440x900 screen leaves about 760px once the browser's own
     chrome is paid for, and the phones — the one element on the page that
     *is* the product shot — were the half that fell off the bottom.

     So the frame's three vertical numbers become ceilings rather than values.
     Each grows back to the design on a tall screen and gives way on a short
     one, in the order they can afford to: the paddings first, the stage last.

     The top padding's floor is what clears the nav, and it is --nav-clear
     plus air rather than the 112px it used to be: the pill sizes itself
     against the window now, so a literal floor would hold the hero 16px below
     a bar that had already moved up. The bottom padding's ceiling is
     --section-pad-lg rather than a literal 112, so it keeps stepping with the
     width breakpoints and with the height taper alike.

     --hero-peek is the part deliberately left unspent: the sliver of the
     trust strip showing under the fold. A hero sized to exactly 100vh is a
     hero that looks like the whole page, and the strip's top edge is the
     cheapest possible thing to say otherwise. */
  --hero-pad-top:    clamp(calc(var(--nav-clear) + 16px), 18vh, 180px);
  --hero-pad-bottom: clamp(56px, 10vh, var(--section-pad-lg));
  --hero-peek:       24px;
  --hero-chrome: calc(var(--hero-pad-top) + var(--hero-pad-bottom) + var(--hero-peek));

  padding-inline: var(--page-padding);
  padding-block: var(--hero-pad-top) var(--hero-pad-bottom);
  background:
    radial-gradient(54% 55% at 74.4% 6.2%, var(--wash-violet), transparent),
    radial-gradient(100% 108% at 26.6% 2.6%, var(--wash-amber), transparent),
    var(--surface-lift);
}

.hero__inner {
  display: flex;
  align-items: center;
  gap: 80px;
}

.hero__copy {
  display: flex;
  flex: 1 1 0;
  min-width: 0;
  flex-direction: column;
  align-items: flex-start;
  gap: 32px;
}

.hero__lockup {
  display: flex;
  flex-direction: column;
  gap: 24px;
}

/* The design's first line is 609px against a 600px column, so it is set to
   overflow into the 80px gutter rather than wrap — which is what the Figma
   frame does too. Only at the top breakpoint: below it the type steps down,
   the column narrows faster than the type does, and a nowrap heading would
   run under the phones. */
@media (min-width: 1280px) {
  .hero .display { white-space: nowrap; }
}

.hero__actions {
  display: flex;
  align-items: center;
  gap: 16px;
  flex-wrap: wrap;
}

.hero__badges {
  display: flex;
  align-items: center;
  gap: 16px;
}

.hero__media {
  display: flex;
  flex: 1 1 0;
  min-width: 0;
  justify-content: center;
}

/* The stage is the frame the phones are positioned inside, and it carries the
   design's own proportions (421.8 x 591.0 at the 1440 reference) so both
   phones stay on their marks at any width without a second set of numbers.
   Stage 3 measures this box rather than restating it. */
.hero__stage {
  position: relative;
  /* Three ceilings, and the third is the window. The stage is the tallest
     thing in the hero, so it is the one that decides whether the hero fits.

     It is capped on *width* rather than given a height, and that is the whole
     trick: aspect-ratio stays in charge of the other axis. A definite height
     plus a max-width would clamp one axis and leave the other, and the two
     phones are positioned by percentages of this box — a stage off its ratio
     puts both of them off their marks rather than merely making them smaller.

     0.71369 is 421.796 / 591.005, the ratio on the line below, written out
     because calc() cannot read it back off aspect-ratio.

     Two declarations, not a custom property holding the same sum: svh only
     resolves inside a real property, so a browser without it drops the second
     line and keeps the first. Folded into a var() it would resolve to nothing
     and take `width` with it, because a custom property is not parsed until
     it is substituted. vh is the *largest* viewport — the one a phone only
     reaches after its toolbars retract — so it is the fallback, not the
     value. */
  width: min(100%, 421.8px, calc((100vh  - var(--hero-chrome)) * 0.71369));
  width: min(100%, 421.8px, calc((100svh - var(--hero-chrome)) * 0.71369));
  aspect-ratio: 421.796 / 591.005;
}

/* One flat upright asset per audience, turned live rather than shipped tilted.
   The keyframes bake the tilt into a *different* bitmap from the one docked in
   the cells later, so there is no angle between them to interpolate — and the
   flat pair is the pair the cells need anyway, which halves the phone payload.
   Measured off the keyframe renders: the visible side face is 6-7% of the
   silhouette against an 11% thickness-to-width ratio, and the front face is
   foreshortened to 0.83 of flat. Both put the turn at ~33 degrees.

   They mirror each other. The kid phone shows its right side and the adult
   phone its left, so the pair opens toward the reader like a book rather than
   leaning the same way twice.

   The angle is a --tilt property rather than a literal because the scrub reads
   it off the slot: motion.css sets it on the runway slots too, and the phone
   that travels between them takes whatever the slot says. One number, one
   place, whichever end of the path is being drawn. */
.hero__phone {
  position: absolute;
  width: 62.15%;
  aspect-ratio: 206.96 / 428.79;
}

.hero__phone img {
  display: block;
  width: 100%;
  height: 100%;
}

/* Positioned by the centre of each phone's *front face* in the keyframe, not
   by its bounding box: the keyframe bitmaps include the side face and the flat
   asset does not, so matching the boxes would shift both phones outward. */
.hero__phone--kid {
  --tilt: -33deg;
  left: 26.81%;
  top: 45.95%;
  transform: translate(-50%, -50%) perspective(1600px) rotateY(var(--tilt));
}

.hero__phone--adult {
  --tilt: 33deg;
  left: 73.35%;
  top: 53.75%;
  transform: translate(-50%, -50%) perspective(1600px) rotateY(var(--tilt));
}

/* ── Trust strip ───────────────────────────────────────────────────────────
   Figma: Layout / 308 / — node 1414:205015. A white band between the hero and
   the features, hairlined top and bottom, holding five claims the rest of the
   page then makes good on.

   The five claims are about 1227px of content in a 1280px row, so the design's
   arrangement — one line, space-between — is true at exactly one width and
   false below it. What happens below is the marquee further down this file. */
.trust-strip {
  --marquee-gap:      24px;
  --marquee-fade:     48px;
  --marquee-duration: 24s;
  /* `normal` runs the row leftward, the way a news ticker does. `reverse`
     runs it rightward. One keyword, and it is the whole of that decision. */
  --marquee-direction: reverse;

  padding-inline: var(--page-padding);
  padding-block: 12px;
  background: var(--card);
  border-block: 1px solid var(--border-card);
}

/* The track exists so there is something to move that is not the list itself.
   Unarmed it is a plain flex parent holding one list and changes nothing about
   the row; armed it holds two and is the animated element. Written as a
   wrapper rather than animating the list because the seamless loop needs the
   *pair* to be the thing translated — see the keyframe below. */
.trust-strip__track {
  display: flex;
}

.trust-strip__list {
  display: flex;
  flex: 1 1 auto;
  min-width: 0;
  align-items: center;
  justify-content: space-between;
  min-height: 72px;
  margin: 0;
  padding: 0;
  list-style: none;
}

.trust-strip__item {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 12px;
}

.trust-strip__item img {
  display: block;
  flex-shrink: 0;
  width: 24px;
  height: 24px;
}

/* Below the width the row fits on, they wrap and centre — space-between on a
   wrapped row leaves the last line stranded against the left edge. This is the
   static arrangement, and it is what a reader gets with JavaScript off or with
   reduced motion asked for; the marquee is layered over it, never instead of
   it.

   1351 and not 1279, which is a correction. The five items measure 1224px, the
   gutters take 128, and the container caps at 1280 — so the row stops fitting
   at a viewport of 1352, not at 1280. In the 73px between the old figure and
   the real one the items did not wrap and did not overflow either: they
   *shrank*, because a flex item may, and the labels wrapped inside them into a
   two-line strip. Narrow enough to have gone unnoticed, and only visible on a
   window somebody had dragged to roughly that width. */
@media (max-width: 1351px) {
  .trust-strip__list {
    flex-wrap: wrap;
    justify-content: center;
  }
}

/* The state site.js measures in, and the reason the measurement can be
   trusted. A flex item that has been squeezed reports its squeezed width, so
   asking an over-full row how wide it wants to be gets the answer "exactly as
   wide as the space I was given" — which is the one answer that can never
   arm the marquee. Held for a single synchronous read and dropped again. */
.trust-strip.is-measuring .trust-strip__list { flex-wrap: nowrap; }
.trust-strip.is-measuring .trust-strip__item { flex: 0 0 auto; }

/* ── The marquee ───────────────────────────────────────────────────────────
   Armed by site.js, and only when the row genuinely does not fit: it measures
   the five items against the container rather than trusting a breakpoint, so
   the row starts sliding at the width it actually overflows at — which moves
   with the font, the copy and the gutter, none of which a media query can
   see.

   `is-marquee` is therefore a *measured* state, not a breakpoint. There is no
   width in this block for that reason.

   The loop is two identical lists translated by half the track. Half the track
   is exactly one list, so the moment the animation restarts, the second copy
   is standing where the first was and the seam is unobservable. That is also
   why the spacing between the two copies is the first list's trailing padding
   rather than a `gap` on the track: a gap would make half the track one list
   *plus half a gap*, and the row would jump by 12px once every cycle.

   The mask is what lets this stay inside the container. Without it the claims
   would be cut off dead at the container's edge — an intact word one frame and
   gone the next — which reads as a rendering fault rather than as a row that
   continues past the edge. */
.trust-strip.is-marquee .container {
  overflow: hidden;
  /* `black` is an alpha here, not a paint: a mask reads only the alpha
     channel, so the colour is unobservable and naming a brand token for it
     would be inventing a meaning it does not have. */
  -webkit-mask-image: linear-gradient(to right,
    transparent, black var(--marquee-fade),
    black calc(100% - var(--marquee-fade)), transparent);
  mask-image: linear-gradient(to right,
    transparent, black var(--marquee-fade),
    black calc(100% - var(--marquee-fade)), transparent);
}

.trust-strip.is-marquee .trust-strip__track {
  width: max-content;
  animation: trust-marquee var(--marquee-duration) linear infinite;
  animation-direction: var(--marquee-direction);
}

/* Moving content that runs longer than five seconds has to be stoppable, and
   hover plus focus-within is the mechanism that costs no control on screen.
   focus-within is the load-bearing half: a keyboard reader tabbing through the
   page has no pointer to hold still with. */
.trust-strip.is-marquee:hover .trust-strip__track,
.trust-strip.is-marquee:focus-within .trust-strip__track {
  animation-play-state: paused;
}

/* Higher specificity than the two width blocks above and below, which is
   deliberate: the marquee has to undo the wrap at 1279 and the snap scroller
   at 767, and both of those are the fallback it is layered over rather than
   rules it replaces. */
.trust-strip.is-marquee .trust-strip__list {
  flex: 0 0 auto;
  flex-wrap: nowrap;
  justify-content: flex-start;
  width: max-content;
  gap: var(--marquee-gap);
  padding-inline: 0 var(--marquee-gap);
  overflow: visible;
  scroll-snap-type: none;
}

.trust-strip.is-marquee .trust-strip__item {
  flex: 0 0 auto;
}

@keyframes trust-marquee {
  from { transform: translateX(0); }
  to   { transform: translateX(-50%); }
}

/* ── Features ──────────────────────────────────────────────────────────────
   Figma: Layout / 398 / — node 1491:225842. Four cards in two rows, then the
   adult band and the showcase, all separated by the same 32px.

   The two column widths are 405 and 843 against a 1280 container, and they are
   written as flex ratios rather than pixels so the pair keeps its proportion
   at every width instead of the narrow card holding its size while the wide
   one absorbs everything. At 1280 the ratio resolves to exactly the design's
   two numbers. */
.features__stack {
  display: flex;
  flex-direction: column;
  gap: 32px;
}

.features__row {
  display: flex;
  align-items: stretch;
  gap: 32px;
}

.features__col--narrow { flex: 405 1 0; min-width: 0; }
.features__col--wide   { flex: 843 1 0; min-width: 0; }

.features__row > .card { display: flex; }

/* ── The split cells ───────────────────────────────────────────────────────
   Figma: row — node 1491:225862. The two halves of the "Separate areas" card,
   and the place the hero's phones dock in Stage 3 — which is why the phones
   here are the same flat asset at the same insets rather than a second
   export.

   The grain is one tile, not the design's two. The second (115px at 70%) is a
   palette PNG whose alpha covers 5.6% of its pixels at a mean of 2.4/255, so
   at 70% layer opacity it changes roughly seven pixels in a thousand by one
   level — it is a no-op that would cost a request. The first is real: 62%
   coverage at a mean alpha of 100/255, and the cell measures a ±16-level
   modulation in green without which the gradient reads as flat vinyl. */
.split-cells {
  display: flex;
  width: 100%;
}

.split-cell {
  position: relative;
  flex: 1 1 0;
  min-width: 0;
  aspect-ratio: 421.5 / 482;
  overflow: hidden;
}

.split-cell--kid   { background-image: var(--gradient-child-cell); }
.split-cell--adult { background-image: var(--gradient-parent-cell); }

/* Flattened to greyscale over a neutral mid: overlay is a no-op against 128,
   so folding the tile's alpha onto that grey reproduces the layer without
   paying for an alpha channel — 18KB against 52KB, for the same pixels. */
.split-cell::before,
.showcase__grain {
  content: "";
  position: absolute;
  inset: 0;
  background-image: url("/assets/art/noise.webp");
  background-image: image-set(url("/assets/art/noise.avif") type("image/avif"),
                              url("/assets/art/noise.webp") type("image/webp"));
  background-size: 165.6px 165.6px;
  mix-blend-mode: overlay;
  opacity: 0.15;
  pointer-events: none;
}

.split-cell__phone {
  position: absolute;
  inset: 5.6% 25.5% 5.44% 25.4%;
}

.split-cell__phone img {
  display: block;
  width: 100%;
  height: 100%;
}

/* Figma: Frame 1 — node 1491:225893. One bitmap rather than a rebuild: its
   phone is neither of the two flat assets and its emoji are a 3D render, so
   layering it would add a third phone bitmap and buy nothing. Its top corners
   are baked round at the same 40px the card clips to, so the two agree. */
.card__panel {
  display: block;
  width: 100%;
  height: auto;
}

/* ── The adult band ────────────────────────────────────────────────────────
   Figma: Container — node 1491:225930. The composition is 636px wide inside a
   column that is narrower than that, and the band clips it — which is the
   design, not an accident: the right-hand activity card is cut by the edge. */
.band {
  --band-pad: 40px;
  --band-pad-block: 80px;
  position: relative;
  display: flex;
  align-items: center;
  gap: 80px;
  overflow: hidden;
  padding: var(--band-pad-block) var(--band-pad);
  border-radius: var(--radius-40);
  background-image: var(--gradient-parent-cell);
}

.band__copy {
  display: flex;
  flex: 1 1 0;
  min-width: 0;
  flex-direction: column;
  gap: 16px;
}

/* The design gives the body copy here a secondary/50 that is white warmed by
   two levels out of 255, against a heading that is plain white. They are the
   same colour on screen, so it does not get a token of its own. (Spelling the
   value out is not an option either: the parity guard reads whole lines, so a
   hex in a comment fails it — which is the guard working, not a false hit.) */
.band__copy .h-section,
.band__copy .body { color: var(--card); }

/* 320px because the design's band is 480 tall on 80px of vertical padding, and
   the 477px composition is clipped top and bottom rather than setting the
   height. Flush to the band's right edge — not to its padding — so the corners
   the export baked round land exactly where the band's own radius clips, which
   is what stops them showing as white notches. */
.band__media {
  position: relative;
  flex: 1 1 0;
  min-width: 0;
  align-self: stretch;
  min-height: 320px;
}

.band__media img {
  position: absolute;
  top: 50%;
  right: calc(-1 * var(--band-pad));
  display: block;
  width: 636px;
  max-width: none;
  height: auto;
  translate: 0 -50%;
}

/* ── The showcase card ─────────────────────────────────────────────────────
   Figma: Card — node 1504:33703, which get_metadata reports as a leaf, so its
   phones cannot be exported without the type sitting under them. The type is
   rebuilt here because a 1280-wide bitmap of 40px display copy is unreadable
   on a phone and invisible to search; the export's background below the phones
   is constant down each column, so scripts/../clean_showcase.py repaints those
   rows from a clean band and leaves an image that is only artwork. */
.showcase {
  position: relative;
  overflow: hidden;
  border-radius: var(--radius-40);
  background: var(--ink) url("/assets/art/showcase.webp") top center / 100% auto no-repeat;
  background-image: image-set(url("/assets/art/showcase.avif") type("image/avif"),
                              url("/assets/art/showcase.webp") type("image/webp"));
  aspect-ratio: 1280 / 896.5;
}

.showcase__copy {
  position: absolute;
  inset: auto 0 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 32px;
  padding-bottom: 8.9%;
  text-align: center;
}

.showcase__copy .h-showcase { color: var(--card); }

.showcase__badges {
  display: flex;
  align-items: center;
  gap: 16px;
}

/* ── Section title ─────────────────────────────────────────────────────────
   Figma: Section Title — node 1491:225991. Heading on the left, a paragraph
   of running text on the right, both vertically centred. Shared by the
   comparison table and the team section; the steps use the left half alone. */
.section-title {
  display: flex;
  align-items: center;
  gap: 40px;
}

/* The eyebrow-and-heading stack. The steps section uses it on its own, so it
   carries no flex sizing of its own: `flex: 1 1 0` on a child of a *column*
   container is a 0 basis in the block direction, and the block collapses. The
   sizing belongs to the two-column row, which is where it is set. */
.title-copy {
  display: flex;
  flex-direction: column;
  gap: 16px;
}

.section-title > .title-copy {
  flex: 1 1 0;
  min-width: 0;
}

/* 540 in the design, but written to shrink rather than hold: it is the
   right-hand column of a two-column row, and a fixed width here is what makes
   the heading column collapse first. */
.section-title > .body-lg { flex: 0 1 540px; }

/* The team section puts two cards where the comparison table puts a paragraph,
   and the design opens the gap to 80 for them. */
.section-title--wide { gap: 80px; }

/* ── The comparison table ──────────────────────────────────────────────────
   Figma: Layout / 308 / — node 1491:225989. The same two washes as the hero
   and the footer, anchored to the bottom edge, over --surface rather than the
   hero's --surface-lift — the page steps one level darker here and stays
   there for the rest of the scroll. */
.compare {
  background:
    radial-gradient(99% 100% at 73.8% 100%, var(--wash-violet), transparent),
    radial-gradient(90% 125% at 25.6% 100%, var(--wash-amber), transparent),
    var(--surface);
}

.compare .container {
  display: flex;
  flex-direction: column;
  gap: var(--stack-gap-md);
}

/* Figma: List — node 1491:225997. A real table, not a grid of divs: this is a
   permission matrix, and a lone tick in a cell means nothing without its row
   and column headers to a screen reader. The bubbles are decorative
   (alt=""); the answer each one stands for is a visually-hidden word. */
.compare__table {
  width: 100%;
  border-collapse: collapse;
  /* Fixed so the first column is set once. Under auto layout every row would
     get a vote on the column widths, and the longest label would decide them
     for all six. */
  table-layout: fixed;
}

.compare__table th,
.compare__table td {
  padding-inline: 24px;
  padding-block: 8px;
  border-bottom: 1px solid var(--border-card);
}

/* The card's own border draws the last edge; a seventh rule inside it would
   sit a pixel above the radius and show as a chord across both corners. */
.compare__table tr:last-child > * { border-bottom: 0; }

.compare__table thead th {
  height: 56px;
  background: var(--surface);
}

/* 572 of the design's 1280. The two answer columns then split what is left,
   which is the pair of flex-1 cells the design resolves to at that width. */
.compare__table thead th:first-child {
  width: 44.6875%;
  /* <th> centres by default; only the two answer columns want that. */
  text-align: left;
}

.compare__head {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 12px;
}

/* The audience keys. Slot 1 → slot 2 on each, so the pair reads as the same
   two accents the split cells use further up the page. */
.compare__dot {
  flex-shrink: 0;
  width: 12px;
  height: 12px;
  border-radius: var(--radius-full);
}

.compare__dot--child { background-image: var(--gradient-child-cell); }
.compare__dot--adult { background-image: var(--gradient-parent-cell); }

.compare__table tbody th,
.compare__table tbody td { height: 72px; }

.compare__table tbody th {
  font-family: var(--font-body);
  font-weight: 500;
  font-size: var(--text-md);
  line-height: 1.5;
  color: var(--ink);
  text-align: left;
}

.compare__table tbody td .icon-bubble { margin-inline: auto; }

/* ── The enrolment steps ───────────────────────────────────────────────────
   Figma: Timeline / 6 / — node 1491:226073. Four cards in a two-column grid;
   a grid rather than two flex rows because grid stretches the pair in a row
   to one height for free, which is the property the panels below depend on. */
.steps {
  background: var(--surface);
  /* The design gives this section top padding only — the rhythm below it is
     the team section's own. */
  padding-bottom: 0;
}

.steps .container {
  display: flex;
  flex-direction: column;
  gap: var(--stack-gap-lg);
}

.steps__grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 32px;
}

/* Figma: Card — node 1491:226080. Pinned to 761px in the design; here the
   copy sets the height and `margin-top: auto` drops the panel to the bottom,
   so the two panels in a row start on the same line however the copy wraps.
   That is the same result Figma reaches by hand when it pads the shorter
   card's copy block out to match the taller one. */
.step {
  gap: 32px;
  padding: 32px;
}

.step__copy {
  display: flex;
  flex-direction: column;
  gap: 16px;
}

.step__text {
  display: flex;
  flex-direction: column;
  gap: 12px;
}

/* Figma: Frame 6 — node 1491:226088. One aspect ratio for all four, where the
   design gives rows 1 and 2 panels 24px apart in height for no reason the
   frames state — and one ratio is what keeps the phone the same size in every
   card. */
.step__panel {
  position: relative;
  margin-top: auto;
  overflow: hidden;
  aspect-ratio: 560 / 505;
  border-radius: var(--radius-32);
  background-image: linear-gradient(177deg,
                                    var(--step-panel-top) 7%,
                                    var(--step-panel-bottom) 95%);
}

.step__phone {
  position: absolute;
  left: 50%;
  top: 50%;
  width: 38.26%;
  aspect-ratio: 214.256 / 443.93;
  translate: -50% -50%;
}

.step__phone img {
  display: block;
  width: 100%;
  height: 100%;
}

/* Positioned by centre and turned about it, so a cutout keeps its mark and
   its angle at every width. The three sit at the design's own coordinates,
   read as a fraction of the panel rather than in pixels. */
.step__cutout {
  position: absolute;
  aspect-ratio: 3 / 2;
  translate: -50% -50%;
}

.step__cutout img {
  display: block;
  width: 100%;
  height: 100%;
}

.step__cutout--didit    { left: 30.67%; top: 18.64%; width: 22.58%; rotate: -10.28deg; }
.step__cutout--approved { left: 70.19%; top: 71.48%; width: 21.97%; rotate: 12.82deg; }
.step__cutout--stamp    { left: 58.39%; top: 89.39%; width: 28.21%; }

/* ── The team ──────────────────────────────────────────────────────────────
   Figma: Timeline / 6 / — node 1504:65429. */
.team { background: var(--surface); }

/* Pinned to the pair's drawn size — 288 + 24 + 288 — rather than taking half
   of whatever the gap leaves. Two flexible columns only give the cards their
   288 once the container reaches its own 1280 cap, which is 1408px of viewport
   and up; at 1280 they are 256 and both names wrap mid-name. The story is a
   paragraph and reads no worse at 472 than at 536. At 1440 this resolves to
   the same 600 the flexible version did, so the design width is unchanged. */
.team__cards {
  display: flex;
  flex: 0 0 600px;
  min-width: 0;
  gap: 24px;
}

/* Figma: Card — node 1533:43149. A 2px border and the smaller radius, both
   deliberate: at 288px wide the marketing card's 40px corners would eat a
   seventh of the card, and its hairline disappears at this size. */
.team-card {
  display: flex;
  flex: 1 1 0;
  min-width: 0;
  flex-direction: column;
  align-items: center;
  gap: 24px;
  padding: 32px;
  border: 2px solid var(--border-card);
  border-radius: var(--radius-32);
  background: var(--card);
}

.team-card__avatar {
  display: block;
  width: 128px;
  height: 128px;
  border-radius: var(--radius-full);
  object-fit: cover;
}

/* The card's own 24px gap is the space above the LinkedIn pill; the avatar sits
   16 from the name, which is why they are their own column rather than two more
   children of the card. */
.team-card__column {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 16px;
}

/* The name and the role carry no gap in the design — they are one block, and
   the 1.5 line-height is the whole of the space between them. */
.team-card__title { text-align: center; }

.team-card__role {
  font-family: var(--font-body);
  font-size: var(--text-md);
  line-height: 1.5;
  color: var(--neutral-mid);
  margin: 0;
}

/* ── The contact card ──────────────────────────────────────────────────────
   Figma: CTA / 23 / — node 1491:226156. The section carries no vertical
   padding of its own: the card *is* the section, and the space above it is
   the team section's bottom padding. */
.contact {
  padding-inline: var(--page-padding);
  background: var(--surface);
}

.contact__card {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 24px;
  --card-pad-block: 80px;
  overflow: hidden;
  padding: var(--card-pad-block) 40px;
  border-radius: var(--radius-80);
  background:
    radial-gradient(44.5% 89% at 31.9% 50%, var(--wash-amber), transparent),
    radial-gradient(45% 99% at 56.3% 43.8%,
                    var(--wash-violet-deep) 0%,
                    var(--wash-violet-mid) 25%,
                    var(--wash-violet-soft) 50%,
                    transparent 100%),
    linear-gradient(136deg, var(--ink) 5%, var(--ink-deep) 99%);
}

/* Figma: Ellipse 1 — node 1491:226158. A 465x302 ellipse of the display
   gradient at 60%, blurred by 150. Drawn rather than shipped: the SVG export
   is a 1065x902 canvas holding that one ellipse and an feGaussianBlur the
   browser would have to run anyway, so this is the same pixels without the
   request. */
.contact__glow {
  position: absolute;
  left: 22.34%;
  top: 23.83%;
  width: 36.33%;
  height: 40.43%;
  border-radius: 50%;
  background-image: var(--gradient-display);
  opacity: 0.6;
  filter: blur(150px);
  pointer-events: none;
}

/* Every child of the card needs a stacking context of its own, or the glow —
   which is a later sibling in the source — paints over the form. */
.contact__content,
.contact__badges { position: relative; }

.contact__content {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 40px;
  width: 100%;
  max-width: 920px;
}

.contact__card .h-cta {
  color: var(--card);
  text-align: center;
}

.contact__form {
  display: flex;
  flex-direction: column;
  gap: 24px;
  width: 100%;
  max-width: var(--max-width-md);
}

.contact__row {
  display: flex;
  gap: 24px;
}

.contact__field {
  display: flex;
  flex: 1 1 0;
  min-width: 0;
  flex-direction: column;
  gap: 8px;
}

.contact__send {
  align-self: center;
  width: 108px;
}

/* Off-screen rather than hidden: display:none and visibility:hidden are both
   things a bot can test for cheaply, and a honeypot that announces itself
   catches nothing. Not .visually-hidden, which is a real utility for content
   a screen reader should still read — this is the opposite, and sharing the
   class would make one of the two wrong the next time it is tuned. */
.contact__hp {
  position: absolute;
  left: -9999px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}

/* Empty until the form is submitted, and hidden while it is — an always-present
   line of small print under a Send button reads as an error nobody caused. */
.contact__status {
  margin: 0;
  text-align: center;
  font-family: var(--font-body);
  font-size: var(--text-sm);
  line-height: 1.5;
  color: var(--field-placeholder);
}

.contact__status:empty { display: none; }

.contact__badges {
  display: flex;
  align-items: center;
  gap: 16px;
  padding-top: 40px;
}

/* ── The two big padded blocks, on height ──────────────────────────────────
   The rhythm tokens in layout.css taper the space *between* sections. These
   two are the only places on the page where a single element carries that
   much padding *inside* it — 80px top and bottom on the adult band, the same
   on the contact card — and neither is reached by a token, because both are
   the surface's own inset rather than the page's rhythm.

   Same shape as the tokens: the design's figure until a fraction of the
   window falls below it. Gated at 1024 for the same reason everything else
   here is, and because both blocks are overridden wholesale by the stacked
   layouts further down, which set the shorthand and would win anyway. */
@media (min-width: 1024px) {
  .band          { --band-pad-block: clamp(56px, 8.5vh, 80px); }
  .contact__card { --card-pad-block: clamp(56px, 8.5vh, 80px); }

  /* The card's own two internal gaps, which are 40px each and add up to more
     than either padding. */
  .contact__content { gap: clamp(28px, 4.2vh, 40px); }
  .contact__badges  { padding-top: clamp(28px, 4.2vh, 40px); }
}

/* ── 1024–1279: the container goes fluid, and the team stops shrinking ─────
   Gutters and type already step at this width, in layout.css and
   components.css. The one thing needing a layout of its own is the team row.

   Its two columns split whatever the 80px gap leaves them, so the cards
   shrink with the container — and the design's 288px is only reached once the
   container hits its own 1280 cap, which is 1408px of viewport and up. At
   1280 the cards are 256 and at 1024 they are 204, and both wrap "David M.
   Perkal" mid-name.

   So this row stops being a row here rather than at 1023 with the others.
   The story is a paragraph and would rather have the full measure than half
   of it, and the pair beneath it holds the design's exact 288 + 24 + 288. */
@media (max-width: 1279px) {
  .section-title--wide {
    flex-direction: column;
    align-items: flex-start;
    gap: 48px;
  }

  .team__cards {
    flex: 0 0 auto;
    width: min(100%, 600px);
  }
}

/* ── Below 1024: every two-column row becomes one ──────────────────────────
   The scrub is off from here down — motion.js disarms below 1024 — so
   everything in this range is the static page, with the phones wherever
   ordinary layout puts them.

   A 405/843 pair at 1023px leaves the narrow card 324px wide holding a 32px
   heading, which is not a layout that ships. */
@media (max-width: 1023px) {
  /* The hero is 600 + 80 + 421.8 at the reference width, and the stage does
     not shrink — it is capped at 421.8 and centred. So the copy column is
     what gives way: 439px at 1023 for a 64px heading, and by 900 the stage
     starts shrinking too, which costs the one element on the page whose size
     *is* the product shot. Stack instead, and let both have the full width. */
  .hero {
    --hero-pad-top:    clamp(calc(var(--nav-clear) + 16px), 16vh, 160px);
    --hero-pad-bottom: clamp(48px, 8vh, var(--section-pad-md));
  }

  /* Stacked, the stage no longer competes with the copy for the window's
     height — it sits below it and the page scrolls, which it was always going
     to do here. Dropping the window ceiling is what stops a phone held
     sideways rendering a 200px product shot for a fold it is not trying to
     fit inside. */
  .hero__stage { width: min(100%, 421.8px); }

  .hero__inner {
    flex-direction: column;
    align-items: stretch;
    gap: 56px;
  }

  /* `flex: 1 1 0` in a column container is a zero basis in the *block*
     direction. These two find their content height back through
     min-height:auto, but stating it is cheaper than depending on it — and the
     rule below is what happens when that fallback is not available. */
  .hero__copy,
  .hero__media { flex: 0 0 auto; }

  /* The heading carries its own line breaks; the lede does not, and unpinned
     it runs the width of the page — 110 characters at 1023. */
  .hero__copy .lede { max-width: var(--max-width-md); }

  .features__row { flex-direction: column; }

  /* The same zero basis, and here it genuinely collapses: `min-height: auto`
     resolves to the content height only while `overflow` is visible, and
     .card sets `overflow: hidden` so it resolves to 0 instead. Every card in
     a stacked row was 2px of border with its whole content clipped away —
     four invisible cards, from 1023 down, for as long as this breakpoint has
     existed. */
  .features__row > .card { flex: 0 0 auto; }

  /* A card that was 405 or 843 wide is now the whole container, and its copy
     goes with it: 959px of 16px body is about 130 characters to a line, which
     is twice what the measure token exists to prevent. The cap is the same one
     every other run of text on the site takes. */
  .features__row > .card .card__text { max-width: var(--max-width-md); }

  /* The illustrations are drawn for the narrow column and take the card's full
     width, so at 959 each is 838px tall and the card becomes mostly artwork.
     Centred rather than left-aligned with the copy: they are drawn with their
     own margin on both sides and read as a plate, not as a figure. */
  .card__art {
    width: min(100%, 560px);
    margin-inline: auto;
  }

  .band {
    flex-direction: column;
    align-items: flex-start;
    gap: 48px;
  }

  /* The third instance of the same zero basis, and the one that hid a whole
     illustration: .band__media holds nothing in flow — its <img> is absolutely
     positioned — so an aspect-ratio is the only thing that can give it a
     height, and a flex basis of 0 in the block direction overrides it. The
     band clips (`overflow: hidden`), so there was not even a spilling image to
     notice. The adult band has been running without its artwork at every width
     below 1024. */
  .band__copy,
  .band__media { flex: 0 0 auto; }

  /* Stacked, the composition becomes its own row — but it still has to stay
     flush to the band's right edge and still has to be cropped top and bottom,
     and both for the same reason: the export has the band's right-hand corners
     baked into it in white. At full height, centred, all four show as notches.

     So the box keeps the desktop crop as a ratio (636 x 320 against the art's
     636 x 477, which is the same 33% the desktop band takes off it), clips it
     itself, and bleeds one --band-pad past the content edge so its right side
     lands on the band's border edge where the band's own radius sits. The cap
     is 636 + the bleed: past that the box would be upscaling a 636px asset. */
  .band__media {
    align-self: flex-end;
    width: min(calc(100% + var(--band-pad)), calc(636px + var(--band-pad)));
    margin-right: calc(-1 * var(--band-pad));
    min-height: 0;
    aspect-ratio: 636 / 320;
    overflow: hidden;
  }

  .band__media img {
    top: 50%;
    right: 0;
    width: 100%;
    height: auto;
    translate: 0 -50%;
  }

  .showcase {
    aspect-ratio: auto;
    background-size: 130% auto;
  }

  /* The copy block closes the artwork rather than sitting after it. The art is
     a background sized to the card's *width*, so where it ends is a fraction
     of the width while the copy's height is set by how the heading wraps —
     the two cross at about 800px and there is no single reserve that ends the
     card inside the image at every size. Fading the copy's own box from
     nothing to --ink covers the join wherever it falls: above the fade the art
     shows through, below it the card's own --ink continues with nothing to
     tell apart. 30% because at 390 the art runs out a third of the way down
     this block, and at 1023 it does not run out at all — what the fade covers
     there is the flat band the image ends in, which is that colour anyway. */
  .showcase__copy {
    position: static;
    padding: 32px 24px 48px;
    background-image: linear-gradient(to bottom, transparent, var(--ink) 30%);
  }

  /* With the copy back in flow the card has to be told to leave room for the
     artwork, which is a background rather than an element. 130% of the card
     width against the art's own 0.7005 ratio puts its full height at 91% —
     but the phones stop at 65.1% of that, and the rest is the flat band the
     copy was drawn to sit on. Reserving all 91% put the copy *below* the band
     instead of on it, which is where the ~300px of empty navy came from.

     62% is where the phones end plus a little air, and the copy's own top
     padding is the rest of it. The card then ends inside the artwork at every
     width down to about 800, so the band is what closes it rather than the
     card's background colour showing past the image. */
  .showcase::before {
    content: "";
    display: block;
    padding-top: 62%;
  }

  /* A heading beside a paragraph needs both to be readable; below this the
     pair is two narrow columns and neither is. */
  .section-title {
    flex-direction: column;
    align-items: flex-start;
    gap: 32px;
  }

  .section-title > .body-lg { flex: 1 1 auto; }

  .steps__grid { grid-template-columns: 1fr; }

  .contact__card { padding: 64px 32px; }

  /* The blur is an absolute length and the card is not, so it has to step down
     with it — at 150 on a 700px card the glow is the card. */
  .contact__glow { filter: blur(110px); }
}

/* ── Footer ────────────────────────────────────────────────────────────────
   Figma: Footer / 17 / — node 1414:205366.

   The two washes are the same pair the hero carries, mirrored: violet to the
   right, amber to the left, both anchored to the bottom edge. Their radii are
   percentages rather than the design's pixel figures so they keep their shape
   on a viewport that is not 1440 wide — at 1440 the two are the same number. */
.site-footer {
  padding-inline: var(--page-padding);
  padding-block: var(--section-pad-md) 40px;
  background:
    radial-gradient(99% 101% at 73.8% 100%, var(--wash-violet), transparent),
    radial-gradient(89% 97% at 25.6% 100%, var(--wash-amber), transparent),
    var(--surface);
}

.site-footer .container {
  display: flex;
  flex-direction: column;
  gap: 48px;
}

.site-footer__brand {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 32px;
  width: 100%;
  max-width: var(--max-width-sm);
  margin-inline: auto;
}

/* Figma: logo — node 1443:213172. 252x191, and the sphere's lower edge meets
   the wordmark's upper edge exactly, so the stack carries no gap. */
.site-footer__lockup {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-decoration: none;
}

.site-footer__lockup .logo-sphere   { width: 118px; height: 118px; }
.site-footer__lockup .logo-wordmark { width: 201px; height: 73px; }

.site-footer__links {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 32px;
}

/* The footer's link row is the eyebrow treatment, not a nav style of its own. */
.site-footer__links a {
  color: inherit;
  text-decoration: none;
  transition: color 0.15s ease;
}

.site-footer__links a:hover { color: var(--ink); }

.site-footer__credits {
  display: flex;
  flex-direction: column;
  gap: 32px;
}

.site-footer__legal {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 24px;
  font-family: var(--font-body);
  font-size: var(--text-sm);
  line-height: 1.5;
  color: var(--neutral-mid);
}

.site-footer__legal p { margin: 0; }

.site-footer__legal nav {
  display: flex;
  gap: 24px;
}

.site-footer__legal a {
  color: inherit;
  text-decoration: underline;
  text-underline-position: from-font;
}

.site-footer__legal a:hover { color: var(--ink); }

/* ── A centred single-message page ─────────────────────────────────────────
   Used by 404.html, and by index.html until Stage 2 replaces <main> with the
   real sections. It clears the fixed nav on its own, which the hero will not
   need to: the hero is designed with the pill floating over it. */
.plate {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 24px;
  min-height: 72vh;
  padding-block: 160px var(--section-pad-lg);
  text-align: center;
}

.plate p { max-width: var(--max-width-md); }

/* ── Below 768: one column, and the strip becomes a swipe ──────────────────
   The nav rules here pre-date the rest: a nav that overflows its own pill is
   not deployable at any stage, so Stage 1 owed them. The rest is Stage 4. */
@media (max-width: 767px) {
  .site-nav { width: calc(100% - 2 * var(--page-padding)); }

  .site-nav__bar { justify-content: space-between; }

  .site-nav__toggle { display: block; }

  .site-nav__links {
    position: absolute;
    top: calc(100% + 8px);
    right: 0;
    left: 0;
    flex-direction: column;
    align-items: stretch;
    gap: 4px;
    padding: 12px;
    border-radius: var(--radius-24);
    background: var(--card);
    box-shadow: var(--shadow-card);
    /* Closed rather than merely invisible: display:none keeps the links out of
       the tab order and off a screen reader's list without a second attribute
       to keep in sync with aria-expanded. */
    display: none;
  }

  .site-nav.is-open .site-nav__links { display: flex; }

  .site-nav__links > a:not(.btn) { padding: 10px 12px; }

  .site-nav__cta {
    width: 100%;
    /* The 4px row gap is right between two text links and too tight against a
       filled button, which reads as part of the list rather than the action. */
    margin-top: 8px;
  }

  /* The five claims are 1227px of text. Wrapped into a 350px column they are
     five centred lines that read as a paragraph rather than as a row, and the
     strip stops looking like the ribbon of proof it is. A scroller keeps it
     one row; the snap makes a swipe land on a claim instead of between two.

     This is now the *fallback* rather than the shipped behaviour: at this
     width the marquee is always armed, and what is left here is what a reader
     with JavaScript off or reduced motion asked for gets instead. Which is
     the right fallback — a row somebody can push is the honest version of a
     row that would otherwise move on its own.

     The gutter moves from the section onto the list so the row bleeds to both
     edges — a scroller that stops 20px short of the edge does not read as
     scrollable — and scroll-padding puts it back for the snap, so a snapped
     item sits on the gutter rather than against the glass.

     No tabindex: a scroll container with no focusable child is keyboard
     focusable on its own in current browsers, and adding one here would put a
     tab stop on a plain list at every width, not only this one. */
  .trust-strip { padding-inline: 0; }

  .trust-strip__list {
    flex-wrap: nowrap;
    justify-content: flex-start;
    overflow-x: auto;
    padding-inline: var(--page-padding);
    scroll-padding-inline: var(--page-padding);
    scroll-snap-type: x mandatory;
    /* The bar would sit on the hairline the section draws under itself. */
    scrollbar-width: none;
  }

  .trust-strip__list::-webkit-scrollbar { display: none; }

  .trust-strip__item {
    flex: 0 0 auto;
    scroll-snap-align: start;
  }

  /* Side by side on a 350px page each cell is 174 wide and the phone inside it
     85 — at which point the screenshot on its screen, the only thing the cell
     is there to show, is no longer legible. Stacked they are full width.

     The cell's aspect ratio is load-bearing and stays: the phone is positioned
     by percentage insets, so its own proportions come out of the cell's. */
  .split-cells { flex-direction: column; }

  /* And the same zero basis a fourth time: .split-cell clips its grain and its
     phone, so overflow is hidden here too and the pair collapsed to nothing
     the moment the row turned. */
  .split-cell { flex: 0 0 auto; }

  .hero {
    --hero-pad-top:    clamp(calc(var(--nav-clear) + 16px), 14vh, 128px);
    --hero-pad-bottom: clamp(40px, 7vh, var(--section-pad-md));
  }
  .hero__inner { gap: 40px; }

  .hero__badges,
  .showcase__badges { flex-wrap: wrap; justify-content: center; }

  /* At 32px the three of them need ~360px and wrap to two lines on a 390
     screen, which reads as a broken row rather than a deliberate one. */
  .site-footer__links { gap: 20px; }

  .site-footer__legal {
    flex-direction: column;
    align-items: center;
    text-align: center;
  }

  .plate { padding-block: 132px var(--section-pad-md); }

  /* 24px of gutter on each of three columns is 144px of a 350px table. The
     first column keeps its share — the labels are the only thing on the row
     that has to be read. */
  .compare__table th,
  .compare__table td { padding-inline: 16px; }

  .compare__table thead th:first-child { width: 46%; }

  /* The audience key is a dot, a gap and a word in a centred flex box that is
     narrower than the three of them from about 390px down — so it overflows
     evenly and the right-hand "Adult" ends flush against the card's edge.
     Taking the gap and the label down is what buys the padding back; the
     alternative was a narrower first column, which is where the row labels
     live and the only thing on the row that has to be read. */
  .compare__head { gap: 8px; }
  .compare__head .eyebrow { font-size: 12px; }

  /* Two 288px cards do not fit a 350px page, and the avatar alone is 128. */
  .team__cards { flex-direction: column; }

  .contact__card { padding: 48px 24px; }
  .contact__glow { filter: blur(80px); }
  .contact__row  { flex-direction: column; }
}
