/* Page sections below the hero — layout, structure and motion per the Figma homepage
   frame. Numbers are the mock's (1512 ref), scaled through --u where they must stay
   proportional. */

/* ---- Shared ----------------------------------------------------------------------- */

.section {
  position: relative;
}

.stack       { display: flex; flex-direction: column; }
.stack-8     { gap: 8px; }
.stack-12    { gap: 12px; }
.stack-16    { gap: 16px; }
.stack-24    { gap: 24px; }
.stack-32    { gap: 32px; }
.center      { align-items: center; text-align: center; }

/* ---- 03 · Why us — CEO intro + audio waveform --------------------------------------- */

/* Pinned scroll: tall section, sticky viewport-high inner. Scroll through it = the
   audio track "plays" (ui.js). */
.s-why {
  height: 400vh; /* 5 phrases x 80vh each: a short gesture visibly advances the phrase */
}

.why-sticky {
  position: sticky;
  top: 0;
  height: 100vh;
  overflow: hidden;
  /* Closing with the window (.page-tint, --shrink): the section's own copy and track are
     cut to the same slot, so the screen shuts on them rather than leaving them standing
     over the white */
  clip-path: inset(0 calc(var(--shrink, 0) * var(--page-x)) calc(var(--shrink, 0) * 100%) calc(var(--shrink, 0) * var(--page-x)));
}

/* Text pinned to a fixed band under the header, track pinned to the bottom — neither
   depends on how many lines the current phrase has (the phrase cell is as tall as the
   longest phrase, so centering the pair made short phrases float with a huge gap) */
.s-why .why-inner {
  position: relative;
  height: 100%;
  width: 100%;
}

/* Leaving for "what we do": the whole pinned screen -- the track and the copy on it -- goes
   up and out, the way the hero's copy does. Scroll-bound on --leave (0..1, ui.js), so it
   tracks the wheel rather than running on a clock of its own, and the sticky viewport's
   overflow does the hiding. */
.s-why .why-sticky > * {
  transform: translateY(calc(var(--leave, 0) * -40vh));
  opacity: clamp(0, calc((1 - var(--leave, 0)) * 1.9), 1); /* thins out a touch ahead of the move */
  will-change: transform, opacity;
}

/* The player sits on the screen's middle line, right where the white plate's curtain
   (--shrink, the clip above) was cutting it in half on the way to "what we do". It leaves
   ahead of the rest instead: a little further up (on top of the -40vh the whole screen
   travels) and gone by a third of the leave, before that edge reaches it. `translate` and
   `filter` are its own, so the centring transform and the pill's opacity states stay. */
.s-why .player {
  translate: 0 calc(var(--leave, 0) * -30vh);
  filter: opacity(clamp(0, calc(1 - var(--leave, 0) * 3), 1));
}

/* Centered in the band between the header and the track (track = 200px + 5vh) */
.why-head {
  position: absolute;
  z-index: 2; /* text stays above the bars when they stand tall on entry */
  top: calc(var(--header-h) + 132px); /* the band is centred, so the eyebrow + phrase sit half of this (132px) lower */
  bottom: calc(200px + 9vh);
  left: 0;
  right: 0;
  justify-content: center;
  gap: var(--gap-eyebrow); /* eyebrow to phrase */
  padding: 0 var(--page-x-lg);
}

/* Only the active phrase is in flow (the cell is exactly its height, so the group
   re-centers per phrase); the others overlay it from the same top edge for the
   rise-in / exit-up handoff */
.why-lines {
  position: relative;
  width: 100%;
}

.why-line {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  margin: 0 auto;
  text-align: center;
  /* Motion comes from the per-line masks below (same as the site-wide reveal); the
     phrase itself only gates visibility so an exiting line never overlaps the next */
  visibility: hidden;
  transition: visibility 0s linear 0.9s;
  /* Karaoke fill overrides the gradient text: words are muted (same grey as the unplayed
     bars) and turn ink as the playhead reaches them (ui.js) */
  background: none;
  color: var(--line);
  -webkit-background-clip: border-box;
  background-clip: border-box;
}

/* Continuous fill: each word carries --f 0..1 (ui.js) and paints ink up to that point,
   muted beyond — the ink edge glides through the phrase like the scroll indicator's
   fill, instead of whole words flipping. --f is registered so it also eases. */
.why-line .w {
  color: transparent;
  background-image: linear-gradient(
    90deg,
    var(--ink) calc(var(--f, 0) * 100%),
    var(--line) calc(var(--f, 0) * 100%)
  );
  -webkit-background-clip: text;
  background-clip: text;
  /* No transition on --f: the chase loop already advances it every frame, and an
     easing lag here let the next word start before the previous one finished */
}

/* `.no-fill` on the section: no karaoke, words are plain ink — phrases simply change.
   Remove the class to bring the fill back. */
.s-why.no-fill .why-line .w {
  background-image: none;
  color: var(--ink);
}

.why-line.is-active {
  position: relative;
  visibility: visible;
  transition-delay: 0s;
}

/* Line masks: hidden below → active rises up → past exits upward */
.why-line .rl {
  display: block;
  overflow: hidden;
  padding-bottom: 0.12em;
  margin-bottom: -0.12em;
}

/* Handoff order: the outgoing phrase leaves first (fast, no delay), the incoming one
   starts after it is gone (0.35s) — they never overlap on screen */
.why-line .rli {
  display: block;
  transform: translateY(var(--rise-mask));
  transition: transform 0.9s var(--ease-out);
  transition-delay: calc(var(--d, 0s) + 0.35s);
  will-change: transform;
}
/* The first phrase has no outgoing one to wait for: at the handover (story-handoff.js,
   the hand has just become the EQ) it rises at once. (0.35s -> 0s) */
.why-line:first-child .rli { transition-delay: var(--d, 0s); }

.why-line.is-active .rli {
  transform: none;
}

.why-line.is-past .rli {
  transform: translateY(var(--rise-mask-out));
  transition-duration: 0.45s;
  transition-delay: 0s;
}

/* Scrolling back: the phrase on screen goes at once, and the one before drops in from above
   at once, all its lines together, a fifth quicker than a phrase rises (0.9 -> 0.75s). As a
   0.3s block the way back read as many times faster than the way on; line by line after a
   wait (0.29s, 0.117s apart), as held up (19 Oct) */
.s-why.is-back .why-line .rli {
  transition-duration: 0s;
  transition-delay: 0s;
}
.s-why.is-back .why-line.is-active .rli { transition-duration: 0.75s; }

/* The closing phrase is set larger than the rest: the section opens on a statement and
   ends on one */
.why-line-p.why-line-last {
  font-size: max(40px, min(calc(110 * var(--u)), calc(92 * var(--uh))));
  letter-spacing: var(--track-display); /* display size: the hero's tracking */
  max-width: max(640px, calc(1200 * var(--u)));
}

/* The phrases themselves: sized so a full sentence still fits the viewport */
.why-line-p {
  max-width: max(560px, calc(1040 * var(--u)));
  /* Smaller than the headline so a full thought fits in 2–3 lines; capped by viewport
     height like the hero */
  font-size: max(26px, min(calc(60 * var(--u)), calc(60 * var(--uh))));
  line-height: 1.35;
  letter-spacing: var(--track-head);
  text-wrap: balance; /* even line lengths, no single-word last line */
}

/* Past the mock the phrase's size is held back by the screen's height (--uh); if its
   column kept growing with the width alone, at 2560 a phrase would run ~50 characters a
   line, a paragraph rather than a line spoken. The column is capped at the mock's own
   measure in em (1040 / 60, 1200 / 110), so every width sets the phrase the way 1512 does.
   The line masks are built from the rendered lines, so they follow the new breaks by
   themselves. */
@media (min-width: 1513px) {
  .why-line-p { max-width: min(calc(1040 * var(--u)), 17.35em); }
  .why-line-p.why-line-last { max-width: min(calc(1200 * var(--u)), 10.9em); }
}

/* Track box: a 120px band 5vh above the screen's bottom edge. The waveform itself is the
   3D EQ on the scene canvas (foreground-eq.js); this box places the player on its line. */
.waveform {
  position: absolute;
  z-index: 1;
  left: 0;
  right: 0;
  bottom: 5vh;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 4px;
  width: 100%;
  height: 120px; /* the band's full width; the pill sits on its centre line */
}

/* The docked control is also the touch and keyboard fallback for the cursor. */
.player,
.player.rv-block,
.player.rv-block.is-in {
  position: absolute;
  top: 50%;
  left: 50%;
  z-index: 2;
  width: 112px;
  height: 112px;
  transform: translate(-50%, -50%);
  border-radius: var(--r-round);
  transition: width .45s ease, height .45s ease, border-radius .45s ease, opacity var(--dur-fast) ease;
}
/* A soft halo behind the player. */
.player::before {
  content: "";
  position: absolute;
  inset: -22px -26px;
  border-radius: inherit;
  background: radial-gradient(ellipse at 50% 50%, rgba(255, 255, 255, 0.55) 30%, rgba(255, 255, 255, 0) 72%);
  filter: blur(12px);
  z-index: -1;
  pointer-events: none;
}
.player-play, .player-cta {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  border: 0;
  border-radius: inherit;
  background: var(--white);
  color: var(--ink);
  box-shadow: var(--shadow-float);
  transition: opacity .25s ease, visibility .25s ease;
}
.player-play { cursor: pointer; perspective: 650px; }
/* --ring-out: the ring stands this far outside the disc, air between (the cursor carries a copy of the disc) */
.player-play, .cursor-visual { isolation: isolate; --ring-out: 10px; }
.player-disc {
  position: absolute;
  inset: 0;
  border-radius: var(--r-round);
  transform-style: preserve-3d;
  transition: transform .65s cubic-bezier(.4, 0, .2, 1);
}
.has-played .player-disc { transform: rotateY(180deg); }
.player-face {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  border-radius: var(--r-round);
  background: var(--white);
  backface-visibility: hidden;
  -webkit-backface-visibility: hidden;
}
.player-back { transform: rotateY(180deg); }
.player-ava { position: absolute; inset: 0; overflow: hidden; border-radius: var(--r-round); background: var(--ava-ground); }
.player-ava img, .player-talk-avatar img { width: 100%; height: 100%; object-fit: cover; border-radius: inherit; }
.player-action {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  color: var(--ink);
  font-size: var(--fs-btn);
  line-height: 1;
}
.player-action-icon { width: 0; height: 0; border-block: 7px solid transparent; border-left: 10px solid currentColor; margin-left: 2px; } /* a quarter bigger than a plain glyph triangle; the left margin is a sixth of the width, keeping the centroid on the disc centre */
.player-readout {
  position: absolute;
  left: 50%;
  /* Centred on the ring's line (the ring's radius is 52.5/56 of half its box, and its box
     is the disc plus --ring-out on every side), then a little further out for air between
     the digits and the portrait's edge */
  top: calc(100% + var(--ring-out) * 0.9375 - 3.5px + 4px);
  translate: -50% -50%;
  color: var(--ink);
  font-size: var(--fs-micro);
  font-weight: 400;
  font-variant-numeric: tabular-nums;
  line-height: 1;
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--dur-fast) ease;
}
/* Plain ink: it stands outside the disc, and a difference blend inside the isolated
   button only ever saw a transparent backdrop and came out white */
.has-played .player-readout { opacity: 1; }
/* The ring is one open loop: closed while idle, and once the readout is up it breaks at the
   bottom (--ring-gap, in hundredths of the circumference) with the time standing in the
   break. The track and the fill share the break, and the fill runs from the break's left
   edge, over the top, to its right edge. */
.player-ring {
  --ring-gap: 0;
  position: absolute;
  /* An SVG is a replaced element: inset alone does not size it, the box must be explicit */
  top: calc(-1 * var(--ring-out));
  left: calc(-1 * var(--ring-out));
  width: calc(100% + 2 * var(--ring-out));
  height: calc(100% + 2 * var(--ring-out));
  max-width: none; /* global.css caps every svg at 100%: this one must outgrow its button */
  overflow: visible;
  /* 90deg puts the path's start at the bottom; half the gap (1.8deg per hundredth) moves
     it to the break's left edge, from where the stroke runs clockwise */
  transform: rotate(calc(90deg + var(--ring-gap) * 1.8deg));
  transition: transform .45s ease;
  pointer-events: none;
}
.player-ring circle {
  fill: none;
  stroke: var(--line);
  stroke-width: .8;
  stroke-dasharray: calc(100 - var(--ring-gap)) 100;
  transition: stroke-dasharray .45s ease;
}
.player-ring .player-ring-fill { stroke: var(--ink); stroke-dashoffset: calc((100 - var(--ring-gap)) * (1 - var(--p, 0))); }
/* The ring stands outside the disc, over the page ground, never over the portrait: plain
   ink in both states (a difference blend here saw only the button's isolated, transparent
   backdrop and came out white) */
.has-played .player-ring { --ring-gap: 15; }
/* While the voice is off, the ring is the phrases' pagination: one equal segment per phrase,
   the ones up to the phrase on screen in ink (why-voice.js writes the dashes; a new one grows
   along the ring). On the disc, and on the cursor's copy of it (desktop, why-cursor.js). */
.s-why:not(.has-played) .player-ring circle,
.cursor:not(.has-played) .player-ring circle {
  stroke-dasharray: var(--seg-track, 100 0);
  stroke-dashoffset: -1; /* half a gap (why-voice.js SEG_GAP): the gaps on the joins */
}
.s-why:not(.has-played) .player-ring .player-ring-fill,
.cursor:not(.has-played) .player-ring .player-ring-fill { stroke-dasharray: var(--seg-fill, 0 100); }
.player-cta, .cursor-talk {
  justify-content: flex-start;
  gap: 10px;
  padding: 0 20px 0 6px; /* the 6px on the right goes to the avatar-to-dot air instead */
  background: var(--white);
  color: var(--ink);
  font-size: var(--fs-btn);
  font-weight: 600;
  line-height: 1;
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
  text-decoration: none;
  white-space: nowrap;
}
.player-talk-avatar { flex: 0 0 52px; width: 52px; height: 52px; margin-right: 10px; border-radius: var(--r-round); background: var(--ava-ground); }
.player-cta {
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
}
.s-why.is-final .player { width: 224px; height: 64px; border-radius: 40px; }
.s-why.is-final .player-play { opacity: 0; visibility: hidden; pointer-events: none; }
.s-why.is-final .player-cta { opacity: 1; visibility: visible; pointer-events: auto; }
.s-why.has-play-cursor .player:not(:focus-within) { opacity: 0; pointer-events: none; }
.s-why.has-play-cursor .player:not(:focus-within) > * { pointer-events: none; }
.player :focus-visible { outline: 2px solid var(--ink); outline-offset: 5px; }
@media (prefers-reduced-motion: reduce) {
  .player, .player.rv-block, .player-play, .player-cta, .player-disc, .player-readout { transition: none; }
  .player-cta .online-dot::before, .cursor-talk .online-dot::before { animation: none; }
}

.online-dot {
  position: relative;
  width: 8px;
  height: 8px;
  margin-right: 0; /* with the CTA's 12px gap, that's 12px to the label */
  border-radius: var(--r-round);
  background: var(--live);
  flex: 0 0 8px;
}

.online-dot::before {
  content: "";
  position: absolute;
  inset: -3px;
  border-radius: var(--r-round);
  background: var(--live-glow);
  animation: onlinePulse 2.2s ease-out infinite;
}

@keyframes onlinePulse {
  0% { transform: scale(0.6); opacity: 0.9; }
  70% { transform: scale(2.1); opacity: 0; }
  100% { transform: scale(2.1); opacity: 0; }
}

/* ---- 04 · What we do — one pinned screen the scroll steps through --------------- */

/* Five services, eight integrations and the stack. The card stands still and the
   scroll re-cuts it -- the list advances, the small copy and the grey slot move to a new
   corner for each of the three subsections, and the mark inside the slot flies into that
   subsection's own form (tab-figure.js). services-ui.js owns the steps and text masks. */
.s-what {
  --steps: 14; /* five services, eight integrations, the stack (services-ui.js sets the live count) */
  --step-run: 35vh; /* scroll per service, integration or stack step (one wheel gesture steps one row: smooth-scroll.js) */
  height: calc(100vh + var(--steps) * var(--step-run));
  padding: 0;
}

.what-sticky {
  position: sticky;
  top: 0;
  height: 100vh;
  display: grid;
  place-items: stretch;
  /* No inset of its own: the card is the screen, so its copy can stand on the page's own
     gutter and line up with the hero's rather than on a margin inside a margin */
  padding: 0;
}

/* The card is the whole composition: everything in it is placed against these edges */
.what-card {
  --card-x: var(--page-x); /* the hero's own gutter -- the two left edges are one line */
  /* The card is the whole screen now, so its own copy has to clear the fixed header itself */
  --card-top: clamp(88px, 14vh, 150px); /* the eyebrow's line, clear of the header; 14vh leaves the list room to grow */
  position: relative;
  overflow: hidden;
  background: var(--white);
}

.what-card .what-sec {
  position: absolute;
  left: var(--card-x);
  top: var(--card-top);
  z-index: 3;
}
.what-card[data-sec="services"] .what-sec { top: calc(var(--card-top) + 24px); }

/* Every piece of copy uses the shared line masks, including the decorative slash. */
html.js .what-card :is(.what-row, .what-sec, .what-num, .what-tag, .what-copy, .chip):not(.rv) {
  visibility: hidden;
}
.what-card .eyebrow::before { content: none; }
.what-card .eyebrow .rli::before {
  content: "/";
  margin-right: var(--slash-gap);
  font-size: var(--slash-fs);
  vertical-align: var(--slash-y);
}
.what-card .what-num .rli::before { content: "["; margin-right: .55em; font-size: 1em; }
.what-card .rv[data-mask-reset] .rli,
.what-card .rv.rv-block[data-mask-reset] { transition: none; }
/* The copy turns over with every gesture, so its masks move quicker than the page's own
   (global.css: in --dur-reveal 0.7s, out 0.4s): the old line out, the new one in, briskly */
.what-card .rv.is-in .rli { transition-duration: 0.45s; }
.what-card .rv.is-in.is-out .rli { transition-duration: 0.25s; }

/* The list: only the subsection you are in is in play, and inside it the titles you have
   passed are gone, the one you are on is inked, the ones to come stand by in light grey */
.what-list {
  position: absolute;
  left: var(--card-x);
  top: calc(var(--card-top) + clamp(34px, 6vh, 72px));
  /* Stops short of the slot's edge rather than running up to it: the gutter between the
     two columns is part of the composition */
  width: calc(50% - var(--card-x) - clamp(32px, 6vw, 110px));
  bottom: clamp(20px, 6vh, 60px);
  z-index: 2;
  overflow: hidden;
}

.what-group {
  position: absolute;
  inset: 0 0 auto 0;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: max(5px, calc(9 * var(--u))); /* a little tighter between entries */
  visibility: hidden;
}

/* Services finish at the former number's lower edge; integrations stay centred. */
.what-card[data-sec="services"] .what-list,
.what-card[data-sec="integrations"] .what-list {
  top: 0;
  bottom: 0;
}
.what-card[data-sec="services"] .what-list { bottom: 10%; }
.what-card[data-sec="services"] .what-group[data-sec="services"] {
  top: auto;
  bottom: 0;
}
.what-card[data-sec="integrations"] .what-group[data-sec="integrations"] {
  top: 50%;
  translate: 0 -50%;
}

/* Nothing in the list moves: the whole of it stays put, and only the one you are on is
   inked. Coming and going is the page's own text reveal -- every entry rises from under
   its own mask, and leaves the same way (.rv, global.css; ui.js builds the wrappers). */
.what-group.is-on { visibility: visible; }

.what-row {
  margin: 0;
  /* Type comes from the List steps in typography.css (.list-lg / .list / .list-xl on the
     rows themselves): each subsection is sized so all of it stands in the column at once. */
  /* The rows that are off: quiet, but still 3.15:1 on the white card -- they are links to
     their entries, so WCAG 1.4.3 applies to them (large text: 3:1). Was #cfd1d5, 1.52:1. */
  color: var(--quiet-lg);
  cursor: pointer;
  transition: color var(--dur-slow) ease, opacity var(--dur-slow) ease;
}

/* The list is how you move through the section, not a caption beside it: hovering an entry
   shows it as it will read once you are on it, rather than as a third tone of its own */
@media (hover: hover) {
  .what-row:hover { color: var(--ink); }
}
.what-row.is-now { color: var(--ink); cursor: default; }

/* The stack's message is its headline; the eyebrow already names the subsection. */
.what-card[data-sec="tech"] .what-list {
  top: 0;
  bottom: calc(10% + 44px + clamp(32px, 8vh, 80px));
  width: calc(66% - var(--card-x));
}
.what-group[data-sec="tech"] {
  top: auto;
  bottom: 0;
}
.what-group[data-sec="tech"] .what-row {
  width: 100%;
  cursor: default;
}

/* The chips belong to the stack alone */
.what-chips {
  position: absolute;
  left: var(--card-x);
  bottom: 10%;
  width: calc(100% - var(--card-x) * 2);
  z-index: 2;
  margin: 0;
  padding: 0;
  list-style: none;
  display: flex;
  flex-wrap: nowrap;
  gap: 8px;
  visibility: hidden;
  pointer-events: none;
}

.what-chips .chip { flex-shrink: 0; }
.what-chips .chip.rv-block { transition-delay: var(--d-in, 0s); }
.what-chips .chip.rv-block.is-in.is-out {
  transition-duration: 0.4s;
  transition-delay: calc(var(--d-in, 0s) * var(--exit-stagger));
}

.what-card[data-sec="tech"] .what-chips {
  visibility: visible;
  pointer-events: auto;
}

/* Keep the number above its copy while longer descriptions grow upward. */
.what-detail {
  position: absolute;
  left: 50%;
  bottom: calc(45% - 3em);
  z-index: 3;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 32px;
  /* Not dead: the children set their own sizes, but the 3em in the bottom offsets above and
     in the mobile rule is measured against THIS size */
  font-size: var(--fs-cap);
}

/* Past the mock the copy stays its small size but a share of the height would keep opening
   under it, drifting the number and its line away from the mark below. Held instead at
   the mock's distance above the panel's top edge (69%): 31% + 102px = 45% - 3em at 982. */
@media (min-width: 1513px) {
  .what-card[data-sec="services"] .what-detail { bottom: calc(31% + 102px); }
}

.what-num {
  font-size: var(--fs-cap);
  letter-spacing: var(--track-caps);
}

.what-card[data-sec="integrations"] :is(.what-num, .what-tag),
.what-card[data-sec="tech"] .what-num { display: none; }

/* The tagline and its description are one block under the number (Figma: 12px between). */
.what-text {
  display: flex;
  flex-direction: column;
  gap: 12px;
}

/* A service with a page of its own (its row's data-href, services-ui.js) offers it on its
   plate's top edge, near the right: a white disc with a small plus which, under the cursor, opens
   into a pill -- "explore" and, at its end, the plus turned into our calls' dash
   (.btn-meter). It rises with the copy (block-rise), a beat after it (--cta-follow, as a
   call's button follows its title). A click opens the plate out into the service's page,
   as a works tile into its case (case-leave.js). Phones: on the small plate's corner. */
.what-go {
  position: absolute;
  /* 48px, a step bigger on a big screen with the calls' pills (global.css .btn, --ui-head) */
  --go: calc(48 * var(--ui-head));
  top: calc(69% - var(--go) / 2); /* centred on the services plate's top edge (.what-slot::before) */
  /* its centre 80px in from the plate's right edge -- on a big screen a step further in, with
     the disc itself (--ui-head: 86px at 1920, 96px at 2560). As far in as the plate had grown
     (--u: 101 / 135px) it stood too far from the corner (19 Oct) */
  right: calc(var(--card-x) + 80 * var(--ui-head) - var(--go) / 2);
  z-index: 4;
  display: inline-flex;
  align-items: center;
  height: var(--go);
  border-radius: var(--r-pill);
  background: var(--white);
  color: var(--ink);
  box-shadow: var(--lift-rest);
  font-size: var(--fs-btn);
  font-weight: 600;
  line-height: 1;
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
  text-decoration: none;
  white-space: nowrap;
  pointer-events: auto;
}
.what-go[hidden] { display: none; } /* (the display above outranks the attribute's own) */
.what-go.rv-block.is-in:not(.is-out) { transition-delay: var(--cta-follow); }
.what-card .what-go.rv-block.is-in.is-out { transition-duration: 0.25s; } /* = the rows' exit (.what-card .rv.is-in.is-out .rli) */
.what-go-label {
  max-width: 0;
  overflow: hidden;
  opacity: 0;
  transition: max-width var(--dur-base) var(--ease-out), padding var(--dur-base) var(--ease-out), opacity var(--dur-fast) ease;
}
/* The mark: a 12px plus of two 2px bars, square-ended, drawn with crisp edges -- the disc
   stands between whole pixels (a share of the card's height) and on a scaled screen (125,
   150%) the two bars, smoothed each its own way, read as two weights. Snapped to the
   screen's pixels, both get the same. (It stays 12px on a big screen's bigger disc: grown
   to a fraction of a pixel, the bars would snap apart again.) Open, its upright folds away and its bar draws out to
   the calls' dash, 16px (.btn-meter's 17, to the nearest that stays centred). */
.what-go-plus {
  flex: none;
  display: grid;
  place-items: center;
  width: var(--go);
  height: var(--go);
}
.what-go-plus svg {
  width: 12px;
  height: 12px;
  overflow: visible;
  fill: currentColor;
  shape-rendering: crispEdges;
}
.what-go-plus rect {
  transform-box: fill-box;
  transform-origin: center;
  transition: scale var(--dur-base) var(--ease-out);
}
/* Opened under a cursor (or the keyboard's focus) only: on a touch screen a tap held the
   hover and the disc grew into "explore" on its way out to the page */
.what-go:focus-visible .what-go-label { max-width: 8em; padding-left: 24px; opacity: 1; }
.what-go:focus-visible .go-bar { scale: 1.3333 1; } /* 12 → 16px */
.what-go:focus-visible .go-upright { scale: 1 0; }
@media (hover: hover) {
  .what-go:hover .what-go-label { max-width: 8em; padding-left: 24px; opacity: 1; }
  .what-go:hover .go-bar { scale: 1.3333 1; }
  .what-go:hover .go-upright { scale: 1 0; }
}
@media (prefers-reduced-motion: reduce) {
  .what-go-label, .what-go-plus rect { transition: none; }
}

.what-tag,
.what-copy {
  margin: 0;
  /* Short paragraph: 36ch fits four or five lines */
  max-width: 36ch;
  /* A paragraph, not a caption: tracks the UI unit (14 at 1512, 18.9 at 2560), floor 14px
     below the reference width */
  font-size: var(--fs-copy);
  font-weight: 300;
  line-height: 1.5;
  color: var(--muted);
}
/* The tagline stands out from the description by size, the headings' italic accent and
   ink (the copy under it stays muted), not by weight (it was a 500 upright at the copy's
   own 14). Ink as on phones (mobile-what.css) and on the case pages (case.css .cb-point-tag). */
.what-tag {
  font-size: var(--fs-tag);
  font-style: italic;
  font-weight: 400;
  color: var(--ink);
}

/* Copy and numbering leave through their masks before their text or position changes. */

/* The background grows independently of the mark's position. */
.what-slot {
  position: absolute;
  inset: 0;
  overflow: hidden;
}
.what-slot::before {
  content: "";
  position: absolute;
  left: 50%;
  top: 69%;
  right: 0;
  bottom: 0;
  background: var(--plate-grey) var(--plate-lit);
  clip-path: inset(0);
  /* It only ever opens: a corner of the card, then the right half to the full height of the
     screen, then the screen itself. Nothing about it slides sideways, so the growth is the
     whole of the change. */
  /* Ease into the expansion and settle gently instead of jumping at its first frame */
  transition-property: left, top, right, bottom, clip-path;
  transition-duration: var(--dur-wipe-panel);
  transition-timing-function: var(--ease-io);
}

.what-card[data-slot-sec="integrations"] .what-slot::before { left: 50%; top: 0; }
.what-card[data-slot-sec="tech"] .what-slot::before { left: 0; top: 0; }
.what-card[data-sec="integrations"] .what-detail {
  left: var(--card-x);
  bottom: 10%;
  /* One line each (the longest is 50 characters): as wide as the copy needs, up to the
     left column's edge, the same gutter to the slot as the list keeps */
  width: auto;
  max-width: calc(50% - var(--card-x) - clamp(32px, 6vw, 110px));
}
.what-card[data-sec="integrations"] .what-copy { max-width: 100%; white-space: nowrap; }
.what-card[data-sec="tech"] .what-detail { display: none; }

@media (prefers-reduced-motion: reduce) {
  .what-card .rli,
  .what-card .rv.rv-block,
  .what-card .what-slot::before,
  .what-card .tab-figure,
  .what-card .tab-figure canvas { transition: none; }
}

/* The mark moves with the visual transition, independently of the text reveal. */
.tab-figure {
  position: absolute;
  left: 50%;
  top: 69%;
  right: 0;
  bottom: 0;
  display: grid;
  place-items: center;
  overflow: hidden;
  pointer-events: none;
  clip-path: inset(0);
  transition-property: left, top, right, bottom, clip-path;
  transition-duration: var(--dur-wipe-panel); /* = .what-slot::before */
  transition-timing-function: var(--ease-io);
}

/* The panel and its mark enter through the same top-to-bottom mask as Works. */
html.js .what-card:not(.is-visual-in) .what-slot::before,
html.js .what-card:not(.is-visual-in) .tab-figure {
  clip-path: inset(0 0 100% 0);
  transition: none;
}

/* The service panel shares the page's side gutter. */
.what-card[data-slot-sec="services"] .what-slot::before,
.what-card[data-slot-sec="services"] .tab-figure {
  right: var(--card-x);
}

/* Follow the requested visual immediately; delayed text reveals do not reposition it. */
.what-card[data-slot-sec="integrations"] .tab-figure,
.what-card[data-slot-sec="tech"] .tab-figure { top: 0; }
@media (min-width: 769px) {
  /* Centre the stack mark in the upper-right area while preserving its size. */
  .what-card[data-slot-sec="tech"] .tab-figure { top: -15%; bottom: 15%; }
}

.tab-figure canvas {
  display: block;
  width: min(74%, 74vh);
  height: min(74%, 74vh);
  translate: 0 0;
  opacity: 1;
  transition: translate 1.2s var(--ease-out), opacity 0.6s ease;
}
.what-card[data-slot-sec="services"] .tab-figure canvas { opacity: 0.8; }

/* Optical alignment for the two forms whose visual mass sits above their centre. */
.tab-figure canvas[data-form="funnel"],
.tab-figure canvas[data-form="rings"] { translate: 0 12px; }

/* Glass chip -- the stack's items. --glass-mx / --glass-tilt come from main.js each frame;
   --k is set per chip in the markup so the highlights move out of phase. */
.chip {
  position: relative;
  isolation: isolate;
  overflow: hidden;
  display: inline-flex;
  align-items: center;
  height: 44px;
  padding: 0 20px;
  border-radius: var(--r-pill);
  background: var(--white); /* solid white by request */
  box-shadow: var(--lift-rest);
  font-size: var(--fs-small);
  font-weight: 400;
  line-height: 1;
  letter-spacing: 0.01em;
  color: var(--ink);
  white-space: nowrap;
}

/* A band of light sweeping across as the cursor moves (+-12% x --k of the sheet width) */
.chip::before {
  content: "";
  position: absolute;
  inset: -40% -60%;
  z-index: -1;
  background: linear-gradient(
    100deg,
    rgba(255, 255, 255, 0) 30%,
    rgba(255, 255, 255, 0.32) 50%,
    rgba(255, 255, 255, 0) 70%
  );
  transform: translateX(calc(var(--glass-mx, 0) * var(--k, 1) * 3%))
    rotate(calc(var(--glass-tilt, 0) * var(--k, 1) * 1deg));
  will-change: transform;
  pointer-events: none;
}

/* ---- 05 · Signature works — a centred title between passing case cards ----------- */

.s-works {
  --wp: 0;
  display: grid;
  padding: 0;
  background: var(--white);
  isolation: isolate;
}

/* The background sky and falling stars stay behind the title and cards. */
.works-sky,
.works-stars {
  grid-area: 1 / 1;
  align-self: start;
  position: sticky;
  top: 0;
  z-index: 0;
  height: 100svh;
  overflow: hidden;
  pointer-events: none;
  contain: paint;
}

.works-sky canvas {
  position: absolute;
  inset: -10% 0;
  display: block;
  width: 100%;
  height: 120%;
  translate: 0 var(--sky-shift, 0px);
}

.works-star {
  position: absolute;
  top: 0;
  left: 0;
  width: var(--star-size);
  height: var(--star-size);
  color: var(--blue);
  opacity: 0;
}

.works-star::before {
  content: "";
  position: absolute;
  left: 50%;
  bottom: 50%;
  /* The Hero's tail (hero-comet.js): grains 2px at the head thinning to under 1, at 0.65 of
     the head's strength. */
  width: max(1px, calc(var(--star-size) / 3));
  height: calc(var(--star-size) * 7);
  border-radius: var(--r-pill);
  background: linear-gradient(to top, rgba(0, 85, 255, 0.65), transparent);
  clip-path: polygon(0 100%, 100% 100%, 50% 0); /* tapers away from the head */
  transform: translateX(-50%);
}

.works-star::after {
  content: "";
  position: absolute;
  inset: 0;
  /* The Hero uses round point heads with a soft edge. */
  background: radial-gradient(circle closest-side, currentColor 32%, transparent 68%);
}

.works-heading {
  grid-area: 1 / 1;
  align-self: start;
  position: sticky;
  top: 0;
  z-index: 2;
  display: grid;
  place-items: center;
  height: 100svh;
  pointer-events: none;
}

/* Give the reveal mask enough depth for Archivo's long descenders. The negative margin
   keeps the title's visual footprint unchanged. */
.s-works .works-title .rl {
  padding-bottom: 0.2em;
  margin-bottom: -0.2em;
  clip-path: none;
}

.s-works .works-title {
  max-width: 94%;
  font-weight: 300;
  line-height: 1.08;
  letter-spacing: var(--track-display);
  text-align: center;
  white-space: nowrap;
  transform: scale(calc(1 - var(--wp) * 0.08));
  transform-origin: center;
}

.project-list {
  grid-area: 1 / 1;
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 24svh;
  padding: 60svh 0 20svh; /* the reviews sit closer under the last work */
}

.project {
  position: relative;
  flex: none;
  /* Square, capped so the whole tile stays on one screen. */
  width: min(48.25%, 72svh);
  height: auto;
  aspect-ratio: 1;
}

.project:nth-child(odd) { align-self: flex-end; z-index: 1; }
.project:nth-child(even) { align-self: flex-start; z-index: 3; }
.project.is-active { z-index: 4; }
.project:focus-visible { outline: 2px solid var(--ink); outline-offset: 6px; }

/* A tile with a case page: one link over its whole face, over the caption (which takes no
   pointer) -- the tab stop and the click (index.html, js/ui/case-leave.js) */
.project-link {
  position: absolute;
  inset: 0;
  z-index: 2;
}
.project-link:focus-visible { outline: 2px solid var(--ink); outline-offset: 6px; }

/* Into a case (js/ui/case-leave.js): the page steps aside the way it came in while the
   chosen tile's picture leaves its plate and the plate opens over the screen (.case-cover,
   global.css) -- the header's two items back up off the top (header-in reversed), the
   right-edge map fades, the other tiles wipe up off their plates, the chosen tile drops
   its caption shade; the titles leave up through their masks (.rv.is-out). */
/* (.project-list: one class past html.js .project.is-active .project-visual::after, below) */
html.case-leaving .project-list .project.is-launching .project-visual::after { opacity: 0; }
html.case-leaving header > :is(.logo, .menu-btn) {
  transform: translateY(calc(-1 * (var(--header-h) + env(safe-area-inset-top, 0px))));
  transition: transform 0.45s var(--ease-in);
}
html.case-leaving header > .menu-btn { transition-delay: 0.06s; }
html.case-leaving :is(.scroll-progress, .sp-line) {
  opacity: 0;
  transition: opacity var(--dur-base) ease;
}
html.case-leaving .project:not(.is-launching) .project-visual {
  clip-path: inset(0 0 100% 0);
  transition: clip-path 0.6s var(--ease-in);
}

/* Back from the case (case-leave.js, once the plate has folded into its tile): each comes
   back its own way -- the header's items down (header-in), the map in, the other tiles
   wiping down onto the page; the titles rise through their masks on their own (.rv.is-in) */
html.case-returning header > :is(.logo, .menu-btn) {
  transition: transform var(--dur-reveal) var(--ease-out);
}
html.case-returning header > .menu-btn { transition-delay: 0.15s; }
html.case-returning :is(.scroll-progress, .sp-line) { transition: opacity var(--dur-slow) ease; }
html.case-returning .project:not(.is-launching) .project-visual {
  clip-path: inset(0);
  transition: clip-path var(--dur-wipe-panel) var(--ease-io);
}

/* The tile arrives whole: once its photo is in (works-ui.js), plate and photo wipe down
   together from the top. No grey first -- a plate standing alone read as a big picture
   still loading. The plate stays under the photo: it is the ground a tile's way into its
   case stretches out (case-leave.js). */
.project-visual {
  position: absolute;
  inset: 0;
  overflow: hidden;
  background: var(--plate-grey) var(--plate-lit);
  clip-path: inset(0);
  transition: clip-path var(--dur-wipe) var(--ease-io);
}

.project-img {
  position: absolute;
  inset: -8% 0;
  width: 100%;
  height: 116%;
  object-fit: cover;
  /* Separate scroll translation from the hover scale; the extra crop covers both ends. */
  translate: 0 var(--project-image-y, 0px);
  clip-path: inset(0); /* (the way into a case wipes it up off its plate, case-leave.js) */
  transition: transform 1s var(--ease-out);
}

html.js .project:not(.is-revealed) .project-visual {
  clip-path: inset(0 0 100% 0);
  transition: none;
}

.project.is-active .project-img { transform: scale(1.04); }

.project-visual::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(180deg, rgba(0, 0, 0, 0.03) 20%, rgba(0, 0, 0, 0.76) 100%);
  transition: opacity 0.6s ease;
  pointer-events: none;
}

html.js .project-visual::after { opacity: 0; }
html.js .project.is-active .project-visual::after { opacity: 1; }

.project .project-data {
  position: absolute;
  inset: auto clamp(24px, 3.4vw, 64px) clamp(24px, 3.4vw, 64px);
  z-index: 1;
  display: flex;
  flex-direction: column;
  gap: 12px;
  color: var(--white);
  pointer-events: none;
  transition: opacity var(--dur-base) ease 0.4s;
}

html.js .project-data { opacity: 0; }
html.js .project.is-active .project-data { opacity: 1; transition-delay: 0s; }

/* The page's own overline (.eyebrow), only in white: it stands on the photo */
.project .project-eyebrow {
  color: var(--white);
}

/* Touch: the overline is split into line masks (works-ui.js), and a mask is a block --
   the slash on the overline itself would stand alone on a line above the name. It goes
   inside the mask and rises with the name (as in .what-card and the presence stats). */
.project .project-eyebrow.rv:not(.rv-block)::before { content: none; }
.project .project-eyebrow .rli::before {
  content: "/";
  margin-right: var(--slash-gap);
  font-size: var(--slash-fs);
  vertical-align: var(--slash-y);
}

.project .h5 {
  font-size: clamp(28px, 3vw, calc(48 * var(--u))); /* ceiling scales with --u past the mock instead of capping at a flat 48px */
  line-height: 1.1;
  letter-spacing: var(--track-head);
  color: var(--white);
}

.project .body-lg {
  max-width: 34ch;
  font-size: var(--fs-small);
  line-height: 1.5;
  color: var(--on-photo);
}


/* Re-arm the shared text masks before each hover or keyboard entrance. */
.project.is-resetting .rli { transform: translateY(var(--rise-mask)); transition: none; }
.project.is-resetting .rv.rv-block { opacity: 0; transform: translateY(var(--rise-block)); transition: none; }
.project .rv.rv-block { transition-delay: var(--d-in, 0s); }
.project .rv.rv-block.is-out { transition-delay: 0s; }

@media (prefers-reduced-motion: reduce) {
  .works-stars { display: none; }
  .works-sky canvas { translate: 0 0; }
  .project .project-img { translate: 0 0; }
  .s-works .works-title,
  .project .project-img { transform: none; }
  .project .project-visual,
  .project .project-visual::after,
  .project .project-data,
  .project .rv,
  .s-works .rli { transition: none; }
}

/* ---- 06 · Testimonials ------------------------------------------------------------ */

.s-testimonials {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: max(48px, calc(72 * var(--u))); /* featured quote to the divider */
  padding: max(72px, calc(120 * var(--u))) var(--page-x) max(96px, calc(160 * var(--u))) var(--page-x-lg);
  overflow: clip;
}

.quote-featured {
  --quote-indent: max(40px, calc(160 * var(--u)));
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 32px;
  /* No room held back for the progress track: the inline portrait doesn't spill into that
     strip, so no empty band reads as a gap on the right */
  margin: 0;
}

/* Native inline text keeps the first-line inset; the portrait is one more inline piece. */
.quote-featured .h3 { text-indent: var(--quote-indent); }
.quote-word { white-space: nowrap; }
.quote-char { opacity: 0; }
.quote-char.is-typed { opacity: 1; }

.quote-featured-body { display: flow-root; font-size: var(--fs-h3); }
/* The portrait sits in the line of text like a word: a 1.3em square hung a little under
   the baseline, which fits inside the .h3 line box (line-height 1.5) so no line grows. */
.quote-portrait {
  display: inline-block;
  width: 1.3em;
  aspect-ratio: 1;
  vertical-align: -0.3em;
  margin: 0 0.08em;
}

.quote-portrait-visual {
  position: relative;
  display: block;
  width: 100%;
  aspect-ratio: 1;
  overflow: hidden;
  background: var(--plate-grey) var(--plate-lit);
  clip-path: inset(0);
  transition: clip-path var(--dur-wipe-inline) var(--ease-io); /* = a case's typed mark (case.css .cb-typed) */
}

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

/* Photo and plate wipe down together once the typing has reached them (featured-review.js):
   nothing stands in the line before, no grey square first */
html.js .quote-portrait:not(.is-in) .quote-portrait-visual {
  clip-path: inset(0 0 100% 0);
}

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

/* The typewriter caret follows the last glyph, centred on the active text line. */
.quote-caret {
  position: absolute;
  left: 0;
  top: 0;
  width: max(2px, 0.055em);
  height: 0.9em;
  background: var(--ink);
  font-size: var(--fs-h3); /* = .h3 */
  transform: translate(var(--cx, 0px), var(--cy, 0px)) translateY(-50%);
  opacity: 0;
  z-index: 1;
  pointer-events: none;
  transition: opacity var(--dur-fast) ease;
  will-change: transform;
}

.quote-caret.is-on {
  opacity: 1;
  animation: caretBlink 0.9s steps(1, end) infinite;
}

@keyframes caretBlink {
  0%, 45% { opacity: 1; }
  50%, 100% { opacity: 0; }
}

@media (prefers-reduced-motion: reduce) {
  .quote-caret { display: none; }
}
.quote-row {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: max(40px, calc(80 * var(--u)));
  padding-top: max(48px, calc(64 * var(--u))); /* divider to the small reviews */
  border-top: 1px solid var(--hairline);
}

.quote-card {
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 24px;
  margin: 0;
}

.quote-card .quote-client { margin-top: auto; }

.quote-card .quote-mark {
  width: 16px;
  height: 12px;
  opacity: 0;
  transform: translateY(8px);
  transition: opacity var(--dur-slow) ease, transform var(--dur-slow) ease;
}

.quote-card.is-in .quote-mark {
  opacity: 0.45;
  transform: none;
}

/* The signature follows the lines: a four-line card's last line starts rising at 0.3s and
   settles by about 1s, so the name comes in as it lands */
.quote-card .quote-client .rv-block.is-in {
  transition-delay: 0.6s;
}

.quote-client {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 8px 12px;
}

.quote-client .label { font-size: var(--fs-micro); line-height: 1.5; }
.quote-client .caption-xl { font-size: var(--fs-small); line-height: 1.5; color: var(--muted); }
.quote-client [aria-hidden="true"] { color: var(--ink); }

/* Featured quote: it types through in one go once in view (ui.js), so there is no scroll
   run to hold; the stage is just its content */
.quote-stage {
  height: auto;
}

.quote-sticky {
  position: sticky;
  top: 16vh;
}

/* ---- 07 · Team --------------------------------------------------------------------- */

.s-team {
  --team-open: 1;
  --team-cut: calc((1 - var(--team-open)) * 100%);
  --team-inset: calc((1 - var(--team-open)) * var(--page-x));
  /* The band at the top of the viewport kept clear of the board: in the dunes story the
     inverted terrain hangs there (presence-story.js CEILING_LIFT), everything else sits under it */
  --team-sky: 0px;
  --team-floor: 5vh;
  min-height: 100vh;
  isolation: isolate;
  overflow: clip;
}

html.js .s-team { --team-open: 0; }

/* Reverse the closing window: the top rises and both side insets shrink to zero together.
   The white surround stays inside this section, leaving the whole width open at the end. */
.s-team::before {
  content: "";
  position: absolute;
  top: var(--team-cut);
  right: var(--team-inset);
  bottom: 0;
  left: var(--team-inset);
  z-index: 0;
  box-shadow: 0 0 0 100vmax var(--white);
  pointer-events: none;
}

.team-panel {
  position: relative;
  z-index: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  /* One viewport, contents centred in what is left under the sky band: the team reads as a
     whole screen, not something scrolled through */
  box-sizing: border-box;
  padding: var(--team-sky) 0 var(--team-floor);
  min-height: 100vh;
  clip-path: inset(var(--team-cut) var(--team-inset) 0);
}

/* While the section is coming up from below, the board is pinned to the viewport and only
   the curtain moves: ui.js sets .is-fixing for as long as the section's top is inside the
   viewport, the panel and the white surround go fixed, and the curtain's edge (--team-cut)
   follows the section's top. At top = 0 the pinned and the in-flow positions coincide, so
   the class can come off with no jump, and the board then scrolls away as any section. */
.s-team.is-fixing .team-panel {
  position: fixed;
  inset: 0;
  min-height: 0;
  height: 100vh;
}

.s-team.is-fixing::before {
  position: fixed;
}

/* The why-us close in reverse: the board no longer stands still while the curtain's edge
   slices down through the title and the copy -- it rises into place as the window opens
   (30vh below at closed, home when fully open), so what the edge uncovers is already on
   its way in. */
.s-team .team-stage {
  translate: 0 calc((1 - var(--team-open)) * 30vh);
}

@media (prefers-reduced-motion: reduce) {
  html.js .s-team { --team-open: 1; }
}

/* The board (the second "Team" layout). The content column is a 988 x 580 box: x as a
   share of its width, y as a share of its height, so every piece keeps its place at any
   page width. The eyebrow sits top-left, the title top-right (ragged left), the four
   portraits step down from the left margin line to the right one, and the copy (left) and
   the note (right) close the bottom edge. The two text pieces on the left stand a little
   in from the portrait's edge, as the layout has them. */
.team-stage {
  --team-text-inset: 1.85%;
  position: relative;
  align-self: center;
  aspect-ratio: 988 / 580;
  /* Full width between the gutters, unless that is taller than the room under the sky
     band: then the board shrinks to fit (a 1920 x 1080 screen already cut the title off) */
  width: min(100% - 2 * var(--page-x), (100vh - var(--team-sky) - var(--team-floor)) * 988 / 580);
  margin: 0;
}

.team-head {
  position: absolute;
  inset: 0 0 auto var(--team-text-inset);
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
}

.team-title {
  margin: -0.12em 0 0; /* the cap height sits on the eyebrow's top */
  font-size: max(40px, calc(62 * var(--u)));
  font-weight: 300;
  line-height: 1.05;
  letter-spacing: -0.025em; /* not --track-display: its 4.6em column breaks the line by this exact width */
  text-align: right;
  max-width: 7.2em; /* "The people / behind it" */
}

.team-lead {
  position: absolute;
  bottom: 0;
  left: var(--team-text-inset);
  width: 36%;
  margin: 0;
  font-size: var(--fs-small);
  font-weight: 300;
  line-height: 1.6;
  color: var(--text);
}


/* A portrait: plate and photo wipe down together once the photo is in -- the same reveal
   as the works' tiles (.project-visual), with the same curve and length. Once the board is
   scrolling away, each drifts up at its own depth (--team-p from ui.js, 0..1 over the
   board's exit): a light parallax between them, none while the board is pinned. */
.team-photo {
  --depth: 0;
  position: absolute;
  margin: 0;
  background: var(--plate-grey) var(--plate-lit);
  overflow: hidden;
  translate: 0 calc(var(--team-p, 0) * var(--depth) * -64px);
  will-change: translate;
}

.team-photo-ivan      { --depth: 0.35; top: 7.1%;  left: 0;     width: 24%;   aspect-ratio: 155 / 148; }
.team-photo-stanislav { --depth: 0.7;  top: 28.9%; left: 26.7%; width: 20.4%; aspect-ratio: 132 / 128; }
.team-photo-oleksandr { --depth: 1;    top: 50.8%; left: 51.8%; width: 20.1%; aspect-ratio: 130 / 128; }
.team-photo-stanislav-ry { --depth: 0.5; top: 19.2%; left: 76.5%; right: 0;   aspect-ratio: 150 / 139; }
.team-photo-keindra   { --depth: 0.6;  top: 53%;   left: 4%;    width: 20.4%; aspect-ratio: 136 / 165; }
.team-photo-stanislav, .team-photo-stanislav-ry { container-type: inline-size; }

.team-img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: 50% 14%;
}

.team-placeholder {
  display: grid;
  place-items: center;
  background: var(--plate-grey) var(--plate-lit);
  color: var(--muted);
  font-size: 36cqw;
  font-weight: 300;
  letter-spacing: -0.06em;
}

/* (not in the flight: its plates are drawn in by the 3D, and their captions hang below them) */
html.js .s-team:not(.team-flight) .team-photo {
  clip-path: inset(0);
  transition: clip-path var(--dur-wipe) var(--ease-io);
}
html.js .s-team:not(.team-flight) .team-photo:not(.is-revealed) {
  clip-path: inset(0 0 100% 0);
  transition: none;
}

@media (prefers-reduced-motion: reduce) {
  html.js .s-team:not(.team-flight) .team-photo { transition: none; }
}

/* ---- 08 · Global presence & numbers ----------------------------------------------- */

/* A full screen plus the sheet's corner radius; the dunes story gives the section its own
   run below (.has-presence-story) */
.s-presence {
  position: relative;
  min-height: calc(100vh + var(--sheet-r));
}

.globe {
  position: absolute;
  top: calc(50% + 24px); /* the globe sits a little lower than dead-centre */
  left: 50%;
  width: max(420px, calc(666 * var(--u)));
  height: max(452px, calc(718 * var(--u)));
  transform: translate(-50%, -50%);
  pointer-events: none;
}

.stat {
  position: absolute;
  display: flex;
  flex-direction: column;
  gap: 12px;
}
.stat .stat-head { gap: 8px; }
.stat .body-lg { width: 100%; max-width: max(303px, calc(303 * var(--u))); } /* the measure grows with the text */
/* Keep the decorative slash inside the same rising mask as its label. */
.s-presence .stat .eyebrow.rv::before { content: none; }
.s-presence .stat .eyebrow .rli::before {
  content: '/';
  margin-right: var(--slash-gap);
  font-size: var(--slash-fs);
  vertical-align: var(--slash-y);
}

/* Keyed off the globe's centre (50% + 24px, see .globe), not the section's top, so the
   pairs stay centred on the sphere whatever the section's height. The pair sits a little
   above the sphere's centre line. */
.stat-tl { top: calc(50% + 24px - 296px); left: var(--page-x); } /* left column at the hero gutter (--page-x), not --page-x-lg */
.stat-bl { top: calc(50% + 24px + 52px); left: var(--page-x); }
.stat-tr { top: calc(50% + 24px - 296px); right: var(--page-x-lg); align-items: flex-end; text-align: right; }
.stat-br { top: calc(50% + 24px + 52px); right: var(--page-x-lg); align-items: flex-end; text-align: right; }
.stat-tr .body-lg, .stat-br .body-lg { max-width: max(231px, calc(231 * var(--u))); }

/* The numbers leave up through their masks on desktop, on the way on to the close
   (presence-story.js STATS_OUT_AT -> .is-out): the shared line-exit (global.css), which
   brings them back down from above as the page turns back up. Phones swap them in
   css/mobile.css html.contact-in. */

/* The contact button's wrapper (.s-contact): a block of its own, since the button's own
   transform belongs to the tilt. */
.cta-act { display: block; }

/* "start a project": the meter 2px closer to the label than the pill's gap */
.cta-act .btn > span:last-child { margin-left: -2px; }

/* "Start a project": leans toward the cursor wherever it moves on the page (ui.js writes
   the transform with its own perspective; the hover lift is folded in there too) */
.btn-tilt {
  transition: transform 0.35s ease, box-shadow var(--dur-fast) ease;
  will-change: transform;
}

/* Dunes story · the ceiling behind Core Team continues into the planet. The native-scroll
   stage is enabled only after its controller is ready; failed WebGL keeps a compact,
   readable layout: the four stats in a two-column grid. */
html[data-scene="dunes"] .presence-sticky { display: contents; }
/* The hanging terrain takes the top of the screen over Core Team; the board sits under it */
html[data-scene="dunes"] .s-team { --team-sky: 32vh; }

html[data-scene="dunes"].no-globe:not(.has-presence-story) .s-presence {
  min-height: 0;
  padding: 96px var(--page-x) 80px;
}
html[data-scene="dunes"].no-globe:not(.has-presence-story) .presence-sticky {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 48px 40px;
}
html[data-scene="dunes"].no-globe:not(.has-presence-story) .s-presence .globe { display: none; }
html[data-scene="dunes"].no-globe:not(.has-presence-story) .s-presence .stat {
  position: static;
  align-items: flex-start;
  text-align: left;
}

html[data-scene="dunes"].has-presence-story .s-presence {
  height: 420svh;
  min-height: 0;
  display: block;
  padding: 0;
  overflow: visible;
}
html[data-scene="dunes"].has-presence-story .presence-sticky {
  display: block;
  position: sticky;
  top: 0;
  width: 100%;
  height: 100svh;
  overflow: clip;
}
html[data-scene="dunes"].has-presence-story .s-presence .globe {
  display: block;
  position: absolute;
  top: 50%;
  left: 50%;
  width: min(54vw, 70svh, 760px);
  height: min(54vw, 70svh, 760px);
  transform: translate(-50%, -50%);
}
html[data-scene="dunes"].has-presence-story .s-presence .stat {
  position: absolute;
  width: clamp(160px, 19vw, 280px); /* fits "scalable digital solutions." on one line */
  visibility: hidden;
  opacity: 1;
  pointer-events: none;
}
html[data-scene="dunes"].has-presence-story .s-presence.presence-globe-ready .stat {
  visibility: visible;
}

/* The office callouts: each <li> is moved to its office's screen point (presence-ui.js,
   translate3d on the element). A small solid dot on the point, breathing a soft halo,
   and the city in a hairline pill beside it -- the page's pill language (.btn, the talk
   pill), not a map's leader line. The pill sits to the right of the sphere on the right
   half, to the left on the left (data-side). Opacity is the projection's visibility:
   they fade as the globe turns them away, and they are gone while it becomes the mark. */
.presence-offices {
  position: absolute;
  inset: 0;
  margin: 0;
  padding: 0;
  list-style: none;
  pointer-events: none;
  z-index: 2;
}
html:not(.has-presence-story) .presence-offices { display: none; }
.office-callout {
  position: absolute;
  top: 0;
  left: 0;
  opacity: 0;
  will-change: transform, opacity;
}
.office-dot {
  position: absolute;
  left: 0;
  top: 0;
  width: 7px;
  height: 7px;
  translate: -50% -50%;
  border-radius: var(--r-round);
  background: var(--blue);
  /* a white collar so the dot still reads where the sphere's own grains are densest */
  box-shadow: 0 0 0 2px var(--white);
}
.office-dot::after {
  /* the halo: a ring that swells out and fades, twice a beat apart */
  content: "";
  position: absolute;
  inset: -3px;
  border-radius: var(--r-round);
  border: 1px solid var(--blue);
  opacity: 0;
  animation: office-halo 2.8s ease-out infinite;
}
@keyframes office-halo {
  0% { transform: scale(0.6); opacity: 0.7; }
  70%, 100% { transform: scale(2.6); opacity: 0; }
}
.office-name {
  position: absolute;
  top: 0;
  left: 12px;
  translate: 0 -50%;
  padding: 6px 12px;
  border-radius: var(--r-pill);
  /* Solid, with a soft lift: a translucent pill and a lighter label would disappear into
     the globe's own blue grains */
  background: var(--white);
  box-shadow: 0 2px 10px rgba(20, 20, 20, 0.12);
  font-size: var(--fs-cap);
  font-weight: 500;
  line-height: 1;
  letter-spacing: var(--track-caps-tight);
  text-transform: uppercase;
  color: var(--ink);
  white-space: nowrap;
}
.office-callout[data-side="left"] .office-name { left: auto; right: 12px; }
@media (max-width: 768px) {
  .office-name { font-size: 12px; padding: 4px 9px; }
}
@media (prefers-reduced-motion: reduce) {
  .office-dot::after { animation: none; }
}
/* The right pair's copy takes the whole column too (the 231px cap would break "Proven
   delivery." over the line) */
html[data-scene="dunes"].has-presence-story .s-presence .stat-tr .body-lg,
html[data-scene="dunes"].has-presence-story .s-presence .stat-br .body-lg { max-width: none; }
html[data-scene="dunes"].has-presence-story .s-presence .stat-tl,
html[data-scene="dunes"].has-presence-story .s-presence .stat-tr { top: calc(50% - min(28svh, 250px)); }
html[data-scene="dunes"].has-presence-story .s-presence .stat-bl,
html[data-scene="dunes"].has-presence-story .s-presence .stat-br { top: calc(50% + min(13svh, 110px)); }
/* Wider margins than the hero gutter: the right column would otherwise run under the
   fixed progress track and its labels (right edge 27px, labels to ~110px in) */
html[data-scene="dunes"].has-presence-story .s-presence .stat-tl,
html[data-scene="dunes"].has-presence-story .s-presence .stat-bl { left: var(--page-x-lg); }
html[data-scene="dunes"].has-presence-story .s-presence .stat-tr,
html[data-scene="dunes"].has-presence-story .s-presence .stat-br { right: calc(var(--page-x-lg) + 32px); }

/* Past the mock the numbers grow x1.69; if the column (280px) and the offsets (250 / 110px)
   stood still, "4 Countries" would break over two lines and the taller blocks would hang
   off the centre. Here the column grows with the numbers, the copy under them at the
   drawer's slower rate (--ui), and the pairs are held by the gap between them -- the top
   one by its lower edge, the bottom one by its top -- so the table stays centred however
   tall the blocks get. (74 / 110 = where those edges sit at 1512.) */
@media (min-width: 1513px) {
  html[data-scene="dunes"].has-presence-story .s-presence .stat { width: calc(280 * var(--u)); }
  html[data-scene="dunes"].has-presence-story .s-presence .stat .h4 { white-space: nowrap; }
  html[data-scene="dunes"].has-presence-story .s-presence .stat .body-lg { font-size: calc(18 * var(--ui)); }
  /* The copy's measure follows its own type (--ui), not the column (--u): with the column
     alone it had room for "Trusted partnerships. Consistent results." on one line at 2560,
     and the four blocks stopped reading as one table of two-line captions. 280 = the
     column at 1512, where every caption already breaks over two lines. */
  html[data-scene="dunes"].has-presence-story .s-presence .stat .body-lg { max-width: calc(280 * var(--ui)); }
  html[data-scene="dunes"].has-presence-story .s-presence .stat-tl,
  html[data-scene="dunes"].has-presence-story .s-presence .stat-tr { top: auto; bottom: calc(50% + 74 * var(--u)); }
  html[data-scene="dunes"].has-presence-story .s-presence .stat-bl,
  html[data-scene="dunes"].has-presence-story .s-presence .stat-br { top: calc(50% + 110 * var(--u)); }
}

@media (max-width: 768px) {
  html[data-scene="dunes"].no-globe:not(.has-presence-story) .presence-sticky { gap: 36px 24px; }
  html[data-scene="dunes"].has-presence-story .s-presence .globe {
    top: 38%;
    width: min(72vw, 42svh);
    height: min(72vw, 42svh);
  }
  html[data-scene="dunes"].has-presence-story .s-presence .stat {
    top: auto;
    width: calc(50% - 36px);
    gap: 6px;
    align-items: flex-start;
    text-align: left;
  }
  html[data-scene="dunes"].has-presence-story .s-presence .stat-tl,
  html[data-scene="dunes"].has-presence-story .s-presence .stat-tr { bottom: 26svh; }
  html[data-scene="dunes"].has-presence-story .s-presence .stat-bl,
  html[data-scene="dunes"].has-presence-story .s-presence .stat-br { bottom: 7svh; }
  html[data-scene="dunes"].has-presence-story .s-presence .stat-head { gap: 4px; }
  html[data-scene="dunes"].has-presence-story .s-presence .stat .eyebrow { font-size: 12px; }
  html[data-scene="dunes"].has-presence-story .s-presence .stat .h4 { font-size: 25px; }
  html[data-scene="dunes"].has-presence-story .s-presence .stat .body-lg { font-size: 12px; line-height: 1.5; }
}
@media (prefers-reduced-motion: reduce) {
  html[data-scene="dunes"].has-presence-story .s-presence { height: 240svh; }
  html[data-scene="dunes"].has-presence-story .s-presence .rv,
  html[data-scene="dunes"].has-presence-story .s-presence .rli {
    animation: none !important;
    transition: none !important;
  }
}

/* ---- 10 · Footer ------------------------------------------------------------------- */

.s-footer {
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  gap: 48px;
  min-height: 100svh;
  padding: clamp(120px, 16svh, 192px) 24px 0; /* more air under the header; narrow gutters so the wordmark runs edge to edge */
  background: var(--footer-bg);
  color: var(--text);
  border-radius: 0;
}

.s-footer .body-lg,
.s-footer .footer-bottom span { color: var(--text); }
.s-footer .eyebrow { color: var(--quiet); }

.footer-top {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 48px;
  padding: 0 60px 0 24px; /* the text row sits in from the wordmark; wider gutter on the right */
}

.footer-desc {
  font-size: var(--fs-small-lg);
  max-width: 38ch; /* narrow column, ~4 short lines, grows with the type */
}

.footer-cols {
  display: flex;
  gap: max(48px, calc(120 * var(--u)));
}

.footer-col { gap: 24px; }
.footer-col ul { list-style: none; display: flex; flex-direction: column; gap: 16px; }
/* The sections' list on wider screens is set as two columns, filled top to bottom
   (2 + 2), so it stands no taller than the contacts beside it. Phones keep the one
   accordion list (mobile.css). */
@media (min-width: 768px) {
  .footer-sections {
    display: grid;
    grid-template-rows: repeat(2, auto);
    grid-auto-flow: column;
    column-gap: max(32px, calc(48 * var(--u)));
  }
}
/* mailto/visited would otherwise go blue/purple */
.footer-col a {
  position: relative;
  display: inline-block;
  text-decoration: none;
  /* Hover simply brings the link up out of the plane; the rest of the column holds back,
     so the one under the cursor is the only bright thing. The fade rides on the COLOUR,
     not on opacity: the line-reveal sets opacity:1 on .rv-block.is-in and would win. */
  color: var(--text);
  transition: color 0.32s ease;
}

/* Footer links wait for the actual clipped edge (ui.js) and then rise in sequence.
   Keep the hover colour independent, so its response never inherits the entrance delay. */
.footer-col a.rv.rv-block {
  transition:
    opacity var(--dur-slow) ease var(--footer-nav-delay, 0s),
    transform var(--dur-slow) ease var(--footer-nav-delay, 0s),
    color 0.32s ease;
}

.footer-col a:hover {
  color: var(--ink);
}

/* The address copies itself (ui.js), the menu's way: both lines share one mask, the
   address leaves up as the note comes from below, and they trade back after. A grid
   stack, so the mask is as wide as the longer of the two and nothing around it moves. */
.footer-swap {
  display: grid;
  overflow: hidden;
  padding-bottom: 0.12em;
  margin-bottom: -0.12em;
}
.footer-swap > span {
  grid-area: 1 / 1;
  white-space: nowrap;
  transition: transform var(--dur-swap-out) var(--ease-in); /* whichever is leaving goes quickly and alone */
}
.footer-swap-note { transform: translateY(var(--rise-mask)); color: var(--ink); }
.footer-email.is-copied .footer-swap-text { transform: translateY(var(--rise-mask-out)); }
.footer-email.is-copied .footer-swap-note,
.footer-email.has-copied:not(.is-copied) .footer-swap-text {
  transition: transform var(--dur-swap-in) var(--ease-out) var(--dur-swap-gap); /* ...and the other only starts once the line is clear */
}
.footer-email.is-copied .footer-swap-note { transform: translateY(0); }

.footer-mark { gap: 0; } /* the rule lands exactly on the bottom row's top edge */
/* Full-screen footer: the wordmark's box takes all the height left under the columns and
   the line sits at its bottom, on the rule above the bottom row. */
.footer-mark, .footer-mark > .stack { flex: 1 1 auto; min-height: 0; }
.footer-wordmark {
  display: flex;
  align-items: flex-end;
  justify-content: center;
  flex: 1 1 auto;
  min-height: 0;
  white-space: nowrap;
  line-height: 1.2;
  /* Archivo descenders reach ~0.2em below the 1.2 line box; 0.05em keeps the tails of g
     and y just short of the rule */
  padding-bottom: 0.05em;
  /* Tracking lives on the letter boxes (.ch margin), not on letter-spacing: spacing shrinks
     each letter's box by the same amount and the mask / background-clip would then cut the
     glyph's right edge (the g would lose its bowl) */
  letter-spacing: 0;
  /* The gradient fill must sit on the moving letters, not the parent: background-clip on
     the parent paints glyphs at their layout position and ignores the transform (see
     .display.rv in global.css) */
  background-image: none;
}

/* The letters rise from under their masks one by one, left to right (footer-letters.js
   builds the spans and adds .is-in / .is-done) */
.footer-wordmark .ch {
  display: inline-block;
  overflow: hidden;
  vertical-align: bottom;
  margin-right: -0.035em; /* = .display-1 tracking, as box overlap (see .footer-wordmark) */
  /* Keep descenders inside the mask without adding visible space. 0.12em would cut the
     tails of g and y clean off at this size -- Archivo reaches ~0.22em below the 1.2 line
     box. */
  padding-bottom: 0.26em;
  margin-bottom: -0.26em;
}

.footer-wordmark .ch:last-child { margin-right: 0; }

.footer-wordmark .chi {
  display: inline-block;
  letter-spacing: 0;
  color: var(--ink); /* flat ink, no display gradient */
  transform: translateY(var(--rise-mask));
  /* ten letters on a 0.05s stagger would read as a long wait at --dur-reveal (0.7s), so
     this uses --dur-slow (0.5s) */
  transition: transform var(--dur-slow) var(--ease-out) var(--d, 0s);
  will-change: transform;
}

.footer-wordmark.is-in .chi {
  transform: none;
}

/* Once the letters have finished rising the mask has no job left, and any mask deep enough
   for the rise is a mask that can clip a descender at some font size. So it is dropped
   entirely (footer-letters.js adds .is-done when the last letter lands) and the tails of g
   and y are simply drawn. */
.footer-wordmark.is-done .ch {
  overflow: visible;
  padding-bottom: 0;
  margin-bottom: 0;
}

@media (prefers-reduced-motion: reduce) {
  .footer-wordmark .chi { transition: none; }
}
.footer-rule { height: 0.5px; background: var(--hairline); }

/* Positioned above the wordmark (which has a transform, so it paints in the positioned
   layer and would otherwise cover this). No background of its own. Equal padding top and
   bottom: the row IS the footer's bottom air. */
.footer-bottom {
  position: relative;
  z-index: 1;
  padding: 16px 0;
  display: flex;
  justify-content: space-between;
  font-size: var(--fs-small);
  font-weight: 300;
  line-height: 1.6;
  letter-spacing: var(--track-body);
  color: var(--text);
}

/* Contact follows the tech-stack composition: copy left, particle mark right. */
.s-contact { position: relative; height: 145svh; padding: 0; }
/* The landing's close on desktop: one screen, no hold -- it arrives by a glide and rests
   (contact-pull.js), and the next gesture opens the footer at once (145 -> 100svh: the
   pinned 45svh before the curtain read as a slow footer). The case's close (.cb-cta, the
   same class) and phones keep theirs. */
@media (min-width: 769px) { #contact { height: 100svh; } }
.contact-sticky {
  position: sticky;
  top: 0;
  height: 100svh;
  display: flex;
  align-items: flex-end;
  padding: 0 var(--page-x) 10svh;
}
.contact-overline {
  position: absolute;
  top: clamp(112px, 18svh, 186px);
  left: var(--page-x);
  font-size: clamp(9px, 0.76vw, calc(12 * var(--ui-label)));
}
.contact-copy {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  width: 62%;
  gap: 56px; /* more air between the heading and the button */
  text-align: left;
}
.contact-copy .cta-head { width: 100%; align-items: flex-start; text-align: left; gap: 28px; }
.contact-copy .display {
  width: 100%;
  font-size: clamp(54px, 6.25vw, calc(106 * var(--u))); /* ceiling scales with --u past the mock instead of capping at a flat 106px */
  line-height: 1.1;
  text-wrap: balance;
}
.contact-copy .display.rv .rl {
  padding: 0 .06em .24em;
  margin: 0 -.06em -.24em;
}
.contact-copy .display.rv .rli { padding-bottom: .24em; margin-bottom: -.24em; }

/* Desktop: the close's lines rise once the stats have gone (presence-ui.js html.contact-in),
   not while they leave; turning back up drops them under their masks again (line-drop), the
   button with them. Phones: css/mobile.css. */
@media (min-width: 768px) {
  /* The close shares presence's pinned screen, as on phones (css/mobile.css) and as a
     service's close: pulled up by a screen and the stretch from the stats on (420svh run:
     its last 28%, ~90svh; 240svh under 769px: ~40svh -- a screen covers both), it pins over
     the stats with no ground of its own, its lines under their masks and the pointer passing
     through to the globe, and both let go together at presence's end, where the glide rests
     (team-pull.js closeStop). The stats leave where they stand and the close rises in their
     screen, one after the other (presence-ui.js). */
  html.has-presence-story #contact {
    margin-top: -200svh;
    height: 200svh;
    pointer-events: none;
  }
  html.contact-in.has-presence-story #contact .contact-sticky { pointer-events: auto; }
  html.has-presence-story:not(.contact-in) .s-contact .rv.is-in .rli {
    transform: translateY(var(--rise-mask));
    transition: transform var(--dur-drop) var(--ease-in) var(--d-out, 0s);
  }
  html.has-presence-story:not(.contact-in) .s-contact .rv.rv-block.is-in {
    opacity: 0;
    transform: translateY(var(--rise-block));
    transition-delay: 0s;
  }
}

/* ---- Mobile — collapse to single column, drop absolute compositions --------------- */

/* The services screen at a tablet's 768px: the plate and its mark under the list, across
   the card (!important: over the desktop's own places above). Not on phones -- their
   accordion (mobile-what.css) places them itself, with nothing to fight. */
@media (min-width: 768px) and (max-width: 768px) {
  .what-slot::before,
  .what-card .tab-figure { left: 0 !important; right: 0 !important; top: 74% !important; }
  .what-card[data-slot-sec="services"] .what-slot::before,
  .what-card[data-slot-sec="services"] .tab-figure {
    left: var(--card-x) !important;
    right: var(--card-x) !important;
  }
  .what-detail { left: 20px !important; bottom: calc(40% - 3em) !important; }
  .what-go { top: calc(74% - 24px); }
}

@media (max-width: 768px) {
  .why-head { padding: 0 24px; bottom: calc(90px + 12vh); }
  .waveform { height: 90px; bottom: 8vh; }

  .s-what { padding: 48px 24px 96px; }
  .s-what .display { white-space: normal; }
  .what-sticky { padding: 16px; }
  .what-list { width: 62%; }
  .what-card[data-sec="tech"] .what-list { width: calc(100% - var(--card-x) * 2); }
  .what-chips {
    overflow-x: auto;
    padding-block: 20px;
    margin-block: -20px;
  }

  .s-works .works-title { font-size: clamp(58px, 12vw, 90px); }
  .project-list { padding: 66svh 24px 24svh; gap: 28svh; }
  .project { width: 100%; min-height: 280px; }
  .project .project-data { inset: auto 24px 24px; }

  .s-testimonials { padding: 72px 24px 96px; gap: 56px; }
  .quote-featured { --quote-indent: 0px; gap: 24px; }
  .quote-row { grid-template-columns: 1fr; gap: 40px; }
  .quote-sticky { position: static; }

  /* Mobile: the board becomes a column -- title, copy, then the portraits two abreast */
  .team-stage { aspect-ratio: auto; margin: 0 24px; width: auto; display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
  .team-head { position: static; grid-column: 1 / -1; flex-direction: column; gap: 12px; }
  .team-title { text-align: left; margin-top: 0; }
  .team-lead { position: static; grid-column: 1 / -1; width: auto; }
  .team-photo { position: static; width: auto; translate: none; }

  .s-presence { min-height: 0; padding: 48px 24px; display: flex; flex-direction: column; gap: 40px; }
  .globe { position: static; transform: none; width: 100%; height: auto; }
  .stat { position: static; align-items: flex-start; text-align: left; }

  .footer-top { flex-direction: column; padding: 0; }
  .footer-cols { flex-wrap: wrap; }
  .footer-bottom { flex-direction: column; gap: 8px; }

  .contact-sticky { padding-bottom: 13svh; }
  .contact-copy { width: 100%; gap: 24px; }
  .contact-copy .cta-head { gap: 20px; }
  .contact-overline { top: var(--overline-top-narrow); font-size: 12px; }
  .contact-copy .display { max-width: 10em; font-size: clamp(36px, 10.7vw, 48px); }

  .s-footer { gap: 24px; padding-top: 108px; }
  .footer-top { gap: 24px; }
  .footer-desc { font-size: 14px; }
  .footer-cols { width: 100%; gap: 24px 32px; }
  .footer-col { gap: 12px; }
  .footer-col ul { gap: 10px; }
  .footer-col a { font-size: var(--fs-small); }
  .footer-bottom { font-size: 12px; }
}
