/* ============================================================
   Strategy Marketing Firm
   Navy-drenched world, off-white relief, faceted knight mark.
   ============================================================ */

/* ---------- fonts ----------
   Self-hosted rather than fetched from fonts.googleapis.com: that stylesheet
   is a render-blocking request to a third origin, and it has to resolve before
   the first heading can paint. Served from here it is one same-origin file
   behind a preload.

   Both families are variable fonts with their axes pinned to what this design
   actually uses. Archivo shipped the whole wdth 62-125 range for a build that
   sets 112% and nothing else; pinning the axis and holding wght to 700-800
   took the latin file from 88KB to 24KB. Keep the ranges in the descriptors
   matching the pinned files - widening one here does not widen the font. */

@font-face {
  font-family: "Archivo";
  font-style: normal;
  font-weight: 700 800;
  font-stretch: 112%;
  font-display: swap;
  src: url("../fonts/archivo-latin.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6,
    U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122,
    U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: "Archivo";
  font-style: normal;
  font-weight: 700 800;
  font-stretch: 112%;
  font-display: swap;
  src: url("../fonts/archivo-latin-ext.woff2") format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7,
    U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F,
    U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F,
    U+A720-A7FF;
}

@font-face {
  font-family: "Schibsted Grotesk";
  font-style: normal;
  font-weight: 400 700;
  font-display: swap;
  src: url("../fonts/schibsted-latin.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6,
    U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122,
    U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: "Schibsted Grotesk";
  font-style: normal;
  font-weight: 400 700;
  font-display: swap;
  src: url("../fonts/schibsted-latin-ext.woff2") format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7,
    U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F,
    U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F,
    U+A720-A7FF;
}

:root {
  /* ground */
  --navy-900: #1e1d33;
  --navy: #2b2a47;
  --navy-600: #3a3860;
  --navy-400: #565383;

  /* relief */
  --off-white: #f1f0f5;
  --white: #ffffff;

  /* the mark's facets */
  --facet-light: #c8c7d6;
  --facet-mid: #9695ae;

  /* text */
  --on-navy: #f1f0f5;
  --on-navy-2: #b6b4cf;
  --on-light: #22213a;
  --on-light-2: #55537a;

  /* derived surfaces — alpha values that have no flat equivalent in the ramp.
     Each is one of the tokens above at reduced strength, and they are named for
     the job rather than the colour so a future ground change moves them all. */
  --rule-light: rgba(34, 33, 58, 0.16);
  --piece-shadow: rgba(30, 29, 51, 0.22);
  --piece-shadow-soft: rgba(30, 29, 51, 0.2);
  --checker: rgba(241, 240, 245, 0.05);

  --font-display: "Archivo", "Helvetica Neue", Arial, sans-serif;
  --font-body: "Schibsted Grotesk", "Helvetica Neue", Arial, sans-serif;

  --shell: min(1180px, 100% - 2.5rem);
  --section-y: clamp(5rem, 12vw, 9rem);

  /* the spacing scale DESIGN.md documents. The two off-scale steps below it
     (1.75rem under a display heading, 2.25rem above a CTA) are deliberate and
     already used by .capture__tags and .capture__cta; they stay literal. */
  --space-xs: 0.5rem;
  --space-sm: 0.75rem;
  --space-md: 1.5rem;
  --space-lg: 2.5rem;
  --space-xl: 3rem;

  --ease: cubic-bezier(0.22, 1, 0.36, 1);
  /* --ease is a hard ease-out: it covers most of its distance in the first
     fifth of the duration, which is right for something arriving and settling,
     and wrong for anything whose travel is the point — a fill that grows reads
     as a snap on that curve. This one starts gently, so the climb is visible. */
  --ease-rise: cubic-bezier(0.4, 0, 0.2, 1);
}

*,
*::before,
*::after {
  box-sizing: border-box;
}

/* No global scroll-behavior: smooth. On a 13,500px document it turned the
   Contact nav link into a three-second ride through every capture, and a page
   loaded straight at #contact never arrived at all — the fragment resolves at
   parse time, then the hero and the web fonts re-lay the document underneath
   the running scroll animation. Anchor clicks are smoothed in main.js, where
   the fragment can also be re-applied once layout has settled. */
html {
  -webkit-text-size-adjust: 100%;
}

body {
  margin: 0;
  background: var(--navy);
  color: var(--on-navy);
  font-family: var(--font-body);
  font-size: clamp(1rem, 0.96rem + 0.2vw, 1.0625rem);
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
}

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

a {
  color: inherit;
}

h1,
h2,
h3 {
  font-family: var(--font-display);
  font-weight: 800;
  font-stretch: 112%;
  line-height: 1.02;
  letter-spacing: -0.03em;
  margin: 0;
  text-wrap: balance;
}

p {
  margin: 0;
  text-wrap: pretty;
}

::selection {
  background: var(--white);
  color: var(--navy);
}

/* The focus ring obeys the Paired Surface Rule like every other colour
   assignment: white on the navy ground, navy on anything cut out of it. A
   white ring on an off-white block measures 1.13:1 and simply is not there,
   which took out every button on a relief block — both CTAs included. */
:focus-visible {
  outline: 2px solid var(--white);
  outline-offset: 3px;
}

:is(.section--light, .contact, .capture) :focus-visible {
  outline-color: var(--navy);
}

/* the skip link is a white chip wherever it lands, so it takes the dark ring
   even though it sits on the navy ground */
.skip-link:focus-visible {
  outline-color: var(--navy);
}

/* ---------- layout ---------- */

.shell {
  width: var(--shell);
  margin-inline: auto;
}

.section {
  padding-block: var(--section-y);
}

.section--light {
  background: var(--off-white);
  color: var(--on-light);
}

.skip-link {
  position: absolute;
  left: -9999px;
  top: 0;
  z-index: 999;
  background: var(--white);
  color: var(--navy);
  padding: 0.75rem 1.25rem;
  font-weight: 600;
}

.skip-link:focus {
  left: 1rem;
  top: 1rem;
}

/* Section headings that the layout does not want to show but the document
   outline needs: without them a page head jumps h1 -> h3. Safe to clip here,
   nothing inside ever sticks. */
.u-visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* ---------- type ---------- */

.h-xl {
  font-size: clamp(2.6rem, 8.5vw, 6rem);
}

.h-lg {
  font-size: clamp(2.1rem, 5vw, 3.5rem);
}

.lede {
  font-size: clamp(1.0625rem, 0.98rem + 0.5vw, 1.3125rem);
  line-height: 1.55;
  color: var(--on-navy-2);
  max-width: 60ch;
}

:is(.section--light, .contact) .lede {
  color: var(--on-light-2);
}

.measure {
  max-width: 68ch;
}

/* ---------- buttons ---------- */

.btn {
  display: inline-flex;
  align-items: center;
  padding: 0.95rem 1.75rem;
  font-family: var(--font-display);
  font-size: 0.8125rem;
  font-weight: 700;
  font-stretch: 112%;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  text-decoration: none;
  border: 1px solid var(--white);
  background: var(--white);
  color: var(--navy);
  transition: background 0.4s var(--ease), color 0.4s var(--ease),
    border-color 0.4s var(--ease);
}

/* State rules own the colour channel only — background-color, never the
   `background` shorthand. The shorthand resets every longhand it doesn't
   name (image, repeat, position, origin, size), and a :hover rule outranks a
   variant's resting declarations, so a shorthand here silently strips the
   background layers off any variant the moment the pointer lands. Keeping
   these to the longhand is what lets .btn--rule below paint its own layers
   without the base component needing to know it exists. */
.btn:hover {
  background-color: transparent;
  color: var(--white);
}

:is(.section--light, .contact, .capture) .btn {
  border-color: var(--navy);
  background: var(--navy);
  color: var(--off-white);
}

:is(.section--light, .contact, .capture) .btn:hover {
  background-color: transparent;
  color: var(--navy);
}

/* The quiet CTA: no fill, just the label sitting on a rule, filling with navy
   on hover. Used where the button should read as an imprint rather than a
   request.

   --pad-x is the one measurement this component is built from: it sets the
   horizontal padding, the negative margin that cancels it, the rule's inset,
   and (doubled) how much the rule is shorter than the box. Change it once.
   The negative margin is what keeps the label flush with the copy above while
   still giving the hover fill a box to paint into — no layout property has to
   animate, and the hover target never changes size under the pointer.

   Two background layers. Layer one is the fill, held at zero height along the
   bottom edge; layer two is the resting 2px rule. border-bottom is kept at
   `transparent` rather than removed: paired with background-origin: border-box
   it reserves the 2px strip the rule layer paints into. Hovering grows the
   fill up from the rule and covers it — the line is never animated away, it
   becomes the block. background-size is absent from the reduced-motion
   transition list at the foot of this file, so that growth snaps while the
   label colour still eases. */
:is(.section--light, .contact, .capture) .btn--rule {
  --pad-x: 1.35rem;
  padding: var(--space-sm) var(--pad-x);
  margin: 0 calc(var(--pad-x) * -1);
  border: 0;
  border-bottom: 2px solid transparent;
  background-color: transparent;
  background-image: linear-gradient(var(--navy), var(--navy)),
    linear-gradient(var(--navy), var(--navy));
  background-repeat: no-repeat;
  background-origin: border-box;
  background-position: left bottom, var(--pad-x) bottom;
  background-size: 100% 0%, calc(100% - var(--pad-x) * 2) 2px;
  color: var(--navy);
  letter-spacing: 0.14em;
  /* Leaving is quicker than arriving, and the label drops back to navy
     immediately rather than waiting for the fill to clear. */
  transition: background-size 0.4s var(--ease-rise), color 0.12s linear;
}

:is(.section--light, .contact, .capture) .btn--rule:hover {
  background-size: 100% 100%, calc(100% - var(--pad-x) * 2) 2px;
  color: var(--off-white);
  /* Arriving is its own timing, declared here because the transition that
     runs on hover-in is the one on the hovered style. Slower than leaving so
     the rise is legible, and the label holds navy until the fill is most of
     the way up — flip it early and the text washes out against bare ground. */
  transition: background-size 0.52s var(--ease-rise), color 0.12s linear 0.3s;
}

/* ---------- header ---------- */

.site-header {
  position: sticky;
  top: 0;
  z-index: 100;
  background: color-mix(in srgb, var(--navy) 88%, transparent);
  backdrop-filter: blur(14px);
  border-bottom: 1px solid var(--navy-600);
}

.site-header__inner {
  width: var(--shell);
  margin-inline: auto;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1.5rem;
  min-height: 4.5rem;
}

.brand {
  display: inline-flex;
  align-items: center;
  gap: 0.75rem;
  text-decoration: none;
  font-family: var(--font-display);
  font-size: 1rem;
  font-weight: 800;
  font-stretch: 112%;
  letter-spacing: -0.01em;
  line-height: 1.1;
}

/* the mark carries its own proportion — set the width and let it fall where
   it lands, rather than forcing a height and squashing the artwork */
.brand img {
  width: 1.65rem;
  height: auto;
  flex: none;
}

.brand__sub {
  display: block;
  font-family: var(--font-body);
  /* the micro step; 0.6rem rendered at 9.6px, below a comfortable floor */
  font-size: 0.6875rem;
  font-weight: 500;
  letter-spacing: 0.24em;
  text-transform: uppercase;
  color: var(--on-navy-2);
  margin-top: 0.3rem;
}

.nav {
  display: flex;
  align-items: center;
  gap: clamp(1.25rem, 3vw, 2.25rem);
}

.nav a {
  text-decoration: none;
  font-family: var(--font-display);
  font-size: 0.8125rem;
  font-weight: 700;
  font-stretch: 112%;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  padding-block: 0.35rem;
  position: relative;
  color: var(--on-navy-2);
  transition: color 0.3s var(--ease);
}

.nav a:hover,
.nav a[aria-current="page"] {
  color: var(--white);
}

.nav a::after {
  content: "";
  position: absolute;
  left: 0;
  bottom: 0;
  width: 100%;
  height: 2px;
  background: var(--white);
  transform: scaleX(0);
  transform-origin: left;
  transition: transform 0.4s var(--ease);
}

.nav a:hover::after,
.nav a[aria-current="page"]::after {
  transform: scaleX(1);
}

.nav-toggle {
  display: none;
  width: 2.75rem;
  height: 2.75rem;
  align-items: center;
  justify-content: center;
  background: none;
  /* No boundary. WCAG 1.4.11 asks for 3:1 on whatever identifies a control —
     here that is the bars themselves at 12:1, not a box drawn round them, so
     the box is not carrying any of the load. The focus ring is an outline
     rather than a border and is unaffected. */
  border: 0;
  cursor: pointer;
  color: var(--on-navy);
}

.nav-toggle span {
  position: relative;
  display: block;
  width: 1.25rem;
  height: 2px;
  background: currentColor;
  transition: background 0.15s linear;
}

.nav-toggle span::before,
.nav-toggle span::after {
  content: "";
  position: absolute;
  left: 0;
  width: 100%;
  height: 2px;
  background: currentColor;
  transition: transform 0.3s var(--ease);
}

.nav-toggle span::before {
  top: -6px;
}
.nav-toggle span::after {
  top: 6px;
}

.nav-toggle[aria-expanded="true"] span {
  background: transparent;
}
.nav-toggle[aria-expanded="true"] span::before {
  transform: translateY(6px) rotate(45deg);
}
.nav-toggle[aria-expanded="true"] span::after {
  transform: translateY(-6px) rotate(-45deg);
}

@media (max-width: 760px) {
  .nav-toggle {
    display: inline-flex;
  }

  /* The panel hangs below the header, and the header is a sheet with a radius
     and — until this was found — overflow: hidden, which clipped the panel out
     of existence: the burger crossed into its X and nothing appeared. It is
     its own sheet now, inset by the same gap as every other block, so it can
     sit outside the header's box without reading as a flap attached to it. */
  .nav {
    position: absolute;
    inset: calc(100% + var(--gap)) 0 auto;
    flex-direction: column;
    align-items: flex-start;
    gap: 0;
    padding: 0 1.25rem;
    background: var(--navy);
    border-radius: var(--card);
    overflow: hidden;
    /* transform + opacity, not max-height: animating a layout property
       thrashes layout, and visibility keeps the closed menu's links out of
       the tab order instead of merely clipping them out of sight */
    transform: translateY(-0.5rem);
    opacity: 0;
    visibility: hidden;
    transition: transform 0.35s var(--ease), opacity 0.3s var(--ease),
      visibility 0s linear 0.35s;
  }

  .nav[data-open="true"] {
    transform: translateY(0);
    opacity: 1;
    visibility: visible;
    transition: transform 0.35s var(--ease), opacity 0.3s var(--ease),
      visibility 0s;
  }

  .nav a {
    width: 100%;
    padding-block: 1.05rem;
    border-bottom: 1px solid var(--navy-600);
    font-size: 0.9375rem;
  }

  .nav a:last-child {
    border-bottom: 0;
  }

  .nav a::after {
    display: none;
  }
}

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

/* svh throughout the scroll stages, with a vh fallback for browsers that do
   not know the unit. vh resolves against the *large* viewport on mobile, so
   every sticky frame was taller than the visible area until the URL bar
   collapsed — and then reflowed mid-scroll, under a scrubbed timeline. svh is
   the stable one: it does not move when the browser chrome does. */
.hero {
  position: relative;
  height: 265vh;
  height: 265svh;
}

.hero__stage {
  position: sticky;
  top: 4.5rem;
  height: calc(100vh - 4.5rem);
  height: calc(100svh - 4.5rem);
  min-height: 30rem;
  display: grid;
  place-items: center;
  overflow: hidden;
  perspective: 1200px;
}

.hero__wordmark {
  position: relative;
  z-index: 1;
  text-align: center;
  width: var(--shell);
  padding-top: clamp(2rem, 8vh, 5rem);
}

.hero__title {
  font-size: clamp(2.6rem, 10vw, 6rem);
  line-height: 0.98;
  letter-spacing: -0.04em;
}

.hero__title span {
  display: block;
  color: var(--facet-light);
}

.hero__tagline {
  margin: clamp(1.25rem, 3vw, 1.9rem) auto 0;
  max-width: 44ch;
  font-size: clamp(1.0625rem, 0.98rem + 0.5vw, 1.3125rem);
  color: var(--on-navy-2);
}

/* no resting will-change: it held a compositor layer for the whole session to
   serve an animation that finishes inside the first two screens. GSAP promotes
   the element for the duration of the tween on its own. */
.hero__knight {
  position: absolute;
  top: clamp(1rem, 5vh, 3.5rem);
  left: 0;
  right: 0;
  margin-inline: auto;
  /* Behind the wordmark, not in front of it. The piece used to leave before the
     name arrived so stacking never mattered; now it comes to rest underneath it
     and the type has to win. */
  z-index: 0;
  width: clamp(7rem, 18vw, 12rem);
  transform-style: preserve-3d;
}

.hero__knight img {
  width: 100%;
  height: auto;
}

/* The still under this canvas is frame 0 of the same turntable at the same
   430x700, so the handover is not a transition at all — the canvas draws the
   pose that is already on screen and nothing moves.

   It used to be the flat mark at 282x503: a different pose *and* a different
   aspect, so the piece visibly snapped and resized the moment the sheets
   finished downloading. Instant on localhost, a second or two for a real
   visitor. The fade below is only insurance against sub-pixel differences
   between a decoded PNG and a canvas draw of the same image. */
.hero__spin {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  opacity: 0;
  transition: opacity 0.25s linear;
}

.hero__knight.is-spinning .hero__spin {
  opacity: 1;
}

/* The still has to leave as the canvas arrives. Without this it stays stacked
   underneath — a second knight, holding its pose under the parent's tilt while
   the canvas turns above it. Both sides show the same frame at the moment of
   the swap, so crossfading them is invisible rather than a dissolve. */
.hero__knight img {
  transition: opacity 0.25s linear;
}

.hero__knight.is-spinning img {
  opacity: 0;
}

.hero__cue {
  position: absolute;
  bottom: 1.25rem;
  left: 0;
  right: 0;
  margin-inline: auto;
  width: fit-content;
  z-index: 3;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.7rem;
  font-size: 0.6875rem;
  font-weight: 500;
  letter-spacing: 0.24em;
  text-transform: uppercase;
  color: var(--on-navy-2);
}

.hero__cue i {
  display: block;
  width: 1px;
  height: 2rem;
  background: linear-gradient(to bottom, var(--facet-light), transparent);
}

/* ---------- editorial service list ---------- */

.offer {
  border-top: 1px solid var(--navy-600);
}

:is(.section--light, .contact) .offer {
  border-color: var(--rule-light);
}

.offer__row {
  display: grid;
  gap: 0.75rem 3rem;
  padding-block: clamp(2rem, 4vw, 3.25rem);
  border-bottom: 1px solid var(--navy-600);
  align-items: start;
}

:is(.section--light, .contact) .offer__row {
  border-color: var(--rule-light);
}

@media (min-width: 860px) {
  .offer__row {
    grid-template-columns: minmax(0, 7fr) minmax(0, 9fr);
  }
}

.offer__row h3 {
  font-size: clamp(1.6rem, 3.2vw, 2.5rem);
  letter-spacing: -0.03em;
}

.offer__row p {
  color: var(--on-navy-2);
  font-size: clamp(1rem, 0.96rem + 0.2vw, 1.0625rem);
}

:is(.section--light, .contact) .offer__row p {
  color: var(--on-light-2);
}

.offer__tags {
  list-style: none;
  margin: 1.1rem 0 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem 1.5rem;
  font-size: 0.8125rem;
  color: var(--facet-mid);
}

:is(.section--light, .contact) .offer__tags {
  color: var(--on-light-2);
}

/* ---------- steps (sequence carries meaning) ---------- */

.steps {
  list-style: none;
  margin: 0;
  padding: 0;
  counter-reset: step;
  border-top: 1px solid var(--navy-600);
}

:is(.section--light, .contact) .steps {
  border-color: var(--rule-light);
}

.steps li {
  counter-increment: step;
  display: grid;
  gap: 0.5rem 2.5rem;
  padding-block: clamp(1.5rem, 3vw, 2.25rem);
  border-bottom: 1px solid var(--navy-600);
}

:is(.section--light, .contact) .steps li {
  border-color: var(--rule-light);
}

@media (min-width: 720px) {
  .steps li {
    grid-template-columns: 3.5rem minmax(0, 14rem) minmax(0, 1fr);
    align-items: baseline;
  }
}

.steps li::before {
  content: counter(step);
  font-family: var(--font-display);
  font-weight: 700;
  /* the One Register Rule: Archivo is only ever 112% wide here. Without this
     the numeral computes to normal width and leans on the browser's nearest
     match instead of asking for the face the rest of the page uses. */
  font-stretch: 112%;
  color: var(--facet-mid);
  font-size: 0.9375rem;
}

:is(.section--light, .contact) .steps li::before {
  color: var(--on-light-2);
}

.steps h3 {
  font-size: 1.1875rem;
}

.steps p {
  color: var(--on-navy-2);
  font-size: 0.9375rem;
}

:is(.section--light, .contact) .steps p {
  color: var(--on-light-2);
}

/* ---------- team ---------- */

.team-grid {
  display: grid;
  gap: clamp(1.75rem, 3.5vw, 2.75rem);
  /* tracks stop growing at 20rem. With a 1fr cap a two-person roster stretches
     into two portraits half the page wide; auto-fit still wraps a longer one. */
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 240px), 20rem));
  justify-content: start;
}

.member__portrait {
  position: relative;
  aspect-ratio: 4 / 5;
  background: var(--navy-600);
  display: grid;
  place-items: center;
  overflow: hidden;
}

/* Every portrait now carries a photo, so this board pattern is what shows in
   the gap before a lazy image decodes, or permanently if one 404s. It is the
   portrait's fallback state, not a layer of the finished design. */
.member__portrait::before {
  content: "";
  position: absolute;
  inset: 0;
  background-image: conic-gradient(
    var(--checker) 0 25%,
    transparent 0 50%,
    var(--checker) 0 75%,
    transparent 0
  );
  background-size: 44px 44px;
}

/* Sits above the checker on its own, without a z-index, because ::before is
   painted before a positioned sibling. The portrait box already clips, so
   cover crops rather than distorts on any column width. */
.member__photo {
  position: relative;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.member h3 {
  font-size: 1.1875rem;
  margin-top: 1.35rem;
}

.member__role {
  font-size: 0.75rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--facet-mid);
  margin-top: 0.5rem;
}

.member__bio {
  margin-top: 0.9rem;
  font-size: 0.9375rem;
  color: var(--on-navy-2);
}

/* ---------- split ---------- */

.split {
  display: grid;
  gap: clamp(2rem, 5vw, 4.5rem);
  align-items: start;
}

@media (min-width: 900px) {
  .split {
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  }
}

/* ---------- page head ---------- */

.page-head {
  padding-block: clamp(4rem, 10vw, 7rem) clamp(2.5rem, 6vw, 4rem);
}

/* ---------- contact + footer ---------- */

.contact {
  background: var(--off-white);
  color: var(--on-light);
}

.contact__inner {
  width: var(--shell);
  margin-inline: auto;
  padding-block: var(--section-y);
  display: grid;
  gap: clamp(2rem, 5vw, 4rem);
  align-items: end;
}

@media (min-width: 880px) {
  .contact__inner {
    grid-template-columns: minmax(0, 1fr) auto;
  }
}

.site-footer {
  background: var(--navy-900);
  color: var(--on-navy-2);
  font-size: 0.9375rem;
}

.site-footer__inner {
  width: var(--shell);
  margin-inline: auto;
  padding-block: clamp(2.5rem, 5vw, 3.5rem);
  display: grid;
  gap: 2.5rem;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 200px), 1fr));
}

.site-footer h2 {
  font-family: var(--font-body);
  font-size: 0.6875rem;
  font-weight: 500;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--white);
  margin: 0 0 1.1rem;
}

/* the blurb under the wordmark, previously carried inline on all three pages */
.site-footer p {
  max-width: 30ch;
}

.site-footer ul {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  /* the row gap shrinks as the targets grow, so the column ends up only a
     little taller than it was while every link becomes tappable */
  gap: 0.15rem;
}

/* 19px-tall text links sat under the 24px minimum, and the old 0.6rem gap put
   their centres 28.6px apart — close enough that the spacing exception did not
   rescue them either. The box is now the target, not the glyphs. */
.site-footer a {
  display: inline-flex;
  align-items: center;
  min-height: 2.75rem;
  text-decoration: none;
  transition: color 0.3s var(--ease);
}

.site-footer a:hover {
  color: var(--white);
}

.site-footer__bar {
  border-top: 1px solid var(--navy-600);
}

.site-footer__bar div {
  width: var(--shell);
  margin-inline: auto;
  padding-block: 1.5rem;
  display: flex;
  flex-wrap: wrap;
  gap: 0.75rem 1.5rem;
  justify-content: space-between;
  font-size: 0.8125rem;
}

/* ---------- reveals ---------- */

.js-reveal .r-rise,
.js-reveal .r-fade {
  opacity: 0;
}

.js-reveal .r-rise {
  transform: translateY(30px);
}

/* The reduced-motion block used to live here and no longer does — it is the
   last thing in this file. It was being overridden by the capture rules and
   the sheet epilogue below, both of which are declared later at equal
   specificity, so half of it silently did nothing. Anything added to it has to
   stay after every rule it means to override. */

/* ---------- capture: one section, one piece taken ----------
   The scene is scenery. It runs behind the copy at low opacity so the
   collision registers in peripheral vision while the words stay the thing
   you actually read. Pieces stand on a shared rule; the target pivots on
   its own base when it goes over. */

.capture {
  position: relative;
  background: var(--off-white);
  color: var(--on-light);
  /* the overshoot past the frame is the scrubbing room: the frame below sticks
     for exactly this much, which is what lets the knight cross the whole width
     with the section held in view. Unstuck, most of that run would play out
     while the section was still under the fold.

     It is also the pacing. At 150vh the entire capture fitted inside one
     trackpad flick, so the pieces snapped through the move instead of playing
     it; the run now maps onto twice the scroll and advances half as far per
     pixel. */
  height: 200vh;
  height: 200svh;

  /* pawn height; every other piece is a fixed multiple of it, so the set
     keeps the true proportions of the artwork at any viewport. Sized against
     the viewport rather than the frame, so shortening the frame tightens the
     composition without shrinking the pieces. */
  --piece-unit: clamp(8rem, 27vh, 17rem);
  --base: 10%;
}

/* short of a full screen: at 100vh the scene left a dead band under it.
   note: the scroll parent must not clip, or this stops sticking */
.capture__frame {
  position: sticky;
  top: 0;
  height: 80vh;
  height: 80svh;
  overflow: hidden;
}

.capture__scene {
  position: absolute;
  inset: 0;
  pointer-events: none;
}

.capture__slot {
  position: absolute;
  bottom: var(--base);
  left: var(--x);
  transform: translateX(-50%);
}

/* A toppling piece needs roughly its own height of clear floor on the side it
   goes over — call it 1.35x once its half-width is counted. A hand-picked
   percentage only satisfies that at the width it was picked at: at 390px the
   pawn went clean off the left edge. Flooring the position against the piece's
   own height instead makes it hold at any width, and the knight follows the
   target rather than being placed independently. */
.capture[data-from="right"] {
  --tx-safe: max(var(--tx), calc(var(--piece-unit) * var(--tr) * 1.35 + 1rem));
}

.capture[data-from="left"] {
  --tx-safe: min(
    var(--tx),
    calc(100% - var(--piece-unit) * var(--tr) * 1.35 - 1rem)
  );
}

.capture__slot--target {
  left: var(--tx-safe);
}

.capture[data-from="right"] .capture__slot--knight {
  left: calc(var(--tx-safe) + 11%);
}

.capture[data-from="left"] .capture__slot--knight {
  left: calc(var(--tx-safe) - 11%);
}

.capture__target {
  height: calc(var(--piece-unit) * var(--tr, 1));
}

.capture__knight {
  height: calc(var(--piece-unit) * 1.17);
}

.capture__target img,
.capture__knight img {
  height: 100%;
  width: auto;
}

/* the target pivots on the corner it falls over. Held well above the knight's
   opacity on purpose: fade the dark set too far on a light ground and it turns
   the same grey as the white mark, which throws away the whole us-and-them
   reading and leaves two identical pale shapes. */
.capture__target {
  transform-origin: left bottom;
  opacity: 0.4;
}

/* the white mark has almost no contrast on off-white, so it carries a soft
   navy shadow to hold its silhouette. Kept below the target's opacity: the
   dark set turns to flat grey as it fades, and matching the two numbers is
   what collapses them into the same tone. */
.capture__knight {
  transform-origin: center bottom;
  opacity: 0.3;
  filter: drop-shadow(0 0.5rem 1.5rem var(--piece-shadow));
}

.capture__copy {
  position: relative;
  z-index: 2;
  width: var(--shell);
  margin-inline: auto;
  height: 100%;
  display: grid;
  /* sits over the scene rather than above it — the words are the point and the
     board is what they are printed on */
  align-content: center;
}

/* the pieces are pushed out to the wings, so the copy takes the middle — held
   narrow enough that the knight comes to rest beside the words, not behind
   them */
.capture__inner {
  max-width: 40rem;
  margin-inline: auto;
  text-align: center;
}

/* the documented headline step; the lede is already the largest body step */
.capture__title {
  font-family: var(--font-display);
  font-size: clamp(2.6rem, 8.5vw, 6rem);
  font-weight: 800;
  line-height: 1.02;
  letter-spacing: -0.03em;
  margin: 0;
}

.capture__lede {
  font-size: clamp(1.0625rem, 0.98rem + 0.5vw, 1.3125rem);
  line-height: 1.55;
  color: var(--on-light-2);
  margin: 1.5rem auto 0;
  max-width: 46ch;
}

.capture__tags {
  list-style: none;
  margin: 1.75rem 0 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 0.5rem 1.75rem;
  font-size: 0.8125rem;
  color: var(--on-light-2);
}

@media (max-width: 40em) {
  .capture {
    --piece-unit: clamp(4.5rem, 20vw, 8rem);
    --base: 12%;
    height: 180vh;
    height: 180svh;
  }

  .capture__copy {
    align-content: start;
    padding-top: 16vh;
    padding-bottom: 0;
  }
}

/* a piece knocked to the right has to pivot on the corner it goes over, which
   is the opposite one from a piece knocked left */
.capture[data-from="left"] .capture__target {
  transform-origin: right bottom;
}

/* the rook keeps the knight's treatment: both are white pieces on a light
   ground and need the same shadow to hold an edge */
.capture__ally {
  height: calc(var(--piece-unit) * var(--r, 1));
  transform-origin: center bottom;
  opacity: 0.3;
  filter: drop-shadow(0 0.5rem 1.5rem var(--piece-shadow));
}

.capture__ally img {
  height: 100%;
  width: auto;
}

/* ---------- checkmate: the one piece that is never taken ---------- */

/* it leans, it does not fall, so it turns about its own centre */
.mate__king {
  transform-origin: center bottom;
}

/* a sentence rather than a two-word label: the headline step overflows the
   frame at this length, so this one drops to the title step */
.mate .capture__title {
  font-size: clamp(2.1rem, 5vw, 3.5rem);
}

.capture__cta {
  margin: 2.25rem 0 0;
}

.capture__target {
  filter: drop-shadow(0 0.5rem 1.25rem var(--piece-shadow-soft));
}

/* No floor gradient here. The frame is a 640px window onto a 1600px card, so
   any shading painted inside it has to stop at the frame's edge while the
   card's flat off-white carries on below — which read as two different whites
   meeting in a line. The pieces are grounded by their own shadows instead. */

/* ---------- the sheet ----------
   Every block is inset so the ground shows through as a margin, which reads as
   the page being laid on a surface rather than filling the window. It also
   zones the four captures for free: they stop being one continuous off-white
   field and become four cards with the ground between them.

   This is the one place the square-corner rule is set aside, on request. */

:root {
  --gap: clamp(0.5rem, 0.9vw, 0.9rem);
  --card: clamp(0.75rem, 1.3vw, 1.35rem);
  /* header height plus the gap above and below it */
  --stick: calc(4.5rem + var(--gap) * 2);
}

/* viewport-fit=cover lets the ground run under the rounded corners and the
   home indicator, which is the point — but the sheet inset has to clear the
   notch in landscape, so the gap takes whichever is larger. */
body {
  background: var(--navy-900);
  padding: var(--gap);
  padding-left: max(var(--gap), env(safe-area-inset-left));
  padding-right: max(var(--gap), env(safe-area-inset-right));
  padding-bottom: max(var(--gap), env(safe-area-inset-bottom));
}

.site-header,
.site-footer,
main > section {
  border-radius: var(--card);
}

/* clipping is safe only where nothing inside sticks — an ancestor with
   overflow hidden silently kills position: sticky, which is every capture
   frame and the hero stage.

   The header is deliberately not in this list. It has a radius but nothing
   that paints into the corners, and clipping it hid the mobile nav panel,
   which is absolutely positioned below the header's own box. */
.site-footer {
  overflow: hidden;
}

main > * + * {
  margin-top: var(--gap);
}

main {
  margin-top: var(--gap);
}

.site-header {
  top: var(--gap);
}

/* the footer would vanish into a ground of the same value */
.site-footer {
  background: var(--navy);
}

/* anything that sticks now clears a header that is itself inset */
.hero__stage {
  top: var(--stick);
  height: calc(100vh - var(--stick) - var(--gap));
  height: calc(100svh - var(--stick) - var(--gap));
}

.capture__frame {
  top: var(--stick);
  border-radius: var(--card);
}

/* these two never carried a background of their own — they sat directly on the
   body's navy. Now that the body is the ground behind everything, they need it
   explicitly or they read as holes cut in the page rather than as cards. */
.hero,
.section:not(.section--light) {
  background: var(--navy);
}

/* ---------- composition ----------
   The rules below were carried as style attributes on the markup, which put
   thirteen spacing and measure decisions outside the system where nothing
   could keep them consistent. The rendered values are unchanged; they are
   just expressed once here instead of once per page. */

/* a page title holds a short measure so it breaks into deliberate lines */
.page-head .h-xl {
  max-width: 15ch;
}

.page-head .lede {
  margin-top: 1.75rem;
}

.contact .h-lg {
  max-width: 18ch;
}

.contact .lede {
  margin-top: var(--space-md);
}

/* the positioning block runs two ledes in sequence */
.lede + .lede {
  margin-top: var(--space-md);
}

/* the same step the capture CTA already sits on */
.cta-row {
  margin: 2.25rem 0 0;
}

/* a section heading stacked above a ruled list. Scoped away from .split, where
   the heading and the list are two columns of a grid rather than a stack — a
   top margin there pushes the list out of alignment with its own heading. */
.shell:not(.split) > .h-lg + .steps {
  margin-top: var(--space-xl);
}

/* the honesty rows carry a heading and a single paragraph rather than a
   heading and a stack, so they do not want the 7fr/9fr split */
.offer__row--full {
  grid-template-columns: minmax(0, 1fr);
}

/* ---------- hero below the spin breakpoint ----------
   The rendered turntable is desktop-only, so on a phone the scroll sequence was
   265vh of scrubbing that turned a flat image and arrived nowhere. There is
   nothing to scrub toward, so the hero stops being a scroll stage and becomes
   an ordinary block: the name is there on arrival and the piece is already at
   the angle the desktop version spends two screens reaching.

   Must stay ahead of the reduced-motion block, which has to remain last. */
@media (max-width: 899.98px) {
  .hero {
    height: auto;
  }

  .hero__stage {
    /* relative, not static: the piece and the cue are both absolute and need
       this box to anchor to rather than the section behind it.

       And top has to be cleared with it. The sheet epilogue sets
       `top: var(--stick)` for the sticky desktop stage, where it is the offset
       the stage parks at; on a relative box the same declaration is a plain
       downward shove. It pushed the stage 88px below the hero, which hung the
       cue outside the card onto the section beneath and left an empty band
       across the top that no amount of padding was ever going to close. */
    position: relative;
    top: 0;
    height: auto;
    /* Fills the screen, as the desktop stage does. At a fixed 27rem it was
       about half a phone, which made the opening read as a banner above the
       fold rather than an arrival. svh rather than vh so the browser chrome
       collapsing mid-scroll does not resize it. */
    min-height: calc(100vh - var(--stick) - var(--gap));
    min-height: calc(100svh - var(--stick) - var(--gap));
    /* tighter than the desktop stage: every rem here is height the piece
       cannot use, and filling the screen was the point */
    padding-block: clamp(1.5rem, 4vh, 2.5rem);
  }

  /* The wordmark carries a top pad that offsets it against the desktop stage,
     where the piece starts above it. Here the piece is centred behind it, so
     the pad only pushed the whole composition into the lower half of a screen
     it had just been told to fill. */
  .hero__wordmark {
    padding-top: 0;
  }

  /* The composition sits above the cue rather than centred in the whole stage.
     Centred, the piece's narrow head left the top reading empty while its base
     crowded the cue at the bottom; lifting the whole group clears a band for
     the cue and puts the visual mass where the eye lands first. */
  .hero__stage {
    /* The group stays centred — align-content: start pulled the wordmark to the
       top of the stage while the piece stayed put, and the two came apart. A
       heavier bottom pad lifts the centre instead, which moves both together
       and reserves the band the cue sits in. */
    padding-top: clamp(1.25rem, 3vh, 2rem);
    padding-bottom: clamp(2.5rem, 7vh, 3.5rem);
  }

  .hero__cue {
    bottom: 2.25rem;
  }

  .hero__knight {
    /* 48%: the gap to the cue is set by where this lands and where the cue
       sits, not by the stage's padding — both are absolutely positioned, so
       padding only ever moved the wordmark between them. Dropping the piece
       three points and lifting the cue closes the gap from 67px to ~31 without
       resizing anything or pulling the type off the piece. */
    top: 48%;
    transform: translateY(-50%);
    /* Sized against the stage height rather than the width, so it fills a tall
       screen instead of a wide one — 0.614 is the frame's own 430:700 aspect.
       Held to 0.70 of the stage rather than 0.78: the extra height was coming
       out of the band the cue needs. */
    width: min(72vw, calc((100svh - var(--stick) - var(--gap)) * 0.70 * 0.614));
    /* the weight the desktop hero settles to, so type still reads over it */
    opacity: 0.38;
  }
}

/* ---------- reduced motion ----------
   LAST IN THE FILE, and it has to stay last. Everything here overrides a rule
   declared earlier at the same specificity — .capture's height, the frame's
   stickiness, the hero stage the sheet epilogue re-sizes. Sitting where it
   used to, in the middle of the stylesheet, the capture collapse never applied
   and this build handed a reduced-motion visitor nine thousand pixels of
   scrolling past pieces that were no longer moving. */

@media (prefers-reduced-motion: reduce) {
  /* the scroll-driven work is all off, so the height that existed only to
     scrub against is nine screenfuls of nothing */
  .capture {
    height: auto;
  }

  .capture__frame {
    position: static;
    height: auto;
    padding-block: var(--section-y);
  }

  /* the scene is choreography that never plays; the copy is the section */
  .capture__scene {
    display: none;
  }

  .hero {
    height: auto;
  }

  .hero__stage {
    position: static;
    height: auto;
    min-height: 0;
    display: block;
    text-align: center;
    padding-block: clamp(4rem, 12vh, 7rem) clamp(3rem, 8vh, 4.5rem);
  }

  .hero__knight {
    position: static;
    width: clamp(5rem, 12vw, 7.5rem);
    margin: 0 auto clamp(2rem, 5vw, 3rem);
  }

  .hero__cue {
    display: none;
  }

  .r-rise,
  .r-fade {
    opacity: 1 !important;
    transform: none !important;
  }

  /* Not a blanket duration kill. Reduced motion asks for less movement, not
     for hover, focus and the nav underline to change with no perceptible
     transition at all — at 0.01ms every state change became a hard cut, which
     is its own accessibility problem. Restricting the transition property list
     drops the movement and keeps the feedback: colour still eases at a
     readable 150ms, transforms simply snap. */
  *,
  *::before,
  *::after {
    animation-duration: 1ms !important;
    animation-iteration-count: 1 !important;
    transition-property: color, background-color, border-color, outline-color,
      opacity, fill, stroke !important;
    transition-duration: 150ms !important;
    transition-delay: 0s !important;
    scroll-behavior: auto !important;
  }
}
