/* ==========================================================================
   DISPLAY TYPEFACE — Atavian, self hosted
   High contrast didone, set in caps across every heading on the page. It ships
   as a single Regular: no bold, no italic. So headings are weight 400 and
   accent words carry on colour, never a synthesised oblique, which at this
   stroke contrast looks broken. Licence: ATAVIAN-LICENSE.rtf beside the file.
   ========================================================================== */
@font-face {
  font-family: "Atavian";
  src: url("../assets/fonts/atavian-regular.woff2") format("woff2");
  font-weight: 400; font-style: normal; font-display: swap;
}

/* ==========================================================================
   The UpSurge Agency
   Dark base. White and deep royal blue are the only accents.
   Two typefaces. Nothing else.

   The whole page is one continuous dark surface. The single inversion is
   the closing call to action (.invert), which flips to paper so the last
   screen lands at maximum contrast. To keep that section dark instead,
   remove the "invert" class from <section class="cta invert">.
   ========================================================================== */

:root {
  /* palette ------------------------------------------------------------- */
  --ink:       #0A0A0A;   /* the page */
  --paper:     #FAFAF8;   /* type, and the one inverted block */
  --blue:      #2438E0;   /* royal blue, solid fills */
  --blue-lit:  #5468FF;   /* the same blue lifted, for accent type on black */
  --blue-deep: #1B2ECB;   /* the same blue deepened, for accent type on white */

  /* scoped tokens. .invert flips these and everything follows. */
  --muted: rgba(250, 250, 248, 0.58);
  --rule:  rgba(250, 250, 248, 0.16);
  --accent: var(--blue-lit);
  --accent-solid: var(--blue);

  /* type ---------------------------------------------------------------- */
  /* One display face across the whole page. Atavian ships a single Regular
     with no italic and no bold, so every heading is weight 400 and every
     accent word carries on colour alone. */
  --display: "Atavian", "Didot", "Bodoni MT", Georgia, serif;
  --display-hero: var(--display);
  --body:    "Inter", system-ui, -apple-system, "Segoe UI", sans-serif;

  /* Hero type is sized off BUILT TO CONVERT, which runs edge to edge in the
     approved layout. Atavian in caps measures 10.31px of width per 1px of size
     on that line, so dividing the gutter to gutter width by 10.8 fills the
     measure with a small margin for the scrollbar. The other two lines are
     expressed as em fractions of it, so the whole lockup scales as one. */
  --fs-hero:    clamp(1.4rem, calc((100vw - 2 * var(--gut)) / 10.8), 12rem);
  /* Sized so the longest section heading, THREE DISCIPLINES., stays on one
     line in its 7 of 12 column. The rem cap matters: the container stops at
     --maxw while vw keeps climbing, so a pure vw term wraps on wide monitors. */
  --fs-display: clamp(1.75rem, 4.6vw, 4.25rem);
  --fs-lead:    clamp(1.25rem, 1.45vw, 1.5rem);
  --fs-body:    clamp(1rem, 1.05vw, 1.0625rem);
  --fs-micro:   0.75rem;

  /* space --------------------------------------------------------------- */
  --gut: clamp(1.5rem, 5vw, 5.5rem);
  --sec: clamp(7.5rem, 15vw, 16rem);
  --maxw: 1560px;

  --ease: cubic-bezier(0.22, 1, 0.36, 1);
}

.invert {
  background: var(--paper);
  color: var(--ink);
  --muted: rgba(10, 10, 10, 0.58);
  --rule:  rgba(10, 10, 10, 0.14);
  --accent: var(--blue-deep);
  --accent-solid: var(--blue-deep);
}

/* reset -------------------------------------------------------------------- */
*, *::before, *::after { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
html { -webkit-text-size-adjust: 100%; background: var(--ink); }

body {
  background: var(--ink);
  color: var(--paper);
  font-family: var(--body);
  font-size: var(--fs-body);
  font-weight: 400;
  line-height: 1.6;
  letter-spacing: -0.005em;
  overflow-x: hidden;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  font-synthesis: none;
}

img, video, canvas, svg { display: block; max-width: 100%; }
h1, h2, h3, p, figure, blockquote { margin: 0; }
ul { margin: 0; padding: 0; list-style: none; }
a { color: inherit; text-decoration: none; }
em { font-style: italic; }

::selection { background: var(--blue); color: #fff; }

:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 4px;
  border-radius: 2px;
}

.skip {
  position: absolute; left: -9999px; top: 0; z-index: 999;
  background: var(--paper); color: var(--ink); padding: 1rem 1.5rem;
}
.skip:focus { left: 1rem; top: 1rem; }

/* film grain ---------------------------------------------------------------- */
.grain {
  position: fixed; inset: 0; z-index: 9000; pointer-events: none;
  opacity: 0.55; mix-blend-mode: soft-light;
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='180' height='180'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3'/><feColorMatrix type='saturate' values='0'/></filter><rect width='180' height='180' filter='url(%23n)' opacity='0.35'/></svg>");
}

/* intro loader ------------------------------------------------------------- */
/* Over everything but the grain, which stays on top so the loader is the same
   film as the page. The failsafe animation takes it down at nine seconds if
   nothing else has: js/loader.js finishes well inside that even on a stalled
   connection, so it only ever fires when the script did not run. */
.loader {
  position: fixed; inset: 0; z-index: 8900;
  display: grid; place-items: center;
  overflow: hidden;
  background: radial-gradient(120% 90% at 50% 50%, #0b0e2a 0%, #06070f 55%, #050508 100%);
  animation: loader-failsafe 0.6s ease 9s forwards;
}
@keyframes loader-failsafe { to { opacity: 0; visibility: hidden; } }
/* the media query hides it before main.js has run to add .reduced */
html.reduced .loader { display: none; }
@media (prefers-reduced-motion: reduce) { .loader { display: none; } }

.loader__stars {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
}

/* the light at the end of the jump, scaled up from nothing by the script */
.loader__flash {
  position: absolute; left: 50%; top: 50%;
  width: 50vmax; height: 50vmax;
  margin: -25vmax 0 0 -25vmax;
  border-radius: 50%;
  background: radial-gradient(circle,
    #ffffff 0%,
    rgba(214, 222, 255, 0.96) 16%,
    rgba(84, 104, 255, 0.72) 40%,
    rgba(36, 56, 224, 0.28) 58%,
    rgba(36, 56, 224, 0) 72%);
  opacity: 0;
  transform: scale(0);
  pointer-events: none;
}

.loader__center {
  position: relative;
  display: flex; flex-direction: column; align-items: center;
  gap: clamp(2rem, 5vh, 3.25rem);
}
/* The glow is on this wrapper and the mask on the child. A filter runs
   before a mask on the same element, so a glow put on the masked element
   itself is cut away with everything else outside the trident. */
.loader__mark {
  display: block;
  width: clamp(7.5rem, 13vw, 11.5rem);
  aspect-ratio: 900 / 1052;
  color: var(--paper);
  filter: drop-shadow(0 0 2.25rem rgba(36, 56, 224, 0.7)) drop-shadow(0 0 0.4rem rgba(140, 162, 255, 0.55));
}
.loader__mark i {
  display: block; width: 100%; height: 100%;
  background: currentColor;
  -webkit-mask: url("../assets/brand/trident.png") center / contain no-repeat;
  mask: url("../assets/brand/trident.png") center / contain no-repeat;
}
.loader__meter {
  display: flex; flex-direction: column; align-items: center;
  gap: 0.9rem;
}
.loader__bar {
  display: block;
  width: min(20rem, 62vw); height: 2px;
  border-radius: 2px;
  background: rgba(250, 250, 248, 0.14);
  overflow: hidden;
}
.loader__bar i {
  display: block; width: 100%; height: 100%;
  transform-origin: left center;
  transform: scaleX(0);
  background: linear-gradient(90deg, var(--blue), #8fa2ff);
  box-shadow: 0 0 12px rgba(84, 104, 255, 0.8);
}
.loader__count {
  font-size: 0.8125rem; font-weight: 500;
  letter-spacing: 0.24em;
  color: rgba(250, 250, 248, 0.78);
  font-variant-numeric: tabular-nums;
}
.loader__count b { font-weight: 500; }

/* shared -------------------------------------------------------------------- */
.container { width: 100%; max-width: var(--maxw); margin-inline: auto; padding-inline: var(--gut); }

.display {
  font-family: var(--display);
  font-weight: 400;
  line-height: 0.98;
  letter-spacing: -0.012em;
  font-size: var(--fs-display);
  text-transform: uppercase;
}
.display em,
.hero__title em {
  font-style: normal;          /* no italic cut in the family */
  color: var(--accent);
}

.eyebrow {
  font-size: var(--fs-micro);
  font-weight: 500;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--muted);
  display: flex;
  align-items: center;
}
.eyebrow i {
  display: inline-block;
  width: 2.25rem; height: 1px;
  background: var(--accent);
  margin-right: 1rem;
  flex: none;
}

/* masked line reveals ------------------------------------------------------- */
.ln { display: block; overflow: hidden; padding-bottom: 0.08em; }
.hero__title .ln { padding-bottom: 0.18em; margin-bottom: -0.10em; }
.ln-i { display: block; will-change: transform; }

/* buttons ------------------------------------------------------------------- */
.btn {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: 0.9rem;
  padding: 1.15rem 2rem;
  border-radius: 999px;
  font-size: 0.9375rem;
  font-weight: 500;
  letter-spacing: 0.01em;
  overflow: hidden;
  isolation: isolate;
  /* the way back: the label waits for the draining fill to pass behind it */
  transition: color 0.5s var(--ease) 0.6s;
}
.btn:hover { transition: color 0.4s var(--ease); }
.btn > span, .btn > svg { position: relative; z-index: 2; }
.btn svg {
  width: 1.15rem; height: 1.15rem;
  fill: none; stroke: currentColor; stroke-width: 1.4;
  stroke-linecap: round; stroke-linejoin: round;
  transition: transform 0.55s var(--ease);
}
.btn::before {
  content: ""; position: absolute; inset: 0; z-index: 1;
  border-radius: inherit;
  transform: translateY(101%);
  /* The fill is quick in and slow out. A transition runs on the rule of the
     state it is heading to, so the hover rule sets the rise and this one the
     drain. The drain eases in and out rather than out, or the long duration
     would spend itself in the first few frames and then crawl. */
  transition: transform 1.6s cubic-bezier(0.65, 0, 0.35, 1);
}
.btn:hover svg { transform: translateX(0.35rem); }
.btn:hover::before { transform: translateY(0); transition: transform 0.55s var(--ease); }

/* white pill on the dark page, fills royal blue on hover */
.btn--light { background: var(--paper); color: var(--ink); }
.btn--light::before { background: var(--blue); }
.btn--light:hover { color: #fff; }

/* royal blue pill, used on the inverted closing block */
.btn--blue { background: var(--accent-solid); color: #fff; }
.btn--blue::before { background: var(--ink); }
.btn--blue:hover { color: var(--paper); }

.btn--xl { padding: 1.5rem 2.75rem; font-size: 1.0625rem; }

/* directional cues ---------------------------------------------------------- */
.cue {
  display: inline-flex; align-items: center; gap: 0.85rem;
  font-size: var(--fs-micro); letter-spacing: 0.2em;
  text-transform: uppercase; color: var(--muted);
}
.cue i {
  width: 3.5rem; height: 1px; background: currentColor; position: relative;
  opacity: 0.55;
}
.cue i::after {
  content: ""; position: absolute; right: 0; top: -3px;
  width: 7px; height: 7px;
  border-right: 1px solid currentColor; border-bottom: 1px solid currentColor;
  transform: rotate(-45deg);
}

/* ==========================================================================
   NAV
   ========================================================================== */
/* No pill, no blur, no button. Wordmark left, four links top right.
   Because there is no longer a background to sit on, the nav has to invert
   itself over the light sections. `.is-dark` is toggled from main.js against
   measured ranges, which keeps it on palette. Blend modes were the other
   option and were rejected: difference against the blue gradient turns the
   type yellow. */
.nav {
  position: fixed; top: 0; left: 0; right: 0; z-index: 800;
  padding: clamp(1.4rem, 2.4vw, 2.3rem) var(--gut);
  transition: transform 0.55s var(--ease);
  pointer-events: none;
}
.nav.is-hidden { transform: translateY(-160%); }
/* The menu is a layer above the nav, so while it is open the nav steps over
   it, or the close button would be buried under the panel it closes. */
.nav.is-menu-open { z-index: 950; }

.nav__inner {
  max-width: var(--maxw); margin-inline: auto;
  display: flex; align-items: center; gap: clamp(1rem, 4vw, 4rem);
}
.nav__inner > * { pointer-events: auto; }

.nav__logo {
  display: inline-flex; align-items: center; gap: 0.5rem;
  color: var(--paper);
  font-weight: 500; font-size: 0.9375rem; letter-spacing: 0.16em;
  transition: color 0.45s var(--ease);
}
.nav__logo i {
  width: 6px; height: 6px; border-radius: 50%;
  background: var(--blue-lit); display: block;
  transition: transform 0.5s var(--ease), background 0.45s var(--ease);
}
.nav__logo:hover i { transform: scale(1.9); }

/* The trident on its own in the corner. Same asset as the emblem, painted as a
   mask so `currentColor` carries it, which is what lets it flip to ink over the
   light sections alongside the menu button. */
.nav__mark {
  display: block;
  width: 1.95rem; height: 2.28rem;   /* the mark is 900 by 1052 */
  color: var(--paper);
  transition: color 0.45s var(--ease), transform 0.5s var(--ease), opacity 0.35s var(--ease);
}
.nav__mark i {
  display: block; width: 100%; height: 100%;
  background: currentColor;
  -webkit-mask: url("../assets/brand/trident.png") center / contain no-repeat;
  mask: url("../assets/brand/trident.png") center / contain no-repeat;
}
.nav__mark:hover { transform: translateY(-3px); }
.nav.is-dark .nav__mark { color: var(--ink); }
/* the panel carries its own mark while it is open, so this one steps aside */
.nav.is-menu-open .nav__mark { opacity: 0; pointer-events: none; }

/* --- menu toggle ---------------------------------------------------------- */
.menu-toggle {
  margin-left: auto;
  width: 56px; height: 56px;
  display: grid; place-items: center;
  background: none; border: 0; padding: 0;
  cursor: pointer;
  color: var(--paper);
}
.menu-toggle svg { width: 34px; height: 34px; overflow: visible; }
.menu-toggle .bar {
  stroke: currentColor;
  stroke-width: 1.3;
  stroke-linecap: round;
}

/* over the white sections */
.nav.is-dark .menu-toggle { color: var(--ink); }
/* while the menu is open the toggle always sits on the dark panel */
.nav.is-menu-open .menu-toggle { color: var(--paper); }

/* ==========================================================================
   OFF CANVAS MENU
   ========================================================================== */
.menu {
  position: fixed; inset: 0; z-index: 900;
  visibility: hidden;
  pointer-events: none;
}
.menu__bg {
  position: absolute; inset: 0;
  background: rgba(6, 6, 8, 0.62);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
  opacity: 0;
  cursor: pointer;
}

/* two stacked panels, the accent one trailing so a blade of royal blue shows
   as they slide */
.menu__panels { position: absolute; inset: 0; pointer-events: none; }
.menu__panel {
  position: absolute; top: 0; bottom: 0; right: 0;
  width: min(46vw, 560px);
  display: block;
  will-change: transform;
}
.menu__panel--accent { background: var(--blue); }
.menu__panel--base   { background: var(--ink); }

/* Three bands: the mark at the top, the list centred in whatever height is
   left, the call at the foot. The list's auto margins do the centring, so it
   stays in the middle of the free space however tall the other two get. The
   top padding matches the nav's own, so the mark sits level with the close
   button across the panel from it. */
.menu__inner {
  position: absolute; top: 0; bottom: 0; right: 0;
  width: min(46vw, 560px);
  display: flex; flex-direction: column;
  gap: clamp(1.5rem, 4vh, 3rem);
  padding: calc(clamp(1.4rem, 2.4vw, 2.3rem) + 6px) clamp(2rem, 4vw, 4rem) clamp(2rem, 5vh, 3.5rem);
  overflow-y: auto;
}
/* on a short screen the panel scrolls rather than crushing its contents */
.menu__inner > * { flex-shrink: 0; }
.menu__brand {
  display: block;
  width: 2.4rem; height: 2.8rem;          /* the mark is 900 by 1052 */
  color: var(--paper);
  transition: transform 0.5s var(--ease);
}
.menu__brand i {
  display: block; width: 100%; height: 100%;
  background: currentColor;
  -webkit-mask: url("../assets/brand/trident.png") center / contain no-repeat;
  mask: url("../assets/brand/trident.png") center / contain no-repeat;
}
.menu__brand:hover { transform: translateY(-3px); }

.menu__list {
  display: flex; flex-direction: column; gap: clamp(0.5rem, 1.6vh, 1.1rem);
  margin-block: auto;
}

.menu__item {
  display: flex; align-items: baseline; gap: clamp(0.9rem, 1.8vw, 1.5rem);
  font-family: var(--display);
  font-weight: 400;
  text-transform: uppercase;
  font-size: clamp(1.75rem, 3.6vw, 2.9rem);
  line-height: 1;
  letter-spacing: 0;
  color: var(--paper);
  transition: color 0.4s var(--ease);
}
.menu__item i {
  font-family: var(--body);
  font-style: normal;
  font-size: var(--fs-micro);
  font-weight: 500;
  letter-spacing: 0.2em;
  color: var(--blue-lit);
  transition: color 0.4s var(--ease);
}
.menu__item span { position: relative; }
.menu__item span::after {
  content: ""; position: absolute; left: 0; bottom: 0.08em;
  width: 100%; height: 2px; background: var(--blue-lit);
  transform: scaleX(0); transform-origin: right;
  transition: transform 0.5s var(--ease);
}
.menu__item:hover span::after { transform: scaleX(1); transform-origin: left; }

/* The booking block at the foot of the panel. The button stretches to the
   panel's width on a phone, where a short button in a full width panel looks
   like it has been left there. */
.menu__call {
  display: flex; flex-direction: column; align-items: flex-start;
  gap: 0.75rem;
  border-top: 1px solid var(--rule);
  padding-top: clamp(1.25rem, 3vh, 2rem);
}
.menu__callnote {
  margin: 0;
  font-size: 0.875rem; line-height: 1.5;
  color: var(--muted);
}
.menu__mail {
  margin-top: 0.35rem;
  font-size: 0.875rem;
  color: var(--paper);
  transition: color 0.4s var(--ease);
}
.menu__mail:hover, .menu__mail:focus-visible { color: var(--blue-lit); }

@media (max-width: 700px) {
  .menu__panel, .menu__inner { width: 100%; }
  .menu__cta { align-self: stretch; justify-content: center; }
}

/* ==========================================================================
   SECTION 1 — HERO
   ========================================================================== */
.hero { position: relative; height: 172vh; }

.hero__sticky {
  position: sticky; top: 0;
  height: 100vh; height: 100svh;
  overflow: hidden;
}

/* --- cosmic background ---------------------------------------------------- */
.hero__bg {
  position: absolute; inset: 0; z-index: 0;
  overflow: hidden;
  background: #05060D;
  will-change: opacity;
}
.hero__bg-orbit {
  position: absolute;
  left: -6%; top: -6%;
  width: 112%; height: 112%;
  will-change: transform;
}
.hero__bg-video {
  width: 100%; height: 100%;
  object-fit: cover;
  will-change: transform;
}

/* Darkest through the middle, not at the edges. The galaxy core is centre
   frame and the watch has to sit in front of it, so the veil knocks the centre
   back and lets the starfield survive at the corners. The bottom runs to solid
   ink so the hero dissolves into the dawn gradient below it. */
.hero__veil {
  position: absolute; inset: 0;
  background:
    radial-gradient(54% 48% at 50% 45%,
      rgba(10, 10, 10, 0.88) 0%,
      rgba(10, 10, 10, 0.70) 34%,
      rgba(10, 10, 10, 0.32) 68%,
      rgba(10, 10, 10, 0.06) 100%),
    linear-gradient(to bottom,
      rgba(10, 10, 10, 0.80) 0%,
      rgba(10, 10, 10, 0.26) 22%,
      rgba(10, 10, 10, 0.32) 58%,
      rgba(10, 10, 10, 0.80) 86%,
      #0A0A0A 100%);
}

/* The render sits above the type, per the approved layout. pointer-events off,
   or the full bleed canvas would swallow every click in the hero. */
.hero__canvas {
  position: absolute; inset: 0; width: 100%; height: 100%;
  z-index: 3;
  pointer-events: none;
}

/* the object sits in its own light rather than casting a shadow */
.hero__shadow {
  position: absolute; z-index: 1;
  left: 50%; top: 47%;
  width: min(58vw, 820px); aspect-ratio: 1 / 1;
  transform: translate(-50%, -50%);
  background: radial-gradient(closest-side,
    rgba(84, 104, 255, 0.20) 0%,
    rgba(36, 56, 224, 0.08) 38%,
    rgba(36, 56, 224, 0) 68%);
}

.hero__inner {
  position: relative; z-index: 2;
  height: 100%;
  padding: clamp(3.5rem, 6vh, 5.5rem) var(--gut) clamp(2rem, 4vh, 3.5rem);
  max-width: none; margin-inline: auto;
  display: flex; flex-direction: column;
  pointer-events: none;
}
.hero__inner a { pointer-events: auto; }

.hero__eyebrow { letter-spacing: 0.26em; }

.hero__title {
  flex: 1;
  display: flex; flex-direction: column;
  align-items: center; justify-content: flex-start;
  text-align: center;
  font-family: var(--display-hero);
  font-weight: 400;
  font-size: var(--fs-hero);
  line-height: 1.0;
  letter-spacing: -0.008em;
  text-transform: uppercase;
  width: 100%;
}
.hero__title .ln { align-self: center; }

/* MADE TO BE sits at 0.78 of the dominant line, as in the approved layout */
.hero__title > .ln:first-child { font-size: 0.78em; }

/* the spaced out word under it. text-indent cancels the trailing letter space
   so it stays optically centred */
.hero__word {
  font-size: 0.30em;
  letter-spacing: 1.4em;
  text-indent: 1.4em;
  margin-top: 0.7em;
}

/* BUILT TO CONVERT is the dominant line and runs the full measure */
.hero__title .hero__line2 { align-self: center; text-align: center; font-size: 1em; }

/* the gap the watch lives in. It takes whatever height is left, which is what
   pins the dominant line to the bottom of the lockup */
.hero__void { display: block; flex: 1 1 auto; min-height: clamp(5rem, 26vh, 20rem); }

.hero__foot {
  display: flex; align-items: center; justify-content: center;
  margin-top: clamp(1.25rem, 2.6vh, 2.25rem);
}
.hero__support {
  max-width: 34ch;
  font-size: var(--fs-lead);
  line-height: 1.45;
  color: var(--muted);
  text-wrap: balance;
}

.scrollcue {
  position: absolute; z-index: 4;
  left: clamp(0.85rem, 1.7vw, 1.6rem);
  top: 50%; bottom: auto;
  transform: translateY(-50%);
  display: flex; flex-direction: column; align-items: center; gap: 0.9rem;
  font-size: 0.6875rem; letter-spacing: 0.24em; text-transform: uppercase;
  color: var(--muted);
}
.scrollcue--right { left: auto; right: clamp(0.85rem, 1.7vw, 1.6rem); }
.scrollcue span {
  writing-mode: vertical-rl;
  letter-spacing: 0.3em;
}
.scrollcue i {
  display: block; width: 1px; height: 3.25rem;
  background: linear-gradient(to bottom, rgba(250, 250, 248, 0.12), var(--blue-lit));
  overflow: hidden;
}
.scrollcue i::after {
  content: ""; display: block; width: 1px; height: 45%;
  background: var(--paper);
  animation: cueRun 2.2s var(--ease) infinite;
}
@keyframes cueRun {
  0%   { transform: translateY(-100%); }
  100% { transform: translateY(240%); }
}


/* --- annotation callouts --------------------------------------------------
   Anchored to the headline gap, not to viewport percentages. main.js measures
   .hero__void and publishes --void-top / --void-bottom / --void-h, so these sit
   in the gap between the two headline blocks at any viewport height. Tying them
   to vh instead let them drift into the type on shorter screens.

   Each callout is one unit: chip plus its own leader SVG. Move the unit and the
   line follows, which beats maintaining a separate overlay of coordinates. */
.hero__notes {
  position: absolute; inset: 0; z-index: 4;
  pointer-events: none;
}

.note {
  position: absolute;
  display: flex;
  align-items: center;
  will-change: transform, opacity;
}
.note--left  { flex-direction: row; }
.note--right { flex-direction: row-reverse; }

.note__leader {
  width: clamp(80px, 8.5vw, 180px);
  height: clamp(36px, 4.5vh, 76px);
  flex: none;
}
.note__leader path {
  stroke: rgba(250, 250, 248, 0.42);
  stroke-width: 1;
}

.note__box {
  position: relative;
  background: var(--paper);
  color: var(--ink);
  font-family: var(--body);
  font-size: 0.6875rem;
  font-weight: 500;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  line-height: 1;
  padding: 0.6rem 0.95rem;
  white-space: nowrap;
}

.note__box--stacked {
  display: flex; flex-direction: column; align-items: flex-start;
  gap: 0.34rem;
  max-width: 15.5rem;
  white-space: normal;
}
.note__title { white-space: nowrap; }
.note__sub {
  font-size: 0.5rem;
  font-weight: 400;
  letter-spacing: 0.1em;
  line-height: 1.45;
  color: rgba(10, 10, 10, 0.55);
}

/* four corner ticks, one element, eight tiny background slices */
.note__box::before {
  content: "";
  position: absolute; inset: -6px;
  opacity: 0.5;
  background:
    linear-gradient(var(--paper), var(--paper)) 0 0 / 10px 1px no-repeat,
    linear-gradient(var(--paper), var(--paper)) 0 0 / 1px 10px no-repeat,
    linear-gradient(var(--paper), var(--paper)) 100% 0 / 10px 1px no-repeat,
    linear-gradient(var(--paper), var(--paper)) 100% 0 / 1px 10px no-repeat,
    linear-gradient(var(--paper), var(--paper)) 0 100% / 10px 1px no-repeat,
    linear-gradient(var(--paper), var(--paper)) 0 100% / 1px 10px no-repeat,
    linear-gradient(var(--paper), var(--paper)) 100% 100% / 10px 1px no-repeat,
    linear-gradient(var(--paper), var(--paper)) 100% 100% / 1px 10px no-repeat;
}

/* lower left of the render, clearing the dominant line. Positioned from the
   bottom so GSAP is free to use yPercent on it during the scroll. */
.note--left {
  left: 21%;
  bottom: calc(100% - var(--void-bottom, 72%) + 1.6rem);
}

/* upper right, far enough down the gap that a leader drawn from here can
   actually reach the case. The horizontal position is then solved in main.js. */
.note--right {
  right: 21%;
  top: calc(var(--void-top, 28%) + var(--void-h, 40vh) * 0.40);
}

/* ==========================================================================
   SEAMS — no section ever stops, it dissolves
   ========================================================================== */
.seam { position: relative; pointer-events: none; z-index: 1; }

.seam--bloom {
  display: grid; place-items: center;
  overflow: hidden;
}
.seam--bloom span {
  display: block;
  width: min(150vw, 1700px);
  will-change: transform, opacity;
}

/* THE DAWN
   Ink at the top so it meets the hero invisibly, through royal blue, out to
   paper at the bottom so it meets What We Do invisibly. The black ellipse on
   top is what gives the horizon its curve; it rises as you scroll. */
/* Pulled up into the hero's tail so a band of blue is already showing at the
   bottom of the frame when the watch finishes its animation. Its top edge is
   ink on ink, so the overlap is invisible, and .seam is pointer-events none so
   it cannot swallow anything underneath. Roughly the top 44% is on screen when
   the watch finishes, so blue has to arrive well inside that, and the eclipse
   has to clear it. */
.seam--dawn {
  height: 96vh; height: 96svh;
  margin-top: -38vh;
  overflow: hidden;
  /* The seam is opaque and slides up over the hero, so its top edge used to cut
     a hard black line straight across the starfield. Masking the top 11% fades
     the whole layer in, the eclipse included, instead of butting against it. */
  -webkit-mask-image: linear-gradient(to bottom, rgba(0, 0, 0, 0) 0%, #000 11%, #000 100%);
  mask-image: linear-gradient(to bottom, rgba(0, 0, 0, 0) 0%, #000 11%, #000 100%);
  background: linear-gradient(to bottom,
    #0A0A0A 0%,
    #0C1140 16%,
    #1B2ECB 27%,
    #2E56F5 45%,
    #6D9BFF 58%,
    #A9CCFF 70%,
    #D8E9FF 82%,
    #F2F6FF 92%,
    #FAFAF8 100%);
  margin-bottom: -1px;
}

/* The black dome that gives the horizon its curve. It has to finish well up the
   seam, or it paints over the band of blue that shows under the watch at the
   end of the hero, which is the whole point of pulling the seam up. */
.seam__eclipse {
  position: absolute;
  left: 50%; top: -50%;
  width: 215%; height: 72%;
  transform: translateX(-50%);
  border-radius: 50%;
  background: radial-gradient(closest-side,
    #0A0A0A 0%,
    #0A0A0A 56%,
    rgba(10, 10, 10, 0.92) 68%,
    rgba(10, 10, 10, 0.55) 80%,
    rgba(10, 10, 10, 0) 100%);
  will-change: transform;
}


/* THE DUSK
   Ink back to paper, for everywhere the archway does not run. The dome carries
   that transition itself on a desktop, so this is the fallback below the
   breakpoint and under reduced motion, not a second copy of it. */
.seam--to-light {
  height: 160px;
  /* tucked a pixel under the paper it runs into: edges that meet on a
     fractional pixel let the ink page behind show as a hairline */
  margin-bottom: -1px;
  background: linear-gradient(to bottom,
    #0A0A0A 0%,
    #141A45 38%,
    #4A56A8 66%,
    #B9BBD0 85%,
    #FAFAF8 100%);
}
@media (min-width: 901px) and (prefers-reduced-motion: no-preference) {
  .seam--to-light { display: none; }
}

/* The same bridge the other way, paper down to ink, out of What We Do and into
   Why Us. Only where Why Us starts dark: on a desktop it opens white and the
   page runs on as one light block, so there it is hidden, on the same
   condition as the white it stands in for. */
.seam--to-dark {
  height: 160px;
  margin-top: -1px;          /* see .seam--to-light: under What We Do's paper */
  background: linear-gradient(to bottom,
    #FAFAF8 0%,
    #B9BBD0 15%,
    #4A56A8 34%,
    #141A45 62%,
    #0A0A0A 100%);
}
@media (min-width: 901px) and (prefers-reduced-motion: no-preference) {
  .seam--to-dark { display: none; }
}

/* ==========================================================================
   THE ARCHWAY
   The closing section is the arch. It is pulled a viewport up the document and
   rises over the mirror while that frame is still pinned, exactly the way
   Clients rises over Selected Work, so its curved top sweeps across the
   finished picture and its own words come up behind the curve.

   That is the whole reason it is not a separate dome. A dome can only be a
   shape; the words then have to live in a section below it, which means a
   screen of empty white between the two and a call to action nobody reads
   until long after the effect has finished.

   The curve is a shape, not a mask. `.cta__sheet` is the paper, wider than the
   window so the arc reads as a sweep rather than a bubble, and the section
   clips it back. The section itself paints nothing, which is why `.invert`'s
   background is overridden while its tokens stay.
   ========================================================================== */
.cta__sheet {
  position: absolute;
  left: 50%; top: 0;
  transform: translateX(-50%);
  width: 118vw; height: 100%;
  /* 22vh of rise at the centre, not the 36 a dome wants. The type has to clear
     the curve and still be on screen while the section is only halfway up: at
     halfway its top sits at 50vh, so everything in its first 50vh shows, and a
     deeper arc spends all of that on itself. */
  border-radius: 50% 50% 0 0 / 22vh 22vh 0 0;
  background: var(--paper);
  z-index: 0;
}

/* Only where the mirror is pinned for it to rise over. Everywhere else this is
   an ordinary flat chapter and the seam above does the ink to paper. */
@media (min-width: 901px) and (prefers-reduced-motion: no-preference) {
  .cta {
    margin-top: -100vh;
    z-index: 5;                     /* over the pinned frame, which is at auto */
    /* It has to be at least as tall as what it rose over. Its own content came
       to about 790px, so the last 110px of the mirror showed again underneath
       it the moment the pin released. */
    min-height: 100vh; min-height: 100svh;
    /* the leading edge, cast down onto the dark picture it is covering */
    filter: drop-shadow(0 -8px 26px rgba(10, 10, 10, 0.5));
  }
}
@media (max-width: 900px), (prefers-reduced-motion: reduce) {
  .cta__sheet { width: 100%; border-radius: 0; }
}

/* THE DUSK, the same horizon falling back into the dark */
.seam--bloom {
  height: clamp(8rem, 18vh, 14rem);
  margin-block: clamp(-11rem, -16vh, -6rem);
}
.seam--bloom span {
  aspect-ratio: 2.6 / 1;
  background: radial-gradient(closest-side,
    rgba(84, 104, 255, 0.24) 0%,
    rgba(36, 56, 224, 0.07) 42%,
    rgba(36, 56, 224, 0) 74%);
}


/* ==========================================================================
   SECTION HEADS
   ========================================================================== */
.sechead {
  display: grid;
  grid-template-columns: repeat(12, 1fr);
  column-gap: clamp(1.5rem, 3vw, 3rem);
  align-items: end;
  padding-bottom: clamp(4rem, 9vw, 9rem);
}
.sechead .eyebrow { grid-column: 1 / -1; margin-bottom: clamp(0.9rem, 1.7vw, 1.6rem); }
.sechead__title { grid-column: 1 / 8; }

/* The turning emblem. It replaces a paragraph, so it keeps that paragraph's
   grid slot and its baseline padding, and it is the only thing competing with
   the heading on this row: one mark, one line of type, turning. */
/* The slot holds the grid position and carries the entrance, and the emblem
   inside it carries the pointer pull. They cannot be the same element: the
   entrance animates y and the magnet writes x and y, so one would overwrite
   the other on the first pointer move. */
.emblem-slot {
  grid-column: 9 / 13;
  justify-self: center;
  width: min(100%, 17rem);
  margin-bottom: 0.6em;
}
.emblem {
  position: relative;
  width: 100%;
  aspect-ratio: 1;
  will-change: transform;
}
.emblem__ring {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  overflow: visible;
  animation: emblemturn 30s linear infinite;
}
.emblem__ring text {
  font-family: var(--body);
  font-size: 13px;                 /* user units: the viewBox is 240 wide */
  letter-spacing: 0.02em;
  text-transform: uppercase;
  fill: var(--muted);
}
/* Centred with a CSS translate, which is safe here only because nothing in
   js/main.js animates this element: GSAP would overwrite the whole transform. */
/* Sized off its HEIGHT, not its width. The mark is taller than it is wide,
   900 by 1052 once trimmed to the artwork, so a width driven size grows it
   past the ring it is supposed to sit inside. */
.emblem__mark {
  position: absolute;
  top: 50%; left: 50%;
  height: 44%; width: auto;
  transform: translate(-50%, -50%);
}
@keyframes emblemturn { to { transform: rotate(360deg); } }

/* ==========================================================================
   SECTION 2 — WHAT WE DO
   ========================================================================== */
.services {
  position: relative;
  z-index: 2;                      /* above .seam, which sits at 1 */
  margin-top: -12vh;
  /* not var(--sec): the next chapter opens a window out of the white rather
     than starting a new field of it, so the usual section gap read as dead
     space under the button. Trimmed by about a third. */
  padding-block: 2rem clamp(5.25rem, 10.5vw, 11rem);
  /* `.invert` paints a flat paper background, and pulling the section up would
     drop that straight onto the gradient's still blue tail and cut a hard edge
     across it. Fading paper in instead means the chapter arrives out of the
     gradient rather than on top of it.

     The fade length has to be the overlap exactly, never a round number. The
     page behind this section is ink, so wherever the fade is still transparent
     and the seam above has already ended, black shows through: the fade was
     130px against an overlap of 12vh, which is 108px at a 900px viewport, and
     those 22px read as a grey band under a hard line, like a drop shadow
     across the page. 12vh makes the fade finish precisely where the seam stops,
     at any viewport height. The seam's own -1px bottom margin does not add to
     that: adjacent negative margins collapse to the larger one, so the overlap
     is 12vh and nothing more. Measured to the pixel at 900px and at 1400px. */
  background: linear-gradient(to bottom, rgba(250, 250, 248, 0) 0, var(--paper) 12vh);
}

/* The band the streaks cross. Its height is written by main.js from the bottom
   of the section heading, so they pass BEHIND the type and stop there rather
   than carrying on down into the three cards. Faded at the bottom so they thin
   out instead of being clipped off mid flight. */
.services__spark {
  position: absolute;
  inset: 0 0 auto 0;
  height: clamp(16rem, 40vh, 30rem);
  width: 100%;
  z-index: 0;
  pointer-events: none;
  -webkit-mask-image: linear-gradient(to bottom, #000 0%, #000 72%, transparent 100%);
  mask-image: linear-gradient(to bottom, #000 0%, #000 72%, transparent 100%);
}
.services .container { position: relative; z-index: 1; }

/* The block the sequence pins. Its own stacking context, because the streak
   canvas inside it has to stay behind the type, and its own positioning
   context, because that canvas is measured against it. */
.svcstage {
  position: relative; z-index: 1;
  /* The heading waits off the left of this box and the cards off the right, so
     the box has to clip sideways or the page grows a horizontal scrollbar.
     `clip` rather than `hidden` because it creates no scroll container, and it
     is the only value allowed to pair with a visible axis, which the cards'
     drop shadows need. */
  overflow-x: clip;
  overflow-y: visible;
}

/* Three cards that tilt toward the pointer. They are ink panels on the white
   section: a dark solid catching light as it turns reads as an object, where a
   white card on white would only read as a rectangle. Rotation is driven from
   js/main.js; everything here just sets the stage and the resting state. */
.svcgrid {
  --svc-gap: clamp(1.5rem, 2.6vw, 2.75rem);
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--svc-gap);
  perspective: 1400px;
}

.svc {
  position: relative;
  border-radius: 2px;
  color: var(--paper);
  transform-style: preserve-3d;
  will-change: transform;
  box-shadow: 0 18px 50px rgba(10, 10, 10, 0.18);
  transition: box-shadow 0.5s var(--ease);

  /* the card carries its own scope, so tokens inside flip back to the dark set */
  --rule:  rgba(250, 250, 248, 0.16);
  --accent: var(--blue-lit);

  /* How far this card has been lit, nought to one. js/main.js tweens it as the
     sequence runs and everything else on the card reads from it, so the fill,
     the copy contrast and the badge can never disagree about which step is
     live. Body copy has to climb with it: white at 58% over royal blue is
     3.44:1, under the 4.5 it needs, and at 78% it is 5.07:1. */
  --lit: 0;
  --muted: rgba(250, 250, 248, calc(0.58 + 0.2 * var(--lit)));

  /* The badge's inset and size, shared so the connector can aim at the number
     rather than at the middle of nothing. Across a row the wire sits on the top
     edge, level with the badge centres; stacked, it drops straight out of the
     badge above. The inset matches the face's padding, so the number lines up
     with the heading under it. */
  --svc-inset: clamp(1.75rem, 2.4vw, 2.6rem);
  --svc-badge: 2.5rem;
  background: var(--ink);
  background: color-mix(in srgb, var(--blue) calc(var(--lit) * 100%), var(--ink));
}
.svc:hover { box-shadow: 0 34px 80px rgba(10, 10, 10, 0.32); }

/* the accent edge that lights along the top as the card lifts */
.svc::before {
  content: ""; position: absolute; top: 0; left: 0;
  width: 100%; height: 2px; background: var(--blue-lit);
  transform: scaleX(0); transform-origin: left;
  transition: transform 0.7s var(--ease);
}
.svc:hover::before { transform: scaleX(1); }

.svc__face {
  padding: clamp(1.75rem, 2.4vw, 2.6rem);
  display: flex; flex-direction: column;
  height: 100%;
  transform: translateZ(28px);          /* content floats above the panel */
  transform-style: preserve-3d;
}


.svc__title {
  font-family: var(--display); font-weight: 400;
  font-size: clamp(1.5rem, 2.1vw, 2.25rem);
  line-height: 1.0; letter-spacing: 0;
  text-transform: uppercase;
  margin-bottom: 1.1rem;
}
.svc__body { color: var(--muted); max-width: 34ch; margin-bottom: clamp(1.75rem, 2.6vw, 2.5rem); }

/* Every tag carries a dot in the gap before it, and the list is pulled one gap
   to the left with that strip clipped off. So the first tag on every line,
   not just the first in the list, loses its dot: on a narrow card the tags
   wrap, and a dot hanging at the start of the second line reads as a stray. */
.svc__tags {
  display: flex; flex-wrap: wrap; row-gap: 0.5rem;
  margin-top: auto; margin-left: -1.5rem;
  clip-path: inset(-0.5rem 0 -0.5rem 1.5rem);
}
.svc__tags li {
  margin-left: 1.5rem;
  font-size: var(--fs-micro); letter-spacing: 0.12em;
  text-transform: uppercase; color: var(--muted);
  position: relative;
}
.svc__tags li::before {
  content: ""; position: absolute; left: -0.78rem; top: 50%;
  width: 3px; height: 3px; border-radius: 50%;
  background: var(--rule); transform: translateY(-50%);
}

/* The step number, straddling the card's top edge so it reads as a marker
   pinned to the card rather than a heading inside it. Paper on ink, and paper
   on blue once the card lights, so it holds either way. The resting transform
   is set in JavaScript, not here: GSAP composes translateZ with the scale and
   lift it animates, and a CSS transform would simply be overwritten. */
.svc__step {
  position: absolute;
  top: 0; left: var(--svc-inset);
  z-index: 2;
  margin: 0;
  width: var(--svc-badge); height: var(--svc-badge);
  display: grid; place-items: center;
  border-radius: 50%;
  background: var(--paper);
  color: var(--ink);
  font-family: var(--display); font-weight: 400;
  font-size: 1.25rem; line-height: 1;
  box-shadow: 0 6px 18px rgba(10, 10, 10, 0.28);
}

/* The connector back to the card before this one. It lives in the gutter, so
   it is sized from the grid's own gap token rather than a guessed number, and
   the cards do not clip it: `.svc` is overflow visible. Scaled from a variable
   instead of a transform, because the phone layout stacks the cards and has to
   grow the same wire downward instead of sideways. */
.svc__wire {
  position: absolute;
  top: 0; right: calc(-1 * var(--svc-gap));
  width: var(--svc-gap); height: 2px;
  margin-top: -1px;
  background: var(--blue);
  transform: scaleX(var(--wire, 0));
  transform-origin: left center;
}

/* section call to action */
.svcfoot {
  display: flex; flex-direction: column; align-items: center;
  gap: clamp(1rem, 1.8vw, 1.5rem);
  text-align: center;
  margin-top: clamp(3.5rem, 6vw, 6rem);
}
.svcfoot__note {
  font-size: 0.875rem; color: var(--muted); max-width: 42ch;
}

.services .cue { margin-top: clamp(2rem, 3.8vw, 3.4rem); }

/* The hand off out of the services chapter: centred on the page, the label
   over a ring with an arrow that keeps dropping through it. It is a link to the
   next chapter, so the ring fills on hover the way the buttons do. The loop is
   CSS rather than GSAP because nothing drives it but time, and the reduced
   motion block at the foot of this file stops it along with everything else. */
.cue--down {
  display: flex; flex-direction: column; align-items: center;
  gap: 1rem;
  width: max-content; margin-inline: auto;
  font-size: 0.8125rem; letter-spacing: 0.24em; font-weight: 500;
  color: var(--ink);
}
.cue__ring {
  position: relative;
  display: grid; place-items: center;
  width: clamp(3.75rem, 5vw, 4.5rem); aspect-ratio: 1;
  border-radius: 50%;
  border: 1px solid rgba(10, 10, 10, 0.34);
  isolation: isolate;
  transition: border-color 0.5s var(--ease), color 0.4s var(--ease) 0.5s;
}
/* The fill and the arrow are clipped to the ring by a wrapper, not by the
   ring itself, so the pulse below can travel outside it. */
.cue__clip {
  position: absolute; inset: 0;
  display: grid; place-items: center;
  border-radius: 50%;
  overflow: hidden;
}
/* a ripple off the ring each time the arrow lands, on the same 2.2s clock */
.cue__ring::after {
  content: ""; position: absolute; inset: -1px;
  border-radius: 50%;
  border: 1px solid var(--blue);
  opacity: 0;
  animation: cue-pulse 2.2s cubic-bezier(0.22, 1, 0.36, 1) infinite;
  pointer-events: none;
}
.cue__clip::before {
  content: ""; position: absolute; inset: 0; z-index: -1;
  background: var(--blue);
  transform: translateY(-101%);
  transition: transform 1.2s cubic-bezier(0.65, 0, 0.35, 1);
}
.cue__ring svg {
  width: 42%; height: 42%;
  fill: none; stroke: currentColor; stroke-width: 1.7;
  stroke-linecap: round; stroke-linejoin: round;
  animation: cue-drop 2.2s cubic-bezier(0.65, 0, 0.35, 1) infinite;
}
.cue--down:hover .cue__ring,
.cue--down:focus-visible .cue__ring {
  border-color: var(--blue); color: #fff;
  transition: border-color 0.3s var(--ease), color 0.3s var(--ease);
}
.cue--down:hover .cue__clip::before,
.cue--down:focus-visible .cue__clip::before { transform: translateY(0); transition-duration: 0.55s; }

/* in from above, a beat at rest, out through the floor, and round again */
@keyframes cue-drop {
  0%   { transform: translateY(-150%); opacity: 0; }
  30%  { transform: translateY(0);     opacity: 1; }
  62%  { transform: translateY(0);     opacity: 1; }
  100% { transform: translateY(150%);  opacity: 0; }
}
@keyframes cue-pulse {
  0%, 28% { transform: scale(1);    opacity: 0; }
  32%     { transform: scale(1);    opacity: 0.55; }
  80%     { transform: scale(1.55); opacity: 0; }
  100%    { transform: scale(1.55); opacity: 0; }
}

/* ==========================================================================
   SECTION 3 — WHY US
   ========================================================================== */

/* Three rows of number, detail, number. The pair on either side backs the
   sentence between them, so a row reads as one statement rather than as three
   cells. Middle column is narrower than the outer two because it holds prose
   and they hold a single word each. */
.tally {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1.08fr) minmax(0, 1fr);
  column-gap: clamp(1rem, 2.5vw, 2.75rem);
  row-gap: clamp(3.5rem, 8vw, 8rem);
  align-items: center;
  text-align: center;
  padding-bottom: clamp(4.5rem, 9vw, 9rem);
  /* the figures start a third of a viewport out to either side, which on a
     phone is well past the gutter. Same pairing as the services stage: clip
     sideways, leave the vertical axis alone. */
  overflow-x: clip;
  overflow-y: visible;
}
.stat { justify-self: center; max-width: 100%; }
.proof { justify-self: center; max-width: 34ch; }

.stat__num {
  font-family: var(--display); font-weight: 400;
  /* Sized to the column it sits in, not to the page. The outer columns are
     about a third of the container, and 300+ at 8.5vw measures roughly 250px
     against 350px of column at 1280: the biggest the figures can be without
     crowding the prose between them. */
  font-size: clamp(2.75rem, 8.5vw, 9rem);
  line-height: 0.88; letter-spacing: -0.02em;
  /* White rather than the blue accent: over the smoke these carry the chapter,
     and the accent tint reads as a caption colour at this size. */
  color: var(--paper);
  font-variant-numeric: tabular-nums;
}
.stat__label {
  margin-top: clamp(0.9rem, 1.6vw, 1.5rem);
  margin-inline: auto;
  max-width: 24ch;
  font-size: 0.9375rem; line-height: 1.45;
  letter-spacing: 0.06em; text-transform: uppercase;
  color: var(--paper);
}

.proof { max-width: 44ch; }
/* Deliberately small. The figures either side are the contrast in this row and
   the prose is the footnote to them; at clamp(1.35rem, 2vw, 2rem) the heading
   was reading at nearly the same weight as the numerals and the row flattened
   into three equal things. */
.proof h3 {
  font-family: var(--display); font-weight: 400;
  font-size: clamp(1rem, 1.2vw, 1.25rem); line-height: 1.2;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  margin-bottom: 0.7rem;
}
.proof p {
  color: var(--paper);
  font-size: 0.875rem; line-height: 1.6;
}

/* ==========================================================================
   SECTION 3 — WHY US
   The video opens outward. .why__window carries a clip-path that starts as a
   small bubble and animates to inset(0), so the video and the heading inside
   it are revealed rather than scaled: nothing distorts, because the inner
   layers stay at viewport size the whole time and only the mask changes.
   The stage is sticky for the whole section, so once the window is open the
   video is the background the stats scroll over.
   ========================================================================== */
.why { position: relative; padding-block: 0; }

/* White ONLY where the window actually opens. The point of the white is to let
   you see a dark panel expand out of it; where there is no expansion there is
   nothing to see and the section's own white type ends up on white. That is
   exactly what happened: on a phone, and under reduced motion, the window is
   static and the heading, lead, stats and proof copy all sit outside it, so the
   whole chapter measured 1.0 contrast and was invisible. Same condition as the
   animation itself, so the two can never drift apart. */
@media (min-width: 901px) and (prefers-reduced-motion: no-preference) {
  .why { background: var(--paper); }
}

.why__stage {
  position: sticky; top: 0;
  height: 100vh; height: 100svh;
  z-index: 0;
}

.why__window {
  position: absolute; inset: 0;
  overflow: hidden;
  /* animated from js/main.js. Custom properties rather than interpolating the
     whole clip-path string, which is far more fragile.

     The resting window is deliberately large: 56% of the height by 64% of the
     width, over half the screen. It has to hold the chapter heading legibly
     before a single pixel of scroll, because that heading is the first thing
     the chapter says. At the old 33/31 it held the words but the animation
     opened them at a quarter opacity behind a masked climb, so in practice you
     scrolled past a dark rectangle with nothing readable in it. */
  --wy: 22%;
  --wx: 18%;
  --wr: 10px;
  clip-path: inset(var(--wy) var(--wx) round var(--wr));
  will-change: clip-path;
}

.why__video {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: cover;
}

/* strong enough that the stats stay legible once they scroll over the video */
/* Measured, not guessed. Liquid Smoke is almost entirely black: mean relative
   luminance 0.008, brightest pixel 0.147. White type clears 5.0:1 against that
   brightest pixel with no veil at all, so the old 52 to 78 percent ramp was
   bought for nothing and smothered the blue bloom the clip exists for. This
   keeps the worst case near 5.8:1, still above AA for body copy, and lets the
   colour through. Re measure if the clip is ever swapped. */
.why__veil {
  position: absolute; inset: 0;
  background: radial-gradient(82% 72% at 50% 42%,
    rgba(10, 10, 10, 0.18) 0%,
    rgba(10, 10, 10, 0.34) 55%,
    rgba(10, 10, 10, 0.52) 100%);
}

/* Hangs over everything in the chapter: the white section before the window
   opens, and the smoke once it has. The clip's own composition puts the blooms
   in the lower corners and leaves the middle empty, so the heading reads
   straight through it. */
.why__flowers {
  position: absolute; inset: 0;
  z-index: 2;
  pointer-events: none;
  overflow: hidden;
  /* The blooms paint OVER the chapter's heading, and `.why__window` has a
     clip-path, which makes it a stacking context: nothing inside it can be
     lifted above a sibling of the window. So instead of fighting the order,
     the centre of this layer is masked away. The blooms keep the margins,
     where they belong, and the type is never covered rather than merely
     dimmed. */
  -webkit-mask-image: radial-gradient(62% 56% at 50% 46%,
    rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0) 54%, #000 88%);
  mask-image: radial-gradient(62% 56% at 50% 46%,
    rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0) 54%, #000 88%);
}
.why__flowers-video {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: cover;
}
/* Set by main.js once it has checked whether the browser really honours the
   alpha channel, rather than trusting that it can play the file. */
.why__flowers.is-keyed .why__flowers-video { mix-blend-mode: normal; }
.why__flowers.is-flat .why__flowers-video { mix-blend-mode: multiply; }

.why__intro {
  position: absolute; inset: 0;
  display: flex; flex-direction: column;
  align-items: center; justify-content: center;
  text-align: center;
  gap: clamp(1.5rem, 3vw, 2.5rem);
  padding-inline: var(--gut);
}
/* A seat for the type. The ink bloom is centred and so is this heading, so the
   words land on the brightest part of the clip. Soft edged and local, so the
   smoke still reads everywhere around it. */
.why__intro::before {
  content: ""; position: absolute; inset: 0;
  background: radial-gradient(62% 46% at 50% 50%,
    rgba(6, 6, 10, 0.24) 0%,
    rgba(6, 6, 10, 0.14) 52%,
    transparent 76%);
  pointer-events: none;
}
.why__intro > * { position: relative; z-index: 1; }

/* This section's background is now royal blue ink, so the royal blue accent
   cannot also be the type colour: `--blue-lit` and the smoke measure 0.190 and
   0.179 relative luminance, near identical and the same hue, which put the
   heading accent word at 1.22:1 and the stat numerals at 1.81:1. Darkening
   cannot rescue a foreground that dim; it only reaches 3.5:1 at 90 percent
   black, by which point there is no clip left to look at. So the accent is
   lifted to a pale tint for this section only, scoped the same way `.invert`
   scopes its tokens. Re measure if the clip is ever swapped. */
.why { --accent: #AEB9FF; }
/* Every word in the chapter is white: the eyebrow and its rule, and the accent
   word in the heading, which would otherwise take the pale tint above. The
   stat labels and the proof copy are set to paper in their own rules. */
.why .eyebrow { color: var(--paper); }
.why .eyebrow i { background: var(--paper); }
.why__title em { color: var(--paper); }
.why__title { max-width: 16ch; }
/* Paper, not the muted token. This line sits on the smoke rather than on a flat
   ground, and muted over that measured close enough to the clip's own mid tones
   that it read as part of the video. White clears it at 5.8:1 in the worst
   case. Sized at --fs-body, the same step the service cards use, so it reads as
   a supporting line under the heading rather than competing with it. */
.why__lead {
  max-width: 52ch;
  font-size: var(--fs-body); line-height: 1.6;
  color: var(--paper);
}

/* the numbers scroll over the opened video */
.why__body {
  position: relative; z-index: 2;
  padding-top: 88vh;
  /* the section's own padding would sit outside the sticky range, so the tail
     belongs to the body instead and stays covered by the video */
  padding-bottom: var(--sec);
}

/* ==========================================================================
   SECTION 4 — PORTFOLIO
   ========================================================================== */
.work { position: relative; }

.work__frame {
  height: 100vh; height: 100svh;
  display: flex; flex-direction: column; justify-content: center;
  gap: clamp(2rem, 4vh, 3.5rem);
  padding-block: clamp(4.25rem, 9.5vh, 8rem) clamp(2rem, 4.5vh, 4rem);
  overflow: hidden;
}

.work__head {
  display: flex; align-items: flex-end; justify-content: space-between;
  gap: 2rem;
}
.work__title { margin-top: clamp(1rem, 2vw, 1.75rem); }

/* The rolling heading, after GSAP's rolling text demo. Two lines sit in the
   same grid cell, one over the other, and every letter of each pivots on its
   own round a point 0.62em behind the type. main.js writes each letter's
   angle as `--a`, in degrees: 0 is square on, and past a quarter turn either
   way it is edge on or looking away, where backface-visibility drops it. The
   lines are a quarter turn apart, so as one line's letters tip away over the
   top the other's come up from underneath. The cell is as wide as the longer
   line. Without the script the span is plain text, the first line. */
.roll { display: inline-grid; white-space: nowrap; perspective: 3em; }
.roll__line { grid-area: 1 / 1; transform-style: preserve-3d; }
.roll__c {
  display: inline-block;
  -webkit-backface-visibility: hidden; backface-visibility: hidden;
  transform-origin: 50% 50% -0.62em;
  transform: rotateX(calc(var(--a, 0) * 1deg));
}
.cue--side i { transform: none; }

/* Never shrinks below the cards it holds. As a flex child of a frame that is
   exactly a viewport tall it was allowed to, and on a short screen it cut 24px
   off the bottom of every card: the rounded corners and the border just
   stopped. The frame centres its content, so any overflow is split top and
   bottom rather than all landing on the cards. */
.work__viewport { overflow: hidden; flex-shrink: 0; }
.work__track {
  display: flex; align-items: center;
  gap: clamp(1.5rem, 3vw, 4rem);
  padding-inline: var(--gut);
  width: max-content;
  will-change: transform;
}

/* The slot carries the scroll entrance and the card inside it the pointer
   tilt. They cannot share an element: both write rotationX. */
.panel-slot { flex: 0 0 auto; }

/* A rounded card: the copy on the left, the featured image on the right.
   Sized so a whole card sits on screen at once rather than being read through
   a letterbox. */
.panel {
  flex: 0 0 auto;
  width: min(82vw, 1180px);
  display: grid;
  grid-template-columns: minmax(0, 0.92fr) minmax(0, 1.08fr);
  align-items: center;
  gap: clamp(1.5rem, 3.5vw, 4rem);
  /* Every vertical measure in this card takes the smaller of a width term and
     a height term. The frame it lives in is exactly one viewport tall, and the
     card's height comes from its text column, so on a short laptop screen
     sizes driven by width alone ran it off the bottom: 589px of card against
     478px of frame at 1440 by 780, with the list and the link cut off. */
  padding: clamp(1.25rem, min(3vw, 4vh), 3.25rem);
  /* One height for every card, so the row does not step up and down as it
     scrolls past. The copy lengths differ; the media boxes do not. */
  min-height: clamp(18rem, min(38vw, 58vh), 32rem);
  /* That floor used to follow width alone, so on a short screen it held every
     card at 512px whatever was in it: at 1366 by 660 the text needed 281px and
     the card still came out 508, running 88px off the bottom. */
  border-radius: clamp(20px, 2.2vw, 36px);
  border: 1px solid var(--rule);
  background:
    radial-gradient(120% 140% at 12% 0%, rgba(36, 60, 212, 0.14), transparent 58%),
    linear-gradient(160deg, rgba(250, 250, 248, 0.055), rgba(250, 250, 248, 0.018));
  overflow: hidden;
}

.panel__label {
  font-size: 0.7rem; letter-spacing: 0.2em; text-transform: uppercase;
  color: var(--muted);
}

.panel__name {
  margin-top: clamp(0.75rem, min(2.2vw, 2.4vh), 2rem);
  font-size: clamp(1.5rem, min(3.1vw, 5vh), 3.1rem);
  line-height: 1.02;
  text-transform: uppercase;
  text-wrap: balance;
}
/* The result, under the name of what was built. Accent coloured and set a step
   above the body, so a skim of the three cards reads as three outcomes. */
.panel__result {
  margin-top: clamp(0.5rem, min(1vw, 1.3vh), 0.9rem);
  font-size: clamp(1rem, 1.15vw, 1.125rem);
  font-weight: 600;
  letter-spacing: -0.005em;
  color: var(--accent);
}
.panel__copy {
  margin-top: clamp(0.6rem, min(1.4vw, 1.7vh), 1.1rem);
  line-height: 1.55;
  max-width: 46ch;
  color: var(--muted);
}

.panel__list {
  margin-top: clamp(0.9rem, min(3vw, 2.6vh), 2.4rem);
  list-style: none;
  display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 0.7rem clamp(1rem, 2vw, 2rem);
}
.panel__list li {
  position: relative;
  padding-left: 1.4rem;
  font-size: 0.78rem; letter-spacing: 0.1em; text-transform: uppercase;
  color: rgba(250, 250, 248, 0.82);
}
.panel__list li::before {
  content: ""; position: absolute; left: 0; top: 0.52em;
  width: 0.8rem; height: 1px; background: var(--accent);
}

/* Matches the card's own radius, less the padding, so the corner reads as one
   curve rather than two fighting each other. */
.panel__media {
  position: relative;
  border-radius: clamp(12px, 1.4vw, 22px);
  overflow: hidden;
  aspect-ratio: 3 / 2;
  /* On a short screen the picture gives up height before the card overflows.
     The cutouts inside are object-fit: contain, so a shorter box letterboxes
     the image rather than cropping it. No effect at 780px tall and up. */
  max-height: 52vh;
  background: rgba(8, 9, 14, 0.6);
}
.panel__media img {
  width: 100%; height: 100%;
  object-fit: cover;
  transition: transform 0.9s var(--ease);
}
.panel:hover .panel__media img { transform: scale(1.05); }

/* A cut out subject rather than a framed picture. The background is removed in
   the asset itself, as a real alpha channel, so this needs no blend mode and
   works on any surface. `mix-blend-mode: screen` did the job visually but was
   only ever dropping black against black, and the drop-shadow below then had no
   silhouette to follow and drew a rectangle, which is the border that showed. */
.panel__media--cutout {
  background: none;
  border-radius: 0;
  overflow: visible;
  /* Keeps the SAME 3 by 2 reservation as a framed card. Letting it size to the
     image instead left the cards 40px apart in height, which reads as the row
     not lining up as it scrolls past. */
  position: relative;
}
/* Taken out of the sizing flow entirely. A percentage height against a parent
   that is itself sized by aspect-ratio falls back to auto, so the image's own
   proportions were driving the box and it came out 32px taller than a framed
   one. Absolute children cannot do that. */
.panel__media--cutout img {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: contain;
  /* Now follows the real outline, because there is real alpha to follow. */
  filter: drop-shadow(0 26px 46px rgba(16, 34, 140, 0.45));
}
.panel:hover .panel__media--cutout img { transform: scale(1.03); }

/* A royal blue pill, the site's button at a card's scale. The fill on hover
   is paper rather than the ink .btn--blue uses elsewhere, because this sits
   on a dark card and an ink fill would all but vanish into it. Quick in, slow
   out, from .btn. */
.panel__link {
  margin-top: clamp(1rem, min(2.6vw, 2.6vh), 2.25rem);
  padding: 0.85rem 1.5rem;
  gap: 0.7rem;
  font-size: 0.875rem;
}
.panel__link svg { width: 1rem; height: 1rem; }
.panel__link.btn--blue::before { background: var(--paper); }
.panel__link.btn--blue:hover { color: var(--ink); }

.work__progress {
  max-width: var(--maxw); width: 100%;
  margin-inline: auto; padding-inline: var(--gut);
}
.work__progress i {
  display: block; height: 1px; width: 100%;
  background: var(--rule); position: relative;
}
.work__progress i::after {
  content: ""; position: absolute; inset: 0;
  background: var(--blue-lit);
  transform: scaleX(var(--p, 0)); transform-origin: left;
}

/* ==========================================================================
   SECTION 5 — SOCIAL PROOF
   ========================================================================== */
/* Every .shard is a full pane sized box clipped to its polygon, so a scattered
   shard overflows the layout by its translation even when the visible glass is
   nowhere near the edge. That was pushing the page 46px wide and giving it a
   horizontal scrollbar. Clip at the section, which spans the viewport, so the
   scatter still reads to the screen edges but cannot scroll the page sideways.
   `clip` rather than `hidden` so it creates no scroll container and leaves the
   vertical axis alone. The dialog lives at body level, outside this. */
.voices { position: relative; padding-block: var(--sec); overflow-x: clip; }

/* The Clients chapter sweeps up over Selected Work rather than following it.
   Same condition as the carousel's pin, because the whole effect depends on it:
   while the work frame is pinned and fixed, this chapter is pulled a viewport
   up the document, so it reaches the top of the screen exactly as the pin
   releases. The last viewport of that pin is spent watching it rise.

   A negative margin, deliberately, not a transform. This section contains the
   mirror's own pinned sequence, and a transformed ancestor becomes the
   containing block for the `position: fixed` a pin uses, which would break it.
   Margins move the layout and leave that alone.

   Change the 100vh here and change the sweep length in main.js with it: the two
   are the same distance measured from opposite ends. */
@media (min-width: 901px) and (prefers-reduced-motion: no-preference) {
  /* Nothing above the wall. The chapter's own top padding, its eyebrow and the
     stage's margin put about 360px of flat ink above the brick at 1280, and
     that band was what rose over Selected Work first: the sweep read as a black
     bar arriving rather than as the picture arriving. Stripped here, and the
     eyebrow is hung over the wall instead, so the leading edge of the sweep is
     the brick itself.

     Scoped, because below the breakpoint there is no wall at all and the
     eyebrow is an ordinary line of type at the top of an ordinary chapter. */
  /* and nothing after it either: the dome hands straight to the closing
     chapter, so the tail padding would be a band of ink between two whites */
  .voices { padding-block: 0; }
  .shardstage { margin-top: 0; }
  .voices__eyebrow {
    position: absolute;
    top: clamp(1.25rem, 4.5vh, 3.25rem);
    left: var(--gut);
    z-index: 31;
    margin-bottom: 0;
  }

  .voices {
    margin-top: -100vh;
    z-index: 4;                      /* over the pinned frame, which is at auto */
    background: var(--ink);          /* it has to actually cover, not overlap */
    /* the leading edge, so it reads as a sheet sliding over rather than a cut */
    box-shadow: 0 -2px 0 rgba(250, 250, 248, 0.10), 0 -50px 90px rgba(0, 0, 0, 0.65);
  }
}
.voices__eyebrow { margin-bottom: clamp(3rem, 6vw, 5.5rem); }


/* --- Shard cluster ---------------------------------------------------------
   Twelve pieces of one tessellated pane. Each .shard is the full size of the
   pane and is clipped to its own polygon, so with every transform at identity
   the twelve pieces tile the rectangle exactly and read as a single sheet of
   glass. Scattering is therefore just a transform per shard, and assembling is
   the same transform run back to zero. Polygons, scatter offsets and the text
   positions are all generated in js/shards.js.
   --------------------------------------------------------------------------- */
/* Off screen but readable. Each shard shows a trimmed quote and carries the
   full one here, so the button's accessible name is the whole testimonial. */
.vh {
  position: absolute; width: 1px; height: 1px;
  margin: -1px; padding: 0; border: 0;
  overflow: hidden; white-space: nowrap;
  clip-path: inset(50%);
}

.shardstage {
  position: relative;
  margin-top: clamp(3rem, 7vw, 6rem);
}

/* The pinned frame. Full bleed and exactly one viewport tall, so locking it in
   place reads as the page holding still rather than a panel sticking. Its own
   `clip` is what stops a thrown fragment widening the document: the frame spans
   the viewport, so clipping here is clipping at the screen edge. */
.shardstage__frame {
  position: relative;
  width: 100%;
  height: 100vh; height: 100svh;
  display: grid;
  place-items: center;
  overflow: clip;
}
/* Glass over a flat black section has nothing to refract and the blur is
   wasted. This puts something behind the pane worth bending. */
.shardstage::before {
  content: ""; position: absolute; z-index: 0;
  left: 50%; top: 50%;
  width: min(120%, 1100px); aspect-ratio: 1.5 / 1;
  transform: translate(-50%, -50%);
  background: radial-gradient(ellipse at 50% 50%, rgba(36, 60, 212, 0.22), rgba(36, 60, 212, 0.05) 46%, transparent 70%);
  pointer-events: none;
}

/* The plain list is what ships in the HTML and what shows with JS off.
   shards.js hides it once it has built the pane from it. */
.voicelist {
  list-style: none;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(17rem, 1fr));
  gap: clamp(1.5rem, 3vw, 2.5rem);
}
.voicelist blockquote { color: var(--paper); margin-bottom: 0.75rem; }
.voicelist cite { font-style: normal; color: var(--muted); font-size: 0.8rem; letter-spacing: 0.08em; text-transform: uppercase; }
.voicelist a { display: block; margin-top: 0.4rem; color: var(--accent); font-size: 0.8rem; }
.shardstage.is-built .voicelist { display: none; }

/* The heading, split across opposite corners of the stage and funnelling
   inward as you scroll, the same move the shards make. Sits above the wall and
   below nothing: the picture is centred and these are at the margins, so they
   never cross it. */
.pull { position: absolute; inset: 0; z-index: 30; pointer-events: none; }
/* NOT in `ch`. That unit resolves against this element's own font size, which
   is the inherited body size, not the 57px display type inside it: the cap came
   out at 242px while the word CALENDAR needs 321px and simply overflowed. Sized
   in vw so it stays clear of the picture in the middle at every width. */
.pull__side { position: absolute; }
.pull__side .display {
  /* A starting value only. shards.js overwrites this with a size solved from
     the box each half actually gets, because that box depends on how wide the
     picture ended up on screen. */
  font-size: clamp(2rem, 4.9vw, 5rem);
  line-height: 1.0;
  text-transform: uppercase;
  color: var(--paper);
  text-shadow: 0 2px 30px rgba(0, 0, 0, 0.9);
}
/* `left` and `width` are written by shards.js, measured off the mirror so each
   half keeps a set distance from the picture at any viewport. */
/* Hard into the corners: top left and bottom right of the screen, not tucked
   either side of the picture. */
.pull__a { top: clamp(1.25rem, 4.5vh, 3.25rem); text-align: left; }
.pull__b { bottom: clamp(1.25rem, 4.5vh, 3.25rem); text-align: right; }
.pull__b em { font-style: normal; color: var(--accent); }

.pull__hint {
  margin-top: clamp(0.9rem, 2vh, 1.5rem);
  max-width: 26ch;
  font-size: clamp(0.72rem, 0.9vw, 0.86rem);
  letter-spacing: 0.04em;
  line-height: 1.5;
  color: rgba(250, 250, 248, 0.68);
  text-shadow: 0 2px 16px rgba(0, 0, 0, 0.9);
}

/* Rides with the pointer while a shard is under it. Never takes the pointer
   itself, or it would sit between the cursor and the shard it is advertising. */
.hintbubble {
  position: absolute; left: 0; top: 0;
  z-index: 50;
  pointer-events: none;
  opacity: 0;
  will-change: transform, opacity;
}
.hintbubble span {
  display: block;
  transform: translate(1.15rem, 1.15rem);
  padding: 0.5rem 0.95rem;
  border-radius: 999px;
  white-space: nowrap;
  font-size: 0.72rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: #fff;
  background: linear-gradient(140deg, rgba(60, 86, 255, 0.95), rgba(28, 44, 190, 0.95));
  border: 1px solid rgba(160, 180, 255, 0.55);
  box-shadow:
    0 0 26px rgba(60, 86, 255, 0.75),
    0 0 60px rgba(36, 60, 212, 0.45),
    inset 0 1px 0 rgba(255, 255, 255, 0.35);
}

/* The wall the picture hangs on. This is the destination for the shards, not
   a backdrop behind them. */
.shardstage__wall {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: cover;
  z-index: 0;
  pointer-events: none;
}

/* The mirror fills the frame's aperture. Its box is written in pixels by
   shards.js, solved from wherever the cover-fitted wall actually put that
   aperture on screen. Its pieces are positioned from the Blender render's own
   geometry, so at rest they tile it exactly. */
.mirror {
  position: absolute;
  z-index: 2;
  transform-style: preserve-3d;
}

/* The loose field behind. Blender renders it on black; `screen` throws the
   black away and keeps only the fragments, so no alpha channel is needed and
   the file stays a small JPEG. Deliberately NOT isolated: screen needs the
   page's near black behind it, and an isolated group would hand it a
   transparent backdrop and paint the black as black. */
.shardstage__debris {
  position: absolute; inset: -20% -10%;
  /* Explicit, not negative. At -1 this painted behind the section's own
     background and vanished; at auto it would paint over the glass, because
     shards.js appends it after the stage. */
  z-index: 0;
  background-repeat: no-repeat;
  background-position: center;
  background-size: cover;
  mix-blend-mode: screen;
  opacity: 0.7;
  pointer-events: none;
  will-change: transform, opacity;
}

/* One piece of the mirror. The wrapper carries the transform and the glow: the
   button inside is clipped to the glass, and a filter inside a clip cannot
   spill past the very outline it is meant to surround. */
.mpiece {
  position: absolute;
  will-change: transform;
  filter:
    drop-shadow(0 0 calc(var(--lit, 0) * 26px) rgba(84, 104, 255, calc(var(--lit, 0) * 0.9)))
    drop-shadow(0 0 calc(var(--lit, 0) * 9px) rgba(200, 214, 255, calc(var(--lit, 0) * 0.75)));
}

.frag {
  display: block;
  position: relative;
  width: 100%; height: 100%;
  margin: 0; padding: 0;
  border: 0; background: none;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.frag:focus { outline: none; }

.frag__img {
  display: block;
  width: 100%; height: 100%;
  object-fit: fill;
  pointer-events: none;
  filter: brightness(calc(1 + var(--lit, 0) * 0.45))
          saturate(calc(1 + var(--lit, 0) * 0.3));
}
/* Catches the light across the piece under the pointer, so a hover reads as
   the mirror turning rather than as a panel highlighting. */
.frag__sheen {
  position: absolute; inset: 0;
  background: linear-gradient(118deg,
    rgba(255, 255, 255, 0) 32%,
    rgba(190, 210, 255, 0.34) 50%,
    rgba(255, 255, 255, 0) 68%);
  opacity: var(--lit, 0);
  pointer-events: none;
}

/* Sits on the wall just under the picture, rather than over the glass. Its
   position is written in pixels by shards.js, from the same solved cover box
   as the aperture, so it tracks the frame at every viewport. */
.mirror__cta {
  position: absolute;
  /* Centring lives in GSAP as xPercent, NOT here: the ticker writes `y` to this
     element every frame, and a CSS translate is read out of the computed matrix
     as pixels and then overwritten on the first write. */
  z-index: 40;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: clamp(0.9rem, 2vw, 1.4rem);
  text-align: center;
  opacity: 0;
  pointer-events: none;
}
.mirror__cta.is-live { pointer-events: auto; }
/* A seat for the call to action. A mirror has bright facets by definition, and
   the label measured 1.03:1 where it crossed one: near white type on a near
   white reflection. Soft edged and local, so the mirror still reads around it. */
.mirror__cta::before {
  content: ""; position: absolute;
  left: 50%; top: 50%; transform: translate(-50%, -50%);
  width: min(30rem, 72%); aspect-ratio: 2.4 / 1;
  background: radial-gradient(closest-side,
    rgba(3, 5, 14, 0.90) 0%,
    rgba(3, 5, 14, 0.72) 48%,
    rgba(3, 5, 14, 0) 82%);
  pointer-events: none;
}
.mirror__cta > * { position: relative; }
.mirror__eyebrow {
  font-size: 0.7rem; letter-spacing: 0.22em; text-transform: uppercase;
  color: rgba(250, 250, 248, 0.72);
  text-shadow: 0 2px 14px rgba(0, 0, 0, 0.7);
}


/* --- Expanded testimonial -------------------------------------------------- */
.sheet { position: fixed; inset: 0; z-index: 90; }
.sheet[hidden] { display: none; }
.sheet__scrim { position: absolute; inset: 0; background: rgba(4, 4, 6, 0.74); backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px); opacity: 0; }
.sheet__panel {
  position: absolute; right: 0; top: 0; bottom: 0;
  width: min(34rem, 100%);
  padding: clamp(2.5rem, 5vw, 4.5rem) clamp(1.5rem, 4vw, 3.5rem);
  display: flex; flex-direction: column; justify-content: center;
  background:
    radial-gradient(90% 55% at 100% 0%, rgba(84, 104, 255, 0.28), transparent 70%),
    linear-gradient(200deg, rgba(24, 34, 112, 0.5), rgba(9, 9, 12, 0.98) 46%);
  border-left: 1px solid rgba(36, 60, 212, 0.55);
  box-shadow: -40px 0 120px rgba(0, 0, 0, 0.55);
  overflow-y: auto;
}
.sheet__close {
  position: absolute; top: clamp(1.25rem, 3vw, 2rem); right: clamp(1.25rem, 3vw, 2rem);
  width: 2.75rem; height: 2.75rem; display: grid; place-items: center;
  background: none; border: 1px solid var(--rule); border-radius: 50%;
  color: var(--paper); cursor: pointer;
  transition: border-color 0.35s var(--ease), background 0.35s var(--ease);
}
.sheet__close:hover, .sheet__close:focus-visible { border-color: var(--accent); background: rgba(36, 60, 212, 0.18); }
.sheet__close svg { width: 1rem; height: 1rem; fill: none; stroke: currentColor; stroke-width: 1.5; stroke-linecap: round; }
/* The label over the quote, in the page's own eyebrow voice. */
.sheet__eyebrow {
  display: flex; align-items: center; gap: 0.75rem;
  margin: 0 0 clamp(1.25rem, 2.4vw, 1.9rem);
  font-size: var(--fs-micro); letter-spacing: 0.2em; text-transform: uppercase;
  color: var(--muted);
}
.sheet__eyebrow i { width: clamp(1.5rem, 3vw, 2.5rem); height: 1px; background: var(--accent); }

/* Curly quotes, the opening one hung in the margin so the first letter of the
   quote lines up with every line under it rather than being pushed in by the
   mark. text-indent rather than hanging-punctuation, which only Safari has. */
.sheet__quote {
  margin: 0;
  font-family: var(--display); font-size: clamp(1.4rem, 2.6vw, 2.1rem);
  line-height: 1.22; text-transform: uppercase; color: var(--paper);
  text-indent: -0.42em;
}
.sheet__quote::before { content: "\201C"; color: var(--accent); }
.sheet__quote::after  { content: "\201D"; color: var(--accent); }

/* The byline: who said it, set apart from the quote by a hairline. */
.sheet__byline {
  display: flex; align-items: center; gap: 1rem;
  margin-top: clamp(1.75rem, 3.4vw, 2.5rem);
  padding-top: clamp(1.25rem, 2.4vw, 1.75rem);
  border-top: 1px solid var(--rule);
}
.sheet__avatar {
  flex: 0 0 auto;
  width: 3.4rem; height: 3.4rem;
  border-radius: 50%;
  display: grid; place-items: center;
  font-family: var(--display); font-size: 1.15rem; letter-spacing: 0.04em;
  color: var(--paper);
  background:
    radial-gradient(circle at 30% 25%, rgba(84, 104, 255, 0.95), rgba(27, 46, 203, 0.95) 60%, rgba(12, 17, 64, 1));
  background-size: cover; background-position: center;
  box-shadow: 0 0 0 1px rgba(250, 250, 248, 0.14), 0 0 0 5px rgba(36, 60, 212, 0.18);
}
/* a real photo replaces the gradient outright rather than sitting on top of it */
.sheet__avatar.has-photo { background-color: var(--ink); }
.sheet__id { min-width: 0; }
.sheet__who {
  margin: 0;
  font-size: 0.95rem; font-weight: 500; letter-spacing: 0.02em;
  color: var(--paper);
}
.sheet__role {
  margin: 0.2rem 0 0;
  font-size: 0.72rem; letter-spacing: 0.16em; text-transform: uppercase;
  color: var(--muted);
}
.sheet__role[hidden] { display: none; }
/* A button rather than a line of link text: it is the one thing to do from this
   panel, so it should look like something to press. */
.sheet__link {
  margin-top: clamp(1.5rem, 3vw, 2.25rem);
  display: inline-flex; align-items: center; gap: 0.6rem;
  width: max-content;
  padding: 0.8rem 1.4rem;
  border: 1px solid var(--rule);
  border-radius: 999px;
  color: var(--paper);
  font-size: 0.8rem; letter-spacing: 0.1em; text-transform: uppercase;
  transition: background 0.4s var(--ease), color 0.4s var(--ease), border-color 0.4s var(--ease);
}
.sheet__link[hidden] { display: none; }
.sheet__link svg { width: 0.85rem; height: 0.85rem; fill: none; stroke: currentColor; stroke-width: 1.6; stroke-linecap: round; transition: transform 0.4s var(--ease); }
.sheet__link:hover,
.sheet__link:focus-visible { background: var(--paper); border-color: var(--paper); color: var(--ink); }
.sheet__link:hover svg { transform: translate(0.2rem, -0.2rem); }

/* --- Below the pane breakpoint ---------------------------------------------
   With motion, a phone gets the mirror too: shards.js builds it and lays it
   out for a tall screen, and the phone block further down styles what differs.
   Under reduced motion it is not built, and the section is the heading and a
   row of quote cards instead. Everything for that version is scoped to a stage
   that is NOT built, so the two can never both apply. */
@media (max-width: 900px) {
  /* No wall and no shards, so the frame carries only the heading and lays it
     out as ordinary stacked text. */
  .shardstage:not(.is-built) .shardstage__frame { display: block; height: auto; }
  /* The heading as one block at the page's own margins. It had been capped at
     18ch, and ch here resolves against the body size, not the display size
     inside, so the two lines came out 182px wide, centred, eight words tall. */
  .shardstage:not(.is-built) .pull { position: static; margin: 0; padding-inline: var(--gut); }
  .shardstage:not(.is-built) .pull__side { position: static; max-width: none; text-align: left; }
  .shardstage:not(.is-built) .pull__side .display { font-size: clamp(2.1rem, 9.6vw, 3.5rem); }
  .shardstage:not(.is-built) .pull__b { margin-top: 0.2em; }
  /* nothing to click in that version, so the instruction would be a lie */
  .shardstage:not(.is-built) .pull__hint { display: none; }

  /* The testimonials as a row of cards you swipe through, rather than twenty
     quotes stacked three and a half thousand pixels deep. The next card always
     shows at the edge, so the row reads as one to swipe. Equal heights, with
     the attribution and the link on each card's floor. */
  .shardstage:not(.is-built) .voicelist {
    display: flex;
    gap: 0.875rem;
    margin-top: clamp(2rem, 7vw, 3rem);
    padding: 0.25rem var(--gut);
    scroll-padding-inline: var(--gut);
    overflow-x: auto; overflow-y: hidden;
    overscroll-behavior-x: contain;
    scroll-snap-type: x mandatory;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
  }
  .voicelist::-webkit-scrollbar { display: none; }
  .shardstage:not(.is-built) .voicelist li {
    flex: 0 0 min(82vw, 24rem);
    scroll-snap-align: start;
    display: flex; flex-direction: column; align-items: flex-start;
    padding: 1.5rem 1.4rem 1.4rem;
    border-radius: 22px;
    border: 1px solid var(--rule);
    background:
      radial-gradient(120% 90% at 0% 0%, rgba(36, 60, 212, 0.2), transparent 62%),
      linear-gradient(160deg, rgba(250, 250, 248, 0.06), rgba(250, 250, 248, 0.02));
  }
  .shardstage:not(.is-built) .voicelist li::before {
    content: "\201C";
    display: block; height: 0.5em;
    margin-bottom: 0.5rem;
    font-family: var(--display); font-size: 3.5rem; line-height: 0.9;
    color: var(--accent);
  }
  .shardstage:not(.is-built) .voicelist blockquote { margin: 0 0 1.25rem; font-size: 1.0625rem; line-height: 1.55; }
  .shardstage:not(.is-built) .voicelist cite { margin-top: auto; }
  .shardstage:not(.is-built) .voicelist a {
    display: inline-flex; align-items: center;
    margin-top: 0.9rem;
    padding: 0.6rem 1.05rem;
    border: 1px solid var(--rule); border-radius: 999px;
    color: var(--paper); font-size: 0.8125rem;
  }

  /* No hover on a phone, so the label that rides with the pointer has nothing
     to ride with. */
  .hintbubble { display: none; }

  /* A megabyte and a half of keyed video for decoration is not a fair trade on
     a phone, and roughly half this brand's traffic arrives in an in app webview
     where the alpha channel is the least reliable. */
  .why__flowers { display: none; }
}

/* ==========================================================================
   SECTION 6 — FINAL CTA (the single inverted block)
   ========================================================================== */
.cta { position: relative; overflow: hidden; background: none; }
.cta__glow {
  position: absolute; left: 50%; top: 36%;
  width: min(140vw, 1500px); aspect-ratio: 1.6 / 1;
  transform: translate(-50%, -50%);
  background: radial-gradient(closest-side,
    rgba(27, 46, 203, 0.16) 0%,
    rgba(27, 46, 203, 0.05) 42%,
    rgba(27, 46, 203, 0) 72%);
  pointer-events: none;
}

.cta__inner {
  position: relative; z-index: 1;
  /* Set against the arch above it, not against the section gap. The curve is
     22vh deep at the centre but only 19px at the width the heading actually
     occupies, so the type needs far less clearance than the arc's depth
     suggests: 14vh puts air above the eyebrow when the section has landed and
     still has it on screen by a quarter of the way up. Written in the base
     rule, not the media query, because the shorthand here would otherwise
     override a longhand set earlier in the file. The bottom keeps its full
     measure, since the footer is a different colour again. */
  padding-block: clamp(6rem, 14vh, 11rem) var(--sec);
  display: flex; flex-direction: column; align-items: center;
  text-align: center;
  gap: clamp(1.75rem, 3vw, 2.75rem);
}
.cta__title { max-width: 21ch; }
/* holds the reveal so the button's own transform is free for the magnet; flex
   so the inline button does not pick up a line box's descender below it */
.cta__action { display: flex; }
.cta__body {
  max-width: 48ch; font-size: var(--fs-lead); line-height: 1.5;
  color: var(--muted);
}
.cta__fine {
  font-size: var(--fs-micro); letter-spacing: 0.16em;
  text-transform: uppercase; color: rgba(10, 10, 10, 0.42);
}

/* The one royal blue field on the page, and the last thing on it. White type
   on #2438E0 measures 7.69:1; the muted token has to climb with the ground the
   same way the lit service cards do, because paper at 58 percent over this blue
   is only 3.44:1. At 78 it is 5.07:1 and the small print clears AA. */
.foot {
  position: relative;
  z-index: 1;
  overflow: hidden;
  background: var(--blue);
  color: var(--paper);
  --muted: rgba(250, 250, 248, 0.78);
  --rule: rgba(250, 250, 248, 0.22);
  padding-top: clamp(3.5rem, 7vw, 6.5rem);
}

.foot__inner {
  position: relative; z-index: 1;
  display: grid;
  /* three columns of type and the emblem on the end, sized to itself rather
     than to a fraction so the ring never squeezes into an ellipse */
  grid-template-columns: minmax(0, 1.25fr) minmax(0, 0.75fr) minmax(0, 0.95fr) auto;
  gap: clamp(2rem, 4vw, 4rem);
  align-items: start;
  padding-bottom: clamp(3rem, 6vw, 5.5rem);
}

.foot__eyebrow {
  display: flex; align-items: center; gap: 0.75rem;
  font-size: var(--fs-micro); letter-spacing: 0.18em;
  text-transform: uppercase; color: var(--muted);
  margin-bottom: clamp(0.9rem, 1.6vw, 1.4rem);
}
.foot__eyebrow i { width: clamp(1.5rem, 3vw, 2.75rem); height: 1px; background: currentColor; }

.foot__pitch {
  font-family: var(--display); font-weight: 400;
  font-size: clamp(1.5rem, 2.6vw, 2.5rem);
  line-height: 1.12;
  max-width: 18ch;
  margin-bottom: clamp(0.9rem, 1.4vw, 1.25rem);
}
/* the practical half of the pitch: what to do and what it costs you */
.foot__sub {
  max-width: 34ch;
  font-size: 1rem; line-height: 1.55;
  color: var(--muted);
  margin-bottom: clamp(1.5rem, 2.6vw, 2.25rem);
}

/* A modern label rather than tiny tracked capitals: sentence case, semibold,
   set tight, at the size of a real heading. Still Inter, because the page is
   held to two typefaces and Inter is the modern one of the two. */
.foot__coltitle {
  font-size: clamp(1.05rem, 1.25vw, 1.25rem);
  font-weight: 600;
  letter-spacing: -0.01em;
  color: var(--paper);
  padding-bottom: 0.85rem;
  margin-bottom: 1.1rem;
  border-bottom: 1px solid var(--rule);
}

.foot__links { list-style: none; margin: 0; padding: 0; }
.foot__links li + li { margin-top: 0.55rem; }
.foot__links a {
  display: inline-block;
  font-size: clamp(1rem, 1.1vw, 1.0625rem); line-height: 1.5;
  font-weight: 500;
  letter-spacing: -0.005em;
  color: rgba(250, 250, 248, 0.86);
  text-decoration: none;
  transition: color 0.35s var(--ease);
}
.foot__links a:hover,
.foot__links a:focus-visible { color: var(--paper); }
/* A rule that wipes in from the left rather than a browser underline, so the
   hover has direction and matches the rest of the page's motion. */
.foot__links a span {
  position: relative;
  display: inline-block;
  padding-bottom: 2px;
}
.foot__links a span::after {
  content: ""; position: absolute;
  left: 0; bottom: 0; width: 100%; height: 1px;
  background: currentColor;
  transform: scaleX(0); transform-origin: right center;
  transition: transform 0.45s var(--ease);
}
.foot__links a:hover span::after,
.foot__links a:focus-visible span::after { transform: scaleX(1); transform-origin: left center; }

.foot__note {
  margin-top: 1.25rem;
  font-size: 0.875rem; line-height: 1.5;
  max-width: 28ch;
  color: var(--muted);
}

/* The turning emblem again, this time as the way back to the top. It pulls
   toward the pointer from js/main.js, which is why the transform here is only
   on the mark inside it: GSAP owns the anchor's own transform and would
   overwrite anything CSS put there. */
.foot__emblem {
  position: relative;
  display: block;
  width: clamp(9rem, 13vw, 12.5rem);
  aspect-ratio: 1;
  align-self: center;
  color: var(--paper);
  will-change: transform;
}
/* the glow that comes up under it on hover */
.foot__emblem::before {
  content: ""; position: absolute; inset: -8%;
  border-radius: 50%;
  background: radial-gradient(closest-side, rgba(250, 250, 248, 0.2), transparent 70%);
  opacity: 0;
  transition: opacity 0.55s var(--ease);
  pointer-events: none;
}
.foot__emblem:hover::before,
.foot__emblem:focus-visible::before { opacity: 1; }

.foot__emblem-mark {
  position: absolute;
  top: 50%; left: 50%;
  width: 34%; height: 40%;          /* the mark is 900 by 1052 */
  transform: translate(-50%, -50%);
  background: currentColor;
  -webkit-mask: url("../assets/brand/trident.png") center / contain no-repeat;
  mask: url("../assets/brand/trident.png") center / contain no-repeat;
  transition: transform 0.6s var(--ease);
}
.foot__emblem:hover .foot__emblem-mark,
.foot__emblem:focus-visible .foot__emblem-mark {
  transform: translate(-50%, -50%) scale(1.15);
}

.foot__rule {
  position: relative; z-index: 1;
  display: flex; align-items: center; justify-content: space-between;
  gap: 1.25rem; flex-wrap: wrap;
  border-top: 1px solid var(--rule);
  padding-block: clamp(1.5rem, 2.6vw, 2.25rem);
}
.foot__logo {
  display: inline-flex; align-items: center; gap: 0.75rem;
  color: var(--paper); text-decoration: none;
  font-size: var(--fs-micro); letter-spacing: 0.2em; text-transform: uppercase;
}
.foot__logo i {
  width: 1.45rem; height: 1.7rem;
  background: currentColor;
  -webkit-mask: url("../assets/brand/trident.png") center / contain no-repeat;
  mask: url("../assets/brand/trident.png") center / contain no-repeat;
  transition: transform 0.5s var(--ease);
}
.foot__logo:hover i { transform: translateY(-3px); }
.foot__copy {
  font-size: var(--fs-micro); letter-spacing: 0.12em;
  text-transform: uppercase; color: var(--muted);
}

/* ==========================================================================
   RESPONSIVE
   ========================================================================== */
/* Short laptop screens, while the carousel is pinned: 1366 by 660 is what a
   very common 768px laptop screen leaves after the browser's own bars. The
   card's height floor and its picture both answer to height now, which is
   what actually fixed the overflow here; this only tightens the type a step
   so there is air around the card rather than a card that just fits. */
@media (min-width: 901px) and (max-height: 720px) and (prefers-reduced-motion: no-preference) {
  .panel__copy { font-size: 0.9375rem; line-height: 1.5; }
  .work__title { font-size: clamp(1.75rem, 6.5vh, 3.25rem); }
}

@media (max-width: 1100px) {
  .sechead { grid-template-columns: 1fr; row-gap: clamp(2rem, 4vw, 3rem); }
  .sechead__title { grid-column: 1 / -1; }
  .emblem-slot { grid-column: 1 / -1; justify-self: start; margin-bottom: 0; }

  .foot__inner { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }
  .foot__call { grid-column: 1; }
  .foot__emblem { grid-column: 2; justify-self: end; align-self: start; }

  .svcgrid { --svc-gap: clamp(1.25rem, 3vw, 2rem); grid-template-columns: 1fr; perspective: none; }

  /* stacked cards, so the connector runs down the gutter instead of across it.
     Same variable drives it, which is why the wire scales from a property
     rather than from a transform written by GSAP. */
  .svc__wire {
    top: auto; bottom: calc(-1 * var(--svc-gap));
    right: auto; left: calc(var(--svc-inset) + var(--svc-badge) / 2);
    width: 2px; height: var(--svc-gap);
    margin-top: 0; margin-left: -1px;
    transform: scaleY(var(--wire, 0));
    transform-origin: top center;
  }
  .svc__face { transform: none; }
  .svc__body { max-width: 46ch; }

}

@media (max-width: 900px) {

  /* A third of a phone holds neither a numeral this size nor a paragraph, so
     the row unfolds into a single centred column. The entrance still brings the
     outer two in from their own sides, which is what keeps the rhythm once the
     side by side is gone. */
  .tally {
    grid-template-columns: minmax(0, 1fr);
    row-gap: clamp(2.5rem, 8vw, 4rem);
  }
  .proof { max-width: 46ch; }
  /* The three across sizing is a third of the page; here each figure has the
     whole column, so the cap that keeps it out of the prose no longer applies
     and it can be what it is meant to be. Measured 172px for 300+ in a 327px
     column at 375, against 119px on the shared clamp. */
  .stat__num { font-size: clamp(3.75rem, 17vw, 7rem); }

  /* the whole hero has to clear one phone screen, button and all */
  .hero { height: auto; }
  .hero__sticky { position: relative; height: auto; min-height: 100svh; }
  /* The lockup gathers round the watch as one group in the middle of the
     screen, rather than the two headline blocks being pushed to its top and
     bottom edges with the watch alone in between. */
  .hero__inner {
    min-height: 100svh;
    padding: clamp(4.5rem, 9svh, 6rem) var(--gut) clamp(1.5rem, 3vh, 2.5rem);
    justify-content: center;
  }
  /* Bigger than the one line lockup allows. BUILT TO CONVERT was sized to run
     exactly the measure, so it could not grow on one line; at 15vw it breaks
     as BUILT TO over CONVERT, which answers MADE TO BE over BOLD above the
     watch. Measured: CONVERT is 5.05em, so 15vw leaves it 56px clear of the
     gutters at 430. */
  .hero__title { flex: 0 0 auto; font-size: min(15vw, 5.5rem); }
  .hero__title > .ln:first-child { font-size: 0.8em; }
  .hero__title .ln, .hero__title .hero__line2 { align-self: center; text-align: center; }
  /* The gap the watch sits in, now a set height just clear of the watch
     rather than whatever was left over. scene.js centres the model on this
     box, so the watch follows it. The model is drawn at about 32% of the
     screen height on a phone. */
  .hero__void { flex: 0 0 auto; height: 37svh; min-height: 0; margin-top: 1.25rem; }
  /* the margin evens the air round the watch: the ring on top makes the model
     read high of its own centre, 9px under BOLD against 50px over BUILT TO */
  .hero__word { letter-spacing: 0.8em; text-indent: 0.8em; font-size: 0.4em; margin-top: 0.55em; }
  /* the button straight under the headline, centred and a size down */
  .hero__foot { margin-top: clamp(1.25rem, 3svh, 2rem); }
  .hero__foot .btn { width: auto; padding: 0.95rem 1.6rem; font-size: 0.875rem; }
  .scrollcue { display: none; }
  .hero__notes { display: none; }


  .why__title .ln-i { transform: none !important; }

  /* Two of the six figures are desktop only. Stacked in one column the three
     rows came out as figure, sentence, figure, figure, sentence, figure; without
     100 and 18 the column alternates cleanly all the way down. */
  .stat--wide { display: none; }

  .work__frame { height: auto; padding-block: var(--sec); gap: 2.5rem; }
  .work__head { flex-direction: column; align-items: flex-start; gap: 1.25rem; }
  .work__viewport {
    overflow-x: auto; overflow-y: hidden;
    scroll-snap-type: x mandatory;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
  }
  .work__viewport::-webkit-scrollbar { display: none; }
  .panel {
    width: min(86vw, 640px); scroll-snap-align: center;
    grid-template-columns: 1fr;
    gap: clamp(1.25rem, 4vw, 2rem);
  }
  /* Image first on a phone, where the card is a stack rather than a spread. */
  .panel__media { order: -1; }
  .panel__list { grid-template-columns: 1fr; margin-bottom: clamp(1.25rem, 4vw, 1.75rem); }
  /* One height for the row, so the cards do not step up and down as you swipe:
     the track stretches its slots, each slot hands the height to its card, and
     the button sits on the card's floor however long the copy above it is. */
  .work__track { align-items: stretch; }
  .panel-slot { display: flex; }
  .panel { grid-template-rows: auto 1fr; align-items: stretch; }
  .panel__body { display: flex; flex-direction: column; }
  .panel__link { margin-top: auto; }
  .work__progress { display: none; }

  /* the hero does not pin on a phone, so the deep pull up that reveals the
     gradient under the watch on desktop would just eat the headline and the
     button here */
  .seam--dawn { height: 74vh; height: 74svh; margin-top: -2vh; }
}

/* Phones under reduced motion only: no sticky and no opening window. The video
   sits at a fixed ratio with the heading under it, and the numbers follow. */
@media (max-width: 900px) and (prefers-reduced-motion: reduce) {
  .why__stage { position: relative; height: auto; }
  .why__window {
    position: relative; inset: auto;
    aspect-ratio: 4 / 3;
    clip-path: none;
    border-radius: 4px;
  }
  .why__intro {
    position: static;
    padding: clamp(2.5rem, 7vw, 4rem) var(--gut) 0;
    gap: 1.25rem;
  }
  .why__body { padding-top: clamp(3rem, 8vw, 5rem); }
}

/* ==========================================================================
   PHONES, WITH MOTION
   The scroll effects the desktop has, rebuilt for a narrow screen. Every rule
   here is below 901px, so the desktop never sees any of it.
   ========================================================================== */
@media (max-width: 900px) and (prefers-reduced-motion: no-preference) {

  /* Why Us opens out of white, as it does on a desktop: the window is a panel
     in the page and widens to fill the screen as you scroll. The resting shape
     suits a tall screen, most of the width and half the height, so the heading
     has room across. main.js animates these from the same values. With Why Us
     white, What We Do runs straight into it and the paper to ink seam is not
     wanted. */
  .why { background: var(--paper); }
  .why__window { --wy: 24%; --wx: 6%; --wr: 14px; }
  .seam--to-dark { display: none; }

  /* Selected Work pins and the cards slide across as you scroll, as on a
     desktop. The frame is one screen tall, so the card is trimmed to fit it:
     the picture answers to the screen height, the tags go two across, and the
     spacing tightens. Below a 740px tall screen the tags go, since the frame
     cannot grow. */
  .work__frame {
    height: 100vh; height: 100svh;
    padding-block: clamp(4.25rem, 9svh, 5.5rem) clamp(1rem, 2.5svh, 1.75rem);
    gap: clamp(1rem, 2.6svh, 1.75rem);
  }
  .work__head { gap: clamp(0.6rem, 1.4svh, 1rem); }
  .work__viewport { overflow: hidden; scroll-snap-type: none; }
  .panel {
    scroll-snap-align: none;
    min-height: 0;
    padding: clamp(1rem, 2.2svh, 1.4rem);
    gap: clamp(0.75rem, 1.8svh, 1.25rem);
  }
  .panel__media { max-height: 25svh; }
  .panel__name { margin-top: 0.5rem; font-size: clamp(1.4rem, 3.3svh, 1.9rem); }
  .panel__copy { margin-top: 0.5rem; font-size: 0.9375rem; line-height: 1.5; }
  .panel__list {
    margin-top: clamp(0.75rem, 1.8svh, 1.1rem);
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 0.55rem 1rem;
  }
  .panel__list li { font-size: 0.72rem; }

  /* In Their Words is the mirror on a phone too. shards.js places the two
     halves of the heading, one over the frame and one under it, and hangs the
     call to action under the second; these are what the stylesheet adds. The
     button keeps its own width rather than the full width phone buttons take,
     since it sits on the wall under a picture rather than in a column. The
     hint goes under the first half, where it reads as the instruction for the
     picture right below it. */
  .mirror__cta .btn { width: auto; }
  /* It is positioned from the screen's centre line, so shrink to fit gave it
     only the right half of a phone to fit in and the label broke in two. */
  .mirror__cta { gap: 0.75rem; width: max-content; }
  .shardstage.is-built .pull__hint { margin-top: 0.6rem; max-width: 30ch; font-size: 0.75rem; }
  /* The wall runs to the frame's floor, so the chapter's usual tail padding
     was 120px of flat black between the brick and the seam into paper. */
  .voices { padding-bottom: clamp(1rem, 4vw, 2rem); }
}

@media (max-width: 900px) and (max-height: 740px) and (prefers-reduced-motion: no-preference) {
  .panel__list { display: none; }
  /* On a short phone the mirror's frame starts about 150px down, and the hint
     under the first half of the heading ran onto it while the call to action
     ran 14px off the bottom. The picture explains itself; the label over the
     button is the first thing to spare. */
  .shardstage.is-built .pull__hint,
  .mirror__eyebrow { display: none; }
}

@media (max-width: 700px) {
  .foot__inner { grid-template-columns: 1fr; }
  .foot__call, .foot__emblem { grid-column: 1; }
  .foot__emblem { justify-self: start; }
  /* The closing section's top padding is measured against the arch it rises
     with on a desktop. On a phone it follows a flat seam, and 14vh of paper
     under that read as a gap before the eyebrow. */
  .cta__inner { padding-top: clamp(3.5rem, 14vw, 5rem); }
  /* the closing line is the last thing the page says; on a phone it was
     running at the shared section size, 28px at 430 */
  .cta__title { font-size: clamp(2.5rem, 11.5vw, 3.75rem); line-height: 0.96; }
  /* Rows a thumb can hit: 42px tall rather than the 26px of the text alone,
     with the spacing moved from between the rows into them. */
  .foot__links li + li { margin-top: 0.1rem; }
  .foot__links a { padding-block: 0.45rem; }
}

@media (max-width: 560px) {
  /* full width, with the label and arrow centred together rather than pushed
     to either end */
  .btn { width: 100%; justify-content: center; }
  /* the reveal wrapper shrinks to its button, which left this one the only
     short button on the phone */
  .cta__action { align-self: stretch; }
  .btn--xl { padding: 1.25rem 1.75rem; }
  .foot__rule { flex-direction: column; align-items: flex-start; gap: 1rem; }
}

/* ==========================================================================
   REDUCED MOTION
   ========================================================================== */
/* ==========================================================================
   CLICK ANYWHERE TO BOOK
   Over the white call to action the pointer becomes a royal blue disc with a
   particle trail, and a click anywhere on the sheet books the call. Both
   layers live at the end of <body>, not inside .cta: the section carries a
   filter on desktop, and a filter makes an ancestor the containing block for
   anything fixed inside it. Under the nav (800), over everything else.
   ========================================================================== */
.booktrail {
  position: fixed; inset: 0;
  width: 100%; height: 100%;
  z-index: 690;
  pointer-events: none;
}
.bookcursor {
  position: fixed; left: 0; top: 0;
  width: 0; height: 0;
  z-index: 700;
  pointer-events: none;
}
.bookcursor__disc {
  position: absolute; left: 0; top: 0;
  /* sized so "Book a call" fits on one line at the smallest */
  width: clamp(7rem, 8vw, 8rem); aspect-ratio: 1;
  border-radius: 50%;
  display: grid; place-items: center;
  padding: 0.5rem;
  text-align: center;
  background: var(--blue); color: #fff;
  box-shadow: 0 1.25rem 3.5rem -1.25rem rgba(27, 46, 203, 0.6);
  visibility: hidden;        /* main.js scales it in and owns its transform */
}
.bookcursor__label {
  font-size: 0.6875rem; font-weight: 600;
  letter-spacing: 0.12em; line-height: 1;
  text-transform: uppercase;
  white-space: nowrap;
}
/* the disc is the cursor while it is up */
html.is-booking .cta,
html.is-booking .cta * { cursor: none; }

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
  }
}

html.reduced .hero { height: auto; }
html.reduced .hero__sticky { position: relative; height: auto; min-height: 100svh; }
html.reduced .why__stage { position: relative; height: auto; }
html.reduced .why__window { position: relative; inset: auto; aspect-ratio: 16 / 9; clip-path: none; }
html.reduced .why__intro { position: static; padding: 3rem var(--gut) 0; }
html.reduced .why__body { padding-top: 3rem; }
html.reduced .why__title .ln-i { transform: none !important; }
html.reduced .work__frame { height: auto; padding-block: var(--sec); }
html.reduced .work__viewport { overflow-x: auto; }
html.reduced .panel { width: min(80vw, 760px); }
html.reduced .ln-i { transform: none !important; }
