/* =========================================================================
   richian — home.css
   Everything that only the home page has: the hero, the scroll-linked
   sequence and its loader, and the six sections below it.
   Loaded after css/site.css, which owns the tokens, the nav and the reveal
   system this sheet builds on.
   ========================================================================= */

/* ====================== HERO ============================================= */
.hero {
  position: relative;
  height: 100vh;
  /* lvh, NOT svh. svh is the SMALL viewport — the height with Safari's
     toolbars fully expanded — so a 100svh box stops 82pt short of the screen
     the moment they collapse, and the page background shows through as a black
     bar. Measured on an iPhone 16: viewport 659 expanded, 741 collapsed.
     lvh is the tall value and always covers, and unlike dvh it never resizes,
     so the pinned canvases below never reallocate their backing store
     mid-scrub. The cost is that the bottom 82pt sits behind the toolbars while
     they're expanded — which is why anything bottom-anchored inside these
     sections adds (100lvh - 100dvh) to its offset. See .hero__content. */
  height: 100lvh;
  width: 100%;
  overflow: hidden;
  background: #0d0f11;                   /* shows through before the poster paints */
  isolation: isolate;
}

.hero__video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;                     /* full-bleed, centred */
  object-position: 50% 50%;
  z-index: 0;
}

/* Darkens the busy console area behind the copy, plus a light touch at the
   top so the white wordmark holds up against the bright cabin. */
.hero__scrim {
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  background:
    linear-gradient(to top,
      rgba(8,9,10,.88) 0%,
      rgba(8,9,10,.62) 26%,
      rgba(8,9,10,.20) 52%,
      rgba(8,9,10,.04) 70%),
    linear-gradient(to bottom, rgba(8,9,10,.42) 0%, rgba(8,9,10,0) 24%);
}

.hero__logo {
  position: absolute;
  z-index: 3;
  top: clamp(20px, 4.4vh, 46px);
  /* Follows the content column once the cap engages, and keeps its own tighter
     inset below it — see the note on .hero__content. */
  right: max(clamp(22px, 4vw, 56px), var(--page-gutter, 0px));
  display: block;
  text-decoration: none;
}
.hero__logo img {
  display: block;
  width: clamp(296px, 34vw, 480px);
  height: auto;                          /* SVG is 354×125, tagline included */
}

.hero__content {
  position: absolute;
  z-index: 3;
  /* The hero's video bleeds, so this copy is positioned against the viewport
     rather than padded. max() is what puts it on the content column's edge
     once the cap engages (~1377px) while leaving it on its own tighter 5.2vw
     inset below that — the hero composition on a laptop is unchanged, and the
     headline stops stranding at the far left of an ultrawide. --page-gutter is
     0px until the cap bites, so max() picks the clamp on its own. Same device
     on .hero__logo and .seq-copy. */
  left: max(clamp(22px, 5.2vw, 76px), var(--page-gutter, 0px));
  right: max(clamp(22px, 5.2vw, 76px), var(--page-gutter, 0px));
  bottom: clamp(38px, 8vh, 84px);
  /* The hero is 100lvh, so its bottom 82pt is behind Safari's toolbars while
     they're expanded — which is exactly the state the page loads in, and this
     block holds the headline and both CTAs. (100lvh - 100dvh) is that overlap,
     live: it's 82px with the toolbars out, 0 once they collapse, and 0 on every
     desktop browser, so the copy sits flush at the bottom whenever there's
     nothing covering it and lifts clear when there is. Declared second so
     browsers without lvh/dvh drop it and keep the plain offset above. */
  bottom: calc(clamp(38px, 8vh, 84px) + (100lvh - 100dvh));
  max-width: 860px;
}

.hero__title {
  font-family: var(--serif);
  font-weight: 400;
  font-size: clamp(34px, 5vw, 72px);
  line-height: 1.06;
  letter-spacing: -.005em;
  color: #fff;
  margin: 0 0 clamp(16px, 2.2vh, 26px);
}

.hero__lede {
  font-family: var(--sans);
  font-size: clamp(15px, 1.2vw, 17px);
  line-height: 1.5;
  color: rgba(255,255,255,.9);
  margin: 0 0 clamp(24px, 3.4vh, 40px);
  max-width: 46ch;
}

.hero__actions {
  display: flex;
  align-items: center;
  gap: clamp(8px, 2vw, 28px);
  flex-wrap: wrap;
}

@media (max-width: 700px) {
  /* Centred on a phone (right-aligned everywhere else). Done with auto margins
     rather than translateX(-50%) because .hero__logo is [data-reveal] and GSAP
     owns its transform — an X translate here would be wiped by the reveal. */
  .hero__logo {
    left: 0;
    right: 0;
    width: fit-content;
    margin-inline: auto;
  }
  .hero__title br { display: none; }     /* let it wrap naturally on a phone */
  .hero__lede { max-width: none; }
  .hero__actions { gap: 14px; }
  .hero__actions .btn--primary { flex: 1 1 auto; }
}

/* ====================== THE SEQUENCES ==================================== */
/* Two scroll-linked sequences share this block — the unicorn under the hero
   and SF/Toronto above .audience. They're CLASSES, not ids, precisely so the
   second one costs no new CSS. Anything that genuinely differs is a per-
   section custom property set from JS (--seq-fill, --seq-scrim-rgb,
   --seq-scrim-h, --scroll-factor) or a modifier class (.seq-copy--top).
   --seq-fill falls back to the global --seq-bg so the markup still renders
   sanely if JS never runs. */
.seq { position: relative; }

.seq__pin {
  position: relative;
  height: 100vh;
  height: 100lvh;                        /* lvh, not svh — see the note on .hero */
  width: 100vw;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  background: var(--seq-fill, var(--seq-bg));
}

.seq__canvas {
  display: block;
  width: 100%;
  height: 100%;
}

/* ---- Headline copy, revealed near the end of the sequence --------------- */
.seq-copy {
  position: absolute;
  z-index: 3;
  /* On the content column above the cap, on its own inset below it — see
     .hero__content. NB the containing block here is .seq__pin, which is
     width: 100vw and therefore includes the scrollbar, so on a platform with
     classic scrollbars this edge sits ~7px left of a section's. Zero on macOS
     overlay scrollbars, and not worth changing the pin's width over. */
  left: max(clamp(22px, 5.5vw, 104px), var(--page-gutter, 0px));
  top: 50%;
  transform: translateY(-50%);
  width: min(46%, 620px);
  text-align: left;
  pointer-events: none;                  /* never eats scroll/clicks */
}

/* SF/Toronto sets its copy at the top rather than centred, per the mockup.
   NB this sets `transform`, which GSAP also animates on .seq-copy's CHILDREN
   — not on .seq-copy itself — so the two never collide. */
.seq-copy--top {
  top: clamp(84px, 11vh, 132px);
  transform: none;
}

/* Hidden start state, matching the global [data-reveal] one in site.css.
   GSAP animates out of it; the CSS fallback below transitions out of it.
   Keep the two in sync if you retune the look. */
.seq-copy > * {
  opacity: .01;
  filter: blur(14px);
  transform: translateY(26px);
}

.seq-copy__title {
  font-family: var(--serif);
  font-weight: 400;
  font-size: clamp(32px, 5.2vw, 72px);
  line-height: 1.05;
  letter-spacing: -.005em;
  color: var(--seq-cream);
  margin: 0 0 clamp(20px, 2.6vw, 34px);
  text-wrap: balance;
}
.seq-copy__body {
  font-family: var(--sans);
  font-size: clamp(14.5px, 1.15vw, 17px);
  line-height: 1.55;
  color: var(--seq-body);
  margin: 0 0 clamp(14px, 1.5vw, 20px);
}
.seq-copy__body--strong {
  font-weight: 600;
  color: #fff;
  margin-bottom: 0;
}

/* ---- SF/Toronto's copy ---------------------------------------------------
   This headline is a place name that has to hold on ONE line (the mockup
   breaks it nowhere). It used to read "San Francisco & Toronto", which wraps
   at the unicorn's clamp(32px, 5.2vw, 72px) at every desktop width, so it
   carried its own reduced clamp(30px, 3.9vw, 56px). The 2026-08-05 rewrite to
   "SF & The Six" removed the reason: it is short enough to hold one line at
   either scale, so it now shares the unicorn's and the two sequence headlines
   read at one size. Keep this class anyway — the em rule below is its own, and
   restoring a smaller scale for a longer headline is a one-line change here.
   .eyebrow comes from site.css; only the dark-section colour is restated,
   exactly as .people does it. */
.seq-copy .eyebrow { color: var(--mute-dark); }
.sfto__title {
  font-family: var(--serif);
  font-weight: 400;
  font-size: clamp(32px, 5.2vw, 72px);
  line-height: 1.06;
  letter-spacing: -.005em;
  color: var(--seq-cream);
  margin: 0 0 clamp(14px, 1.8vw, 22px);
}
/* Terracotta ampersand — the same held-back-phrase device as .section-title em. */
.sfto__title em { font-style: normal; color: var(--brand); }

/* Lifts the copy off the image. It fades in WITH the copy — see setCopyIn() —
   rather than dimming the frame for the whole sequence, and it's built from
   --seq-scrim-rgb (each sequence's own sampled dark) so it meets that
   sequence's frames instead of a generic black.
   Base state = bottom-anchored and mobile-only: on the unicorn's desktop the
   copy sits beside the frame and needs nothing (display: none). */
.seq__scrim {
  position: absolute;
  inset: auto 0 0 0;
  height: var(--seq-scrim-h);
  z-index: 2;
  display: none;
  opacity: 0;
  pointer-events: none;
  transition: opacity .7s ease;
  background: linear-gradient(to top,
    rgba(var(--seq-scrim-rgb, 24,23,16), .96)  0%,
    rgba(var(--seq-scrim-rgb, 24,23,16), .90) 30%,
    rgba(var(--seq-scrim-rgb, 24,23,16), .66) 56%,
    rgba(var(--seq-scrim-rgb, 24,23,16), .30) 80%,
    rgba(var(--seq-scrim-rgb, 24,23,16), 0)  100%);
}

/* SF/Toronto needs one on DESKTOP too, which the unicorn never did. Its copy
   sits at the top over open sky, and once the frame is cropped to a 16:10
   viewport that region carries the sunset: measured mean luminance ~100 with
   255 highlights across the whole last quarter of the sequence, which is where
   the copy actually appears. Cream text on that is unreadable in places, so
   this is a legibility fix, not a mood one — don't drop it because the mockup
   looks like it has no scrim. Anchored to the top, since that's where the
   copy is. */
.seq__scrim--top {
  display: block;
  inset: 0 0 auto 0;
  background: linear-gradient(to bottom,
    rgba(var(--seq-scrim-rgb, 24,23,16), .94)  0%,
    rgba(var(--seq-scrim-rgb, 24,23,16), .86) 30%,
    rgba(var(--seq-scrim-rgb, 24,23,16), .60) 56%,
    rgba(var(--seq-scrim-rgb, 24,23,16), .26) 80%,
    rgba(var(--seq-scrim-rgb, 24,23,16), 0)  100%);
}

/* Driven by the same signal as the copy reveal, so the two arrive together. */
.seq__pin.is-copy-in .seq__scrim { opacity: 1; }

/* ---- Mobile: full-bleed frame, copy overlaid at the bottom -------------- */
@media (max-width: 700px) {
  .seq__scrim { display: block; }
  /* Both sequences put their copy at the bottom on a phone, so the top-anchored
     variant reverts to the shared bottom one here. */
  .seq__scrim--top {
    inset: auto 0 0 0;
    background: linear-gradient(to top,
      rgba(var(--seq-scrim-rgb, 24,23,16), .96)  0%,
      rgba(var(--seq-scrim-rgb, 24,23,16), .90) 30%,
      rgba(var(--seq-scrim-rgb, 24,23,16), .66) 56%,
      rgba(var(--seq-scrim-rgb, 24,23,16), .30) 80%,
      rgba(var(--seq-scrim-rgb, 24,23,16), 0)  100%);
  }
  .seq-copy {
    top: auto;
    bottom: clamp(20px, 3.6vh, 44px);
    /* Same toolbar-overlap correction as .hero__content — the pin is 100lvh,
       so lift the copy by however much of it the toolbars are covering. */
    bottom: calc(clamp(20px, 3.6vh, 44px) + (100lvh - 100dvh));
    transform: none;
    left: 22px;
    right: 22px;
    width: auto;
  }
}

/* ---- Reveal for the no-GSAP fallback (pure CSS transitions) ------------- */
.seq-copy--css > * {
  transition: opacity .9s ease,
              filter .9s ease,
              transform .9s cubic-bezier(.22,.7,.3,1);
}
.seq-copy--css > *:nth-child(2) { transition-delay: .13s; }
.seq-copy--css > *:nth-child(3) { transition-delay: .26s; }
.seq-copy--css > *:nth-child(4) { transition-delay: .39s; }
.seq-copy--css.is-in > * { opacity: 1; filter: blur(0); transform: none; }

/* Same, for the hero — staggered by an inline --i index on each item. The
   hero is driven by GSAP when it's available; this is the no-GSAP path.
   (Every section below the sequence uses the shared .rv system instead.) */
.hero--css [data-reveal] {
  transition: opacity .9s ease,
              filter .9s ease,
              transform .9s cubic-bezier(.22,.7,.3,1);
  transition-delay: calc(var(--i, 0) * .13s);
}
.hero--css.is-in [data-reveal] { opacity: 1; filter: blur(0); transform: none; }

/* Reduced motion: no reveal choreography, just show the content. */
.seq-copy--static > *,
.hero--static [data-reveal] { opacity: 1 !important; filter: none !important; transform: none !important; }

/* ---- Loading overlay ---------------------------------------------------- */
.seq__loader {
  position: absolute;
  inset: 0;
  z-index: 5;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 18px;
  background: var(--seq-fill, var(--seq-bg));
  transition: opacity .6s ease;
}
.seq__loader.is-hidden { opacity: 0; pointer-events: none; }
.seq__loader-label {
  font-size: 12px;
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--mute-dark);
}
.seq__loader-track {
  width: min(240px, 60vw);
  height: 3px;
  border-radius: 3px;
  background: rgba(255,255,255,.12);
  overflow: hidden;
}
.seq__loader-bar {
  height: 100%;
  width: 0%;
  background: var(--seq-cream);
  border-radius: 3px;
  transition: width .2s ease;
}

/* ---- Fallback when GSAP is unavailable (native sticky scroll) ----------- */
/* --scroll-factor is set per SECTION by computeTrack(), so two sequences with
   different track lengths each get their own height here. */
body.seq-nogsap .seq { height: calc(100vh * var(--scroll-factor)); }
body.seq-nogsap .seq__pin { position: sticky; top: 0; }

/* ====================== "THREE STEPS" ==================================== */
/* Deliberately light — this is the hard break out of the dark sequence.
   It's also the pin's unpin target, so it must have real height. */
.steps {
  position: relative;
  z-index: 1;                            /* sits above the unpinning canvas */
  background: var(--cream-bg);
  color: var(--ink);
  padding: clamp(72px, 11vh, 132px) var(--page-pad, clamp(22px, 8.6vw, 124px)) clamp(80px, 12vh, 140px);
}

/* Base type comes from .section-title in site.css; only what differs is here. */
.steps__title {
  margin-bottom: clamp(44px, 6.4vh, 82px);
  max-width: 26ch;                       /* holds it to two lines on desktop */
  text-wrap: balance;
}

.steps__grid {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: clamp(30px, 3.4vw, 48px);
  align-items: start;
  margin: 0 0 clamp(56px, 8vh, 104px);
}
/* Rules sit between the columns, full-height so they key off the tallest. */
.steps__grid > * { align-self: stretch; }
.step + .step {
  border-left: 1px solid var(--rule);
  padding-left: clamp(24px, 2.6vw, 44px);
}

.step__head {
  font-family: var(--serif);
  font-style: italic;
  font-weight: 400;
  font-size: clamp(21px, 2.1vw, 30px);
  line-height: 1.15;
  color: var(--ink);
  margin: 0 0 clamp(14px, 1.8vw, 22px);
}
.step__num { color: var(--brand); margin-right: .34em; }

.step__body {
  font-family: var(--sans);
  font-size: clamp(14.5px, 1.12vw, 16.5px);
  line-height: 1.62;
  color: var(--ink-body);
  margin: 0;
}

.steps__cta { display: flex; justify-content: center; }

@media (max-width: 860px) {
  .steps__title { max-width: none; }
  .steps__grid {
    grid-template-columns: 1fr;
    gap: clamp(28px, 4.4vh, 40px);
  }
  /* Stacked: the rule moves above each step instead of beside it. */
  .step + .step {
    border-left: 0;
    padding-left: 0;
    border-top: 1px solid var(--rule);
    padding-top: clamp(28px, 4.4vh, 40px);
  }
  .steps__cta .btn--primary { width: 100%; }
}

/* ====================== "THE WORK" ======================================= */
/* Same cream chapter as .steps, but the texture varies so two light
   sections back to back don't read as one long slab. Rules separate rows,
   they never enclose a cell — enclosing turns it into a spec sheet. */
.work {
  position: relative;
  z-index: 1;
  background: var(--cream-panel);
  color: var(--ink);
  padding: clamp(64px, 9vh, 108px) var(--page-pad, clamp(22px, 8.6vw, 124px)) clamp(80px, 12vh, 140px);
}

.work__title {
  margin-bottom: clamp(40px, 5.6vh, 72px);
  max-width: 22ch;
  text-wrap: balance;
}

/* ---- Flagship row: promoted by type scale alone ------------------------- */
/* No box of any kind. A full-bleed tint, an inset fill and a hairline outline
   were all tried and all read as a separate element fighting the section.
   The flagship is already the largest thing here and comes first; it just
   needed the same rule above it that every other row gets, so it reads as
   the first item in the list rather than something floating above it. */
.work__lead {
  padding-block: clamp(26px, 3.6vh, 40px) clamp(30px, 4.2vh, 48px);
}
.work__lead .work__name { font-size: clamp(24px, 2.6vw, 38px); }
.work__lead .work__desc {
  font-size: clamp(15px, 1.2vw, 17.5px);
  max-width: 52ch;
}

/* ---- The remaining four, two up ---------------------------------------- */
.work__row {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: clamp(30px, 4vw, 72px);
  padding: clamp(26px, 3.6vh, 40px) 0 clamp(30px, 4.2vh, 46px);
}

/* Short terracotta tick above each name — the page's accent, in the same
   hairline vocabulary as the rules themselves. */
.work__item .work__name::before {
  content: "";
  display: block;
  width: 24px;
  height: 2px;
  background: var(--brand);
  margin-bottom: clamp(12px, 1.6vh, 18px);
}

.work__name {
  font-family: var(--serif);
  font-weight: 400;
  font-size: clamp(19px, 1.9vw, 26px);
  line-height: 1.16;
  color: var(--ink);
  margin: 0 0 clamp(10px, 1.4vh, 16px);
}
.work__desc {
  font-family: var(--sans);
  font-size: clamp(14.5px, 1.12vw, 16.5px);
  line-height: 1.62;
  color: var(--ink-body);
  margin: 0;
  max-width: 44ch;
}

/* ---- Closing line — lifted out of the grid, not stranded in a cell ------ */
.work__close {
  font-family: var(--serif);
  font-weight: 400;
  font-size: clamp(20px, 2.2vw, 31px);
  line-height: 1.24;
  color: var(--ink);
  text-align: center;
  margin: clamp(34px, 4.6vh, 58px) auto 0;
  max-width: 34ch;
  text-wrap: balance;
}
/* Terracotta, not sand: sand carries the 66px headline fine but goes muddy
   against cream at this size. Block so the answer always lands on its own
   line instead of wrapping mid-phrase. */
.work__close em {
  display: block;
  font-style: normal;
  color: var(--brand);
}

.work__cta {
  display: flex;
  justify-content: center;
  margin-top: clamp(28px, 3.8vh, 44px);
}

@media (max-width: 860px) {
  .work__title { max-width: none; }
  .work__row {
    grid-template-columns: 1fr;
    gap: clamp(28px, 4.4vh, 40px);
  }
  .work__desc { max-width: none; }
  .work__cta .btn--primary { width: 100%; }
}

/* ====================== "CLIENT STORIES" ================================= */
/* Plain cream against .work's tint above it, so two light sections back to
   back still read as two. The carousel geometry lives here and the paging
   logic in home.js (setupStories); the ONE number they share is the 860px
   breakpoint, and JS never hard-codes it — it measures a slide against the
   viewport to learn how many are on a page, so this sheet stays the authority. */
.stories {
  position: relative;
  z-index: 1;
  background: var(--cream-bg);
  color: var(--ink);
  padding: clamp(72px, 11vh, 132px) var(--page-pad, clamp(22px, 8.6vw, 124px)) clamp(80px, 12vh, 140px);
}

.stories__title {
  margin-bottom: clamp(16px, 2.2vh, 26px);
  max-width: 20ch;
  text-wrap: balance;
}

.stories__lede {
  font-family: var(--sans);
  font-size: clamp(15px, 1.2vw, 17.5px);
  line-height: 1.62;
  color: var(--ink-body);
  margin: 0 0 clamp(48px, 8vh, 104px);
  max-width: 46ch;
}

/* Arrows flank the viewport, dots span underneath. Both arrows are grid items
   rather than absolutely positioned, so the viewport is genuinely narrower than
   the section and nothing overlaps the outermost quotes. */
.stories__carousel {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: center;
  column-gap: clamp(10px, 1.8vw, 26px);
}

.stories__arrow {
  grid-row: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  width: clamp(38px, 3.2vw, 44px);
  height: clamp(38px, 3.2vw, 44px);
  padding: 0;
  border: 0;
  border-radius: 999px;
  background: var(--ink);
  color: var(--cream-bg);
  cursor: pointer;
  /* Held back to 70% at rest at every width, so the controls sit under the
     quotes rather than competing with them, and come to full strength on
     hover or keyboard focus. The whole button fades, fill and chevron
     together — a solid chevron in a translucent disc reads as mismatched at
     this size. */
  opacity: .7;
  transition: background-color .25s ease, opacity .25s ease, transform .25s ease;
}
.stories__arrow--prev { grid-column: 1; }
.stories__arrow--next { grid-column: 3; }
.stories__arrow svg { width: 55%; height: 55%; display: block; }
.stories__arrow:hover { background: var(--brand); opacity: 1; }
.stories__arrow:active { transform: translateY(1px); }
.stories__arrow:focus-visible { opacity: 1; outline: 2px solid var(--brand); outline-offset: 3px; }

.stories__viewport {
  grid-row: 1;
  grid-column: 2;
  overflow: hidden;
  touch-action: pan-y;          /* vertical scrolling still belongs to the page */
}

/* All nine slides sit in ONE flex row, which is what makes every attribution
   bottom-align to the same line and keeps the section's height constant as you
   page. The row is 100% of the viewport wide whatever the slide count, so one
   page is always exactly 100% of a translate, desktop or mobile. */
.stories__track {
  display: flex;
  align-items: stretch;
  margin: 0;
  padding: 0;
  list-style: none;
  transform: translate3d(calc(var(--page, 0) * -100% + var(--drag, 0px)), 0, 0);
  transition: transform .55s cubic-bezier(.5, .05, .2, 1);
}
.stories__track.is-dragging { transition: none; }   /* follow the finger 1:1 */

.quote {
  flex: 0 0 calc(100% / 3);
  display: flex;
  flex-direction: column;
  padding-inline: clamp(16px, 2vw, 30px);
}
/* Hairlines BETWEEN the columns of a page, never around a cell — the same
   language as .steps. The first slide of each page starts a page, so it takes
   no rule; that is what 3n+1 is picking out. */
.quote:nth-child(3n + 1) { padding-left: 0; }
.quote:not(:nth-child(3n + 1)) { border-left: 1px solid var(--rule); }
.quote:nth-child(3n) { padding-right: 0; }

.quote__text {
  font-family: var(--sans);
  font-style: italic;
  font-size: clamp(14.5px, 1.12vw, 16.5px);
  line-height: 1.62;
  color: var(--ink-body);
  margin: 0 0 clamp(30px, 4.4vh, 52px);
}

/* margin-top: auto is what bottom-anchors the attribution against the tallest
   quote in the whole set. */
.quote__attr {
  margin: auto 0 0;
  text-align: right;
}
.quote__name {
  display: block;
  font-family: var(--serif);
  font-style: italic;
  font-size: clamp(24px, 2.2vw, 32px);
  line-height: 1.1;
  color: var(--ink);
  margin-bottom: clamp(4px, .6vh, 8px);
}
/* Given name terracotta, surname ink — the same two-tone italic as
   .person__name, so em inherits the italic rather than resetting it. */
.quote__name em { font-style: inherit; color: var(--brand); }
/* A quote with no role line: the attribution is bottom aligned, so the name's
   trailing margin would leave it floating above the block's bottom edge. */
.quote__name:last-child { margin-bottom: 0; }
.quote__role {
  display: block;
  font-family: var(--sans);
  font-size: clamp(13.5px, 1.05vw, 15.5px);
  line-height: 1.4;
  color: var(--ink-body);
}

/* ---- Pagination dots ---------------------------------------------------- */
/* Three pages on desktop and nine on a phone, so the row has to cope with a
   count it cannot show comfortably. Above --dot-window it becomes an iOS style
   sliding window: a fixed five-slot opening, the row translated to keep the
   active dot centred, and the dots at a truncated edge scaled down to say
   "there's more this way". Scaling is a transform, so the slots stay
   equidistant, which is how iOS draws it too. */
.stories__dots {
  --dot-size: 8px;
  --dot-gap: 12px;
  --dot-window: 5;              /* read by JS: how many slots the window shows */
  --win-start: 0;
  grid-column: 1 / -1;
  grid-row: 2;
  justify-self: center;
  overflow: hidden;             /* the window; width is set by the modifier below */
  margin-top: clamp(30px, 4.4vh, 50px);
}
.stories__dots-row {
  display: flex;
  gap: var(--dot-gap);
  transform: translateX(calc(var(--win-start) * (var(--dot-size) + var(--dot-gap)) * -1));
  transition: transform .35s ease;
}
/* Only added when the dots outnumber the window, so three dots are simply
   three dots with no clipping and no scaling. */
.stories__dots--windowed {
  width: calc(var(--dot-window) * var(--dot-size) + (var(--dot-window) - 1) * var(--dot-gap));
}

.stories__dot {
  flex: 0 0 var(--dot-size);
  width: var(--dot-size);
  height: var(--dot-size);
  padding: 0;
  border: 1px solid var(--ink-mute);
  border-radius: 999px;
  background: transparent;
  cursor: pointer;
  transition: background-color .25s ease, border-color .25s ease, transform .35s ease;
}
.stories__dot.is-active { background: var(--ink); border-color: var(--ink); }
.stories__dot:focus-visible { outline: 2px solid var(--brand); outline-offset: 3px; }
/* The two shrink steps at a truncated edge, matching the ratios iOS reads as
   (and that Swiper's dynamic bullets use): one third and two thirds. */
.stories__dot--near { transform: scale(.66); }
.stories__dot--far  { transform: scale(.33); }

@media (max-width: 860px) {
  .stories__title, .stories__lede { max-width: none; }
  /* One quote per page: the slide widens to the whole viewport, which alone
     turns three pages into nine — the same translate still moves one page. */
  .quote {
    flex-basis: 100%;
    padding-inline: 0;
  }
  /* Same selector as the desktop rule, not a bare .quote: :not(:nth-child())
     outweighs a plain class, and a media query adds no specificity of its own,
     so the narrower selector has to be repeated here to win. */
  .quote:not(:nth-child(3n + 1)) { border-left: 0; }
  /* The arrows keep flanking the quote here rather than dropping into the dot
     row, and stay vertically centred on the quote block (grid row 1 is the
     viewport's own height, so align-items: center on the grid does it).
     Room for them comes from pulling THIS ROW alone out into the section's
     padding: the headline and lede keep the shared side padding and so
     stay lined up with .work above and .audience below, which a step in the
     left edge would break. The pull is capped at 22px and always leaves a
     12px margin to the screen edge, so it can't overflow on a 320px phone. */
  .stories__carousel {
    margin-inline: calc(-1 * min(22px, clamp(22px, 8.6vw, 124px) - 12px));
  }
}

/* The slide transition is the one bit of horizontal motion on the page, so it
   goes first under reduced motion; paging still works, it just cuts. */
@media (prefers-reduced-motion: reduce) {
  .stories__track,
  .stories__dots-row,
  .stories__dot { transition: none; }
}

/* ====================== "WHO IT'S FOR" =================================== */
/* The mockup had two white rounded cards floating on cream — a radius and a
   fill that appear nowhere else on this page. Replaced with a hard full-bleed
   split: the two audiences set against each other, divided by a drawn rule,
   the right half carrying the sand tint. Composition does the work, not boxes. */
.audience {
  position: relative;
  z-index: 1;
  background: var(--cream-bg);
  color: var(--ink);
  /* No horizontal padding on the SECTION, unlike every other one here — the
     split below has to reach the browser edges, and the old way of doing that
     (a negative margin cancelling the section's padding) can't survive the
     content cap: --page-pad contains a percentage, and inside a padded box
     that percentage resolves against the content width, not the viewport.
     So the padding moved down onto the children that actually want it. */
  padding: clamp(64px, 9vh, 108px) 0 0;
}

/* Carries what the section's own padding used to. A wrapper rather than
   padding on the eyebrow and the headline directly, because box-sizing is
   border-box sitewide and .audience__title's max-width: 30ch is a measure —
   padding on that element would eat 248px out of it. */
.audience__head { padding-inline: var(--page-pad, clamp(22px, 8.6vw, 124px)); }

.audience__title {
  margin-bottom: clamp(40px, 5.4vh, 68px);
  max-width: 30ch;
  text-wrap: balance;
}
/* No nowrap here. It used to hold "two kinds" together, back when the em was a
   phrase inside a longer headline and "two" could strand at a line end. The em
   is now the WHOLE headline, so a nowrap would stop it wrapping at all and
   overflow a 375px screen. Leave the wrapping to text-wrap: balance above. */

.audience__split {
  position: relative;
  display: grid;
  /* Four tracks, not two: a centre pair holding half the content column each,
     with a 1fr gutter either side floored at --page-edge. That reproduces
     --page-pad exactly at every width — free space above the column is split
     evenly, and below it the floor takes over — WITHOUT any child needing to
     know the viewport width, which a grid item can't: its percentages resolve
     against its own track. The centre tracks are minmax(0, …) so they shrink
     together on a narrow screen instead of overflowing.
     Verify it by measuring .audience__side--a's left edge against .steps'. */
  grid-template-columns:
    minmax(var(--page-edge, clamp(22px, 8.6vw, 124px)), 1fr)
    minmax(0, calc(var(--content-max, 1140px) / 2))
    minmax(0, calc(var(--content-max, 1140px) / 2))
    minmax(var(--page-edge, clamp(22px, 8.6vw, 124px)), 1fr);
  /* The sand tint is a BACKGROUND here rather than a fill on the right half,
     which is what lets it keep bleeding to the browser edge while the copy
     inside it stops at the content column. Its edge is at 50% of a full-width
     element, and the column is centred, so the two midlines coincide — the
     tint boundary still lands exactly where the two halves meet. */
  background: linear-gradient(to right,
    transparent 0 50%, var(--cream-panel) 50% 100%);
}
/* .audience__topline carries no rule of its own any more, and that's not an
   omission: [data-rule] in site.css is display: block and the section is now
   unpadded, so the hairline is full-bleed for free. It used to cancel the
   section's padding with a negative margin. Still deliberately edge to edge
   rather than capped — it runs above both halves of a split that bleeds, and
   a vertical rule between the columns would be invisible anyway since the tint
   edge is already there. */
.audience__side {
  padding-block: clamp(40px, 6vh, 76px) clamp(48px, 7vh, 88px);
}
/* Inner padding only — the outer inset is the grid's gutter track above, so
   the left column lines up with the headline and the two halves meet in the
   middle exactly as before. */
.audience__side--a {
  grid-column: 2;
  padding-right: clamp(24px, 3.4vw, 56px);
}
.audience__side--b {
  grid-column: 3;
  padding-left: clamp(24px, 3.4vw, 56px);
}

.audience__who {
  font-family: var(--serif);
  font-weight: 400;
  font-size: clamp(21px, 2.2vw, 31px);
  line-height: 1.14;
  color: var(--ink);
  margin: 0 0 clamp(14px, 1.8vh, 20px);
  max-width: 20ch;
}
.audience__body {
  font-family: var(--sans);
  font-size: clamp(14.5px, 1.12vw, 16.5px);
  line-height: 1.62;
  color: var(--ink-body);
  margin: 0;
  max-width: 42ch;
}

/* ====================== "THE PEOPLE BEHIND IT" =========================== */
/* Dark, and the same dark as the sequence so it reads as the page folding
   back to its opening register rather than introducing a third tone. */
.people {
  position: relative;
  z-index: 1;
  background: var(--seq-bg);
  color: var(--seq-body);
  padding: clamp(76px, 11vh, 132px) var(--page-pad, clamp(22px, 8.6vw, 124px));
}

/* The one eyebrow on a dark background. Centred, like everything above the row. */
.people .eyebrow {
  color: var(--mute-dark);
  text-align: center;
}

/* NB no text-wrap: balance here, unlike the three cream headlines — see below. */
.people__title {
  color: var(--seq-cream);
  margin: 0 auto clamp(52px, 7vh, 88px);
  max-width: 30ch;
  text-align: center;
}
/* The two sentences are the structure, so the break is explicit rather than
   left to balance — which split them mid-sentence. Same trick as the hero
   headline, and the space before the <br> matters there too.
   Unlike .close__title, this <br> is NOT hidden on mobile. It used to be, and
   that was right while the headline was one colour; now the second sentence is
   terracotta, letting it wrap naturally starts the colour mid-line
   ("infrastructure. We do the") and the change reads as an accident. Keeping
   the break costs a fourth line on a phone and makes the two sentences land as
   two blocks, which is what the colour is there to say. */

/* Portrait, copy, portrait. The outer two columns size to the circles; the
   middle is the argument, held narrow so it reads as a caption between two
   faces rather than as a body of text with decoration either side. */
.people__row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 372px) minmax(0, 1fr);
  gap: clamp(28px, 3.6vw, 56px);
  align-items: start;
  justify-items: center;
  max-width: 1240px;
  margin-inline: auto;
}
/* The copy sits a little below the top of the circles, as in the mockup —
   optical, not structural, so it collapses to 0 when the row stacks. */
.people__copy { padding-top: clamp(0px, 2.6vw, 38px); }
.people__body {
  font-family: var(--sans);
  font-size: clamp(15px, 1.16vw, 17px);
  line-height: 1.66;
  color: var(--seq-body);
  margin: 0 0 clamp(18px, 2.4vh, 26px);
}
.people__body:last-child { margin-bottom: 0; }

/* ---- The two of them. ------------------------------------------------- */
/* The circular crop and its radial glow are baked into the JPGs, which sit on
   WHITE corners — so border-radius: 50% isn't decoration here, it's what stops
   two white squares appearing on the dark section.
   There is deliberately NO filter on these: the portraits carry whatever grade
   the source files have. An earlier grayscale(1) contrast(1.04) was removed at
   the author's request — if you re-add a filter, you're overriding a retouch
   decision made outside the browser. */
.person { text-align: center; }
.person__photo {
  width: 100%;
  max-width: clamp(180px, 23vw, 330px);
  margin: 0 auto clamp(20px, 2.8vh, 32px);
}
.person__photo:empty { display: none; }
.person__photo img {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 1 / 1;
  object-fit: cover;
  border-radius: 50%;
}
.person__name {
  font-family: var(--serif);
  font-weight: 400;
  font-style: italic;
  font-size: clamp(26px, 2.7vw, 38px);
  line-height: 1.1;
  color: var(--seq-cream);
  margin: 0 0 clamp(14px, 1.9vh, 22px);
}
/* Given name held back in terracotta, same device as .section-title em — but
   italic here rather than reset to normal, since the whole name is italic. */
.person__name em { font-style: inherit; color: var(--brand); }

/* The icon SVGs carry their own ring and a baked-in cream fill, so they're
   plain <img>. Deliberately NOT CSS-masked: the glass rasterises this section
   through html-to-image, and an <img> inlines reliably where an external
   mask-image is a coin toss — see the capture notes in CLAUDE.md. That costs
   the terracotta hover, so the affordance is opacity instead. */
.person__social {
  display: flex;
  justify-content: center;
  gap: 24px;
  list-style: none;
  margin: 0;
  padding: 0;
}
.person__social a {
  display: block;
  opacity: .78;
  transition: opacity .25s ease;
}
.person__social a:hover,
.person__social a:focus-visible { opacity: 1; }
.person__social img {
  display: block;
  width: 23px;
  height: 23px;
}

/* ====================== CLOSING CTA ====================================== */
/* The page's crescendo, so this headline is the largest type on it. */
.close {
  position: relative;
  z-index: 1;
  background: var(--cream-bg);
  color: var(--ink);
  padding: clamp(84px, 13vh, 164px) var(--page-pad, clamp(22px, 8.6vw, 124px)) clamp(96px, 15vh, 190px);
  text-align: center;
}
.close__title {
  font-family: var(--serif);
  font-weight: 400;
  font-size: clamp(40px, 6.4vw, 96px);
  line-height: 1.02;
  letter-spacing: -.008em;
  margin: 0 auto clamp(24px, 3.2vh, 38px);
  max-width: 28ch;
}
/* Its em treatment is shared with .section-title, over in site.css. */
@media (max-width: 860px) { .close__title br { display: none; } }
.close__lede {
  font-family: var(--sans);
  font-size: clamp(15px, 1.2vw, 17.5px);
  line-height: 1.62;
  color: var(--ink-body);
  margin: 0 auto clamp(32px, 4.4vh, 52px);
  max-width: 48ch;
}
.close__actions { display: flex; justify-content: center; }
.close__email {
  font-family: var(--sans);
  font-size: clamp(14px, 1.05vw, 15.5px);
  color: var(--ink-mute);
  margin: clamp(18px, 2.4vh, 26px) 0 0;
}
.close__email a {
  color: var(--brand);
  text-decoration: none;
  border-bottom: 1px solid rgba(200,68,30,.35);
  transition: border-color .25s ease;
}
.close__email a:hover { border-bottom-color: var(--brand); }

@media (max-width: 860px) {
  .audience__title, .people__title { max-width: none; }
  /* Stacked, so there are no gutter tracks and no left/right halves — the tint
     goes back to being a fill on the second audience rather than the split's
     background gradient, and each side pads itself again. */
  .audience__split {
    grid-template-columns: 1fr;
    background: none;
  }
  .audience__side {
    grid-column: 1;
    padding-inline: var(--page-pad, clamp(22px, 8.6vw, 124px));
    padding-block: clamp(34px, 5vh, 48px);
  }
  .audience__side--b { background: var(--cream-panel); }
  .audience__body { max-width: none; }
  /* Argument first, then the two faces — so the copy jumps the row. */
  .people__row { grid-template-columns: 1fr; gap: clamp(40px, 6vh, 60px); }
  .people__copy { order: -1; padding-top: 0; }
  .person__photo { max-width: 260px; }
  .close__actions .btn--primary { width: 100%; }
}
