/* Page sections below the hero — layout + structure per the Figma homepage frame.
   Numbers are the mock's (1512 ref), scaled through --u where they must stay
   proportional. Motion / background effects come later; this is the skeleton. */

/* ---- 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 (6 -> 5) */
}

.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;
}

/* Timecode inside the player pill, between the name and the controls */
/* 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 lower: 16px -> 64px -> 40px -> 48px -> 44px -> 40px -> 66px */
  bottom: calc(200px + 9vh);
  left: 0;
  right: 0;
  justify-content: center;
  gap: 24px; /* (32 -> 24) 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(--bar);
  -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(--bar) 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(110%);
  transition: transform 0.9s var(--ease-out-soft);
  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(-110%);
  transition-duration: 0.45s;
  transition-delay: 0s;
}

/* Scrolling back: the phrase that returns (from above) drops in as one block, fast, and
   the one that leaves (downward) goes just as fast -- no staggered mask on the way back */
.s-why.is-back .why-line .rli {
  transition-duration: var(--dur-base);
  transition-delay: 0s;
}

/* The closing phrase is set larger than the rest: the section opens on a statement and
   ends on one (this was the greeting's size, and kept it when the greeting went) */
.why-line-p.why-line-last {
  font-size: clamp(40px, min(calc(110 * var(--u)), calc(92 * var(--uh))), 92px);
  max-width: clamp(640px, calc(1200 * var(--u)), 1200px);
}

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

/* Full-width track. Bars are 1.5px wide, 4px apart, rounded; count and heights are
   generated in ui.js to fill the width. .is-played bars are ink, the rest muted. */
/* Track box is centered 5vh above the edge. While pinned every bar is dropped so its
   foot sits on the screen edge (straight bottom line, only the tops move); on the last
   phrase --rel lifts them back into the symmetric, centered waveform. */
.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; /* = MAX_H in ui.js: the band's full width; the pill sits on its centre line */
}

/* The track itself: the ribbon canvas (ribbon.js, built by ui.js) fills the sticky
   viewport under the text; the band stands on the screen's bottom edge until the last
   phrase, then rises to the player's line */
.wave-ribbon {
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
}

/* 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: 50%;
  transition: width .45s ease, height .45s ease, border-radius .45s ease, opacity var(--dur-fast) ease;
}
.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; }
.player-play, .cursor-visual { isolation: isolate; }
.player-disc {
  position: absolute;
  inset: 0;
  border-radius: 50%;
  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: 50%;
  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: 50%; 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: 13px;
  line-height: 1;
}
.player-action-icon { width: 0; height: 0; border-block: 5.5px solid transparent; border-left: 8px solid currentColor; margin-left: 1px; } /* (4/6 -> 5.5/8: a shade bigger; the nudge keeps its 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: 11px;
  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-play, .cursor-visual { --ring-out: 10px; } /* the ring stands this far outside the disc, air between (the cursor carries a copy of the disc) */
.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; }
.player-cta, .cursor-talk {
  justify-content: flex-start;
  gap: 10px;
  padding: 0 20px 0 6px; /* (right 26 -> 20: the 6px moved to the avatar-to-dot air) */
  background: var(--white);
  color: var(--ink);
  font-size: 13px;
  font-weight: 600;
  line-height: 1;
  letter-spacing: .12em;
  text-transform: uppercase;
  text-decoration: none;
  white-space: nowrap;
}
.player-talk-avatar { flex: 0 0 52px; width: 52px; height: 52px; margin-right: 10px; /* (4 -> 10) */ border-radius: 50%; 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; /* (-2 -> 0) with the CTA's 12px gap that is 12px to the label */
  border-radius: 50%;
  background: #34c759;
  flex: 0 0 8px;
}

.online-dot::before {
  content: "";
  position: absolute;
  inset: -3px;
  border-radius: 50%;
  background: rgba(52, 199, 89, 0.45);
  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, six 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: 12; /* five services, six integrations, the stack */
  --step-run: 35vh; /* half a gesture's former travel per service, integration or stack step */
  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 (18 -> 14vh: the list needs the height) */
  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-copy, .chip):not(.rv) {
  visibility: hidden;
}
.what-card .eyebrow::before { content: none; }
.what-card .eyebrow .rli::before {
  content: "/";
  margin-right: 1.3em;
  font-size: 0.82em;
  vertical-align: 0.05em;
}
.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 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: clamp(5px, calc(9 * var(--u)), 9px); /* (14 -> 9: 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;
  /* Sized so the whole subsection stands in the column at once -- nothing is scrolled out
     of the way, so the tallest of them has to fit. The mock's 69px works only because its
     list scrolls the read entry to the top and drops the rest off the edge (69 -> 54). */
  font-size: clamp(26px, calc(54 * var(--u)), 54px);
  /* Light, like every other display line on the page (400 -> 300): at this size Regular
     read as a heavier register than the headings it sits under */
  font-weight: 300;
  line-height: 1.08;
  letter-spacing: -0.02em;
  color: #cfd1d5;
  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 */
.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%;
  font-size: clamp(34px, calc(69 * var(--u)), 69px);
  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) * 0.6);
}

.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;
  font-size: 12px;
}

.what-num {
  font-size: 11px;
  letter-spacing: 0.22em;
}

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

.what-copy {
  margin: 0;
  max-width: 24ch;
  font-size: 14px;
  font-weight: 300;
  line-height: 1.5;
  color: var(--muted);
}

/* 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);
  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.
     (1.6s -> 1.1s: the growth, and the mask the plate enters through, read as slow) */
  transition-property: left, top, right, bottom, clip-path;
  transition-duration: 1.1s;
  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%;
  font-size: 14px;
  /* One line each (the longest is 57 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: 1.1s; /* = .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%; }
}

/* ...and on the stack it drifts. The card is pinned, so scrolling this step moves nothing
   by itself: sending the mark up against that travel (--step-p, services-ui.js) is what
   puts the depth back into a screen that is otherwise standing still. Not transitioned --
   it is bound to the wheel, and an ease here would only read as lag. */
.what-card[data-slot-sec="tech"] .tab-figure {
  translate: 0 calc(var(--step-p, 0) * -4vh); /* (18 -> 4vh: felt rather than seen) */
}

@media (prefers-reduced-motion: reduce) {
  .what-card[data-slot-sec="tech"] .tab-figure { translate: 0 0; }
}

.tab-figure canvas {
  display: block;
  width: min(74%, 74vh);
  height: min(74%, 74vh);
  translate: 0 0;
  opacity: 1;
  transition: translate 1.2s cubic-bezier(0.25, 0.8, 0.25, 1), 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); /* was glass (white 0.1 + blur): solid white by request */
  box-shadow: var(--lift-rest);
  font-size: 15px;
  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: #0055ff;
  opacity: 0;
}

.works-star::before {
  content: "";
  position: absolute;
  left: 50%;
  bottom: 50%;
  width: 1px;
  height: calc(var(--star-size) * 7);
  background: linear-gradient(to top, currentColor, transparent);
  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 Poppins' 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: -0.035em;
  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; /* (40svh -> 20svh) the reviews sit closer under the last work */
}

.project {
  position: relative;
  flex: none;
  /* Square, capped so the whole tile stays on one screen. (4/5 -> 1) */
  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; }

/* The tile is a grey plate first -- the same ground the marks in "what we do" stand on --
   and the photo arrives over it, wiping down from the top. The plate itself never hides:
   a tile that is nothing until its image comes is a hole in the page, a plate is a place
   the image is going to be. */
.project-visual {
  position: absolute;
  inset: 0;
  overflow: hidden;
  background: var(--plate-grey);
}

.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);
  transition: transform 1s var(--ease-drift),
    clip-path 1.6s var(--ease-io);
}

html.js .project:not(.is-revealed) .project-img {
  clip-path: inset(0 0 100% 0);
  transition: transform 1s var(--ease-drift);
}

.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; }

.project .project-eyebrow {
  font-size: 10px;
  letter-spacing: 0.16em;
  white-space: normal;
  color: var(--white);
}

.project .h5 {
  font-size: clamp(28px, 3vw, 48px);
  line-height: 1.1;
  letter-spacing: -0.03em;
  color: var(--white);
}

.project .body-lg {
  max-width: 34ch;
  font-size: 14px;
  line-height: 1.5;
  color: rgba(255, 255, 255, 0.85);
}

/* Re-arm the shared text masks before each hover or keyboard entrance. */
.project.is-resetting .rli { transform: translateY(110%); transition: none; }
.project.is-resetting .rv.rv-block { opacity: 0; transform: translateY(16px); 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: clamp(48px, calc(72 * var(--u)), 72px); /* (64..100 -> 48..72) featured quote to the divider */
  padding: clamp(72px, calc(120 * var(--u)), 120px) var(--page-x) clamp(96px, calc(160 * var(--u)), 160px) var(--page-x-lg);
  overflow: clip;
}

.quote-featured {
  --quote-indent: clamp(40px, calc(160 * var(--u)), 160px);
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 32px;
  /* No room held back for the progress track: the floated portrait used to spill into
     that strip, the inline one does not, and the empty band read 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: clamp(28px, calc(48 * var(--u)), 48px); }
/* 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);
}

/* The photo wipes down on entry and closes upward on return; its grey base stays still. */
.quote-portrait img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  clip-path: inset(0);
  transition: clip-path 1.6s var(--ease-io);
}

html.js .quote-portrait:not(.is-in) img {
  clip-path: inset(0 0 100% 0);
}
/* The grey plate waits for the typing too, so no lone square sits in the empty line. */
.quote-portrait-visual { transition: opacity var(--dur-base) ease; }
html.js .quote-portrait:not(.is-in) .quote-portrait-visual { opacity: 0; }

@media (prefers-reduced-motion: reduce) {
  .quote-portrait img { 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: clamp(28px, calc(48 * var(--u)), 48px); /* = .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: clamp(40px, calc(80 * var(--u)), 80px);
  padding-top: clamp(48px, calc(64 * var(--u)), 64px); /* (32..48 -> 48..64) divider to the small reviews */
  border-top: 1px solid rgba(20, 20, 20, 0.12);
}

.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 (0.22 -> 0.6) */
.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: 11px; line-height: 1.5; }
.quote-client .caption-xl { font-size: 13px; 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;
}

/* 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;
}

/* ---- 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;
}

@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: clamp(40px, calc(62 * var(--u)), 62px);
  font-weight: 300;
  line-height: 1.05;
  letter-spacing: -0.025em;
  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: 13px;
  font-weight: 300;
  line-height: 1.6;
  color: var(--text);
}

.team-note {
  position: absolute;
  right: 0;
  bottom: 0;
  font-size: 12px;
  font-weight: 300;
  letter-spacing: 0.02em;
  color: var(--text);
}

/* A portrait: the grey plate first, then the photo wipes down over it -- the same reveal
   as the works' images (.project-img), 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);
  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%;
  clip-path: inset(0);
  transition: clip-path 1.6s var(--ease-io);
}

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

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

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

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

/* Presence is the last section now, and the close lives inside it: a full screen plus the
   sheet's corner radius, so at the hold (globe centred) the footer's seam and its corner
   wedges stay below the fold */
.s-presence {
  position: relative;
  min-height: calc(100vh + var(--sheet-r));
}

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


.globe img {
  width: 100%;
  height: 100%;
  object-fit: contain;
}

.stat {
  position: absolute;
  display: flex;
  flex-direction: column;
  gap: 12px;
}


/* Odometer numbers: each digit is a column of 0-9 that rolls to its value (ui.js sets
   --v per column once the stat is in view) */
.odo {
  display: inline-flex;
  vertical-align: top;
  font-variant-numeric: tabular-nums;
}

/* Each column is exactly one line box tall (lh), so the digits sit on the word's baseline */
.odo-col {
  display: inline-block;
  height: 1lh;
  overflow: hidden;
}

.odo-strip {
  display: flex;
  flex-direction: column;
  transform: translateY(0);
  transition: transform 2.8s cubic-bezier(0.2, 0.8, 0.2, 1) var(--d, 0s); /* (1.6s -> 2.8s) */
}

.odo.is-on .odo-strip {
  transform: translateY(calc(var(--v, 0) * -1lh));
}

.odo-strip span {
  display: block;
  height: 1lh;
}
.stat .stat-head { gap: 8px; }
.stat .body-lg { width: 100%; max-width: 303px; }
/* 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: 1.3em;
  font-size: 0.82em;
  vertical-align: 0.05em;
}

/* 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: 231px; }

/* ---- 09 · The close (inside presence) ---------------------------------------------- */

/* Sits over the numbers from the start; its lines are masked away until the globe settles
   (.is-cta from globe.js), then rise through their masks like every other text on the
   page (ui.js adds .is-in to them at that moment, not on scroll) */
.presence-cta {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 40px;
  padding: 130px 10px 70px;
  text-align: center;
  pointer-events: none;
}

.s-presence.is-cta .presence-cta,
html.no-globe .presence-cta {
  pointer-events: auto;
}

/* No globe (main.js PRESENCE_GLOBE_ON): the close simply stands in the middle, where the
   sphere used to be, and the numbers keep their corners */
html.no-globe .presence-cta { opacity: 1; }
html.no-globe .presence-cta .cta-act { transform: none; opacity: 1; }

.presence-cta .cta-head { gap: 32px; }

.presence-cta .display {
  /* Width in em of its own size, so the break is always "Let's talk about / what you're building" */
  max-width: 9.6em;
  width: 9.6em;
  text-wrap: balance;
}

/* The numbers leave up through their masks (.is-out, the hero's exit -- ui.js) */
.s-presence.is-cta .stat { pointer-events: none; }

/* ...with a small dip first (anticipation): the line sinks 8% of its height, then goes up
   through the mask. A keyframe, since one transition cannot reverse direction; the same
   per-line delay as the hero's exit. Undoing the close transitions back from the held end. */
@keyframes stat-out {
  0%   { transform: translateY(0); animation-timing-function: var(--ease-dip); }
  28%  { transform: translateY(8%); animation-timing-function: var(--ease-in); }
  100% { transform: translateY(-110%); }
}

.s-presence .stat .rv.is-in.is-out .rli {
  animation: stat-out 0.45s calc(0.16s + var(--d-in, 0s) * 0.6) both; /* the body's lines go last (0.55s/0.24s -> 0.45s/0.16s) */
}

/* The eyebrow and the number are whole blocks (.rv-block: fade + lift): the same dip, and
   they leave one after the other ahead of the text -- eyebrow, number, then the lines */
@keyframes stat-block-out {
  0%   { opacity: 1; transform: translateY(0); animation-timing-function: var(--ease-dip); }
  28%  { opacity: 1; transform: translateY(3px); animation-timing-function: var(--ease-in); }
  100% { opacity: 0; transform: translateY(-16px); }
}

.s-presence .stat .rv-block.is-in.is-out { animation: stat-block-out 0.42s both; } /* (0.5 -> 0.42) */
.s-presence .stat .eyebrow.rv-block.is-in.is-out { animation-delay: 0s; }
.s-presence .stat .h4.rv-block.is-in.is-out { animation-delay: 0.08s; } /* (0.12 -> 0.08) */

/* The button rises in from below after the lines, like the text, and lifts out the way it
   came when the close is undone. On a wrapper: the button's own transform belongs to the
   tilt. */
.cta-act {
  display: block;
  opacity: 0;
  transform: translateY(40px); /* (16 -> 40) a proper rise, on the text lines' curve */
  transition: opacity var(--dur-base) ease, transform var(--dur-base) ease;
}

.s-presence.is-cta .cta-act {
  opacity: 1;
  transform: none;
  /* delay 0.45 -> 1.35 -> 1.05s: the lines start 0.65s in (ui.js CLOSE_AFTER_MS, once the numbers are gone) */
  transition: opacity 0.45s ease 1.05s, transform var(--dur-reveal) var(--ease-out-soft) 1.05s;
}

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

/* Its green "we're here" dot, the same one as why-us's "let's talk" pill */
.cta-act .online-dot { margin-right: -4px; } /* 16px gap - 4 = the pill's 12px to the label */

/* "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 with the close AFTER the four stats rather than over them. */
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"] .presence-caption { display: none; }

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"].no-globe:not(.has-presence-story) .presence-cta {
  position: static;
  grid-column: 1 / -1;
  padding: 64px 0 0;
}

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 {
  visibility: hidden;
  opacity: 1;
}
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: 50%;
  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: 50%;
  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: 999px;
  /* Solid, with a soft lift: over the globe's own blue grains a translucent pill and a
     300-weight label disappeared into the scene (0.92 -> 1, 400 -> 500, 11 -> 12px) */
  background: var(--white);
  box-shadow: 0 2px 10px rgba(20, 20, 20, 0.12);
  font-size: 12px;
  font-weight: 500;
  line-height: 1;
  letter-spacing: 0.1em;
  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: 10px; padding: 4px 9px; }
}
@media (prefers-reduced-motion: reduce) {
  .office-dot::after { animation: none; }
}
html[data-scene="dunes"].has-presence-story .s-presence .stat {
  position: absolute;
  width: clamp(160px, 19vw, 280px); /* (18vw/260 -> 19vw/280: "scalable digital solutions." on one line) */
  pointer-events: none;
}
/* The right pair's copy takes the whole column too (the 231px cap broke "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 was running 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); }

@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 .presence-caption { top: 76px; font-size: 9px; }
  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: 9px; }
  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,
  html[data-scene="dunes"].has-presence-story .s-presence .odo-strip,
  html[data-scene="dunes"].has-presence-story .s-presence .cta-act {
    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; /* (92/12svh/144 -> 120/16svh/192: more air under the header) narrow gutters: the wordmark runs edge to edge */
  background: var(--footer-bg);
  color: var(--text);
  border-radius: 0;
  --text-gradient: linear-gradient(150deg, var(--ink), #686868);
}

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

.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 on the right, where the menu button stood */
}

.footer-desc {
  font-size: 17px;
  max-width: 462px;
}

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

.footer-col { gap: 24px; }
.footer-col ul { list-style: none; display: flex; flex-direction: column; gap: 16px; }
/* 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);
}

.footer-mark { gap: 0; } /* the rule lands exactly on the plate's top edge, where the blur starts */
/* 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;
  /* Poppins descenders reach ~0.2em below the 1.2 line box: the tails of g and y end
     just short of the rule (0.2em -> 0.05em: closer to the line) */
  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 then cut the
     glyph's right edge (the g lost 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 cut the tails of
     g and y clean off at this size -- Poppins 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(110%);
  /* (--dur-reveal 0.7s -> 0.5s: ten letters on a 0.05s stagger read as a long wait) */
  transition: transform 0.5s var(--ease-out-soft) 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: rgba(20, 20, 20, 0.12); } /* (0.22 -> 0.14 -> 0.09) */

/* Its own plate, above the wordmark (which has a transform, so it paints in the positioned
   layer and would otherwise cover this). No background of its own -- only the blur, so the
   band is invisible over the flat footer and shows up exactly where the wordmark's tails
   drift under it on the scroll. Equal padding top and bottom: the plate IS the footer's
   bottom air now. At rest the descenders clear its top edge; they only reach it while the
   parallax pushes the wordmark down. */
.footer-bottom {
  position: relative;
  z-index: 1;
  padding: 16px 0;
  display: flex;
  justify-content: space-between;
  font-size: 14px;
  font-weight: 300;
  line-height: 1.6;
  letter-spacing: 0.02em;
  color: var(--text);
}


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

@media (max-width: 768px) {
  .why-head { padding: 0 24px; bottom: calc(90px + 12vh); }
  .why-time { display: none; }
  .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-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-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; margin: 0; }
  .quote-row { grid-template-columns: 1fr; gap: 40px; }
  .quote-stage { height: auto; }
  .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; align-items: flex-start; gap: 12px; }
  .team-title { text-align: left; margin-top: 0; }
  .team-lead, .team-note { 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; }

  /* No chunk globe on mobile, so the close simply follows the numbers */
  .presence-cta { position: static; padding: 64px 0 0; opacity: 1; transform: none; pointer-events: auto; }

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

/* Contact follows the tech-stack composition: copy left, particle mark right. */
.s-contact { position: relative; height: 145svh; padding: 0; }
.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, 12px);
}
.contact-copy {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  width: 62%;
  gap: 56px; /* (32 -> 56: 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, 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; }
.contact-copy .btn-meter { margin-left: 0; }
.contact-copy .cta-act { opacity: 1; transform: none; }
@media (max-width: 768px) {
  .contact-sticky { align-items: flex-end; padding-bottom: 13svh; }
  .contact-copy { width: 100%; gap: 24px; }
  .contact-copy .cta-head { gap: 20px; }
  .contact-overline { top: 112px; font-size: 9px; }
  .contact-copy .display { max-width: 10em; font-size: clamp(36px, 10.7vw, 48px); }
}

@media (max-width: 768px) {
  .s-footer { gap: 24px; padding-top: 108px; } /* (88 -> 108) */
  .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: 13px; }
  .footer-bottom { font-size: 11px; }
}
