/* ============================================================
   KINTSUG — marketing site
   Built from Figma "Wireframe - 8" (file nnIMjPO68j2cnJgTSLlg4H,
   node 590:5825). Design canvas: 1440 x 12641.

   STRUCTURE — this file is two layers, in cascade order:

     1. BASE      Values taken verbatim from the Figma node tree:
                  positions, type, colour, radii, shadows. Treat
                  these as the source of truth for the design.

     2. ADAPTATIONS (from "VIEWPORT SLIDES" onward)
                  Everything the design file does not describe:
                  full-viewport chapters, the scroll sequence, the
                  motion layer, and the larger type those chapters
                  needed. These intentionally override the base.

   Do not "tidy" the duplication between the layers by merging a
   base rule into its override. Several overrides are grouped
   selectors (e.g. `.reviews-top, .reviews-mid, .reviews-foot`),
   so hoisting a single-selector base rule past them silently
   flips the cascade — `.reviews-mid` reverts to absolute
   positioning and the reviews chapters collapse.

   Those four classes now live on panels inside the sticky
   STORY BEATS frame at the foot of this file, which re-positions
   them at equal specificity and therefore relies on being last.
   Keep it last.
============================================================ */

/* ============================================================
   THE TYPEFACE

   The design is set in Satoshi, and until now nothing served it:
   `--sans` named it, the machine did not have it, and every line
   on the page fell through to Helvetica Neue. Close enough to
   read, not close enough to be the design — and the testimonial
   asks for an ITALIC, which Helvetica answers by slanting the
   roman rather than by having one.

   Self-hosted rather than linked: four files, 103 KB all told,
   against a third-party stylesheet that has to resolve before
   the first paint. Nothing here needs installing, which is the
   same bargain the rest of the repo makes.

   600 is not a Satoshi weight; the four rules in this file that
   ask for it resolve up to 700, which is the next one there is.
============================================================ */

@font-face {
  font-family: "Satoshi";
  src: url("assets/fonts/satoshi-400.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: "Satoshi";
  src: url("assets/fonts/satoshi-400-italic.woff2") format("woff2");
  font-weight: 400;
  font-style: italic;
  font-display: swap;
}

@font-face {
  font-family: "Satoshi";
  src: url("assets/fonts/satoshi-500.woff2") format("woff2");
  font-weight: 500;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: "Satoshi";
  src: url("assets/fonts/satoshi-700.woff2") format("woff2");
  font-weight: 700;
  font-style: normal;
  font-display: swap;
}


:root {
  --maroon: #6b1d2e;

  --ink: #1a1412;
  --ink-2: #1e1e1e;
  --ink-3: #272727;
  --ink-4: #17100d;
  --ink-5: #0e121b;

  --muted: #525866;
  --muted-2: #4b5563;
  --muted-3: #898683;

  --cream: #fbf7f1;

  /* THE PAGE GUTTER, AS A SHARE OF THE SCREEN.

     Every section below used a hard `100px` here while the content
     inside them was already written in vw — the FAQ column is
     52.78vw, the trust copy 46.88vw, the quote card 86.11vw. So the
     content kept its proportion as the monitor grew and the margin
     did not:

                     1440      1920      2560
       gutter        6.9%      5.2%      3.9%

     which is why the spacing looks different at every resolution: the
     columns spread toward the edges while the gutter holding them in
     stays the same handful of pixels.

     6.944% IS 100/1440, so this is the design's own number expressed
     as its share of the frame — identical at 1440, and now identical
     in proportion everywhere else. The 24px floor is the value the
     narrow layout already used, so the two agree instead of jumping at
     the breakpoint.

     A PERCENTAGE, not vw, and the difference is real: below 1440 this
     page does not reflow, it keeps a fixed 1440 canvas and overflows
     (see RESPONSIVE). So at a 1200 viewport the sections are still
     1440 wide, and a vw gutter would measure 83px against a box that
     is actually 1440 — sized to a screen the section is not using. A
     percentage resolves against the section's own width, so it reads
     100px on the fixed canvas below 1440 and tracks the viewport above
     it. Correct in both regimes rather than just the one. */
  --gutter: max(24px, 6.944%);

  /* The footer deliberately runs wider than the page — 1392 of 1440,
     a 24px gutter rather than 100 — so it gets the same treatment at
     its own ratio (24/1440) instead of inheriting the page's. */
  --gutter-footer: max(16px, 1.667%);

  /* THE CLOSING PANEL'S FIELD (Figma 590:6418).

     Read off the node's own SVG export, not off a screenshot, which
     is how the two corrections below were found at all.

     The node is TWO stacked fills, not one: the cream, and then the
     texture over it at fill-opacity 0.5. The texture on its own is a
     dusty rose, and at full strength that is what the panel looked
     like here — about twice the colour the design carries. Halved
     against the cream it lands where it belongs: a warm off-white
     with the rose only suggested. Every stop below is therefore at
     half alpha over a --cream base.

     The mesh itself is a rose ground lit from three sides: ivory
     spilling in past the top-left corner, a broad near-white bloom
     left of centre, and a warm glow low on the right. CSS has no mesh
     gradient, so it is rebuilt as the lights over the ground.

     The vertical positions are the export's, not my eye: the pattern
     transform draws the image at 114.41% of the panel's height with a
     -7.20% offset, so a light at 0.90 of the IMAGE sits at 0.958 of
     the PANEL. Reading them off the flat image put the low glow 3%
     too high.

     Rebuilt rather than shipped because there is nothing in it to
     photograph: no detail, no edge, no texture. The export's own PNG
     is 2880x1874 for what is ultimately a blur, and it would still be
     resampled at every width; this is exact at all of them. The same
     reasoning produced --maroon-radial above. */
  --cta-field:
    radial-gradient(ellipse 40% 44% at 34% 46.6%,
      rgba(248, 244, 243, 0.5) 0%,
      rgba(246, 240, 239, 0.41) 34%,
      rgba(238, 224, 219, 0.14) 68%,
      rgba(230, 208, 201, 0) 100%),
    radial-gradient(ellipse 58% 48% at 1% -5%,
      rgba(253, 246, 235, 0.5) 0%,
      rgba(252, 243, 232, 0.31) 34%,
      rgba(246, 227, 213, 0) 72%),
    radial-gradient(ellipse 44% 40% at 82% 95.8%,
      rgba(250, 236, 221, 0.5) 0%,
      rgba(249, 234, 217, 0.29) 36%,
      rgba(238, 213, 199, 0) 74%),
    linear-gradient(118deg,
      rgba(231, 206, 198, 0.5) 0%,
      rgba(226, 198, 189, 0.5) 52%,
      rgba(223, 194, 184, 0.5) 100%),
    var(--cream);

  /* The outcome card's drawn size. Measured off the design: the frame
     reads 900x500, and every coordinate inside it below is relative to
     that. It was 1065x649 here, from an earlier frame — the same card
     at a different aspect (1.64 against the design's 1.80), which is
     why nothing inside it landed where the design puts it. */
  --ocw: 900px;
  --och: 500px;

  --sans: "Satoshi", "Helvetica Neue", Arial, sans-serif;
  --serif: "Lora", Georgia, serif;
  --display: "Playfair Display", Georgia, serif;
  --jakarta: "Plus Jakarta Sans", var(--sans);
  --inter: "Inter", var(--sans);
  --dm: "DM Sans", var(--sans);

  /* Deep maroon radial (Figma GRADIENT_DIAMOND).
     17 stops, interpolated in linear-light with a smoothstep ease inside
     each original segment, so the joins at 40%/69% stop banding.

     Geometry is taken from the Figma handles, which are identical on
     every diamond fill in the file (the "one layer" section, the
     outcome cards and the closing block all share them):

       origin  (0.038, 0.040)   -> 3.8% 4%, i.e. the top-left corner
       axis 1  (1.358, 0.866)
       axis 2  (-0.166, 1.360)

     Both axes project to 1.321 of the box, so the diamond is a rotated
     square and an ellipse of 132% is a close stand-in — CSS has no
     diamond gradient and cannot rotate a radial one. What matters is
     the origin: this lights from the top-left and deepens toward the
     bottom-right. Centring it at 50% 50% (as this did) flattens the
     whole thing into an even vignette and loses the depth. */
  --maroon-radial: radial-gradient(
    ellipse 132% 132% at 3.8% 4%,
    #6B1C2E 0%,
    #6A1C2D 6.2%,
    #671A2C 12.5%,
    #63192A 18.8%,
    #5F1727 25%,
    #5B1525 31.2%,
    #591424 37.5%,
    #581424 43.8%,
    #531322 50%,
    #4B111E 56.2%,
    #44101B 62.5%,
    #400F1A 68.8%,
    #3E0F19 75%,
    #390D18 81.2%,
    #310C15 87.5%,
    #2A0B13 93.8%,
    #260A12 100%
  );
}

* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

/* Nothing on this page is wider than the screen — measured at every
   width from 375 up and at thirteen scroll positions down, the document
   never has a horizontal pixel to give. What a sideways trackpad swipe
   was producing is the rubber-band: macOS bounces the page on an axis
   with nothing to scroll to, and for the length of the bounce a
   horizontal scrollbar flashes up with a thumb almost as wide as the
   track. There is nothing to scroll; the browser is just showing you
   the recoil.

   `overscroll-behavior-x: none` takes the bounce away, and with it the
   two-finger swipe-back gesture on this page — a fair trade on a site
   whose whole navigation is on the page.

   `overflow-x: clip` is the belt to that braces: should anything ever
   overhang by half a pixel, there is no horizontal scrollport for it to
   open. CLIP, not hidden — `hidden` would make the root a scroll
   container, and ScrollTrigger pins every other section here. */
html {
  -webkit-text-size-adjust: 100%;
  overscroll-behavior-x: none;
  overflow-x: clip;
}

/* No 1440 floor.

   There was one, released only below 1024 — so every window between
   1024 and 1439 got a horizontal scrollbar, and so did a 1440 window
   once the vertical scrollbar took its 15px out of the viewport. It
   dates from before the gutters became proportional (`--gutter` is
   max(24px, 6.944%)) and before the rail started scaling on --os.

   Measured with it gone: at 1280 not one element in the document
   reaches past the viewport. It was holding nothing up. */
body {
  font-family: var(--sans);
  background: #fff;
  color: var(--ink);
}

img {
  display: block;
  max-width: 100%;
}

a {
  text-decoration: none;
  color: inherit;
}

/* ============================================================
   HERO  (1440 x 855)
============================================================ */

/* The Figma frame is 855 tall, and this was pinned to it. On any window
   shorter than that the hero kept its 855px and simply hung past the
   fold, taking the call to action with it — and .hero-content's own
   short-viewport guard could not help, because its percentages resolve
   against this box, not the screen. Capping it at the viewport is what
   makes that guard mean what it says. */
.hero {
  position: relative;
  height: min(855px, var(--vh, 100svh));
  background: #fff;
  overflow: hidden;
}

.hero-bg {
  position: absolute;
  inset: 0 0 auto 0;
  /* 804 of the 855 frame; on a short window it takes what there is */
  height: min(804px, 100%);
  overflow: hidden;
}

.hero-photo {
  position: absolute;
  left: 2px;
  top: -46px;
  width: 1438px;
  height: 809px;
  overflow: hidden;
  /* carries the slides now; the single background image it used to hold
     moved into .hero-slide img so each plate can be animated on its own */
  background: var(--maroon);
  /* Load-bearing. The slides below carry a z-index to order the outgoing
     plate under the incoming one, and nothing between here and the section
     root establishes a stacking context — so without this those values
     escape and the plate paints *over* .hero-scrim and .hero-glow, burying
     the white pool the hero copy is designed to be read against. */
  isolation: isolate;
}

/* -------- hero slideshow --------
   Two nested elements on purpose: .hero-slide owns the cross-dissolve
   (opacity), its <img> owns the slow drift. */

.hero-slide {
  position: absolute;
  inset: 0;
  overflow: hidden;
  opacity: 0;
  /* only the leaving/entering pair is ever mid-transition, so this stays
     a two-layer composite no matter how many plates are in the stack */
  will-change: opacity;
}

.hero-slide img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* rests at full frame; the drift below pushes slowly IN from here */
  transform: scale(1);
  transform-origin: center;
  will-change: transform;
}

/* JS drives the timing; these are the resting states it toggles between */
.hero-photo.is-running .hero-slide {
  transition: opacity var(--hero-cross, 1800ms) cubic-bezier(0.45, 0, 0.55, 1);
}

.hero-slide.is-active {
  opacity: 1;
  z-index: 2;
}

/* A true cross-dissolve: the incoming plate fades up OVER the outgoing
   one, which stays fully opaque underneath until it is covered. Fading
   both at once would let the maroon backing show through mid-dissolve
   and dip the frame darker at every change. index.js drops .is-leaving
   once the incoming plate is fully up. */
.hero-slide.is-leaving {
  opacity: 1;
  z-index: 1;
}

/* The drift belongs to the whole visible life of a plate, not just the
   part of it that is `is-active`.

   It used to be scoped to .is-active alone, so the moment a plate began
   LEAVING it stopped matching and its image snapped from wherever the
   drift had got to straight back to its resting scale — with no transition, because
   that was scoped to .is-active too. The snap happened at the very
   start of the cross-fade, while the outgoing plate was still fully
   opaque, which is the jolt: one image visibly jumping scale as the
   next one arrives over it.

   Keeping .is-leaving on the same target means the outgoing plate goes
   on drifting while it fades, and the two plates move together. It is
   parked back at scale(1) by index.js once it is off screen and invisible,
   with transitions suppressed so the reset itself is never seen.

   The drift is a slow push IN — 1 to 1.06 — never a pull back out. */
.hero-photo.is-running .hero-slide img {
  transition: transform var(--hero-drift, 7600ms) linear;
}

.hero-photo.is-running .hero-slide.is-active img,
.hero-photo.is-running .hero-slide.is-leaving img {
  transform: scale(1.06);
}

/* linear black -> transparent at 35% overall opacity (Figma fill on Bg image) */
.hero-scrim {
  position: absolute;
  inset: 0;
  background: linear-gradient(180deg, rgba(0, 0, 0, 0.35) 0%, rgba(0, 0, 0, 0) 70%);
}

/* white ellipse, layer blur 224 (CSS blur = radius / 2) */
.hero-glow {
  position: absolute;
  left: -194px;
  top: 393px;
  width: 1829px;
  height: 740px;
  background: #fff;
  border-radius: 50%;
  filter: blur(112px);
}

/* -------- navbar -------- */

/* The navbar frame itself carries no fill and no stroke in the design —
   both are flagged invisible — and its background blur does not register
   in the Figma render (row-detail either side of y=94 is identical).
   So the bar is a pure overlay: only the three pills are glass. */
.nav {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: 94px;
  padding: 24px 32px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  z-index: 5;
}

/* mirror-glass: refracted backdrop + a specular rim on the lit edges */
.nav-logo,
.nav-tabs,
.nav-cta,
.nav-tab.is-active {
  backdrop-filter: blur(14px) saturate(180%);
  -webkit-backdrop-filter: blur(14px) saturate(180%);
  border: 1px solid rgba(255, 255, 255, 0.16);
}

.nav-logo {
  width: 140px;
  height: 46px;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.1);
  display: flex;
  align-items: center;
  justify-content: center;
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.38),
    inset 1px 0 0 rgba(255, 255, 255, 0.2),
    inset 0 -1px 0 rgba(255, 255, 255, 0.07),
    0 2px 10px rgba(0, 0, 0, 0.12);
}

.nav-logo img {
  width: 120px;
  height: 26px;
  object-fit: contain;
}

.nav-right {
  display: flex;
  align-items: center;
  gap: 23px;
}

.nav-tabs {
  height: 44px;
  padding: 4px;
  border-radius: 999px;
  background: rgba(251, 248, 243, 0.2);
  display: flex;
  align-items: center;
  gap: 8px;
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.35),
    inset 1px 0 0 rgba(255, 255, 255, 0.18),
    inset 0 -1px 0 rgba(255, 255, 255, 0.06),
    0 1px 4px rgba(0, 0, 0, 0.05);
}

.nav-tab {
  height: 36px;
  padding: 8px 16px;
  border-radius: 999px;
  display: flex;
  align-items: center;
  font-family: var(--dm);
  font-weight: 500;
  font-size: 14px;
  line-height: 20px;
  color: #000;
  white-space: nowrap;
}

.nav-tab.is-active {
  color: #fff;
  /* fill is fully transparent in the design; the lozenge is pure refraction */
  background: transparent;
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.45),
    inset 1px 0 0 rgba(255, 255, 255, 0.3),
    inset -1px 0 0 rgba(255, 255, 255, 0.12),
    inset 0 -1px 0 rgba(255, 255, 255, 0.08),
    0 1px 4px rgba(107, 29, 46, 0.12);
}

.nav-cta {
  height: 44px;
  padding: 8px 20px;
  border-radius: 999px;
  background: rgba(92, 26, 46, 0.4);
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.3),
    inset 1px 0 0 rgba(255, 255, 255, 0.16),
    inset 0 -1px 0 rgba(255, 255, 255, 0.06),
    0 2px 10px rgba(0, 0, 0, 0.14);
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: var(--dm);
  font-weight: 600;
  font-size: 14px;
  line-height: 20px;
  color: #fff;
  white-space: nowrap;
}

/* -------- hero content -------- */

.hero-content {
  position: absolute;
  /* 548 / 855 of the design frame, so the placement matches the Figma at
     the designed height. The min() only engages on short viewports,
     where it reserves enough room below the top of this block for the
     copy, the gap, the button AND air under it — 250px covered the
     block but left the button sitting on the fold. */
  top: min(64.09%, calc(100% - 300px));
  left: 50%;
  transform: translateX(-50%);
  width: 634px;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 32px;
  z-index: 2;
}

.hero-copy {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 16px;
}

.hero-copy h1 {
  width: 680px;
  font-family: var(--serif);
  font-weight: 400;
  font-size: 41px;
  line-height: 52.5px;
  text-align: center;
  color: var(--muted);
}

.hero-copy p {
  font-family: var(--sans);
  font-weight: 400;
  font-size: 20px;
  line-height: 27px;
  text-align: center;
  color: var(--muted);
}

.hero-btn {
  width: 380px;
  height: 44px;
  padding: 12px 21px;
  border-radius: 999px;
  background: #160f0c;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
}

.hero-btn span {
  font-family: var(--jakarta);
  font-weight: 400;
  font-size: 20px;
  line-height: 20px;
  color: #fff;
}

.hero-btn svg {
  width: 12px;
  height: 12px;
  flex: none;
}

/* ============================================================
   GUEST REVIEWS  (1440 x 1394)
============================================================ */

.reviews {
  position: relative;
  height: 1394px;
  overflow: hidden;
}

.reviews-top {
  position: absolute;
  top: 180px;
  left: 0;
  right: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 22px;
  z-index: 2;
}

.reviews-stars {
  display: flex;
  align-items: center;
  gap: 15px;
}

/* Figma: five 40x40 star frames, 33x32 glyph inside, 15px gap */
.reviews-stars svg {
  width: 40px;
  height: 40px;
  padding: 4px 3px;
  box-sizing: border-box;
}

.reviews-headline {
  font-weight: 400;
  text-align: center;
  color: var(--ink-4);
}

.reviews-headline .lg {
  font-size: 45px;
  line-height: 54.5px;
}

.reviews-headline .sm {
  font-size: 35px;
  line-height: 42.4px;
}

.reviews-mid {
  position: absolute;
  top: 782px;
  left: 0;
  right: 0;
  font-weight: 400;
  font-size: 45px;
  line-height: 54.5px;
  text-align: center;
  color: var(--ink-4);
  z-index: 2;
}

.reviews-mid .accent {
  color: #80163a;
}

.reviews-foot {
  position: absolute;
  top: 1357px;
  left: 0;
  right: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 16px;
  z-index: 2;
}

/* the closing beat, in the same maroon the beat before it turns on —
   it is the same voice, pointing at what comes next */
.reviews-foot-lead {
  font-size: 30px;
  line-height: 40.5px;
  color: #80163a;
  text-align: center;
}

/* -------- floating review-card marquee -------- */

.reviews-marquee {
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
}

.rev-col {
  position: absolute;
  top: 0;
  width: 160px;
  animation: rev-drift 22s linear infinite;
}

.rev-col-inner {
  position: relative;
  height: 578px;
}

@keyframes rev-drift {
  from {
    transform: translateY(0);
  }
  to {
    transform: translateY(-578px);
  }
}

.rev-card {
  position: absolute;
  border-radius: 10px;
  background: #fff;
  box-shadow: 0 0 10px rgba(0, 0, 0, 0.25);
  opacity: 0.35;
  padding: 12px;
  display: flex;
  align-items: flex-start;
  gap: 10px;
}

.rev-avatar {
  border-radius: 50%;
  background: #dddddd;
  flex: none;
}

.rev-body {
  display: flex;
  flex-direction: column;
  gap: 6px;
  flex: 1;
  min-width: 0;
}

.rev-stars {
  display: flex;
  gap: 2px;
}

.rev-stars svg {
  flex: none;
}

.rev-line {
  border-radius: 3px;
  background: #ebeae4;
}

.rev-line-short {
  border-radius: 3px;
  background: #f3f2ec;
}

/* ============================================================
   SCROLL SEQUENCE  — "a day on the floor"
============================================================ */

/* The film AND the three screens that close it. They were two sections
   once, the chapter opening on the film's last plate so the handoff
   read as one shot. They are one section now and there is no handoff
   left to read: the film whites out and the words arrive on that same
   white, in the same frame, with no scroll between the problem and the
   answer.

   overflow is hidden because the tools in the formation start parked
   outside the frame and fly in; without it they widen the page. */
/* UNDER THE CURTAIN, A SCREEN EARLY.

   The negative margin is the whole trick. It puts this section's top
   two screens above where the flow would have it — which is the top of
   the story beats' third screen — so by the time the reader is reading
   "keep scrolling to see what their day looks like on the floor", the
   film is already laid in behind it, pinned and showing its first
   plate. The beats stage is opaque and paints over it (z-index 2 to
   this 1), so none of it is visible until the stage travels away.

   TWO screens, not one, and the second is not slack. The beats section
   carries a fourth `.beats-step` that no beat uses, so that its sticky
   stage is still STUCK while the curtain lifts and the lift can be one
   transform rather than a compositor slide with a main-thread
   correction on top of it. See the note on that step in index.html.

   It costs nothing in scroll either way: whatever the margin takes out
   of the document, the extra step and `curtainPx()` put back. Change
   any one of the three and change the others. */
.seq {
  position: relative;
  z-index: 1;
  margin-top: calc(-2 * var(--vh, 100svh));
  background: #fff;
  overflow: hidden;
}

.seq-canvas {
  position: absolute;
  inset: 0;
  display: block;
  width: 100%;
  height: 100%;
}

/* The closing wash to pure white. Driven by the film's own position,
   not by scroll: it is part of the shot, not part of the page. */
.seq-white {
  position: absolute;
  inset: 0;
  background: #fff;
  opacity: 0;
  pointer-events: none;
}

/* ============================================================
   SECTION NAVIGATOR

   Straight off the design's frame: a 36 x 224 column on the right,
   eight diamonds 27.9 apart inside it, the active one drawn large and
   solid and the rest small and faded. The numbers are the design's
   own — 36 wide, 34 from the right edge (264 tall when it had nine
   marks; one pitch less now), the first diamond's
   centre 21 down from the top of the column and the last 21 up from
   the bottom.

   Each button is the full width of the column and one pitch tall, so
   the target is 36 x 28 rather than the 5px mark inside it.

   It is fixed, so it crosses grounds of every shade on its way down
   the page. Maroon is right over the hero and the white chapters and
   invisible over the dark ones, so index.js turns `is-dark` on when
   the chapter under it is one of those.
============================================================ */

.sitenav {
  position: fixed;
  right: 34px;
  top: 50%;
  transform: translateY(-50%);
  z-index: 40;
  width: 36px;
  height: 224px;
  display: flex;
  flex-direction: column;
  justify-content: center;
  /* the page beneath keeps its own pointer events; only the marks take
     a click, or this column would be a 224px dead strip over the rail */
  pointer-events: none;
  /* see `.sitenav.is-away` */
  visibility: visible;
  opacity: 1;
  transition: opacity 320ms ease;
}

/* IT GETS OFF THE FOOTER.

   Eight marks down the right edge are a reading aid for the chapters.
   The footer is not a chapter — it is the end of the page — and by
   the time it is up the navigator has nothing left to say: the last
   dot is already lit and there is nothing below it to point at. What
   is left is a column of diamonds sitting over the social links and
   the copyright line, which is furniture left standing in an empty
   room.

   index.js throws this the moment the footer's top crosses the bottom
   of the navigator's own column — the instant the two would start
   sharing pixels, and not a scroll earlier, because everything above
   the footer inside `.closing` IS a chapter and does have a dot.

   `visibility` and not `display`: the column has to keep its box
   through the fade, and hiding it outright would take the marks away
   in one frame. The delay on the way out is the length of the fade, so
   visibility flips only once there is nothing left to see; coming back
   it is not transitioned at all and the column is there immediately. */
.sitenav.is-away {
  opacity: 0;
  visibility: hidden;
  transition: opacity 320ms ease, visibility 0s linear 320ms;
}

/* the marks carry their own `pointer-events: auto`, so a transparent
   column is still a clickable one for as long as the fade runs */
.sitenav.is-away .sitenav-dot { pointer-events: none; }

.sitenav-dot {
  flex: none;
  height: 27.9px;
  width: 100%;
  padding: 0;
  border: 0;
  background: none;
  cursor: pointer;
  pointer-events: auto;
  display: flex;
  align-items: center;
  justify-content: center;
  -webkit-tap-highlight-color: transparent;
}

/* A diamond, not a dot: a square on its corner. clip-path rather than a
   45deg rotation, so width and height ARE the across-measure and can be
   transitioned without the rotation fighting them. */
.sitenav-dot i {
  display: block;
  width: 5px;
  height: 5px;
  background: rgba(107, 29, 46, 0.4);
  clip-path: polygon(50% 0, 100% 50%, 50% 100%, 0 50%);
  transition: width 320ms cubic-bezier(0.4, 0, 0.2, 1),
              height 320ms cubic-bezier(0.4, 0, 0.2, 1),
              background-color 320ms ease;
}

.sitenav-dot:hover i { background: rgba(107, 29, 46, 0.7); }

.sitenav-dot.is-on i {
  width: 9px;
  height: 9px;
  background: var(--maroon);
}

.sitenav-dot:focus-visible {
  outline: 2px solid var(--maroon);
  outline-offset: -4px;
  border-radius: 4px;
}

/* THE TOOLTIP. A diamond says where you are, not what is there, so a
   hover or a keyboard focus names the chapter off to its left.

   The label is a `.sitenav-tip` that index.js builds inside each mark
   from the button's own `aria-label`, so the name a screen reader hears
   and the name a mouse sees are one string in one place (the tip itself
   is aria-hidden). It is a real element rather than a pseudo so that its
   own ::after can be the pointer toward the mark. It takes no pointer
   events, so it never becomes a target of its own or blocks the page. */
.sitenav-dot {
  position: relative;
}

.sitenav-tip {
  position: absolute;
  right: calc(100% - 2px);
  top: 50%;
  transform: translate(6px, -50%);
  padding: 7px 10px;
  border-radius: 6px;
  background: #fff;
  color: var(--ink);
  box-shadow: 0 2px 10px rgba(26, 20, 18, 0.16);
  font-family: var(--sans);
  font-size: 14px;
  font-weight: 500;
  line-height: 1;
  white-space: nowrap;
  pointer-events: none;
  opacity: 0;
  visibility: hidden;
  transition: opacity 200ms ease, transform 200ms ease, visibility 0s linear 200ms;
}

/* the pointer: a small triangle off the right edge, aimed at the mark */
.sitenav-tip::after {
  content: "";
  position: absolute;
  left: 100%;
  top: 50%;
  margin-top: -5px;
  border-style: solid;
  border-width: 5px 0 5px 6px;
  border-color: transparent transparent transparent #fff;
}

.sitenav-dot:hover .sitenav-tip,
.sitenav-dot:focus-visible .sitenav-tip {
  opacity: 1;
  visibility: visible;
  transform: translate(0, -50%);
  transition: opacity 200ms ease, transform 200ms ease, visibility 0s;
}

/* the mark being pointed at sits in a soft halo, so the reader can see
   which diamond the label belongs to. Behind the diamond, not over it. */
.sitenav-dot::before {
  content: "";
  position: absolute;
  left: 50%;
  top: 50%;
  width: 20px;
  height: 20px;
  margin: -10px 0 0 -10px;
  border-radius: 50%;
  background: rgba(107, 29, 46, 0.12);
  pointer-events: none;
  opacity: 0;
  transform: scale(0.6);
  transition: opacity 200ms ease, transform 200ms ease;
}

.sitenav.is-dark .sitenav-dot::before { background: rgba(255, 255, 255, 0.18); }

.sitenav-dot i { position: relative; }

.sitenav-dot:hover::before,
.sitenav-dot:focus-visible::before {
  opacity: 1;
  transform: scale(1);
}

/* over the dark chapters */
.sitenav.is-dark .sitenav-dot i { background: rgba(255, 255, 255, 0.45); }
.sitenav.is-dark .sitenav-dot:hover i { background: rgba(255, 255, 255, 0.75); }
.sitenav.is-dark .sitenav-dot.is-on i { background: #fff; }
.sitenav.is-dark .sitenav-dot:focus-visible { outline-color: #fff; }

@media (prefers-reduced-motion: reduce) {
  .sitenav-dot i { transition: none; }
  .sitenav-tip,
  .sitenav-dot:hover .sitenav-tip,
  .sitenav-dot:focus-visible .sitenav-tip { transition: none; transform: translate(0, -50%); }
  .sitenav-dot::before { transition: none; transform: none; }
  /* it leaves and returns outright rather than fading */
  .sitenav { transition: none; }
  .sitenav.is-away { transition: none; }
}

/* No room beside the content on a phone, and the chapters are short
   enough there that a thumb gets down the page without help. */
@media (max-width: 1023px) {
  .sitenav { display: none; }
}

/* The washed room behind the diagnosis and the turn.

   z-index 1 puts it over the canvas and its white-out and under
   .layer-in-disc at 2, which is where the words, the tools and the
   maroon live — so this is a ground and never covers anything.

   It appears only once the film is complete. Before that the section
   belongs to the film, and a still of the room sitting on top of the
   footage would read as a hole in it.

   NO TRANSITION, AND THAT IS THE POINT. This used to fade in over
   700ms when the film set `.is-complete`, which is a clock — and the
   chapter on top of it has none. Scrub back up and the room was still
   arriving while the words that are written on it were already
   leaving. The chapter writes this opacity itself now, from the same
   scroll position as everything else it draws, so the room comes and
   goes exactly as fast as the reader does. See ROOM in
   initDiscChapter. */
.seq-ground {
  position: absolute;
  inset: 0;
  z-index: 1;
  background: url("assets/images/post_scrub_bg.svg") center / cover no-repeat;
  opacity: 0;
  pointer-events: none;
}

.seq-loading {
  position: absolute;
  left: 50%;
  bottom: 40px;
  width: 160px;
  height: 2px;
  transform: translateX(-50%);
  background: rgba(26, 20, 18, 0.12);
  border-radius: 2px;
  overflow: hidden;
  opacity: 0;
  transition: opacity 0.4s ease;
  pointer-events: none;
}

.seq-loading.is-visible { opacity: 1; }

/* The reader has scrubbed past what is decoded. The scroll is theirs
   and it keeps moving; what they are looking at is the last plate the
   loader had, held, while it runs to catch up. Say so, rather than
   letting a picture that has gone still read as a broken one. */
.seq.is-waiting .seq-loading { opacity: 1; }

.seq-loading i {
  display: block;
  height: 100%;
  width: 0%;
  background: var(--maroon);
  border-radius: 2px;
  transition: width 0.25s linear;
}

/* ============================================================
   DISCONNECTED SYSTEMS  (2 x 1440 x 855)
============================================================ */

/* ============================================================
   THE DIAGNOSIS, AND THE ANSWER

   One pinned chapter carrying three beats. The section is the washed
   room the film ended on; everything else is staged on top of it and
   driven from index.js — see initDiscChapter.

   The section itself must stay plain: the maroon is NOT its background.
   It arrives as .layer-ground, blooming from the centre, so that the
   tools already on screen are never covered by their own answer.
============================================================ */

/* Each beat owns the whole frame and they cross-fade in place, so the
   reader's eye never has to travel between them.

   THE CROSS-FADE IS A SCROLL DISTANCE, NOT A DURATION. There was a
   620ms transition here and `.is-active` flipped it; that is a clock,
   and the chapter has none. A reader scrubbing back through the seam
   asked for the previous beat and got a fixed-length dissolve playing
   at its own pace underneath a scroll moving at theirs — the two beats
   visibly out of step with the hand driving them. The opacity is
   written from the scroll position now, so the fade IS the gesture.
   See PANELS in initDiscChapter.

   `.is-active` stays for the no-JS and reduced-motion cases, where
   there is no scroll position to read and a beat is simply up. */
.disc-beat {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 72px;
  padding: 0 var(--gutter);
  z-index: 3;
  opacity: 0;
  pointer-events: none;
}

.disc-beat.is-active { opacity: 1; }

.disc-head {
  font-weight: 400;
  font-size: 42px;
  line-height: 56.7px;
  text-align: center;
  color: var(--ink-3);
}

.disc-stats {
  display: flex;
  align-items: stretch;
  justify-content: center;
  gap: 18px;
}

/* NO CARD BEHIND THE FIGURES.

   Each number used to sit on its own pale card — 42% white, 2px radius
   — on the argument that plain text on the blown-out window had nothing
   to hold it. It reads as a panel laid over the film rather than as
   part of it, which is the one thing this chapter cannot afford: the
   room behind is the film's own last frame, and the diagnosis is
   supposed to be written ON it.

   The padding stays. It is what spaces the two figures apart now that
   there is no box for it to be inside of, and `width` still holds them
   to a matched column. Only the surface is gone. */
.disc-stat {
  width: 280px;
  display: flex;
  flex-direction: column;
  gap: 4px;
  padding: 22px 26px 24px;
  background: none;
}

.disc-stat-num {
  font-weight: 400;
  font-size: 42px;
  line-height: 56.7px;
  color: var(--maroon);
}

.disc-stat-label {
  font-weight: 400;
  font-size: 20px;
  line-height: 27px;
  color: #000;
}

/* "This stops now" is set in exactly the same size as the diagnosis it
   replaces. It lands because it is alone on the frame, not because it
   is loud — blowing it up was mine, and the reference does not. */

/* one word at a time, left to right, the way the reference reveals
   every heading in this chapter. index.js wraps the words. */
/* NO `will-change` HERE, and that is the point.

   It was `will-change: opacity, transform`, on every word of every
   split heading on the page — 64 of them, promoted to their own
   compositor layer from first paint and held there for the life of
   the document, for an animation each one runs ONCE and never again.
   Counting the reveal groups too, the page was holding 96 permanent
   layers.

   On the machine this was built on — an M-series Mac with unified
   memory — that costs nothing measurable: frames sit flat at 16.7ms
   with every effect on. On a Windows laptop sharing a couple of
   hundred megabytes of VRAM with the desktop compositor it is exactly
   the shape of thing that makes scrolling stutter, which is the
   difference being reported and the difference this machine cannot
   reproduce.

   Chrome's own guidance is that `will-change` is for shortly before
   an animation, not for the page's lifetime. index.js now sets it
   when a reveal starts and clears it on the last frame, which is the
   pattern `.scroller-track.is-moving` a few hundred lines up already
   uses correctly. */
.w {
  display: inline-block;
  opacity: 0;
  transform: translate3d(0, 0.32em, 0);
}

/* The spaces between words are TEXT NODES now — see splitWords in
   index.js. There was a `.w-space { white-space: pre }` span here, and
   a preserved space does not hang at a line break: it stayed in the
   line box and pushed every wrapped line off centre, and it kept its
   width while the browser chose where to break, costing a whole extra
   line on a phone. Ordinary whitespace between two inline-blocks
   renders perfectly well without help. */

/* Each LINE of a multi-line heading is its own step, so it needs to be
   its own box. Direct children only: the word spans the splitter puts
   inside these lines are also spans, and a descendant selector here
   turned every word into a block — one word per line, down the page. */
.reviews-headline > .lg,
.reviews-headline > .sm,
.reviews-mid-text > span { display: block; }


/* ============================================================
   ONE LAYER  (1440 x 895)
============================================================ */

.layer {
  position: relative;
  height: 895px;
  background: var(--maroon-radial);
  box-shadow: 0 4px 54px 5px rgba(0, 0, 0, 0.25);
  overflow: hidden;
  /* the same 1440 canvas as .slide, and the same reason — see there */
  justify-content: center;
}

.layer-texture {
  position: absolute;
  left: -420px;
  top: -298px;
  width: 2264px;
  height: 1508px;
  opacity: 0.1;
  object-fit: cover;
  /* Figma composites this at 10% on LINEAR_BURN. CSS has no linear-burn;
     color-burn is its nearest relative and at this opacity it lands very
     close. Plain alpha (what this was) lightens and flattens the
     gradient underneath instead of texturing it. */
  mix-blend-mode: color-burn;
}

/* 899px is the Figma's measurement, and it assumed a font this site
   never serves: nothing loads Plus Jakarta Sans, so the heading is set
   in whatever the machine falls back to. In Helvetica Neue it needs
   864 of those 899 — four per cent of headroom — and any fallback even
   slightly wider wraps it to two lines.

   Two lines is what does the damage. It pushes the subtitle down 48px,
   and the subtitle already clears the orbit by only 11: the outer ring
   carries an orb at its apex (top 181) and the subtitle ends at 170. So
   a font substitution nobody can see coming lands the tools on top of
   the copy.

   Widening the box changes nothing about how the heading looks — it is
   centred, and one line stays one line in the same place — it only moves
   the width at which it breaks, from 4% away to about 34%. */
.layer-titles {
  position: absolute;
  top: 83px;
  left: 50%;
  transform: translateX(-50%);
  width: min(1160px, 90%);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 11px;
  z-index: 2;
}

.layer-titles h2 {
  font-family: var(--jakarta);
  font-weight: 400;
  font-size: 41.8px;
  line-height: 48.4px;
  text-align: center;
  color: #fff;
}

.layer-titles p {
  font-family: var(--jakarta);
  font-weight: 400;
  font-size: 19.1px;
  line-height: 28px;
  letter-spacing: -0.4px;
  text-align: center;
  color: #fff;
}

/* coordinates below are Figma positions relative to the section origin */
.layer-orbit {
  position: absolute;
  inset: 0;
  z-index: 2;
}

/* TWO dashed 2px strokes in #E0D7D7, carried at 20%, concentric on
   (713, 455).

   A note here used to claim the file draws only one, and that a second
   was me designing rather than building. That was wrong: the frame has
   an inner stroke and an outer one, and the tools ride the OUTER. What
   had been built was the inner ring's radius with the tools sitting on
   it, which is why the formation read tight — the tools were orbiting
   the wrong circle.

   ALL THREE ARE `<svg>` NOW, not `border: dashed`, so the dash
   pattern is the file's rather than the browser's and each stroke
   carries its own `stroke-opacity` in index.html — 1, 0.5 and 0.2,
   outermost to innermost. These rules keep the boxes and the origins;
   the strokes sit inside them exactly where the border-box borders did.

   Both rest at 0 because the formation brings them in; the orbit
   timeline animates them to STROKE_OPACITY, which is the file's global
   20% renegotiated against the maroon — see index.js. */
.layer-ring {
  position: absolute;
  left: 544px;
  top: 286px;
  width: 340px;
  height: 340px;
  opacity: 0;
}

/* THE THIRD STROKE, inside the other two.

   Not from the file — the file draws two — but the same construction:
   concentric on (713.5, 455.5), and at the same fraction of the outer
   stroke through every rescale. 247 wide, so left/top are 713.5 -
   123.5 and 455.5 - 123.5. It clears the 158px mark by 45px a side,
   which is enough that the two read as separate circles rather than as
   a halo on the logo.

   IT IS NO LONGER DRAWN BY THE SAME RULE as the other two. They are
   still `border: dashed`, whose pattern is the browser's to pick; this
   one is an SVG circle drawn to the file's dashes. Everything else is
   unchanged — same box, same origin, same opacity, and it turns with
   them; see `ring-turn` below for the three paces.

   The box still says 247 because the stroke is inside it: `box-sizing:
   border-box` put the old 3px border between r=120.5 and r=123.5, and
   the circle's r=122 with a 3px stroke occupies exactly that band.

   The 0.2 the border carried now lives on the circle's `stroke-opacity`
   in index.html. Same weight, and still multiplied by whatever the
   formation has faded this element to. */
.layer-ring-in {
  position: absolute;
  left: 590px;
  top: 332px;
  width: 247px;
  height: 247px;
  opacity: 0;
}

/* The one the tools ride.

   THE SCALE OF THE WHOLE FORMATION LIVES HERE, and it is one number.

   The file draws the outer stroke at r=238 and the inner at r=178.
   Those were taken to 0.8 of that (190 and 142) because the formation
   read too large for the frame, and are now at 0.92 (219 and 164) —
   still inside the frame and a good deal more present, which is what
   it was asked for.

   NOTHING HERE MOVES ON ITS OWN. The tools are 72px wide and centred
   ON the outer stroke, so a stroke that changes size without them
   leaves seven logos floating beside a circle they are supposed to be
   standing on. The three strokes and the seven `left`/`top` values
   below are all one rescale about (713.5, 455.5), which holds every
   tool at the angle the file places it and every stroke concentric.

   To change the scale again: take the FILE's radii (238 / 178), not
   these, apply the new factor, and re-derive the seven tools from
   their ANGLES rather than by rescaling the pixel values below —

     left = 713.5 + r*cos(angle) - 36
     top  = 455.5 + r*sin(angle) - 36

   with the angles listed under .layer-ring-outer. Rescaling the
   rounded coordinates instead compounds: doing it that way on this
   pass put the seven tools between r=216.6 and r=220.7 against a
   stroke at 219, and doing it twice more would show. Derived from the
   angles they sit between 218.5 and 219.5, which is the round-to-whole
   pixels and nothing else. */
.layer-ring-outer {
  position: absolute;
  left: 495px;
  top: 237px;
  width: 438px;
  height: 438px;
  opacity: 0;
}

/* THE STROKES TURN. THE TOOLS DO NOT.

   This used to be one rotation on a GROUP holding the outer stroke and
   all seven tools, so that the tools kept their spacing on the circle
   as it went round. It worked, and it was the wrong idea: a logo is a
   thing you read, and seven of them creeping round a circle is seven
   things that will not sit still long enough to be read. Worse, it
   needed a second animation to counter-rotate every logo inside its
   own orb so that "HRMS" did not arrive upside down at the bottom —
   fourteen animations to keep seven pictures upright.

   The tools are simply placed now, and what turns is the three dashed
   strokes they sit around. Nothing has to be kept upright, nothing
   creeps, and the movement is where movement belongs: on a dashed
   circle, which is a thing whose only job is to look like it is
   turning.

   `.orbit-spin` survives as a plain wrapper — it still groups the
   outer stroke with the tools for layout, and initOrbitFormation
   measures offsets through it — but it no longer moves. */
.orbit-spin {
  position: absolute;
  inset: 0;
}

/* `rotate`, NOT `transform`.

   The formation timeline owns `transform` on `.layer-ring` — it scales
   the stroke up from 0.8 as the ring forms — so a rotation written
   there would be overwritten sixty times a second, which is the same
   trap the tools' drift note describes. The individual `rotate`
   property is composed with `transform` by the browser rather than
   replacing it, so GSAP keeps the scale and this keeps the turn. */
@keyframes ring-turn {
  to { rotate: 360deg; }
}

/* THREE STROKES, ONE DIRECTION, THREE PACES, OUTERMOST FASTEST.

   ALL THREE TURN CLOCKWISE. The middle one used to run `reverse`, on
   the idea that counter-rotation reads as separate circles — it does,
   and it also reads as two mechanisms rather than one formation, which
   is the opposite of what this beat is saying. Three circles going the
   same way at three speeds are still unmistakably three circles.

   The paces do that work on their own. Equal paces and the dashes hold
   their relative positions for ever, which the eye reads as a single
   rigid object; different ones and the pattern between them never
   repeats.

   Outermost fastest is the opposite of orbital mechanics and the right
   way round here — the eye is drawn to the fastest thing, and the
   outer stroke is the one describing the shape of the whole formation.
   The durations are deliberately not multiples of each other, so no
   two of them come back into alignment on any cycle a reader would sit
   through. */
.layer-ring-outer { animation: ring-turn 38s linear infinite; }
.layer-ring       { animation: ring-turn 53s linear infinite; }
.layer-ring-in    { animation: ring-turn 71s linear infinite; }

/* AND THEY TURN ONLY ONCE THERE IS SOMETHING TO TURN.

   `is-formed` is the frame the last tool reaches the ring. Before it
   the strokes are still at opacity 0 and there is nothing to see, but
   an animation running against nothing is still an animation running,
   and this chapter is most of a minute of scroll away from being
   looked at.

   Absent until then rather than paused, which leaves each stroke at
   0deg — the angle it would start from anyway — so switching them on
   is not a jump from anywhere. */
.layer-in-disc:not(.is-formed) .layer-ring,
.layer-in-disc:not(.is-formed) .layer-ring-outer,
.layer-in-disc:not(.is-formed) .layer-ring-in {
  animation: none;
}

/* AND THEY ARE VISIBLE ONCE THEY ARE FORMED, without needing a tween
   to say so.

   The three strokes ship at opacity 0 and it is initOrbitFormation
   that raises them — but that function returns early under reduced
   motion, so the reduced-motion reader got the mark and seven logos
   arranged around a circle that was not drawn. The formation with the
   formation missing.

   A rule rather than another branch in the JS: GSAP writes opacity
   inline on every frame it drives, and inline beats a stylesheet, so
   this cannot fight the scrub. It only ever applies where nothing else
   has an opinion. Keep the number in step with STROKE_OPACITY in
   index.js. */
.layer.is-formed .layer-ring,
.layer.is-formed .layer-ring-outer,
.layer.is-formed .layer-ring-in {
  opacity: 0.42;
}

/* WHAT THEY DO INSTEAD: next to nothing, on purpose.

   Enough wander that the corners are not dead, and no more. These are
   the hotel's own software sitting around the edges of a problem —
   present, not a thing to watch — so the furthest any of them gets
   from where it is parked is about four pixels. The path closes on
   itself, so the loop never ends on a snap back to the start.

   `translate`, NOT `transform`. GSAP owns `transform` on these: it is
   what flies them in from off-frame and what carries them home, and
   two owners on one property is the oldest bug in this file — it is
   what the `ring-turn` note above is still working around. The
   individual `translate` property is COMPOSED with `transform` by the
   browser rather than replacing it, so the drift rides on top of
   wherever the scroll has put them and neither side needs to know the
   other exists. */
@keyframes orb-adrift {
  0%   { translate: 0 0; }
  20%  { translate: 3px -2px; }
  40%  { translate: 4px 2px; }
  60%  { translate: -1px 4px; }
  80%  { translate: -3px 1px; }
  100% { translate: 0 0; }
}

.layer-in-disc:not(.is-grounded) .orb {
  animation: orb-adrift 9s ease-in-out infinite;
}

/* A different length and a different starting phase each, so they are
   not seven things breathing in time — which is what would turn four
   pixels of drift into one visible pulse. The negative delays start
   each of them part-way through its own loop on the first frame. */
.layer-in-disc:not(.is-grounded) .orb-excel  { animation-duration: 8.3s;  animation-delay: -1.1s; }
.layer-in-disc:not(.is-grounded) .orb-pos    { animation-duration: 10.7s; animation-delay: -3.4s; }
.layer-in-disc:not(.is-grounded) .orb-chev   { animation-duration: 9.1s;  animation-delay: -5.2s; }
.layer-in-disc:not(.is-grounded) .orb-lms    { animation-duration: 11.3s; animation-delay: -2.3s; }
.layer-in-disc:not(.is-grounded) .orb-oracle { animation-duration: 8.9s;  animation-delay: -6.7s; }
.layer-in-disc:not(.is-grounded) .orb-hrms   { animation-duration: 10.1s; animation-delay: -0.6s; }
.layer-in-disc:not(.is-grounded) .orb-wa     { animation-duration: 9.7s;  animation-delay: -4.2s; }

/* A ring that turns forever is exactly the kind of thing that makes a
   page unusable for someone who asked the OS for less movement — and
   so is a logo that never quite settles. */
@media (prefers-reduced-motion: reduce) {
  .layer-ring,
  .layer-ring-outer,
  .layer-ring-in,
  .orb { animation: none; }
}

.layer-mark {
  position: absolute;
  left: 634px;
  top: 376px;
  width: 158px;
  height: 158px;
  object-fit: contain;
}

/* THE TOOLS: A LOGO IN A LENS.

   Every one of the seven is the same object in the design — its logo,
   held inside a glass disc a little larger than it. The
   glass is what makes seven unrelated marks read as one set, so all
   seven carry it, Excel and the chevrons included; an earlier pass had
   stripped it off because the glass was drawn at the logo's own size,
   which is a circle behind a circle. Drawn LARGER than the logo it is
   a rim, and a rim is the look.

   The glass is a pseudo-element and not the orb's own box, on purpose:
   the orb's 72px box is what the positions below, the fly-in
   (initOrbitFormation reads each orb's offset box) and the drift are
   all measured against. The lens spills 10px past it on every side and
   moves nothing that was placed. `z-index: 1` makes the
   orb its own stacking context, so the lens's -1 sits behind the logo
   and not behind the ring. */
.orb {
  position: absolute;
  z-index: 1;
  width: 72px;
  height: 72px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
}

/* THE LENS: 92px of glass round a 58px logo, built on the file's
   white fill (after Figma's Frame 1000005104, thinned to 3% so it is
   see-through) and given the things that make a flat tint read as
   glass:

     sheen    light pooling across the top of the disc, as off a lens
     depth    a faint glow inside the edge, and a soft shadow under it
              so it sits ON the ground rather than printed into it
     rim      ::after — a 1px edge caught bright at the top-left and
              gone by the bottom-right, the light's direction

   Both layers are pseudo-elements at z-index -1 inside the orb's own
   stacking context, so they sit behind the logo and in front of the
   ring. */
.orb::before,
.orb::after {
  content: "";
  position: absolute;
  inset: -10px;               /* a 92px lens on the 72px slot */
  z-index: -1;
  border-radius: 999px;
  transition: opacity 900ms linear;
}

.orb::before {
  /* Kept thin so the maroon and the ring read THROUGH it: the disc is
     mostly edge and highlight, with very little body of its own. */
  background:
    radial-gradient(120% 70% at 30% 0%, rgba(255, 255, 255, 0.12), rgba(255, 255, 255, 0) 60%),
    linear-gradient(160deg, rgba(255, 255, 255, 0.06), rgba(255, 255, 255, 0.01) 55%, rgba(255, 255, 255, 0.04)),
    rgba(255, 255, 255, 0.03);
  box-shadow:
    inset 0 0 14px rgba(255, 255, 255, 0.05),
    0 8px 24px rgba(18, 0, 7, 0.2);
  /* no backdrop blur: the dashed ring has to stay legible through the
     disc, and any blur smears its dashes away under the glass */
}

.orb::after {
  padding: 1px;
  background: linear-gradient(
    140deg,
    rgba(255, 255, 255, 0.45),
    rgba(255, 255, 255, 0.1) 40%,
    rgba(255, 255, 255, 0.04) 70%,
    rgba(255, 255, 255, 0.2)
  );
  /* only the 1px ring survives: the content box is cut out of the
     border box */
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
  mask: linear-gradient(#000 0 0) content-box exclude, linear-gradient(#000 0 0);
  pointer-events: none;
}

/* Glass is only glass over something. While the tools are scattered
   over the washed room, before the maroon blooms, a 10% white lens on
   a blown-out photograph is a grey smudge — so the lens waits for the
   ground and fades in with it, and what you see on the maroon is the
   design's glass. */
.layer-in-disc:not(.is-grounded) .orb::before,
.layer-in-disc:not(.is-grounded) .orb::after {
  opacity: 0;
}

/* The logo holds at 58px, the file's own size; the 92px lens round
   it leaves 17px of glass on every side.
   Every logo file is a disc of its own (white, or pink for POS), so
   each is clipped round rather than trusted to be. */
.orb img {
  width: 58px;
  height: 58px;
  border-radius: 50%;
  object-fit: contain;
}

/* The tools ride the OUTER ring, r=219 from (713.5, 455.5) — the file's
   238, scaled with all three strokes; see .layer-ring-outer — at the
   angles the design places them, clockwise from the top:

     excel  -97   pos   -45   chev   6   lms (eZee)   55
     oracle 110   hrms  166   wa  -147

   left = 713.5 + 219·cos(a) − 36, top = 455.5 + 219·sin(a) − 36.

   Deliberately NOT symmetric: the widest gap is the one between Excel
   and WhatsApp over the top-left, and evening it out is what makes it
   stop looking like the design.

   The fly-in needs no matching change: initOrbit derives each tool's
   OFF and PARK positions from its own offset against the ring centre,
   and sorts them by angle for the sweep, so moving these moves the
   direction each arrives from too. */
.orb-excel  { left: 651px; top: 202px; }
.orb-pos    { left: 832px; top: 265px; }
.orb-chev   { left: 895px; top: 442px; }
.orb-lms    { left: 803px; top: 599px; }
.orb-oracle { left: 603px; top: 625px; }
.orb-hrms   { left: 465px; top: 472px; }
.orb-wa     { left: 494px; top: 300px; }

/* ============================================================
   PEOPLE AT THE CENTER  (1440 x 855)
============================================================ */

/* ============================================================
   WITH PEOPLE AT THE CENTER

   No longer a section of its own — it is the last screen of the film's
   section, revealed out of the formed ring. Held at opacity 0 rather
   than display:none so the photographs decode while the ring is still
   on screen; a reveal that opens onto an undecoded image is a grey hole.
============================================================ */
.people {
  position: absolute;
  inset: 0;
  z-index: 3;
  overflow: hidden;
  display: flex;
  align-items: center;
  justify-content: center;
  pointer-events: none;
  /* Off until the ring hands over to it; paintReveal() opens it with
     a radial mask from the centre. Opacity rather than display:none or visibility so the
     photographs still decode while the film is playing. */
  opacity: 0;
}

.people-img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;

  /* THE CLOSING SHOT PUSHES IN, and it is driven from index.js — see
     zoom() in initDiscChapter. It scales in with the radial reveal, on
     the reveal's own curve, and carries straight on at the speed the
     reveal left it at, easing out over the seconds after. It begins at
     the plate's own framing: the push is the camera continuing towards
     the shot, never opening zoomed and settling back. */
  transform: scale(1);
  transform-origin: center;
}

.people-scrim {
  position: absolute;
  inset: 0;
  background: rgba(0, 0, 0, 0.3);
}

/* DEAD CENTRE, not the Figma's offset.

   The file sets this caption 51.5px right of the canvas centre and a
   little above the vertical middle (400 of 855). On the canvas, beside
   everything else in the frame, that offset is a composition. On its
   own — and this line IS on its own, one sentence over a photograph
   with nothing to balance against — it reads as a line that missed.

   So it is centred on both axes. The width and the wrap are unchanged;
   only where the box sits. */
.center-title {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: 540px;
  white-space: nowrap;
  font-weight: 400;
  font-size: 41.8px;
  line-height: 48.4px;
  text-align: center;
  color: #fff;
}

/* ============================================================
   ORCHESTRATION HEADING + SCROLLER
============================================================ */

.orchestration {
  height: 323px;
  padding-top: 207px;
  background: #fff;
}

/* The design's own values. The 42.9 / -1.04 this had before were solved
   against Satoshi, which is what it rendered in while Jakarta was never
   loaded; with the real face the panel's -2.2 is the right tracking. */
.orchestration h2 {
  /* Satoshi 400, per the design — not Plus Jakarta */
  font-family: var(--sans);
  font-weight: 400;
  font-size: 41.8px;
  line-height: 48.4px;
  letter-spacing: -2.2px;
  color: var(--ink-5);
  text-align: center;
}

/* Opaque, deliberately. Pinning makes this section `position: fixed`,
   and a transparent one lets whatever it is passing show through any
   sub-pixel sliver at its top or bottom edge — which here is the dark
   scroll-sequence directly above it, so the slivers read as a flicker
   along the top and bottom of the screen as the pin engages and
   releases. A ground of its own leaves nothing to show through. */
/* IT PAINTS OVER WHAT RIDES UP INTO IT.

   `.stats` below carries `margin-top: -56px`, on the reasoning that it
   overlaps "the tail of the one before it, which at these distances is
   empty white on both sides". True of every other boundary on the page
   and false of this one: the section before `.stats` is this rail, and
   this rail is PINNED. For the length of that pin it is `position:
   fixed` and fills the screen, so its "tail" is not empty white — it is
   the bottom of the outcome cards.

   So the last 56px of the pin had a white section painted across it,
   cutting the cards off flat, and it began exactly where the reader
   reaches the fourth card and pushes on.

   A stacking order settles it and costs nothing. Both sides of that
   overlap are white once the pin has released — this section's tail and
   `.stats`'s 200px of padding — so which one wins is invisible there;
   the margin still closes the gap in flow, which is all it was ever
   for. While the pin is engaged, the rail wins, which is the whole
   point. Below 1024 the negative margins do not exist and neither does
   the problem.

   1, not more: the section navigator sits at 40 and must stay over
   everything. */
.scroller {
  position: relative;
  z-index: 1;
  background: #fff;
  overflow-x: auto;
  overflow-y: hidden;
  scroll-snap-type: x mandatory;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
}

.scroller::-webkit-scrollbar {
  display: none;
}

/* THE FRAME THE CARDS SLIDE BEHIND.

   It clips them, and it is rounded, so a card halfway out of the rail
   keeps a corner instead of ending in a flat vertical slab the full
   height of the card. Its radius is the card's own 21px, so a card
   resting flush inside it and the frame itself share one outline.

   `height: auto` on purpose: the track's only content is the cards, so
   the frame is exactly a card tall and its rounded corners land on the
   card's corners rather than somewhere out in the white.

   THE INSET LIVES HERE NOW, as this element's margin. It used to be
   `.scroller-track`'s horizontal padding. Same numbers, moved one level
   out — padding would have sat INSIDE the clip and left the cards
   touching the screen edges again. */
.scroller-window {
  /* auto, NOT hidden, and that is the reduced-motion reader's rail.

     initHorizontalScroller returns early under `reduced` and for anyone
     without ScrollTrigger, and then nothing translates the track — the
     rail is scrolled natively, the way it was before this frame existed.
     A `hidden` window would clip three of the four cards away with no
     way to reach them. `auto` clips exactly the same when something
     else is driving (see the overflow JS sets in that function) and
     leaves the cards reachable when nothing is.

     The rounding applies either way: a scroll container still clips to
     its border radius. */
  overflow-x: auto;
  overflow-y: hidden;
  scroll-snap-type: x mandatory;
  scrollbar-width: none;
  -webkit-overflow-scrolling: touch;
  border-radius: 21px;
  margin: 0 89px 0 76px;

  /* `.scroller` is a column flex that CENTRES its children, so left to
     itself this box sizes to its content — and its content is a
     4613px track, which is the whole rail and clips nothing. Stretch
     is what makes it the width of the section instead. `min-width: 0`
     because the track's `max-content` width would otherwise be a floor
     under it. */
  align-self: stretch;
  min-width: 0;
}

.scroller-window::-webkit-scrollbar {
  display: none;
}

.scroller-track {
  display: flex;
  align-items: center;
  gap: 85px;
  padding: 0;
  width: max-content;
  backface-visibility: hidden;
}

/* set by ScrollTrigger only while the rail is on screen */
.scroller-track.is-moving {
  will-change: transform;
}

.ocard {
  position: relative;
  flex: none;
  width: var(--ocw);
  height: var(--och);
  /* 24 in the file (`rx="24"` on the 900x500 rect); it was 21 here */
  border-radius: 24px;
  background: var(--maroon-radial);
  overflow: hidden;
  scroll-snap-align: center;
}

/* Placed so the INK lands where the design's ink lands, not so the
   boxes agree: the title's cap top at 72 from the card's top, the
   paragraph's at 165.5. The 1px and the 3px here are the difference
   between a box edge and the first mark inside it. */
.ocard-head {
  position: absolute;
  left: 48px;
  top: 64px;
  width: 507px;
  display: flex;
  flex-direction: column;
  gap: 11px;
}

/* All four share the left edge in the design; the 67/68/82 that stood
   here were the spread of a measurement, not a decision. */

/* Two lines, broken where the design breaks them — subject on the
   first, what it does on the second. The `<br>` in the markup is the
   break; nothing here should be asked to find it.

   This used to be `white-space: nowrap`, from an earlier frame where
   the titles were single-line and sized to hug their text. They are
   not any more, and nowrap was holding them on one line past the edge
   of the 507px head box.

   Size and tracking are unchanged and were already right: 37.5/-1.63
   at weight 500 measures 183.3px for "Your Guests" against the
   design's 183.2. The leading is the part that moved — 42.2 between
   baselines, where one line had no leading to show. */
.ocard-head h3 {
  font-family: var(--jakarta);
  font-weight: 500;
  font-size: 37.5px;
  line-height: 42.2px;
  letter-spacing: -1.63px;
  color: #fff;
}

.ocard-head p {
  font-family: var(--jakarta);
  font-weight: 400;
  font-size: 13.9px;
  line-height: 20.7px;
  letter-spacing: -0.3px;
  color: #fff;
}

/* 263, not 191. The pills sit at 52.6% of the card's height in the
   design and were sitting at 29.4% here — the single most visible
   consequence of the card having been the wrong box. */
.ocard-pills {
  position: absolute;
  left: 48px;
  top: 263px;
  list-style: none;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 20px;
}


.ocard-pills li {
  height: 34px;
  padding: 4px 16px;
  border-radius: 999px;
  background: rgba(247, 247, 245, 0.2);

  /* THE HAIRLINE IS 4% OF ITSELF, NOT ALL OF IT.

     The file strokes each pill `#4E4846` at `stroke-opacity="0.04"`.
     This carried the colour and dropped the opacity, so a near-invisible
     seam was drawn as a solid near-black ring — on a maroon card that is
     the difference between a translucent capsule and an outlined button,
     and it is the loudest thing in the pill. */
  border: 1px solid rgba(78, 72, 70, 0.04);
  display: flex;
  align-items: center;
  gap: 10px;
  font-family: var(--jakarta);
  font-weight: 400;
  font-size: 13.3px;
  line-height: 24px;
  color: #fff;
  white-space: nowrap;
}

.ocard-pills img {
  width: 18px;
  height: 18px;
  flex: none;
}

/* The design's right-hand panel: it starts 546 across (measured off
   two chip-free rows, which agree to 2px) and runs the card's full
   height to the bottom edge. It was 603/173/489x681, which belonged to
   the 1065x649 box and now overflows the card on both axes.

   COVER, not contain. The exports are 987x2270 — a 2x asset, 493 wide
   at 1x against this frame's 354, so it scales by width and the rest
   falls off the bottom the way the card's own `overflow: hidden`
   already expects. `contain` would fit by HEIGHT instead and paint the
   whole 2270 into 500 — 217px wide inside a 354px box, the mockup
   looking small and zoomed out next to the file.

   NOTE: the artwork itself is still the old export. The design draws
   this panel DARK, with the chips and the department list sitting
   straight on the maroon; assets/images/mock-N.png is the earlier
   version, where the same flow sits on a white card. That is a
   re-export, not a CSS change. */
/* THE PANEL'S OWN BOX, OFF THE FILE.

   546/354 was the bitmap's box. The panel is a layer in the design now
   (`x=600.881 y=54.5986 w=359 h=500 rx=24` against a card at x=58.881,
   y=54.5986), which is 542 from the card's left edge and 359 across —
   4px further left and 5px wider than the bitmap was.

   It runs 1px past the card's right edge in the file too; `.ocard`
   clips that, exactly as the card's own clip path does in Figma.

   `object-fit` is gone with the bitmap: the SVG carries its own
   359x500 viewBox and scales itself. It is absolutely positioned, so
   none of this is measurable by the rail — see railTravel(), which
   reads the track, not the cards' contents. */
.ocard-mock {
  position: absolute;
  left: 542px;
  top: 0;
  width: 359px;
  height: 500px;
}

/* ============================================================
   ONE SYSTEM — STATS  (1440 x 833)
============================================================ */

.stats {
  height: 833px;
  padding: 200px var(--gutter);
  background: #fff;
}

.stats-header {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0;
}

.stats-header h2 {
  font-weight: 400;
  font-size: 50.3px;
  line-height: 67.9px;
  text-align: center;
  color: var(--ink-2);
}

.stats-header p {
  font-weight: 400;
  font-size: 32px;
  line-height: 48px;
  text-align: center;
  color: var(--muted-2);
}

/* Four 280s and three 40s come to 1240 — exactly the content width at
   1440, and wider than it at every laptop size below. The section
   centres its children, so this row shrink-wrapped to its own content
   and hung 70px off each side of a 1100 viewport. It never showed up as
   page overflow because `.stats.slide` clips; it showed up as the first
   and last stat card with their outer edges sliced off. */
.stats-row {
  margin-top: 90px;
  max-width: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 40px;
}

.stat-card {
  /* 280 where there is room, a share of what there is where there is
     not — the alternative at 1100 was four full-width cards with the
     outer two cut in half */
  flex: 0 1 280px;
  min-width: 0;
  width: 280px;
  height: 227px;
  padding: 72px 20px;
  border-radius: 20px;
  background: #fff;
  box-shadow: 0 4px 16px -2px rgba(108, 43, 56, 0.1);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 4px;
}

.stat-num {
  font-weight: 400;
  font-size: 42px;
  line-height: 56.7px;
  color: var(--maroon);
  /* The counter rewrites this text from 0 upward, so the BOX must not be
     sized by whatever it currently reads. Left to hug its content, the
     span is one digit wide on one frame and two on the next, and since
     the card centres its children the figure re-centres every time it
     gains a digit — a visible shimmy all the way up. Full width of the
     card with the text centred inside it: the box never moves and only
     the glyphs do. Tabular figures stop the digits jostling each other
     as they cycle, which is the same defect one place smaller. */
  align-self: stretch;
  text-align: center;
  font-variant-numeric: tabular-nums;
}

.stat-label {
  display: flex;
  align-items: center;
  gap: 4px;
  font-weight: 400;
  font-size: 16px;
  line-height: 21.6px;
  color: #000;
  white-space: nowrap;
}

/* 18, which is the frame every icon in this file is drawn on — the
   exports in assets/icons are all `viewBox="0 0 18 18"` with a 13.5
   glyph inside, and the stat icons are the same set. It was 17, which
   is the size of the glyph in the one icon that had been eyeballed
   rather than exported. */
.stat-label svg {
  width: 18px;
  height: 18px;
  flex: none;
}

/* ============================================================
   TRUSTED BY LEADERS  (1440 x 833)

   One card visible at a time. Both cards occupy the SAME grid
   cell, so the stage is drawn to the taller quote and the card
   never changes size as the slider hands over — the thing that
   makes an auto-advancing quote feel like a page jump.

   The inactive card keeps its space but leaves the accessibility
   tree and the pointer: visibility, not display, because a
   display:none card has no box to measure and the stage would
   collapse to whichever quote happened to be showing.
============================================================ */

/* The padding is what keeps this chapter SHORTER than a screen.
   Title + card + dots come to ~664; 100 top and bottom leaves the
   section under 100svh at the design height, so `.leaders.slide`
   centres it in the screen rather than overrunning it — which matters
   because the dots are both the control and the only sign that another
   quote is coming, and a section taller than the viewport parks them
   below the fold.

   This used to be a snap stop and the note here said so. It is not one
   any more, and the reason above is the one that survives: it is about
   the dots being visible when the section is, not about where a snap
   lattice comes to rest. */
.leaders {
  padding: 100px var(--gutter);
  background: #fff;
}

.leaders-title {
  font-weight: 400;
  font-size: 50.3px;
  line-height: 67.9px;
  text-align: center;
  color: var(--ink-2);
}

/* Just the content box — which is to say, the same gutter as every
   other section on the page.

   This was `min(86.11vw, 100%)` under a 1560px cap, from when the
   gutter was a hard 100px and the card had to work out its own share
   of the screen. With --gutter proportional, 100% IS the drawn width
   (1240 at 1440) and stays the drawn proportion at every other width,
   so the vw and the cap were both doing a job that no longer exists.

   The cap was actively wrong once the gutter moved: it stopped the
   card at 1560, so on a 27-inch screen this section sat in a 500px
   margin while the FAQ and the trust band beside it sat in 178px —
   the section reading as differently spaced from its neighbours at
   exactly the sizes where that is most obvious. */
/* The cards all sit in ONE grid cell and are moved sideways from
   there, so the cell — and with it the section — is as tall as the
   longest quote and never resizes mid-hand-off.

   `overflow: hidden` is what makes it a slider rather than three
   cards flying across the gutter. The 24px of padding is room for
   the card's own shadow, which the clip would otherwise shave off
   along the sides; the negative margin hands that width back, so
   the card still measures the full content width. That padding also
   sits between the title and the card, so the visible gap is the
   margin plus 24. */
.leaders-stage {
  margin: 60px -24px 0;
  width: calc(100% + 48px);
  padding: 24px;
  overflow: hidden;
  display: grid;
  align-items: stretch;
  /* claim the horizontal axis for the swipe, leave the vertical one
     to the page — a diagonal drag still scrolls */
  touch-action: pan-y;
}

/* Only while a drag is actually running: a mouse-down that turns
   into a swipe should not paint the quote blue on its way past. */
.leaders-stage.is-dragging {
  cursor: grabbing;
  user-select: none;
  -webkit-user-select: none;
}

/* Holds the <symbol> that every `.leader-mark` references. Out of
   flow and zero-sized: it draws nothing itself. */
.leaders-defs {
  position: absolute;
  width: 0;
  height: 0;
  overflow: hidden;
}

.leader-card {
  grid-area: 1 / 1;
  position: relative;
  overflow: hidden;
  margin: 0;
  padding: 104px 90px 76px;
  border-radius: 24px;
  background: var(--cream);
  box-shadow: 0 4px 16px -2px rgba(108, 43, 56, 0.1);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 56px;
  /* the neighbours are parked a full step out and clipped away, so
     nothing here needs to be hidden — and a hidden card cannot be
     dragged back in */
  will-change: transform;
}

/* The oversized opening quote, tucked into the corner and clipped
   by the card's own radius exactly as it is in the design.

   Drawn, not typed, and now the design's own drawing: the path is
   the Figma vector, and the numbers here are its numbers — 187x134
   at -15, -33 from the card's top-left, which is where the file puts
   it relative to the card. A quarter of the first comma falls outside
   the corner and is clipped, in the file and here both.

   It was typed once, as a 300px `\201C` in var(--sans), which is fine
   until you notice that no webfont is served here at all — the sans
   resolves to Helvetica Neue, whose quote is a pair of straight
   wedges, and clipped at the top it read as two bare rectangles. */
.leader-mark {
  position: absolute;
  left: -15px;
  top: -33px;
  width: 187px;
  height: 134px;
  fill: rgba(122, 25, 46, 0.05);
  pointer-events: none;
}

.leader-quote {
  position: relative;
  margin: 0;
  max-width: 1145px;
}

/* Satoshi Italic 20/38.4, +0.05 tracking — the text spec on node
   978:2775, down to the leading. The 45px this carried before came
   from the older wireframe frame, where the quote was set roman. */
.leader-quote p {
  font-weight: 400;
  font-style: italic;
  font-size: 20px;
  line-height: 38.4px;
  letter-spacing: 0.05px;
  text-align: center;
  color: var(--ink);
}

.leader-by {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
}

.leader-name {
  font-weight: 500;
  font-size: 16px;
  line-height: 24px;
  color: var(--ink);
}

.leader-role {
  font-weight: 400;
  font-size: 16px;
  line-height: 24px;
  color: var(--muted-3);
}

/* --- the 10-second hand-off, shown ------------------------
   Each dot is a track; the active one fills across it over the
   dwell. The fill is a scaleX transform driven by JS, so the
   countdown can be paused and restarted without re-running a
   CSS animation from an awkward point. */

.leaders-dots {
  margin-top: 16px;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
}

.leaders-dot {
  width: 10px;
  height: 10px;
  padding: 0;
  border: 0;
  border-radius: 999px;
  background: rgba(107, 29, 46, 0.16);
  cursor: pointer;
  overflow: hidden;
  transition: width 0.45s cubic-bezier(0.4, 0, 0.2, 1),
              background-color 0.45s ease;
}

.leaders-dot.is-on {
  width: 56px;
}

.leaders-dot i {
  display: block;
  width: 100%;
  height: 100%;
  border-radius: inherit;
  background: var(--maroon);
  transform: scaleX(0);
  transform-origin: left center;
}

.leaders-dot:not(.is-on) i {
  transform: scaleX(0);
}

@media (prefers-reduced-motion: reduce) {
  .leaders-dot.is-on i { transform: scaleX(1); }
}

/* ============================================================
   FAQ  (1440 x 786)
============================================================ */

.faq {
  height: 786px;
  padding: 120px var(--gutter);
  background: #fff;
  display: flex;
  align-items: flex-start;
  gap: 80px;
}

/* ============================================================
   THE SECTIONS BELOW THE RAIL, AT THE SAME SHARE OF THE SCREEN

   These were fixed pixel widths, which means they hold the design's
   proportion at exactly one viewport — 1440 — and lose it everywhere
   else. Measured against the viewport:

                 1440      1920      2560
     FAQ list    52.8%     39.6%     29.7%
     Trust copy  46.9%     35.2%     26.4%

   On a 27-inch monitor the FAQ was occupying under a third of the width
   it was drawn to occupy, stranded in the middle of the screen while
   the cards above it ran nearly wall to wall.

   Each width below is the SAME number as before, expressed as its share
   of the 1440 frame instead of as pixels: 400/1440 = 27.78vw, and so on.
   Identical at 1440, and now identical in proportion at every other
   width too.

   Type is deliberately NOT scaled with them. Widths are what was asked
   for, and a line of body copy that grows with the monitor stops being
   readable long before it stops fitting — the column gets wider, the
   words stay the size they were drawn.
============================================================ */
.faq-title {
  width: 27.78vw;   /* 400/1440 */
  flex: none;
  font-weight: 400;
  font-size: 40px;
  line-height: 46px;
  color: var(--ink);
}

.faq-list {
  width: 52.78vw;   /* 760/1440 */
  flex: none;
}

.faq-item {
  border-bottom: 1px solid #e1dcd6;
}

.faq-item summary {
  height: 66px;
  padding: 10px 0 24px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 24px;
  cursor: pointer;
  list-style: none;
  font-weight: 500;
  font-size: 18px;
  line-height: 24.3px;
  color: var(--ink);
}

.faq-item summary::-webkit-details-marker {
  display: none;
}

.faq-icon {
  width: 32px;
  height: 32px;
  flex: none;
  border-radius: 50%;
  background: #f7f7f4;
  position: relative;
}

.faq-icon::before,
.faq-icon::after {
  content: "";
  position: absolute;
  left: 50%;
  top: 50%;
  background: #6e1b2a;
  border-radius: 2px;
  transform: translate(-50%, -50%);
}

.faq-icon::before {
  width: 9px;
  height: 2px;
}

.faq-icon::after {
  width: 2px;
  height: 9px;
}

/* ============================================================
   BUILT WITH TRUST  (1440 x 414)
============================================================ */

.trust {
  height: 414px;
  padding: 150px var(--gutter) 140px;
  background: #fff;
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: 117px;
}

.trust-copy {
  width: 46.88vw;   /* 675/1440 */
  display: flex;
  flex-direction: column;
  gap: 25px;
}

.trust-copy h2 {
  font-weight: 400;
  font-size: 40px;
  line-height: 46px;
  color: var(--ink);
}

.trust-copy p {
  font-weight: 500;
  font-size: 18.9px;
  line-height: 28px;
  letter-spacing: -0.4px;
  color: var(--muted-3);
}

.trust-badges {
  width: 22.78vw;   /* 328/1440 */
  flex: none;
  height: 120px;
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 20px;
}

.trust-gdpr {
  width: 120px;
  height: 120px;
}

.trust-dpdp {
  width: 120px;
  height: 120px;
  margin-right: 20px;
}

.trust-iso {
  width: 96px;
  height: 96px;
  border-radius: 50%;
  border: 1px solid #000;
  display: block;
  overflow: hidden;
}

.trust-iso img {
  width: 96px;
  height: 96px;
  object-fit: contain;
  opacity: 0.8;
}

/* ============================================================
   CLOSING THE GAPS BETWEEN THE LAST FOUR CHAPTERS

   Each of these sections is a full viewport with its content centred
   in it, which leaves a great deal of white above every heading. These
   four boundaries were each asked to come in by a set amount.

   It is `margin-top` and not `padding-top`, which is the obvious thing
   and does not work here. Measured:

     .stats / .leaders / .faq   content is CENTRED in a 100svh box, so
                                taking 12px off the top padding re-centres
                                it and moves the heading 6px. Half of what
                                you asked for, every time.
     .trust                     fixed 414px, align-items: flex-end — the
                                top padding does nothing at all. The
                                bottom one is what positions the copy.

   A negative top margin pulls the whole section up by exactly the number
   written, closes the gap above it by exactly that much, and leaves the
   composition inside it untouched. The section overlaps the tail of the
   one before it, which at these distances is empty white on both sides.

   They compound down the page — by `.trust` the cumulative shift is
   180px — and that is the intent: each boundary was judged on its own,
   against the one above it already being tighter.

   They have been taken in over several passes and the numbers below
   are the running totals, not one pass's worth. To take one in
   further, add to its number here rather than reaching for the
   padding, and check the clear gap it leaves — the distance from the
   last content above it to the first content below.

   Measured at 1728, those gaps are currently 365, 366, 196 and 139px.
   The 139 is `.trust` -> `.closing`, which carries no margin of its own
   and so has not moved on any pass; it is the one that will feel
   cramped first if this goes much further.

   Desktop only. Below 1024px these sections are re-spaced from scratch
   in the responsive block and were not what was being looked at. */
@media (min-width: 1024px) {
  /* above "One system, no rip-and-replace." */
  .stats { margin-top: -56px; }

  /* above "Trusted by leaders" */
  .leaders { margin-top: -112px; }

  /* between the testimonial card and "Questions operators usually ask" */
  .faq { margin-top: -126px; }

  /* above "Built with trust" */
  .trust { margin-top: -126px; }
}

/* ============================================================
   CLOSING — CTA + FOOTER  (1440 x 1304)
============================================================ */

.closing {
  position: relative;
  height: 1304px;
  background: var(--maroon-radial);
  overflow: hidden;
}

.cta {
  position: absolute;
  top: 0;
  left: 0;
  width: 1440px;
  height: 819px;
  border-radius: 0 0 30px 30px;
  background: var(--cta-field);
  /* the export's own drop shadow (dy 10, stdDeviation 10, 15% black);
     it carries no stroke, so the 1px #e8e0d4 border that used to be
     here was drawing an edge the design does not have */
  box-shadow: 0 10px 20px rgba(0, 0, 0, 0.15);
  display: flex;
  flex-direction: column;
  align-items: center;
}

.cta-title {
  margin-top: 314px;
  width: 672px;
  text-align: center;
  color: #1c1410;
  font-weight: 400;
}

.cta-serif {
  font-family: var(--display);
  font-size: 48px;
  line-height: 55px;
}

.cta-sans {
  font-family: var(--sans);
  font-size: 31px;
  line-height: 55px;
}

.cta-btn {
  margin-top: 61px;
  height: 58px;
  padding: 16px 32px;
  border-radius: 999px;
  /* 110.9deg, computed from the export's gradient handles
     (674.008,485.16 -> 806.337,535.82). It was 180deg — straight down —
     which flattens the sheen the diagonal is there to give. */
  background: linear-gradient(111deg, #8d3d4a 0%, #5d232c 100%);
  box-shadow:
    0 2px 5px rgba(0, 0, 0, 0.25),
    0 4px 8px rgba(111, 111, 111, 0.25),
    inset 3px 4px 10px rgba(181, 43, 71, 0.5),
    inset 1px 1px 5px rgba(207, 79, 105, 0.1),
    inset -2px -3px 8px rgba(82, 29, 39, 0.5);
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: var(--dm);
  font-weight: 600;
  font-size: 18px;
  line-height: 20px;
  color: #fbf8f3;
}

/* -------- footer -------- */

.footer {
  position: absolute;
  top: 903px;
  left: 50%;
  transform: translateX(-50%);
  width: 1392px;
}

/* THE DESIGN'S SPACING AT THE DESIGN'S SIZES; ONLY THE EXCESS MOVES.

   The Figma puts the columns 211px after the brand block and leaves a
   comfortable slack between them and the right edge. That is the
   layout, and at 1440 and below it should be exactly the layout — an
   earlier pass here used plain `space-between`, which threw the
   columns hard against the right edge at every width and made a 13"
   laptop look like the two halves had been pushed apart.

   What was actually wrong was only the EXCESS. A fixed 211px gap means
   everything above the design width piles up on the right as dead
   maroon: at 2560 the footer huddled in the left half of the screen
   with fourteen hundred pixels of nothing beside it.

   So the columns keep a constant distance from the right edge instead
   of a constant distance from the brand. `space-between` hands them
   the right edge, and the margin below holds them off it by the slack
   the design already has — which lands the gap back on 211 at 1440 and
   lets it grow with the window above that. Nothing is flush to
   anything, and nothing huddles.

   `space-between` also cannot overflow: it only ever distributes space
   that exists. The 48px gap is a floor for the narrow desktops between
   the 1024 breakpoint and the design width, where there is nothing
   spare to distribute. */
.footer-top {
  display: flex;
  justify-content: space-between;
  gap: 48px;
  padding: 0 24px 60px 24px;
}

.footer-brand {
  width: 380px;
  flex: none;
}

/* The exported asset is black artwork on transparency, which on this
   maroon ground is a logo you cannot see. brightness(0) flattens every
   opaque pixel to black and invert(1) takes it to white, and because it
   only touches colour the alpha edges stay as clean as the source. */
.footer-logo {
  width: 345px;
  height: 75px;
  object-fit: contain;
  filter: brightness(0) invert(1);
}

.footer-tagline {
  margin-left: 99px;
  font-size: 19.8px;
  line-height: 27.5px;
  color: rgba(255, 255, 255, 0.5);
}

/* THE SLACK THE DESIGN LEAVES TO THE RIGHT OF THE COLUMNS, held as a
   constant so that `space-between` above lands on the Figma's own
   spacing rather than on the edge.

   Read it as: nothing until the window has room for the design, then
   the design's own slack, and no more. The upper bound is what sets
   the gap back to 211px at 1440; the lower bound lets it collapse on
   the way down so a narrow desktop is never pushed into overflow. The
   1160 is where the row has exactly enough width for the brand, the
   columns and the 211px between them — below it there is no slack to
   keep. */
.footer-cols {
  display: flex;
  gap: 60px;
}

.footer-col {
  display: flex;
  flex-direction: column;
}

.footer-col:nth-child(1) { width: 150px; }
.footer-col:nth-child(2) { width: 129px; }
.footer-col:nth-child(3) { width: 99px; }

.footer-col h4 {
  font-family: var(--dm);
  font-weight: 600;
  font-size: 12px;
  line-height: 16px;
  letter-spacing: 0.6px;
  text-transform: uppercase;
  color: rgba(255, 255, 255, 0.4);
  margin-bottom: 16px;
}

.footer-col a {
  font-family: var(--dm);
  font-weight: 400;
  font-size: 14px;
  line-height: 20px;
  color: rgba(255, 255, 255, 0.65);
  margin-bottom: 8px;
}

.footer-col a:last-child {
  margin-bottom: 0;
}

.footer-bottom {
  margin-top: 40px;
  padding: 32px 24px 0;
  border-top: 1px solid rgba(255, 255, 255, 0.12);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
}

.footer-copy {
  font-family: var(--dm);
  font-size: 12px;
  line-height: 16px;
  color: rgba(255, 255, 255, 0.35);
}

.footer-social {
  display: flex;
  align-items: center;
  gap: 11px;
}

.footer-social a {
  width: 17px;
  height: 17px;
  display: block;
}

.footer-social svg {
  width: 17px;
  height: 17px;
}

.footer-legal {
  display: flex;
  gap: 24px;
}

.footer-legal a {
  font-family: var(--dm);
  font-size: 12px;
  line-height: 16px;
  color: rgba(255, 255, 255, 0.35);
}

/* ============================================================
   RESPONSIVE
   The Figma file only specifies a 1440 desktop frame, so below
   that width the page scales the fixed 1440 canvas down rather
   than inventing a layout the design does not define.
============================================================ */


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

/* ============================================================
   VIEWPORT SLIDES
   Each designed 1440x855-ish frame now fills the screen. The
   artwork covers the viewport while a fixed 1440 "stage" keeps
   the Figma layout proportions intact and centred.
============================================================ */

/* `justify-content`, and it is load-bearing below 1440.

   The stage inside is a hard 1440 canvas with its contents on Figma
   coordinates. `place-items: center` centres the ITEM in its track, and
   when the item is wider than the track the track is what overflows —
   so the canvas sat flush left and everything on it was centred on 720
   while the viewport's centre was somewhere else entirely. At 1100 that
   put the hero's headline 170px right of where it belonged.

   Centring the track hangs the canvas off BOTH edges instead, which is
   how a fixed canvas should give way: the imagery crops evenly and
   everything laid out on the canvas's centre line stays on the screen's
   centre line. */
.slide {
  position: relative;
  display: grid;
  place-items: center;
  justify-content: center;
  /* divided by the page zoom so the slide still fills the real viewport */
  min-height: 100svh;
  overflow: hidden;
}

.stage {
  position: relative;
  width: 1440px;
  height: 855px;
  flex: none;
}

/* the hero stage tracks the viewport so its content never falls below the fold */
.hero .stage {
  height: 100%;
}

.layer .stage { height: 895px; }

/* hero artwork fills the screen, content keeps its designed offset */
.hero-bg {
  inset: 0;
  height: 100%;
}

.hero-photo {
  left: 0;
  top: 0;
  width: 100%;
  height: 100%;
}

.hero-glow {
  left: -14%;
  top: 46%;
  width: 128%;
  height: 87%;
}

.layer-texture {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  width: 157%;
  height: auto;
  min-height: 168%;
  object-fit: cover;
}

/* flow sections centred in the viewport */
.stats.slide,
.leaders.slide,
.faq.slide,
.cta.slide {
  height: auto;
  min-height: 100svh;
  display: flex;
  flex-direction: column;
  justify-content: center;
}

.faq.slide {
  flex-direction: row;
  align-items: center;
}

/* THE TITLE SAT HALFWAY DOWN THE LIST.

   `align-items: center` centres each item on its own, so the 92px
   title landed in the middle of the ~400px list. The design has it
   level with the first question.

   Top-aligning the items inside a line, and centring the LINE in the
   viewport, needs `align-content` — which only applies to a wrapping
   container. The list takes whatever the title and gap leave
   (flex-basis 0) rather than a percentage that has to sum to exactly
   100%, so the row can never actually wrap: 1240 − 400 − 80 = 760
   at 1440, the same as before. Desktop only; the phone column has
   its own override. */
@media (min-width: 1024px) {
  .faq.slide {
    flex-wrap: wrap;
    align-content: center;
    align-items: flex-start;
  }

  .faq.slide .faq-list {
    flex: 1 1 0;
    min-width: 0;
  }
}

/* closing: the CTA panel is its own screen, and the footer is UNCOVERED
   from behind it rather than climbing over it.

   The footer sits under the panel — lower in the stack, its top edge
   permanently tucked behind the panel's lower one — and index.js holds
   it lifted so that it starts completely hidden there. Coming down the
   last stretch, the panel rises out of the frame and the footer is
   simply revealed underneath, exactly where it always was.

   The arithmetic makes the footer's bottom edge track the viewport's
   bottom edge for the whole reveal, so nothing about it appears to
   move: the panel lifts off it. That falls out of the geometry rather
   than being tuned — the section is one panel plus the footer's exposed
   tail, so the scroll it has to give and the distance the footer has to
   be lifted are the same number. See initFooterRise.

   It used to ride UP OVER the panel, at a higher z-index, which read as
   a band being laid on top of the page rather than as the end of it. */
.closing {
  height: auto;
  display: block;
  overflow: hidden;
}

.cta {
  /* Untouched. The panel is composed as a full screen and its copy is
     centred in that screen; nothing here may move it.

     It was briefly sticky, with the footer's height reserved as bottom
     padding so the Book a Call button would clear the footer's resting
     place. That worked and was wrong: freezing the panel at the top of
     the viewport is what put the button under the footer in the first
     place, and the padding then paid for it by lifting the whole ask
     off its mark. The panel scrolls, as it always did, and by the time
     the footer has risen the button has travelled well above it.

     The stack in here is grain 1 < footer 2 < panel 3, and each step
     matters. Above the grain because `.closing::after` is a
     pseudo-element and paints after its siblings, so anything at or
     below 1 goes under it. Above the footer because the panel is what
     HIDES the footer — that is the whole effect.

     The panel was at 2 and the footer at 3 when the footer came up over
     it; the pair is simply swapped, with the footer moved to 2 rather
     than the panel to 4 so it still clears the grain.

     Its own drop shadow does real work now. It used to fall on the
     section's own ground; it now falls on the footer, which is the
     thing that says the footer is UNDER the panel rather than butted
     up against it. */
  position: relative;
  top: auto;
  left: auto;
  width: 100%;
  align-items: center;
  overflow: hidden;     /* keeps that grain inside the rounded corners */
  z-index: 3;
}

.cta-title { margin-top: 0; }

.footer {
  position: relative;
  top: auto;
  left: auto;
  /* Load-bearing. The dead desktop rule further up still carries
     `transform: translateX(-50%)` from when this was absolutely
     positioned at left:50%. Drop this and that comes back — GSAP then
     parses it as x:-720 and keeps it, planting the whole footer half a
     viewport to the left. */
  transform: none;
  margin: 0 auto;
  /* was a fixed 1392px, which is the design's width at 1440 and a band
     stranded mid-screen at 2560. Same proportion, held by the gutter. */
  width: 100%;
  z-index: 2;   /* under the panel at 3, over the grain at 1 */

  /* HOW FAR ITS TOP EDGE STAYS TUCKED BEHIND THE PANEL.

     None of it is ever seen: the footer's top edge, and the 30px
     rounded corners on it, live under the panel for the whole section,
     which is what lets the footer read as something the panel was
     sitting on rather than a separate band below it. Anything past the
     corner radius does that job; 48 is a comfortable margin over it.

     It is PADDING as well as margin, and both in the same breath, which
     is the whole point. The margin pulls the footer up behind the
     panel; the padding gives back exactly what the margin took, so the
     footer's own 84px of air above the logo survives the tuck. What is
     hidden is only the padding that was added to hide.

     Get this wrong and it eats the design. At -132px against 84px of
     padding — which is what this was when the footer still rode OVER
     the panel, where those pixels were on top and visible — the tuck
     swallowed 48px of the footer ITSELF: the logo cut in half, and the
     first link missing from every column.

     index.js reads the margin to work out how far the footer has to be
     lifted to disappear completely. */
  --tuck: 48px;
  margin-top: calc(var(--tuck) * -1);
  padding: calc(84px + var(--tuck)) var(--gutter-footer) 52px;

  /* Its own ground. Without one the footer's text would sit on the
     section's radial, which is lighter where they meet; these two stops
     are that radial at the point of contact, so the reveal uncovers
     what reads as the same surface rather than a panel laid on it. */
  background: var(--maroon-radial);
  border-radius: 30px 30px 0 0;

  /* No shadow of its own. It had one thrown UPWARD, to lift it off the
     panel it used to climb over. It is underneath now, so that shadow
     would be cast into the panel's own back and never seen — and the
     depth it was faking is real this time: the panel's drop shadow
     falls on the footer. */
}

/* No ScrollTrigger, or reduced motion: nothing scrubs the footer off
   its resting margin, so it would sit over the panel from the start
   with nothing having moved. Stack them plainly instead. */
html:not(.js-motion) .footer {
  /* zeroing the tuck takes the margin AND the padding that paid for it,
     so the footer lands on its own designed spacing */
  --tuck: 0px;
  border-radius: 0;
  box-shadow: none;
  background: none;
}

/* ============================================================
   GRAIN — breaks up 8-bit banding on the large maroon fields
============================================================ */

/* `.cta::after` joins these because the closing panel is now a
   GRADIENT rather than a photograph of paper. The paper texture was
   doing this job by accident — its grain broke up the ramp for free.
   A clean gradient has nothing to hide behind: the panel's base ramp
   travels about fourteen levels of blue across 1440px, which lands a
   banding step roughly every 120px on an 8-bit screen.

   `.closing::after` does not cover it: that grain sits at z-index 1
   and the panel is z-index 2, so it passes UNDER the very field that
   now needs it. */
.grain,
.layer::after,
.cta::after,
.closing::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  z-index: 1;
  opacity: 0.05;
  mix-blend-mode: overlay;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.8' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='180' height='180' filter='url(%23n)'/%3E%3C/svg%3E");
}

/* the layer stage sits above that section's grain overlay. The card parts
   used to be listed here too, for a grain overlay the cards no longer
   have — and `position: relative` was silently turning their Figma `top`
   values into flow-relative offsets, dropping the pills from 191 to 276
   and the mockup from 173 to 454. */
.layer .stage {
  position: relative;
  z-index: 2;
}

/* ============================================================
   SCROLL MOTION
============================================================ */

/* Same as `.w`: hidden from first paint, but NOT promoted from first
   paint. initReveals hands each group its `will-change` as the tween
   starts and takes it back when the tween ends. */
html.js-motion [data-reveal],
html.js-motion [data-reveal-stagger] > * {
  opacity: 0;
}

html {
  scroll-behavior: auto;
}

@media (prefers-reduced-motion: reduce) {
  html.js-motion [data-reveal],
  html.js-motion [data-reveal-stagger] > * {
    opacity: 1 !important;
    transform: none !important;
  }
}

/* ============================================================
   ONE LAYER — ripple + orbit formation
============================================================ */

/* The ripple used to be three CSS discs pulsing out of the mark,
   because the Figma plate had a ripple baked into it that nothing here
   could reproduce. assets/ripple-seq is that ripple, rendered — so the
   stand-in is gone rather than layered on top of the real one. */

/* the orbs are flown in by GSAP, so hide them until it runs */
html.js-motion .layer .orb,
html.js-motion .layer-mark {
  opacity: 0;
}

@media (prefers-reduced-motion: reduce) {
  html.js-motion .layer .orb,
  html.js-motion .layer-mark { opacity: 1; }
}

/* the reviews section becomes a stack of full-screen beats; the
   floating card marquee still spans all of them behind the text */
.reviews {
  height: auto;
}

.reviews-top,
.reviews-mid,
.reviews-foot {
  position: static;
  top: auto;
  left: auto;
  right: auto;
  min-height: 100svh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
}

.reviews-top,
.reviews-foot { gap: 0; }

/* inner wrapper carries the reveal, so the snap target itself is never
   transformed — a transform on the target shifts ScrollTrigger's computed
   start and the snap would land short of the section */
.beat {
  display: flex;
  flex-direction: column;
  align-items: center;
}

.reviews-top .beat { gap: 22px; }
.reviews-foot .beat { gap: 16px; }

.reviews-mid-text {
  font: inherit;
  color: inherit;
  text-align: center;
}

/* -- 60% of guest reviews -- */

.reviews-top .beat { gap: 34px; }

.reviews-stars { gap: 18px; }

.reviews-stars svg {
  width: 56px;
  height: 56px;
  padding: 5px 4px;
}

/* Sized off the reference film, which sets this chapter a good deal
   smaller than the Figma did. Measured against the frame width there:
   the big line runs to ~34% of the viewport, the small one ~27%. At 76px
   the closing line of the next beat no longer fit on one line, and a
   beat that wraps stops being a beat. */
.reviews-headline .lg {
  font-size: 51px;
  line-height: 1.2;
}

.reviews-headline .sm {
  font-size: 39px;
  line-height: 1.25;
}

/* -- not your tech / amenities -- */

/* ~52% of the viewport for the longest line, as the reference has it,
   which also keeps "But the people who served them." on one line */
.reviews-mid {
  font-size: 50px;
  line-height: 1.3;
}

/* -- but here's what their day looks like -- */

.reviews-foot .beat { gap: 22px; }

/* one line, like the reference — the sentence is longer than the one
   this replaced, so 52px no longer fit */
.reviews-foot-lead {
  font-size: 40px;
  line-height: 1.3;
  letter-spacing: -0.5px;
}

/* ============================================================
   FAQ — answers + expand/collapse
============================================================ */

.faq-item summary {
  transition: color 0.25s ease;
}

.faq-item summary:hover {
  color: var(--maroon);
}

/* Open turns the + into an x by TILTING it, rather than retracting the
   upright stroke to leave a minus. Both strokes stay the whole way, so
   the mark never loses a limb mid-turn — it is one continuous rotation,
   and the shape it lands on says "close" rather than "collapse". */
.faq-icon {
  transition: transform 0.42s cubic-bezier(0.22, 0.61, 0.36, 1),
    background-color 0.3s ease;
}

.faq-item.is-open .faq-icon {
  transform: rotate(45deg);
  background: #f0e7e6;
}

/* height is driven by GSAP; the element must start closed and clipped */
.faq-answer {
  overflow: hidden;
  height: 0;
  opacity: 0;
}

.faq-answer p {
  padding: 0 56px 26px 0;
  font-size: 16.5px;
  line-height: 1.72;
  letter-spacing: -0.1px;
  color: #5b5654;
}

/* with no JS, <details> still opens natively — undo the clipping */
html:not(.js-motion) .faq-item[open] .faq-answer {
  height: auto;
  opacity: 1;
}

@media (prefers-reduced-motion: reduce) {
  .faq-item[open] .faq-answer {
    height: auto;
    opacity: 1;
  }
  .faq-icon { transition: none; }
}

/* ============================================================
   3. MOBILE / TABLET
   The Figma file has no artboard below 1440, so everything here
   is an interpretation rather than a spec. It is scoped entirely
   inside media queries: nothing above 1023px is affected.
============================================================ */

@media (max-width: 1023px) {

  /* a chapter must be exactly the visible height. `svh` is the *small*
     viewport height and falls short of it whenever the browser UI is
     retracted, which left a sliver of the next chapter showing and put
     every snap stop 32px early. --vh is published from innerWidth-stable
     measurements in index.js. */
  .slide,
  .stats.slide,
  .faq.slide,
  .cta.slide,
  .reviews-top,
  .reviews-mid,
  .reviews-foot {
    min-height: var(--vh, 100svh);
  }

  /* the fixed design stage becomes fluid */
  .stage {
    width: 100%;
    height: 100%;
  }

  .layer .stage { height: 100%; }

  /* -------- nav -------- */

  .nav {
    height: 68px;
    padding: 12px 16px;
  }

  .nav-logo { width: 112px; height: 38px; }
  .nav-logo img { width: 94px; height: 21px; }
  .nav-tabs { display: none; }          /* three tabs do not fit a phone bar */
  .nav-cta { height: 38px; padding: 8px 16px; font-size: 13px; }

  /* -------- hero -------- */

  .hero-content {
    width: 100%;
    padding: 0 24px;
    gap: 26px;
    top: min(62%, calc(100% - 280px));
  }

  .hero-copy { gap: 12px; }

  .hero-copy h1 {
    width: 100%;
    font-size: 32px;
    line-height: 1.24;
  }

  .hero-copy p { font-size: 16px; line-height: 1.5; }

  .hero-btn {
    width: 100%;
    max-width: 340px;
    height: 52px;
  }

  .hero-btn span { font-size: 16px; }

  /* -------- reviews -------- */

  /* the floating cards are placed on desktop coordinates and have
     nothing meaningful to say at this width */
  .reviews-marquee { display: none; }

  .reviews-top,
  .reviews-mid,
  .reviews-foot { padding: 0 24px; }

  .reviews-stars svg { width: 34px; height: 34px; padding: 3px 2px; }
  .reviews-stars { gap: 8px; }
  .reviews-top .beat { gap: 22px; }

  .reviews-headline .lg { font-size: 34px; }
  .reviews-headline .sm { font-size: 26px; }
  .reviews-mid { font-size: 32px; line-height: 1.3; }
  .reviews-foot-lead { font-size: 26px; line-height: 1.3; }

  /* -------- the diagnosis -------- */

  .disc-beat {
    padding: 0 24px;
    gap: 44px;
  }

  .disc-head { font-size: 26px; line-height: 1.32; }

  .disc-stats { flex-direction: column; gap: 14px; }
  .disc-stat { width: 100%; align-items: center; text-align: center; padding: 18px 20px; }
  .disc-stat-num { font-size: 32px; line-height: 1.2; }
  .disc-stat-label { font-size: 15px; }

  /* -------- one layer -------- */

  /* the orbit is geometry on a fixed grid, so it is scaled whole
     rather than reflowed. The transform sits on a descendant of the
     pinned sequence, not on the pin target itself, so ScrollTrigger is
     unaffected by it. */
  .layer-orbit {
    width: 1440px;
    height: 895px;
    /* explicit sides rather than the `inset` shorthand: `inset` resets
       top back to auto and drops the orbit to the top of the stage */
    right: auto;
    bottom: auto;
    left: 50%;
    /* dead centre. It sat at 58%, which read as the whole composition
       having slipped toward the bottom of the frame. */
    top: 50%;
    transform: translate(-50%, -50%) scale(0.5);
    transform-origin: center center;
  }

  .layer-titles {
    width: 100%;
    padding: 0 24px;
    /* tightened from 10%: the headroom above the headline was pushing
       the whole block down the frame */
    top: 6%;
  }

  .layer-titles h2 { font-size: 26px; line-height: 1.25; }
  .layer-titles p { font-size: 15px; }


  /* -------- people at the center -------- */

  .center-title {
    left: 0;
    width: 100%;
    padding: 0 24px;
    white-space: normal;
    top: 50%;
    transform: translateY(-50%);
    font-size: 28px;
    line-height: 1.3;
  }

  /* -------- outcome cards -------- */

  /* same one-frame stack as desktop, at phone proportions */
  .orchestration {
    height: auto;
    padding: 0 24px;
    background: none;
    flex: none;
  }
  .orchestration h2 { font-size: 26px; line-height: 1.25; letter-spacing: -0.6px; }

  .scroller-track { gap: 20px; padding: 0; }
  .scroller-window { margin: 0 24px; }

  /* one screen, all four cards the same height. The mock exports are
     987x2270 — they overflow their Figma frame — so `height: auto`
     made the image 697px tall and the card 920px. It is cropped to
     whatever space is left instead. */
  .scroller {
    height: var(--vh, 100svh);
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 26px;
  }

  /* was height:100% — with the title now sharing the frame that pushed
     the rail past the bottom of the screen */
  .scroller-track {
    height: auto;
    flex: none;
    align-items: center;
    align-self: flex-start;   /* same cross-axis trap as desktop */
  }

  .ocard {
    width: 84vw;
    max-width: 400px;
    height: min(560px, calc(72vh - 30px));
    display: flex;
    flex-direction: column;
    padding: 24px 18px 0;
    border-radius: 18px;
  }

  .ocard-head,
  .ocard-pills,
  .ocard-mock {
    position: static;
    left: auto;
    top: auto;
    width: auto;
  }


  .ocard-head h3 { font-size: 24px; line-height: 1.1; letter-spacing: -0.8px; }
  .ocard-head p { font-size: 12.5px; line-height: 1.5; }

  .ocard-pills {
    flex-direction: row;
    flex-wrap: wrap;
    gap: 8px;
    margin-top: 18px;
  }

  .ocard-pills li { height: 30px; font-size: 12px; padding: 3px 12px; gap: 7px; }
  .ocard-pills img { width: 15px; height: 15px; }

  .ocard-mock {
    width: 100%;
    flex: 1 1 auto;
    min-height: 0;
    height: auto;
    margin-top: 16px;
    object-fit: cover;
    object-position: top center;
  }

  /* -------- stats -------- */

  .stats { padding: 90px 24px; }
  .stats-header h2 { font-size: 30px; line-height: 1.24; }
  .stats-header p { font-size: 17px; line-height: 1.5; }

  .stats-row {
    margin-top: 40px;
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 14px;
  }

  .stat-card { width: 100%; height: auto; padding: 26px 12px; border-radius: 16px; }
  .stat-num { font-size: 30px; line-height: 1.25; }
  .stat-label { font-size: 13px; white-space: normal; text-align: center; }

  /* -------- faq -------- */

  /* must match .faq.slide's specificity, or the desktop row wins */
  .faq.slide {
    flex-direction: column;
    align-items: stretch;
    justify-content: center;
    gap: 18px;
    padding: 52px 24px;
  }

  .faq-title { width: 100%; font-size: 28px; line-height: 1.22; }
  .faq-list { width: 100%; }

  .faq-item summary { height: auto; min-height: 52px; font-size: 15px; gap: 12px; padding: 12px 0; }
  .faq-icon { width: 28px; height: 28px; }
  .faq-answer p { padding: 0 0 18px; font-size: 14px; line-height: 1.6; }

  /* -------- built with trust -------- */

  .trust {
    height: auto;
    flex-direction: column;
    align-items: flex-start;
    gap: 32px;
    padding: 80px 24px;
  }

  .trust-copy { width: 100%; gap: 16px; }
  .trust-copy h2 { font-size: 28px; }
  .trust-copy p { font-size: 15.5px; line-height: 1.6; }
  .trust-copy p br { display: none; }
  .trust-badges { width: 100%; justify-content: flex-start; height: auto; }
  .trust-gdpr, .trust-dpdp { width: 74px; height: 74px; }
  .trust-iso img { width: 74px; height: 74px; }

  /* -------- cta + footer -------- */

  /* The footer does NOT come up over the panel here, and the panel does
     not stick. That gesture needs a footer meaningfully shorter than
     the screen so there is a panel left to lay it over; stacked into a
     single column this one is 620 tall against an 812 viewport — more
     than three quarters of it — so the "overlay" would be the footer
     with a sliver of cream above, and the Book a Call button would sit
     324px underneath it. Plain stacking is the honest answer at this
     width, and it is what this section did before. */
  .cta {
    position: relative;
    border-radius: 0 0 20px 20px;
    padding: 0 24px;
  }

  .cta-title { width: 100%; margin-top: 0; }
  .cta-serif { font-size: 30px; line-height: 1.25; }
  .cta-sans { font-size: 19px; line-height: 1.5; }
  .cta-btn { margin-top: 34px; height: 52px; font-size: 16px; }

  .footer {
    width: 100%;
    padding: 56px 24px 40px;
    margin-top: 0;
    border-radius: 0;
    box-shadow: none;
    background: none;
  }

  .footer-top { flex-direction: column; gap: 40px; padding: 0; }
  .footer-brand { width: 100%; }
  .footer-logo { width: 230px; height: 50px; }
  .footer-tagline { margin-left: 0; margin-top: 12px; font-size: 15px; }

  .footer-cols { flex-wrap: wrap; gap: 32px 44px; }
  .footer-col:nth-child(1),
  .footer-col:nth-child(2),
  .footer-col:nth-child(3) { width: auto; min-width: 120px; }

  .footer-bottom {
    flex-direction: column;
    align-items: flex-start;
    gap: 18px;
    padding: 26px 0 0;
    margin-top: 34px;
  }
}

/* phones: trim the last few things that still crowd at 390px */
@media (max-width: 640px) {
  .stats-row { grid-template-columns: 1fr; gap: 12px; }
  .stat-card { padding: 20px 12px; }
  .layer-orbit { transform: translate(-50%, -50%) scale(0.42); }
  .ocard { width: 88vw; }
  .reviews-headline .lg { font-size: 30px; }
  .reviews-mid { font-size: 27px; }
  .hero-copy h1 { font-size: 28px; }
}

/* touch devices: backdrop blur and large blurs are expensive to
   composite while scrolling, and the grain is invisible at this size */
@media (hover: none) and (max-width: 1023px) {
  .nav-logo, .nav-tabs, .nav-cta, .nav-tab.is-active {
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
  }
  .grain, .layer::after, .closing::after { display: none; }
  .hero-glow { filter: blur(70px); }
}

/* ============================================================
   HERO INTRO — navbar drop-in
   The navbar waits for the first hero plate to decode, then the three
   glass pieces drop in from above. `.js-intro` is set by an inline
   script in <head>, so if JS never runs nothing is ever hidden.
============================================================ */

.js-intro .nav-logo,
.js-intro .nav-tabs,
.js-intro .nav-cta {
  opacity: 0;
  transform: translate3d(0, -22px, 0);
}

/* the class lands on .nav once the photo is up; the stagger is per-piece
   delay rather than three timers, so they cannot drift apart */
.js-intro .nav.is-in .nav-logo,
.js-intro .nav.is-in .nav-tabs,
.js-intro .nav.is-in .nav-cta {
  opacity: 1;
  transform: translate3d(0, 0, 0);
  transition:
    opacity 800ms cubic-bezier(0.22, 1, 0.36, 1),
    transform 900ms cubic-bezier(0.22, 1, 0.36, 1);
}

.js-intro .nav.is-in .nav-logo { transition-delay: 0ms; }
.js-intro .nav.is-in .nav-tabs { transition-delay: 90ms; }
.js-intro .nav.is-in .nav-cta  { transition-delay: 180ms; }

/* Reduced motion: no push, no drift, no drop-in. The first plate simply
   sits there and the navbar is present from the start. */
@media (prefers-reduced-motion: reduce) {
  .hero-photo.is-running .hero-slide,
  .hero-photo.is-running .hero-slide.is-active img {
    transition: none;
  }

  .hero-slide { transform: none; }
  .hero-slide img { transform: none; }

  .nav-logo,
  .nav-tabs,
  .nav-cta {
    opacity: 1;
    transform: none;
  }
}

/* THE CURTAIN, AND WHAT IS STANDING BEHIND IT.

   The film does not arrive from below any more. It is pulled up a
   whole screen (see `.seq`) so that it is already standing, at its
   first frame, behind the last of the story beats — and this stage is
   the sheet of white in front of it. Scroll on from "keep scrolling to
   see what their day looks like on the floor" and the stage simply
   stops being sticky and travels up off the top of the screen, the way
   it always did. What changed is what is underneath when it goes.

   It is the difference between a page arriving and a page being
   uncovered, and it is worth the two properties it costs:

     z-index   this section paints over the film, which is z-index 1
     background  an opaque one. Sticky and transparent was survivable
                 while there was nothing behind it. There is now, and
                 without this the film shows straight through the
                 reviews. */
.beats {
  position: relative;
  z-index: 2;
}

.beats-stage {
  position: sticky;
  top: 0;
  height: var(--vh, 100svh);
  overflow: hidden;
  background: #fff;
  /* contains the panels' z-index against the rest of the page, the
     same trap the hero slideshow hit */
  isolation: isolate;
}

/* pure scroll length — one screen each, so the stage stays stuck
   while the reader travels through beats 2, 3 and 4 */
/* THE SHEET'S SOFT HEM.

   Only while the curtain is actually lifting — initCurtain mounts and
   unmounts the class, and writes `--curtain-feather` per frame. At rest
   the depth is 0 and the class is off, so the stage is the same flat
   opaque white it has always been while the reader is reading the
   beats.

   The mask takes the white, the panels and the drifting review cards
   with it, which is right: all three are the curtain, and a card that
   stayed at full strength over the film would be the seam all over
   again in miniature.

   Anchored to the element's own bottom edge, so it travels with the
   lift for free — this is the same "static mask on a moving layer"
   the intro ground uses, with the one difference that here the depth
   has to open from nothing rather than sit below the frame at rest. */
.beats-stage.is-lifting {
  -webkit-mask-image: linear-gradient(
    to bottom,
    #000 0,
    #000 calc(100% - var(--curtain-feather, 0px)),
    rgba(0, 0, 0, 0) 100%
  );
  mask-image: linear-gradient(
    to bottom,
    #000 0,
    #000 calc(100% - var(--curtain-feather, 0px)),
    rgba(0, 0, 0, 0) 100%
  );
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
  -webkit-mask-size: 100% 100%;
  mask-size: 100% 100%;
}

.beats-step {
  height: var(--vh, 100svh);
  pointer-events: none;
}

.beat-panel {
  position: absolute;
  inset: 0;
  z-index: 2;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
  padding: 0 160px;
  /* every beat sits in the same place, so nothing reflows when the
     text changes — that is what keeps the swap free of jolt */
  min-height: 0;

  /* A PURE cross-fade. The panel used to travel 26px as well, which was
     a second movement on top of the one the lines inside are already
     making as they arrive word by word — two motions over the same
     text, slightly out of step with each other, which is exactly what
     the jerk was. The lines carry the entrance now; the panel only
     decides which set of them you are looking at. */
  opacity: 0;
  visibility: hidden;
  transition:
    opacity 620ms cubic-bezier(0.33, 0, 0.2, 1),
    visibility 0s linear 620ms;
}

.beat-panel.is-active {
  opacity: 1;
  visibility: visible;
  z-index: 3;
  transition:
    opacity 620ms cubic-bezier(0.33, 0, 0.2, 1),
    visibility 0s linear 0s;
}

/* No CSS animation on these, and that is the point of the note.

   The stars once had a CSS stagger of their own AND were being tweened
   by stepsIn at the same time — two owners animating one element, each
   undoing the other, which is why they used to flicker. index.js has
   owned them since: `data-in` says when they arrive and `data-each`
   says how, one after another from the left, on the same clock as the
   words below them — and on the same STEP as the first of those lines,
   so the top of the panel is never empty while the reader waits. See
   the note on the element in index.html. Nothing here should animate
   them again. */

/* the marquee is the constant the beats are read against, so it must
   not restack above them */
.beats-stage .reviews-marquee { z-index: 1; }

@media (max-width: 1023px) {
  .beat-panel { padding: 0 24px; }
}

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

/* ============================================================
   DIAGNOSIS -> ONE LAYER hand-off
============================================================ */

/* The "one layer" stage lives inside the pinned diagnosis, stacked over
   the washed room the film ended on. The tools are on screen for the
   whole diagnosis, out at the edges; the formation calls them in and
   the maroon blooms under them — one continuous act rather than a cut
   to a second red screen.

   It keeps the .layer class so every tuned rule above still applies;
   these only override the box and the stacking.

   The wrapper is ALWAYS visible and always transparent: it is the frame
   the tools sit in. Only .layer-ground — the maroon itself — is
   revealed, or the tools would be hidden by their own answer. */
.layer-in-disc {
  position: absolute;
  inset: 0;
  height: 100%;
  background: none;
  box-shadow: none;
  /* above the room, below nothing else in the section */
  z-index: 2;

  /* The same centring `.slide` gives every other stage on the page.

     It has to be repeated here because this wrapper is `position:
     absolute` on the section, which takes it OUT of the slide's grid:
     the slide centres THIS box, and nothing was centring the 1440 grid
     inside it. The stage was simply the first block in a block
     container, so it sat flush left and the whole composition — the
     mark, both strokes, the seven tools, the headline — was pulled
     toward the left edge by half the difference between the viewport
     and 1440.

     At 1440 that difference is nothing and at a 14-inch 1512 it is 36px,
     which is why this read as fine on the machine it was built on. At a
     16-inch 1728 it is 144px and on a 27-inch it is several hundred:
     the ring hangs off to one side with a dead strip of room beside it.

     Below 1440 the stage overflows its container and the grid leaves it
     flush left, exactly as `.slide` already does for the hero — this
     changes nothing at those widths. */
  display: grid;
  place-items: center;
}

.layer-in-disc .stage { height: 100%; }

/* The maroon, revealed by a radial BLOOM from behind the mark rather
   than a wipe from an edge. The reference opens it from the centre,
   which is the only reading that makes sense here: the ground is what
   the mark brings with it, so it has to arrive from the mark.

   The soft edge is a mask that JS rewrites every frame — see
   playFormation. A CSS transition cannot help here: no mask value
   interpolates, which is the same wall the hero's opening ran into. */
/* The maroon is a render now.

   It was `--maroon-radial` under a radial mask whose radius was tweened
   from 0 — a gradient pretending to be a drop landing. The plates in
   assets/ripple-seq are the real thing: 61 frames from an empty white
   field, through the drop and its bloom, into the standing ripples the
   section rests on. Nothing here paints any more; the canvas does. */
.layer-ground {
  position: absolute;
  inset: 0;
  opacity: 0;
}

.layer-ripple {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
}

/* the answer's own titles wait for the ground to be under them */
.layer-in-disc .layer-titles { opacity: 0; }

@media (prefers-reduced-motion: reduce) {
  /* nothing to ride, so the layer is simply present */
  .layer-in-disc .layer-titles { opacity: 1; }
  .layer-ground { opacity: 1; }
}

/* ============================================================
   ONE LAYER — centring inside the pinned diagnosis

   As a standalone section the orbit could rely on its Figma
   coordinates, because that section was always about as tall as the
   895px grid they were measured against. Inside the pin the stage is
   exactly one viewport, whatever that happens to be, so those fixed
   tops put the mark wherever the viewport height left it. Centring the
   grid as a unit keeps the mark in the middle of the frame at any
   height, which is the whole point of the shot.
============================================================ */

@media (min-width: 1024px) {
  .layer-in-disc .layer-orbit {
    width: 1440px;
    height: 895px;
    right: auto;
    bottom: auto;
    left: 50%;
    top: 50%;
    /* Centred, and then a little BELOW centre, to open air between the
       headline and the top of the ring.

       The offset belongs here and nowhere else. `.layer-orbit`'s own
       rule carries the Figma tops, but this selector is more specific
       and lands later, so anything written up there is dead at this
       width — a `top` added to the base rule was, silently, and looked
       for all the world like it had worked.

       Down rather than up on .layer-titles: the headline keeps the y
       the file gives it instead of creeping toward the top edge, and
       there are ~190px going spare below the lowest tool. It does cost
       the dead-centre framing the note above argues for; that is the
       trade, and this is the one number to change if it reads wrong. */
    transform: translate(-50%, calc(-50% + 24px));
    transform-origin: center center;
  }

  /* was 83px, measured from the top of a taller section */
  .layer-in-disc .layer-titles { top: 56px; }
}

/* ============================================================
   OUTCOME RAIL — heading, card scale, snap

   The cards are drawn at a fixed 900x500 (--ocw/--och) with every element inside
   them placed on Figma coordinates, so they cannot simply be widened:
   the contents would stay put. Instead .ocard-inner keeps the design
   size and is scaled as a unit, and .ocard reserves exactly the scaled
   box. --os is published per viewport by index.js.

   Below 1024px `display: contents` removes the wrapper from layout
   entirely, so the phone rules further up still apply unchanged.
============================================================ */

.ocard-inner { display: contents; }

@media (min-width: 1024px) {
  /* One frame: title over cards, the 67px Figma leaves between them
     (heading y7506+49, card y7622). The pair is centred as a block, so
     the title reads as the heading of what is under it rather than as a
     page of its own. */
  .scroller {
    height: var(--vh, 100svh);
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: calc(62px * var(--os, 1));
  }

  /* The heading sits over the FIRST CARD, flush with its left edge, and
     scales with it.

     It was centred as a block of its own, so its left edge landed
     wherever its own text width happened to put it — 22% of the
     viewport against the card's 9% — and it read as a page title with
     a rail underneath rather than as the heading of the thing it is
     over. Same padding as the rail gives it the card's own left edge.

     The type scales on --os for the same reason the card's does: at
     2560 the card is nearly twice its drawn size and a heading still
     sitting at 41.8px would be a caption on top of it. */
  .orchestration {
    height: auto;
    align-self: stretch;
    /* the 5px taken out of the section's gap, put back above the title */
    padding: calc(5px * var(--os, 1)) calc(75.2px * var(--os, 1)) 0;
    text-align: left;
    background: none;
    flex: none;
  }

  /* text-align, not just the box.

     The base rule centres this heading, and putting `text-align: left`
     on the wrapper did nothing about it — the h2's own rule wins. So the
     h2's BOX sat flush with the card at 89 while its glyphs started 256
     further in, and a measurement of the box said "aligned" while the
     thing on screen plainly was not. It is the text that has to line up
     with the card, so the text is what gets told. */
  /* In card units, like everything else on the rail. The design sets
     this line at 88.3% of the card's width; at 41.8 in the old frame's
     units it was reaching 68%, which is why it read as a caption over
     the cards rather than their heading. */
  .orchestration h2 {
    text-align: left;
    font-size: calc(41.8px * var(--os, 1));
    line-height: calc(48.4px * var(--os, 1));
    letter-spacing: calc(-2.2px * var(--os, 1));
  }

  .scroller-track {
    flex: none;
    align-items: center;
    /* The column flex centres its children on the cross axis, which for
       a 5375px-wide track inside a 1440px box means shifting it ~1967px
       left — every card offset then measures from the wrong origin and
       the snap lands nowhere. The rail owns its own horizontal origin;
       its origin is the frame's left edge now, not a padding — see
     `.scroller-window`, which is where the inset moved to. */
    align-self: flex-start;
  }

  /* `zoom`, not `transform: scale()`.

     The track is already a translating layer, and there is a note in
     index.js about card text going blurry once it rasterised inside two
     nested transformed layers — a scale here would rebuild exactly that
     condition, and at --os above 1 it would be upscaling a raster of
     text rather than drawing it larger.

     zoom lays the card out at the target size, so every glyph and the
     987x2270 mock are rendered natively at that size. .ocard reserves
     the same computed box, and its `overflow: hidden` means that on
     anything that somehow lacks zoom the card crops rather than spills. */
  .ocard-inner {
    display: block;
    position: relative;
    width: var(--ocw);
    height: var(--och);
    zoom: var(--os, 1);
  }

  .ocard {
    /* reserve the scaled box, not the design box, or the rail leaves a
       card's worth of empty space after every card */
    width: calc(var(--ocw) * var(--os, 1));
    height: calc(var(--och) * var(--os, 1));
  }

  /* 118px is the rail's own itemSpacing in Figma (frame 590:8841). The
     gap is also what lets the next card show at the edge of the screen,
     the cue that there is more to come.

     The LEFT padding is Figma's 89 now, not half the leftover width.
     Centring the first card was a reading of the design that the design
     does not support: measured off the reference, the card sits 89 from
     the left and its right edge lands at 80% of the frame, with the next
     card peeking beyond it. Centring pushed it to 13% and took the left
     edge -- the one the heading lines up with -- with it.

     The RIGHT padding MATCHES the left: the rail closes on the same
     margin it opened with.

     It was the whole leftover (100vw minus one card and its margin),
     which let the last card reach the same left margin the others rest
     at and parked it there with 292px of bare white beside it — the
     rail looking broken rather than finished. Nothing is the other
     extreme, and leaves the last card jammed against the screen edge
     while the first one sits comfortably inside its margin.

     Both the stops in index.js and the pin's own length read the
     rendered track, so neither needs telling. */
  .scroller-track {
    gap: calc(99.7px * var(--os, 1));
    padding-left: 0;
    padding-right: 0;
  }

  /* the rail's margin, which is what the padding above used to be */
  .scroller-window {
    margin-left: calc(75.2px * var(--os, 1));
    margin-right: calc(75.2px * var(--os, 1));
  }
}

/* ============================================================
   HERO OPENING — title card

   Four beats, driven by classes from index.js:
     (default)     the cream ground, line not yet in
     .is-line      the line fades up and holds
     .is-fading    the line goes, against the cream it arrived on
     .is-clearing  the empty ground wipes away, revealing the photograph
     .is-done      the card is gone and takes no space or clicks

   The beats are strictly in series. An earlier cut ran the wipe under a
   line that was still up, so the photograph rose behind live text and
   the text only left afterwards — two events competing for the same
   moment. The line is now clear of the frame before the ground moves.
============================================================ */

.intro {
  position: absolute;
  inset: 0;
  z-index: 6;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0 32px;
  overflow: hidden;
}

/* The cream ground, carried on its own layer so it can be moved.

   The reference film erases this from the BOTTOM UP — the photograph
   rises into the frame behind the line, which stays put. A flat opacity
   fade reads quite differently.

   The soft edge is a STATIC mask on a layer that is then translated.
   Animating the mask itself does not work: neither `mask-position` nor a
   gradient stop interpolates here (mask-position snaps straight from one
   end to the other), whereas a transform always does. Moving a fixed
   soft edge gives exactly the same picture, reliably.

   The layer is taller than the frame so that at rest its faded edge sits
   just below the bottom, out of sight. */
.intro-ground {
  position: absolute;
  left: 0;
  right: 0;
  top: 0;
  height: 132%;
  /* sampled from the film: the bloom sits left of centre and the ground
     deepens toward the right edge */
  background:
    radial-gradient(70% 90% at 18% 34%, #F6E9E2 0%, #F1E3DD 38%, #E9D6CE 72%, #DCC2B8 100%);
  -webkit-mask-image: linear-gradient(to bottom, #000 0 74%, rgba(0, 0, 0, 0) 95%);
  mask-image: linear-gradient(to bottom, #000 0 74%, rgba(0, 0, 0, 0) 95%);
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
  -webkit-mask-size: 100% 100%;
  mask-size: 100% 100%;
  transform: translate3d(0, 0, 0);
  transition: transform 1050ms cubic-bezier(0.4, 0, 0.2, 1);
  will-change: transform;
}

.intro-line {
  font-family: var(--dm);
  font-weight: 400;
  font-size: clamp(17px, 2.05vw, 29px);
  line-height: 1.35;
  letter-spacing: -0.2px;
  text-align: center;
  color: #5A2C29;
  opacity: 0;
  transform: translate3d(0, 10px, 0);
  transition:
    opacity 900ms cubic-bezier(0.33, 0, 0.2, 1),
    transform 1100ms cubic-bezier(0.33, 0, 0.2, 1);
}

.intro.is-line .intro-line {
  opacity: 1;
  transform: translate3d(0, 0, 0);
}

/* the line leaves first, sinking back the way it came. The duration is
   paired with INTRO_LINE_OUT in index.js, which is how long the ground
   waits before it starts moving — keep the two together. */
.intro.is-fading .intro-line {
  opacity: 0;
  transform: translate3d(0, -6px, 0);
  transition:
    opacity 620ms cubic-bezier(0.4, 0, 1, 1),
    transform 700ms cubic-bezier(0.4, 0, 0.2, 1);
}

/* then the emptied ground sweeps away bottom-up, raising the photograph */
.intro.is-clearing .intro-ground {
  transform: translate3d(0, -132%, 0);
}

.intro.is-done {
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
}

/* the headline and call to action wait for the card, like the navbar */
.js-intro .hero-copy,
.js-intro .hero-btn {
  opacity: 0;
  transform: translate3d(0, 18px, 0);
}

.js-intro .hero.is-open .hero-copy,
.js-intro .hero.is-open .hero-btn {
  opacity: 1;
  transform: translate3d(0, 0, 0);
  transition:
    opacity 900ms cubic-bezier(0.22, 1, 0.36, 1),
    transform 1000ms cubic-bezier(0.22, 1, 0.36, 1);
}

.js-intro .hero.is-open .hero-btn { transition-delay: 120ms; }

/* The top scrim and the navbar arrive together from above, and the warm
   glow swells up from the bottom edge — the two directions the film
   opens on once the photograph is up. */
.js-intro .hero-scrim {
  opacity: 0;
  transform: translate3d(0, -14%, 0);
}

.js-intro .hero.is-open .hero-scrim {
  opacity: 1;
  transform: translate3d(0, 0, 0);
  transition:
    opacity 900ms cubic-bezier(0.22, 1, 0.36, 1),
    transform 1000ms cubic-bezier(0.22, 1, 0.36, 1);
}

.js-intro .hero-glow {
  opacity: 0;
  transform: translate3d(0, 34%, 0);
}

.js-intro .hero.is-open .hero-glow {
  opacity: 1;
  transform: translate3d(0, 0, 0);
  /* slower than everything else: in the film the wash is still climbing
     well after the navbar and headline have settled */
  transition:
    opacity 1500ms cubic-bezier(0.33, 0, 0.2, 1),
    transform 1900ms cubic-bezier(0.33, 0, 0.2, 1);
  transition-delay: 160ms;
}

@media (prefers-reduced-motion: reduce) {
  .intro { display: none; }
  .hero-copy,
  .hero-btn,
  .hero-scrim,
  .hero-glow { opacity: 1; transform: none; }
}

/* ============================================================
   DEVICE RANGE
   One design, every screen it has to survive.

   The page was built against a 1440 x 855 Figma frame on a 13"
   MacBook, and everything below is what the other machines needed.
   It is deliberately LAST in the file: every rule here corrects
   something written above it, at a width where that thing is wrong,
   and it can only do that from the end.

   Three regimes, and they do not overlap:

     >= 1024   THE CANVAS. Chapters are the Figma frame at absolute
               coordinates, scaled whole by --ps (see index.js §6).
     <  1024   FLUID. .stage stops being a canvas, and the chapters
               are re-laid-out for the width. Most of that already
               lived in the max-width: 1023px block above; what is
               here is what that block was missing.
     short     A PHONE ON ITS SIDE is 375px tall. It is not a small
               tablet and it cannot have a full-screen chapter's
               worth of type in it.

   Every number is either the design's own, or the design's own
   expressed as a share of the frame it was measured in.
============================================================ */


/* ------------------------------------------------------------
   >= 1024 — THE CANVAS, AT THE SIZE OF THE SCREEN
------------------------------------------------------------ */

@media (min-width: 1024px) {

  /* The one line this whole section exists for. index.js §6 has the
     argument for the factor and for zoom over transform; what matters
     here is that it lands on .stage and on nothing else, because
     .stage is exactly the part of a chapter that is drawn on Figma
     coordinates. The artwork behind it is full-bleed and stays that
     way. */
  .stage {
    zoom: var(--ps, 1);
  }

  /* THE STORY BEATS ARE A COMPOSED FRAME TOO — they simply are not
     drawn on .stage, because they have no absolute layout to hold:
     three centred lines that cross-fade in place. So they miss the
     rule above, and at 2560 a 51px headline that is meant to run to
     about a third of the frame was running to a fifth of it.

     Scaled on --ps, like the canvas they are cut between. This is
     NOT the same judgement as the FAQ and the trust band a few
     screens later, which keep their drawn type on purpose: those are
     paragraphs, and a line of body copy that grows with the monitor
     stops being readable long before it stops fitting. These are one
     line, held for one screen, read at a glance. They are a title
     card, and a title card scales. */
  .reviews-headline .lg { font-size: calc(51px * var(--ps, 1)); }
  .reviews-headline .sm { font-size: calc(39px * var(--ps, 1)); }
  .reviews-mid          { font-size: calc(50px * var(--ps, 1)); }
  .reviews-foot-lead    { font-size: calc(40px * var(--ps, 1)); }

  .reviews-stars { gap: calc(18px * var(--ps, 1)); }

  .reviews-stars svg {
    width: calc(56px * var(--ps, 1));
    height: calc(56px * var(--ps, 1));
  }

  .reviews-top .beat  { gap: calc(34px * var(--ps, 1)); }
  .reviews-foot .beat { gap: calc(22px * var(--ps, 1)); }

  /* 160/1440 of the frame, which is what the 160 was. As pixels it
     was a third of a 1024 window — enough to break "60% of guest
     reviews" onto two lines, and a beat that wraps stops being a
     beat. */
  .beat-panel {
    padding: 0 max(24px, 11.111%);
  }

  /* "WITH PEOPLE AT THE CENTER" WAS NOT CENTRED ANYWHERE BUT 1440.

     Its parent is .people, which is inset:0 on the section and
     therefore the width of the VIEWPORT — but the rule above places
     it at a flat `left: 501.5px`, which is a coordinate on the 1440
     canvas. The two only agree at 1440. Everywhere else the line
     simply sat 501.5px from the left edge of the screen:

       1366   88px left of centre
       1920   348px
       2560   508px — most of the way to the left edge

     It is centred outright now, on both axes — see the base rule for
     why the file's 51.5px offset and its 400/855 are not kept. What
     this block still has to do is the SCALE: the width and the type
     are canvas measurements, so they ride --ps like everything else
     in here. Placing it by its own centre rather than by an edge is
     also what keeps it honest against a height the window does not
     have. */
  .center-title {
    left: 50%;
    top: 50%;
    width: calc(540px * var(--ps, 1));
    transform: translate(-50%, -50%);
    font-size: calc(41.8px * var(--ps, 1));
    line-height: 1.157;
  }

  /* THE FAQ ROW DID NOT FIT ITS OWN SECTION BELOW 1440.

     Two of the three numbers across it were already proportional and
     the third was not: the title is 27.78vw, the list 52.78vw, and
     the space between them was a flat 80px. Add the two gutters and
     the row wants 94.45% of the screen plus 80px — which is exactly
     1440 at 1440, and more than there is at every width under it:

       1024   12px too wide
       1280    4px
       1366    2px

     It never showed as page overflow because `.slide` clips, so what
     it actually did was shave the right-hand edge off the list —
     the column the expand icons sit in.

     All three are now the SAME fraction of the row they divide
     (400 + 80 + 760 = 1240, the content width at 1440), so they sum
     to exactly 100% of it at any width instead of to 1440px. That
     also takes the scrollbar out of the sum: a vw is measured across
     the scrollbar and a percentage of this flex box is not, which is
     most of the 12px at 1024. Identical at 1440. */
  .faq-title { width: 32.258%; }      /* 400/1240 */
  .faq-list  { width: 61.290%; }      /* 760/1240 */
  .faq.slide { gap: 6.452%; }         /* 80/1240 */

  /* THE STAT ROW WAS THE LAST SHRINK-WRAPPED THING ON THE PAGE.

     Four 280s and three 40s, all fixed, in a section that centres
     them — so the row measured 1240 at 1440 (correct, the drawn
     width) and still 1240 at 2560, where the section is 2204 wide
     and the FAQ and trust bands either side of it had already gone
     proportional. Four small cards in the middle of a monitor, with
     the run of the section they belong to on both sides of them.

     Same numbers as a share of the row: 280/1240 and 40/1240, which
     add to 100% and come to 280 and 40 on the nose at 1440.

     The card's HEIGHT stays drawn, and so does its type. That is the
     same line this file draws for the FAQ column and the trust copy
     a few screens later — the box takes its share of the screen, the
     reading inside it stays the size it was designed to be read at. */
  .stats-row {
    width: 100%;
    gap: 3.226%;                      /* 40/1240 */
  }

  .stat-card {
    flex: 0 1 22.581%;                /* 280/1240 */
    width: auto;
  }
}

/* 520 - 639 — the stat grid drops to ONE column at 640, which is the
   right answer on a phone and a waste of a 600px foldable or a small
   tablet in portrait. Two columns until there is room for four. */
@media (min-width: 520px) and (max-width: 639px) {
  .stats-row { grid-template-columns: 1fr 1fr; }
}


/* ------------------------------------------------------------
   < 1024 — WHAT THE FLUID LAYOUT WAS MISSING
------------------------------------------------------------ */

@media (max-width: 1023px) {

  /* THE BUG THAT TOOK THE WHOLE PHONE LAYOUT DOWN WITH IT.

     .slide is `display: grid; place-items: center; justify-content:
     center`, and that last one is load-bearing above: it is what
     hangs the fixed 1440 canvas off BOTH edges instead of just the
     right one. What it also does is stop the column track filling
     the container — a centred track is sized to its content.

     Above 1024 the content is 1440px of canvas, so the track is
     1440 and nothing is wrong. Below 1024 the canvas goes fluid —
     `.stage { width: 100% }` — and 100% of a track that is sized to
     its contents is a circular definition that resolves to ZERO.

     The stage was 0px wide on every phone. Everything laid out
     inside it kept only its own padding:

       .hero-content   48px, so the H1 set one word per line
       .disc-beat      48px
       .layer-titles   48px

     An explicit track cannot collapse, and with a single 100% track
     `justify-content: center` has nothing left to centre, so the
     rule above is neutralised here rather than fought.

     .layer-in-disc is the second copy of that centring — it is
     `position: absolute` on the film's section, so it is outside the
     slide's grid and has to centre its own stage. It collapsed the
     same way and took the diagnosis, the answer's headline and the
     ring's frame down with it. */
  .slide,
  .layer-in-disc {
    grid-template-columns: 100%;
  }

  /* The same `place-items: center` reaching the flex override at the
     foot of VIEWPORT SLIDES, where it lands as `align-items: center`
     — the CROSS axis of a column, which is the horizontal one. The
     row shrink-wrapped to one card's worth of text (192px inside a
     327px column) and the four stats stacked in a gutter down the
     middle of the screen. Its siblings all carry an explicit width
     and were never affected, which is why this was the only one. */
  .stats-row {
    width: 100%;
  }

  /* -------- trusted by leaders --------

     The one section with NO phone rules at all: it kept the
     desktop card whole, including `padding: 104px 90px 76px`. On a
     375px screen that is 180px of side padding around a 147px
     column of 20px italic — the longest quote came out 1502px tall
     and the section 1916px, so the reader got a screen and a half
     of one testimonial and the dots were somewhere below the fold.

     Same composition, phone proportions. */
  .leaders { padding: 76px 24px; }

  .leaders-title { font-size: 28px; line-height: 1.24; }

  .leader-card {
    padding: 54px 24px 32px;
    border-radius: 18px;
    gap: 26px;
  }

  .leader-quote p {
    font-size: 15px;
    line-height: 1.68;
  }

  /* the opening mark is a decoration at 187px and an obstruction at
     this width; it keeps its proportion and gets out of the way */
  .leader-mark {
    left: -4px;
    top: -22px;
    width: 96px;
    height: 69px;
  }

  .leader-name,
  .leader-role { font-size: 14px; line-height: 20px; }

  /* -------- the wrapped headings --------

     Every display line on this page was written to sit on ONE line at
     1440, so none of them was ever given a second line to look right
     on. At 375 they all take two or three, and greedy wrapping fills
     each line to the edge and leaves the remainder — "Keep scrolling
     to see what / their day looks like on the / floor", a whole line
     spent on one word under two full ones.

     `balance` asks for lines of even length instead, which is what a
     typesetter would have done with the same sentence:

       Keep scrolling to
       see what their day
       looks like on the floor

     Only the display type, and only where it wraps. Body copy is left
     greedy on purpose — balancing a paragraph is both slower and
     wrong, and browsers cap the effect at a few lines anyway, so it is
     a no-op on anything long. Unsupported browsers ignore it and get
     exactly what they get today. */
  .hero-copy h1,
  .reviews-headline .lg,
  .reviews-headline .sm,
  .reviews-mid-text > span,
  .reviews-foot-lead,
  .disc-head,
  .layer-titles h2,
  .center-title,
  .orchestration h2,
  .ocard-head h3,
  .stats-header h2,
  .stats-header p,
  .leaders-title,
  .faq-title,
  .trust-copy h2,
  .cta-title {
    text-wrap: balance;
  }

  /* -------- the notch --------

     `viewport-fit=cover` is set in <head>, which is what lets the
     hero photograph run under the status bar and the home
     indicator. The cost is that everything else has to say so: in
     LANDSCAPE the notch eats into the side the logo is on, and the
     home indicator sits on top of the last row of the footer.

     max() rather than addition — the inset is zero on every phone
     without a notch and on every desktop, and 16 + 0 must still be
     16. */
  .nav {
    padding-left: max(16px, env(safe-area-inset-left));
    padding-right: max(16px, env(safe-area-inset-right));
  }

  .footer {
    padding-bottom: calc(40px + env(safe-area-inset-bottom));
  }
}


/* ------------------------------------------------------------
   640 - 1023 — TABLETS ARE NOT LARGE PHONES

   The phone rules run all the way to 1023, so an iPad in portrait
   (768) and in landscape (1024 minus nothing, so still here on a
   10.2") was reading 34px headlines and 15px body across two-thirds
   of a foot of glass. Same layout — it is the right one — with the
   type brought back toward the frame it is drawn at.
------------------------------------------------------------ */

@media (min-width: 640px) and (max-width: 1023px) {
  .hero-copy h1 { font-size: 44px; }
  .hero-copy p  { font-size: 19px; }

  .reviews-headline .lg { font-size: 44px; }
  .reviews-headline .sm { font-size: 34px; }
  .reviews-mid          { font-size: 40px; }
  .reviews-foot-lead    { font-size: 32px; }

  .disc-head        { font-size: 34px; }
  .disc-stat-num    { font-size: 40px; }
  .disc-stat-label  { font-size: 17px; }
  .disc-stats       { flex-direction: row; justify-content: center; }
  .disc-stat        { width: auto; min-width: 260px; }

  .layer-titles h2 { font-size: 34px; }
  .layer-titles p  { font-size: 17px; }
  .layer-orbit     { transform: translate(-50%, -50%) scale(0.72); }

  .center-title { font-size: 36px; }

  .orchestration h2 { font-size: 34px; }
  .ocard { width: 64vw; max-width: 520px; }

  .stats-header h2 { font-size: 40px; }
  .stats-header p  { font-size: 20px; }
  .stats-row       { grid-template-columns: repeat(4, 1fr); }
  .stat-num        { font-size: 34px; }

  .leaders-title   { font-size: 36px; }
  .leader-card     { padding: 72px 56px 44px; }
  .leader-quote p  { font-size: 17px; line-height: 1.75; }

  .faq-title       { font-size: 34px; }
  .faq-item summary { font-size: 17px; }
  .faq-answer p    { font-size: 15px; }

  .trust-copy h2   { font-size: 34px; }
  .trust-copy p    { font-size: 17px; }

  .cta-serif { font-size: 40px; }
  .cta-sans  { font-size: 23px; }
}


/* ------------------------------------------------------------
   SHORT VIEWPORTS — THE PHONE ON ITS SIDE

   An iPhone in landscape is 375-430px TALL. Every full-screen
   chapter on this page is written for a portrait phone and simply
   does not fit one: the hero's copy block alone is 442px, so the
   headline, the sub and the button were laid out past the bottom of
   a screen that has no more to give.

   Height is the scarce axis here and width is not, so the type comes
   down and the vertical rhythm closes up, rather than anything being
   re-arranged. Deliberately keyed to HEIGHT and not to
   `orientation: landscape` — a short window on a laptop has exactly
   the same problem and is not an orientation.
------------------------------------------------------------ */

@media (max-width: 1023px) and (max-height: 520px) {

  .hero-content {
    /* the block is ~230px at this type size; this keeps the button
       clear of the bottom edge on a 375px-tall screen */
    top: min(54%, calc(100% - 200px));
    gap: 14px;
  }

  .hero-copy { gap: 6px; }
  .hero-copy h1 { font-size: 25px; }
  .hero-copy p  { font-size: 14px; }
  .hero-btn     { height: 44px; }
  .hero-btn span { font-size: 14px; }

  .reviews-stars svg { width: 24px; height: 24px; }
  .reviews-top .beat { gap: 14px; }
  .reviews-headline .lg { font-size: 25px; }
  .reviews-headline .sm { font-size: 20px; }
  .reviews-mid          { font-size: 22px; }
  .reviews-foot-lead    { font-size: 21px; }

  .disc-beat       { gap: 20px; }
  .disc-head       { font-size: 19px; }
  .disc-stats      { flex-direction: row; gap: 12px; }
  .disc-stat       { padding: 10px 14px; }
  .disc-stat-num   { font-size: 21px; }
  .disc-stat-label { font-size: 12px; }

  .layer-titles    { top: 8%; }
  .layer-titles h2 { font-size: 20px; }
  .layer-titles p  { font-size: 13px; }
  .layer-orbit     { transform: translate(-50%, -50%) scale(0.3); }

  .center-title { font-size: 22px; }

  .scroller { gap: 14px; }
  .orchestration h2 { font-size: 19px; }

  /* Narrower and tighter, and the HEIGHT IS LEFT ALONE. The phone
     rule above already caps it at `min(560px, 72vh - 30px)`, which on
     a 390px-tall screen is 251 — correct, and correct because it is
     in viewport units. A `height: 100%` here resolves against
     .scroller-track, which is `height: auto` at this width, so it
     falls back to the content's own height: the card came out 1066px
     tall inside a 390px window.

     The mock is what gives, not the type: the card's words have to be
     readable and the picture under them only has to be present. */
  .ocard {
    width: 52vw;
    max-width: 420px;
    padding: 16px 16px 0;
  }

  .ocard-head h3 { font-size: 19px; }
  .ocard-head p  { font-size: 11.5px; }
  .ocard-pills   { margin-top: 10px; gap: 6px; }
  .ocard-pills li { height: 26px; font-size: 11px; }
  .ocard-mock    { margin-top: 10px; }

  /* Below this the page is ordinary reading, so the full-screen
     chapters stop being full-screen: a 375px-tall window cannot hold
     a stat row, six FAQ answers or a testimonial AND centre them.
     Letting these grow past the viewport is the honest answer —
     there is nothing composed about them to preserve, and snapping
     has already ended by here (see data-snap-end). */
  .stats.slide,
  .leaders.slide,
  .faq.slide,
  .cta.slide {
    min-height: 0;
  }

  .stats  { padding: 56px 24px; }
  .leaders { padding: 48px 24px; }
  .faq.slide { padding: 48px 24px; }

  .stats-header h2 { font-size: 24px; }
  .stats-header p  { font-size: 15px; }
  .stats-row { grid-template-columns: repeat(4, 1fr); margin-top: 24px; }
  .stat-card { padding: 16px 8px; }
  .stat-num  { font-size: 22px; }
  .stat-label { font-size: 11px; }

  .leaders-title  { font-size: 24px; }
  .leader-card    { padding: 40px 24px 24px; gap: 18px; }
  .leader-quote p { font-size: 13px; line-height: 1.6; }
  .leader-mark    { width: 68px; height: 49px; top: -14px; }

  .faq-title { font-size: 24px; }

  .cta-serif { font-size: 24px; }
  .cta-sans  { font-size: 16px; }
  .cta-btn   { margin-top: 22px; height: 46px; font-size: 15px; }
}


/* ------------------------------------------------------------
   SHORT DESKTOPS — the 15.6" laptop, and any half-height window

   These three sections are written to be a screen each, and their
   vertical padding is drawn against an 855px frame: the stats band
   alone carries 200px top and bottom. On a 768px laptop that makes a
   "one screen" chapter 833px tall, so every one of them is a little
   taller than the screen it is meant to fill and the reader gets a
   scroll between each that shows them two half-chapters.

   The padding is the only thing that gives — the type, the cards and
   the columns are all still at their drawn size. Keyed to height
   alone, because a 1440x768 window has exactly the same problem as a
   1366x768 laptop and neither is about width.
------------------------------------------------------------ */

@media (min-width: 1024px) and (max-height: 820px) {
  .stats   { padding-top: 120px; padding-bottom: 120px; }
  .stats-row { margin-top: 56px; }
  .leaders { padding-top: 64px; padding-bottom: 64px; }
  .faq     { padding-top: 72px; padding-bottom: 72px; }
  .trust   { padding-top: 96px; padding-bottom: 96px; }
}


/* ------------------------------------------------------------
   TOUCH — the gestures, not the layout
------------------------------------------------------------ */

/* Lenis owns touch on this page (`syncTouch`), which means it reads
   the finger and moves the page itself rather than letting the
   browser do it. Two native behaviours fight that and both have to
   go:

     PULL-TO-REFRESH  a downward flick at the top of the page is how
                      a reader gets back to the hero. On Android
                      Chrome it was reloading the site instead.

     SCROLL CHAINING  the testimonial slider and the FAQ answers can
                      reach their own ends mid-gesture, at which
                      point the scroll is handed to the page under
                      them and the reader is somewhere else.

   `contain`, NOT `none`: the rubber-band at the very top and bottom
   is the only feedback iOS gives that a page has ended, and this
   page's last screen is a call to action that benefits from the
   reader knowing they have reached it. -x stays `none` (see the
   rule up at `html`), because sideways there is genuinely nothing.  */
html {
  overscroll-behavior-y: contain;
}

/* A tap on a phone should not paint a grey box over the thing it
   hit and then leave it there for 300ms. Every interactive element
   on this page already answers a tap in its own way — the FAQ
   opens, the dot fills, the button presses — so the highlight is
   never the only feedback, just the ugliest. */
a,
button,
summary,
.leaders-stage {
  -webkit-tap-highlight-color: transparent;
}

/* The two things on the page a finger can DRAG sideways, told which
   axis they are claiming. Without this the browser has to wait for
   the gesture to resolve before it will scroll — which is the
   "sticky for a moment, then it goes" feel — and on the rail a
   diagonal swipe would scroll the page horizontally instead of
   moving the cards.

   pan-y, not none: a vertical drag that happens to start on a card
   still belongs to the page, and taking it would make the rail a
   scroll trap. */
.scroller-track,
.ocard {
  touch-action: pan-y;
}

/* ============================================================
   TOUCH TARGETS

   Everything below is gated on `(hover: none)`, which is a finger and
   never a mouse, AND on the phone breakpoint. A desktop — touch-screen
   laptops included, since their PRIMARY pointer still reports
   `hover: hover` — matches neither half and reads none of it.

   The page was drawn at 1440 for a cursor, and a cursor is a single
   pixel: a 10px dot and a 17px icon are generous targets for it. A
   fingertip is about 9mm, which is ~44 CSS px, and every control in
   here was under half that. They were not mis-styled — they were
   styled for the wrong input.

   THE VISUAL SIZE DOES NOT CHANGE. What grows is the box the browser
   hit-tests, and it grows through a TRANSPARENT BORDER rather than
   padding, because `.leaders-dot` clips its own fill with
   `overflow: hidden` — which clips to the padding box, so a pseudo
   element or real padding would be cut off, while a border sits
   outside it and is never clipped.

   Two things make that trick safe here and both are easy to get wrong:

     `background-clip: padding-box`, or the dot's own background paints
     out under the transparent border and the dot renders at the size of
     its hit area instead of its own.

     `box-sizing: border-box` is global on this page (see the reset), so
     a border is taken OUT of the declared width rather than added to
     it. Every width below is therefore the hit area, and the visible
     dot is what is left after the border — 24-12 = 12 across, 44-32 =
     12 down. The same arithmetic is why `.is-on` is 68 and not 56: it
     has to carry the same 12px of side border as the others to leave
     the 56 the design asks for.
============================================================ */

@media (hover: none) and (max-width: 1023px) {

  /* The carousel dots: 10x10 on a page where the nearest other target
     is 300px away, so a miss does nothing at all and reads as the page
     being dead. The row's own gap goes to zero because each dot now
     carries 6px of transparent border a side — so the targets sit flush
     against each other and there is no gutter between them to miss
     into, while the dots on screen are 12px apart. */
  .leaders-dots {
    gap: 0;
  }

  .leaders-dot {
    width: 24px;
    height: 44px;
    border: 16px solid transparent;
    border-left-width: 6px;
    border-right-width: 6px;
    background-clip: padding-box;

    /* AND THE RADIUS HAS TO BE RE-STATED, per axis, or the dots come
       out as SQUARES. This is the part of the border trick that bites.

       The base rule is `border-radius: 999px`, which works everywhere
       else because a radius that cannot fit is scaled down to fit. It
       is scaled against the BORDER box — 24x44 here — so 999 is first
       reduced to 12, and the padding box's radius is then that minus
       the border on each axis: 12 - 6 = 6 across, and 12 - 16 = -4 ->
       0 down. A corner rounded on one axis and square on the other is
       square, and all three dots rendered as little blocks.

       So both axes are given the number that leaves 6px — half of the
       12px dot, i.e. a circle — after the border is taken off:
       12 - 6 = 6 across, 22 - 16 = 6 down. Neither is scaled, because
       each axis's pair exactly fills the border box (2x12 = 24,
       2x22 = 44). The same values make the active pill a pill: its
       padding box is 56x12 and 6px is half its height. */
    border-radius: 12px / 22px;
  }

  /* the fill inherits the radius above, which is shaped for the dot's
     border box and not for its own; the clip does the real work, but
     give it a radius that is right for the box it actually has */
  .leaders-dot i {
    border-radius: 999px;
  }

  .leaders-dot.is-on {
    width: 68px;
  }

  /* 17x17 each, and the two of them 11px apart — the smallest targets
     on the page. The box becomes 44 square and the icon is centred in
     it at its own size; the gap goes because the boxes now supply more
     than it did. */
  .footer-social {
    gap: 0;
  }

  .footer-social a {
    width: 44px;
    height: 44px;
    display: grid;
    place-items: center;
  }

  /* Footer links are 20px line boxes stacked 8px apart: the target is
     the text and nothing else, and two of them are closer together than
     a fingertip is wide. The margin becomes padding, which is inside
     the hit box rather than outside it, so the same ink ends up ~40px
     tall and touching its neighbour instead of 20px tall with a dead
     8px gutter between. */
  .footer-col a {
    padding: 10px 0;
    margin-bottom: 0;
  }

  .footer-legal a {
    padding: 12px 0;
  }

  /* Both nav pieces are 38 tall, which is close enough that only the
     top and bottom rows of a tap miss — but they are the two things on
     the page a reader is most likely to reach for. */
  .nav-cta,
  .nav-logo {
    min-height: 44px;
  }
}
