/* bizav.ai — site stylesheet.
   Plain CSS, no build step. One block per section of the page, in the order
   the page runs; each block's comments carry the design reasoning (Figma node
   numbers, measurements at the 1440px design width, why a rule is load-bearing).
   Every section's arrival animation is keyed off a data-reveal attribute that
   assets/js/main.js writes, so a reader without JavaScript gets the page whole. */

/* ==================================================================
   index
   ================================================================== */

*,
*::before,
*::after {
  box-sizing: border-box;
}

html,
body {
  margin: 0;
  padding: 0;
}

html {
  /* iOS Safari inflates the text in some blocks on its own — "text
     autosizing" — which makes anything set not to wrap come out wider on a
     phone than it does anywhere else, and wide enough to push past the edge
     of the page. This holds the sizes the stylesheet actually asks for. */
  -webkit-text-size-adjust: 100%;
  text-size-adjust: 100%;

  /* And a hard stop on sideways scroll. Every section on this page is laid
     out to fit the viewport — measured at 320 through 440, nothing reaches
     the edge — but three of them host third-party scenes (Unicorn Studio,
     the shader canvases) that build their own canvas at whatever size they
     like, on the device rather than here. One of those spilling a few
     pixels past the right edge is enough to make the whole document wider
     than the window, and then every section's background, which is only as
     wide as the window, leaves a white strip down the side of the page.

     clip rather than hidden: hidden would turn the root into a scroll
     container, which changes how the fixed bar and the anchor jumps behave.
     clip just refuses to show anything outside the box. */
  overflow-x: clip;
}

body {
  font-family:
    "IBM Plex Sans",
    -apple-system,
    BlinkMacSystemFont,
    "Segoe UI",
    sans-serif;
  font-variation-settings: "wdth" 100;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  background-color: #ffffff;
  color: #08060d;
}

img,
svg {
  max-width: 100%;
}

/* --- rhythm between sections ---
   Each section's own padding-block is sized off its own Figma frame, and
   those frames were never laid out as one continuous page — stacked with
   nothing between them, most of the site read as one uninterrupted sheet:
   two consecutive white sections (or two black ones, Aira into Signals)
   share an edge with no seam at all, and even a colour change on its own is
   a hard cut rather than a pause. This is what gives every section a beat
   of plain space on both sides of that seam before its own padding starts,
   so a reader's eye gets a place to land between one section's close and
   the next one's opening line — the gap "most landing pages have" that was
   asked for.

   A margin rather than more padding on each section: padding is that
   section's own internal spacing, tuned to its own content; this is a
   property of the page's layout, so it belongs to the boundary between
   sections rather than to either one. The universal selector below is safe
   because every top-level section here is a bare <section> or <footer>
   with no nested one — confirmed, not assumed — so it can only ever match
   at the page's own seams. */
section + section,
section + footer {
  margin-top: clamp(2rem, 4vw, 3.5rem);
}

/* Aira into Signals is the one boundary where the margin looks wrong rather
   than restful: the gap is the body's own white, so it cuts a pale stripe
   between two sections that are both already black, and a reader sees that
   as a rendering seam rather than a paced beat. Every other same-colour
   run in the page is white into white, where that same margin is invisible
   against the page and simply reads as room. Flush here is what keeps
   those two reading as one dark block instead of two with a stray gap
   between them. */
.aira + .signals {
  margin-top: 0;
}

/* Hero into Companies is a deliberate join, not an accidental one: only
   Companies' bottom corners are rounded — see its own comment — because the
   design has it sitting flush against the hero above, the pale panel
   overlapping the video's own lower edge. The universal gap would put a
   band of plain white between the video and that panel, cutting apart a
   seam the design draws as one continuous piece.

   Reaches through .app__rest rather than a bare `.hero + .companies`: the
   audience switch's Owners view wraps everything past the hero in that div
   so the whole block can fade as one (see main.js), which puts it, not
   Companies, directly after the hero in the DOM. */
.hero + .app__rest > .companies {
  margin-top: 0;
}

/* The bar's own in-page links. Both rules below are page-level rather than
   any one section's: they exist because the bar is fixed and about 70px
   tall, which is a property of the page, not of the sections landed on. */

/* Without this the jump lands a section's top edge at the viewport's, which
   puts its first line under the glass. scroll-margin is taken off the jump
   itself rather than adding anything to the section's own box. */
#terminal,
#aira,
#signals,
#vision,
#owners-about,
#owners-how,
#owners-aira,
#owners-audience {
  scroll-margin-top: 5.5rem;
}

/* Glide rather than cut, for the jumps those links make — and only for
   whoever has not asked their system for less motion. Nothing else on the
   page scrolls the document programmatically; the chat log inside Aira
   scrolls itself, and carries its own rule for this. */
@media (prefers-reduced-motion: no-preference) {
  html {
    scroll-behavior: smooth;
  }
}

/* ==================================================================
   App
   ================================================================== */

/* The rest of the page, below the hero — hidden while the audience switch
   reads Owners, since that view is Hero-only for now. Always mounted (see
   the comment in main.js); this is purely what "hidden" means for it.

   Opacity only, on purpose: a max-height transition down to 0 was the first
   version, and it does not animate proportionally to a fixed duration the
   way opacity does — the box stays at its starting height for most of the
   transition-duration and only visibly collapses right at the end, since
   the timing function runs against the huge px range rather than against
   the page's own real height. Fading it in place, still scrollable while it
   does, is the honest version of "for a start" rather than a collapse that
   would have looked broken. */
.app__rest,
.app__owners {
  opacity: 1;
  transition: opacity 400ms ease;
}

.app__rest--hidden,
.app__owners--hidden {
  opacity: 0;
  pointer-events: none;
}

@media (prefers-reduced-motion: reduce) {
  .app__rest,
  .app__owners {
    transition: none;
  }
}

/* ==================================================================
   Nav
   ================================================================== */

.nav {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  /* Every measurement in the bar below is an em off this — the logo, the
     CTA, the gaps, the padding, the annotation. Change it and the whole bar
     resizes in proportion; each ratio is the design's own value over its
     original 16px, so the relationships are the ones from the design.
     Stated in rem, so nothing compounds down the tree. */
  --nav-text: 0.9rem;
  font-size: var(--nav-text);
  /* The design's 29px against a 16px item came to an 86px bar, which is a
     lot of the screen for a row 38px tall. This is the wordmark plus about
     14px of air on each side — enough that nothing reads as crowded against
     the edge, and no more. Padding rather than an offset: pinned to the top,
     an offset would leave a strip of the page showing above the glass.
     Symmetric, so the row stays centred in it. */
  padding-block: 1.0625em;
  /* The one thing that is not proportional: it is the hero's 40px inset,
     from the same expression, because the wordmark and the headline share a
     left edge in the design. */
  padding-inline: clamp(1.5rem, 2.78vw, 3.5rem);

  /* --- over the hero ---
     Glass from the very top of the page now, not from a scroll threshold:
     the bar sits on the video the whole way down it. The tint is what white
     type reads on and the blur is what makes it glass rather than a scrim —
     0.504 is the value tuned against the footage.

     Custom properties don't transition themselves, but the properties that
     consume them do: the computed colour changes and each element's own
     transition carries it. Both blur values below carry the same filter
     functions in the same order so they interpolate; a mismatched list
     would swap discretely instead. */
  --nav-bg: rgba(0, 0, 0, 0.099);
  /* The open menu's own ground, which cannot be --nav-bg: that tint is
     tuned for a bar with a row of type on it, and at the top of the page it
     is transparent outright. A panel of four rows over the hero's own
     footage needs to be a surface. */
  /* Nearly opaque on purpose. The bar above carries a backdrop-filter,
     which makes it a backdrop root — so a blur on the card inside it has
     nothing to sample (the page it would blur is outside that root) and
     comes out inert. Without a blur to soften it, a tint thin enough to
     see the hero through reads as murk over the footage rather than as a
     surface, so the card is a surface. */
  --nav-menu-bg: rgba(16, 16, 18, 0.96);
  /* What a row under the pointer takes, and the lift that separates the
     card from whatever is behind it. */
  --nav-menu-hover: rgba(255, 255, 255, 0.12);
  --nav-menu-shadow: 0 18px 44px rgba(0, 0, 0, 0.45);
  --nav-blur: blur(16px) saturate(120%);
  --nav-fg: #ffffff;
  /* The wordmark's own ink. It follows the bar's type over the hero, where
     both are white, but parts from it on the solid state below: the links
     read at #454545 by design, and a mark filled with that same grey goes
     pale against white in a way type at the same value does not. */
  --nav-logo: var(--nav-fg);
  --nav-fg-soft: rgba(255, 255, 255, 0.8);
  --nav-rule: rgba(255, 255, 255, 0.36);
  /* The CTA is the inverse of the type around it, and the fill layer reads
     these two the other way round to invert on hover. */
  --nav-cta-bg: #ffffff;
  --nav-cta-fg: #0a0a0a;
  /* A hairline on the glass too. 0.16 rather than the white state's 0.12
     because a white line at the same alpha reads fainter over a dark ground
     than a black one does over a pale one. */
  --nav-border: rgba(255, 255, 255, 0.16);

  background-color: var(--nav-bg);
  -webkit-backdrop-filter: var(--nav-blur);
  backdrop-filter: var(--nav-blur);
  border-bottom: 1px solid var(--nav-border);
  /* The hero's video is absolutely positioned and comes later in the document,
     so without this it paints straight over the bar. */
  z-index: 10;
  transition:
    background-color 300ms ease,
    border-color 300ms ease,
    -webkit-backdrop-filter 300ms ease,
    backdrop-filter 300ms ease;
}

/* --- at the very top ---
   No glass yet: transparent and borderless, so the page's very first frame
   is the hero's video with nothing laid over it. The moment the page moves
   even one pixel this gives way to the glass state above, which is why this
   overrides only the three properties that differ — everything else the
   glass block already set (the white type, the CTA colours) is exactly
   right here too, since the bar is still sitting on the same video.

   blur(0px) here rather than dropping backdrop-filter entirely: the glass
   state's blur(16px) needs a matching filter-function list to interpolate
   from, the same reason the solid state below carries blur(0) instead of
   none. */
.nav[data-top] {
  --nav-bg: transparent;
  --nav-blur: blur(0px) saturate(100%);
  --nav-border: transparent;
}

/* ...but an open menu is a surface whatever the bar behind it is doing, so
   it keeps its own ground and the rule that separates its rows. */
.nav[data-top] .nav__links[data-open] {
  --nav-border: rgba(255, 255, 255, 0.16);
}

/* --- past the hero ---
   Everything below the hero is white, so the bar turns white and the type on
   it inverts. The blur goes with it: there is nothing worth seeing through a
   bar the same colour as the page behind it, and it costs a compositing pass
   on every frame. */
.nav[data-solid] {
  --nav-bg: #ffffff;
  --nav-blur: blur(0) saturate(100%);
  --nav-fg: #454545;
  /* The page's own ink, which is what every other mark on a white ground
     here is set in. */
  --nav-logo: #0a0a0a;
  --nav-fg-soft: rgba(10, 10, 10, 0.8);
  --nav-rule: rgba(10, 10, 10, 0.36);
  --nav-cta-bg: #0a0a0a;
  --nav-cta-fg: #ffffff;
  --nav-menu-bg: #ffffff;
  --nav-menu-hover: rgba(10, 10, 10, 0.05);
  --nav-menu-shadow: 0 18px 44px rgba(10, 10, 10, 0.14);
  /* The only thing separating a white bar from a white page. */
  --nav-border: rgba(10, 10, 10, 0.12);
}

/* Three columns rather than flex's space-between: the link row sits dead
   centre of the bar this way regardless of how wide the wordmark or the
   actions end up, instead of centring in whatever space is left once the
   heavier side has taken its share. */
.nav__bar {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  gap: 1.5em;
}

/* --- logo --- */

.nav__logo {
  display: flex;
  align-items: center;
  /* Stated rather than left to auto-placement: the link row goes absolute
     on narrow bars, and an absolutely positioned grid child occupies no
     cell — so the actions would slide left into the middle column and the
     menu button would end up 47px short of the bar's own right edge. */
  grid-column: 1;
  justify-self: start;
  flex-shrink: 0;
}

.nav__logo-mark {
  display: block;
  /* The design's 152px against a 16px item. */
  width: 9.5em;
  /* A mask has no intrinsic size, so the file's own 106x33 is stated here
     rather than left to height: auto. Owners' own lockup is a different
     shape (194x46), set on the element itself as --logo-ratio — same
     pattern as the audience switch's --switch-icon-size. */
  aspect-ratio: var(--logo-ratio, 106 / 33);
  background-color: var(--nav-logo);
  -webkit-mask: var(--logo-mask, url("../img/bizavLogo.svg")) center /
    contain no-repeat;
  mask: var(--logo-mask, url("../img/bizavLogo.svg")) center / contain
    no-repeat;
  transition: background-color 300ms ease;
}

/* --- links --- */

.nav__links {
  display: flex;
  align-items: center;
  justify-self: center;
  /* The design's 36px. Shared with the products sub-list, so the split into
     two lists is invisible. */
  --nav-link-gap: 2.25em;
  grid-column: 2;
  gap: var(--nav-link-gap);
  margin: 0;
  padding: 0;
  list-style: none;
}

/* --- actions --- */

.nav__actions {
  display: flex;
  align-items: center;
  grid-column: 3;
  justify-self: end;
  /* The design's 35px — a shade tighter than the link gap, so the CTA reads
     as separate from "Log in" rather than a matching pair. */
  gap: 2.1875em;
}

.nav__signin {
  position: relative;
  display: block;
  padding-block: 0.4375em;
  font-weight: 500;
  font-size: var(--nav-text);
  line-height: normal;
  letter-spacing: -0.025em;
  color: var(--nav-fg);
  text-decoration: none;
  white-space: nowrap;
  transition: color 300ms ease;
}

.nav__signin::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0.25em;
  height: 1px;
  background-color: currentColor;
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform 260ms cubic-bezier(0.33, 0, 0.2, 1);
}

.nav__signin:hover::after,
.nav__signin:focus-visible::after {
  transform: scaleX(1);
}

/* Anchor for the annotation: it spans exactly this sub-list's width, the way
   the bracket spans the three product links in the design. */
.nav__products {
  position: relative;
}

.nav__products-list {
  display: flex;
  align-items: center;
  gap: var(--nav-link-gap);
  margin: 0;
  padding: 0;
  list-style: none;
}

.nav__link {
  position: relative;
  display: inline-flex;
  align-items: center;
  /* Between a mark and its word. Close enough that the two read as one
     label rather than as an icon with a link after it. */
  gap: 0.45em;
  /* Bare text leaves the target under the 24px minimum; this takes it to
     ~28px. Symmetric, so the row's centre stays put. */
  padding-block: 0.4375em;
  font-weight: 500;
  font-size: var(--nav-text);
  line-height: normal;
  letter-spacing: -0.025em;
  color: var(--nav-fg);
  text-decoration: none;
  white-space: nowrap;
  transition: color 300ms ease;
}

/* Drawn rather than faded in: a rule that arrives all at once reads as the
   link having changed state, one that sweeps reads as the pointer running
   along it. scaleX off a left origin, so it is a transform the compositor
   can carry rather than a width the layout has to re-resolve each frame. */
/* The rule between the products and what follows them. Drawn in the middle
   of the row's own gap rather than as a border on either neighbour, so
   neither item's box changes size and the row's spacing stays the design's.
   Its ink is the bar's, so it turns with the bar. */
.nav__divided {
  position: relative;
}

.nav__divided::before {
  content: "";
  position: absolute;
  left: calc(var(--nav-link-gap) / -2);
  top: 50%;
  width: 1px;
  /* A cap-height's worth — a rule as tall as the whole row would read as a
     column divider rather than a pause between two words. */
  height: 1.15em;
  transform: translateY(-50%);
  background-color: var(--nav-fg);
  opacity: 0.3;
  transition: background-color 300ms ease;
}

.nav__link-icon {
  display: block;
  width: var(--nav-icon-size, 1.1875em);
  height: var(--nav-icon-size, 1.1875em);
  flex-shrink: 0;
  /* The bar's own ink, so the marks turn with the text when the bar goes
     white over a scrolled page. */
  background-color: currentColor;
  mask-image: var(--nav-icon);
  mask-repeat: no-repeat;
  mask-position: center;
  mask-size: contain;
  -webkit-mask-image: var(--nav-icon);
  -webkit-mask-repeat: no-repeat;
  -webkit-mask-position: center;
  -webkit-mask-size: contain;
}

/* The anchor for the rule below, which is why the label is a box of its
   own. */
.nav__link-label {
  position: relative;
  display: block;
}

.nav__link-label::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  /* Clear of the descenders without dropping so far it reads as a separate
     rule sitting under the row. */
  bottom: 0.25em;
  height: 1px;
  background-color: currentColor;
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform 260ms cubic-bezier(0.33, 0, 0.2, 1);
}

/* Keyboard users get the same rule, not just the focus ring. */
.nav__link:hover .nav__link-label::after,
.nav__link:focus-visible .nav__link-label::after {
  transform: scaleX(1);
}

/* --- annotation --- */

.nav__annotation {
  /* The label lands at start + 1520ms + 720ms; the mark then holds for five
     seconds before the same three steps run backwards. */
  --annotation-out: calc(var(--annotation-start, 0ms) + 2240ms + 5000ms);
  position: absolute;
  /* The design leaves 16px between the link text and the bracket, less the
     0.4375em the sub-list runs past that text for the hit area. */
  top: calc(100% + 0.5625em);
  left: 0;
  right: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  /* It hangs over the hero, where it must not swallow clicks. */
  pointer-events: none;
}

.nav__annotation-bracket {
  position: relative;
  width: 100%;
  height: 2em;
}

/* One hairline each, all three the same real pixel whatever the bracket ends
   up measuring — which is the point of building it this way. */
.nav__annotation-stub,
.nav__annotation-rule {
  position: absolute;
  background-color: var(--nav-rule);
}

.nav__annotation-stub {
  top: 0;
  bottom: 0;
  width: 1px;
}

.nav__annotation-stub--left {
  left: 0;
}

.nav__annotation-stub--right {
  right: 0;
}

.nav__annotation-rule {
  left: 0;
  right: 0;
  bottom: 0;
  height: 1px;
}

.nav__annotation-arrow {
  display: block;
  width: 0.5em;
  height: 0.5625em;
  /* Half a pixel into the rule, the overlap the design has — the two read
     as one mark rather than as a triangle parked under a line. */
  margin-top: -0.5px;
}

.nav__annotation-arrow path {
  /* The design flattens this to a solid #B6AEA8 — sampled off the sky in
     that frame. Matching the rule keeps it honest over a video whose colour
     changes shot to shot, and lets it invert with the bar. */
  fill: var(--nav-rule);
}

.nav__annotation-label {
  margin-top: 0.09375em;
  font-weight: 400;
  font-size: var(--nav-text);
  line-height: normal;
  letter-spacing: -0.025em;
  color: var(--nav-fg-soft);
  text-shadow: 0 1px 3px rgba(0, 0, 0, 0.35);
  white-space: nowrap;
}

/* --- annotation intro ---
   One sequence: the bracket draws, the arrow drops out of it, the label
   types. Each delay is the previous step's end, so the three read as one
   gesture rather than three animations that happen to overlap.

   --annotation-start is set in JS from the hero's own timeline: the sequence
   only begins once the chips have retracted and the carousel has taken over,
   so the two never animate at the same time.

   Each element then runs the same keyframes a second time in reverse to
   leave, in the opposite order — label, arrow, bracket. The outgoing pass is
   forwards-only on purpose: a "both" fill would apply its start state during
   the delay and wipe out the incoming pass before it ever ran. */

/* Each piece is uncovered from the end the last one finished at, so the three
   read as one line travelling: down the left stub, across, up the right one.
   Played in reverse they retreat the way they came — which is why the
   retract below is these same three, reversed and in the opposite order. */
@keyframes nav-annotation-down {
  from {
    clip-path: inset(0 0 100% 0);
  }
  to {
    clip-path: inset(0 0 0 0);
  }
}

@keyframes nav-annotation-across {
  from {
    clip-path: inset(0 100% 0 0);
  }
  to {
    clip-path: inset(0 0 0 0);
  }
}

@keyframes nav-annotation-up {
  from {
    clip-path: inset(100% 0 0 0);
  }
  to {
    clip-path: inset(0 0 0 0);
  }
}

@keyframes nav-annotation-drop {
  from {
    opacity: 0;
    transform: translateY(-8px);
  }
  to {
    opacity: 1;
    transform: none;
  }
}

/* Revealed by clip rather than by width: the label is centred, and a growing
   box would slide it sideways as it filled. */
@keyframes nav-annotation-type {
  from {
    clip-path: inset(0 100% 0 0);
  }
  to {
    clip-path: inset(0 0 0 0);
  }
}

/* The 800ms the whole bracket used to take, split between the three by how
   much of it each one is: the stubs are 32 of the old path's 278 units, the
   rule the 214 between them. */
.nav__annotation-stub--left {
  animation:
    nav-annotation-down 92ms linear calc(var(--annotation-start, 0ms) + 400ms)
      both,
    /* Last out, as it was first in. */ nav-annotation-down 92ms linear
      calc(var(--annotation-out) + 1748ms) reverse forwards;
}

.nav__annotation-rule {
  animation:
    nav-annotation-across 616ms cubic-bezier(0.65, 0, 0.35, 1)
      calc(var(--annotation-start, 0ms) + 492ms) both,
    nav-annotation-across 616ms cubic-bezier(0.65, 0, 0.35, 1)
      calc(var(--annotation-out) + 1132ms) reverse forwards;
}

.nav__annotation-stub--right {
  animation:
    nav-annotation-up 92ms linear calc(var(--annotation-start, 0ms) + 1108ms)
      both,
    /* First out, as it was last in. */ nav-annotation-up 92ms linear
      calc(var(--annotation-out) + 1040ms) reverse forwards;
}

.nav__annotation-arrow {
  animation:
    nav-annotation-drop 320ms cubic-bezier(0.16, 1, 0.3, 1)
      calc(var(--annotation-start, 0ms) + 1200ms) both,
    nav-annotation-drop 320ms cubic-bezier(0.16, 1, 0.3, 1)
      calc(var(--annotation-out) + 720ms) reverse forwards;
}

.nav__annotation-label {
  /* One step per character of "Bizav.ai products". */
  animation:
    nav-annotation-type 720ms steps(17, end)
      calc(var(--annotation-start, 0ms) + 1520ms) both,
    /* Reversed, the clip closes from the right — characters come off the
       end, the way a backspace would take them. */
    nav-annotation-type 720ms steps(17, end) var(--annotation-out) reverse
      forwards;
}

/* --- CTA ---
   A plain white pill in the design now: the chevron accent belonged to the
   glass bar this replaced, and at 148x44 there is no room for one. */

.nav__cta {
  /* Containing block for the fill below. */
  position: relative;
  overflow: hidden;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  /* Fixed, the way the design's is — its own padding would not get it
     there. 44px against a 16px item. */
  height: 2.75em;
  padding-inline: 1.125em;
  border: 0;
  border-radius: 999px;
  background-color: var(--nav-cta-bg);
  box-shadow: 0 1px 6px rgba(0, 0, 0, 0.25);
  /* Regular, not the links' medium — the design sets it apart by ground,
     not by weight. */
  font: inherit;
  font-weight: 400;
  font-size: var(--nav-text);
  line-height: normal;
  letter-spacing: -0.025em;
  color: var(--nav-cta-fg);
  white-space: nowrap;
  cursor: pointer;
  flex-shrink: 0;
  transition:
    transform 150ms ease,
    background-color 300ms ease,
    color 300ms ease;
}

.nav__cta:active {
  transform: scale(0.98);
}

/* The black ground and the white label are one layer, revealed together.
   Transitioning the button's own colour against a separate fill cannot avoid
   a frame where the text is white on the half that is still white — clipping
   both at once has no such frame.

   Grown from the bottom-right corner: 150% because the far corner sits at
   sqrt(2) of the radius a percentage resolves against, so anything under
   ~142% would stop short of it. */
.nav__cta-fill {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: inherit;
  /* The bar's two CTA colours, read the other way round — which is what
     makes the hover an inversion in both states rather than a black wipe
     over a bar that is already black. */
  background-color: var(--nav-cta-fg);
  color: var(--nav-cta-bg);
  clip-path: circle(0% at 100% 100%);
  transition:
    clip-path 420ms cubic-bezier(0.33, 0, 0.2, 1),
    background-color 300ms ease,
    color 300ms ease;
}

.nav__cta:hover .nav__cta-fill,
.nav__cta:focus-visible .nav__cta-fill {
  clip-path: circle(150% at 100% 100%);
}

/* --- waitlist cta ---
   The demo pill's second: same size and shape, drawn as an outline in the
   pill's own colour so "Request a demo" stays the one solid button. Hover
   fills it with that colour from the same corner the demo pill's sweep
   starts at. */

.nav__waitlist {
  position: relative;
  overflow: hidden;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  height: 2.75em;
  padding-inline: 1.125em;
  border: 1px solid var(--nav-cta-bg);
  border-radius: 999px;
  background-color: transparent;
  font: inherit;
  font-weight: 400;
  font-size: var(--nav-text);
  line-height: normal;
  letter-spacing: -0.025em;
  color: var(--nav-cta-bg);
  white-space: nowrap;
  cursor: pointer;
  flex-shrink: 0;
  /* The two pills are a pair: pulled in from the actions' 35px gap to 12px
     so they read as one group. The pill is gone below 560px, where that gap
     changes, so this needs no override there. */
  margin-inline-end: calc(0.75em - 2.1875em);
  transition:
    transform 150ms ease,
    border-color 300ms ease,
    color 300ms ease;
}

.nav__waitlist:active {
  transform: scale(0.98);
}

.nav__waitlist-fill {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: inherit;
  background-color: var(--nav-cta-bg);
  color: var(--nav-cta-fg);
  clip-path: circle(0% at 100% 100%);
  transition:
    clip-path 420ms cubic-bezier(0.33, 0, 0.2, 1),
    background-color 300ms ease,
    color 300ms ease;
}

.nav__waitlist:hover .nav__waitlist-fill,
.nav__waitlist:focus-visible .nav__waitlist-fill {
  clip-path: circle(150% at 100% 100%);
}

/* The waitlist's menu row: only where the pill itself has left the bar. A
   button, so it drops the UA's own box to sit like the link rows. */
.nav__menu-waitlist {
  display: none;
}

.nav__menu-waitlist .nav__link {
  width: 100%;
  border: 0;
  background-color: transparent;
  font-family: inherit;
  text-align: left;
  cursor: pointer;
}

@media (max-width: 559px) {
  .nav__waitlist {
    display: none;
  }

  .nav__menu-waitlist:not([hidden]) {
    display: block;
  }
}

/* --- owners download cta ---
   The design draws this bar with a different call to action entirely — the
   same white pill the Owners hero's own button is, not a relabelled version
   of "Request a demo". Sized off the bar's own --nav-text rather than the
   hero's fixed 44px, so it scales with the rest of the bar. */

.nav__download {
  display: inline-flex;
  align-items: center;
  gap: 0.5em;
  height: 2.75em;
  padding-inline: 1.125em;
  border: 0;
  border-radius: 999px;
  background-color: #ffffff;
  box-shadow: 0 1px 6px rgba(0, 0, 0, 0.25);
  font: inherit;
  font-weight: 400;
  font-size: var(--nav-text);
  letter-spacing: -0.025em;
  color: #0a0a0a;
  cursor: pointer;
  white-space: nowrap;
  flex-shrink: 0;
  transition: transform 150ms ease;
}

.nav__download:active {
  transform: scale(0.98);
}

.nav__download-dot {
  width: 0.25em;
  height: 0.25em;
  flex-shrink: 0;
  border-radius: 50%;
  background-color: #66e208;
}

.nav__download-icon {
  display: block;
  width: 1.25em;
  height: 1.25em;
  flex-shrink: 0;
}

/* --- focus ---
   The browser default ring is unreliable against a moving video, so every
   interactive element in the bar gets an explicit one. */

.nav__link:focus-visible,
.nav__signin:focus-visible,
.nav__logo:focus-visible,
.nav__cta:focus-visible,
.nav__waitlist:focus-visible,
.nav__download:focus-visible {
  outline: 2px solid #3b82f6;
  outline-offset: 3px;
}

/* --- responsive --- */

/* --- the menu button --- */

/* Hidden until the row folds. The same height as the CTA beside it, so the
   two read as one pair of controls rather than a button and a smaller
   afterthought, and coloured off the same --nav-* flags as everything else
   in the bar. */
.nav__burger {
  display: none;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0.3125em;
  width: 2.75em;
  height: 2.75em;
  padding: 0;
  border: 1px solid var(--nav-border);
  border-radius: 999px;
  background-color: transparent;
  /* A button starts on the UA's own font, not the bar's — which put this
     box at 37px against the CTA's 40 while both asked for 2.75em. */
  font: inherit;
  font-size: var(--nav-text);
  color: var(--nav-fg);
  cursor: pointer;
  flex-shrink: 0;
  transition:
    border-color 300ms ease,
    background-color 200ms ease;
}

.nav__burger-bar {
  display: block;
  width: 1.125em;
  height: 1.5px;
  border-radius: 2px;
  background-color: currentColor;
  transition:
    transform 240ms cubic-bezier(0.33, 0, 0.2, 1),
    opacity 140ms ease;
}

/* Into an X: the outer two rotate onto the middle's line, which goes. The
   translate is the gap plus the bar's own height, so both land dead centre
   whatever the type scale does to the em. */
.nav__burger[aria-expanded="true"] .nav__burger-bar:first-child {
  transform: translateY(calc(0.3125em + 1.5px)) rotate(45deg);
}

.nav__burger[aria-expanded="true"] .nav__burger-bar:nth-child(2) {
  opacity: 0;
}

.nav__burger[aria-expanded="true"] .nav__burger-bar:last-child {
  transform: translateY(calc(-0.3125em - 1.5px)) rotate(-45deg);
}

.nav__burger:hover,
.nav__burger[aria-expanded="true"] {
  background-color: var(--nav-menu-hover);
}

.nav__burger:focus-visible {
  outline: 2px solid #3b82f6;
  outline-offset: 3px;
}

/* --- narrow bars: the row becomes a card under the button ---
   880, not 720: with both pills the links no longer fit beside them below
   ~860 (MENU_BREAKPOINT in main.js matches). */

@media (max-width: 880px) {
  .nav__burger {
    display: flex;
  }

  /* A card hung off the button's own corner rather than a strip across the
     bar: it belongs to the control that opened it, and it only takes the
     room its rows need. Right-anchored and scaled from the same corner, so
     it reads as coming out of the button. */
  .nav__links {
    position: absolute;
    top: calc(100% + 0.5rem);
    right: clamp(1.5rem, 2.78vw, 3.5rem);
    left: auto;
    z-index: 1;
    flex-direction: column;
    align-items: stretch;
    /* Rows carry their own padding now — the row gap is just the seam
       between two tap targets. */
    gap: 2px;
    width: min(17rem, calc(100vw - 2 * clamp(1.5rem, 2.78vw, 3.5rem)));
    padding: 0.375rem;
    border: 1px solid var(--nav-border);
    border-radius: 18px;
    background-color: var(--nav-menu-bg);
    box-shadow: var(--nav-menu-shadow);

    /* Closed, but still in the DOM: opacity and transform animate, and
       display: none would not. visibility is what takes it out of the tab
       order and off the accessibility tree while it is closed. */
    opacity: 0;
    visibility: hidden;
    transform: translateY(-0.375rem) scale(0.97);
    transform-origin: top right;
    transition:
      opacity 180ms ease,
      transform 220ms cubic-bezier(0.33, 0, 0.2, 1),
      visibility 220ms;
  }

  .nav__links[data-open] {
    opacity: 1;
    visibility: visible;
    transform: none;
  }

  /* The brokers' three product links are a nested list; in a column it has
     to stack with the rest rather than beside it. */
  .nav__products-list {
    flex-direction: column;
    align-items: stretch;
    gap: 2px;
  }

  /* A bracket drawn around a horizontal run of three, pointing at them from
     below — none of which it is any more. */
  .nav__annotation {
    display: none;
  }

  /* Menu rows: a whole-row target that lights up, which is what a pointer
     expects of a menu. Bigger than the bar's own type, too — this is the
     page's navigation now, not a strip of labels. */
  .nav__link {
    padding: 0.75em 0.875em;
    border-radius: 12px;
    font-size: 1rem;
    transition:
      background-color 160ms ease,
      color 300ms ease;
  }

  .nav__link:hover,
  .nav__link:focus-visible {
    background-color: var(--nav-menu-hover);
  }

  /* The row's own ground says what is under the pointer here; the sliding
     rule was for labels sitting in a line with nothing else to show it. */
  .nav__link-label::after {
    display: none;
  }

  /* Stacked, an upright rule in the gap would stand beside the row rather
     than between the two groups. The same rule lies across instead, inset to
     where the menu's own rows begin — kept as the pseudo-element rather than
     swapped for a border so it carries the same ink and the same weight. */
  .nav__divided {
    margin-top: 0.5rem;
  }

  .nav__divided::before {
    left: 0.875em;
    right: 0.875em;
    top: -0.25rem;
    width: auto;
    height: 1px;
    transform: none;
  }
}

@media (max-width: 480px) {
  /* The bar is down to the wordmark, the CTA and the button by now, and on
     the Owners bar those three plus the design's own 35px gap come to more
     than a 390px screen has — the button ended up out in the bar's own
     right padding. The gap goes first, then the CTA's shoulders, then the
     wordmark, which is the piece with the most to spare at this size. */
  .nav__actions {
    gap: 0.5em;
  }

  .nav__cta,
  .nav__waitlist,
  .nav__download {
    padding-inline: 0.75em;
  }

  .nav__logo-mark {
    width: 7.5em;
  }

  /* The Owners pill is the wider of the two CTAs, and even after the trims
     above its group still ran 12px past the bar's own right edge — which
     pushed the button out with it, since a grid item wider than its column
     overflows the end rather than the start. The dot is the one piece in
     there carrying nothing: it is decorative, and the icon beside it
     already says "download". */
  .nav__download-dot {
    display: none;
  }
}

@media (prefers-reduced-motion: reduce) {
  .nav__cta:active,
  .nav__waitlist:active,
  .nav__download:active {
    transform: none;
  }

  /* Both states stay — they are what tells you what you are pointing at.
     Only the sweep and the wipe go. So does the bar's own crossfade. */
  .nav,
  .nav__logo-mark,
  .nav__link,
  .nav__cta,
  .nav__waitlist,
  .nav__download,
  .nav__link-label::after,
  .nav__cta-fill,
  .nav__waitlist-fill {
    transition: none;
  }

  /* The annotation still shows, it just arrives whole. */
  .nav__annotation-stub,
  .nav__annotation-rule,
  .nav__annotation-arrow,
  .nav__annotation-label {
    animation: none;
  }

  .nav__annotation-stub,
  .nav__annotation-rule {
    clip-path: none;
  }
}

/* ==================================================================
   Hero
   ================================================================== */

.hero {
  position: relative;
  width: 100%;
  /* svh, not vh: on mobile 100vh is measured against the *largest* viewport
     (URL bar hidden), so a vh hero always overflows on load. svh guarantees
     the frame fits without the jank dvh causes when the bar collapses.
     The px floor keeps the content from being crushed on landscape phones. */
  min-height: max(100vh, 420px);
  min-height: max(100svh, 420px);
  display: flex;
  align-items: flex-end;
  overflow: hidden;
  background-color: #08060d;
  /* Single source of truth for the body line and the chips that continue it.
     The chips size themselves in em off this, so the two can't drift apart. */
  --hero-body-size: 1.0625rem;
  /* Chips sit just under the body line so they read as tags inside the
     sentence rather than as peers of it. Every chip value is in em, so this
     one ratio drives the label, padding, gap and icon together — and it is
     also what lets the three of them be kept on one line on a narrow screen
     by this one number.

     The three at full size measure 331px across. That is 4px more than a
     375px phone leaves them and 59px more than a 320px one, so they broke
     to a second line and the intro animation was left retracting a chip
     from a row that wasn't there. Rather than a breakpoint that rearranges
     them, the size itself gives way: the vw term takes over below about
     396px — the width at which the row stops fitting — and hands back the
     few pixels needed, floored well above legibility. The cap is the
     original value, so nothing above that width changes at all. */
  --hero-chip-size: clamp(0.75rem, 3.8vw, calc(var(--hero-body-size) * 0.941));
}

/* --- background --- */

.hero__media {
  position: absolute;
  inset: 0;
  pointer-events: none;
}

/* Stacked rather than swapping one element's src: both stay decoded and
   playing, and the switch is only ever which one is opaque — a src change on
   a single <video> reloads the stream and drops a frame to black first. */
.hero__video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center top;
  display: block;
  opacity: 0;
  transition: opacity 500ms ease;
}

.hero__video[data-active] {
  opacity: 1;
}

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

/* One layer, matching the design: a shallow ramp from 30% to 50% black.
   The range is narrow enough that two stops don't band the way a scrim
   reaching full opacity at the bottom would. */
.hero__overlay {
  position: absolute;
  inset: 0;
  background-image: linear-gradient(
    to bottom,
    rgba(0, 0, 0, 0.3),
    rgba(0, 0, 0, 0.5)
  );
}

/* --- content ---
   Fluid values are tuned so the 1440px viewport lands on the Figma spec
   (64px title, 17px body, 40px inset, 58px bottom), then scale smoothly
   instead of stepping at breakpoints. */

.hero__content {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  /* Flat, not fluid: the design puts the same 16px between all four blocks
     and 16px is already the floor the fluid values elsewhere bottom out at. */
  gap: 1rem;
  padding-inline: clamp(1.5rem, 2.78vw, 3.5rem);
  padding-bottom: clamp(2.5rem, 4.03vw, 3.625rem);
  max-width: 100%;
}

/* --- audience stage ---
   Both panels — Brokers' and Owners' — share one grid cell, the same trick
   the testimonials carousel uses: the stage's own height follows whichever
   panel is taller instead of snapping to the other's when the switch fires,
   and the switch above never moves because it lives outside the stage
   entirely. */

.hero__stage {
  position: relative;
  display: grid;
  width: 100%;
  max-width: 100%;
}

.hero__panel {
  grid-area: 1 / 1;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  /* Both panels share this one cell so they can cross-fade in place, which
     means the cell is as tall as the taller of the two — the brokers' own,
     with its chips and its four stats. Packed from the top, the shorter
     Owners panel then floats with the difference under it: 177px of it at
     390, where the brokers' stats wrap into three rows. From the bottom,
     it sits on the hero's own floor either way. */
  justify-content: flex-end;
  gap: 1rem;
  min-width: 0;
  max-width: 100%;
  opacity: 0;
  transform: translateY(10px);
  transition:
    opacity 500ms cubic-bezier(0.16, 1, 0.3, 1),
    transform 500ms cubic-bezier(0.16, 1, 0.3, 1);
}

/* The one on show is the only one in the cell's flow, so the stage is as
   tall as it rather than as tall as the taller of the two. That is what
   brings the switch above down onto the Owners panel instead of leaving it
   parked at the height the brokers' own chips and stats need. The other
   panel is lifted out below — still painted, still fading, just not
   counted. */
.hero__panel[data-active] {
  position: relative;
  opacity: 1;
  transform: none;
  /* Above the outgoing panel while both are mid-fade, so the incoming one
     is what a reader sees the instant it is more opaque than the other
     rather than whichever happens to be later in the DOM. */
  z-index: 1;
}

/* Out of the flow, pinned to the same floor it would have sat on, so it
   fades out where it was rather than jumping as it leaves. */
.hero__panel:not([data-active]) {
  position: absolute;
  right: 0;
  bottom: 0;
  left: 0;
  pointer-events: none;
}

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

/* --- audience switch ---
   The selection itself lives in App now: switching to Owners fades the rest
   of the page out too (see main.js), which the hero alone had no way to
   reach — so the state moved up and this reads it back over props. */

.hero__switch {
  display: inline-flex;
  align-items: center;
  /* The 4px inset is the whole reason the selected pill reads as sitting
     inside a track rather than replacing it. */
  padding: 4px;
  border: 0.5px solid rgba(255, 255, 255, 0.2);
  border-radius: 999px;
  background-color: rgba(158, 158, 158, 0.06);
  /* Same glass as the product chips below — one blur radius across the hero. */
  -webkit-backdrop-filter: blur(6px);
  backdrop-filter: blur(6px);
}

.hero__switch-option {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 7px 18px;
  border: 0;
  border-radius: 999px;
  background-color: transparent;
  font: inherit;
  font-weight: 400;
  font-size: 1rem;
  line-height: normal;
  letter-spacing: -0.025em;
  color: rgba(255, 255, 255, 0.65);
  cursor: pointer;
  white-space: nowrap;
  transition:
    background-color 200ms ease,
    box-shadow 200ms ease,
    color 200ms ease;
}

.hero__switch-option[aria-pressed="true"] {
  background-color: #ffffff;
  box-shadow: 0 1px 6px rgba(0, 0, 0, 0.25);
  color: #0a0a0a;
}

.hero__switch-slot {
  /* The anchor for the note beside it. */
  position: relative;
  display: inline-flex;
}

/* The barred cursor, on the wrapper as well as on the button inside it. A
   disabled control is not a pointer target at all in some browsers — no
   hover, no events, its own cursor never used — and is in others, where it
   covers the wrapper entirely. Setting both is what makes the pointer read
   the same either way, and the wrapper is also what the note hangs off. */
.hero__switch-slot[data-soon] {
  cursor: not-allowed;
}

/* The note, beside the option. pointer-events: none so it can never sit
   between the cursor and the track underneath it. */
.hero__switch-tip {
  position: absolute;
  left: calc(100% + 14px);
  top: 50%;
  z-index: 2;
  transform: translateY(-50%);
  padding: 6px 14px;
  border: 0.5px solid rgba(255, 255, 255, 0.22);
  border-radius: 999px;
  /* Its own ground rather than the track's near-transparent glass: this has
     to be readable over whatever frame of the video is behind it. */
  background-color: rgba(10, 10, 10, 0.72);
  -webkit-backdrop-filter: blur(8px);
  backdrop-filter: blur(8px);
  font-size: 0.8125rem;
  line-height: normal;
  letter-spacing: -0.025em;
  color: rgba(255, 255, 255, 0.92);
  white-space: nowrap;
  opacity: 0;
  pointer-events: none;
  transition: opacity 160ms ease;
}

/* Only where there is a pointer to hover with. On a touch screen hover
   latches after a tap and there is no way to clear it. */
@media (hover: hover) {
  .hero__switch-slot[data-soon]:hover .hero__switch-tip {
    opacity: 1;
  }
}

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

/* Legible rather than faded out: it has to keep announcing the product, so
   it reads as a label with a status on it instead of a control waiting to be
   pressed — no pointer, no hover, and the dot and icon dimmed with it. */
.hero__switch-option:disabled {
  color: rgba(255, 255, 255, 0.45);
  /* The same barred cursor the wrapper carries. Whether a disabled button is
     the pointer's target at all differs between browsers, so the cursor is
     set on both: whichever of the two the browser hands the pointer to, it
     shows the same thing. */
  cursor: not-allowed;
}

.hero__switch-soon {
  /* Smaller and set apart, so "Owners" stays the word being read and this
     stays a note attached to it. In em, so it follows the option's own size
     down on a narrow screen. */
  font-size: 0.75em;
  letter-spacing: 0;
  opacity: 0.75;
}

.hero__switch-soon::before {
  /* Written in the markup as two separate words, so a screen reader says
     "Owners coming soon" rather than reading out a dash. */
  content: "· ";
}

.hero__switch-option:focus-visible {
  outline: 2px solid #3b82f6;
  outline-offset: 3px;
}

.hero__switch-dot {
  width: 4px;
  height: 4px;
  flex-shrink: 0;
  border-radius: 50%;
  background-color: #66e208;
  /* Present at all times now — see the comment in main.js — so switching
     is only ever this fading in on one button and out on the other, never a
     size change on either. */
  opacity: 0;
  transform: scale(0.4);
  transition:
    opacity 200ms ease,
    transform 200ms ease;
}

.hero__switch-dot[data-active] {
  opacity: 1;
  transform: none;
}

.hero__switch-icon {
  display: block;
  /* Brokers' mark is 18px and Owners' 16px in the design — the glyphs have
     different amounts of air, so one box size would read as two weights. */
  width: var(--switch-icon-size);
  height: var(--switch-icon-size);
  flex-shrink: 0;
  /* Masked, not an <img>: the exported files have their fill baked in, and
     the glyph has to invert with the option it sits in. */
  background-color: currentColor;
  -webkit-mask: var(--switch-icon) center / contain no-repeat;
  mask: var(--switch-icon) center / contain no-repeat;
}

/* --- owners download cta ---
   The design's own white pill, at its own 44px height rather than the
   switch option's — the two only happen to share a colour, not a size. */

.hero__download {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  height: 44px;
  padding-inline: 18px;
  border: 0;
  border-radius: 999px;
  background-color: #ffffff;
  box-shadow: 0 1px 6px rgba(0, 0, 0, 0.25);
  font: inherit;
  font-weight: 400;
  font-size: 1rem;
  letter-spacing: -0.025em;
  color: #0a0a0a;
  cursor: pointer;
  white-space: nowrap;
  flex-shrink: 0;
}

.hero__download-dot {
  width: 4px;
  height: 4px;
  flex-shrink: 0;
  border-radius: 50%;
  background-color: #66e208;
}

.hero__download-icon {
  display: block;
  width: 20px;
  height: 20px;
  flex-shrink: 0;
}

.hero__download:focus-visible {
  outline: 2px solid #3b82f6;
  outline-offset: 3px;
}

/* --- headline --- */

.hero__title {
  margin: 0;
  font-weight: 400;
  font-size: clamp(2rem, 4.44vw, 4rem);
  line-height: 1.125;
  letter-spacing: -0.025em;
  color: #ffffff;
  /* Locks the type to the surface without reading as a drop shadow. */
  text-shadow:
    0 1px 2px rgba(0, 0, 0, 0.25),
    0 2px 14px rgba(0, 0, 0, 0.2);
  overflow-wrap: break-word;
}

.hero__title span {
  display: block;
}

.hero__subtitle {
  display: flex;
  align-items: center;
  /* Wider than the chip-to-chip gap below, so the three chips read as one
     group rather than as three items trailing off the sentence. */
  gap: 1rem;
  flex-wrap: wrap;
}

.hero__subtitle-text {
  margin: 0;
  font-weight: 400;
  /* The chips size themselves off this, so the sentence and the tags
     continuing it can't drift apart. */
  font-size: var(--hero-body-size);
  line-height: 1.4;
  letter-spacing: -0.025em;
  color: rgba(255, 255, 255, 0.8);
  text-shadow: 0 1px 3px rgba(0, 0, 0, 0.35);
  max-width: 62ch;
  overflow-wrap: break-word;
  text-wrap: pretty;
}

/* --- product pills (Anti-Metal Button) --- */

.hero__products {
  display: flex;
  align-items: center;
  /* Shared with the emerge keyframes below, which offset each chip by its
     own width plus this gap. */
  --chip-gap: 0.75rem;
  gap: var(--chip-gap);
  flex-wrap: wrap;
  margin: 0;
  padding: 0;
  list-style: none;
}

.hero__pill {
  position: relative;
  display: inline-flex;
  align-items: center;
  /* font-size lives on the chip, not the label, so every em below resolves
     against the label's size — padding, gap and icon all scale together. */
  font-size: var(--hero-chip-size);
  gap: 0.5em;
  padding-block: 0.25em;
  /* 1em in the design. Trimmed a little so three chips fitting on a narrow
     screen doesn't come down to the last pixel — at 320 it buys them ten. */
  padding-inline: 0.85em;
  border: 0.5px solid rgba(255, 255, 255, 0.2);
  border-radius: 4px;
  background-color: rgba(158, 158, 158, 0.04);
  /* The fill is almost fully transparent by design — the glass read comes
     from blurring the video behind the pill, not from the background. */
  -webkit-backdrop-filter: blur(6px);
  backdrop-filter: blur(6px);
  overflow: hidden;
  white-space: nowrap;
}

.hero__pill-icon {
  /* Sized in em off the label. Aira and Signals get a larger box on
     purpose: one is a radial burst with empty space at the edges, the other
     a wave half as tall as it is wide, so at the same box size as Terminal
     both read visibly smaller. 1.3em is the label's own line box, which is
     what keeps them from driving the chip taller than the design's 30px. */
  width: var(--pill-icon-scale, 1.1875em);
  height: var(--pill-icon-scale, 1.1875em);
  display: block;
  flex-shrink: 0;
}

.hero__pill-label {
  font-weight: 500;
  line-height: normal;
  letter-spacing: -0.025em;
}

/* --- vertical carousel inside the first chip --- */

.hero__carousel {
  display: grid;
  align-items: center;
  /* The cell is as wide as the widest product, so the shorter ones would
     otherwise sit flush left with dead space trailing them. */
  justify-items: center;
}

.hero__carousel-item {
  /* All three products share one grid cell, so the chip is sized by the
     widest of them and holds still while they cycle. The chip's own
     overflow: hidden is what clips the ones parked off-frame. */
  grid-area: 1 / 1;
  display: inline-flex;
  align-items: center;
  gap: 0.5em;
}

.hero__carousel-item[data-slot="current"] {
  opacity: 1;
  transform: none;
}

.hero__carousel-item[data-slot="below"] {
  opacity: 0;
  transform: translateY(100%);
}

.hero__carousel-item[data-slot="current"],
.hero__carousel-item[data-slot="below"] {
  transition:
    transform 420ms cubic-bezier(0.16, 1, 0.3, 1),
    opacity 260ms linear;
}

/* No transition on the way back up. An item that just dropped out of the
   bottom gets recycled to the top, and animating that would drag it back
   across the chip in full view. */
.hero__carousel-item[data-slot="above"] {
  opacity: 0;
  transform: translateY(-100%);
  transition: none;
}

/* --- stats --- */

.hero__stats {
  /* stretch, so the dividers run the full height of the tallest column
     the way the single rule does in the design. */
  display: flex;
  align-items: stretch;
  flex-wrap: wrap;
  /* The design's 34px text-to-text spacing, kept whole: the rule is drawn
     in the middle of it rather than on a column's edge. */
  --stat-gap: clamp(1.25rem, 2.36vw, 2.125rem);
  gap: var(--stat-gap);
  margin: 0;
  padding: 0;
  list-style: none;
  text-shadow: 0 1px 3px rgba(0, 0, 0, 0.35);
}

.hero__stat {
  /* Anchor for the rule between columns. */
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0.25rem;
  min-width: 75px;
  text-align: center;
}

/* Centred in the gap rather than drawn as a border on the column's edge:
   box-sizing is border-box everywhere in this project, so a border plus the
   padding that balanced it were both being taken out of the 75px column —
   which pulled every rule after the first left of where the design has it. */
.hero__stat + .hero__stat::before {
  content: "";
  position: absolute;
  top: 0;
  bottom: 0;
  left: calc(var(--stat-gap) / -2);
  width: 1px;
  background-color: rgba(255, 255, 255, 0.18);
}

.hero__stat-value {
  font-weight: 600;
  font-size: 1rem;
  line-height: normal;
  letter-spacing: -0.025em;
  color: rgba(255, 255, 255, 0.9);
}

.hero__stat-label {
  font-weight: 400;
  font-size: 0.875rem;
  line-height: normal;
  letter-spacing: -0.025em;
  color: rgba(255, 255, 255, 0.6);
}

/* --- entrance --- */

@keyframes hero-rise {
  from {
    opacity: 0;
    transform: translateY(14px);
  }
  to {
    opacity: 1;
    transform: none;
  }
}

/* Each chip starts shifted left by its own width plus the gap, which lands
   it exactly inside the previous chip's footprint — so it begins fully
   occluded and slides out from underneath. */
@keyframes hero-chip-emerge {
  from {
    opacity: 0;
    transform: translateX(calc(-100% - var(--chip-gap)));
  }
  /* Opaque early: by this point the easing has already carried it clear of
     its predecessor, so it is seen emerging rather than fading in. */
  22% {
    opacity: 1;
  }
  to {
    opacity: 1;
    transform: none;
  }
}

/* The mirror of the above, for a chip that comes out of a parent standing to
   its right rather than its left: it starts a width and a gap the other way,
   so it begins occluded on that side instead. Only the phone reads it — see
   the breakpoint, where the row comes out of its middle. */
@keyframes hero-chip-emerge-left {
  from {
    opacity: 0;
    transform: translateX(calc(100% + var(--chip-gap)));
  }
  22% {
    opacity: 1;
  }
  to {
    opacity: 1;
    transform: none;
  }
}

/* Animated on the leaf elements, not on .hero__subtitle: a transform or an
   opacity below 1 on an ancestor makes that ancestor a backdrop root, which
   leaves the pills with nothing to blur until the animation settles. An
   element's own transform is fine.

   Scoped to the panel that starts active: both panels' titles exist in the
   DOM from the first frame — see .hero__stage below — and without this the
   Owners panel's copy would rise in too, unseen but not free, behind its
   own opacity: 0. */
.hero__switch,
.hero__panel[data-active] .hero__title,
.hero__panel[data-active] .hero__subtitle-text,
.hero__panel[data-active] .hero__stats {
  animation: hero-rise 700ms cubic-bezier(0.16, 1, 0.3, 1) both;
}

.hero__panel[data-active] .hero__subtitle-text {
  animation-delay: 120ms;
}

.hero__panel[data-active] .hero__stats {
  animation-delay: 240ms;
}

/* The descending z-index is what makes each chip read as coming *out of* its
   predecessor instead of sliding in over it. It also covers the fact that
   Owners is wider than Aira: at rest it overhangs to the left, and only
   Terminal sitting on top keeps that overhang hidden. */
.hero__pill:nth-child(1) {
  z-index: 3;
  animation: hero-rise 450ms cubic-bezier(0.16, 1, 0.3, 1) 180ms both;
}

.hero__pill:nth-child(2) {
  z-index: 2;
  animation: hero-chip-emerge 420ms cubic-bezier(0.16, 1, 0.3, 1) 560ms both;
}

.hero__pill:nth-child(3) {
  z-index: 1;
  animation: hero-chip-emerge 420ms cubic-bezier(0.16, 1, 0.3, 1) 820ms both;
}

/* Once the audience has been switched, the arrival is over: the panels
   cross-fade from here on, and re-running the rise on whichever panel just
   became active fights that fade — the incoming copy climbs 14px through it
   while the outgoing copy, having just lost the same animation, snaps back
   to where the animation was holding it. The chips go with it for the same
   reason: they would re-emerge from under each other on every return to
   Brokers. */
.hero[data-switched] .hero__panel[data-active] .hero__title,
.hero[data-switched] .hero__panel[data-active] .hero__subtitle-text,
.hero[data-switched] .hero__panel[data-active] .hero__stats,
.hero[data-switched] .hero__pill {
  animation: none;
}

/* --- retract ---
   --retract-x is measured in JS: each chip has to land behind the *first*
   chip, and the distance to it isn't something CSS can express. The
   per-chip "own width + gap" trick used on the way out won't do here,
   because both chips move at once — chip 3 would stop at chip 2's vacated
   slot instead of continuing behind Terminal. */

@keyframes hero-chip-retract {
  from {
    opacity: 1;
    transform: none;
  }
  to {
    opacity: 0;
    transform: translateX(var(--retract-x, 0));
  }
}

/* Reverse of the entrance order — the outermost chip tucks in first. */
.hero__products--retract .hero__pill:nth-child(3) {
  animation: hero-chip-retract 420ms cubic-bezier(0.16, 1, 0.3, 1) both;
}

.hero__products--retract .hero__pill:nth-child(2) {
  animation: hero-chip-retract 420ms cubic-bezier(0.16, 1, 0.3, 1) 70ms both;
}

/* Dropped from layout, not just hidden: left in place they would keep the
   list three chips wide and push the whole row's wrap point out. */
.hero__products--carousel .hero__pill:nth-child(n + 2) {
  display: none;
}

/* Under reduced motion the phase never advances past "intro", so the three
   chips simply stand still. */
@media (prefers-reduced-motion: reduce) {
  .hero__switch,
  .hero__panel[data-active] .hero__title,
  .hero__panel[data-active] .hero__subtitle-text,
  .hero__panel[data-active] .hero__stats,
  .hero__pill {
    animation: none;
  }

  .hero__switch-option,
  .hero__switch-dot {
    transition: none;
  }

  .hero__carousel-item {
    transition: none;
  }
}

/* --- responsive --- */

@media (max-width: 700px) {
  /* Hung from the top rather than stood on the floor.
     
     The stack is 478 tall and the screen is not, and bottom-anchored the
     whole of that difference piles up in one place: above the switch, under
     the bar. At 390x844 it was 295px of it, a third of the screen, empty. Nor
     does it hold still — a taller phone puts more there and a shorter one
     less, so the one gap nobody wants is the one gap that moves.

     Measured from the top it is a number instead of a remainder: this padding
     less the bar, the same on every phone, and the slack falls to the bottom
     where there is a photograph to fall on. The stack keeps the floor it has
     on a desk, where the row is wide enough that there is no slack to speak
     of and none of this applies.

     The height stays a full screen on purpose. Shortening it to fit the stack
     reads as the tidier fix and is not: the section under this one arrives
     when it crosses a line 32% up from the bottom of the window, and a hero
     that ends above that line leaves it sitting just under the fold in its
     un-arrived state — a band of blank page under the photograph until
     something scrolls. */
  .hero {
    align-items: flex-start;
    padding-top: 8rem;
  }

  /* Centred, the way a phone hero is: with one column and no photograph to
     hang the copy off, a left-aligned block leaves a ragged column of white
     down the right of the screen and nothing to balance it. The switch above
     it and the row of stats below go with it — the whole thing reads as one
     centred stack rather than a centred headline over a left-aligned
     everything else. */
  .hero__content,
  .hero__panel {
    align-items: center;
  }

  .hero__title,
  .hero__subtitle-text {
    text-align: center;
  }

  /* Both are rows that wrap: the sentence with its chips, and the chips
     among themselves. Centred, a line that wraps stays under the middle of
     the one above it. */
  .hero__subtitle,
  .hero__products,
  .hero__stats {
    justify-content: center;
  }

  /* Below this width the Figma line break stops being useful — let the
     headline wrap on its own and balance the resulting lines. */
  .hero__title span {
    display: inline;
  }

  .hero__title {
    text-wrap: balance;
  }

  /* The chips follow this down, so the whole subtitle block shrinks as one. */
  .hero {
    --hero-body-size: 1rem;
  }

  /* The row comes out of its middle here, not out of its left end.
     
     On a desk the three chips continue a sentence that runs left to right, so
     coming out of the first one and going back into it is the same direction
     the reading is going. Centred on a phone there is no such direction: the
     row is a shape in the middle of the screen, and a shape that builds
     itself off its left edge is lopsided the whole way out and then jumps
     when the other two drop from the layout and what is left re-centres.

     Out of the middle instead. The chip carrying the carousel is put in the
     middle of the row — it is the one that stays, so it is the one the other
     two should be arriving from and returning into — and its neighbours come
     out to either side of it and go back the same way. The row is balanced at
     every frame of it, and the chip left standing at the end is already very
     nearly where the row's own centre was, so there is next to nothing left
     to re-centre.

     Order alone does the arranging: nth-child still counts the document, so
     the rules above and the offsets measured in JS all go on meaning what
     they meant. The one thing that has to be told is that the left-hand chip
     now has its parent on the right, so it emerges the other way. And the
     stacking wants no help at all — the carousel chip already outranks both
     of them, which in the middle is exactly where that is wanted. */
  .hero__pill:nth-child(1) {
    order: 2;
  }

  .hero__pill:nth-child(2) {
    order: 1;
    animation-name: hero-chip-emerge-left;
  }

  .hero__pill:nth-child(3) {
    order: 3;
  }

  /* Above the track rather than past its right edge: there is no room
     beside it at this width, and a note running off the side of the screen
     would be clipped rather than read. */
  .hero__switch-tip {
    left: auto;
    right: 0;
    top: auto;
    bottom: calc(100% + 10px);
    transform: none;
  }

  /* The Owners option carries its status now, and the pair of them stopped
     fitting the narrowest screens — 325px against the 272 a 320px phone
     leaves. The track gives back the difference the same way the chips below
     it do: out of its own padding and type rather than by dropping a word. */
  .hero__switch-option {
    padding-inline: 11px;
    font-size: 0.9375rem;
  }

  /* A beat more than the panel's own 1rem between everything else. The
     sentence and the chips that finish it are one thought; the numbers under
     them are a separate claim, and at this width they are a block of four
     rather than a single line, so they need a little air to stop reading as
     a fourth line of the paragraph above.

     More air than that now the stack hangs from the top: 12px was measured
     against a block sitting on the floor of the screen, where whatever was
     left over showed above it. Hung from the top the leftover shows below
     instead, and there is a lot of it — 278px of photograph under the stats
     at 390x844 with the four of them still crowding the line above. This
     spends some of it on the one gap in the stack that was carrying the
     least. */
  .hero__stats {
    margin-top: 3.5rem;
  }

  .hero__stat-value {
    font-size: 1.2rem;
  }

  .hero__stat-label {
    font-size: 1rem;
  }
}

/* The stats break where they actually stop fitting, not with the rest of the
   phone layout: the row holds the shape the design gives it — one line, four
   columns — well below where the rest of this section goes to its phone
   layout. The break is set where about fifty pixels of slack are left rather
   than at the width where they last fit exactly: a font rendering a little
   wider on someone else's device should cost nothing.

   Measured rather than reckoned, which moved it. The row was taken to be
   421px on the grounds of four 75px columns and their rules, and 75 is a
   minimum rather than a width — the fourth stat's label is a sentence and it
   comes out 251 across, so the row is 536 and wants 584 of window. Set at
   520 the band from there to 584 was still on the flex row, wrapping the
   fourth onto a line of its own and leaving its rule hanging in the open air
   beside it with nothing on the other side. It fits from 590 up; this is
   that plus the slack. */
@media (max-width: 640px) {
  /* Three across and the fourth under them. Four across has never fitted on
     a phone — at 375 the row needs 440px — so the row has to break
     somewhere, and this is where it wants to: the first three are a word
     against a number and sit happily in a third of the width each, while the
     fourth is a sentence against a number and is the one that cannot. Two
     and two split them down the middle instead, which put that sentence in
     half a screen and broke it over four lines, and left the strongest
     figure on the page reading as one of a pair rather than as the line the
     three above it are building to.

     The alternatives were worse: shrinking all four to fit costs the
     legibility these numbers exist for, and hiding one behind a swipe would
     hide this one — read once on the way past and never returned to — behind
     a drag that starts at the very edge of the screen, where iOS takes it
     for a back gesture.

     Each stat centred in its own column and the block centred under the
     copy: it is part of the centred stack above it, not a table hanging off
     one edge of it. */
  .hero__stats {
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    align-items: start;
    justify-content: center;
    column-gap: var(--stat-gap);
    /* Twice the gap between the columns, and then some. The fourth has to
       read as set down under the three rather than as a fourth column that
       wrapped onto a second line, and at anything near the column gap that is
       exactly what it reads as. */
    row-gap: 2.5rem;
  }

  .hero__stat {
    align-items: center;
    min-width: 0;
    text-align: center;
  }

  /* The full width, so the sentence under it breaks where the measure wants
     rather than where a third of the screen forces it. */
  .hero__stat:nth-child(4) {
    grid-column: 1 / -1;
  }

  /* Between the three across and nowhere else. The + selector would also put
     one down the left of the fourth, which has nothing beside it to be
     divided from — a rule hanging in the open under the first column. */
  .hero__stat + .hero__stat::before {
    content: none;
  }

  .hero__stat:nth-child(2)::before,
  .hero__stat:nth-child(3)::before {
    content: "";
  }
}

/* ==================================================================
   Loading
   ================================================================== */

/* One screen, one clock. The composition is the design's 1440x705 frame
   ("Loading stage final — layout v2"), and it holds together only as a whole —
   so every measure in it, across and down alike, is a share of the viewport
   width, and the block is centred on the vertical rather than pinned to a
   share of a height that scales on its own. The loader covers an empty page,
   so there is no scrollbar to put vw and the container's own width out of
   step. */

.loading {
  position: fixed;
  inset: 0;
  z-index: 100;
  background-color: #ffffff;
  /* What the ground turns once the drawing is gone: the sky of the design's
     second screen, which is a photograph of a jet above cloud — this is its
     own blue, averaged off the band of sky across the top of that frame. One
     token, because the ground, the keyframe that takes it there and the pane
     that lands the dive must not be able to drift apart. */
  --sky: #ced4e1;
  /* And what it all washes to as the camera moves in on the letter: the
     brand's indigo, the colour the frame is standing in when the site takes
     it. */
  --brand: #2b2b8b;
  /* How far the camera goes into the hole in the "a" — see @keyframes
     loading-zoom. One number for every window: the lockup is measured in vw,
     so the hole and the frame grow together and this clears the corner of any
     of them. */
  --dive-scale: 250;
  /* How far past the left edge of the frame the box the drawing and the
     photograph are cropped into is allowed to reach. Nothing here, so the
     composition is shown as drawn; see the phone breakpoint for why it is not
     nothing there. Held as a positive length and negated where it is used:
     folded in already negative it would produce "100% - -71vh" in the width
     below, which is not valid arithmetic and silently drops the whole
     declaration. */
  --art-overhang: 0px;
  /* The sketch is bled past every edge of the frame, and the logo is scaled a
     couple of hundred times over on the way out; the screen crops both, it
     does not grow to fit them. */
  overflow: hidden;
  /* HANDOVER_MS in main.js. The two have to agree: the component unmounts
     on its own clock and this is what it is waiting for. */
  transition:
    opacity 600ms ease,
    visibility 0s linear 600ms;
}

.loading[data-leaving] {
  opacity: 0;
  /* Hidden rather than just transparent once it has faded, so it can't sit
     over the site swallowing clicks during the last frame. */
  visibility: hidden;
  pointer-events: none;
}

/* --- the drawing ---
   The design's background, bled to all four edges the way the design bleeds
   it: one pencil sketch of a jet over cloud, swept on left to right while the
   bar fills. It is a raster, so the sweep is a mask rather than a thousand
   paths: a soft front crosses the frame at an even speed, and every point it
   passes takes --line-ms to come up — the same arithmetic the per-path draw
   ran on, so it reads the same. */

/* Both pictures are cropped into this box rather than into the frame, and on
   a phone the box reaches out past the frame's left edge — see the breakpoint.
   Sliding the picture itself does not work and is worth saying why: an inline
   svg held at slice, and an <img> held at cover, are each exactly the size of
   their own element with the overspill already clipped inside it, so there is
   nothing there to slide into. Moving the element moves the hole it is seen
   through with it and takes the picture off the screen. Moving the box is
   what moves the crop. */
.loading__art {
  position: absolute;
  top: 0;
  bottom: 0;
  left: calc(-1 * var(--art-overhang));
  right: 0;
  width: auto;
}

/* The sketch is drawn on a 3:2 page and the window is whatever it is. Cover,
   so it fills the frame and lets the frame crop it — the same way the design
   runs it off all four edges — rather than letterboxing a drawing that has no
   edges to show. */
.loading__art img {
  display: block;
  width: 100%;
  height: 100%;
  max-width: none;
  object-fit: cover;
  /* Pencil on paper at full strength fights the headline over the wing; this
     holds it back to the weight the design's sketch sits at. */
  opacity: 0.42;
}

/* --- the photograph ---
   The design's second screen. It is the image the sketch was drawn from —
   same aircraft, same pose — so bringing it up under a drawing that is on its
   way out reads as the drawing becoming the thing, not as one picture wiping
   another.

   In the document from the first frame and held at nothing, so the browser
   has the whole load and the whole hold to fetch and decode it: it gets one
   chance to arrive, on the same 600ms as the logo's walk, and a decode at
   that moment would be a visible hitch. */
/* The same box as the drawing, off the same token, so the two are cropped
   alike and stay registered through the dissolve. */
.loading__photo {
  position: absolute;
  top: 0;
  left: calc(-1 * var(--art-overhang));
  /* Measured out rather than pinned to both edges. This is an <img>, and an
     absolutely positioned replaced element does not stretch between a left and
     a right the way the drawing's <div> does — it keeps its own size and sits
     at the left, which put the whole photograph off the side of the screen.
     So it is given the width the box works out to instead. */
  width: calc(100% + var(--art-overhang));
  height: 100%;
  /* The stylesheet caps every image and svg at the width of its container, so
     that width would otherwise be handed straight back — which is what put the
     photograph in a 390px box hanging off the left of a phone screen while the
     drawing beside it, being a <div>, took the full box. */
  max-width: none;
  /* Shot on the same 3:2 page as the sketch and cropped by the same rule, so
     the jet in the photograph lands exactly on the jet in the drawing. */
  object-fit: cover;
  opacity: 0;
}

/* Where the front is, as a share of the frame's width. Registered so it
   interpolates; unregistered, a custom property only ever jumps. */
@property --sweep {
  syntax: "<percentage>";
  inherits: false;
  initial-value: 0%;
}

/* Everything left of the front is drawn, the --soft behind it is still coming
   up. The front starts at the left edge and finishes --soft past the right,
   so the last of the drawing has had its full fade by the time it stops. */
.loading__art {
  --soft: var(--draw-soft, 25%);
  -webkit-mask-image: linear-gradient(
    to right,
    #000 calc(var(--sweep) - var(--soft)),
    transparent var(--sweep)
  );
  mask-image: linear-gradient(
    to right,
    #000 calc(var(--sweep) - var(--soft)),
    transparent var(--sweep)
  );
  animation: loading-draw var(--load-ms) linear both;
}

@keyframes loading-draw {
  from {
    --sweep: 0%;
  }
  to {
    --sweep: calc(100% + var(--soft));
  }
}

/* --- the copy ---
   348 wide at x=120, ranged left: logo, headline, then the bar 56 below it.
   It is there from the first frame — the bar has to report from the first
   frame, and the column reads as one block with it.

   Placed by the top of the logo rather than the top of the design's own box,
   which opens with a 40 spacer: 217 down the 705 frame is 135.5 above its
   middle, and the middle is the one line that holds when the frame doesn't. */

.loading__copy {
  position: absolute;
  left: 8.333vw;
  top: calc(50% - 9.41vw);
  width: 24.167vw;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
}

.loading__lockup {
  display: block;
  /* 215 of 1440 across. */
  width: 14.931vw;
  height: auto;
  max-width: none;
}

.loading__headline {
  margin: 0;
  font-weight: 400;
  /* 60px at the design width, on its own 64px leading. */
  font-size: clamp(1.75rem, 4.167vw, 3.75rem);
  line-height: 1.0667;
  letter-spacing: -0.015em;
  color: #000000;
  text-align: left;
}

.loading__line {
  display: block;
}

/* --- the bar ---
   348 wide, 14 above the line that reads it out. */

.loading__group {
  /* The design's own gap 56, off the headline's last line. */
  margin-top: 3.889vw;
  display: flex;
  flex-direction: column;
  gap: 14px;
  width: min(24.167vw, 100%);
}

.loading__track {
  width: 100%;
  height: 2px;
  border-radius: 1px;
  background-color: rgba(15, 15, 15, 0.12);
  overflow: hidden;
}

.loading__fill {
  width: calc(var(--progress, 0) * 100%);
  height: 100%;
  border-radius: 1px;
  background-color: #171717;
}

/* The label on the left, the figure hard against the right end of the track,
   the way the design sets them. */
.loading__row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  font-size: 0.875rem;
  line-height: 20px;
}

.loading__status {
  /* All the lines share one cell: the block is one line tall and as wide as
     the longest message, so nothing shifts as they cycle. */
  display: grid;
  justify-items: start;
  margin: 0;
  font-weight: 400;
  color: #6b7078;
  text-align: left;
}

.loading__message {
  grid-area: 1 / 1;
  transition:
    transform 320ms cubic-bezier(0.16, 1, 0.3, 1),
    opacity 200ms linear;
}

.loading__message[data-slot="current"] {
  opacity: 1;
  transform: none;
}

/* Spent lines leave upward and the next one waits below, so the strip only
   ever travels in one direction. */
.loading__message[data-slot="past"] {
  opacity: 0;
  transform: translateY(-8px);
}

.loading__message[data-slot="next"] {
  opacity: 0;
  transform: translateY(8px);
}

.loading__percent {
  flex-shrink: 0;
  font-weight: 500;
  color: #171717;
  /* The figure is read right to left as it climbs, and a tabular cell keeps
     the three widths it passes through from shuffling the label beside it. */
  font-variant-numeric: tabular-nums;
}

/* Below the design's proportions the copy stops fitting in a quarter of the
   frame. The drawing stays: it is bled and cropped either way, and it is the
   paper the screen is printed on. */
@media (max-width: 900px) {
  /* Both pictures are half as wide again as they are tall and a phone is the
     other way about, so the frame throws away most of their width and keeps the
     middle — which is wing and engine, with the cabin off the right-hand edge
     of the crop entirely.

     So the box they are cropped into is let out past the left of the frame.
     The picture then centres itself in a box wider than the screen, and the
     screen is looking at the right-hand end of it: the cabin comes in, and
     nothing can uncover, because the box still reaches both edges of the
     frame whatever this is set to.

     In vh because the crop is driven by height — the picture is scaled to
     cover the frame's height and its width follows — so the same number holds
     as phones get taller or shorter. */
  .loading {
    --art-overhang: 40vh;
  }

  /* The crop puts the cabin straight under the copy here, so the pencil
     steps back further to keep the status line readable over it. */
  .loading__art img {
    opacity: 0.3;
  }

  .loading__copy {
    left: 6%;
    right: 6%;
    top: 50%;
    width: auto;
    transform: translateY(-50%);
  }

  .loading__lockup {
    width: min(215px, 55%);
  }

  .loading__group {
    margin-top: 2rem;
    width: 100%;
  }
}

/* --- the exit ---
   The drawings retreat the way they came while everything but the logo
   softens out of focus — together, not one after the other. Then the logo
   walks to the middle of the screen and grows to the size the design's second
   screen draws it at, the photograph that screen is built on comes up under
   it, and the frame dives into the eye of the "a" in "bizav". --dx and --dy
   are measured in JS, because where the logo starts depends on where the copy
   wrapped.

   Nothing here may reuse a keyframe name the element is already running: same
   name means the browser updates that animation instead of starting one, and
   it keeps the start time it was given a stage ago, so a delay measured from
   the exit has already run out and the element snaps straight to the end. */

@keyframes loading-vanish {
  from {
    opacity: 1;
  }
  to {
    opacity: 0;
  }
}

/* The drawing is not taken away on the exit. It stands, fully drawn, while
   the photograph fades up over it — the draw animation's fill holds it. */

/* Blur and opacity together: fading alone reads as a dissolve, blurring alone
   as a smear. */
@keyframes loading-defocus {
  from {
    opacity: 1;
    filter: blur(0);
  }
  to {
    opacity: 0;
    filter: blur(12px);
  }
}

.loading[data-stage="exit"] .loading__headline,
.loading[data-stage="exit"] .loading__group {
  animation: loading-defocus var(--vanish-ms) ease-in both;
}

.loading[data-stage="exit"] .loading__lockup {
  position: relative;
  z-index: 1;
  /* The hole inside the first "a", the one in "bizav" — the counter, not the
     ink around it: a circle of radius 5.08 centred on 114.86, 27.07 of the
     asset's own 172x54. Scaling about that point pins it to the middle of the
     frame, so the ring of ink around it opens outwards past all four edges
     and what is left filling the screen is whatever the hole was showing —
     which by then is the brand's indigo. */
  transform-origin: 66.78% 50.13%;
  animation:
    /* Waits out the un-drawing: the screen is empty but for the logo before
       it moves. */
    loading-center var(--lift-ms) cubic-bezier(0.65, 0, 0.35, 1) var(--lift-at)
      both,
    /* Forwards-only: a "both" fill would apply its start state during the
       delay and cancel the walk before it ran. */
    loading-zoom var(--zoom-ms) cubic-bezier(0.55, 0, 1, 0.45) var(--zoom-at)
      forwards,
    /* Black to white as the photograph comes up under it. An inversion
       rather than a second asset: the mark is one flat ink on nothing, so
       inverting it turns that ink white and leaves the paper it is cut out of
       alone. On its own, longer clock: the shadow it picks up on the way
       holds the white mark off the bright sky while the dissolve is at its
       palest, and lets go once the photograph is fully in. */
    loading-flip var(--flip-ms) linear var(--colour-at) both;
}

@keyframes loading-flip {
  0% {
    filter: invert(0) drop-shadow(0 0 0 transparent)
      drop-shadow(0 0 0 transparent);
  }
  40%,
  80% {
    filter: invert(1) drop-shadow(0 1px 3px rgba(0, 0, 0, 0.6))
      drop-shadow(0 3px 16px rgba(0, 0, 0, 0.55));
  }
  100% {
    filter: invert(1) drop-shadow(0 0 0 transparent)
      drop-shadow(0 0 0 transparent);
  }
}

/* The walk, and the growth that comes with it: the design draws the mark 215
   across on the first screen and 269 on the second, so arriving in the middle
   and arriving at that size are one move.

   The second translate in each of these is the correction the origin costs.
   Everything here scales about the hole in the "a" rather than the middle of
   the box, which drags the box off centre by (0.5 - origin) x (1 - scale) of
   its own width — so that is what is given back, and at the end of the dive
   the whole of it is given back instead, which puts the hole itself dead
   centre, where it has to be for the frame to empty evenly. */

@keyframes loading-center {
  from {
    transform: translate(0, 0) scale(1);
  }
  to {
    transform: translate(var(--dx, 0), var(--dy, 0)) translate(4.215%, 0)
      scale(1.2512);
  }
}

@keyframes loading-zoom {
  from {
    transform: translate(var(--dx, 0), var(--dy, 0)) translate(4.215%, 0)
      scale(1.2512);
  }
  to {
    transform: translate(var(--dx, 0), var(--dy, 0)) translate(-16.78%, 0)
      /* How far through. The hole is some 6px across at rest, so this opens
         it to a couple of thousand of radius — well past the corner of any
         frame it will be shown in, which is what puts the whole logo out of
         shot rather than leaving a ring of it sitting somewhere in view. */
      scale(var(--dive-scale));
  }
}

/* The photograph fades up over the finished drawing as the logo sets off.
   It is the image the sketch was drawn from — same aircraft, same pose, same
   crop — so the dissolve reads as the pencil turning into the real thing
   rather than one picture replacing another. The mark turns white on the
   same clock.

   Then the dive carries it: the camera goes into the letter and the sky is
   pushed in under it, so what opens past the ring is a frame already moving,
   and the site is handed that rather than a flat colour. Forwards-only, for
   the same reason the zoom is — a filled start state would cancel the fade
   that is still running when the delay begins. */
.loading[data-stage="exit"] .loading__photo {
  animation:
    loading-sky var(--colour-ms) ease-in-out var(--colour-at) both,
    loading-push var(--zoom-ms) cubic-bezier(0.33, 0, 0.15, 1) var(--zoom-at)
      forwards;
}

@keyframes loading-sky {
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
}

@keyframes loading-push {
  from {
    transform: scale(1);
  }
  to {
    transform: scale(1.28);
  }
}

/* The wash. It comes up over the photograph on the move's own clock, so the
   sky is gone by the time the camera settles and what is left standing is the
   mark on the brand's colour — which is also a colour the site can be handed
   from, where a photograph of a noon sky is not. Above the photograph by
   document order; under the mark, which lifts itself on z-index. */
.loading__wash {
  position: absolute;
  inset: 0;
  background-color: var(--brand);
  opacity: 0;
}

.loading[data-stage="exit"] .loading__wash {
  animation: loading-wash var(--zoom-ms) ease-in-out var(--zoom-at) both;
}

@keyframes loading-wash {
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
}

/* The flat ground turns with it. Nothing of it shows while the photograph is
   there — this is what the screen is wearing if the image never arrives. */
.loading[data-stage="exit"] {
  animation: loading-colour var(--colour-ms) ease-in-out var(--colour-at) both;
}

@keyframes loading-colour {
  from {
    background-color: #ffffff;
  }
  to {
    background-color: var(--sky);
  }
}

@media (prefers-reduced-motion: reduce) {
  /* The bar still fills — that is the information. Only the slide goes. */
  .loading__message {
    transition: opacity 200ms linear;
    transform: none;
  }

  /* One fade for the drawing rather than a sweep. */
  .loading__art {
    -webkit-mask-image: none;
    mask-image: none;
    animation: loading-fade var(--load-ms) ease-out both;
  }

  /* Only the blur goes; the copy still leaves on the same clock. */
  .loading[data-stage="exit"] .loading__headline,
  .loading[data-stage="exit"] .loading__group {
    animation-name: loading-vanish;
  }

  /* No dive: the second screen is simply arrived at and held. The turn from
     black to white stays — this rule replaces the whole shorthand, and the
     photograph comes up under the mark either way, so dropping it would leave
     a black mark on a blue sky. */
  .loading[data-stage="exit"] .loading__lockup {
    animation:
      loading-center var(--lift-ms) ease-out var(--lift-at) both,
      loading-flip var(--flip-ms) linear var(--colour-at) both;
  }

  /* The photograph still arrives — it is the design's own screen, not an
     effect. Only the camera move on it goes. */
  .loading[data-stage="exit"] .loading__photo {
    animation: loading-sky var(--colour-ms) ease-in-out var(--colour-at) both;
  }

  @keyframes loading-fade {
    from {
      opacity: 0;
    }
    to {
      opacity: 1;
    }
  }
}

/* ==================================================================
   Companies
   ================================================================== */

/* Node 243:24379 — a full-bleed strip under the hero, 37px tall: 8px of
   padding above and below a 16px line. The line and the three planes after
   it repeat across it and travel, left to right, without stopping. */

.companies {
  position: relative;
  /* The runs are wider than the screen on purpose — this is what keeps the
     overflow off the page. */
  overflow: hidden;
}

.companies__bg {
  position: absolute;
  inset: 0;
  background-color: #2b2b8b;
}

/* --- the loop ---

   Two identical runs side by side, moved by exactly one run's width and then
   snapped back. Because the second run starts where the first ends, the frame
   after the snap is identical to the frame before it, so the join cannot be
   seen. The distance is -50% of the track, which is one run whatever the runs
   turn out to measure — nothing here has to know the width.

   Right, not left: the strip reads as being pushed out from under the hero in
   the same direction the eye travels. Flipping it is the two percentages
   below, swapped. */
.companies__track {
  position: relative;
  display: flex;
  width: max-content;
  animation: companies-drift 60s linear infinite;
}

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

.companies__run {
  display: flex;
  align-items: center;
  /* The design's own 14px between the line and the planes, and the same again
     before the next line. */
  gap: 14px;
  padding-right: 14px;
}

.companies__item {
  display: flex;
  align-items: center;
  gap: 14px;
}

.companies__line {
  margin: 0;
  padding-block: 8px;
  /* Light — the same weight the rest of the page uses for a running line. */
  font-weight: 300;
  font-size: 1rem;
  line-height: normal;
  letter-spacing: -0.025em;
  color: #ffffff;
  white-space: nowrap;
}

.companies__planes {
  display: flex;
  align-items: center;
  /* 17px in the design, between the planes only. */
  gap: 17px;
}

.companies__plane {
  display: block;
  width: 16px;
  height: 16px;
  flex-shrink: 0;
  /* Painted rather than drawn: the glyph is a mask over the strip's own ink,
     so one file covers all thirty of them and they cannot drift in colour
     from the line they sit with. */
  background-color: #ffffff;
  mask-image: var(--companies-plane);
  mask-repeat: no-repeat;
  mask-position: center;
  mask-size: contain;
  -webkit-mask-image: var(--companies-plane);
  -webkit-mask-repeat: no-repeat;
  -webkit-mask-position: center;
  -webkit-mask-size: contain;
}

/* --- the strip arriving ---

   The bar is uncovered from its top edge down, as though it had been
   underneath the hero all along and the scroll pulled it out; the line
   follows, in focus from a blur, once the bar is most of the way there.

   Keyed off data-reveal, which main.js writes onto the section on
   mount: nothing here hides anything until the script that can show it again
   is running, so a reader without JavaScript gets the strip outright. "done"
   is set once, on the first crossing, and never unset — scrolling back up
   leaves it where it is. */

.companies[data-reveal] .companies__bg {
  /* Bottom edge held up against the top one. In percentages, so it does not
     need to know the strip's height. */
  clip-path: inset(0 0 100% 0);
}

.companies[data-reveal="done"] .companies__bg {
  clip-path: inset(0 0 0 0);
  /* On the shown state only, so arming is instant and only the arrival
     animates. */
  transition: clip-path 820ms cubic-bezier(0.16, 1, 0.3, 1);
}

.companies[data-reveal] .companies__track {
  opacity: 0;
  filter: blur(14px);
}

.companies[data-reveal="done"] .companies__track {
  opacity: 1;
  filter: blur(0);
  /* Starts while the bar is still opening, so the two read as one move rather
     than as two events. The travel is an animation and this is a transition,
     so the two run on the same element without either cancelling the
     other. */
  transition:
    opacity 700ms ease 420ms,
    filter 700ms ease 420ms;
}

@media (prefers-reduced-motion: reduce) {
  /* The strip is simply there, and stays still. A line of type moving under
     someone who has asked for less motion is the whole thing this setting is
     about. */
  .companies__track {
    animation: none;
  }

  .companies[data-reveal] .companies__bg,
  .companies[data-reveal] .companies__track {
    clip-path: none;
    opacity: 1;
    filter: none;
    transition: none;
  }
}

/* ==================================================================
   Comparison
   ================================================================== */

/* Fluid values land on the Figma spec at 1440px (72px above, 76px below,
   40px title, 18px lead, 1280px column, 40px panel padding) and scale from
   there instead of stepping at breakpoints. */

.comparison {
  /* The design's gaps to the section above and the one below, which are
     genuine air between the two boxes rather than either one's padding. */
  padding-block: clamp(2.5rem, 5vw, 4.5rem) clamp(2.75rem, 5.28vw, 4.75rem);
  /* Only reached below ~1360px — above it the column below is what sets the
     80px inset the design has. */
  padding-inline: clamp(1.5rem, 2.78vw, 3.5rem);
  background-color: #ffffff;
}

.comparison__inner {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2rem;
  max-width: 1280px;
  margin-inline: auto;
}

/* --- header --- */

.comparison__header {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 1rem;
  width: 100%;
}

.comparison__eyebrow {
  margin: 0;
  font-weight: 400;
  font-size: 0.625rem;
  line-height: normal;
  /* The design's 1px at 10px. In em so it survives a change of size. */
  letter-spacing: 0.1em;
  color: #0a0a0a;
  text-align: center;
}

.comparison__title {
  margin: 0;
  font-weight: 400;
  font-size: clamp(1.75rem, 2.78vw, 2.5rem);
  line-height: 1.3;
  letter-spacing: -0.025em;
  color: #0a0a0a;
  text-align: center;
  text-wrap: pretty;
}

.comparison__title span {
  display: block;
}

.comparison__lead {
  margin: 0;
  /* Where the design's own line break falls: at the design width this wraps
     after "actually goes" on its own. */
  max-width: 55rem;
  font-weight: 400;
  font-size: clamp(1rem, 1.25vw, 1.125rem);
  line-height: 1.5556;
  color: #4b5563;
  text-align: center;
  text-wrap: pretty;
}

/* --- panels --- */

.comparison__panels {
  /* The two panels arrive from the sides, and a box moved by a transform
     still counts towards the page's own scrollable width — armed, the right
     panel sat 40px past the right edge and made the document wider than the
     window until the section was scrolled to. Clipped here, they slide in
     from behind this row's edges instead, and the page never grows.

     clip rather than hidden: hidden would make this a scroll container. */
  overflow-x: clip;
  display: flex;
  /* Start, not stretch: the two panels hold their own heights in the design,
     and the right one is 4px taller because its heading carries a wordmark. */
  align-items: flex-start;
  gap: 1.5rem;
  /* The basis is what decides where they stack: two of them plus the gap
     need ~728px, so below that the row wraps into a column. */
  flex-wrap: wrap;
  width: 100%;
}

.comparison__panel {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 1.5rem;
  flex: 1 1 22rem;
  /* Without this the panel cannot shrink under its content's min-content
     width, and a long bullet would push the pair wider than the column. */
  min-width: 0;
  padding: clamp(1.5rem, 2.78vw, 2.5rem);
  border: 1px solid #e4e4e7;
  border-radius: 12px;
  background-color: #f6f6f7;
}

/* The one you are being sold sits on white inside a firmer border — the
   design's only two differences between the panels, besides the copy. */
.comparison__panel--bizav {
  border-color: #c9cbd1;
  background-color: #ffffff;
}

.comparison__panel-header {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.5rem;
  width: 100%;
}

.comparison__panel-name {
  /* Flex, because on the right panel the second half of the heading is the
     wordmark rather than a word. */
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin: 0;
  font-weight: 600;
  font-size: 1.5rem;
  line-height: 1.3333;
  letter-spacing: -0.01em;
  color: #4b5563;
}

.comparison__panel--bizav .comparison__panel-name {
  color: #0a0a0a;
}

.comparison__panel-logo {
  display: block;
  /* 113px in the design. Height stays auto so the aspect comes from the file
     instead of a second number to keep in sync. */
  width: 113px;
  height: auto;
}

.comparison__panel-note {
  margin: 0;
  font-weight: 400;
  font-size: 0.875rem;
  line-height: 1.4286;
  color: #8a8f98;
}

/* Held one step darker than the left panel's, which the design uses to keep
   this side reading as the live one. */
.comparison__panel--bizav .comparison__panel-note {
  color: #4b5563;
}

/* --- bullets --- */

.comparison__items {
  display: flex;
  flex-direction: column;
  gap: 0.875rem;
  width: 100%;
  margin: 0;
  padding: 0;
  list-style: none;
}

.comparison__item {
  display: flex;
  /* Start, so the marker stays on the first line of a bullet that wraps
     rather than drifting to the middle of it. */
  align-items: flex-start;
  gap: 1rem;
  font-weight: 400;
  font-size: 0.875rem;
  line-height: 1.4286;
  color: #4b5563;
}

.comparison__panel--bizav .comparison__item {
  color: #0a0a0a;
}

.comparison__icon {
  display: block;
  width: 18px;
  height: 18px;
  flex-shrink: 0;
}

/* --- responsive --- */

@media (max-width: 560px) {
  /* Below this the break after the comma stops helping — let the headline
     wrap on its own and balance the lines that come out. */
  .comparison__title span {
    display: inline;
  }

  .comparison__title {
    text-wrap: balance;
  }
}

/* --- the section arriving ---

   In the order it is read: the label and the line it introduces come in
   together, out of focus; the paragraph under them follows; then the two
   panels arrive from the sides they sit on — today's from the left, ours
   from the right — so the comparison assembles itself around the middle
   rather than appearing whole.

   Keyed off data-reveal, which main.js writes onto the section on
   mount: nothing here hides anything until the script that can show it again
   is running, so a reader without JavaScript gets the section outright.
   "done" is set once, on the first crossing, and never unset — scrolling
   back up leaves it where it is. */

.comparison[data-reveal] .comparison__eyebrow,
.comparison[data-reveal] .comparison__title,
.comparison[data-reveal] .comparison__lead {
  opacity: 0;
  filter: blur(14px);
  transform: translateY(10px);
}

.comparison[data-reveal] .comparison__panel {
  opacity: 0;
  filter: blur(10px);
  transform: translateX(var(--comparison-from));
}

/* Each from its own side. 40px: far enough to read as coming in from
   somewhere, short enough that it is never a slide across the page.
   
   The side is handed over as a value rather than written as two transform
   rules of its own: :first-child would make those rules more specific than
   the shared "transform: none" below, and the panels would come into focus
   forty pixels from where they belong and stay there. A custom property
   carries the difference without carrying the specificity. */
.comparison__panel:first-child {
  --comparison-from: -40px;
}

.comparison__panel:last-child {
  --comparison-from: 40px;
}

.comparison[data-reveal="done"] .comparison__eyebrow,
.comparison[data-reveal="done"] .comparison__title,
.comparison[data-reveal="done"] .comparison__lead,
.comparison[data-reveal="done"] .comparison__panel {
  opacity: 1;
  filter: blur(0);
  transform: none;
  /* On the shown state only, so arming is instant and only the arrival
     animates. */
  transition:
    opacity 700ms ease,
    filter 700ms ease,
    transform 700ms cubic-bezier(0.16, 1, 0.3, 1);
}

/* The beats. The label and the title share the first one — the label is that
   title's own, not a step before it. */
.comparison[data-reveal="done"] .comparison__lead {
  transition-delay: 260ms;
}

.comparison[data-reveal="done"] .comparison__panel:first-child {
  transition-delay: 520ms;
}

.comparison[data-reveal="done"] .comparison__panel:last-child {
  transition-delay: 700ms;
}

@media (prefers-reduced-motion: reduce) {
  /* No arrival at all rather than a slower one: the section is simply
     there. */
  .comparison[data-reveal] .comparison__eyebrow,
  .comparison[data-reveal] .comparison__title,
  .comparison[data-reveal] .comparison__lead,
  .comparison[data-reveal] .comparison__panel {
    opacity: 1;
    filter: none;
    transform: none;
    transition: none;
  }
}

/* ==================================================================
   Products
   ================================================================== */

/* Fluid values land on the Figma spec at 1440px (40px title, 18px card copy,
   1356px row, 442x523 visuals, 15px between cards) and scale from there
   instead of stepping at breakpoints. */

.products {
  /* No padding above: the section before this one already carries the gap in
     its own bottom padding, and its box ends exactly where this starts. */
  padding-bottom: clamp(4rem, 12.43vw, 11.1875rem);
  padding-inline: clamp(1.5rem, 2.78vw, 3.5rem);
  background-color: #ffffff;
}

/* --- header --- */

.products__header {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 1rem;
  /* The design's own 471px measure, which is what puts the headline's break
     where it is. */
  max-width: 471px;
  margin-inline: auto;
}

.products__eyebrow {
  margin: 0;
  font-weight: 400;
  font-size: 0.625rem;
  line-height: normal;
  /* The design's 1px at 10px. In em so it survives a change of size. */
  letter-spacing: 0.1em;
  color: #0a0a0a;
  text-align: center;
}

.products__mark {
  position: relative;
  display: block;
  width: 80px;
  height: 78.867px;
  flex-shrink: 0;
}

.products__mark-outer,
.products__mark-inner {
  position: absolute;
  display: block;
}

.products__mark-outer {
  top: 0;
  left: 0;
  width: 80px;
  height: 78.867px;
}

/* Concentric with the outer ring — the offsets are the design's, not a
   centring calculation, because the two rings are not the same shape. */
.products__mark-inner {
  top: 13.14px;
  left: 13.34px;
  width: 53.329px;
  height: 52.575px;
}

.products__title {
  margin: 0;
  font-weight: 400;
  font-size: clamp(1.75rem, 2.78vw, 2.5rem);
  line-height: 1.3;
  letter-spacing: -0.025em;
  color: #0a0a0a;
  text-align: center;
  text-wrap: pretty;
}

.products__title span {
  display: block;
}

.products__lead {
  margin: 0;
  font-weight: 400;
  font-size: 1rem;
  line-height: normal;
  letter-spacing: -0.025em;
  color: #0a0a0a;
  text-align: center;
}

/* --- cards --- */

/* Columns rather than a wrapping flex row. Wrapping let a card that broke
   onto a line of its own grow into the whole width — and since the panel
   inside it holds the design's 442/523 ratio, that width came straight back
   as height: the third card ran some three times as tall as the two above
   it. Fixed column counts give a broken card exactly one column, so it
   comes out the same size as its siblings whichever row it lands on. */
.products__list {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  /* Start, not stretch: the cards hold their own heights in the design, the
     first one running deeper because its copy is a line longer. */
  align-items: start;
  /* The design's own 15px, side to side. */
  column-gap: 15px;
  /* Stacked, that same 15px reads wrong: a card's own panel-to-copy gap is
     16px, so the seam between two cards would be the tighter of the two and
     the copy would stop belonging to the panel above it. */
  row-gap: clamp(2.5rem, 4vw, 3rem);
  /* Three 442px cards and two gaps. Wider than the header's measure, which
     is the copy's alone. */
  max-width: 1356px;
  /* The design's 40px under the header. */
  margin: 2.5rem auto 0;
  padding: 0;
  list-style: none;
}

.products__card {
  display: flex;
  flex-direction: column;
  gap: 1rem;
  min-width: 0;
}

.products__visual {
  position: relative;
  /* Definite, so the ratio only decides the height: left to stretch, WebKit
     resolves the other axis first and derives a narrower box from the
     content — see the same note on .metrics__card. */
  width: 100%;
  /* The design's 442x523. Ratio rather than a height, so the panel keeps its
     shape as the card narrows. */
  aspect-ratio: 442 / 523;
  border-radius: 16px;
  /* Figma's own placeholder grey, which is what shows through anywhere an
     image has not landed yet. */
  background-color: #d9d9d9;
  overflow: hidden;
}

/* Black in the design — the waveform sits on top of it there, and this is
   the panel it sits on. */
.products__visual--signals {
  background-color: #000000;
}

/* Where the design insets it: the full width of the panel, and the middle
   364 of its 523 — so 15.1% down and 69.6% tall.

   width, not a right offset: a canvas is a replaced element, so an absolutely
   positioned one with left and right both set still takes its intrinsic width
   and drops the right. The canvas sizes its drawing buffer from its own box,
   so that fed straight back into the layout and ran away. */
.products__waveform {
  position: absolute;
  left: 0;
  top: 15.1%;
  width: 100%;
  height: 69.6%;
}

/* The shader's own darkest colour, so a browser without WebGL — where
   neither canvas paints — leaves this card dark rather than placeholder
   grey. */
.products__visual--aira {
  background-color: #0d1b0f;
}

.products__shader {
  position: absolute;
  inset: 0;
}

/* Over the shader, and transparent around the logo it draws. Offset by half
   the 20% it gives up, so the smaller box stays centred in the panel — the
   width and height come from the props, since the component sets them inline
   and an inset here would be overridden on two sides and not the other two. */
.products__scene {
  position: absolute;
  top: 10%;
  left: 10%;
}

/* --- hover: the product itself, behind glass ---

   One layer carries both the blur over the card's own visual and the mockup
   drawn on top of it, so a single opacity takes the whole thing in and out.
   backdrop-filter goes from a zero blur rather than from none: none does not
   interpolate, and leaving a live blur under an invisible layer would cost a
   compositing pass on three cards for nothing.

   container-type is what the mockup's cqw sizing hangs off — see
   ProductPreview.css. */
.products__preview {
  position: absolute;
  inset: 0;
  container-type: inline-size;
  border-radius: 16px;
  background-color: rgba(0, 0, 0, 0);
  -webkit-backdrop-filter: blur(0);
  backdrop-filter: blur(0);
  opacity: 0;
  /* It sits over the panel's own canvases, which must keep their pointer
     events — the waveform tracks the cursor. */
  pointer-events: none;
  transition:
    opacity 420ms cubic-bezier(0.33, 0, 0.2, 1),
    background-color 420ms cubic-bezier(0.33, 0, 0.2, 1),
    -webkit-backdrop-filter 420ms cubic-bezier(0.33, 0, 0.2, 1),
    backdrop-filter 420ms cubic-bezier(0.33, 0, 0.2, 1);
}

/* Keyed off the card, not the panel, so running the pointer over the copy
   below holds the preview open too. Real pointers only: on touch the hover
   latches after a tap and there is no way to clear it. */
@media (hover: hover) {
  .products__card:hover .products__preview {
    background-color: rgba(0, 0, 0, 0.06);
    -webkit-backdrop-filter: blur(10px);
    backdrop-filter: blur(10px);
    opacity: 1;
  }
}

/* The mockup arrives after the glass rather than with it: it starts a tenth
   larger and out of focus, then settles as it fades in. Sequenced by delaying
   only the incoming transition — the resting rule below carries no delay, so
   leaving the card takes the content away first and the glass after it.

   The blur is in em, which resolves against .preview's cqw-based font-size,
   so it scales with the card like everything else in the mockup. */
.products__preview .preview {
  opacity: 0;
  transform: scale(1.1);
  filter: blur(0.8em);
  transition:
    opacity 300ms ease,
    transform 300ms ease,
    filter 300ms ease;
}

@media (hover: hover) {
  .products__card:hover .products__preview .preview {
    opacity: 1;
    transform: scale(1);
    filter: blur(0);
    transition:
      opacity 520ms cubic-bezier(0.33, 0, 0.2, 1) 260ms,
      transform 520ms cubic-bezier(0.33, 0, 0.2, 1) 260ms,
      filter 520ms cubic-bezier(0.33, 0, 0.2, 1) 260ms;
  }
}

@media (prefers-reduced-motion: reduce) {
  .products__preview,
  .products__preview .preview {
    transition: none;
  }

  /* No ramp means no reason to start it displaced. */
  .products__preview .preview {
    transform: none;
    filter: none;
  }
}

.products__shot {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.products__copy {
  display: flex;
  flex-direction: column;
  gap: 1rem;
  /* The design runs the copy to 415px inside a 442px card — a measure, not
     padding, so it only binds at the design width. */
  max-width: 415px;
}

.products__name {
  margin: 0;
  font-weight: 400;
  font-size: clamp(1rem, 1.25vw, 1.125rem);
  line-height: normal;
  letter-spacing: -0.025em;
  color: #0a0a0a;
}

.products__name strong {
  font-weight: 700;
}

.products__body {
  margin: 0;
  /* Light, which index.html loads — 300 is not one of the weights the
     rest of the site uses. */
  font-weight: 300;
  font-size: clamp(1rem, 1.25vw, 1.125rem);
  line-height: normal;
  letter-spacing: -0.025em;
  color: #0a0a0a;
}

/* --- the row coming in ---

   Each card and its own words arrive as one piece, blurred and a little low,
   one after the next. Keyed off data-reveal, which main.js writes onto
   the row on mount: nothing here hides a card until the script that can show
   it again is running, so a reader without JavaScript gets all three
   outright. "done" is set once, on the first crossing, and never unset —
   scrolling back up leaves them where they are. */

.products__list[data-reveal] .products__card {
  opacity: 0;
  filter: blur(14px);
  transform: translateY(14px);
}

.products__list[data-reveal="done"] .products__card {
  opacity: 1;
  filter: blur(0);
  transform: none;
  /* On the shown state only, so arming is instant and only the arrival
     animates. */
  transition:
    opacity 700ms ease,
    filter 700ms ease,
    transform 700ms cubic-bezier(0.16, 1, 0.3, 1);
}

/* The pause between them. Short enough to read as one gesture across the
   row rather than three separate events. */
.products__list[data-reveal="done"] .products__card:nth-child(2) {
  transition-delay: 160ms;
}

.products__list[data-reveal="done"] .products__card:nth-child(3) {
  transition-delay: 320ms;
}

@media (prefers-reduced-motion: reduce) {
  /* No arrival at all rather than a slower one: the cards are simply
     there. */
  .products__list[data-reveal] .products__card {
    opacity: 1;
    filter: none;
    transform: none;
    transition: none;
  }
}

/* --- responsive --- */

/* Where the three stopped fitting on their own before: 3 x 20rem plus the
   two gaps, and the section's own inset. Two columns from here, so the
   third card takes one of them rather than the row. */
@media (max-width: 1049px) {
  .products__list {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

@media (max-width: 700px) {
  /* One column, capped at the design's own card width — a column wider than
     that would come back as height through the panel's ratio again. The cap
     goes on the row rather than on the cards: an auto inline margin on a
     grid item turns off its stretch, which left each card sized to its own
     copy instead (442 for the longest, 415 for the other two). The row's
     own margin: auto above centres it. */
  .products__list {
    grid-template-columns: 1fr;
    max-width: 442px;
  }
}

@media (max-width: 560px) {
  /* Below this the break after "private" stops helping — let the headline
     wrap on its own and balance the lines that come out. */
  .products__title span {
    display: inline;
  }

  .products__title {
    text-wrap: balance;
  }
}

/* ==================================================================
   ProductPreview
   ================================================================== */

/* The three mockups are drawn at the design's 442px panel, in the pixels the
   design gives them. The panel is fluid, so rather than repeat every value as
   a percentage they hang off one anchor: this font-size is 10px at 442px, and
   every measurement below is the design's px divided by ten, in em. Change
   the panel's width and the whole mockup scales with it.

   cqw needs a container, which .products__preview declares. */
.preview {
  position: absolute;
  inset: 0;
  font-size: 2.2624cqw;
  /* IBM Plex Sans is the site's; the design draws these two in Archivo and
     SF Pro, which are the Terminal product's own faces. Substituted rather
     than loaded — two more families for a hover flourish is not a trade
     worth making. */
  font-family: inherit;
  line-height: normal;
  letter-spacing: normal;
}

/* --- shared --- */

.preview__panel {
  position: absolute;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
}

.preview__rule {
  width: 100%;
  height: 0.1em;
  flex-shrink: 0;
  background-color: rgba(255, 255, 255, 0.08);
}

/* --- 1. Terminal side menu --- */

.preview__panel--menu {
  top: 3em;
  left: 8.1em;
  width: 28em;
  height: 46.3em;
  gap: 0.4em;
  padding: 2.4em 1.6em;
  border: 0.1em solid rgba(255, 255, 255, 0.08);
  border-radius: 2.4em;
  background-color: rgba(0, 0, 0, 0.46);
  -webkit-backdrop-filter: blur(3.4em);
  backdrop-filter: blur(3.4em);
}

.preview__menu-logo {
  display: flex;
  width: 100%;
  padding: 0.4em 0 1.6em 1.2em;
  flex-shrink: 0;
}

.preview__menu-logo img {
  display: block;
  width: 20.4em;
  height: 4.6em;
}

/* The design closes the menu on a green rule rather than the white one it
   opens with. */
.preview__rule--foot {
  background-color: #1e2e22;
}

.preview__toggle-wrap {
  width: 100%;
  padding: 0.8em 0 1.2em;
  flex-shrink: 0;
}

.preview__toggle {
  display: flex;
  align-items: center;
  padding: 0.3em;
  border-radius: 1em;
  background-color: rgba(255, 255, 255, 0.05);
  overflow: hidden;
}

.preview__seg {
  display: flex;
  flex: 1 0 0;
  min-width: 0;
  align-items: center;
  justify-content: center;
  gap: 0.5em;
  padding-block: 0.7em;
  border-radius: 0.8em;
  font-weight: 500;
  font-size: 1.3em;
  color: #b5b5b5;
  white-space: nowrap;
}

.preview__seg--on {
  border: 0.1em solid rgba(255, 255, 255, 0.08);
  background-color: rgba(255, 255, 255, 0.08);
  font-weight: 600;
  color: #ffffff;
}

/* Sized in em off the segment's own label rather than the root, which is the
   relationship the design has. */
.preview__seg-icon {
  display: block;
  width: 1em;
  height: 1em;
  flex-shrink: 0;
}

.preview__row {
  display: flex;
  width: 100%;
  align-items: center;
  gap: 1.2em;
  padding: 0.9em 1.2em;
  border-radius: 1em;
  flex-shrink: 0;
  font-weight: 500;
  font-size: 1.5em;
  color: #b5b5b5;
  white-space: nowrap;
}

.preview__row--on {
  background-color: rgba(255, 255, 255, 0.05);
  font-weight: 600;
  color: #ffffff;
}

/* Both of these are em off the row's label, so the icon column lines up
   whatever the row's state. */
.preview__row-icon {
  display: block;
  width: 1.3333em;
  height: 1.3333em;
  flex-shrink: 0;
}

.preview__row-tile {
  display: flex;
  width: 1.7333em;
  height: 1.7333em;
  flex-shrink: 0;
  align-items: center;
  justify-content: center;
  border-radius: 0.4667em;
  background-color: #a3e635;
  overflow: hidden;
}

.preview__row-tile img {
  display: block;
  width: 1em;
  height: 1em;
}

/* --- 2. Aira chat --- */

/* The design's own gap is 2em and its panel comes out about 488 tall in a
   523 panel. The browser wraps the agent's reply to six lines where Figma
   fits five — same size, different metrics — so the rhythm has to absorb the
   extra line rather than let the suggestion pill clip off the bottom. */
.preview__panel--chat {
  top: 2.4em;
  left: 7.7em;
  width: 28.9em;
  gap: 1.3em;
  padding: 2.4em;
  border: 0.1em solid rgba(255, 255, 255, 0.16);
  border-radius: 2.8em;
  background-color: rgba(0, 0, 0, 0.32);
  -webkit-backdrop-filter: blur(3.8em);
  backdrop-filter: blur(3.8em);
  overflow: hidden;
}

.preview__chat-head {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  flex-shrink: 0;
}

.preview__chat-who {
  display: flex;
  align-items: center;
  gap: 0.4em;
  font-weight: 500;
  font-size: 1.7em;
  line-height: 1.4706;
  color: #fbfbfb;
  white-space: nowrap;
}

/* The burst is wider than its box in the design and cropped to it. */
.preview__chat-avatar {
  display: block;
  width: 1.2941em;
  height: 1.4118em;
  flex-shrink: 0;
  object-fit: cover;
}

.preview__chat-status {
  font-weight: 400;
  font-size: 1.2em;
  line-height: 1.4167;
  color: rgba(251, 251, 251, 0.55);
  white-space: nowrap;
}

.preview__rule--chat {
  background-color: rgba(255, 255, 255, 0.12);
}

.preview__bubble {
  margin: 0;
  flex-shrink: 0;
  padding: 1.1em 2em;
  border: 0.1em solid;
  font-weight: 400;
  font-size: 1.6em;
  line-height: 1.4375;
}

/* The square corner marks who is speaking: bottom-right for the person,
   bottom-left for the agent. */
.preview__bubble--user {
  align-self: stretch;
  border-radius: 1.375em 1.375em 0.25em 1.375em;
  border-color: rgba(255, 255, 255, 0.14);
  background-color: rgba(255, 255, 255, 0.12);
  color: #fbfbfb;
  -webkit-backdrop-filter: blur(1.375em);
  backdrop-filter: blur(1.375em);
}

.preview__bubble--ai {
  align-self: stretch;
  border-radius: 1.375em 1.375em 1.375em 0.25em;
  border-color: rgba(255, 255, 255, 0.1);
  background-color: rgba(255, 255, 255, 0.06);
  color: rgba(251, 251, 251, 0.92);
  -webkit-backdrop-filter: blur(1.375em);
  backdrop-filter: blur(1.375em);
}

.preview__chat-line {
  display: block;
  width: 100%;
  height: 0.1em;
  flex-shrink: 0;
}

.preview__chat-suggest {
  flex-shrink: 0;
  padding: 0.6em 1.4em;
  /* The design sets this one in SF Pro — the system face, so the system
     stack is the honest substitute rather than the site's own. */
  font-family:
    -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
  font-weight: 500;
  font-size: 1.3em;
  line-height: 1.5385;
  letter-spacing: -0.01em;
  color: #171717;
  background-color: #ffffff;
  border-radius: 1.2308em 1.2308em 1.2308em 0.3077em;
  white-space: nowrap;
}

/* --- 3. Signals aircraft cards --- */

/* The cards themselves are AircraftCard, measured in em off this root's own
   font-size — see AircraftCard.css. Only the stack is this panel's business. */
.preview__panel--signals {
  top: 8.3em;
  left: 4.8em;
  width: 35em;
  gap: 1.1em;
}

.preview__panel--signals .tail {
  width: 100%;
  flex-shrink: 0;
}

/* ==================================================================
   AircraftCard
   ================================================================== */

/* One em to ten design pixels throughout — see main.js. Nothing
   here sets a font-size or a width: both come from the caller, which is what
   lets the same card be 360x112 in one place and a tenth of that in another
   without a second set of numbers. */

.tail {
  display: flex;
  height: 11.2em;
  align-items: center;
  border: 0.1em solid rgba(255, 255, 255, 0.1);
  border-radius: 1.6em;
  background-color: rgba(22, 22, 22, 0.6);
  -webkit-backdrop-filter: blur(1em);
  backdrop-filter: blur(1em);
  overflow: hidden;
}

.tail__media {
  position: relative;
  width: 14.8em;
  height: 100%;
  flex-shrink: 0;
  overflow: hidden;
}

.tail__glow,
.tail__pool,
.tail__render {
  position: absolute;
  display: block;
}

.tail__glow {
  top: 0.8em;
  left: -0.1em;
  width: 15em;
  height: 9.6em;
}

.tail__pool {
  top: 7.2em;
  left: 2em;
  width: 10.8em;
  height: 1.6em;
}

.tail__render {
  top: 3.5em;
  left: 0.8em;
  width: 13.2em;
  height: 4.2em;
  object-fit: cover;
}

/* The divider is the media column's own right edge rather than an element:
   one less absolutely positioned hairline to keep in place. */
.tail__media::after {
  content: "";
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  width: 0.1em;
  background-color: rgba(255, 255, 255, 0.08);
}

.tail__body {
  display: flex;
  flex: 1 0 0;
  min-width: 0;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.8em;
  padding-inline: 1.6em;
  overflow: hidden;
}

.tail__name {
  font-weight: 600;
  font-size: 1.6em;
  line-height: 1.375;
  color: #ffffff;
  white-space: nowrap;
}

.tail__meta {
  max-width: 100%;
  font-weight: 400;
  font-size: 1.2em;
  line-height: 1.3333;
  color: rgba(255, 255, 255, 0.45);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.tail__figures {
  display: flex;
  align-items: center;
  gap: 0.8em;
  white-space: nowrap;
}

.tail__ticker {
  font-weight: 700;
  font-size: 1.6em;
  line-height: 1.25;
  /* The design's 4.48px at 16px. */
  letter-spacing: 0.28em;
  color: #ffffff;
}

.tail__change {
  font-weight: 500;
  font-size: 1.4em;
  line-height: 1.2857;
  color: #2daa00;
}

/* ==================================================================
   Aira
   ================================================================== */

/* The design draws this section a size larger than the rest of the site —
   a 56px title against 40px everywhere else, and 24px body against 17-18px.
   Everything below keeps the design's own ratios but reads them against the
   site's scale instead: eyebrow 10, title 40, sub 16, body 18, controls 14-15.
   That is the design's 24px body times 0.75, rounded to the sizes the other
   sections already use.

   Fluid values land on those at 1440px and scale from there rather than
   stepping at breakpoints. */

.aira {
  position: relative;
  padding-block: clamp(3rem, 5.56vw, 5rem) clamp(2rem, 3.89vw, 3.5rem);
  /* The design's 140 left and 139 right — a pixel apart, which is a slip
     rather than an intention, so both take the same value. */
  padding-inline: clamp(1.5rem, 9.72vw, 8.75rem);
  /* The image is the section's own; this is what shows through where it does
     not reach, and what the veil below fades into. */
  background-color: #000000;
  overflow: hidden;
}

/* --- background --- */

.aira__bg {
  position: absolute;
  inset: 0;
  pointer-events: none;
}

.aira__bg-shader {
  position: absolute;
  inset: 0;
}

/* The design's own gradient over it: black at the foot, clear at the head,
   which is what settles the section onto the white above it and the footer
   below. */
.aira__bg-veil {
  position: absolute;
  inset: 0;
  background-image: linear-gradient(to top, #000000, rgba(102, 102, 102, 0));
}

/* --- layout --- */

.aira__row {
  position: relative;
  display: flex;
  align-items: center;
  /* The copy column flexes to fill, so this changes nothing for it — it is
     the chat view that needs it: that column is fixed at the design's 558 and
     without this the burst would slide in beside it instead of holding the
     right edge it has in both designs. */
  justify-content: space-between;
  /* 97px at the design width, between a 575 column and a 489 one — the three
     come to exactly the 1161 the padding leaves. */
  gap: clamp(2rem, 6.74vw, 6.0625rem);
  /* The copy no longer fills the burst's height now that the type is smaller,
     so the row is as tall as the burst and the column centres in it. */
  flex-wrap: wrap;
  max-width: 1161px;
  margin-inline: auto;
}

.aira__copy {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: clamp(1.25rem, 1.94vw, 1.75rem);
  flex: 1 1 24rem;
  min-width: 0;
}

/* --- headline block --- */

.aira__head {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.5rem;
  /* The design's own measure for this block, which is what puts the break in
     the line below it. */
  max-width: 391px;
}

.aira__eyebrow {
  margin: 0;
  font-weight: 400;
  font-size: 0.625rem;
  line-height: normal;
  /* The same 0.1em the other two section eyebrows carry. */
  letter-spacing: 0.1em;
  color: #ffffff;
  white-space: nowrap;
}

.aira__title {
  margin: 0;
  font-weight: 700;
  /* 40px, the size the other section titles run at. */
  font-size: clamp(1.75rem, 2.78vw, 2.5rem);
  line-height: normal;
  /* The design's 9.8px at 56px, kept as a ratio — wide enough that the four
     letters read as a mark rather than as a word. */
  letter-spacing: 0.175em;
  color: #ffffff;
}

.aira__sub {
  margin: 0;
  font-weight: 400;
  font-size: clamp(0.9375rem, 1.11vw, 1rem);
  line-height: normal;
  letter-spacing: -0.025em;
  color: #ffffff;
}

.aira__body {
  margin: 0;
  font-weight: 400;
  /* 18px, the site's largest body size. */
  font-size: clamp(1rem, 1.25vw, 1.125rem);
  line-height: 1.5;
  letter-spacing: -0.025em;
  color: #ffffff;
  text-wrap: pretty;
}

.aira__body strong {
  font-weight: 700;
}

/* --- actions --- */

.aira__actions {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 1.25rem;
}

.aira__cta {
  display: inline-flex;
  align-items: center;
  gap: 0.75rem;
  /* Asymmetric on purpose: a hair around the orb, and clearance past the
     label, which is what centres the pair inside the pill. */
  padding: 7px 26px 7px 7px;
  border: 1px solid rgba(255, 255, 255, 0.16);
  /* A pill whatever the type does to its height. */
  border-radius: 999px;
  background-color: rgba(255, 255, 255, 0.12);
  font: inherit;
  font-weight: 400;
  font-size: clamp(0.875rem, 1.04vw, 0.9375rem);
  line-height: 1.4;
  color: #fbfbfb;
  cursor: pointer;
  white-space: nowrap;
  transition: background-color 200ms ease;
}

.aira__cta:hover {
  background-color: rgba(255, 255, 255, 0.18);
}

.aira__cta:focus-visible {
  outline: 2px solid #3b82f6;
  outline-offset: 3px;
}

/* Sized in em off the label, so the pill's height follows the type: the
   design's own ratio of 3.11 against its label, which here comes out near
   47px. The still I replaced was a solid burst and had to come down to 2.6
   to stop it shouting over the words; the live orb is sparse dots, so it
   carries the design's ratio without doing that — and it needs the room,
   since the package tunes its dot count and dot size for a 64px render
   rather than scaling them, so displaying it much smaller than that thins
   the dots towards sub-pixel.

   The class lands on the component's own canvas, and the !important is the
   package's own pattern for this: it writes width/height inline from its
   size preset, so a plain rule would lose to it. */
.aira__cta-orb {
  display: block;
  width: 3.11em !important;
  height: 3.11em !important;
  flex-shrink: 0;
}

/* The note and the badge read as one unit, so they sit closer to each other
   than the actions column's own 20px rhythm puts them. */
.aira__app {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.5rem;
}

/* A caption rather than body copy: a step under the paragraph above it, and
   dimmer, so it introduces the badge instead of competing with the pitch. */
.aira__app-note {
  margin: 0;
  font-weight: 400;
  font-size: clamp(0.8125rem, 0.97vw, 0.875rem);
  line-height: 1.45;
  letter-spacing: -0.01em;
  color: rgba(255, 255, 255, 0.68);
}

.aira__badge {
  display: block;
  border-radius: 8px;
}

.aira__badge img {
  display: block;
  /* The badge is Apple's artwork at 146px; 130 keeps it in step with the
     smaller pill above it without going under their guidance. */
  width: 130px;
  height: auto;
}

.aira__badge:focus-visible {
  outline: 2px solid #3b82f6;
  outline-offset: 3px;
}

/* The two stores side by side; they wrap under each other only when the
   column is too narrow to hold both. */
.aira__badges {
  display: flex;
  flex-wrap: wrap;
  gap: 0.625rem;
}

/* --- the burst and the labels orbiting it --- */

.aira__orb {
  position: relative;
  /* The whole visual's size, as a factor of the design's 489x527 — the scene
     fills this box, so this one number is the only thing to turn. Past 1.0
     the burst is deliberately larger than the design has it, which is what
     makes it the section's anchor rather than a peer of the copy. */
  --aira-orb-scale: 0.9;
  flex: 0 1 calc(489px * var(--aira-orb-scale));
  /* Ratio rather than a height, so the box keeps the design's shape as the
     column narrows. */
  aspect-ratio: 489 / 527;
  /* Centres it the moment it wraps onto a line of its own, and does nothing
     at all while it shares one: auto margins only take free space that is
     left after the flexible lengths resolve, and the copy beside it grows
     into all of it (flex-grow: 1 above). Alone on a line, this box does not
     grow — grow is 0 — so the line's whole remainder is free, and the two
     auto margins split it. The row's own justify-content cannot do this:
     space-between leaves a single item on a line at the start, which is
     what left the burst hugging the left edge between 1110px, where it
     wraps, and the 900px the old rule waited for. */
  margin-inline: auto;
  /* Not hidden: two of the labels reach past this box in the design, one of
     them past the section's own right inset. */
}

/* Clips the oversized frame to the slot, so the scene cannot paint across
   the copy column. The labels sit outside this, as direct children of the
   slot, and so keep the overhang the design gives them. */
.aira__orb-clip {
  position: absolute;
  inset: 0;
  overflow: hidden;
  /* Nothing in here is interactive. */
  pointer-events: none;
}

/* The scene draws its mark across the middle 0.636 of its canvas width and
   0.623 of its height, and it draws it 0.92% of the canvas width left of
   centre — measured off a render with the backgrounds hidden and the clip
   opened, stable from a threshold of 8 to 110. So the canvas is oversized and
   centred, which lands the mark on the slot's own bounds; that is what makes
   the labels' percentages — taken straight from the design, where the burst
   filled its box — fall on the mark.

   155% rather than 160: at 160 the mark came out 448 wide in a 440 slot and,
   with the scene's own leftward bias on top of that, overhung the clip's left
   edge by 10px — which the clip then sliced off as a flat vertical edge
   through the blades. 155 leaves the mark 434 wide, a 3px margin each side.
   The translate offset is what cancels the bias: a percentage there resolves
   against the frame's own width, so it holds at any size. */
.aira__orb-frame {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 155%;
  height: 155%;
  transform: translate(calc(-50% + 0.92%), -50%);
}

/* The embed's own div is in flow inside the frame, which is what it should
   be: it fills it. */
.aira__orb-scene {
  display: block;
}

.aira__label {
  position: absolute;
  /* Where on the orbit it hangs — see the table in main.js, which hands
     these over as properties so this stays overridable further down. */
  left: var(--aira-left);
  top: var(--aira-top);
  display: flex;
  align-items: center;
  height: clamp(2.25rem, 3.06vw, 2.75rem);
  padding-inline: 18px;
  border: 1px solid rgba(255, 255, 255, 0.16);
  border-radius: 999px;
  background-color: rgba(0, 0, 0, 0.12);
  -webkit-backdrop-filter: blur(6px);
  backdrop-filter: blur(6px);
  font-weight: 400;
  font-size: clamp(0.8125rem, 0.97vw, 0.875rem);
  line-height: 1.4;
  color: #fbfbfb;
  white-space: nowrap;
  /* Duration and phase come from the chip's own data — see main.js. */
  animation: aira-float var(--aira-float, 5.5s) ease-in-out
    var(--aira-float-phase, 0s) infinite;
}

/* A percentage on translateY resolves against the chip's own height, so the
   drift stays in proportion to the chip however the section scales. 22% of a
   44px pill is about 10px. */
@keyframes aira-float {
  0%,
  100% {
    transform: translateY(0);
  }
  50% {
    transform: translateY(-22%);
  }
}

/* --- the Owners page's own arrangement of this section --- */

/* Node 215:21752 draws the same section stacked rather than side by side:
   the copy centred at the top, the burst centred under it, and no actions
   row at all (see the variant prop in main.js). Only the arrangement is
   the variant's — the type scale, the burst and its chips stay the
   brokers' own, so the two pages read as one section rather than two.

   The design's own figures here: 64 above and 70 below, 32 between the
   copy and the burst, 38 inside the copy, a 963 measure on the paragraph,
   and the burst at its full 489 rather than the 0.9 it comes down to when
   it has a column of copy to share the row with. */
.aira--owners {
  padding-block: clamp(2.5rem, 4.44vw, 4rem) clamp(2.5rem, 4.86vw, 4.375rem);
}

.aira--owners .aira__row {
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: clamp(1.25rem, 2.22vw, 2rem);
}

.aira--owners .aira__copy {
  /* flex-basis is the main axis, and the main axis is vertical now: the
     row's own `flex: 1 1 24rem` would read as a height here. */
  flex: 0 1 auto;
  align-items: center;
  gap: clamp(1.5rem, 2.64vw, 2.375rem);
  text-align: center;
}

.aira--owners .aira__head {
  align-items: center;
}

.aira--owners .aira__body {
  max-width: clamp(20rem, 66.88vw, 60.1875rem);
}

.aira--owners .aira__orb {
  --aira-orb-scale: 1;
  flex: 0 0 auto;
  width: calc(489px * var(--aira-orb-scale));
  /* Two of the chips hang past this box by design; the box itself is what
     has to stay inside the section. */
  max-width: 100%;
}

/* --- the section arriving ---

   Four beats, in the order the section reads: the ground opens out of the
   top-left corner, the copy comes in from the left behind it, the burst
   after that, then the chips one at a time down the orbit.

   Keyed off data-reveal, which main.js writes onto the section on mount —
   nothing here hides anything until the script that shows it again is
   running, so a reader without JavaScript gets the section outright.
   "pending" hides, "done" plays, and once the last chip has landed the
   section goes to "rested", where none of this applies any more: the
   clip-path in particular has to go, or the browser recomputes it on every
   change to this section's box — and the chat inside it changes that box on
   every line it adds. */

.aira[data-reveal="pending"] .aira__bg {
  clip-path: circle(0% at 0% 0%);
}

.aira[data-reveal="pending"] .aira__copy {
  opacity: 0;
  transform: translateX(-28px);
}

.aira[data-reveal="pending"] .aira__orb {
  opacity: 0;
  transform: scale(0.96);
}

.aira[data-reveal="pending"] .aira__label {
  opacity: 0;
}

/* 150% rather than 100: the radius is measured to the corner it starts
   from, and the far corner of a wide section is further than that. */
.aira[data-reveal="done"] .aira__bg {
  clip-path: circle(150% at 0% 0%);
  transition: clip-path 1100ms cubic-bezier(0.33, 0, 0.2, 1);
}

.aira[data-reveal="done"] .aira__copy {
  opacity: 1;
  transform: none;
  transition:
    opacity 700ms ease 450ms,
    transform 800ms cubic-bezier(0.16, 1, 0.3, 1) 450ms;
}

.aira[data-reveal="done"] .aira__orb {
  opacity: 1;
  transform: none;
  transition:
    opacity 700ms ease 950ms,
    transform 900ms cubic-bezier(0.16, 1, 0.3, 1) 950ms;
}

/* Opacity only, and no transform: these carry the float animation, and an
   animation wins over a transition on the same property — a transform here
   would simply be ignored. Their own delay comes in from the markup, in the
   order they hang on the orbit.

   Slow on purpose, and well spaced: 420ms each, 320ms apart, so one chip
   has all but finished before the next one starts and the four read as four
   separate arrivals down the orbit rather than as one group fading up. */
.aira[data-reveal="done"] .aira__label {
  opacity: 1;
  transition: opacity 420ms ease var(--aira-in, 0ms);
}

@media (prefers-reduced-motion: reduce) {
  /* The section is simply there. */
  .aira[data-reveal] .aira__bg,
  .aira[data-reveal] .aira__copy,
  .aira[data-reveal] .aira__orb,
  .aira[data-reveal] .aira__label,
  .aira[data-reveal="done"] .aira__bg {
    clip-path: none;
    opacity: 1;
    transform: none;
    transition: none;
  }
}

/* --- responsive --- */

/* The block that used to sit here centred the burst from 900px down. It is
   gone because the auto inline margin on .aira__orb does that job from the
   pixel it actually wraps at instead — see the comment there. Its other two
   declarations were doing nothing either: the burst wraps on its own well
   above 900px, and it shrinks rather than overflows below it. */

@media (max-width: 560px) {
  /* The labels used to be dropped here. They are not in the way of each other
     — the orbit spreads them far enough apart down the burst that the gaps
     between them stay clear at this width — and they are meant to be over the
     mark, which is what an orbit is. The one real problem was that they run
     off the right of the screen, and it is worth being exact about why: each
     is placed by a left offset taken from the design, and a left offset has no
     idea how wide the text sitting in it turns out to be. On a wide frame
     there is enough room to the right that it never shows. Here there is not,
     and the longest of them hangs over the edge.

     So the two that orbit to the right of the burst are hung from the right
     instead, where the edge they must not cross is the edge they are measured
     from and the length of the text stops mattering. The two on the left keep
     their own offsets, which have the same amount of room they always had. */
  .aira__label[data-side="right"] {
    left: auto;
    right: 0;
  }

  /* Centred. The column is the whole width of the screen here rather than one
     half of a row, and the burst under it is centred in that width — so a
     ragged left edge above it reads as two things that failed to line up
     rather than as a column beside a mark. Everything in the column goes,
     type and controls alike: the pill, the note under it and the badge are
     items of this same column, so centring it centres them.

     These are the three declarations the owners variant already uses for the
     same reason, that one being stacked and centred by design. */
  .aira__copy {
    align-items: center;
    text-align: center;
  }

  /* Every column in the column. The copy holds a head, an actions block and
     an app block, and each of those is a flex column of its own with its own
     alignment — centring only the outer one centres the boxes and leaves what
     is inside them ranged left. The actions block is the one that shows it:
     it is as wide as the app note under it, so the pill sat ten pixels left
     of the axis everything else had lined up on. */
  .aira__head,
  .aira__actions,
  .aira__app {
    align-items: center;
  }

  .aira__badges {
    justify-content: center;
  }
}

@media (prefers-reduced-motion: reduce) {
  .aira__label {
    animation: none;
  }
}

/* ==================================================================
   AiraChat
   ================================================================== */

/* The chat the CTA opens — node 178:16870, which is the same AIRA section
   with the copy column swapped for a transcript and a Back pill.

   The design draws this panel at 558px with 16-17px type, and unlike its
   headline block that is already the size the rest of the site runs at, so
   these values are the design's own rather than the 0.75 the section's
   headline and body were brought down to. */

.aira-chat {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 1.25rem;
  /* Takes the copy column's place: the design's 558, shrinking below that
     rather than growing past it. */
  flex: 0 1 558px;
  min-width: 0;
  animation: aira-chat-enter 420ms cubic-bezier(0.33, 0, 0.2, 1) both;
}

@keyframes aira-chat-enter {
  from {
    opacity: 0;
    transform: translateY(0.5rem);
  }
}

/* --- panel --- */

.aira-chat__panel {
  display: flex;
  width: 100%;
  flex-direction: column;
  align-items: stretch;
  padding: 24px;
  border: 1px solid rgba(255, 255, 255, 0.16);
  border-radius: 28px;
  background-color: rgba(0, 0, 0, 0.12);
  -webkit-backdrop-filter: blur(6px);
  backdrop-filter: blur(6px);
  overflow: hidden;
}

.aira-chat__head {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  flex-shrink: 0;
}

.aira-chat__who {
  display: flex;
  align-items: center;
  gap: 4px;
  font-weight: 500;
  font-size: 17px;
  line-height: 25px;
  color: #fbfbfb;
  white-space: nowrap;
}

/* The burst is wider than its box in the design and cropped to it. */
.aira-chat__avatar {
  display: block;
  width: 22px;
  height: 24px;
  flex-shrink: 0;
  object-fit: cover;
}

.aira-chat__status {
  font-weight: 400;
  font-size: 12px;
  line-height: 17px;
  color: rgba(251, 251, 251, 0.55);
  white-space: nowrap;
}

.aira-chat__divider {
  margin-top: 20px;
  height: 1px;
  flex-shrink: 0;
  background-color: rgba(255, 255, 255, 0.12);
}

/* --- the transcript --- */

/* Capped so the panel cannot outgrow the burst beside it: the row centres
   both columns on the taller one, and a transcript that kept growing would
   push the section — and the footer under it — down as it played. 300 leaves
   the panel, the gap and the Back pill at the burst's own height at 1440,
   and the opening exchange still fits without scrolling; only the follow-up
   scrolls, which main.js follows to the newest message.

   The scrollbar is hidden rather than styled — a track down the inside of a
   glass panel reads as a mistake, and nothing here asks to be scrolled by
   hand. */
.aira-chat__log {
  max-height: 300px;
  overflow-y: auto;
  overscroll-behavior: contain;
  scrollbar-width: none;
  /* The component moves scrollTop directly; this is what makes that a glide
     rather than a jump. */
  scroll-behavior: smooth;
  /* Once the follow-up pushes the opening off the top, the message up there
     is cut by the log's edge a few pixels under the divider. The fade is what
     makes that read as scrolled-away rather than as a clipping mistake; in
     the opening exchange, which never overflows, it falls entirely inside the
     first message's top padding and shows nothing. */
  -webkit-mask-image: linear-gradient(to bottom, transparent 0, #000 18px);
  mask-image: linear-gradient(to bottom, transparent 0, #000 18px);
}

.aira-chat__log::-webkit-scrollbar {
  display: none;
}

/* Each message unfolds rather than appearing at full height: a grid row
   interpolates 0fr to 1fr, which is the one way to animate to a height the
   content decides. The clip is what hides the part not yet unfolded. */
.aira-chat__reveal {
  display: grid;
  grid-template-rows: 1fr;
  animation: aira-chat-open 420ms cubic-bezier(0.33, 0, 0.2, 1) both;
}

.aira-chat__clip {
  min-height: 0;
  overflow: hidden;
}

/* The panel's 20px rhythm lives here rather than as a gap on the log, so the
   space a message takes and the space before it unfold together — a gap
   would snap into place a beat before the bubble it belongs to. */
.aira-chat__slot {
  padding-top: 20px;
  animation: aira-chat-rise 420ms ease both 120ms;
}

.aira-chat__slot[data-kind="user"] {
  display: flex;
  justify-content: flex-end;
}

.aira-chat__slot[data-kind="suggest"] {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 20px;
}

@keyframes aira-chat-open {
  from {
    grid-template-rows: 0fr;
  }
}

@keyframes aira-chat-rise {
  from {
    opacity: 0;
    transform: translateY(0.375rem);
  }
}

/* On its way out: the row folds back to nothing while the bubble fades, so
   the answer taking its place grows into the space it leaves. */
.aira-chat__reveal[data-exit] {
  animation: aira-chat-close 300ms cubic-bezier(0.4, 0, 1, 1) both;
}

.aira-chat__reveal[data-exit] .aira-chat__slot {
  animation: aira-chat-sink 220ms ease both;
}

@keyframes aira-chat-close {
  to {
    grid-template-rows: 0fr;
  }
}

@keyframes aira-chat-sink {
  to {
    opacity: 0;
    transform: translateY(-0.25rem);
  }
}

/* --- bubbles --- */

.aira-chat__bubble {
  margin: 0;
  padding: 14px 20px;
  border: 1px solid;
  font-weight: 400;
  font-size: 16px;
  line-height: 23px;
  -webkit-backdrop-filter: blur(6px);
  backdrop-filter: blur(6px);
  text-wrap: pretty;
}

/* The square corner marks who is speaking: bottom-right for the person,
   bottom-left for the agent. */
.aira-chat__bubble--user {
  width: 310px;
  max-width: 100%;
  border-radius: 22px 22px 4px 22px;
  border-color: rgba(255, 255, 255, 0.14);
  background-color: rgba(255, 255, 255, 0.12);
  color: #fbfbfb;
}

.aira-chat__bubble--ai {
  border-radius: 22px 22px 22px 4px;
  border-color: rgba(255, 255, 255, 0.1);
  background-color: rgba(255, 255, 255, 0.06);
  color: rgba(251, 251, 251, 0.92);
}

/* The agent's bubble, but only as wide as the words — the design has no
   thinking state, and a full-width bar would read as an answer that had
   arrived. */
.aira-chat__bubble--thinking {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding-block: 10px;
  border-radius: 22px 22px 22px 4px;
  border-color: rgba(255, 255, 255, 0.1);
  background-color: rgba(255, 255, 255, 0.06);
  color: rgba(251, 251, 251, 0.72);
}

.aira-chat__thinking-orb {
  display: block;
  flex-shrink: 0;
}

.aira-chat__hair {
  display: block;
  width: 100%;
  height: 1px;
  background-color: rgba(255, 255, 255, 0.18);
}

/* --- the suggestion the reader can send --- */

.aira-chat__suggest {
  padding: 6px 14px;
  border: 0;
  /* The design sets this one in SF Pro — the system face, so the system
     stack is the honest substitute rather than the site's own. */
  font-family:
    -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
  font-weight: 500;
  font-size: 13px;
  line-height: 20px;
  letter-spacing: -0.01em;
  color: #171717;
  background-color: #ffffff;
  border-radius: 16px 16px 16px 4px;
  cursor: pointer;
  white-space: nowrap;
  transition:
    transform 160ms ease,
    box-shadow 160ms ease;
}

.aira-chat__suggest:hover {
  transform: translateY(-1px);
  box-shadow: 0 6px 18px rgba(0, 0, 0, 0.35);
}

.aira-chat__suggest:disabled {
  cursor: default;
}

.aira-chat__suggest:focus-visible {
  outline: 2px solid #3b82f6;
  outline-offset: 3px;
}

/* --- back --- */

/* The glass pill the chips and the CTA are cut from, sized off its own label
   so it matches the button it replaced rather than the design's 18px — the
   section's type came down by a quarter and this sits in that column. */
.aira-chat__back {
  padding: 0.667em 1.334em;
  border: 1px solid rgba(255, 255, 255, 0.16);
  border-radius: 999px;
  background-color: rgba(0, 0, 0, 0.12);
  -webkit-backdrop-filter: blur(6px);
  backdrop-filter: blur(6px);
  font: inherit;
  font-weight: 400;
  font-size: clamp(0.875rem, 1.04vw, 0.9375rem);
  line-height: 1.4;
  color: #fbfbfb;
  cursor: pointer;
  white-space: nowrap;
  transition: background-color 200ms ease;
}

.aira-chat__back:hover {
  background-color: rgba(0, 0, 0, 0.3);
}

.aira-chat__back:focus-visible {
  outline: 2px solid #3b82f6;
  outline-offset: 3px;
}

/* --- responsive --- */

@media (max-width: 900px) {
  .aira-chat {
    flex-basis: 100%;
  }
}

@media (max-width: 520px) {
  .aira-chat__panel {
    padding: 18px;
    border-radius: 22px;
  }

  .aira-chat__bubble {
    padding: 12px 16px;
    font-size: 15px;
    line-height: 22px;
  }
}

@media (prefers-reduced-motion: reduce) {
  /* The staging stays — it is the content, not decoration — but nothing
     slides, folds or fades to get there. */
  .aira-chat,
  .aira-chat__reveal,
  .aira-chat__slot,
  .aira-chat__reveal[data-exit],
  .aira-chat__reveal[data-exit] .aira-chat__slot {
    animation: none;
  }

  .aira-chat__reveal[data-exit] {
    display: none;
  }

  .aira-chat__suggest:hover {
    transform: none;
  }

  .aira-chat__log {
    scroll-behavior: auto;
  }
}

/* ==================================================================
   SonicWaveform
   ================================================================== */

/* Only what the canvas itself needs. The box comes from whoever places it —
   two classes both setting height would leave the winner to stylesheet
   order, and the canvas reads its own box to size the drawing buffer. */
.sonic-waveform {
  display: block;
}

/* ==================================================================
   Signals
   ================================================================== */

/* Node 205:19412 — 1440x677, a black band with the waveform running through
   the middle of it. Fluid values land on the design's numbers at 1440.

   The design's own measurements, for the arithmetic below: the wave band is
   1440x455 at y=115, the type block starts at y=152, the cards sit at y=458
   and are 112 tall, and the section ends 107 past them. */

.signals {
  position: relative;
  padding-block: clamp(3.5rem, 10.56vw, 9.5rem) clamp(3rem, 7.43vw, 6.6875rem);
  /* The site's own 140 inset. The cards' 155 comes from their max-width
     centring inside it, not from a second value. */
  padding-inline: clamp(1.5rem, 9.72vw, 8.75rem);
  background-color: #000000;
  /* The wave runs the full width and is cut by the section's edges. */
  overflow: hidden;
}

/* --- the waveform band --- */

/* Percentages of the section's own height rather than a fixed 455: 17% and
   15.8% of 677 are the design's 115 and 107, and read as proportions when
   the copy wraps to another line and the section grows. */
.signals__wave {
  position: absolute;
  left: 0;
  right: 0;
  top: 16.99%;
  bottom: 15.8%;
  overflow: hidden;
}

/* A canvas is a replaced element: with only top and bottom set it would take
   its intrinsic size and feed that back into layout, so the box it should
   fill has to be stated. */
.signals__wave-canvas {
  width: 100%;
  height: 100%;
}

/* --- content --- */

.signals__inner {
  /* Above the wave, and the reason the wave keeps its pointer pull only in
     the open space around the type. */
  position: relative;
}

.signals__head {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 1rem;
  /* The design's 912. It sits 19px left of centre there, which is a slip
     rather than an intention, so this is centred. */
  max-width: 57rem;
  margin-inline: auto;
  text-align: center;
}

.signals__eyebrow {
  margin: 0;
  font-weight: 400;
  font-size: 0.75rem;
  line-height: normal;
  /* The same 0.1em the other section eyebrows carry. */
  letter-spacing: 0.1em;
  color: #ffffff;
  white-space: nowrap;
}

/* Regular, not the 700 the other section titles run at — that is what the
   design sets here, and at this size it is what keeps the claim quiet enough
   to read as an observation rather than a headline. */
.signals__title {
  margin: 0;
  font-weight: 400;
  font-size: clamp(1.75rem, 2.78vw, 2.5rem);
  line-height: normal;
  letter-spacing: -0.025em;
  color: #ffffff;
}

.signals__title span {
  display: block;
}

.signals__body {
  margin: 0;
  font-weight: 400;
  font-size: clamp(0.9375rem, 1.11vw, 1rem);
  line-height: normal;
  letter-spacing: -0.025em;
  color: #ffffff;
  text-wrap: pretty;
}

/* --- the three tails --- */

/* 1130 inside the section's 1160 puts the row at the design's 155 from each
   edge. The font-size is the cards' whole scale — AircraftCard is measured in
   em at one em per ten design pixels, so 10px draws it at 360x112. */
.signals__cards {
  display: flex;
  align-items: center;
  gap: 2.4em;
  max-width: 1130px;
  /* 91px at the design width, between the copy and the cards. */
  margin: clamp(2.5rem, 6.32vw, 5.6875rem) auto 0;
  font-size: 10px;
}

/* The wrapper is the flex child now — each card is wrapped so the arrival
   below has something to animate that is not the shared card itself. */
.signals__card {
  flex: 1 0 0;
  min-width: 0;
}

/* --- the section arriving ---

   Four beats, in the order it reads: the wave opens out of the bottom-right
   corner, the title comes in, the copy under it, then the three tails one at
   a time.

   Keyed off data-reveal, which main.js writes onto the section on mount
   — nothing here hides anything until the script that shows it again is
   running, so a reader without JavaScript gets the section outright. "done"
   is set once, on the first crossing, and never unset. */

.signals[data-reveal] .signals__wave {
  clip-path: circle(0% at 100% 100%);
}

.signals[data-reveal] .signals__eyebrow,
.signals[data-reveal] .signals__title,
.signals[data-reveal] .signals__body,
.signals[data-reveal] .signals__card {
  opacity: 0;
  transform: translateY(16px);
}

/* 150% rather than 100: the radius is measured from the corner it starts
   at, and the far corner of a wide band is further away than that. */
.signals[data-reveal="done"] .signals__wave {
  clip-path: circle(150% at 100% 100%);
  transition: clip-path 1100ms cubic-bezier(0.33, 0, 0.2, 1);
}

/* The label belongs to the title it sits over, so the two arrive together. */
.signals[data-reveal="done"] .signals__eyebrow,
.signals[data-reveal="done"] .signals__title {
  opacity: 1;
  transform: none;
  transition:
    opacity 700ms ease 450ms,
    transform 800ms cubic-bezier(0.16, 1, 0.3, 1) 450ms;
}

.signals[data-reveal="done"] .signals__body {
  opacity: 1;
  transform: none;
  transition:
    opacity 700ms ease 800ms,
    transform 800ms cubic-bezier(0.16, 1, 0.3, 1) 800ms;
}

/* Well spaced, so one tail has all but landed before the next starts and
   the three read as three arrivals rather than one row fading up. Each
   card's own delay comes in from the markup. */
.signals[data-reveal="done"] .signals__card {
  opacity: 1;
  transform: none;
  transition:
    opacity 420ms ease var(--signals-in, 0ms),
    transform 520ms cubic-bezier(0.16, 1, 0.3, 1) var(--signals-in, 0ms);
}

@media (prefers-reduced-motion: reduce) {
  /* The section is simply there. */
  .signals[data-reveal] .signals__wave,
  .signals[data-reveal] .signals__eyebrow,
  .signals[data-reveal] .signals__title,
  .signals[data-reveal] .signals__body,
  .signals[data-reveal] .signals__card {
    clip-path: none;
    opacity: 1;
    transform: none;
    transition: none;
  }
}

/* --- responsive --- */

@media (max-width: 900px) {
  /* Three 148px media columns plus their type stop fitting across; stacked,
     each card keeps the design's proportions instead of squeezing the name
     into an ellipsis. */
  .signals__cards {
    flex-wrap: wrap;
  }

  .signals__card {
    flex-basis: 100%;
  }
}

@media (max-width: 620px) {
  /* The design's break belongs to a line that fits; below this the title
     reads as one sentence and wraps where it needs to. */
  .signals__title span {
    display: inline;
  }
}

/* ==================================================================
   SignalMetrics
   ================================================================== */

/* Node 205:19561 — 1440x608, which is the card row (1362x523 at x=40, y=46)
   plus 46 above it and 39 below. Fluid values land on those at 1440.

   The cards carry no fill: an outline on the page's white, which is what
   lifts them off the black band above without adding another surface. */

.metrics {
  padding-block: clamp(2rem, 3.19vw, 2.875rem) clamp(1.75rem, 2.71vw, 2.4375rem);
  /* The same 40px inset the products row above uses. */
  padding-inline: clamp(1.5rem, 2.78vw, 3.5rem);
  background-color: #ffffff;
}

/* Columns rather than a wrapping flex row, and for the same reason as the
   products row above: wrapping let a card that broke onto a line of its own
   grow into the whole width, which the 442/523 ratio below then turned into
   height. Fixed column counts give a broken card exactly one column, so it
   comes out the size of its siblings whichever row it lands on. The
   breakpoints are that row's own, so the two never disagree about how many
   cards fit. */
.metrics__list {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  align-items: stretch;
  /* The design's own 18px, side to side. */
  column-gap: 18px;
  /* Stacked, 18px would sit tighter than the 24px inside a card's own copy
     block, and the seam between two cards would read as the smaller break
     of the two. */
  row-gap: clamp(2.5rem, 4vw, 3rem);
  /* Three 442px cards and two 18px gaps. */
  max-width: 1362px;
  /* Both zeroed: a ul carries a 1em block margin of its own, which would add
     16px to the padding at each end of the section. */
  margin: 0 auto;
  padding: 0;
  list-style: none;
}

.metrics__card {
  position: relative;
  display: flex;
  flex-direction: column;
  /* The copy sits on the floor of the card — the design leaves the top two
     thirds empty. */
  justify-content: flex-end;
  min-width: 0;
  /* The inline size settled here rather than left to the column's own
     stretch. An aspect-ratio only says how the two axes relate, not which
     one is decided first, and WebKit resolves this one the other way round
     to Chrome: on iOS it took the card's height from its content — the
     middle card's caption is a line shorter than the two beside it — and
     then handed back a width through the ratio, so that card came out
     visibly narrower than its neighbours. A definite width settles the
     inline axis first, and leaves the ratio only the height to decide. */
  width: 100%;
  /* The design's 442x523, as a ratio rather than a height, so the card keeps
     its shape as the row narrows. */
  aspect-ratio: 442 / 523;
  padding: 1rem 1rem 2.5rem;
  border: 1px solid rgba(0, 0, 0, 0.16);
  border-radius: 16px;
  overflow: hidden;
}

/* --- the first card's hover scene --- */

.metrics__scene {
  position: absolute;
  inset: 0;
  opacity: 0;
  transition: opacity 400ms ease;
}

/* Only once main.js has marked the card ready — its scene has painted —
   so the hover never fades up over a canvas that is still black. */
.metrics__card[data-scene][data-ready]:hover .metrics__scene {
  opacity: 1;
}

/* The scene, pinned to the card behind everything else in it. The embed
   writes its own position:relative inline, which is what makes an absolute
   wrapper necessary rather than sizing the div itself. */
.metrics__scene-embed {
  position: absolute !important;
  inset: 0;
}

.metrics__body {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  /* The design gives the first card 32 and the other two 24. Taking the
     pair's value: 24 with the first card's two-line caption is what the
     other two already do. */
  gap: 1.5rem;
  /* The design's own 384 measure inside a 410 padding box, which is what
     puts the break in the captions. As a cap rather than a width: a width
     is a box the card has to shrink back down, and on a phone the card is
     narrower than this to begin with. */
  width: 100%;
  max-width: 384px;
  color: #0a0a0a;
  /* Only a card with a scene switches at all — see the rule below — and only
     that card's own hover drives it, so a sibling's transition finishing
     late can never leave this one part-way there. */
  transition: color 400ms ease;
}

.metrics__card[data-scene][data-ready]:hover .metrics__body {
  color: #ffffff;
}

.metrics__label {
  margin: 0;
  font-weight: 400;
  font-size: 0.75rem;
  line-height: normal;
  /* The design's 1.2px at 12px — the same 0.1em every other eyebrow on the
     page carries. */
  letter-spacing: 0.1em;
}

.metrics__figure {
  margin: 0;
  font-weight: 600;
  /* 40px, the size the section titles run at. */
  font-size: clamp(1.75rem, 2.78vw, 2.5rem);
  line-height: normal;
  letter-spacing: -0.025em;
}

.metrics__note {
  margin: 0;
  /* Light, which is the design's own weight here and the only place on the
     page that uses it. */
  font-weight: 300;
  font-size: clamp(1rem, 1.25vw, 1.125rem);
  line-height: normal;
  letter-spacing: -0.025em;
  text-wrap: pretty;
}

@media (prefers-reduced-motion: reduce) {
  .metrics__scene,
  .metrics__body {
    transition: none;
  }
}

/* --- the row arriving ---

   Each card rises into place, one after the next. Keyed off data-reveal,
   which main.js writes onto the row on mount: nothing here hides a
   card until the script that can show it again is running, so a reader
   without JavaScript gets all three outright. "done" is set once, on the
   first crossing, and never unset — scrolling back up leaves them where they
   are.

   The rise is on the card, not on its copy, so the outline comes up with the
   words inside it as one piece. */

.metrics__list[data-reveal] .metrics__card {
  opacity: 0;
  transform: translateY(28px);
}

.metrics__list[data-reveal="done"] .metrics__card {
  opacity: 1;
  transform: none;
  /* Each card's own delay comes in from the markup, in the order they sit
     in the row. */
  transition:
    opacity 480ms ease var(--metrics-in, 0ms),
    transform 620ms cubic-bezier(0.16, 1, 0.3, 1) var(--metrics-in, 0ms);
}

@media (prefers-reduced-motion: reduce) {
  .metrics__list[data-reveal] .metrics__card {
    opacity: 1;
    transform: none;
    transition: none;
  }
}

/* --- responsive --- */

/* Where the three stop fitting: 3 x 20rem plus the gaps and the section's
   own inset. Two columns from here, so the third card takes one of them
   rather than the row. */
@media (max-width: 1049px) {
  .metrics__list {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

@media (max-width: 700px) {
  /* One column, capped at the design's own card width. The cap is what used
     to be handled by dropping the card's aspect-ratio here and giving it a
     min-height instead: a full-width card at 442/523 was 700-odd pixels of
     mostly empty box. Capping the row keeps the design's shape at every
     width instead of trading it away on small screens. */
  .metrics__list {
    grid-template-columns: 1fr;
    max-width: 442px;
  }
}

/* ==================================================================
   WhyNotSubscription
   ================================================================== */

/* Node 205:19561's current content — a 1440x1429 frame: 74px above the
   copy, 194 of copy, 63 down to the table, 1024 of table, 74 below. Fluid
   values land on those at 1440px and scale from there rather than stepping
   at breakpoints. */

.why-not {
  padding-block: clamp(3rem, 5.14vw, 4.625rem);
  padding-inline: clamp(1.5rem, 2.78vw, 2.5rem);
  background-color: #ffffff;
}

/* --- header --- */

.why-not__head {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 1rem;
  /* The design's own 912px measure — the same width the Signals section's
     header runs at. */
  max-width: 57rem;
  margin-inline: auto;
  text-align: center;
}

.why-not__eyebrow {
  margin: 0;
  font-weight: 600;
  font-size: 0.75rem;
  line-height: normal;
  /* The design's 0.8px at 12px. */
  letter-spacing: 0.08em;
  color: #8a8f98;
}

.why-not__title {
  margin: 0;
  font-weight: 400;
  font-size: clamp(1.75rem, 2.78vw, 2.5rem);
  line-height: normal;
  letter-spacing: -0.025em;
  color: #0a0a0a;
}

.why-not__title span {
  display: block;
}

.why-not__body {
  margin: 0;
  font-weight: 400;
  font-size: clamp(0.9375rem, 1.11vw, 1rem);
  line-height: normal;
  letter-spacing: -0.025em;
  color: #0a0a0a;
  text-wrap: pretty;
}

/* --- the table --- */

.why-not__table {
  position: relative;
  /* 63px at the design width, between the copy and the table. */
  margin: clamp(2rem, 4.375vw, 3.9375rem) auto 0;
  max-width: 1351px;
  border: 1px solid #eeedee;
  border-radius: 16px;
  overflow: hidden;
}

/* One line down the middle of the whole table, header included, rather than
   a border on every cell — the design's own "Column Divider", which is what
   keeps the seam continuous across the row colour change below. */
.why-not__table::after {
  content: "";
  position: absolute;
  top: 0;
  bottom: 0;
  left: 50%;
  width: 1px;
  background-color: #eeedee;
  pointer-events: none;
}

.why-not__row {
  display: flex;
  align-items: stretch;
}

.why-not__row + .why-not__row {
  border-top: 1px solid #eeedee;
}

.why-not__cell {
  display: flex;
  flex: 1 1 0;
  min-width: 0;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.625rem;
  /* The design's own 30/36 box, which is what gives the row its height —
     rows run 123px tall at a single-line title and body, growing with
     whatever wraps past that rather than a fixed figure. Three rows in the
     design run 27px taller than that on an unwrapped body, which reads as
     an edit that trimmed the copy without following through on the row —
     so this takes the shorter, content-driven height instead of repeating
     the mismatch. */
  padding: 1.875rem 2.25rem;
}

.why-not__cell--competitor {
  background-color: #ffffff;
}

.why-not__cell--bizav {
  background-color: #f6f9ff;
}

/* --- header row --- */

.why-not__row--head .why-not__cell {
  flex-direction: row;
  align-items: center;
  gap: 1rem;
  padding: 1.5625rem 2.25rem;
}

.why-not__badge {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 31px;
  height: 31px;
  flex-shrink: 0;
  border-radius: 999px;
}

.why-not__badge--minus {
  background-color: #eeedee;
}

.why-not__badge--plus {
  background-color: #161616;
}

.why-not__badge img {
  display: block;
  width: 16px;
  height: 16px;
}

.why-not__col-title {
  margin: 0;
  font-weight: 600;
  font-size: 1rem;
  line-height: 1.5;
  color: #161616;
}

/* --- row cells --- */

.why-not__row-title {
  margin: 0;
  font-weight: 600;
  font-size: 1.125rem;
  line-height: 1.4444;
}

/* The design's own split: the left column's row titles sit a shade lighter
   than the right's, which is what gives the bizav.ai side the extra half
   step of emphasis down the whole table. */
.why-not__row-title--competitor {
  color: #2e2e2e;
}

.why-not__row-title--bizav {
  color: #161616;
}

.why-not__row-note {
  margin: 0;
  font-weight: 400;
  font-size: 1rem;
  line-height: 1.6875;
  color: #6b6b6b;
}

/* --- responsive --- */

@media (max-width: 720px) {
  /* Two columns of prose stop being legible side by side well before the
     table itself would wrap; stacking is the same trade the row-vs-panel
     sections elsewhere on the page make at this width. */
  .why-not__row {
    flex-direction: column;
  }

  .why-not__row--head .why-not__cell:first-child {
    border-bottom: 1px solid #eeedee;
  }

  .why-not__row:not(.why-not__row--head) .why-not__cell:first-child {
    border-bottom: 1px solid #eeedee;
  }

  .why-not__table::after {
    display: none;
  }
}

/* ==================================================================
   Pricing
   ================================================================== */

/* Node 215:20884 — 1280x458 inside a 1440-wide frame. Fluid values land on
   the design's own numbers at 1440px.

   Colours here are the design's own, as authored — not adapted to the rest
   of the site's palette. */

.pricing {
  padding-block: clamp(2.5rem, 4.79vw, 4.3125rem) clamp(4rem, 8.4vw, 7.5625rem);
  padding-inline: clamp(1.5rem, 5.56vw, 5rem);
  background-color: #ffffff;
}

/* --- header --- */

.pricing__head {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.5rem;
  text-align: center;
}

.pricing__eyebrow {
  margin: 0;
  font-weight: 400;
  font-size: 0.75rem;
  line-height: normal;
  /* The design's 1.2px at 12px — the same 0.1em every other eyebrow on the
     page carries. */
  letter-spacing: 0.1em;
  color: #0a0a0a;
}

.pricing__title {
  margin: 0;
  font-weight: 400;
  font-size: clamp(1.75rem, 2.78vw, 2.5rem);
  line-height: normal;
  letter-spacing: -0.025em;
  color: #0a0a0a;
}

/* --- cards --- */

.pricing__cards {
  display: flex;
  align-items: stretch;
  gap: 1rem;
  flex-wrap: wrap;
  max-width: 1280px;
  margin: 2.5rem auto 0;
}

.pricing__card {
  display: flex;
  flex: 1 1 20rem;
  min-width: 0;
  flex-direction: column;
  align-items: stretch;
  border: 1px solid #e5e5e5;
  border-radius: 8px;
  background-color: #ffffff;
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
}

/* The design's own callout: a blue ring and a pale blue wash on the
   In-House card. */
.pricing__card[data-featured] {
  border: 2px solid #3b82f6;
  background-color: #eff6ff;
}

.pricing__card-head {
  display: flex;
  flex-direction: column;
  gap: 0.375rem;
  padding: 1.5rem 1.5rem 0.5rem;
}

.pricing__title-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  /* The name and the badge are both unbreakable pieces, and on a narrow
     screen "In-House Plan" plus "Most adopted" is wider than the card they
     sit in — which pushed the badge past the right edge of the page. Nothing
     clips it there, so the document itself grew wider than the viewport and
     every section's background, being only as wide as the viewport, left a
     white strip down the right of the whole page. Letting the row break puts
     the badge under the name instead. */
  flex-wrap: wrap;
  gap: 0.75rem;
  padding-bottom: 0.5rem;
}

.pricing__plan {
  margin: 0;
  font-weight: 600;
  font-size: 1.875rem;
  line-height: 1.2;
  color: #111827;
}

.pricing__badge {
  display: inline-flex;
  align-items: center;
  flex-shrink: 0;
  padding: 0.25rem 0.75rem;
  border-radius: 999px;
  background-color: #3b82f6;
  font-weight: 500;
  font-size: 0.875rem;
  line-height: 1.4286;
  color: #ffffff;
  white-space: nowrap;
}

.pricing__description {
  margin: 0;
  font-weight: 400;
  font-size: 0.875rem;
  line-height: 1.4286;
  color: #4b5563;
}

.pricing__card-body {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  padding: 0 1.5rem 1.5rem;
}

/* --- the call to action --- */

.pricing__cta {
  padding-block: 1rem;
  border-radius: 12px;
  font: inherit;
  font-weight: 500;
  font-size: 1.25rem;
  line-height: 1.4;
  color: #ffffff;
  cursor: pointer;
  transition: background-color 200ms ease;
}

.pricing__cta--outline {
  border: 1px solid #404040;
  background-color: #525252;
  box-shadow: 0 10px 15px -3px rgba(23, 23, 23, 0.35);
}

.pricing__cta--outline:hover {
  background-color: #404040;
}

.pricing__cta--solid {
  border: 1px solid #60a5fa;
  background-color: #2563eb;
  box-shadow: 0 10px 15px -3px rgba(59, 130, 246, 0.5);
}

.pricing__cta--solid:hover {
  background-color: #1d4ed8;
}

.pricing__cta:focus-visible {
  outline: 2px solid #3b82f6;
  outline-offset: 3px;
}

/* --- the checklist --- */

.pricing__includes {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 0.75rem;
  padding-top: 1rem;
  border-top: 1px solid #e5e5e5;
  margin-top: 1rem;
}

.pricing__includes-label {
  margin: 0;
  font-weight: 500;
  font-size: 1rem;
  line-height: 1.5;
  color: #111827;
}

.pricing__list {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 0.5rem;
  margin: 0;
  padding: 0;
  list-style: none;
}

.pricing__item {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  font-weight: 600;
  font-size: 0.875rem;
  line-height: 1.4286;
  color: #4b5563;
}

/* The design's own check: a pale mint circle, a blue ring, a blue glyph. */
.pricing__check {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 24px;
  height: 24px;
  flex-shrink: 0;
  border: 1px solid #3b82f6;
  border-radius: 999px;
  background-color: #f0fdf4;
}

.pricing__check img {
  display: block;
  width: 16px;
  height: 16px;
}

/* --- responsive --- */

@media (max-width: 720px) {
  .pricing__cards {
    flex-wrap: wrap;
  }

  .pricing__card {
    flex-basis: 100%;
  }
}

/* ==================================================================
   Vision
   ================================================================== */

/* Node 215:21286 — a 1237px group: a 297-wide photo card overlapping the left
   edge of a 986x579 quote panel. Fluid values land on the design's own
   numbers at 1440px and scale from there rather than stepping at
   breakpoints. */

.vision {
  padding-block: clamp(3rem, 6.11vw, 5.75rem);
  /* The design's own 101px inset either side of the 1237 group. */
  padding-inline: clamp(1.5rem, 7.08vw, 6.375rem);
  background-color: #ffffff;
}

/* --- header --- */

.vision__head {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 1rem;
  text-align: center;
}

.vision__eyebrow {
  margin: 0;
  font-weight: 400;
  /* The design's own 10px here, the same size Products and Aira's eyebrows
     already run at. */
  font-size: 0.625rem;
  line-height: normal;
  letter-spacing: 0.1em;
  color: #0a0a0a;
}

.vision__title {
  margin: 0;
  font-weight: 400;
  font-size: clamp(1.75rem, 2.78vw, 2.5rem);
  line-height: normal;
  letter-spacing: -0.025em;
  color: #0a0a0a;
}

/* --- the overlap --- */

/* The design's own 251/986/297 out of a 1237 whole, kept as percentages of
   this box so the overlap holds at any width — a card's left/top as a
   percentage resolves against this container's box, which is exactly what
   ties the two together as the group scales. */
.vision__group {
  position: relative;
  margin: clamp(1.5rem, 2.15vw, 1.9375rem) auto 0;
  max-width: 1237px;
}

.vision__panel {
  position: relative;
  /* 251 / 1237 — and the rest of the margin zeroed, because a figure
     carries margin: 1em 40px from the UA stylesheet, which would otherwise
     take 40px off each side of this panel. */
  margin: 0 0 0 20.29%;
  /* 986 / 1237. */
  width: 79.71%;
  /* No overflow: hidden. The card below lives inside this figure — it has
     to, to be able to sit on the photograph on narrow screens — and on wide
     ones it reaches out past this box's left edge to where the design puts
     it. Nothing else here needs clipping: the photograph's own box is
     exactly this box, and object-fit crops inside it. */
  background-color: #d9d9d9;
}

.vision__panel-bg {
  position: absolute;
  inset: 0;
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.vision__panel-veil {
  position: absolute;
  inset: 0;
  background-color: rgba(0, 0, 0, 0.6);
}

/* No aspect-ratio on the panel, and the design's 579 height is not lost by
   leaving it out: the padding below is the design's own 170 top and bottom as
   a share of the panel's own 986 width, so 170 + the text's 239 + 170 comes
   to that 579 at the design's own size. The difference is what happens as the
   panel narrows — the text needs more height while a ratio would hand it
   less, which put 15-54px of it past the panel between 900 and 760, and some
   240px at 390, with overflow: hidden taking the difference off the bottom.
   Height from the content cannot clip the content.

   The insets are percentages for the same reason: they resolve against this
   box's own width, so they hold their proportion instead of drifting out of
   step with the box they are padding the way viewport-based values did. */
.vision__quote {
  position: relative;
  display: flex;
  flex-direction: column;
  /* 57px at the design's 579-tall panel. */
  gap: clamp(2.5rem, 3.96vw, 3.5625rem);
  /* The design's own measure, which is what puts the line breaks where
     they are. */
  max-width: 817px;
  margin: 0;
  padding: 17.24% 5.78% 17.24% 11.36%;
  font-weight: 400;
  font-size: clamp(1.125rem, 1.39vw, 1.25rem);
  line-height: normal;
  letter-spacing: -0.025em;
  color: #ffffff;
  /* Both edges flush, rather than a ragged right against the panel it sits
     on. At this measure — some eighty characters a line — the words are
     spaced out by a hair to make it, not stretched into rivers. */
  text-align: justify;
}

.vision__quote p {
  margin: 0;
  text-wrap: pretty;
}

/* The card overlapping the panel's left edge — the design's 297-wide box at
   the group's own left edge, which from inside the panel is 251 of the
   panel's 986 back to the left (25.46%) and 297 of that 986 wide (30.12%).
   40 / 579 down, as a percentage of the panel's height. */
.vision__card {
  position: absolute;
  left: -25.46%;
  top: 6.91%;
  width: 30.12%;
  margin: 0;
  display: flex;
  flex-direction: column;
  gap: 1rem;
}

.vision__portrait {
  display: block;
  width: 100%;
  /* The design's own 297x414. */
  aspect-ratio: 297 / 414;
  object-fit: cover;
  background-color: #d9d9d9;
}

.vision__byline {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.5rem;
}

/* No nowrap on these: the card is a share of the page, and at the widths
   where it is narrower than the line, nowrap hid the overflow under the
   panel instead of wrapping it. */
.vision__name,
.vision__role {
  margin: 0;
  font-weight: 400;
  font-size: clamp(1rem, 1.25vw, 1.125rem);
  line-height: normal;
  letter-spacing: -0.025em;
  color: #0a0a0a;
}

/* The design's own 111x23, at the file's own size: it is a small bitmap, and
   any upscaling shows as a soft edge. The ratio is stated because the file
   loads lazily — left to the file, the box is zero until the bytes land and
   the byline above it shifts when they do. object-fit: contain means a mark
   of some other shape would letterbox here rather than stretch. */
.vision__signature {
  display: block;
  width: 111px;
  max-width: 100%;
  aspect-ratio: 111 / 23;
  object-fit: contain;
  object-position: left;
}

/* --- responsive --- */

/* 950 is where the card stops being able to hold its own byline: the card is
   24.01% of a group that is itself the viewport less 14.16%, so it runs about
   0.206 of the window — and "Founder & CEO of bizav.ai" needs some 195px,
   which is 0.206 of 950. Below that the name and the signature would sit
   under the dark panel rather than on the card. */
@media (max-width: 950px) {
  .vision__panel {
    position: relative;
    width: 100%;
    margin-left: 0;
  }

  /* Percentages of a 342px panel would be a hairline of padding; down here
     the quote takes plain room instead. */
  .vision__quote {
    padding: 2rem 1.5rem 1.25rem;
  }

  /* The card stops being a card and becomes what it is at this width: the
     caption under the quote, on the photograph rather than beside it. A row,
     because a 297-wide portrait stacked here would be 414 tall. */
  .vision__card {
    /* relative, not static: the photograph and the veil above are both
       absolutely positioned, and positioned boxes paint after every plain
       static one in the same stacking context — so a static card here went
       under the photograph. Only the signature survived it, and only
       because its own filter promoted it into a context of its own. */
    position: relative;
    /* The offsets that reach out past the panel on wide screens have to go
       with the absolute positioning they belong to, or relative keeps
       applying them and the portrait hangs off the panel's left edge. */
    left: auto;
    top: auto;
    width: auto;
    flex-direction: row;
    align-items: center;
    gap: 1rem;
    padding: 0 1.5rem 2rem;
  }

  .vision__portrait {
    width: clamp(5.5rem, 19vw, 7.5rem);
    border-radius: 12px;
    flex-shrink: 0;
  }

  .vision__byline {
    gap: 0.25rem;
    min-width: 0;
  }

  /* On the photograph now, not on the page's white. */
  .vision__name {
    color: #ffffff;
  }

  .vision__role {
    color: rgba(255, 255, 255, 0.72);
  }

  /* Pure black ink on transparent — nothing else in the file — so the invert
     is exact: it comes out white and keeps its own edges. */
  .vision__signature {
    margin-top: 0.375rem;
    filter: invert(1);
  }
}

/* --- the section arriving ---

   The order it is read in: the founder first, in from the left; his name,
   his role and the signature under it one after the other; then the office
   behind them, uncovered left to right; and the words last, into focus from
   a blur.

   The beats are custom properties rather than delays written into each rule,
   because the composition changes below 950 — there the card sits *on* the
   photograph instead of beside it, and its name and role are white — so the
   photograph has to arrive first or the byline would land on the page's own
   white and be invisible until it did. Only the numbers change.

   Keyed off data-reveal, which main.js writes onto the section on mount:
   nothing here hides anything until the script that can show it again is
   running, so a reader without JavaScript gets the section outright. "done"
   is set once, on the first crossing, and never unset — scrolling back up
   leaves it where it is. */

.vision {
  --vision-portrait-in: 0ms;
  --vision-name-in: 320ms;
  --vision-role-in: 430ms;
  --vision-sign-in: 540ms;
  --vision-bg-in: 780ms;
  --vision-quote-in: 1320ms;
}

@media (max-width: 950px) {
  .vision {
    --vision-bg-in: 0ms;
    --vision-portrait-in: 620ms;
    --vision-name-in: 900ms;
    --vision-role-in: 1010ms;
    --vision-sign-in: 1120ms;
    --vision-quote-in: 1460ms;
  }
}

/* The panel's own grey is the placeholder behind the photograph, and while
   the photograph is still held back it would be a grey slab sitting where
   nothing has arrived yet. It comes back the instant the wipe has covered
   it — a step, not a fade, so it is never seen doing it. */
.vision[data-reveal] .vision__panel {
  background-color: transparent;
}

.vision[data-reveal="done"] .vision__panel {
  /* Restated, not just transitioned: the armed rule above is keyed off the
     attribute being present at all, so it goes on applying once the value is
     "done" and the colour would never come back on its own. Same specificity
     and further down, which is what lets this win.

     The delay is the wipe's own start plus its length, so the grey reappears
     exactly as the photograph finishes covering it. */
  background-color: #d9d9d9;
  transition: background-color 0s linear calc(var(--vision-bg-in) + 900ms);
}

.vision[data-reveal] .vision__portrait {
  opacity: 0;
  transform: translateX(-24px);
}

.vision[data-reveal] .vision__byline-line {
  opacity: 0;
  transform: translateX(-16px);
}

.vision[data-reveal] .vision__signature {
  opacity: 0;
  transform: translateX(-16px);
}

/* Uncovered from its left edge across, the photograph and the veil over it
   as one — a wipe rather than a slide, so the frame stays where it is and
   only what is inside it arrives. */
.vision[data-reveal] .vision__panel-bg,
.vision[data-reveal] .vision__panel-veil {
  clip-path: inset(0 100% 0 0);
}

.vision[data-reveal] .vision__quote {
  opacity: 0;
  filter: blur(14px);
}

.vision[data-reveal="done"] .vision__portrait,
.vision[data-reveal="done"] .vision__byline-line,
.vision[data-reveal="done"] .vision__signature {
  opacity: 1;
  transform: none;
}

.vision[data-reveal="done"] .vision__portrait {
  transition:
    opacity 680ms ease var(--vision-portrait-in),
    transform 760ms cubic-bezier(0.16, 1, 0.3, 1) var(--vision-portrait-in);
}

.vision[data-reveal="done"] .vision__name {
  transition:
    opacity 520ms ease var(--vision-name-in),
    transform 600ms cubic-bezier(0.16, 1, 0.3, 1) var(--vision-name-in);
}

.vision[data-reveal="done"] .vision__role {
  transition:
    opacity 520ms ease var(--vision-role-in),
    transform 600ms cubic-bezier(0.16, 1, 0.3, 1) var(--vision-role-in);
}

/* opacity and transform only — never filter. Below 950 the signature is
   inverted to read white on the photograph, and a blur written here would
   take that inversion with it. */
.vision[data-reveal="done"] .vision__signature {
  transition:
    opacity 520ms ease var(--vision-sign-in),
    transform 600ms cubic-bezier(0.16, 1, 0.3, 1) var(--vision-sign-in);
}

.vision[data-reveal="done"] .vision__panel-bg,
.vision[data-reveal="done"] .vision__panel-veil {
  clip-path: inset(0 0 0 0);
  transition: clip-path 900ms cubic-bezier(0.16, 1, 0.3, 1) var(--vision-bg-in);
}

.vision[data-reveal="done"] .vision__quote {
  opacity: 1;
  filter: blur(0);
  transition:
    opacity 700ms ease var(--vision-quote-in),
    filter 700ms ease var(--vision-quote-in);
}

@media (prefers-reduced-motion: reduce) {
  /* No arrival at all rather than a slower one: the section is simply
     there. */
  .vision[data-reveal] .vision__panel,
  .vision[data-reveal] .vision__portrait,
  .vision[data-reveal] .vision__byline-line,
  .vision[data-reveal] .vision__signature,
  .vision[data-reveal] .vision__panel-bg,
  .vision[data-reveal] .vision__panel-veil,
  .vision[data-reveal] .vision__quote {
    opacity: 1;
    clip-path: none;
    transform: none;
    transition: none;
  }

  .vision[data-reveal] .vision__panel {
    background-color: #d9d9d9;
  }

  .vision[data-reveal] .vision__quote {
    filter: none;
  }
}

/* Narrow enough that justifying it would start pulling the words apart to
   reach the right edge — under about forty characters a line there is not
   enough in a line to absorb it. Ragged is the better of the two from
   here. */
@media (max-width: 620px) {
  .vision__quote {
    text-align: start;
  }
}

/* ==================================================================
   Footer
   ================================================================== */

/* Node 215:21170 — a 1360x244 container inside the same 1440 frame, so the
   fluid values below land on that at 1440px. Considerably simpler than the
   footer this file drew before: a logo and one bottom bar, nothing else. */

.footer {
  --footer-text: #0a0a0a;
  --footer-muted: #4b5563;
  --footer-line: #e2e8f0;

  border-top: 1px solid var(--footer-line);
  background-color: #ffffff;
  color: var(--footer-text);
}

.footer__inner {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  /* The design's own 48px, top, bottom and between the logo and the bar
     alike. */
  gap: clamp(2rem, 3.33vw, 3rem);
  max-width: 1360px;
  margin-inline: auto;
  padding-block: clamp(2rem, 3.33vw, 3rem);
  padding-inline: clamp(1.5rem, 2.22vw, 2rem);
}

.footer__logo {
  display: block;
  /* The design's own 152x48 — the same lockup the loading screen and the
     comparison panel already carry at their own sizes. */
  width: 152px;
  height: 48px;
}

.footer__bottom {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 1rem;
  padding-top: clamp(2rem, 3.33vw, 3rem);
  border-top: 1px solid var(--footer-line);
}

.footer__copyright {
  margin: 0;
  font-weight: 400;
  font-size: 0.875rem;
  line-height: 1.4286;
  color: var(--footer-muted);
  white-space: nowrap;
}

.footer__legal {
  display: flex;
  align-items: center;
  gap: 1rem;
  flex-wrap: wrap;
}

.footer__link {
  font-weight: 400;
  font-size: 0.875rem;
  line-height: 1.4286;
  color: var(--footer-text);
  text-decoration: none;
  white-space: nowrap;
  transition: opacity 200ms ease;
}

.footer__link:hover,
.footer__link:focus-visible {
  opacity: 0.6;
}

.footer__link:focus-visible {
  outline: 2px solid #3b82f6;
  outline-offset: 3px;
  border-radius: 2px;
}

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

/* ==================================================================
   DemoDialog
   ================================================================== */

/* The brokers bar's demo request. The reference this came from is dark; the
   panel below is this site's own instead — white ground, #0a0a0a ink,
   #4b5563 for the quieter copy, the 1px rgba(0,0,0,0.18) hairline the pills
   already use, and #3b82f6 on focus like every other control here. */

.demo {
  /* A dialog is display: none until it is shown, and the browser gives it a
     border, a margin and a max-height of its own. */
  width: min(100% - 2rem, 34rem);
  max-height: calc(100dvh - 2rem);
  padding: 0;
  border: 0;
  border-radius: 16px;
  background-color: transparent;
  overflow: visible;
  animation: demo-in 260ms cubic-bezier(0.33, 0, 0.2, 1);
}

/* The blurred overlay the panel sits over. ::backdrop is the dialog's own
   layer, so nothing has to be stacked against the fixed bar above. */
.demo::backdrop {
  background-color: rgba(10, 10, 10, 0.45);
  -webkit-backdrop-filter: blur(10px);
  backdrop-filter: blur(10px);
  animation: demo-veil-in 260ms ease;
}

.demo__panel {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.75rem;
  max-height: calc(100dvh - 2rem);
  padding: clamp(1.5rem, 2.78vw, 2.5rem);
  border-radius: 16px;
  background-color: #ffffff;
  box-shadow: 0 24px 60px rgba(10, 10, 10, 0.28);
  overflow-y: auto;
}

.demo__close {
  position: absolute;
  top: clamp(1rem, 1.67vw, 1.5rem);
  right: clamp(1rem, 1.67vw, 1.5rem);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2.25rem;
  height: 2.25rem;
  padding: 0;
  border: 1px solid rgba(0, 0, 0, 0.18);
  border-radius: 999px;
  background-color: #ffffff;
  color: #0a0a0a;
  cursor: pointer;
  transition:
    background-color 200ms ease,
    color 200ms ease;
}

.demo__close svg {
  width: 1rem;
  height: 1rem;
}

.demo__close:hover {
  background-color: #0a0a0a;
  color: #ffffff;
}

/* The same ring every other control here gets, in place of the browser's
   own — which this button, being first in the panel, is the one most
   likely to show. */
.demo__close:focus-visible {
  outline: 2px solid #3b82f6;
  outline-offset: 2px;
}

.demo__eyebrow {
  margin: 0;
  /* Clear of the close button, which overlaps this line's own row. */
  padding-right: 3rem;
  font-weight: 400;
  font-size: 0.625rem;
  line-height: normal;
  letter-spacing: 0.1em;
  color: #4b5563;
}

.demo__title {
  margin: 0;
  font-weight: 400;
  font-size: clamp(1.5rem, 2.22vw, 2rem);
  line-height: 1.15;
  letter-spacing: -0.025em;
  color: #0a0a0a;
  text-wrap: pretty;
}

.demo__body {
  margin: 0;
  font-weight: 400;
  font-size: 1rem;
  line-height: 1.5;
  color: #4b5563;
  text-wrap: pretty;
}

/* --- the form --- */

.demo__form,
.demo__done {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 1rem;
  width: 100%;
  /* Off the copy above it, which runs at the panel's own rhythm. */
  margin-top: 0.75rem;
}

.demo__field {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  /* Lets the two on one row shrink rather than overflow it. */
  min-width: 0;
}

.demo__label {
  font-weight: 400;
  font-size: 0.625rem;
  line-height: normal;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: #4b5563;
}

.demo__input {
  width: 100%;
  height: 3rem;
  padding-inline: 0.875rem;
  border: 1px solid rgba(0, 0, 0, 0.18);
  border-radius: 8px;
  background-color: #ffffff;
  font: inherit;
  font-weight: 400;
  font-size: 1rem;
  letter-spacing: -0.025em;
  color: #0a0a0a;
  transition: border-color 200ms ease;
}

.demo__input::placeholder {
  color: rgba(10, 10, 10, 0.38);
}

.demo__input:hover {
  border-color: rgba(0, 0, 0, 0.32);
}

.demo__input:focus-visible {
  outline: 2px solid #3b82f6;
  outline-offset: 2px;
  border-color: rgba(0, 0, 0, 0.32);
}

.demo__row {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 1rem;
}

/* The chevron is ours, so the select loses the platform's own. */
.demo__select {
  position: relative;
}

.demo__select .demo__input {
  padding-right: 2.5rem;
  appearance: none;
  cursor: pointer;
}

.demo__chevron {
  position: absolute;
  top: 50%;
  right: 0.875rem;
  width: 1.125rem;
  height: 1.125rem;
  transform: translateY(-50%);
  color: #4b5563;
  pointer-events: none;
}

.demo__submit {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 3rem;
  margin-top: 0.5rem;
  padding-inline: 1.5rem;
  border: 0;
  border-radius: 999px;
  background-color: #0a0a0a;
  font: inherit;
  font-weight: 400;
  font-size: 1rem;
  letter-spacing: -0.025em;
  color: #ffffff;
  cursor: pointer;
  transition:
    background-color 200ms ease,
    transform 150ms ease;
}

.demo__submit:hover {
  background-color: #262626;
}

.demo__submit:active {
  transform: scale(0.99);
}

.demo__submit:focus-visible {
  outline: 2px solid #3b82f6;
  outline-offset: 3px;
}

/* --- the receipt --- */

.demo__done-line,
.demo__done-note {
  margin: 0;
}

.demo__done-line {
  font-weight: 400;
  font-size: 1.125rem;
  letter-spacing: -0.025em;
  color: #0a0a0a;
}

.demo__done-note {
  font-weight: 400;
  font-size: 1rem;
  line-height: 1.5;
  color: #4b5563;
}

@media (max-width: 480px) {
  /* Two selects on one row leave neither its own label's width. */
  .demo__row {
    grid-template-columns: 1fr;
  }
}

@keyframes demo-in {
  from {
    opacity: 0;
    transform: translateY(8px) scale(0.98);
  }
}

@keyframes demo-veil-in {
  from {
    opacity: 0;
  }
}

@media (prefers-reduced-motion: reduce) {
  .demo,
  .demo::backdrop {
    animation: none;
  }

  .demo__close,
  .demo__input,
  .demo__submit {
    transition: none;
  }
}

/* ==================================================================
   OwnerProblem
   ================================================================== */

/* Node 215:21303 — a plain centred statement, the design's own 669px
   measure. Fluid values follow the same scale every other section's
   eyebrow/title/body opener already runs on. */

.owner-problem {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 1rem;
  max-width: 42rem;
  margin-inline: auto;
  padding-block: clamp(3rem, 5.56vw, 5rem);
  padding-inline: clamp(1.5rem, 2.78vw, 3.5rem);
  text-align: center;
  background-color: #ffffff;
}

.owner-problem__eyebrow {
  margin: 0;
  font-weight: 400;
  font-size: 0.625rem;
  line-height: normal;
  letter-spacing: 0.1em;
  color: #0a0a0a;
}

.owner-problem__title {
  margin: 0;
  font-weight: 400;
  font-size: clamp(1.75rem, 2.78vw, 2.5rem);
  line-height: normal;
  letter-spacing: -0.025em;
  color: #0a0a0a;
  text-wrap: pretty;
}

.owner-problem__body {
  margin: 0;
  font-weight: 400;
  font-size: clamp(0.9375rem, 1.11vw, 1rem);
  line-height: 1.5;
  letter-spacing: -0.025em;
  color: #4b5563;
}

/* ==================================================================
   OwnerFeatures
   ================================================================== */

/* Node 215:21303's "WHAT OWNERS DOES" panel — full-bleed, the design's own
   1440-wide box, with the tilted phone floating past its own bottom-right
   corner into the white section below rather than sitting inside it. */

/* One inset on both sides rather than the design's own pair (160 left
   against a phone hard on the right edge): equal margins are what let the
   copy and the phone read as two halves of one panel, and pulling both in
   to 200 closes the 200px of dead air that was sitting between them. */
.owner-features {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 2rem;
  padding-block: clamp(1.5rem, 2.22vw, 2rem) clamp(2rem, 3.33vw, 3rem);
  padding-inline: clamp(1.5rem, 13.89vw, 12.5rem);
  background-color: rgba(243, 243, 243, 0.5);
}

.owner-features__copy {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 1.5rem;
  max-width: 33rem;
}

.owner-features__eyebrow {
  margin: 0;
  font-weight: 400;
  font-size: 0.625rem;
  line-height: normal;
  letter-spacing: 0.1em;
  color: #0a0a0a;
}

.owner-features__head {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 1rem;
}

.owner-features__title {
  margin: 0;
  font-weight: 400;
  font-size: clamp(1.75rem, 2.78vw, 2.5rem);
  line-height: normal;
  letter-spacing: -0.025em;
  color: #0a0a0a;
}

.owner-features__body {
  margin: 0;
  font-weight: 400;
  font-size: 1rem;
  line-height: 1.75;
  color: #4b5563;
}

.owner-features__body strong {
  font-weight: 700;
  color: #0e0e0e;
}

.owner-features__pills {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  margin: 0;
  padding: 0;
  list-style: none;
}

.owner-features__pill {
  padding: 7px 18px;
  border: 1px solid rgba(0, 0, 0, 0.18);
  border-radius: 999px;
  background-color: #ffffff;
  font-weight: 400;
  font-size: 1rem;
  letter-spacing: -0.025em;
  color: #0a0a0a;
  white-space: nowrap;
}

.owner-features__badge img {
  display: block;
  width: 146px;
  height: auto;
}

/* The provided phone render — cropped to its own visible content
   (2250×3088) rather than the original 4650×3425 canvas it shipped on,
   which carried a huge transparent margin around the phone. That margin
   was harmless while the image floated off-layout as an absolutely
   positioned overlay, but once centered as a normal flex item (on
   request) its own box — margin included — started taking real space in
   the row, ballooning the section's height and shoving everything below
   it down the page. Cropped, its box finally matches what's actually
   visible. Decorative photography, not an interactive scene, so
   pointer-events: none stays. */
.owner-features__phone {
  display: block;
  flex-shrink: 0;
  width: clamp(14.4rem, 27.57vw, 24.82rem);
  aspect-ratio: 2250 / 3088;
  pointer-events: none;
}

@media (max-width: 900px) {
  /* Below this the phone would either overlap the copy or run off the
     section entirely — dropped rather than shrunk past usefulness, the
     design has no mobile frame for this panel to fall back on. */
  .owner-features__phone {
    display: none;
  }

  .owner-features__copy {
    padding-right: 0;
  }
}

@media (max-width: 720px) {
  .owner-features {
    text-align: center;
  }

  .owner-features__copy {
    align-items: center;
  }

  .owner-features__pills {
    justify-content: center;
  }
}

/* ==================================================================
   OwnerWorkings
   ================================================================== */

/* Node 215:21303's "WORKINGS" section — a plain white opener (the design's
   own dark-on-light header, not the black/shader treatment this used to
   carry wholesale) followed by three cards of the design's own 442×523
   box, evenly spaced across the design's 1440-wide row. */

.owner-workings {
  display: flex;
  flex-direction: column;
  align-items: center;
  /* Measured off the design's own render: 36px between the body line's
     own bottom and the cards' top edge, not the much wider gap this had
     been using. */
  gap: clamp(1.5rem, 2.5vw, 2.25rem);
  padding-block: clamp(3rem, 5.56vw, 5rem);
  padding-inline: clamp(1.5rem, 2.92vw, 2.625rem);
  background-color: #ffffff;
}

.owner-workings__head {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 1rem;
  max-width: 42rem;
  text-align: center;
}

.owner-workings__eyebrow {
  margin: 0;
  font-weight: 400;
  font-size: 0.625rem;
  line-height: normal;
  letter-spacing: 0.1em;
  color: #0a0a0a;
}

.owner-workings__title {
  margin: 0;
  font-weight: 400;
  font-size: clamp(1.75rem, 2.78vw, 2.5rem);
  line-height: normal;
  letter-spacing: -0.025em;
  color: #0a0a0a;
  text-wrap: pretty;
}

.owner-workings__body {
  margin: 0;
  font-weight: 400;
  font-size: clamp(0.9375rem, 1.11vw, 1rem);
  line-height: 1.5;
  letter-spacing: -0.025em;
  color: #4b5563;
}

/* Grid rather than a flex row: three basis-27.625rem flex children plus
   their gaps run wider than the row at 1440 once padding is subtracted, and
   wrap early — minmax(0, 1fr) columns divide the row evenly instead. */
.owner-workings__cards {
  display: grid;
  margin: 0;
  padding: 0;
  list-style: none;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 1rem;
  width: 100%;
  max-width: 90rem;
}

/* One cell: the card, and under it the step it illustrates. The 16px is
   the design's own seam between its two rows. */
.owner-workings__step {
  display: flex;
  flex-direction: column;
  gap: 1rem;
  min-width: 0;
}

.owner-workings__copy {
  display: flex;
  flex-direction: column;
  gap: 1rem;
}

.owner-workings__step-title {
  margin: 0;
  font-weight: 400;
  font-size: 1.125rem;
  line-height: normal;
  letter-spacing: -0.025em;
  color: #0a0a0a;
  text-wrap: pretty;
}

.owner-workings__step-body {
  margin: 0;
  font-weight: 300;
  font-size: 1.125rem;
  line-height: normal;
  letter-spacing: -0.025em;
  color: #0a0a0a;
  text-wrap: pretty;
}

/* The design's own 442×523 card, rounded 16px, its animated fill clipped to
   those corners along with everything stacked on it. */
.owner-workings__card {
  position: relative;
  display: flex;
  /* The phone sits on this box's bottom edge rather than being held off
     the top by a padding that has to add up to the card's own height. Laid
     out from the bottom, the two edges meet whatever the card measures. */
  align-items: flex-end;
  justify-content: center;
  /* Definite, so the ratio only decides the height: left to stretch, WebKit
     resolves the other axis first and derives a narrower box from the
     content — see the same note on .metrics__card. */
  width: 100%;
  aspect-ratio: 442 / 523;
  border-radius: 1rem;
  overflow: hidden;
  background-color: #000000;
}

.owner-workings__card-bg {
  position: absolute;
  inset: 0;
  pointer-events: none;
}

.owner-workings__card-shader {
  position: absolute;
  inset: 0;
}

.owner-workings__card-veil {
  position: absolute;
  inset: 0;
  background-image: linear-gradient(to top, #000000, rgba(102, 102, 102, 0));
}

/* A full phone mockup now, not a cropped screen export — sized by width
   alone so its own aspect ratio carries the height; the card's own
   overflow: hidden (above) is what clips it if it ever runs past the
   card's bottom edge instead of a fixed crop box here.

   A share of the card rather than of the viewport, which is what keeps the
   phone the same size relative to the card it sits on. The vw measure this
   used before stopped growing at 1440 while the card — a third of a row
   that runs to 90rem — kept going, and the difference came out as a black
   band under the phone. 70.6% is the design's own 312 against its 442, and
   at that share the phone's height comes to the card's own minus the 78 it
   holds off the top: both edges land where the design puts them.

   position: relative is load-bearing, not decorative: .owner-workings__card-bg
   is position: absolute, which makes it a *positioned* box, and positioned
   boxes paint after every plain static one in the same stacking context —
   in document order or not. Without this, the shader (fully opaque once
   WebGL actually renders, unlike the blank canvas this reads as when it
   can't) painted over the phone instead of behind it, despite coming
   first in the markup. Being positioned too puts this back in the same
   paint pass, ordered by the markup like it looks like it should be. */
.owner-workings__card-phone {
  position: relative;
  display: block;
  width: 70.6%;
  /* The asset's own 624x890, stated rather than left to the file: these
     load lazily, and height: auto alone is 0 until the bytes arrive — so
     the phone's box would pop into place on load instead of being held
     from the first paint. */
  aspect-ratio: 624 / 890;
  height: auto;
  flex-shrink: 0;
  pointer-events: none;
}

@media (max-width: 720px) {
  .owner-workings__cards {
    grid-template-columns: 1fr;
    max-width: 27.625rem;
    margin-inline: auto;
    /* Wider than the row's own 40 once these are one under another: a step
       here is a card and its words, 16px apart, and the space that
       separates one step from the next has to read as clearly larger than
       the space inside one. */
    row-gap: clamp(3.5rem, 10vw, 4.5rem);
  }
}

/* ==================================================================
   OwnerAudience
   ================================================================== */

/* Node 215:21303's "Who it's for?" row — five outline cards on the same
   recipe SignalMetrics.css uses: a border, a floor-pinned body, content
   deciding the height rather than the design's own fixed 280px. */

.owner-audience {
  padding-block: clamp(2.5rem, 4.44vw, 4rem);
  padding-inline: clamp(1.5rem, 2.78vw, 3.5rem);
  background-color: #ffffff;
}

.owner-audience__head {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.5rem;
  margin-bottom: clamp(2rem, 3.33vw, 3rem);
  text-align: center;
}

.owner-audience__title {
  margin: 0;
  font-weight: 400;
  font-size: clamp(1.75rem, 2.78vw, 2.5rem);
  line-height: normal;
  letter-spacing: -0.025em;
  color: #0a0a0a;
}

.owner-audience__sub {
  margin: 0;
  font-weight: 400;
  font-size: 1.125rem;
  line-height: 1.5556;
  color: #4b5563;
}

.owner-audience__list {
  display: flex;
  flex-wrap: wrap;
  gap: 1rem;
  max-width: 1360px;
  margin: 0 auto;
  padding: 0;
  list-style: none;
}

.owner-audience__card {
  display: flex;
  flex: 1 1 18rem;
  min-width: 0;
  min-height: 17.5rem;
  border: 1px solid rgba(0, 0, 0, 0.16);
  border-radius: 16px;
  padding: 1rem 1rem 2.5rem;
}

.owner-audience__body {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.5rem;
  align-self: flex-end;
  width: 100%;
}

.owner-audience__number {
  margin: 0;
  font-weight: 400;
  font-size: 1.5rem;
  letter-spacing: 0.15em;
  color: #0a0a0a;
}

.owner-audience__card-title {
  margin: 0;
  font-weight: 400;
  font-size: 1.125rem;
  line-height: normal;
  letter-spacing: -0.025em;
  color: #0a0a0a;
}

.owner-audience__card-body {
  margin: 0;
  font-weight: 300;
  font-size: 1.125rem;
  line-height: normal;
  letter-spacing: -0.025em;
  color: #0a0a0a;
}

/* ==================================================================
   OwnerCTA
   ================================================================== */

/* Node 215:21794's closing banner — a rounded card inset from the page over
   a photograph of its own, rather than the full-bleed shader panel this
   section used to be. The section itself is only the white around it and
   the inset that holds it off the page's edges. */

.owner-cta {
  padding-inline: clamp(1rem, 2.78vw, 3.5rem);
  background-color: #ffffff;
}

/* The design's own 1130x401 at 1440, rounded 24. min-height rather than a
   fixed height: the design centres its copy in a box it drew at one width,
   and a fixed height would clip that copy at every width it didn't. */
.owner-cta__card {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  max-width: 1130px;
  min-height: clamp(16rem, 27.85vw, 25.0625rem);
  margin-inline: auto;
  padding: clamp(2rem, 3.33vw, 3rem) clamp(1.25rem, 3.06vw, 2.75rem);
  border-radius: 24px;
  /* Clips the photograph and the wash to the radius. */
  overflow: hidden;
  /* What the copy sits on until the photograph has loaded. */
  background-color: #0a0a0a;
}

.owner-cta__photo {
  position: absolute;
  inset: 0;
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  pointer-events: none;
}

.owner-cta__veil {
  position: absolute;
  inset: 0;
  background-color: rgba(0, 0, 0, 0.38);
  -webkit-backdrop-filter: blur(10px);
  backdrop-filter: blur(10px);
  pointer-events: none;
}

/* position: relative is load-bearing: the photograph and the wash above are
   both positioned boxes, and positioned boxes paint after every plain
   static one in the same stacking context, whatever the markup order — so
   without this the wash would cover the copy. */
.owner-cta__content {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  /* The design's own 16px. */
  gap: 1rem;
  text-align: center;
}

.owner-cta__title {
  margin: 0;
  font-weight: 500;
  font-size: clamp(2rem, 3.89vw, 3.5rem);
  line-height: normal;
  letter-spacing: -0.025em;
  color: #ffffff;
}

.owner-cta__body {
  margin: 0;
  font-weight: 300;
  font-size: 1.125rem;
  line-height: normal;
  letter-spacing: -0.025em;
  color: #ffffff;
}

.owner-cta__download {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  height: 44px;
  padding-inline: 18px;
  border: 0;
  border-radius: 999px;
  background-color: #ffffff;
  box-shadow: 0 1px 6px rgba(0, 0, 0, 0.25);
  font: inherit;
  font-weight: 400;
  font-size: 1rem;
  letter-spacing: -0.025em;
  color: #0a0a0a;
  cursor: pointer;
  white-space: nowrap;
}

.owner-cta__download-dot {
  width: 4px;
  height: 4px;
  flex-shrink: 0;
  border-radius: 50%;
  background-color: #66e208;
}

.owner-cta__download-icon {
  display: block;
  width: 20px;
  height: 20px;
  flex-shrink: 0;
}

.owner-cta__download:focus-visible {
  outline: 2px solid #3b82f6;
  outline-offset: 3px;
}

/* ==================================================================
   static build — page-level additions
   ================================================================== */

/* The loader owns the screen until it is done: every animation in the bar
   and the hero holds at its first frame until main.js flips the flag, so the
   hero's intro plays when it is seen and not behind the cover. Set on <html>
   by the markup itself, so it holds from the first paint. */
html[data-site="pending"] .nav *,
html[data-site="pending"] .hero * {
  animation-play-state: paused !important;
}

/* A return within the same session skips the intro (see the inline script
   in <head>), and a browser without JavaScript never gets a loader at all —
   the <noscript> block in index.html carries the same rule. */
html[data-intro="skip"] .loading {
  display: none;
}

/* The audience switch, the chat and the dialog all toggle `hidden` on
   elements whose class sets a display of their own; the UA rule loses to
   those, so this is what makes the attribute mean what it says. */
[hidden] {
  display: none !important;
}

/* The shader canvases fill whatever box places them. */
canvas[data-shader] {
  display: block;
  width: 100%;
  height: 100%;
}

/* The demo form has a real endpoint now, so it needs somewhere to say when
   that endpoint did not answer. */
.demo__error {
  margin: 0;
  font-size: 0.875rem;
  line-height: 1.5;
  color: #b91c1c;
}

.demo__submit:disabled {
  opacity: 0.6;
  cursor: progress;
}

.demo__done-line:focus {
  outline: none;
}

/* The FROM ORIGIN / FASTER CLOSINGS / YOUR EDGE cards, lit one at a time by
   main.js as each comes into view on a touch screen, where there is no hover
   — the same state the hover rules above produce, and gated the same way on
   the scene being ready, so a card never lights over a black canvas. */
.metrics__card[data-scene][data-ready][data-active] .metrics__scene {
  opacity: 1;
}

.metrics__card[data-scene][data-ready][data-active] .metrics__body {
  color: #ffffff;
}

/* ==================================================================
   Companies — the logo panel under the strip
   The design's own pale panel, flush against the blue strip above it (no
   seam — it lives inside the same section), square-cornered onto the white
   page below: a centred claim, then a row of company marks. Fluid values land on
   the design's numbers at 1440px: 50 above the claim, 24px type, 40 down to
   the marks, 29px-tall marks 80px apart, 48 below. */

.companies__panel {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: clamp(1.5rem, 2.78vw, 2.5rem);
  padding: clamp(2rem, 3.47vw, 3.125rem) clamp(1.5rem, 2.78vw, 3.5rem) clamp(2rem, 3.33vw, 3rem);
  background-color: #f3f3f3;
}

.companies__claim {
  margin: 0;
  font-weight: 400;
  font-size: clamp(1.125rem, 1.67vw, 1.5rem);
  line-height: normal;
  letter-spacing: -0.025em;
  color: #0a0a0a;
  text-align: center;
  text-wrap: balance;
}

.companies__logos {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  /* 80px between marks at the design width; tighter as the row narrows, and
     the same distance between rows once they wrap. */
  gap: clamp(1.5rem, 5.56vw, 5rem);
  margin: 0;
  padding: 0;
  list-style: none;
}

.companies__logos li {
  display: flex;
  align-items: center;
  justify-content: center;
  /* Every mark gets the same box — 180x48 at the design width — and fits
     inside it. A wide wordmark fills the width, a stacked mark fills the
     height, so no logo comes out much bigger than its neighbours whatever
     shape its file is. */
  width: clamp(7.5rem, 12.5vw, 11.25rem);
  height: clamp(2rem, 3.33vw, 3rem);
}

.companies__logo {
  display: block;
  max-width: 100%;
  max-height: 100%;
  width: auto;
  height: auto;
  object-fit: contain;
}

/* CFS Jets is nearly square — jet over name — so at the shared height it
   covers a fraction of the area the wordmarks do. A taller box brings its
   footprint level with theirs. */
.companies__logos li:has(.companies__logo--cfs) {
  height: clamp(2.5rem, 4.03vw, 3.625rem);
}

/* --- the panel arriving from behind the strip ---

   The panel slides down out of the strip's bottom edge when the section
   arrives: it starts lifted by its own height inside a slot that clips it,
   so nothing of it shows until it moves, and as it comes down it is uncovered
   from the strip's edge — which reads as sliding out from behind the hero
   and the strip above it. Keyed off the same data-reveal the strip uses; a
   reader without JavaScript gets the panel in place. */

.companies__strip {
  position: relative;
  overflow: hidden;
}

.companies__slot {
  overflow: hidden;
}

.companies[data-reveal] .companies__panel {
  transform: translateY(-100%);
}

.companies[data-reveal="done"] .companies__panel {
  transform: none;
  /* A beat after the strip's own edge has started to show, and long enough
     to read as a slide rather than a drop. Same curve the rest of the page
     arrives on. */
  transition: transform 1100ms cubic-bezier(0.16, 1, 0.3, 1) 120ms;
}

@media (prefers-reduced-motion: reduce) {
  .companies[data-reveal] .companies__panel {
    transform: none;
    transition: none;
  }
}

/* --- hovering one mark softens the others ---
   Pointer only: on touch a hover latches after a tap. :has() keys it off a
   mark actually being under the pointer, so crossing the gaps between marks
   changes nothing. The blur travels rather than snapping, and comes off the
   same way. */

@media (hover: hover) {
  .companies__logo {
    transition: filter 360ms cubic-bezier(0.33, 0, 0.2, 1);
  }

  .companies__logos:has(li:hover) li:not(:hover) .companies__logo {
    filter: blur(4px);
  }
}

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


