/* ==========================================================================
   Eight Group — global styles
   Tokens mirror the Figma variables (8G palette / Web type styles)
   ========================================================================== */

/* ---- Fonts ------------------------------------------------------------- */
/* Larken — supplied brand font */
@font-face {
  font-family: "Larken";
  src: url("../assets/fonts/Larken-Regular.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Larken";
  src: url("../assets/fonts/Larken-RegularItalic.woff2") format("woff2");
  font-weight: 400;
  font-style: italic;
  font-display: swap;
}
@font-face {
  font-family: "Larken";
  src: url("../assets/fonts/Larken-Medium.woff2") format("woff2");
  font-weight: 500;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Larken";
  src: url("../assets/fonts/Larken-MediumItalic.woff2") format("woff2");
  font-weight: 500;
  font-style: italic;
  font-display: swap;
}
/* Hanken Grotesk — loaded from Google Fonts (see <head>). Note: it won't
   render in this sandbox's preview screenshots (network blocked here), but
   will load fine in any real browser. */

/* ---- Design tokens ------------------------------------------------------ */
:root {
  --c-dark-green: #0b2b2a;   /* 8G - Dark Green   */
  --c-teal:       #1a4a48;   /* 8G - Teal         */
  --c-gold:       #a77421;   /* 8G - Gold         */
  --c-cream:      #f5f5f0;   /* 8G - Light Cream  */
  --c-cream-50:   #f2f2ec;   /* 8G - Dark Cream 50 */
  --c-orange:     #ea5f3e;   /* 8G - Orange       */
  --c-orange-40:  #f7bfb2;   /* Orange 40 tint (type on the signup panel) */
  --c-green-80:   #3c5555;   /* Dark Green 80 tint */
  --c-green-60:   #6d807f;   /* Dark Green 60 tint (meta text) */
  --c-green-40:   #9daaaa;   /* Dark Green 40 tint (text on dark) */
  --c-green-20:   #ced5d4;   /* Dark Green 20 tint (rules) */

  --font-serif: "Larken", Georgia, serif;
  --font-sans: "Hanken Grotesk", "Helvetica Neue", Helvetica, Arial, sans-serif;

  --pad-x: 2vw;              /* page gutter (desktop) */
  /* Column gutter for the 12-column grid — proportional, like the page
     gutter. Capped at the width where `.content-wrapper` hits its 1800px
     max: past that the wrapper stops growing, so a pure vw gutter would keep
     widening and quietly eat the columns. 1.5vw = 21.6px at the design's
     1440, within a pixel of the 20px it replaces. */
  --grid-gutter: min(1.5vw, 28px);
  --header-h: 107px;
  /* The content wrapper's ceiling. A variable because the article's media
     blocks have to match it exactly without being inside a wrapper. */
  --content-max: 1800px;
  /* Ceiling for the article's reading column. 7 of 12 columns is ~1000px in
     an 1800 wrapper, which is past a comfortable line length. */
  --prose-max: 750px;

  /* Shared transition lists. Kept as variables so an element that both
     reveals and has its own hover transitions can compose the two — setting
     `transition` twice would otherwise silently drop one of them. */
  --t-btn: background-color 0.25s ease, border-color 0.25s ease,
           padding 0.3s ease, color 0.15s ease 0.18s;
  /* NB: the reveal transition is NOT kept here. A var() nested inside a custom
     property is resolved where that property is *declared* — so a
     var(--reveal-delay) written at :root would bake in :root's value (0s) and
     every element would inherit a zero delay. It's declared inline below. */
}

/* ---- Reset / base ------------------------------------------------------- */
*, *::before, *::after { box-sizing: border-box; }
body {
  margin: 0;
  font-family: var(--font-sans);
  font-size: 18px;
  line-height: 1.3;
  color: var(--c-dark-green);
  background: #fff;
  -webkit-font-smoothing: antialiased;
}
img, svg { display: block; }
a { color: inherit; text-decoration: none; }
p { margin: 0; }
h1, h2, h3, hgroup { margin: 0; }
h1, h2, h3 { font-weight: 400; }
h1 { text-wrap: balance; }
h2 { text-wrap: pretty; } /* h2s are often wider — pretty avoids over-short lines */

/* Animated underline for text links: grows from the left on hover,
   retreats to the right on mouse-out */
.footer-nav a, .site-footer__address a, .section-legal__col a {
  position: relative;
}
.footer-nav a::after,
.site-footer__address a::after, .section-legal__col a::after {
  content: "";
  position: absolute;
  left: 0;
  bottom: 0;
  width: 100%;
  height: 1px;
  background: currentColor;
  transform: scaleX(0);
  transform-origin: right;
  transition: transform 0.3s ease;
}
.footer-nav a:hover::after,
.site-footer__address a:hover::after, .section-legal__col a:hover::after {
  transform: scaleX(1);
  transform-origin: left;
}

/* Site-wide section wrapper: gutters + max content width.
   Mirrors the "content-wrapper" frames in the Figma file — every section's
   content sits inside one of these; vertical padding is set per section. */
.content-wrapper {
  max-width: var(--content-max);
  margin: 0 auto;
  padding-left: var(--pad-x);
  padding-right: var(--pad-x);
}

/* ---- 12-column grid ------------------------------------------------------
   The layout grid the Figma file is drawn on: 12 fluid columns and a
   proportional gutter, inside the content wrapper. At the design's 1440 the
   content area is 1380 and a column is 95.4px, so the file's spans land
   within a pixel — 3 cols = 329, 4 = 446, 5 = 563, 8 = 915, 9 = 1032
   (design: 330 / 447 / 562 / 916 / 1030).

   Put `.grid-12` on the row and give each child a span (`.col-9`) and, where
   it doesn't simply follow the previous one, a start (`.col-start-4`). Tracks
   are `1fr` and the gutter is `--grid-gutter` (1.5vw, capped — see :root), so
   the whole grid scales with the page rather than the columns absorbing a
   fixed gutter as the viewport shrinks.

   The spans are written as `grid-column-end: span N`, not the `grid-column`
   shorthand, so a `.col-start-N` alongside them isn't wiped out — the
   shorthand would reset the end back to `auto` and collapse the item to one
   column. Two other rules of the road: a section rule for a `.grid-12` row
   must never set `display` (it would beat `.grid-12` on source order), and
   `order` still works, so mobile can reorder a stacked row.

   Row gaps are a per-section decision: set `--grid-row-gap` on the row.
   Below 860px the whole thing collapses to a single column and every span
   goes full width, which is the mobile translation this site already uses. */
.grid-12 {
  display: grid;
  grid-template-columns: repeat(12, minmax(0, 1fr));
  column-gap: var(--grid-gutter);
  row-gap: var(--grid-row-gap, 0px);
}
.col-1  { grid-column-end: span 1 }
.col-2  { grid-column-end: span 2 }
.col-3  { grid-column-end: span 3 }
.col-4  { grid-column-end: span 4 }
.col-5  { grid-column-end: span 5 }
.col-6  { grid-column-end: span 6 }
.col-7  { grid-column-end: span 7 }
.col-8  { grid-column-end: span 8 }
.col-9  { grid-column-end: span 9 }
.col-10 { grid-column-end: span 10 }
.col-11 { grid-column-end: span 11 }
.col-12 { grid-column-end: span 12 }
.col-start-1  { grid-column-start: 1 }
.col-start-2  { grid-column-start: 2 }
.col-start-3  { grid-column-start: 3 }
.col-start-4  { grid-column-start: 4 }
.col-start-5  { grid-column-start: 5 }
.col-start-6  { grid-column-start: 6 }
.col-start-7  { grid-column-start: 7 }
.col-start-8  { grid-column-start: 8 }
.col-start-9  { grid-column-start: 9 }
.col-start-10 { grid-column-start: 10 }
.col-start-11 { grid-column-start: 11 }
.col-start-12 { grid-column-start: 12 }

/* ---- Sticky scenes -------------------------------------------------------
   How to run more than one sticky section on a page without them fighting.

   Two rules, and the first is the one that actually matters:

   1. **Scope.** A sticky element is pinned for as long as its *containing
      block* is on screen — so a band sitting directly in <main> is live for
      the whole page, and two of them end up on screen together (which is
      exactly what happened: the alpine band was pinned to the bottom of the
      window from load and painted over the vision photo). Wrapping each band
      with the section it interacts with in a `.sticky-scene` makes the
      wrapper the containing block, so the band physically cannot appear
      before that scene arrives or after it has passed. No JS, no z-index
      arms race, and scenes can be reordered freely.

   2. **Layer.** Inside a scene the band sits behind and the sections sit in
      front — `.sticky-scene__back` against the lift below. A static element
      paints below a positioned one, so every section has to be positioned
      for that to hold, whether it is a direct child of <main> or of a scene.

   To add a sticky section: wrap it and its partner section in a
   `.sticky-scene`, mark the layer that goes behind, and give it its own
   `position: sticky` + offset (see the two bands for the `top` vs `bottom`
   cases). Don't put `overflow` on the wrapper — it disables sticky. */
main > *,
.sticky-scene > * { position: relative; z-index: 1; }
.sticky-scene__back { z-index: 0; }

/* ---- Buttons ------------------------------------------------------------ */
.btn {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 5px;
  /* One variable drives height and the swipe geometry, so the 35.5° angle
     holds at every size (run = height / tan 35.5° = height × 1.4025) */
  --btn-h: 56px;
  --btn-run: calc(var(--btn-h) * 1.4025);
  height: var(--btn-h);
  padding: 10px 20px 10px 25px;
  border: 0;                 /* every .btn was an <a> until the contact form's
                                <button>, which arrived with a UA border */
  border-radius: 999px;
  overflow: hidden;
  font-family: var(--font-sans);
  font-size: 16px;
  font-weight: 500;
  line-height: 1.3;
  white-space: nowrap;
  cursor: pointer;
  /* colour is delayed so it flips under the swipe, not ahead of it */
  transition: var(--t-btn);
}
.btn > * { position: relative; z-index: 1; }

/* Arrow icon: resting state is the chevron only; on hover the shaft draws
   in and the chevron slides right to form the arrow, while the button
   widens subtly to accommodate it */
.btn-arrow { width: 16px; height: 16px; overflow: visible; }
.btn-arrow__chevron {
  transform: translateX(-2.6px);
  transition: transform 0.3s ease;
}
.btn-arrow__shaft {
  transform: scaleX(0);
  transform-origin: 13.6px 8px;
  transition: transform 0.3s ease;
}
.btn:hover .btn-arrow__chevron,
.btn:focus-visible .btn-arrow__chevron { transform: translateX(0); }
.btn:hover .btn-arrow__shaft,
.btn:focus-visible .btn-arrow__shaft { transform: scaleX(1); }
.btn:hover, .btn:focus-visible { padding-right: 26px; }
@media (prefers-reduced-motion: reduce) {
  .btn-arrow__chevron, .btn-arrow__shaft { transition: none; }
}

/* Solid primary buttons: colour swipe on hover (per Figma mock 1386-7687).
   The layer slides in from the left with an angled leading edge, top corner
   leading (35.5° from horizontal → 79px run over the 56px height). */
.btn--teal, .btn--gold, .btn--orange { color: var(--c-cream); }
.btn--teal { background: var(--c-teal); }
.btn--gold { background: var(--c-gold); }
.btn--orange { background: var(--c-orange); }
/* Cream on a dark background (the Join CTA): dark label, swipes to orange */
.btn--cream { background: var(--c-cream); color: var(--c-dark-green); }
.btn--cream:hover, .btn--cream:focus-visible { color: var(--c-cream); }
.btn--teal::before, .btn--gold::before, .btn--orange::before, .btn--cream::before {
  content: "";
  position: absolute;
  /* 1px bleed top and bottom so the angled edge fully covers the pill */
  top: -1px;
  bottom: -1px;
  left: 0;
  width: calc(100% + var(--btn-run));
  clip-path: polygon(0 0, 100% 0, calc(100% - var(--btn-run)) 100%, 0 100%);
  transform: translateX(-102%);
  transition: transform 0.45s cubic-bezier(0.65, 0, 0.35, 1);
}
.btn--teal::before { background: var(--c-gold); }
.btn--gold::before { background: var(--c-teal); }
/* Orange swipes to cream, so the label flips to dark green mid-sweep */
.btn--orange::before { background: var(--c-cream); }
.btn--orange:hover, .btn--orange:focus-visible { color: var(--c-dark-green); }
.btn--cream::before { background: var(--c-orange); }
.btn--teal:hover::before, .btn--teal:focus-visible::before,
.btn--gold:hover::before, .btn--gold:focus-visible::before,
.btn--orange:hover::before, .btn--orange:focus-visible::before,
.btn--cream:hover::before, .btn--cream:focus-visible::before {
  transform: translateX(0);
}
@media (prefers-reduced-motion: reduce) {
  .btn--teal::before, .btn--gold::before, .btn--orange::before,
  .btn--cream::before { transition: none; }
}
/* Outline on light backgrounds — fills with dark green on hover */
.btn--outline-green {
  border: 1px solid var(--c-dark-green);
  color: var(--c-dark-green);
}
.btn--outline-green::before {
  content: "";
  position: absolute;
  top: -1px;
  bottom: -1px;
  left: 0;
  width: calc(100% + var(--btn-run));
  clip-path: polygon(0 0, 100% 0, calc(100% - var(--btn-run)) 100%, 0 100%);
  background: var(--c-dark-green);
  transform: translateX(-102%);
  transition: transform 0.45s cubic-bezier(0.65, 0, 0.35, 1);
}
.btn--outline-green:hover, .btn--outline-green:focus-visible { color: var(--c-cream); }
.btn--outline-green:hover::before,
.btn--outline-green:focus-visible::before { transform: translateX(0); }

/* Outline gold — fills with gold on hover */
.btn--outline-gold {
  border: 1px solid var(--c-gold);
  color: var(--c-gold);
}
.btn--outline-gold::before {
  content: "";
  position: absolute;
  top: -1px;
  bottom: -1px;
  left: 0;
  width: calc(100% + var(--btn-run));
  clip-path: polygon(0 0, 100% 0, calc(100% - var(--btn-run)) 100%, 0 100%);
  background: var(--c-gold);
  transform: translateX(-102%);
  transition: transform 0.45s cubic-bezier(0.65, 0, 0.35, 1);
}
.btn--outline-gold:hover, .btn--outline-gold:focus-visible { color: var(--c-cream); }
.btn--outline-gold:hover::before,
.btn--outline-gold:focus-visible::before { transform: translateX(0); }

/* Tertiary link — no pill, just gold label + arrow (shares the arrow hover) */
.link-tertiary {
  display: inline-flex;
  align-items: center;
  gap: 1px;
  padding: 10px 0;
  font-size: 16px;
  font-weight: 500;
  line-height: 1.3;
  color: var(--c-gold);
  transition: gap 0.3s ease;
}
.link-tertiary:hover, .link-tertiary:focus-visible { gap: 6px; }
.link-tertiary:hover .btn-arrow__chevron,
.link-tertiary:focus-visible .btn-arrow__chevron { transform: translateX(0); }
.link-tertiary:hover .btn-arrow__shaft,
.link-tertiary:focus-visible .btn-arrow__shaft { transform: scaleX(1); }

/* White pill with a hairline stroke — the News pager. Sits on cream, so it
   needs a fill of its own to read as a control; otherwise it follows the
   outline-green language and swipes to dark green. */
.btn--white {
  border: 1px solid var(--c-green-20);
  background: #fff;
  color: var(--c-dark-green);
}
/* The swipe is cream rather than dark green (Will's call — "more subtle"),
   so the label keeps its dark-green colour throughout: the fill only warms
   the pill, it doesn't invert it. It uses `--c-cream-50`, not `--c-cream`:
   the page band behind these buttons IS `--c-cream`, so that value made the
   hovered pill vanish into the background. The 50 tint is a shade deeper —
   same idea, still visible. */
.btn--white::before {
  content: "";
  position: absolute;
  top: -1px;
  bottom: -1px;
  left: 0;
  width: calc(100% + var(--btn-run));
  clip-path: polygon(0 0, 100% 0, calc(100% - var(--btn-run)) 100%, 0 100%);
  background: var(--c-cream-50);
  transform: translateX(-102%);
  transition: transform 0.45s cubic-bezier(0.65, 0, 0.35, 1);
}
.btn--white:hover::before, .btn--white:focus-visible::before { transform: translateX(0); }
/* A pager direction with nowhere to go — the CMS adds `disabled` */
.btn--white[disabled] { opacity: 0.4; cursor: default; }
.btn--white[disabled]:hover::before { transform: translateX(-102%); }
@media (prefers-reduced-motion: reduce) {
  .btn--white::before { transition: none; }
}
/* The Previous arrow is the same asset pointing the other way */
.btn-arrow--back { transform: scaleX(-1); }

/* Outline on dark backgrounds — fills with cream on hover.
   Transparent so it works on teal (mobile menu) and dark green (stats). */
.btn--outline-cream {
  background: transparent;
  color: var(--c-cream);
  border: 1px solid var(--c-cream);
}
.btn--outline-cream::before {
  content: "";
  position: absolute;
  top: -1px;
  bottom: -1px;
  left: 0;
  width: calc(100% + var(--btn-run));
  clip-path: polygon(0 0, 100% 0, calc(100% - var(--btn-run)) 100%, 0 100%);
  background: var(--c-cream);
  transform: translateX(-102%);
  transition: transform 0.45s cubic-bezier(0.65, 0, 0.35, 1);
}
.btn--outline-cream:hover, .btn--outline-cream:focus-visible { color: var(--c-dark-green); }
.btn--outline-cream:hover::before,
.btn--outline-cream:focus-visible::before { transform: translateX(0); }

/* ---- Page load sequence -------------------------------------------------
   0.12s  logomark draws in (compressed Figma draw spec)
   1.75s  logomark reverses back out (trim2 first, then trim1)...
   1.80s  ...just as the loader wipes away left→right (35.5° angled edge)
   2.90s  wipe complete (its decelerating tail looks clear from ~2.7s)
   2.70s  header fades in and drops into place, overlapping the wipe tail
   2.90s+ heading falls in line by line (masks built in main.js, 0.15s stagger)
   3.30s  paragraph, then button, settle in
   3.60s  video section swipes in (same angle), closing the sequence        */
.loader {
  position: fixed;
  inset: 0;
  z-index: 100;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--c-dark-green);
  animation: loader-wipe 1.1s cubic-bezier(0.65, 0, 0.35, 1) var(--loader-delay, 1.8s) forwards;
}
/* Inner pages get the swipe alone — no logomark draw, so it starts almost at
   once. The homepage keeps the full opening. */
.loader--swipe { --loader-delay: 0.15s; }

/* Page transition: leaving a page runs the same wipe in reverse duty — it
   sweeps in from the left to cover, then the next page's loader sweeps it off
   in the same direction. Both share the 35.5° edge, top corner leading, so
   the two halves read as one continuous movement across the navigation. */
.loader.is-covering {
  visibility: visible;
  animation: page-cover 0.7s cubic-bezier(0.65, 0, 0.35, 1) forwards;
}
@keyframes page-cover {
  from { clip-path: polygon(0 0, 0 0, calc(0% - 140vh) 100%, 0 100%); }
  to   { clip-path: polygon(0 0, calc(100% + 140vh) 0, 100% 100%, 0 100%); }
}
.logomark--loader { width: 188px; height: auto; }
.logomark--loader .logomark__trim1 {
  animation: lm-trim1 1.6s linear 0.12s both,
             lm-undraw 0.55s cubic-bezier(0.65, 0, 0.35, 1) 1.83s forwards;
}
.logomark--loader .logomark__trim2 {
  animation: lm-trim2 1.6s linear 0.12s both,
             lm-undraw 0.55s cubic-bezier(0.65, 0, 0.35, 1) 1.75s forwards;
}
@keyframes lm-undraw {
  from { stroke-dasharray: 1 1; }
  to   { stroke-dasharray: 0 1; }
}
@keyframes loader-wipe {
  from {
    clip-path: polygon(0 0, 100% 0, 100% 100%, -140vh 100%);
  }
  to {
    clip-path: polygon(calc(100% + 140vh) 0, 100% 0, 100% 100%, 100% 100%);
    visibility: hidden;
  }
}

/* Entrances (fill: both keeps elements hidden until their delay) */
@keyframes drop-in {
  from { opacity: 0; transform: translateY(-16px); }
  to   { opacity: 1; transform: none; }
}
@keyframes settle-in {
  from { opacity: 0; transform: translateY(-12px); }
  to   { opacity: 1; transform: none; }
}
/* The header drops in just as the loader's wipe tail clears, so it is always
   in place BEFORE the page's own content reveals. The two loaders finish at
   different times — the homepage's runs 1.8s -> 2.9s, an inner page's swipe
   runs 0.15s -> 1.25s — so this delay has to follow the variant. It was a
   flat 2.7s tuned for the homepage, which left every inner page showing its
   heading a second and a half before the header arrived; invisible where a
   hero band pushed the content down the page, obvious on the article, where
   the title sits directly under the header. Will caught it there. */
.site-header { animation: drop-in 0.65s cubic-bezier(0.33, 1, 0.68, 1) var(--header-in, 2.7s) both; }
body:has(.loader--swipe) { --header-in: 1.05s; }

/* Video section swipes in last, same 35.5° angle (run = height / tan 35.5°).
   fill-mode is `backwards`, not `both`: backwards holds the hidden state
   through the delay but lets the clip-path fall away once the wipe is done —
   with `both` the final polygon stays applied and keeps clipping the line
   graphic that now overhangs the section. */
.section-video {
  --wipe-run: 752px;
  animation: video-reveal 1s cubic-bezier(0.65, 0, 0.35, 1) 3.6s backwards;
}
@keyframes video-reveal {
  from { clip-path: polygon(0 0, 0 0, calc(-1 * var(--wipe-run)) 100%, 0 100%); }
  to   { clip-path: polygon(0 0, calc(100% + var(--wipe-run)) 0, 100% 100%, 0 100%); }
}
/* A running transform/opacity animation makes the header a stacking context,
   which would trap the menu button beneath the overlay — so the animation is
   cleared by main.js once it finishes, and suppressed if the menu opens early */
body.menu-open .site-header { animation: none; }

/* Heading line masks (spans injected by main.js) */
.line-mask {
  display: block;
  overflow: hidden;
  /* let descenders paint while masked */
  padding-bottom: 0.12em;
  margin-bottom: -0.12em;
}
.line { display: block; transform: translateY(-110%); opacity: 0; }
/* Two animations so each gets its own curve: the drop eases out (fast, then
   settles), while the fade runs linearly and starts late — the line is still
   mostly behind the mask early on, so an early fade isn't visible. Delays are
   composed from --line-delay, which main.js sets per line for the stagger. */
[data-reveal-lines].is-visible .line {
  animation: line-fall 0.9s cubic-bezier(0.33, 1, 0.68, 1) var(--line-delay, 0s) both,
             line-fade 0.7s linear calc(var(--line-delay, 0s) + 0.2s) both;
}
@keyframes line-fall {
  from { transform: translateY(-110%); }
  to   { transform: none; }
}
@keyframes line-fade {
  from { opacity: 0; }
  to   { opacity: 1; }
}

@media (prefers-reduced-motion: reduce) {
  .loader { display: none; }
  .site-header,
  .section-title__aside p,
  .section-title__aside .btn,
  .section-video,
  .line { animation: none !important; }
}

/* ---- Header ------------------------------------------------------------- */
.site-header {
  position: sticky;
  top: 0;
  /* Above the page (sections raise their own content to z-index: 1), but below
     the mobile menu overlay (50) so the overlay can cover it, and below the
     loader (100). */
  z-index: 40;
  /* Transparent: the section behind shows through. The header still takes its
     place in the flow, so nothing runs underneath it at rest. */
  background: transparent;
  /* Coming back: brisk, so it's there when you want it. */
  transition: transform 0.6s cubic-bezier(0.33, 1, 0.68, 1), opacity 0.4s ease;
}
/* Scrolling down takes the whole bar with it; scrolling back brings it in.
   Leaving is unhurried — a long ease-in so it drifts rather than snaps, with
   the fade running most of that time. main.js also holds it in place until
   you're 400px down and have made a sustained downward move. */
.site-header.is-hidden {
  transform: translateY(-100%);
  opacity: 0;
  pointer-events: none;
  transition: transform 1.1s cubic-bezier(0.5, 0, 0.35, 1), opacity 0.95s ease-in;
}

/* Over a dark section the gold mark and the menu lines disappear, so both
   switch to cream. main.js sets .is-over-dark from the sections marked
   data-header-theme="light". The nav pill and CTA need no help — they carry
   their own backgrounds — and by the time the header returns mid-page the
   logotype has faded out, so the mark is the only bare artwork at any width. */
.site-header.is-over-dark .logomark__trim1,
.site-header.is-over-dark .logomark__trim2 { stroke: var(--c-cream); }
.site-header.is-over-dark .menu-btn { color: var(--c-cream); }
/* ...unless the section behind it asks for gold (data-header-logo="gold" —
   the photo heroes). Less legible than cream on a busy photo, deliberately:
   Will wants the colour there. The menu button stays cream, since a hamburger
   has no brand value to trade legibility for. */
.site-header.is-over-dark.is-logo-gold .logomark__trim1,
.site-header.is-over-dark.is-logo-gold .logomark__trim2 { stroke: var(--c-gold); }
.logomark__trim1, .logomark__trim2 { transition: stroke 0.35s ease; }
/* While the menu is open the header has to rise above the overlay, or the
   stacking context it owns would trap the menu button underneath. The logo
   goes with it, so it's faded out and the teal panel reads clean — only the
   (cream) button shows through. */
body.menu-open .site-header { z-index: 60; }
.site-header .logo { transition: opacity 0.3s ease; }
body.menu-open .site-header .logo { opacity: 0; pointer-events: none; }
/* The header bar still sits above the open panel (the close button lives in
   it), and its full-width inner div was swallowing every click that landed on
   the panel's own logo underneath. Let the bar pass clicks through and give
   them back to the button alone. */
body.menu-open .site-header,
body.menu-open .site-header__inner { pointer-events: none; }
body.menu-open .menu-btn { pointer-events: auto; }

/* In-page links land clear of the sticky bar */
section[id], [id]:target { scroll-margin-top: calc(var(--header-h) + 20px); }
.site-header__inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 20px;
  max-width: 1800px;
  margin: 0 auto;
  padding: 25px var(--pad-x);
  min-height: var(--header-h);
}
.site-header__cta { flex-shrink: 0; }

/* Logo lock-up: animated logomark above logotype */
.logo { display: block; }
.logo__mark { display: block; width: 45.7px; margin-bottom: 16.6px; }
.logo__mark .logomark { width: 45.7px; height: 19.8px; }
.logo__type { width: 153.7px; height: auto; }

/* Scroll-linked header logo (Figma 1409-111596 "Header - on scroll").
   main.js writes --logo-p on the header: 0 at the top of the page, 1 once the
   transformation is complete. The logomark grows 45.7px → 89.5px wide (×1.958)
   and drops 14.2px to sit centred in the lock-up's height, while the logotype
   fades out from under it. Both are transforms/opacity only, so the lock-up
   keeps its 67px box and the header stays exactly 107px tall throughout. */
.site-header .logo__mark {
  transform-origin: left top;
  transform: translateY(calc(var(--logo-p, 0) * 14.2px))
             scale(calc(1 + var(--logo-p, 0) * 0.958));
  will-change: transform;
}
.site-header .logo__type {
  /* clear by ~60% of the way, so the mark finishes growing into empty space */
  opacity: calc(1 - var(--logo-p, 0) * 1.7);
}


/* Logomark draw-on animation (Figma motion spec timing; plays once on load) */
.logomark--animated .logomark__trim1 {
  stroke-dashoffset: 0;
  animation: lm-trim1 2.002s linear 1 forwards;
}
.logomark--animated .logomark__trim2 {
  stroke-dashoffset: 0;
  animation: lm-trim2 2.002s linear 1 forwards;
}
@keyframes lm-trim1 {
  0%       { stroke-dasharray: 0 1; visibility: hidden;
             animation-timing-function: cubic-bezier(0.98, 0, 0.58, 1.01); }
  60.149%  { stroke-dasharray: 1 1; visibility: visible; }
  100%     { stroke-dasharray: 1 1; visibility: visible; }
}
@keyframes lm-trim2 {
  0%       { stroke-dasharray: 0 1; visibility: hidden; }
  9.579%   { stroke-dasharray: 0 1; visibility: hidden;
             animation-timing-function: cubic-bezier(0.979, 0.001, 0.577, 1.01); }
  69.94%   { stroke-dasharray: 1 1; visibility: visible; }
  100%     { stroke-dasharray: 1 1; visibility: visible; }
}
@media (prefers-reduced-motion: reduce) {
  .logomark--animated .logomark__trim1,
  .logomark--animated .logomark__trim2 { animation: none; }
}
/* Hover replay, phase 1: quick un-draw — main.js then restarts the
   standard draw. Must come after the --animated rules to win the cascade. */
.logomark--undrawing .logomark__trim1,
.logomark--undrawing .logomark__trim2 {
  animation: lm-undraw 0.35s cubic-bezier(0.65, 0, 0.35, 1) both;
}

/* Pill nav — cream background lives on a pseudo element so it can scale
   up slightly on hover without touching the links */
.nav-pill {
  position: relative;
  display: flex;
  align-items: center;
  gap: 25px;
  height: 56px;
  padding: 10px 40px;
  border-radius: 999px;
}
.nav-pill::before {
  content: "";
  position: absolute;
  inset: 0;
  background: var(--c-cream);
  border-radius: 999px;
  transition: transform 0.3s ease;
}
.nav-pill:hover::before { transform: scale(1.04); }
.nav-pill a {
  position: relative;
  z-index: 1;
  font-size: 16px;
  line-height: 1.3;
  color: var(--c-dark-green);
  transition: opacity 0.25s ease;
}
/* Spotlight: siblings dim only while an individual item is hovered */
.nav-pill:has(a:hover) a:not(:hover) { opacity: 0.5; }
/* Active-page signpost (add .is-active to the current page's link):
   when nothing is hovered, non-active items sit at 50% */
.nav-pill:has(.is-active):not(:has(a:hover)) a:not(.is-active) { opacity: 0.5; }

/* Menu toggle — single button; the three lines morph into a cross when
   the menu is open (middle line fades, outer lines rotate to meet) */
.menu-btn {
  display: none;
  align-items: center;
  justify-content: center;
  width: 36px;
  height: 56px;
  padding: 0;
  border: 0;
  background: none;
  cursor: pointer;
  color: var(--c-dark-green);
  position: relative;
  z-index: 60; /* stays above the menu overlay */
  transition: color 0.3s ease;
}
body.menu-open .menu-btn { color: var(--c-cream); }
.menu-icon path { transition: transform 0.3s ease, opacity 0.2s ease; }
.menu-icon__l1 { transform-origin: 18px 21px; }
.menu-icon__l2 { transform-origin: 18px 28px; }
.menu-icon__l3 { transform-origin: 18px 35px; }
body.menu-open .menu-icon__l1 { transform: translateY(7px) rotate(45deg) scaleX(0.8); }
body.menu-open .menu-icon__l2 { opacity: 0; }
body.menu-open .menu-icon__l3 { transform: translateY(-7px) rotate(-45deg) scaleX(0.8); }

/* ---- Mobile menu overlay ------------------------------------------------ */
.mobile-menu {
  position: fixed;
  inset: 0;
  z-index: 50;
  /* scrim over the rest of the page; tap it to close */
  background: rgba(0, 0, 0, 0.75);
  opacity: 0;
  visibility: hidden;
  transition: opacity 0.4s ease, visibility 0.4s ease;
}
.mobile-menu.is-open { opacity: 1; visibility: visible; }
.mobile-menu__panel {
  position: relative;
  display: flex;
  flex-direction: column;
  height: 80vh;
  height: 80dvh;
  min-height: 340px;
  background: var(--c-teal);
  padding: 5px 15px 25px;
  transform: translateY(-100%);
  transition: transform 0.4s cubic-bezier(0.65, 0, 0.35, 1);
}
.mobile-menu.is-open .mobile-menu__panel { transform: translateY(0); }
.mobile-menu__bar {
  display: flex;
  align-items: center;
  min-height: 56px;
}
.mobile-menu__bar .logomark { width: 59.4px; height: auto; }
.mobile-menu__nav {
  display: flex;
  flex-direction: column;
  gap: 5px;
  margin-top: auto;
}
/* Held back at half strength so the current page — which keeps full opacity
   via .is-active — reads as "you are here" without needing a second colour. */
.mobile-menu__nav a {
  font-size: 24px;
  line-height: 1.3;
  color: var(--c-cream);
  opacity: 0.5;
  transition: opacity 0.3s ease;
}
.mobile-menu__nav a.is-active,
.mobile-menu__nav a:hover,
.mobile-menu__nav a:focus-visible { opacity: 1; }
.mobile-menu__cta {
  position: absolute;
  right: 15px;
  bottom: 25px;
}

/* ---- Title section ------------------------------------------------------ */
/* Keeps the button above the video, which on mobile is pulled up underneath it
   by a negative margin. Must stay above .section-video's z-index (2, which
   that section needs so its line graphic can overhang the intro below). */
.section-title { position: relative; z-index: 3; }
.section-title .content-wrapper {
  padding-top: 131px;
  padding-bottom: 40px;
}
/* 8 : 4 on the 12-column grid — the design's 916 : 444. The intro row below
   is the same split, so both right-hand columns start on column 9. */
.section-title__content { align-items: stretch; }
.section-title__heading {
  max-width: 12em;
  font-family: var(--font-serif);
  font-size: clamp(36px, 5vw, 72px);
  line-height: 1;
  letter-spacing: -0.02em;
  color: var(--c-dark-green);
}
.section-title__aside {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  justify-content: space-between;
  /* A floor under space-between: as the column narrows the paragraph grows
     taller, the leftover space runs out, and the copy would otherwise sit
     straight on top of the button. (Matches the mobile rule's gap.) */
  gap: 20px;
  padding: 5px 0 10px 0;
}
.section-title__aside p {
  font-size: 18px;
  line-height: 1.3;
  color: var(--c-green-80);
}

/* Aside/secondary copy: the columns are proportional, so on a wide screen they
   grow past a comfortable measure. Cap the type, not the column — the column
   still holds its share of the grid so everything stays aligned. */
.section-title__aside p,
.section-intro__aside,
.article__excerpt,
.section-cta__text { max-width: 25em; }

/* Inline icon sprite + clip-path defs (tools/build-icons.py, build-shapes.py)
   — definitions only, never rendered */
.icon-sprite, .shape-defs { position: absolute; width: 0; height: 0; overflow: hidden; }

/* Phosphor icons. The sprite's shapes carry no stroke/fill of their own, so
   these inherit down into the <use> shadow tree. Paired with
   vector-effect: non-scaling-stroke, stroke-width is in real px at any size. */
.icon {
  display: block;
  fill: none;
  stroke: currentColor;
  stroke-width: 3px;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* Accessible hidden labels */
.visually-hidden {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}

/* ---- Video section ------------------------------------------------------ */
.section-video {
  position: relative;
  /* above the intro section, so the graphic can overhang into it */
  z-index: 2;
  background: var(--c-dark-green);
  height: 536px;
}
.section-video__media {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  opacity: 0.5;
}
/* Orange line graphic laid over the footage (Figma `section/graphic`). It is
   full-bleed, so it scales with the viewport and keeps its own 1440:312 ratio
   — hence the offset is in vw, so it hangs over the intro section by the same
   proportion at any width. --video-graphic-y is the single nudge value.

   It draws in left to right as you scroll: main.js writes --graphic-p (0 at
   the top of the page, 1 once you've scrolled GRAPHIC_OVER px) and the inset
   follows. The var defaults to 1, so without JS it is simply all there. */
.section-video__graphic {
  position: absolute;
  left: 0;
  bottom: var(--video-graphic-y, -5.5vw);
  width: 100%;
  height: auto;
  pointer-events: none;
  clip-path: inset(0 calc((1 - var(--graphic-p, 1)) * 100%) 0 0);
}
@media (prefers-reduced-motion: reduce) {
  .section-video__graphic { clip-path: none; }
}

/* ---- Intro section ------------------------------------------------------ */
.eyebrow {
  font-size: 14px;
  line-height: 1.3;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--c-green-40);
}
/* An eyebrow used as a column label, top-aligned in a grid row beside much
   larger type. 14px sets its first baseline higher than a 48–72px heading's,
   so the label reads as sitting above the line it should share — 5px puts it
   back (Will's call, alpine is the reference). Only for side labels: an
   eyebrow stacked above its own heading needs no nudge. */
.eyebrow--label { margin-top: 5px; }
.section-intro {
  position: relative;
  overflow: hidden;
  background: linear-gradient(240deg, var(--c-teal) 1.6%, var(--c-dark-green) 72.9%);
}
.section-intro .content-wrapper {
  position: relative;
  z-index: 1;
  padding-top: 80px;
  padding-bottom: 40px;
}
.section-intro__content { display: flex; flex-direction: column; gap: 152px; }
.section-intro__head { display: flex; flex-direction: column; gap: 32px; }
/* 8 : 4, matching the title row above (design 914 : 446) */
.section-intro__cols { align-items: start; }
.section-intro__heading {
  font-family: var(--font-serif);
  font-size: clamp(30px, 3.33vw, 48px);
  line-height: 1.05;
  color: var(--c-cream);
  max-width: 19em;
}
.section-intro__heading em { font-style: italic; font-weight: 400; }
.section-intro__aside {
  font-size: 18px;
  line-height: 1.3;
  color: var(--c-green-40);
}
.section-intro__body { display: flex; flex-direction: column; gap: 87px; align-items: flex-start; }

/* Cards */
.card-list {
  display: flex;
  gap: 15px;
  width: 100%;
  margin: 0;
  padding: 0;
  list-style: none;
  align-items: stretch;
}
.card {
  flex: 1 1 0;
  min-width: 1px;
  display: flex;
  flex-direction: column;
  gap: 15px;
  padding: 25px;
  border-radius: 4px;
  background: var(--c-teal);
}
.card__icon { display: block; width: 64px; height: 64px; color: var(--c-orange); }
.card__title {
  font-family: var(--font-serif);
  font-weight: 500;
  font-size: 24px;
  line-height: 1.15;
  color: #fff;
  margin-top: 3px;
}
.card__title em { font-style: italic; }
.card__text {
  font-size: 18px;
  line-height: 1.3;
  color: var(--c-green-40);
}

/* Corner graphic */
.section-intro__graphic {
  position: absolute;
  right: 0;
  bottom: 0;
  height: 86px;
  width: auto;
}

/* ---- Reveal system (driven by main.js) ----------------------------------
   [data-reveal]             fade + drop      — h3, paragraphs, cards, buttons
   [data-reveal="fade"]      fade only, no movement at all
   [data-reveal="from-left"] fade + slide in from that side; also -right,
                             -above, -below
   [data-reveal-lines]       falls in line by line — h1, h2
   Both trigger on scroll, or on a load-sequence cue via data-reveal-at.
   Stagger siblings with --reveal-delay (or data-reveal-delay in the markup).
   Scoped to .js (set by main.js) so that without JS nothing is hidden.

   Why the variants are split this way: the layered image blocks (angle,
   round, support) position their parts with `transform: translateX(...)`, so
   a reveal that *sets* transform would throw the artwork out of register.
   Only the default variant touches transform directly. `fade` leaves it
   alone, and the `from-*` variants write --slide-x / --slide-y, which the
   element composes into its own transform — see .angle-image__stripe. */
.js [data-reveal] {
  opacity: 0;
  transition: opacity 0.9s ease var(--reveal-delay, 0s),
              transform 0.9s cubic-bezier(0.33, 1, 0.68, 1) var(--reveal-delay, 0s);
}
/* Default variant: an empty attribute, i.e. plain `data-reveal` */
.js [data-reveal=""] { transform: translateY(-16px); }
.js [data-reveal=""].is-visible { transform: none; }
.js [data-reveal="from-left"]  { --slide-x: -34px; }
.js [data-reveal="from-right"] { --slide-x: 34px; }
.js [data-reveal="from-above"] { --slide-y: -34px; }
.js [data-reveal="from-below"] { --slide-y: 34px; }
/* A button that also reveals keeps its own hover transitions */
.js .btn[data-reveal] {
  transition: opacity 0.9s ease var(--reveal-delay, 0s),
              transform 0.9s cubic-bezier(0.33, 1, 0.68, 1) var(--reveal-delay, 0s),
              var(--t-btn);
}
/* Settled state. `--reveal-opacity` lets an element rest below full opacity
   (e.g. a background graphic at 0.35) without the reveal snapping it to 1. */
.js [data-reveal].is-visible { opacity: var(--reveal-opacity, 1); --slide-x: 0px; --slide-y: 0px; }
/* Headings stay hidden until their lines have been split and can fall in */
.js [data-reveal-lines]:not(.is-visible) { visibility: hidden; }
/* Intro section: a considered sequence rather than everything at once.
   eyebrow → heading lines (0.25s, +0.18s each) → aside → cards → button */
.section-intro__aside { --reveal-delay: 0.75s; }
.section-intro .card:nth-child(1) { --reveal-delay: 1.05s; }
.section-intro .card:nth-child(2) { --reveal-delay: 1.35s; }
.section-intro .card:nth-child(3) { --reveal-delay: 1.65s; }
.section-intro .btn { --reveal-delay: 1.95s; }
/* About values: the same 0.3s rhythm, five cards.
   (Scoped — an unscoped .card:nth-child would reach across both pages.) */
.about-values .card:nth-child(1) { --reveal-delay: 0.75s; }
.about-values .card:nth-child(2) { --reveal-delay: 1.05s; }
.about-values .card:nth-child(3) { --reveal-delay: 1.35s; }
.about-values .card:nth-child(4) { --reveal-delay: 1.65s; }
.about-values .card:nth-child(5) { --reveal-delay: 1.95s; }
@media (prefers-reduced-motion: reduce) {
  .js [data-reveal] { opacity: var(--reveal-opacity, 1); transition: none; --slide-x: 0px; --slide-y: 0px; }
  .js [data-reveal=""] { transform: none; }
  .js [data-reveal-lines]:not(.is-visible) { visibility: visible; }
  .line { transform: none; }
}

/* ---- Companies section -------------------------------------------------- */
.section-companies {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 60px;
  padding: 80px 0;
  background: var(--c-teal);
  overflow: hidden;
}
/* Faint factory photo behind the teal (Figma 1359:48886 — the image layer is
   set to 10%). Everything else in the section is lifted above it. */
.section-companies__bg {
  position: absolute;
  inset: 0;
  opacity: 0.1;
  pointer-events: none;
}
/* Scale parallax: main.js writes --parallax-p (−1 as the section rises into
   view, +1 as it leaves), so the photo grows ~12% across the pass. Scaling up
   only — a scale below 1 would pull the crop in from the section's edges. */
.section-companies__image {
  width: 100%;
  height: 100%;
  object-fit: cover;
  transform: scale(calc(1.06 + var(--parallax-p, 0) * 0.06));
  will-change: transform;
  /* The design hangs a taller crop off the top of the section rather than
     centring it; 29% down the scaled photo frames the same band. */
  object-position: 50% 29%;
}
.section-companies > *:not(.section-companies__bg) {
  position: relative;
  z-index: 1;
}
.section-companies .content-wrapper { width: 100%; }
.section-companies__heading {
  font-family: var(--font-serif);
  font-size: clamp(36px, 3.9vw, 56px);
  line-height: 1.05;
  text-align: center;
  color: var(--c-cream);
}
.section-companies__heading em { font-style: italic; }
.section-companies__foot { display: flex; justify-content: center; }

/* Logo marquee — full-bleed, so items run off both edges */
.marquee {
  --item-w: 250px;
  --item-h: 156px;
  --item-gap: 8px;
  --logo-scale: 1;
  /* one set is 8 × 258px = 2064px; ~30px/s reads as a calm drift */
  --marquee-duration: 70s;
  width: 100%;
  overflow: hidden;
}
.marquee__track {
  display: flex;
  width: max-content;
  margin: 0;
  padding: 0;
  list-style: none;
  /* The gap lives on the items (not flex `gap`) so one set is an exact third
     of the track, making translateX(-33.3333%) a seamless loop. */
  animation: marquee-scroll var(--marquee-duration) linear infinite;
  animation-delay: 0.9s;          /* lets the items arrive before it moves */
  animation-play-state: paused;
}
.marquee.is-visible .marquee__track { animation-play-state: running; }
@keyframes marquee-scroll {
  from { transform: translateX(0); }
  to   { transform: translateX(-50%); }  /* exactly one of the two sets */
}
.marquee__item {
  flex: 0 0 var(--item-w);
  height: var(--item-h);
  margin-right: var(--item-gap);
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 4px;
  background: var(--c-dark-green);
  /* sequential arrival — small overlap keeps the whole run brisk */
  opacity: 0;
  transform: translateY(14px);
  transition: opacity 0.6s ease calc(var(--i, 0) * 0.1s),
              transform 0.6s cubic-bezier(0.33, 1, 0.68, 1) calc(var(--i, 0) * 0.1s);
}
.marquee.is-visible .marquee__item { opacity: 1; transform: none; }
.marquee__logo { height: auto; }
/* Each mark is drawn at its own width — they differ in aspect and optical
   weight, so a tall square mark at a wordmark's width dwarfs it. That used to
   be eight rules here, one per partner. It is now the `logoRowWidth` field on
   the partner's record, written inline by partials/marquee-item.html, so a
   ninth partner needs no CSS. `--logo-scale` still lives here: it is the
   responsive part, and it applies to all of them equally. */

@media (prefers-reduced-motion: reduce) {
  .marquee__track { animation: none; }
  .marquee__item { opacity: 1; transform: none; transition: none; }
}

/* ---- Stats section ------------------------------------------------------ */
.section-stats { background: var(--c-dark-green); }
.section-stats .content-wrapper {
  /* deeper inset than the site default — 140px at the 1440 design width */
  padding: 100px 9.72%;
}
.section-stats__content { display: flex; flex-direction: column; gap: 86px; }
.section-stats__head {
  display: flex;
  align-items: center;
  gap: 20px;
}
.section-stats__heading {
  flex: 1 0 0;
  min-width: 1px;
  font-family: var(--font-serif);
  font-size: clamp(30px, 2.5vw, 36px);
  line-height: 1.1;
  color: var(--c-cream);
}
.section-stats__heading em { font-style: italic; }

.stat-list {
  display: flex;
  gap: 30px;
  margin: 0;
  padding: 0;
  list-style: none;
}
.stat {
  flex: 1 0 0;
  min-width: 1px;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 10px;
}
.stat__value {
  font-size: clamp(40px, 3.85vw, 55.4px);
  font-weight: 200;                 /* Hanken Grotesk ExtraLight */
  line-height: 1.1;
  letter-spacing: -0.04em;
  color: var(--c-cream);
  font-variant-numeric: tabular-nums; /* stops digits jittering while counting */
}
.stat__label {
  font-size: clamp(12px, 0.93vw, 13.45px);
  line-height: 1.3;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--c-green-40);
}
/* Dotted rule closes each stat */
.stat::after {
  content: "";
  display: block;
  width: 100%;
  border-top: 1.5px dotted var(--c-green-40);
}

/* ---- Testimonial section ------------------------------------------------ */
.section-testimonial {
  position: relative;
  overflow: hidden;
  background: var(--c-cream);
}
.section-testimonial .content-wrapper {
  position: relative;
  z-index: 1;
  padding-top: 40px;
  padding-bottom: 40px;
}
.section-testimonial__content {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 319px; /* per design — the graphic fills the space below the quote */
}
.section-testimonial__cols {
  display: flex;
  gap: 20px;
  align-items: flex-start;
  width: 100%;
}
.quote {
  flex: 1153 1 0;
  min-width: 1px;
  display: flex;
  flex-direction: column;
  gap: 30px;
  margin: 0;
}
.quote__text {
  margin: 0;
  font-family: var(--font-serif);
  font-size: clamp(24px, 3.2vw, 46px);
  line-height: 1.05;
  color: var(--c-dark-green);
}
.quote__cite {
  font-size: 18px;
  line-height: 1.3;
  color: var(--c-green-80);
}
.quote__cite cite { font-style: normal; }
.section-testimonial__logo {
  flex: 207 1 0;
  min-width: 1px;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 15px 44px;
}
.section-testimonial__logo img { width: 119px; height: auto; }

/* Every testimonial photo block, whatever shape it is cut to — .angle-image on
   the homepage and the decentralised model page, .round-image on About and
   Founders. They are absolutely positioned against the full-width section
   rather than the content wrapper, so their percentage width keeps growing
   past the point where the text beside them stops: by 2560 the photo was over
   the quote and the partner logo. The cap is the one thing they share, so it
   lives on one class rather than being repeated per shape. Lifted on mobile,
   where each block goes full width by design (see the media block). */
.testimonial-image { max-width: 800px; }

/* Angled image block — reusable. Two offset copies of angle-image-graphic.svg
   sit behind a photo masked to that same shape. The block keeps the graphic's
   757:313 aspect so the 35.5° edge and its rounded corners stay exact at any
   size; the offsets are a % of width, so they scale with it. */
.angle-image {
  position: absolute;
  right: 0;
  bottom: 0;
  width: 52.56%;      /* 756.9 / 1440 in the design */
  aspect-ratio: 757 / 313;
}
.angle-image__stripe,
.angle-image__photo {
  position: absolute;
  inset: 0;
  /* 1% overscale: the source SVG's path stops a fraction short of its own
     viewBox, so at 100% a hairline of background shows along the edges */
  width: 101%;
  height: 101%;
}
/* The static offset and the reveal slide have to live in one transform, so
   the artwork keeps its register while it moves (see the reveal system). */
.angle-image__stripe--back {
  opacity: 0.5;
  transform: translateX(-12.06%)  /* 91.3 / 756.9 */
             translate(var(--slide-x, 0px), var(--slide-y, 0px));
}
.angle-image__stripe--front {
  transform: translateX(-6.21%)   /* 47.02 / 756.9 */
             translate(var(--slide-x, 0px), var(--slide-y, 0px));
}
/* The stripe's own 0.5 opacity is the end state, so it can't inherit the
   reveal's `opacity: 1` */
.js .angle-image__stripe--back.is-visible { opacity: 0.5; }
/* The photo fills the block and is clipped to the graphic's silhouette, so the
   source image stays whole and uncropped — CMS-swappable. The clip path is
   defined inline in index.html (tools/build-shapes.py): an external SVG mask
   silently fails over file://, leaving the photo invisible. */
.angle-image__photo {
  /* The full photo is loaded and positioned within the shape — never
     pre-cropped. 28% down matches the framing in the Figma design. */
  object-fit: cover;
  object-position: 50% 28%;
  clip-path: url("#shape-angle-image-graphic");
}

/* ---- CTA section --------------------------------------------------------- */
.section-cta {
  position: relative;
  overflow: hidden;
  color: #fff;
}
/* Full-bleed background image. The dark green underneath shows while the
   image loads and if it ever fails, so the cream type stays legible. */
.section-cta__bg {
  position: absolute;
  inset: 0;
  overflow: hidden;       /* the image is oversized so it can drift */
  background: var(--c-dark-green);
}
/* Horizontal parallax: main.js writes --parallax-p, running −1 as the section
   enters the viewport to +1 as it leaves, and the image drifts sideways
   across that range. It's 124% wide and pulled back 12%, so at either
   extreme there is still image covering the section — never an edge. */
.section-cta__image {
  width: 124%;
  margin-left: -12%;
  height: 100%;
  object-fit: cover;      /* covers the section at any aspect ratio */
  object-position: 50% 50%;
  transform: translateX(calc(var(--parallax-p, 0) * -8%));
  will-change: transform;
}
@media (prefers-reduced-motion: reduce) {
  .section-cta__image { transform: none; }
  .section-companies__image { transform: none; }
}
.section-cta .content-wrapper {
  position: relative;
  z-index: 1;
  padding-top: 80px;
  padding-bottom: 80px;
}
.section-cta__content { display: flex; flex-direction: column; gap: 253px; }
.section-cta__head {
  display: flex;
  flex-direction: column;
  gap: 28px;
  max-width: 55%;   /* 759 of the 1380 content width */
}
.eyebrow--light { color: #fff; }
.eyebrow--dark { color: var(--c-green-80); }   /* on cream/white backgrounds */
.section-cta__heading {
  font-family: var(--font-serif);
  font-size: clamp(36px, 3.9vw, 56px);
  line-height: 1.05;
  color: var(--c-cream);
}
/* 3 : (4) : 5 on the 12-column grid — the footer's 331 : 447 : 562 — so the
   paragraph starts exactly where the footer nav column does. The middle track
   is empty, so the grid needs no spacer element: the text just starts on
   column 8. Note this rule must NOT set `display`, or it would beat `.grid-12`
   on source order. */
.section-cta__cols { align-items: end; }
.section-cta__text {
  font-size: 18px;
  line-height: 1.3;
  color: #fff;
}

/* ---- News section -------------------------------------------------------- */
.section-news { background: var(--c-cream); }
.section-news .content-wrapper { padding-top: 100px; padding-bottom: 100px; }
.section-news__content {
  display: flex;
  align-items: stretch;
  justify-content: space-between;
  gap: 20px;
}
.section-news__aside {
  flex: 329 1 0;
  min-width: 1px;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  justify-content: space-between;
}
.section-news__heading {
  font-family: var(--font-serif);
  font-size: clamp(30px, 2.5vw, 36px);
  line-height: 1.1;
  color: var(--c-dark-green);
}

.article {
  flex: 1025 1 0;
  min-width: 1px;
  display: flex;
  gap: 48px;
  align-items: flex-start;
  padding-top: 30px;
  border-top: 1px solid var(--c-green-20);
}
.article__media {
  flex: 0 0 44.1%;      /* 452 of the article's 1025 */
  aspect-ratio: 452 / 454;
  border-radius: 4px;
  overflow: hidden;
}
/* Full panorama loaded and framed by object-position — never pre-cropped, so
   the shot can be repositioned or swapped (e.g. from a CMS) in one value. */
.article__image {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: var(--focal, 50% 50%);
  transition: transform 0.6s cubic-bezier(0.33, 1, 0.68, 1);
}
.article:hover .article__image { transform: scale(1.03); }
.article__body {
  flex: 1 0 0;
  min-width: 1px;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 20px;
}
.article__date {
  font-size: 16px;
  line-height: 1.3;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--c-green-60);
}
.article__title {
  font-family: var(--font-serif);
  font-size: clamp(21px, 2.5vw, 36px);
  line-height: 1.1;
  color: var(--c-dark-green);
}
.article__title a { transition: color 0.25s ease; }
.article__title a:hover { color: var(--c-gold); }
.article__excerpt {
  font-size: 16px;
  line-height: 1.3;
  color: var(--c-green-80);
}

/* ---- Footer -------------------------------------------------------------- */
.section-footer .content-wrapper {
  padding-top: 40px;
  padding-bottom: 40px;
}
/* 3 : 4 : 5 on the 12-column grid — the design's 331 : 447 : 562 — the same
   row the CTA above sits on, so the two line up at every width. No `display`
   here: `.grid-12` owns that (see the grid notes). */
.site-footer__content {
  align-items: stretch;
  /* The design gives each column a large fixed gap between its top and bottom
     blocks (167 / 110 / 168px). Setting the row height instead lets
     space-between produce those gaps while keeping every column's last item
     aligned along the bottom — and it still grows if the copy wraps further. */
  min-height: 311px;
}
/* Each column pins its first block to the top and last to the bottom */
.site-footer__col {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  justify-content: space-between;
  gap: 40px;
}
/* Footer logo lock-up (animated logomark above logotype, ~92% header size) */
.logo--footer .logo__mark { width: 42px; margin-bottom: 15.3px; }
.logo--footer .logo__mark .logomark { width: 42px; height: 18.2px; }
.logo--footer .logo__type { width: 141.2px; }
.site-footer__tagline {
  max-width: 12em;
  font-family: var(--font-serif);
  font-weight: 500;
  font-size: 24px;
  line-height: 1.15;
  color: var(--c-dark-green);
  text-wrap: balance;
}
.site-footer__address {
  display: flex;
  flex-direction: column;
  gap: 16px;
  font-size: 16px;
  line-height: 1.6;
  color: var(--c-dark-green);
}
.site-footer__linkedin { display: block; transition: opacity 0.25s ease; }
.site-footer__linkedin:hover { opacity: 0.6; }
.footer-nav {
  display: flex;
  gap: 19px;
  width: 100%;
  font-size: 18px;
  color: var(--c-dark-green);
}
.footer-nav__col {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 4px;
  flex: 1 1 0;
  min-width: 1px;
}
.footer-nav a { transition: opacity 0.25s ease; }
.footer-nav a:hover { opacity: 0.5; }

/* Newsletter */
.newsletter {
  display: flex;
  flex-wrap: wrap;          /* so the stub's note can take its own line */
  align-items: center;
  gap: 18px;
  width: 100%;
}
.newsletter__note { flex: 0 0 100%; font-size: 16px; line-height: 1.3; color: var(--c-green-80); }
.newsletter__input {
  flex: 1 0 0;
  min-width: 1px;
  height: 56px;
  border: 0;
  border-radius: 999px;
  background: var(--c-cream-50);
  padding: 0 36px 0 30px;
  font-family: var(--font-sans);
  font-size: 18px;
  color: var(--c-dark-green);
}
.newsletter__input::placeholder { color: var(--c-dark-green); opacity: 1; }
.newsletter__input:focus-visible { outline: 2px solid var(--c-gold); outline-offset: 2px; }
.newsletter .btn { border: 0; }

/* Legal strip */
.section-legal {
  background: var(--c-dark-green);
  color: #fff;
}
.section-legal .content-wrapper {
  padding-top: 25px;
  padding-bottom: 25px;
}
.section-legal__cols {
  display: flex;
  gap: 50px;
  align-items: flex-start;
}
/* Legal columns scale proportionally (design ratios 992 : 338 at 1440) */
.section-legal__disclaimer {
  flex: 992 1 0;
  min-width: 1px;
  font-size: 12px;
  line-height: 1.3;
}
.section-legal__col {
  flex: 338 1 0;
  min-width: 1px;
  font-size: 16px;
  line-height: 1.3;
}
.section-legal__col p:first-child { margin-bottom: 10px; }
.section-legal__col a { transition: opacity 0.25s ease; }
.section-legal__col a:hover { opacity: 0.7; }

/* ========================================================================== */
/* ABOUT PAGE                                                                 */
/* ========================================================================== */

/* The hero photo runs to the very top of the page with the header floating
   over it (Figma puts the header instance at y ≈ 0, above the hero), so here
   the header comes out of the flow entirely. Everything else about it — the
   hide-on-scroll, the transparency, the dark-section colour switch — is
   unchanged.
   Selected by *shape* rather than by page class: any page whose <main> opens
   on a full-bleed band gets it automatically, which is one less thing to
   remember per page (it was forgotten on both Partners and Join). */
body:has(main > .image-band:first-child) .site-header {
  position: fixed;
  left: 0;
  right: 0;
  top: 0;
  /* the swipe is shorter on inner pages, so the header arrives sooner */
  animation-delay: 1.05s;
}
/* This page shows the mark alone from the start, at its scrolled size, so the
   logo transformation has nowhere to go. main.js leaves --logo-p alone when
   it sees this class rather than fighting the rule with an inline style. */
.site-header--compact { --logo-p: 1; }

/* ---- Full-bleed photo band (reusable) ----------------------------------- */
/* The About page breaks its sections with full-width photo bands — the hero,
   the vision break, the alpine break. Two variables per band: --band-h for the
   height and --band-pos for the crop.
   Vertical parallax: main.js writes --parallax-p, −1 as the band rises into
   view and +1 as it leaves. The image drifts *down* as the page scrolls up, so
   it travels slower than the frame around it — that lag is the whole effect.
   It's 124% tall and pulled up 12% so there is always image above and below
   the frame at either extreme; oversize first, then translate, or an edge
   shows. The full frame is loaded and positioned rather than cropped, per the
   site rule, so a band can be reframed or swapped with one value. */
.image-band {
  position: relative;
  height: var(--band-h, 423px);
  background: var(--c-dark-green);
  overflow: hidden;
}
.image-band__image {
  position: absolute;
  left: 0;
  top: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: 50% var(--band-pos, 50%);
}
/* The oversize geometry belongs to the parallax, so it is scoped to the
   opt-in attribute: adding or removing data-parallax is the whole switch.
   Without this scoping a still band inherits a 124%-tall image pulled up 12%,
   which pushes the photo's top edge out of frame — visible as a bleed where
   the band meets a light section. */
.image-band__image[data-parallax] {
  top: -12%;
  height: 124%;
  transform: translateY(calc(var(--parallax-p, 0) * 8%));
  will-change: transform;
}
@media (prefers-reduced-motion: reduce) {
  .image-band__image[data-parallax] { transform: none; }
}

.about-hero { --band-h: 423px; }

/* The vision break is taller and **sticky**: it pins to the top of the
   viewport and the section below scrolls up over it. The crop sits at the top
   of the frame so the sunset horizon is the focal point — and the image is
   deliberately *not* parallaxed (no data-parallax in the markup): the pinning
   is movement enough, and the top of the photo has to stay in frame at all
   times or its edge bleeds against the white section above.
   Scoped by its `.sticky-scene` wrapper (band + the vision section that rides
   over it), so it is only ever pinned across that scene. */
.about-vision-image {
  --band-h: clamp(420px, 70vh, 760px);
  --band-pos: 0%;
  position: sticky;
  /* Hold once the band's *bottom* edge meets the bottom of the screen, then
     let the section below ride up over it.
     Note this is a `top` offset, not `bottom: 0`. Sticky `bottom` pins an
     element that is still below the fold — it drags it up early so it is
     already flush with the screen bottom on arrival, then releases it. Pinning
     *after* a downward approach is always a `top` offset; putting that offset
     one band-height up from the bottom of the screen puts the band's bottom
     edge exactly on it. svh (not vh/dvh) so a mobile address bar neither
     offsets the pin nor makes it jitter as the bar hides. */
  top: calc(100vh - var(--band-h));
  top: calc(100svh - var(--band-h));
}

/* ---- About: intro ------------------------------------------------------- */
.about-intro { background: #fff; }
.about-intro .content-wrapper { padding-top: 40px; padding-bottom: 80px; }
.about-intro__content {
  display: flex;
  flex-direction: column;
  /* 211px at the design's 1440 — it shrinks with the viewport rather than
     holding a gap that would swallow a small screen */
  gap: clamp(60px, 14.65vw, 211px);
}
.about-intro__head { display: flex; flex-direction: column; gap: 20px; }
/* An h2 carrying the page's display heading, so it keeps h1 sizing and h1
   line-breaking (the base rule gives h2s `pretty`) */
.about-intro__heading {
  font-family: var(--font-serif);
  font-size: clamp(36px, 5vw, 72px);
  line-height: 1;
  letter-spacing: -0.02em;
  color: var(--c-dark-green);
  max-width: 10em;   /* the design's 708px at 72px */
  text-wrap: balance;
}
.about-intro__heading em { font-style: italic; font-weight: 400; }

/* 446 : 914 from the design, as ratios so they hold at any width */
.about-intro__cols { display: flex; gap: 20px; align-items: flex-start; }
.about-intro__col--mission { flex: 446 1 0; min-width: 1px; }
.about-intro__col--mission p {
  font-size: 18px;
  line-height: 1.3;
  color: var(--c-green-80);
  max-width: 22.5em;   /* the design caps the copy inside its column */
}
.about-intro__quote {
  flex: 914 1 0;
  min-width: 1px;
  display: flex;
  flex-direction: column;
  gap: 39px;
  margin: 0;
}
.about-intro__quote-text {
  font-family: var(--font-serif);
  font-size: clamp(24px, 2.5vw, 36px);
  line-height: 1.1;
  color: var(--c-dark-green);
}
.about-intro__quote-cite {
  font-style: normal;   /* <cite> italicises by default */
  font-size: 16px;
  line-height: 1.3;
  color: var(--c-green-80);
}

/* ---- About: vision ------------------------------------------------------ */
.about-vision {
  position: relative;
  overflow: hidden;
  background: var(--c-cream);
}
/* Orange line graphic: enters flat from the left edge and curves down out of
   the section. Will's export is already in this orientation — no flip needed.
   Anchored to the bottom at its natural 771:223 ratio (53.5% of the design's
   1440), which lands the flat run at the design's height and lets the curve
   clip off the bottom edge exactly as the Figma frame does. */
.about-vision__graphic {
  position: absolute;
  left: 0;
  bottom: 0;
  width: 53.5%;
  height: auto;
  pointer-events: none;
  /* Draws in left to right as it arrives, the same treatment as the video
     line graphic on the homepage. main.js writes --wipe-p (0 → 1 across the
     element's arrival); the var defaults to 1 so without JS it is simply all
     there. */
  clip-path: inset(0 calc((1 - var(--wipe-p, 1)) * 100%) 0 0);
}
@media (prefers-reduced-motion: reduce) {
  .about-vision__graphic { clip-path: none; }
}
.about-vision .content-wrapper {
  position: relative;
  z-index: 1;
  padding-top: 40px;
  padding-bottom: 350px;   /* the room the graphic sweeps through */
}
/* 802 : 558 from the design */
.about-vision__cols { display: flex; gap: 20px; align-items: flex-start; }
.about-vision__heading {
  flex: 802 1 0;
  min-width: 1px;
  font-family: var(--font-serif);
  font-size: clamp(36px, 3.9vw, 56px);
  line-height: 1.05;
  color: var(--c-dark-green);
}
.about-vision__col {
  flex: 558 1 0;
  min-width: 1px;
  display: flex;
  flex-direction: column;
  gap: 15px;
  font-size: 21px;
  line-height: 1.3;
  color: var(--c-green-80);
}
.about-vision__col p { max-width: 20.6em; }   /* the design's 432 of 558 */

/* ---- About: values ------------------------------------------------------ */
/* Same structure and reveal sequence as the homepage intro cards — five
   across and centred rather than three and left-aligned. */
.about-values {
  background: linear-gradient(234deg, var(--c-teal) 1.6%, var(--c-dark-green) 72.9%);
}
.about-values .content-wrapper { padding-top: 60px; padding-bottom: 160px; }
.about-values__content { display: flex; flex-direction: column; gap: 87px; }
.about-values__head { display: flex; flex-direction: column; gap: 12px; }
.about-values__heading {
  font-family: var(--font-serif);
  font-size: clamp(36px, 3.9vw, 56px);
  line-height: 1.05;
  color: #fff;
}
.about-values__sub { font-size: 18px; line-height: 1.3; color: var(--c-green-40); }

/* ---- About: team -------------------------------------------------------- */
.about-team { background: #fff; }
.about-team .content-wrapper { padding-top: 40px; padding-bottom: 100px; }
.about-team__heading {
  font-family: var(--font-serif);
  /* Marked up as an h2 (the page already has its h1 in the intro) but set at
     h1 size — Will's design treats it as a second opening statement. */
  font-size: clamp(36px, 5vw, 72px);
  line-height: 1.05;
  color: var(--c-dark-green);
  max-width: 12em;
}
.about-team__heading em { font-style: italic; }
/* 330 : 1030 from the design — the lead copy and the photo grid share the
   right-hand column, the "Meet The Team" label sits out to the left of it. */
.about-team__grid {
  display: grid;
  grid-template-columns: 330fr 1030fr;
  gap: 50px 20px;
  margin-top: 100px;
}
.about-team__lead {
  grid-column: 2;
  font-size: 18px;
  line-height: 1.3;
  color: var(--c-green-80);
  max-width: 30em;
}
.about-team__label { grid-column: 1; grid-row: 2; }

.team-list {
  grid-column: 2;
  grid-row: 2;
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 30px 20px;
  margin: 0;
  padding: 0;
  list-style: none;
}
/* Cards arrive left to right across each row */
.team-list__item:nth-child(4n + 1) { --reveal-delay: 0s; }
.team-list__item:nth-child(4n + 2) { --reveal-delay: 0.08s; }
.team-list__item:nth-child(4n + 3) { --reveal-delay: 0.16s; }
.team-list__item:nth-child(4n + 4) { --reveal-delay: 0.24s; }

/* The whole card is the control that opens the bio */
.team-card {
  display: flex;
  flex-direction: column;
  gap: 10px;
  width: 100%;
  padding: 0;
  border: 0;
  background: none;
  font: inherit;
  color: inherit;
  text-align: left;
  cursor: pointer;
}
.team-card__media {
  display: block;
  aspect-ratio: 245 / 367.427;   /* the design's frame, so no crop drift */
  border-radius: 4px;
  overflow: hidden;
  background: var(--c-cream-50);
}
.team-card__media img { width: 100%; height: 100%; object-fit: cover; }
/* Members without a photograph use Will's team-placeholder artwork through
   the same <img> path as everyone else — no special-case styling needed. */
.team-card__title { display: flex; align-items: center; gap: 1px; }
.team-card__text {
  flex: 1 1 auto;
  min-width: 1px;
  font-size: 16px;
  line-height: 1.3;
  color: var(--c-green-80);
}
.team-card__text strong { display: block; font-weight: 700; }
.team-card__open {
  flex: 0 0 auto;
  width: 34.4px;
  height: 34.4px;
  color: var(--c-orange);
  /* the site-wide 3px reads as a heavy slab at this size */
  stroke-width: 1.5px;
  opacity: 0;
  transition: opacity 0.25s ease;
}
.team-card:hover .team-card__open,
.team-card:focus-visible .team-card__open { opacity: 1; }

/* ---- About: team bio modal ---------------------------------------------- */
/* <dialog> carries the focus trap, Escape-to-close and top-layer stacking, so
   the only JS needed is filling it in. */
/* Enter and exit: a short fade with a small rise, reversed on close.
   `@starting-style` gives the opening frame something to animate *from*, and
   `allow-discrete` keeps the element around long enough for the closing
   transition to play (a dialog otherwise snaps straight to display: none).
   Browsers without either simply get no animation. */
.team-modal {
  /* Restating the UA's own `dialog:modal` positioning rather than using
     `relative`. A relative dialog drops back into normal flow and renders at
     its position in the document — fine at scroll 0, off-screen anywhere else.
     Fixed also gives the absolutely-positioned close button its containing
     block, which is what `relative` was there for. */
  position: fixed;
  inset: 0;
  margin: auto;
  opacity: 0;
  transform: translateY(20px);
  transition: opacity 0.28s ease,
              transform 0.34s cubic-bezier(0.33, 1, 0.68, 1),
              overlay 0.34s allow-discrete,
              display 0.34s allow-discrete;
  width: min(835px, calc(100vw - 40px));
  max-height: calc(100vh - 40px);
  padding: 50px;
  border: 0;
  border-radius: 8px;
  background: var(--c-cream);
  color: var(--c-green-80);
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25);
  /* The dialog is its own scroller for bios that outrun the viewport. Two
     things are needed beyond `overflow: auto`: `data-lenis-prevent` in the
     markup (Lenis eats wheel and touchmove while it is stopped), and
     `overscroll-behavior: contain` so reaching the end of the bio doesn't
     chain the gesture on to the page behind. */
  overflow-y: auto;
  overscroll-behavior: contain;
}
.team-modal[open] { opacity: 1; transform: translateY(0); }
@starting-style {
  .team-modal[open] { opacity: 0; transform: translateY(20px); }
}
.team-modal::backdrop {
  background: rgba(11, 43, 42, 0.5);
  opacity: 0;
  transition: opacity 0.28s ease,
              overlay 0.28s allow-discrete,
              display 0.28s allow-discrete;
}
.team-modal[open]::backdrop { opacity: 1; }
@starting-style {
  .team-modal[open]::backdrop { opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
  .team-modal, .team-modal[open] { transform: none; transition: none; }
  .team-modal::backdrop, .team-modal[open]::backdrop { transition: none; }
}
.team-modal__cols { display: flex; gap: 30px; align-items: flex-start; }
.team-modal__person {
  flex: 0 0 242.5px;
  display: flex;
  flex-direction: column;
  gap: 10px;
}
.team-modal__photo {
  width: 100%;
  height: auto;
  aspect-ratio: 245 / 367.427;
  object-fit: cover;
  border-radius: 4px;
}
.team-modal__title { font-size: 16px; line-height: 1.3; }
.team-modal__name { display: block; font-weight: 700; }
.team-modal__body {
  flex: 1 1 auto;
  min-width: 1px;
  padding-right: 70px;   /* clear of the close button */
  font-size: 18px;
  line-height: 1.3;
}
.team-modal__body > * + * { margin-top: 15px; }
/* LinkedIn above the mailto, as two stacked links (Figma shows them on
   separate lines). align-items keeps each underline the width of its own
   text rather than the whole column. */
.team-modal__links {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
}
.team-modal__links a {
  color: var(--c-orange);
  text-decoration: underline;
}
.team-modal__close {
  position: absolute;
  top: 50px;
  right: 50px;
  width: 32px;
  height: 32px;
  padding: 0;
  border: 0;
  background: none;
  color: var(--c-dark-green);
  cursor: pointer;
}
.team-modal__close .icon { width: 100%; height: 100%; stroke-width: 1.5px; }
/* Freezing the page behind the dialog. The lock goes on the *root*, not the
   body: `html { overflow: hidden }` leaves window.scrollY untouched, so there
   is nothing to save and restore — and nothing for Lenis to fight over when it
   restarts. Pinning the body with position:fixed also works visually but zeroes
   the document scroll, and putting it back afterwards is what was throwing the
   page to the top on close. Measured: scrollY identical before, during and
   after, and the wheel is still blocked. */
html.modal-open {
  overflow: hidden;
  overscroll-behavior: contain;
}

/* ---- About: support ----------------------------------------------------- */
.about-support {
  position: relative;
  overflow: hidden;
  background: var(--c-cream);
}
.about-support .content-wrapper { padding-top: 130px; padding-bottom: 190px; }
/* 696 : 558 : 126 from the design — the copy sits in the middle track, which
   leaves the left third clear for the image block bleeding off the edge. */
.about-support__grid {
  display: grid;
  grid-template-columns: 696fr 558fr 126fr;
}
.about-support__col {
  grid-column: 2;
  display: flex;
  flex-direction: column;
  gap: 40px;
}
.about-support__head { display: flex; flex-direction: column; gap: 31px; }
.about-support__heading {
  font-family: var(--font-serif);
  text-wrap: balance;
  font-size: clamp(36px, 5vw, 72px);
  line-height: 1;
  letter-spacing: -0.02em;
  color: var(--c-dark-green);
}
.about-support__text {
  font-size: 18px;
  line-height: 1.3;
  color: var(--c-green-80);
  max-width: 25em;
}
/* `.btn` is inline-flex, but as a flex CHILD it would stretch to the column's
   width — align-self keeps it to its label, the way Figma draws it. */
.about-support__btn { align-self: flex-start; }

/* Photo over its own graphic. The block is the design's 616×414 background
   frame, pinned to the section's left edge and clipped, so both children can
   be placed by percentages of it and stay in register at any width. */
.support-image {
  position: absolute;
  left: 0;
  top: 130px;              /* level with the content, i.e. the wrapper padding */
  width: 42.78%;           /* 616 / 1440 */
  aspect-ratio: 616 / 414;
  overflow: hidden;
}
/* Flush to the bottom of the frame at its natural 616:191 — which is exactly
   the visible crop Will exported, so no offset maths is needed. It sits under
   the photo (first in the DOM) and reads as a band running out from beneath. */
.support-image__graphic {
  position: absolute;
  left: 0;
  bottom: 0;
  width: 100%;
  height: auto;
  transform: translate(var(--slide-x, 0px), var(--slide-y, 0px));
}
.support-image__photo {
  position: absolute;
  left: 0;
  top: 0;
  width: 82.57%;           /* 508.635 / 616 */
  aspect-ratio: 508.635 / 366.87;
  height: auto;
  object-fit: cover;
}

/* ---- Testimonial block (About + Join) -------------------------------------------------- */
.testimonial-block {
  position: relative;
  overflow: hidden;
  background: var(--c-teal);
}
.testimonial-block .content-wrapper {
  position: relative;
  z-index: 1;
  padding-top: 40px;
  padding-bottom: 40px;
}
.testimonial-block__content { display: flex; flex-direction: column; gap: 243px; }
.testimonial-block__cols { display: flex; gap: 20px; align-items: flex-start; }
.testimonial-block__quote {
  flex: 1149 1 0;
  min-width: 1px;
  display: flex;
  flex-direction: column;
  gap: 25px;
  margin: 0;
}
.testimonial-block__text {
  font-family: var(--font-serif);
  font-size: clamp(28px, 3.2vw, 46px);
  line-height: 1.05;
  color: var(--c-cream);
}
.testimonial-block__cite {
  font-style: normal;
  font-size: 18px;
  line-height: 1.3;
  color: var(--c-green-40);
}
.testimonial-block__logo {
  flex: 211 0 0;
  min-width: 1px;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 24px 16px;
}
.testimonial-block__logo img { width: 100%; height: auto; max-width: 179px; }
.testimonial-block__btn { align-self: flex-start; }

/* The round photo block — the same idea as .angle-image on the homepage, with
   the rounded shape instead of the angled one. Three layers stepping right by
   an equal offset: two copies of the graphic, then the photo clipped to that
   same shape. Keeping them as separate elements is the point: the photo can be
   swapped without touching the artwork underneath.
   The frame is the design's 692×302 window; the graphic inside it is wider and
   runs off the right edge, which is why the frame clips. */
.round-image {
  position: absolute;
  right: 0;
  bottom: 0;
  width: 48.06%;            /* 692 / 1440 */
  aspect-ratio: 692 / 302;
  overflow: hidden;
}
/* Each layer fills the frame. Will's export (658×285) is within a whisker of
   the frame's own 692:302, so the curve lands true without any scaling maths —
   the earlier attempt sized these to the graphic's *untrimmed* width and put
   the clipped photo off-frame entirely. */
.round-image__stripe,
.round-image__photo {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
}
/* Figma steps the three layers right by 37.7px of the 692 frame = 5.45% each:
   the furthest-left copy is the bottom one and shows as the outer band. */
.round-image__stripe--back {
  opacity: 0.5;
  transform: translate(var(--slide-x, 0px), var(--slide-y, 0px));
}
.js .round-image__stripe--back.is-visible { opacity: 0.5; }
.round-image__stripe--front {
  transform: translateX(5.45%)
             translate(var(--slide-x, 0px), var(--slide-y, 0px));
}
.round-image__photo {
  transform: translateX(10.9%);
  object-fit: cover;
  /* The source is a 1040×1300 portrait shown in a shallow band, so the crop
     has to be aimed: 80% puts the engineer in frame rather than the lift
     mechanism above him. One value to reframe if the photo is swapped. */
  object-position: 50% 80%;
  clip-path: url("#shape-round-image-graphic");
}

/* ---- About: alpine ------------------------------------------------------ */
.about-alpine { background: var(--c-cream); }
.about-alpine .content-wrapper { padding-top: 80px; padding-bottom: 80px; }
.about-alpine__content { display: flex; flex-direction: column; gap: 55px; }
/* Both rows are 3 : 9 on the 12-column grid, so the label sits over the logo
   and the heading, paragraph and stats all start on column 4 */
.about-alpine__cols { align-items: start; }
/* Narrower than its column and balanced, so the label reads as two even
   lines rather than a long one and an orphan */
.about-alpine__label { max-width: 15em; text-wrap: balance; }
/* Top-aligned: the logo is optically heavy, so it takes a 5px nudge rather
   than the row bottom-aligning to the stats' rules */
.about-alpine__foot { align-items: start; }
.about-alpine__col {
  display: flex;
  flex-direction: column;
  gap: clamp(60px, 14.9vw, 215px);   /* 215 at the design's 1440 */
}
.about-alpine__heading {
  font-family: var(--font-serif);
  font-size: clamp(36px, 5vw, 72px);
  line-height: 1;
  letter-spacing: -0.02em;
  color: var(--c-dark-green);
  max-width: 11.4em;                 /* was 9.4 (the design's 672); widened
                                        for the longer two-sentence heading */
}
.about-alpine__text {
  font-size: 18px;
  line-height: 1.3;
  color: var(--c-green-80);
  max-width: 36em;
}
.about-alpine__text a { color: var(--c-orange); text-decoration: underline; }
.about-alpine__logo {
  margin-top: 5px;
  width: 100%;
  max-width: 164px;    /* the logotype's natural width in the design */
  height: auto;
}
/* Cream-background variant of the shared stat row */
.stat-list--light .stat__value { color: var(--c-orange); }
.stat-list--light .stat__label { color: var(--c-green-80); }
.stat-list--light .stat::after { border-top-color: var(--c-green-20); }

/* The band is revealed by the section above scrolling off it, not by sliding
   into view — so here `bottom: 0` IS the right sticky, the opposite of the
   vision break. It pins the band while it is still below the fold, and the
   opaque cream alpine section uncovers it on the way past.
   `bottom` sticky drags an element up-page from the moment its containing
   block is on screen, which is precisely why this one needs its
   `.sticky-scene` wrapper: unscoped, it was pinned to the bottom of the very
   first screen and painted over the vision band. Scoped to [alpine section +
   band], it can only be pinned while the alpine section is passing. */
.about-alpine-image {
  --band-h: 600px;
  --band-pos: 57%;
  position: sticky;
  bottom: 0;
}

/* Centred variant of the shared card row */
.card-list--center .card {
  align-items: center;
  text-align: center;
  gap: 25px;
}
.card-list--center .card__text { font-size: 16px; }

/* ===== Partners page ======================================================
   Figma 807:23504. Hero band, intro (8 : 4), the eight partner cards, CTA. */

/* ---- Partners: hero ----------------------------------------------------- */
/* Design height is the .image-band default (423px), so only the crop is set */
/* Aug 2026 swap: this band is now the Irwin drone still. Unlike the portrait
   it replaced, the pipeline crops it to exactly the box the parallax fills at
   rest — the crop IS the framing — so the offset goes back to the default
   rather than carrying a hand-set percentage. Left explicit so the next swap
   sees a value to change, not an absence. */
.partners-hero { --band-pos: 50%; }

/* ---- Partners: intro ---------------------------------------------------- */
.partners-intro { background: #fff; }
.partners-intro .content-wrapper { padding-top: 40px; padding-bottom: 200px; }
.partners-intro__content { display: flex; flex-direction: column; gap: 20px; }
.partners-intro__cols { align-items: start; }
.partners-intro__heading {
  font-family: var(--font-serif);
  font-size: clamp(36px, 5vw, 72px);
  line-height: 1;
  letter-spacing: -0.02em;
  color: var(--c-dark-green);
  max-width: 8.9em;          /* the design's 638 at 72px */
  text-wrap: balance;        /* h1 sizing, so h1 line-breaking (see headings) */
}
.partners-intro__heading em { font-style: italic; font-weight: 400; }
.partners-intro__aside {
  display: flex;
  flex-direction: column;
  gap: 15px;
  font-size: 18px;
  line-height: 1.3;
  color: var(--c-green-80);
  max-width: 25em;           /* cap the type, not the column */
}

/* ---- Partners: companies ------------------------------------------------ */
.partners-companies {
  position: relative;
  overflow: hidden;
  background: var(--c-cream);
}
.partners-companies .content-wrapper {
  position: relative;
  z-index: 1;
  padding-top: 40px;
  padding-bottom: 180px;
}
.partners-companies__content { display: flex; flex-direction: column; gap: clamp(60px, 10.8vw, 155px); }
.partners-companies__heading {
  font-family: var(--font-serif);
  font-size: clamp(36px, 3.9vw, 56px);
  line-height: 1.05;
  color: var(--c-dark-green);
}
.partners-companies__heading em { font-style: italic; }
/* Will's curve band, full width across the bottom of the section at its
   natural 1440:439 — the same left-to-right wipe as the vision graphic. */
.partners-companies__graphic {
  position: absolute;
  left: 0;
  bottom: 90px;
  width: 100%;
  height: auto;
  clip-path: inset(0 calc((1 - var(--wipe-p, 1)) * 100%) 0 0);
}

/* ---- Partner cards (flip) ----------------------------------------------- */
/* Four across at 3 grid columns each; the design's 18px row gap. */
.partner-list {
  --grid-row-gap: 18px;
  list-style: none;
  margin: 0;
  padding: 0;
}
.partner-card {
  /* The flip lives on the inner element, so the card itself keeps the
     perspective — without it the rotation reads as a flat squash. */
  perspective: 1600px;
}
/* Sequential arrival, same idea as the homepage and values rows — 0.12s apart
   rather than 0.3, because eight cards at 0.3 would run for over two seconds.
   Scoped to the section: an unscoped nth-child would reach other pages. */
.partners-companies .partner-card:nth-child(1) { --reveal-delay: 0s; }
.partners-companies .partner-card:nth-child(2) { --reveal-delay: 0.12s; }
.partners-companies .partner-card:nth-child(3) { --reveal-delay: 0.24s; }
.partners-companies .partner-card:nth-child(4) { --reveal-delay: 0.36s; }
.partners-companies .partner-card:nth-child(5) { --reveal-delay: 0.48s; }
.partners-companies .partner-card:nth-child(6) { --reveal-delay: 0.6s; }
.partners-companies .partner-card:nth-child(7) { --reveal-delay: 0.72s; }
.partners-companies .partner-card:nth-child(8) { --reveal-delay: 0.84s; }
.partner-card__inner {
  position: relative;
  aspect-ratio: 331.5 / 443.9;      /* the design's card */
  transform-style: preserve-3d;
  transition: transform 0.7s cubic-bezier(0.33, 1, 0.68, 1);
}
/* Hover for pointers, focus-within so the back's link is reachable by keyboard,
   and .is-flipped for taps (main.js) — a phone has no hover. */
.partner-card:hover .partner-card__inner,
.partner-card:focus-within .partner-card__inner,
.partner-card.is-flipped .partner-card__inner { transform: rotateY(180deg); }
.partner-card__face {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  border-radius: 4px;
  background: var(--c-dark-green);
  /* Hides the reverse of each face mid-rotation */
  backface-visibility: hidden;
}
.partner-card__back { transform: rotateY(180deg); }
/* Photo band across the bottom half, dimmed so the logo stays legible */
.partner-card__photo {
  position: absolute;
  left: 0;
  bottom: 0;
  width: 100%;
  height: 50%;
  object-fit: cover;
  object-position: 50% 50%;
  opacity: 0.35;
}
.partner-card__logo {
  position: relative;               /* over the photo */
  width: var(--logo-w, 60%);
  height: auto;
}
/* Each logotype has its own drawn width in the design — as a % of the card so
   they scale together. This used to be eight rules here, one per partner
   (`.partner-card--irwin { --logo-w: 41.7% }`). It is now the `logoWidth`
   field on the partner's record, written inline by partials/partner-card.html,
   so adding a ninth partner needs no CSS at all. The 60% fallback above is
   what a record with no width set gets. */

.partner-card__back {
  /* 25px is the design's gap at the card's drawn size. The ceiling used to be
     34px, which is what 2.4vw reaches at 1440 — so the built card was running
     9px looser than the mock everywhere above ~1040px. */
  gap: clamp(16px, 2.4vw, 25px);
  padding: 15px;
  text-align: center;
}
/* Name + head office are one group. The design sets 10px between them against
   25px to the blocks either side, so the location reads as a subtitle of the
   name rather than as a third thing on the card. Keeping them in their own
   flex column is what makes that hold when the card gap changes with the
   viewport — a single gap on the parent could not express both spacings. */
.partner-card__title {
  display: flex;
  flex-direction: column;
  gap: clamp(8px, 0.7vw, 10px);
  width: 100%;
}
.partner-card__name {
  font-family: var(--font-serif);
  font-weight: 500;
  font-size: clamp(20px, 1.7vw, 24px);
  line-height: 1.15;
  color: var(--c-cream);
}
/* "HQ Hebburn" — the label is bold, the place is not. Same size and colour as
   the description below it, which is what keeps it reading as information
   about the company rather than as a second heading. */
.partner-card__hq {
  font-size: clamp(15px, 1.25vw, 18px);
  line-height: 1.3;
  color: #fff;
}
.partner-card__hq strong { font-weight: 700; }
.partner-card__text {
  font-size: clamp(15px, 1.25vw, 18px);
  line-height: 1.3;
  color: #fff;
  /* Two or three lines centred in a narrow card, where a short last line reads
     as a mistake. `balance` evens them out. Safe here because the browser only
     applies it up to a handful of lines and these descriptions are short —
     it is the same reason h1 gets `balance` and h2 gets `pretty`. */
  text-wrap: balance;
}
/* The button is the only thing on the card a pointer can hit — everything else
   is decorative, and a click anywhere else flips the card back (main.js). */
.partner-card__btn { flex: none; }

@media (prefers-reduced-motion: reduce) {
  /* No rotation: the faces cross-fade instead */
  .partner-card__inner { transition: none; }
  .partner-card:hover .partner-card__inner,
  .partner-card:focus-within .partner-card__inner,
  .partner-card.is-flipped .partner-card__inner { transform: none; }
  .partner-card__face { transition: opacity 0.2s ease; }
  .partner-card__back { transform: none; opacity: 0; }
  .partner-card:hover .partner-card__back,
  .partner-card:focus-within .partner-card__back,
  .partner-card.is-flipped .partner-card__back { opacity: 1; }
}

/* ===== Join page =========================================================
   Figma 1226:26824. Hero, intro, the difference carousel, founders. */

.join-hero { --band-pos: 45%; }

/* ---- Join: intro -------------------------------------------------------- */
.join-intro { background: #fff; }
.join-intro .content-wrapper { padding-top: 40px; padding-bottom: 180px; }
.join-intro__content { display: flex; flex-direction: column; gap: 20px; }
.join-intro__cols { align-items: start; }
.join-intro__heading {
  font-family: var(--font-serif);
  font-size: clamp(36px, 5vw, 72px);
  line-height: 1;
  letter-spacing: -0.02em;
  color: var(--c-dark-green);
  max-width: 9.2em;          /* the design's 657 at 72px */
  text-wrap: balance;
}
.join-intro__heading em { font-style: italic; font-weight: 400; }
/* Two paragraphs since Will's Aug 2026 break, so it stacks like the Contact
   intro aside. The 25em cap is on the TYPE — the column keeps its full grid
   share, so the row still lines up down the page. */
.join-intro__aside {
  display: flex;
  flex-direction: column;
  gap: 18px;
  font-size: 18px;
  line-height: 1.3;
  color: var(--c-green-80);
  max-width: 25em;
}
.join-intro__aside p { margin: 0; }

/* ---- Join: what makes us different -------------------------------------- */
.join-difference {
  position: relative;
  overflow: hidden;
  background: linear-gradient(231.6deg, var(--c-teal) 1.6%, var(--c-dark-green) 72.9%);
}
/* Everything in the section sits above the artwork — the carousel is a
   plain <ul>, so without this the positioned graphic would paint over it. */
.join-difference > * { position: relative; z-index: 1; }
.join-difference .content-wrapper { padding-top: 40px; }
.join-difference__graphic {
  position: absolute;
  /* Anchored to the right, where Figma puts the fan — it rises out from under
     the fourth and fifth cards rather than from the left gutter. */
  right: 11%;
  bottom: 0;
  z-index: 0;
  width: 56.25%;           /* 810 / 1440 */
  height: auto;
  clip-path: inset(0 calc((1 - var(--wipe-p, 1)) * 100%) 0 0);
}
/* Heading on the left, arrows on the right, sharing a baseline */
.join-difference__head {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: 30px;
}
.join-difference__title { display: flex; flex-direction: column; gap: 25px; max-width: 53%; }
.join-difference__heading {
  font-family: var(--font-serif);
  font-size: clamp(36px, 3.9vw, 56px);
  line-height: 1.05;
  color: var(--c-cream);
}
.join-difference__heading em { font-style: italic; }

/* ---- Carousel (reusable) ------------------------------------------------
   A full-bleed scroll track: it starts level with the heading above it and
   runs off the right edge. Native overflow scrolling does the work — swipe on
   touch, arrows on desktop — with scroll-snap keeping cards on that line.
   main.js only moves the scroller and toggles the buttons' disabled state, so
   with no JS it still swipes.

   --carousel-inset is the content wrapper's inner left edge expressed from
   here: the track is a SIBLING of .content-wrapper, not a child, so it cannot
   inherit the centring and has to recompute it. Below --content-max the max()
   collapses to zero and the inset is just the page gutter, exactly as before.
   Above it the wrapper starts centring while a gutter-anchored track would
   stay pinned to the viewport edge — at 2560 that left the first card ~380px
   adrift of the heading, and the seven cards stopped short of the right edge
   so the row read as a mistake rather than a track.
   Percent rather than vw deliberately: 100vw counts the scrollbar, % does
   not, and the wrapper is sized in %-space too — mixing them would put the
   card a scrollbar's width out of true. */
.carousel {
  --carousel-inset: calc(max(0px, (100% - var(--content-max)) / 2) + var(--pad-x));
  display: flex;
  gap: 15px;
  list-style: none;
  margin: 96px 0 0;
  padding: 0 var(--pad-x) 120px var(--carousel-inset);
  overflow-x: auto;
  overscroll-behavior-x: contain;
  scroll-snap-type: x mandatory;
  scroll-padding-left: var(--carousel-inset);
  scrollbar-width: none;             /* the arrows are the affordance */
}
.carousel::-webkit-scrollbar { display: none; }
.carousel > * {
  flex: 0 0 clamp(230px, 20vw, 300px);   /* 286.5 at the design's 1440 */
  scroll-snap-align: start;
}
.carousel:focus-visible { outline: 2px solid var(--c-orange); outline-offset: -4px; }

.carousel-nav { display: flex; gap: 0; flex: none; }
.carousel-nav__btn {
  display: grid;
  place-items: center;
  width: 64px;
  height: 64px;
  padding: 0;
  /* Transparent from the start so the hover stroke doesn't resize the button */
  border: 1px solid transparent;
  border-radius: 999px;
  background: transparent;
  color: var(--c-orange);
  cursor: pointer;
  transition: opacity 0.25s ease, border-color 0.25s ease;
}
.carousel-nav__caret { display: block; width: 44px; height: 44px; }
.carousel-nav__btn:hover, .carousel-nav__btn:focus-visible { border-color: var(--c-orange); }
/* At either end the button stays in place but reads as unavailable — moving
   or removing it would shift the other one under the pointer. */
.carousel-nav__btn[disabled] { opacity: 0.35; cursor: default; border-color: transparent; }

/* ---- Join: difference cards --------------------------------------------- */
.difference-card {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 15px;
  padding: 35px 20px;
  border-radius: 4px;
  background: var(--c-teal);
  text-align: center;
}
.difference-card__icon { display: block; width: 64px; height: 64px; color: var(--c-orange); }
.difference-card__title {
  font-family: var(--font-serif);
  font-size: clamp(28px, 2.5vw, 36px);
  line-height: 1.1;
  color: #fff;
}
.difference-card__text {
  font-size: clamp(17px, 1.46vw, 21px);
  line-height: 1.3;
  color: var(--c-green-40);
}

/* ---- Join: founders ------------------------------------------------------
   The About support block mirrored — photo and artwork left, copy right. */
.join-founders { position: relative; overflow: hidden; background: #fff; }
.join-founders .content-wrapper { padding-top: 130px; padding-bottom: 140px; }
.founders-image {
  position: absolute;
  left: 0;
  top: 134px;
  width: 32.9%;            /* 474 / 1440 */
  aspect-ratio: 474 / 583;
  overflow: hidden;
}
/* Will's artwork fills the block; the photo sits over it, bleeding off the
   left edge so the curve shows down the right side and along the bottom
   (design: photo 530 wide at x=-91 in a 474 frame). */
.founders-image__graphic {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  transform: translate(var(--slide-x, 0px), var(--slide-y, 0px));
}
/* Aug 2026: Will re-cropped this (Figma 1595:31372). The photo used to bleed
   off the LEFT (530 wide at x=-91); it now sits flush left at 439 of the 474
   frame, so the curve shows down the RIGHT edge and along the bottom instead.
   `--focal` carries the horizontal crop — see the markup for the derivation. */
.founders-image__photo {
  position: absolute;
  left: 0;
  top: -0.62%;             /* -3.61 / 583 */
  width: 92.62%;           /* 439 / 474 */
  height: 91.36%;          /* 532.61 / 583 */
  object-fit: cover;
  object-position: var(--focal, 50% 50%);
}
.join-founders__cols { position: relative; z-index: 1; }
.join-founders__col { display: flex; flex-direction: column; gap: 40px; }
.join-founders__head { display: flex; flex-direction: column; gap: 22px; }
.join-founders__heading {
  font-family: var(--font-serif);
  font-size: clamp(36px, 3.9vw, 56px);
  line-height: 1.05;
  color: var(--c-dark-green);
  max-width: 12em;
}
/* One flowing text block, not a flex column with a gap — the spacing lives on
   the elements, the way the article's prose does, so a paragraph added here
   brings its own rhythm instead of inheriting one gap for everything. */
.join-founders__body > * { margin: 0; }
.join-founders__lead {
  font-size: 21px;
  line-height: 1.3;
  color: var(--c-green-80);
  max-width: 27em;
  margin: 0 0 1em;              /* 21px */
}
/* Must sit AFTER `.join-founders__body > *` — same specificity (0,1,0), so
   source order is the only thing making this margin stick. 30px is Figma's
   gap between the tick list and the button (list ends 252, button at 282). */
.join-founders__btn { margin-top: 30px; }

/* Will's angled brand bullet (assets/bullet.svg, 12×12) */
/* padding-left indents the whole list; the bullet hangs back into it */
.tick-list { list-style: none; margin: 0; padding: 0 0 0 30px; list-style: none; }
.tick-list li + li { margin-top: 10px; }
.tick-list li {
  position: relative;
  padding-left: 24px;
  font-size: 18px;
  line-height: 1.3;
  color: var(--c-green-80);
  max-width: 27em;
}
.tick-list li::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0.36em;
  width: 12px;
  height: 12px;
  background: url("../assets/bullet.svg") no-repeat center / contain;
}

/* ---- Join: process ------------------------------------------------------
   The one scroll-scrubbed section on the site. The scene is taller than the
   viewport and its inner panel is sticky, so the section holds still while the
   page scrolls through it; main.js writes --scrub-p (0 → 1) across that range
   and CSS does the rest. Nothing here depends on JS to be *legible* — without
   it --scrub-p is unset and the fallbacks leave the line drawn and every step
   visible. */
.join-process { background: #fff; padding: 0 var(--pad-x); }
.process-scene { height: 260vh; }
.process-sticky {
  position: sticky;
  top: 0;
  display: flex;
  align-items: center;
  height: 100vh;
  height: 100svh;
}
/* Belt and braces for a short window: if the panel can't fit, don't pin it —
   a clipped box is worse than no animation. (Measured at 1440/1200/1024/900
   wide it fits comfortably; this covers the laptop-with-devtools case.) */
@media (max-height: 560px) {
  .process-scene { height: auto; }
  .process-sticky { position: static; height: auto; display: block; }
  .process-box { min-height: 0; }
  .process-line { width: calc(100% + 2 * var(--box-pad)); }
  .process-step { opacity: 1; }
  .process-step__marker { transform: none; }
}
.process-box {
  --box-pad: clamp(30px, 4.9vw, 70px);
  position: relative;
  display: flex;
  align-items: center;
  overflow: hidden;
  width: 100%;
  min-height: 80vh;
  min-height: 80svh;
  border-radius: 8px;
  background: linear-gradient(232.4deg, var(--c-teal) 1.6%, var(--c-dark-green) 72.9%);
}
.process-box__graphic {
  position: absolute;
  left: 0;
  bottom: 0;
  width: 100%;
  height: auto;
}
.process-box__inner {
  position: relative;
  width: 100%;
  display: flex;
  flex-direction: column;
  gap: clamp(40px, 5.3vw, 76px);
  padding: clamp(50px, 7.6vw, 110px) var(--box-pad);
}
.process-head { display: flex; flex-direction: column; gap: 18px; max-width: 42em; }
.process-heading {
  font-family: var(--font-serif);
  font-size: clamp(30px, 3.9vw, 56px);
  line-height: 1.05;
  color: var(--c-cream);
}
.process-heading em { font-style: italic; }

.process-list {
  position: relative;
  display: flex;
  gap: 20px;
  list-style: none;
  margin: 0;
  padding: 0;
}
/* Runs the full width of the box, through the middle of the markers — hence
   the negative insets, which cancel the box padding. */
/* Dashed, in the brand orange. Drawn by growing its *width* rather than
   scaleX — scaling a dashed pattern stretches the dashes as it goes.
   Reaches the far edge at 85% of the scene, so the finished timeline has a
   beat of stillness before the section releases. */
.process-line {
  --line-p: clamp(0, calc(var(--scrub-p, 1) / 0.85), 1);
  position: absolute;
  top: 16px;
  left: calc(-1 * var(--box-pad));
  height: 1px;
  width: calc(var(--line-p) * (100% + 2 * var(--box-pad)));
  background: repeating-linear-gradient(to right,
              var(--c-orange) 0 7px, transparent 7px 14px);
}
.process-step {
  /* --t is the point on the line where this step's marker sits (set per step
     in the markup); the step fades up over the next 10% of the scene, so the
     line always arrives first. */
  --step-p: clamp(0, calc((var(--scrub-p, 1) - var(--t)) / 0.1), 1);
  flex: 1 1 0;
  min-width: 1px;
  display: flex;
  flex-direction: column;
  gap: 15px;
  /* Fade only — the movement belongs to the chevron */
  opacity: var(--step-p);
}
/* The marker slides in from the left as it fades (the fade is inherited from
   the step's own opacity), so it travels the same way the timeline draws.
   The negative margin pulls the icon's optical edge back into line with the
   text below it. */
.process-step__marker {
  display: block;
  width: 32px;
  height: 32px;
  margin-left: -10px;
  color: var(--c-orange);
  transform: translateX(calc((1 - var(--step-p)) * -26px));
}
.process-step__title {
  font-family: var(--font-serif);
  font-weight: 500;
  font-size: clamp(20px, 1.67vw, 24px);
  line-height: 1.15;
  color: #fff;
}
.process-step__text { font-size: 16px; line-height: 1.3; color: var(--c-green-40); }

@media (prefers-reduced-motion: reduce) {
  /* main.js pins --scrub-p at 1, but belt and braces */
  .process-line { width: calc(100% + 2 * var(--box-pad)); }
  .process-step { opacity: 1; }
  .process-step__marker { transform: none; }
}

/* ---- Join: founder promises --------------------------------------------- */
.join-promises { background: #fff; }
.join-promises .content-wrapper { padding-top: 50px; padding-bottom: 150px; }
.join-promises__content { display: flex; flex-direction: column; gap: 80px; align-items: center; }
.join-promises__head {
  display: flex;
  flex-direction: column;
  gap: 33px;
  align-items: center;
  text-align: center;
  max-width: 61%;            /* 875 of the design's 1440 */
}
.join-promises__heading {
  font-family: var(--font-serif);
  font-size: clamp(36px, 3.9vw, 56px);
  line-height: 1.05;
  color: var(--c-dark-green);
}
.promise-list {
  --grid-gutter: 15px;       /* the design's tighter card gap */
  list-style: none;
  margin: 0;
  padding: 0;
  width: 100%;
}
.promise-card {
  grid-column: span 3;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 15px;
  padding: 35px 20px;
  border-radius: 4px;
  background: var(--c-cream);
  box-shadow: 0 2px 2px rgba(0, 0, 0, 0.15);
  text-align: center;
}
.promise-card__icon { display: block; width: 64px; height: 64px; color: var(--c-orange); }
.promise-card__title { font-size: 21px; line-height: 1.3; font-weight: 400; color: var(--c-dark-green); }
.promise-card__text { font-size: 18px; line-height: 1.3; color: var(--c-green-80); }

/* ---- Join: FAQs ---------------------------------------------------------
   Native <details>, so it opens with no JS and is keyboard-operable for free;
   name="join-faq" makes it exclusive (opening one closes the last).
   The open/close animation is driven by main.js (see the FAQ accordion block
   there) rather than by ::details-content — that only animated in Chromium,
   so iOS opened with a snap. With no JS it still opens, just instantly. */

.join-faqs { position: relative; overflow: hidden; background: #fff; }
/* Will's gold fan, entering from the left edge. In Figma (section-faqs
   1226:27082) the section is 897 tall and the fan's box starts 13px below its
   top and runs the rest of the way down — i.e. it is sized off the section's
   HEIGHT, not the page width. Driving it off `top`/`height` keeps that true at
   any viewport; a %-of-width rule made it 1000px+ tall on a wide screen and
   the section clipped all but one line. */
.join-faqs__graphic {
  position: absolute;
  left: 0;
  top: 13px;
  height: calc(100% - 13px);
  width: auto;
  /* Draws downward as it arrives — the fan reads as being pulled out of the
     top edge. Same --wipe-p the horizontal graphics use, insetting from the
     bottom instead of the right. */
  clip-path: inset(0 0 calc((1 - var(--wipe-p, 1)) * 100%) 0);
}
.join-faqs .content-wrapper { position: relative; z-index: 1; padding-top: 100px; padding-bottom: 120px; }
.join-faqs__cols { align-items: start; }
.join-faqs__head { display: flex; flex-direction: column; gap: 25px; }
.join-faqs__heading {
  font-family: var(--font-serif);
  font-size: clamp(36px, 3.9vw, 56px);
  line-height: 1.05;
  color: var(--c-dark-green);
}
.join-faqs__heading em { font-style: italic; }

.faq-item { border-top: 1px solid var(--c-green-20); }
.faq-item:last-child { border-bottom: 1px solid var(--c-green-20); }
.faq-item__q {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 20px;
  padding: 22px 0;
  cursor: pointer;
  list-style: none;                 /* hides the UA marker */
  font-family: var(--font-serif);
  font-size: clamp(18px, 1.5vw, 21px);
  line-height: 1.25;
  color: var(--c-dark-green);
}
.faq-item__q::-webkit-details-marker { display: none; }
.faq-item__q:focus-visible { outline: 2px solid var(--c-orange); outline-offset: 4px; }
/* Plus that becomes a minus — two bars, one of which folds away */
.faq-item__icon {
  position: relative;
  flex: none;
  width: 18px;
  height: 18px;
  margin-top: 0.2em;
}
.faq-item__icon::before,
.faq-item__icon::after {
  content: "";
  position: absolute;
  inset: 50% 0 auto 0;
  height: 1px;
  background: var(--c-dark-green);
  transition: transform 0.3s ease;
}
.faq-item__icon::after { transform: rotate(90deg); }
.faq-item[open] .faq-item__icon::after { transform: rotate(0deg); }
.faq-item__a {
  padding: 0 40px 26px 0;
  font-size: 16px;
  line-height: 1.3;
  color: var(--c-green-80);
  max-width: 34em;
}
@media (prefers-reduced-motion: reduce) {
  .faq-item__icon::before, .faq-item__icon::after { transition: none; }
  .join-faqs__graphic, .contact-form__graphic { clip-path: none; }
}

/* ---- Contact ------------------------------------------------------------
   The design crops the hero to the TOP of the photograph — the skylight has
   to stay in frame — so the band sits at 0% rather than the 50% default. */
.contact-hero { --band-pos: 0%; }

/* ---- Contact: intro ------------------------------------------------------
   The same 8 : 4 shape as the About, Partners and Join intros, so all four
   inner pages open identically. */
.contact-intro .content-wrapper { padding-top: 40px; padding-bottom: 100px; }
.contact-intro__content { display: flex; flex-direction: column; gap: 20px; }
.contact-intro__cols { align-items: start; }
.contact-intro__heading {
  font-family: var(--font-serif);
  font-size: clamp(36px, 5vw, 72px);
  line-height: 1;
  letter-spacing: -0.02em;
  color: var(--c-dark-green);
  max-width: 8.1em;          /* the design's 582 at 72px */
  text-wrap: balance;
}
.contact-intro__heading em { font-style: italic; font-weight: 400; }
.contact-intro__aside {
  display: flex;
  flex-direction: column;
  gap: 18px;
  font-size: 18px;
  line-height: 1.3;
  color: var(--c-green-80);
}

/* ---- Contact: form ------------------------------------------------------
   Cream section. The form sits on columns 1-7 (796px at the design's 1440)
   and the office details on 9-12. */
.contact-form-section {
  position: relative;
  overflow: hidden;
  background: var(--c-cream);
}
.contact-form-section .content-wrapper {
  position: relative;
  z-index: 1;
  padding-top: 40px;
  padding-bottom: 80px;
}
.contact-form-section__cols { align-items: start; }
/* Will's gold fan, entering from the right edge 92px down and running off the
   bottom. Sized off the section height for the same reason as the Join FAQ
   graphic — a width-based rule outgrows the section on a wide screen. It is
   the last child in the design, so it crosses in *front* of the content. */
.contact-form__graphic {
  position: absolute;
  right: 0;
  top: 92px;
  height: calc(100% - 92px);
  width: auto;
  z-index: 2;
  pointer-events: none;
  clip-path: inset(0 0 calc((1 - var(--wipe-p, 1)) * 100%) 0);
}

/* Honeypot. Hidden from people, left in the DOM for bots to fill — anything
   that arrives with it filled is dropped by Netlify without a trace.
   `position: absolute` + `left: -9999px` rather than `display: none`: some
   bots skip anything display-none, which defeats the point. `aria-hidden` and
   `tabindex="-1"` on the markup keep it away from screen readers and the tab
   order, so nobody using the site can reach it by accident. */
.form-honeypot {
  position: absolute;
  left: -9999px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}

.contact-form { display: flex; flex-direction: column; gap: 24px; align-items: flex-start; }
.form-field {
  display: flex;
  flex-direction: column;
  gap: 10px;
  width: 100%;
  /* fieldset resets — two of these are <fieldset> for the grouped controls */
  margin: 0;
  padding: 0;
  border: 0;
  min-width: 0;
}
.form-field__label {
  padding: 0;                /* <legend> carries UA padding */
  font-size: 21px;
  line-height: 1.3;
  color: var(--c-dark-green);
}
/* First and last name share a row; the 22px gap is the design's */
.form-field__row { display: flex; gap: 22px; }
.form-sub { display: flex; flex-direction: column; gap: 10px; flex: 1 1 0; min-width: 1px; }
.form-sub__label { font-size: 16px; line-height: 1.3; color: var(--c-green-80); }

/* No border in the design — the white field on cream carries the edge */
.form-input {
  width: 100%;
  height: 65px;
  border: 0;
  border-radius: 40px;
  background: #fff;
  padding: 0 30px;
  font-family: var(--font-sans);
  font-size: 18px;
  line-height: 1.3;
  color: var(--c-dark-green);
  transition: box-shadow 0.25s ease;
}
.form-input--area {
  height: 257px;
  padding: 22px 30px;
  resize: vertical;
}
.form-input:focus-visible { outline: 2px solid var(--c-gold); outline-offset: 2px; }
/* Native validation only speaks up after a submit attempt (main.js adds the
   class), so an untouched field is never marked wrong. */
.contact-form.is-validated .form-input:invalid { box-shadow: inset 0 0 0 1px var(--c-orange); }

/* Checkboxes: the native input styled directly with appearance: none, rather
   than hidden behind a decorative span — it keeps the real control where the
   pointer and the focus ring expect it. */
.form-checks { display: flex; flex-direction: column; gap: 10px; margin-top: 10px; }
.form-check {
  display: flex;
  align-items: center;
  gap: 13px;
  cursor: pointer;
  font-size: 16px;
  line-height: 1.3;
  color: var(--c-green-80);
}
.form-check input {
  appearance: none;
  -webkit-appearance: none;
  flex: 0 0 20px;
  width: 20px;
  height: 20px;
  margin: 0;
  display: grid;
  place-content: center;
  border: 1px solid var(--c-dark-green);
  border-radius: 4px;
  background: #fff;
  cursor: pointer;
  transition: background-color 0.2s ease;
}
/* The tick is two borders of a rotated box — no extra asset */
.form-check input::before {
  content: "";
  width: 5px;
  height: 10px;
  margin-top: -3px;
  border: solid #fff;
  border-width: 0 2px 2px 0;
  transform: rotate(45deg) scale(0);
  transition: transform 0.2s ease;
}
.form-check input:checked { background: var(--c-dark-green); }
.form-check input:checked::before { transform: rotate(45deg) scale(1); }
.form-check input:focus-visible { outline: 2px solid var(--c-gold); outline-offset: 2px; }

/* This one swipes to dark green rather than the orange button's usual cream —
   cream on cream had nothing to land against. Declared after .btn--orange, so
   it wins on source order at equal specificity. */
.contact-form__submit { margin-top: 6px; }
.contact-form__submit::before { background: var(--c-dark-green); }
.contact-form__submit:hover,
.contact-form__submit:focus-visible { color: var(--c-cream); }
.contact-form__note { font-size: 16px; line-height: 1.3; color: var(--c-green-80); }

/* Office details column */
.contact-details { display: flex; flex-direction: column; gap: 55px; }
.contact-details__block { display: flex; flex-direction: column; gap: 25px; }
.contact-details__text {
  font-size: 21px;
  line-height: 1.3;
  color: var(--c-dark-green);
}
.contact-details__email {
  /* The column is a stretch flex container, so without this the <a> — and
     with it the hover underline — fills the full column width rather than
     the width of the text. */
  align-self: flex-start;
  font-size: 21px;
  line-height: 1.3;
  color: var(--c-dark-green);
  text-decoration: none;
  background-image: linear-gradient(currentColor, currentColor);
  background-repeat: no-repeat;
  background-size: 0 1px;
  background-position: 0 100%;
  transition: background-size 0.3s ease;
  overflow-wrap: anywhere;
}
.contact-details__email:hover { background-size: 100% 1px; }
.contact-details__linkedin { align-self: flex-start; }

/* ---- News ---------------------------------------------------------------
   No --band-pos here: the hero's variants are a square crop of Will's
   portrait, cut around the design's visible centre, so the default 50%
   frames exactly what the design frames. */

/* ---- News: intro --------------------------------------------------------
   2 : 9 on the grid. The heading starts on column 3, which is where the
   article list starts too, so one line runs down the left of the page. */
.news-intro .content-wrapper { padding-top: 40px; padding-bottom: 130px; }
.news-intro__cols { align-items: start; }
.news-intro__heading {
  font-family: var(--font-serif);
  font-size: clamp(34px, 3.9vw, 56px);
  line-height: 1.05;
  color: var(--c-dark-green);
}

/* ---- News: the cream band ----------------------------------------------
   One background behind the list, the pager and the signup panel — in the
   design they are three frames sharing a parent, not three sections. */
.news-band { background: var(--c-cream); }
.news-list .content-wrapper { padding-top: 40px; padding-bottom: 0; }
.news-list__items { display: flex; flex-direction: column; }

/* Every row carries a bottom rule, including the last — the design has it,
   and it closes the list off against the pager below. */
.news-card {
  display: flex;
  align-items: flex-start;
  gap: 48px;
  padding: 30px 0;
  border-bottom: 1px solid var(--c-green-20);
}
/* 452 : 526 of the 1026 row, so the proportions hold at any width */
.news-card__media {
  flex: 452 1 0;
  min-width: 1px;
  display: block;
  aspect-ratio: 1 / 1;
  overflow: hidden;
  border-radius: 4px;
}
.news-card__media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: var(--focal, 50% 50%);
  transition: transform 0.6s cubic-bezier(0.33, 1, 0.68, 1);
}
.news-card:hover .news-card__media img { transform: scale(1.04); }
.news-card__body {
  flex: 526 1 0;
  min-width: 1px;
  display: flex;
  flex-direction: column;
  gap: 20px;
}
/* A wider tracking and a lighter tint than the page eyebrows — this one is
   a date, not a label, so it sits back a little further. */
.news-card__date {
  font-size: 16px;
  line-height: 1.3;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--c-green-60);
}
.news-card__title {
  font-family: var(--font-serif);
  font-size: clamp(24px, 2.5vw, 36px);
  line-height: 1.1;
  font-weight: 400;
  color: var(--c-dark-green);
}
/* The title is the card's only focusable link; the photo and "Read more" are
   hidden from the tab order and pick up its hover through the card. */
.news-card__title a {
  color: inherit;
  background-image: linear-gradient(currentColor, currentColor);
  background-repeat: no-repeat;
  background-size: 0 1px;
  background-position: 0 100%;
  transition: background-size 0.35s ease;
}
.news-card:hover .news-card__title a,
.news-card__title a:focus-visible { background-size: 100% 1px; }
.news-card__excerpt {
  font-size: 16px;
  line-height: 1.3;
  color: var(--c-green-80);
}
.news-card__more { align-self: flex-start; }
.news-card:hover .news-card__more { gap: 6px; }
.news-card:hover .news-card__more .btn-arrow__chevron { transform: translateX(0); }
.news-card:hover .news-card__more .btn-arrow__shaft { transform: scaleX(1); }

/* ---- News: pager -------------------------------------------------------- */
.news-pager .content-wrapper { padding-top: 20px; padding-bottom: 100px; }
.news-pager__nav { display: flex; gap: 23px; }

/* ---- News: signup panel -------------------------------------------------
   Dark green with an orange gradient over it at 60%, rounded and clipped.
   Built in CSS rather than from Will's flattened bg-cta-news.jpg export —
   that file bakes in a seam where the gradient rectangle stops short of the
   panel's bottom edge (see the note in CLAUDE.md). */
/* Inside a .content-wrapper so the panel keeps the page gutter and the same
   1800px ceiling as every other section — the design's own 40/52 inset is
   asymmetric and doesn't scale. */
.news-signup .content-wrapper { padding-top: 100px; padding-bottom: 50px; }
/* Will's own composite export carries the gradient and the fan artwork in
   one file. The horizontal step partway down is deliberate — he confirmed it.
   Dark green sits under it so the cream type is legible while it loads. */
.news-signup__panel {
  position: relative;
  overflow: hidden;
  border-radius: 8px;
  background: var(--c-dark-green);
}
.news-signup__bg {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
/* On the page's own 12-column grid, with no horizontal padding of its own —
   that is what lets the form's left edge land on column 8, the same column
   the footer nav below starts on. The panel's visual inset is carried by
   padding on the two children instead, so it never shifts the columns. */
.news-signup__inner {
  position: relative;
  z-index: 1;
  align-items: start;
  padding: 50px 0;
}
.news-signup__head {
  display: flex;
  flex-direction: column;
  gap: 28px;
  padding-left: 40px;
}
.news-signup__eyebrow { color: var(--c-orange-40); }
.news-signup__heading {
  max-width: 8em;
  font-family: var(--font-serif);
  font-size: clamp(34px, 3.9vw, 56px);
  line-height: 1.05;
  color: var(--c-cream);
}
/* Spans columns 8-12; the right inset is padding, so the inputs' LEFT edge
   stays exactly on the column line. */
.news-signup__form {
  display: flex;
  flex-direction: column;
  gap: 18px;
  padding-right: 40px;
}
/* The design draws these with 4px corners; Will took them to the contact
   form's 40px instead, which browsers clamp to a pill at this height. */
.news-signup__input {
  height: 58px;
  border: 0;
  border-radius: 40px;
  background: var(--c-cream-50);
  padding: 0 36px 0 30px;
  font-family: var(--font-sans);
  font-size: 18px;
  line-height: 1.3;
  color: var(--c-dark-green);
}
.news-signup__input::placeholder { color: var(--c-dark-green); opacity: 1; }
.news-signup__input:focus-visible { outline: 2px solid var(--c-gold); outline-offset: 2px; }
.news-signup__btn { align-self: flex-start; margin-top: 12px; }
.news-signup__note { color: var(--c-orange-40); }
.news-signup__legal { font-size: 16px; line-height: 1.3; color: var(--c-orange-40); }
.news-signup__legal a {
  background-image: linear-gradient(currentColor, currentColor);
  background-repeat: no-repeat;
  background-size: 0 1px;
  background-position: 0 100%;
  transition: background-size 0.3s ease;
}
.news-signup__legal a:hover { background-size: 100% 1px; }

/* ---- Article (single post) — the prefix is `post-`, NOT `article-` --------
   `.article` and `.article__body` were already taken by the homepage's news
   teaser (a flex row with a 48px gap), and reusing them here collapsed the
   whole page to a 58px column. Keep the `post-` prefix.

   The body is a stack of blocks — prose and media — so a post can alternate
   them freely; nothing depends on what came before, which is what makes the
   same markup safe to generate from a CMS in any order.

   The gradient is a background on .post itself, not a painted layer. It was a
   layer once and never showed: `.post > *` (z-index 1) matched the layer too
   and overrode its `position: absolute`, dropping it out of the stack. */
.post {
  position: relative;
  /* Figma "Light Cream Grad" on the article wrapper (1533:10996) */
  background: linear-gradient(to bottom, #fff 0%, var(--c-cream) 100%);
  /* absorbs the scrollbar's worth of overhang from the full-bleed block */
  overflow-x: clip;
}

/* ---- Article: title ---- */
.post-head .content-wrapper { padding-top: 110px; padding-bottom: 20px; }
.post-head__cols { align-items: start; }
.post-head__date {
  font-size: 16px;
  line-height: 1.3;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--c-green-80);
  padding-top: 0.5em;          /* optical: sits with the title's first line */
}
.post-head__title {
  font-family: var(--font-serif);
  font-size: clamp(34px, 3.9vw, 56px);
  line-height: 1.05;
  color: var(--c-dark-green);
}

/* ---- Article: block stack ----
   No gap. Each block carries its OWN margin, so the rhythm can differ by
   block type: a full-bleed image wants more air than a paragraph does. The
   body is a flex column, so these margins do NOT collapse — 20 below the
   prose plus 20 above the next image is 40 between them, which is what the
   design draws (each of its sections has 20px of its own padding). */
.post__body { display: flex; flex-direction: column; padding-bottom: 50px; }
.post-prose { margin: 20px 0; }
.post-media { margin: 20px 0; }
.post-media--wide { margin: 20px 0; }
.post-media--full { margin: 40px 0; }

/* ---- Article: the shell every block shares ----
   The blocks can't live inside .content-wrapper — the full-bleed one has to
   escape it — so they reproduce it. `--content-max` is the single source both
   read from, so the two can't drift apart. */
.post-prose,
.post-media {
  width: 100%;
  max-width: var(--content-max);
  margin-left: auto;
  margin-right: auto;
  padding: 0 var(--pad-x);
}

/* ---- Article: prose ----
   ONE wrapper carries the column position, and the children carry nothing
   but type. That is deliberate: a CMS can emit any heading level, list,
   table or embed inside here and it inherits the column for free — putting
   the offset on every child meant every new element needed the calc repeating.
   Width is capped at --prose-max so line length stays readable on a wide
   screen; without it, 7 of 12 columns is ~1000px in an 1800 wrapper. */
.post-prose__col {
  width: min(
    calc((100% - 11 * var(--grid-gutter)) / 12 * 7 + 6 * var(--grid-gutter)),
    var(--prose-max)
  );
  margin-left: calc((100% - 11 * var(--grid-gutter)) / 12 * 3 + 3 * var(--grid-gutter));
}

/* Type ramp. Margins are in `em`, so they scale with the type rather than
   drifting apart from it — the px in the comments are what they resolve to at
   the desktop sizes. Margins collapse between siblings here (the column is a
   plain block), so a heading after a paragraph gets its own top margin, not
   the sum of both. */
.post-prose p {
  font-size: clamp(18px, 1.46vw, 21px);
  line-height: 1.3;
  color: var(--c-green-80);
  margin-bottom: 1em;                    /* 21px */
}
.post-prose h2 {
  font-family: var(--font-serif);
  font-weight: 400;
  font-size: clamp(30px, 2.9vw, 42px);
  line-height: 1.1;
  color: var(--c-dark-green);
  margin-top: 1em;                       /* 42px at 42 — em, so it tracks the
                                            type rather than drifting from it */
  margin-bottom: 0.36em;                 /* 15px */
}
.post-prose h3 {
  font-family: var(--font-serif);
  font-weight: 500;
  font-size: clamp(21px, 1.67vw, 24px);
  line-height: 1.15;
  color: var(--c-dark-green);
  margin-top: 1.6em;                     /* 38px */
  margin-bottom: 0.6em;                  /* 14px */
}
.post-prose h4 {
  font-weight: 700;
  font-size: 18px;
  line-height: 1.3;
  color: var(--c-dark-green);
  margin-top: 1.85em;                    /* 33px */
  margin-bottom: 0.55em;                 /* 10px */
}
.post-prose h5 {
  font-weight: 500;
  font-size: 16px;
  line-height: 1.3;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--c-green-60);
  margin-top: 2em;                       /* 32px */
  margin-bottom: 0.5em;                  /* 8px */
}
/* On the COLUMN, not on .post-prose — the children moved inside the wrapper
   when it was introduced, and these two selectors stopped matching, so a
   block opening with a heading carried its full top margin. */
.post-prose__col > *:first-child { margin-top: 0; }
.post-prose__col > *:last-child { margin-bottom: 0; }
/* `:not(.btn)` — a real button (the thank-you/subscribed pages' "Back to
   the homepage", or any future one) is a `.btn` living inside `.post-prose`,
   not an inline text link, and shouldn't pick up prose's gold colour and
   animated underline. Excluding it here means `.btn`'s own rules are the
   only ones touching it — no specificity fight, no `!important` needed on
   either side. */
.post-prose a:not(.btn) {
  color: var(--c-gold);
  background-image: linear-gradient(currentColor, currentColor);
  background-repeat: no-repeat;
  background-size: 100% 1px;
  background-position: 0 100%;
  transition: background-size 0.3s ease;
}
.post-prose a:not(.btn):hover { background-size: 0 1px; }
/* Lists carry the brand bullet — the same `bullet.svg` wedge as .tick-list,
   so a list in an article or a policy matches the ones on the Founders page.
   Only `ul` is restyled; an `ol` keeps its numbers. */
.post-prose ul, .post-prose ol { margin-bottom: 2em; padding-left: 20px; }
/* Standfirst — the deck under a post's title. Bigger than body copy, same
   colour, and it is a <p>: the page already has its one h1.
   TWO-CLASS SELECTOR, and for the same reason as the media-frame modifiers:
   `.post-prose p` is (0,1,1) and beat a bare `.post-lead` (0,1,0), so the
   size silently did nothing and every standfirst rendered at body size.
   The step is the house ratio — the same 21-against-18 relationship the
   Founders and Contact leads use, scaled to this column's 21px body. */
.post-prose .post-lead {
  font-size: clamp(20px, 1.9vw, 25px);
  line-height: 1.35;
  margin-bottom: 1em;
}

.post-prose ul { list-style: none; }
.post-prose li {
  font-size: clamp(18px, 1.46vw, 21px);
  line-height: 1.3;
  color: var(--c-green-80);
  margin-bottom: 0.7em;
}
.post-prose ul li { position: relative; padding-left: 30px; }
.post-prose ul li::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0.36em;
  width: 12px;
  height: 12px;
  background: url("../assets/bullet.svg") no-repeat center / contain;
}
/* No rule, no indent, no background in the design — the quote is simply
   bigger and in the serif, at the same left edge as the prose. */
.post-prose blockquote { margin: 1.9em 0; }   /* 40px */
.post-prose blockquote p {
  font-family: var(--font-serif);
  font-size: clamp(26px, 2.5vw, 36px);
  line-height: 1.1;
  color: var(--c-dark-green);
  margin-bottom: 0.72em;                 /* 26px */
}
.post-prose blockquote cite {
  display: block;
  font-style: normal;
  font-size: 18px;
  line-height: 1.3;
  color: var(--c-green-80);
}

/* ---- Article: media blocks ----
   Three widths, one shell — an <img>, a self-hosted <video> or an <iframe>
   embed all drop into the same frame. */
.post-media__frame {
  overflow: hidden;
  border-radius: 4px;
  background: var(--c-green-20);   /* holds the space while media loads */
}
.post-media__frame > img,
.post-media__frame > video,
.post-media__frame > iframe {
  display: block;
  width: 100%;
  height: 100%;
  border: 0;
  object-fit: cover;
  /* Per-image focal point. A square crop of a 2:3 headshot cuts the top of
     the head off at the 50% default, so the image sets `--focal` inline and
     nothing else has to know about it. Same hook on the news card and the
     homepage teaser — a CMS focal-point field maps straight on to it. */
  object-position: var(--focal, 50% 50%);
}
/* Base = inline: the prose column, capped the same way, so it reads as part
   of the text. The two modifiers widen it from there. */
.post-media__frame,
.post-media figcaption {
  width: min(
    calc((100% - 11 * var(--grid-gutter)) / 12 * 7 + 6 * var(--grid-gutter)),
    var(--prose-max)
  );
  margin-left: calc((100% - 11 * var(--grid-gutter)) / 12 * 3 + 3 * var(--grid-gutter));
}
.post-media .post-media__frame { aspect-ratio: 1 / 1; }
/* Wide: the full grid width, inside the page gutter — the design's own */
.post-media--wide .post-media__frame { width: 100%; margin-left: 0; aspect-ratio: 1380 / 573; }
/* Full-bleed: only the FRAME breaks out, by pulling itself to viewport width.
   The figure keeps the shell, which is what lets the caption stay on the
   prose column — the caption is the reason it is done this way round. */
.post-media--full .post-media__frame {
  width: 100vw;
  margin-left: calc(50% - 50vw);
  border-radius: 0;
  aspect-ratio: 1380 / 573;
}
/* A wider frame for video, or for artwork the block's own ratio doesn't suit.
   NOTE THE TWO-CLASS SELECTOR. Every ratio above is set by a two-class rule
   (`.post-media .post-media__frame`, `.post-media--full .post-media__frame`),
   so a single-class modifier loses on specificity and silently does nothing —
   which is what these did until the Academy logo needed one. Matching the
   count and sitting after them is what makes the override land. */
.post-media .post-media__frame--16x9 { aspect-ratio: 16 / 9; }
.post-media .post-media__frame--21x9 { aspect-ratio: 21 / 9; }
.post-media figcaption {
  margin-top: 15px;
  font-size: 16px;
  line-height: 1.3;
  color: var(--c-green-60);
}

/* ---- Article: pager ---- */
.post-pager .content-wrapper { padding-top: 60px; padding-bottom: 60px; }
.post-pager__nav { display: flex; gap: 23px; }

/* ---- Standard page template (Privacy, Modern Slavery, and whatever next) --
   Deliberately named `page-`, not `legal-`: the shape is title -> photograph
   -> one column of copy, which suits any text-led page, not only a policy.
   It inverts the other inner pages, which open with the photograph.
   Four things vary per page: the eyebrow, the h1 (and its measure), the hero
   image (+ its --band-pos) and the prose. */
.page-intro .content-wrapper { padding-top: 110px; padding-bottom: 40px; }
.page-intro__cols { align-items: start; }
/* Two lines in one block — the page label and the revision date */
.page-intro__meta {
  font-size: 14px;
  line-height: 1.3;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--c-green-80);
}
/* Each page sets its own measure, because the design composes the two
   titles differently: Privacy is deliberately broken over two lines at 494px
   (it would sit on one at full width), Modern Slavery runs to three at 774. */
.page-intro__title--privacy { max-width: 6.86em; }   /* 494 at 72px */
.page-intro__title--slavery { max-width: 10.75em; }  /* 774 at 72px */
/* The two Aug 2026 pages have no Figma frame. Measures are set so each title
   breaks where it reads best rather than running the full column: CSR to two
   lines, People to one. */
.page-intro__title--csr    { max-width: 10.75em; }
.page-intro__title--people { max-width: 10.75em; }
.page-intro__title {
  font-family: var(--font-serif);
  font-size: clamp(34px, 5vw, 72px);
  line-height: 1;
  letter-spacing: -0.02em;
  color: var(--c-dark-green);
}

/* Still band — no data-parallax, so .image-band's plain geometry applies */
.page-hero { --band-h: 423px; }
/* The offsets travelled with the photographs in the Aug 2026 swap: Modern
   Slavery now shows what Privacy used to (48.7%), and Privacy's new photo is
   centre-cropped, so it sits at the 50% default. */
.page-hero--privacy { --band-pos: 50%; }
.page-hero--slavery { --band-pos: 48.7%; }
/* Will's own photographs, framed from Figma (CSR 1595:31351, People
   1595:31338). BOTH VALUES ARE DERIVED, NOT EYEBALLED, and the sum is worth
   keeping because every future hero needs it:

     the <img> box is 124% of the band, offset -12% (the parallax geometry),
     so with a landscape source `cover` matches the band WIDTH and the
     rendered height is  bandWidth / aspect.  Then

         --band-pos  =  (-0.12 * bandH  -  figmaImageY) / (imgH - 1.24 * bandH)

   Figma draws the band 1440 x 423.096; plug in the image's y and the height
   it renders at, and the result frames exactly what the design frames.
   Checked both ways: our rendered heights (960.8 / 960.2) match the heights
   Figma drew to a tenth of a pixel. */
.page-hero--csr    { --band-pos: 71.6%; }
.page-hero--people { --band-pos: 18.5%; }

/* Spacing above the button on the two `page-thanks` pages (thank-you.html,
   subscribed.html) — independent of the section around it. */
.thanks-actions { margin-top: 35px; }

.page-content {
  position: relative;
  /* NO `overflow: hidden` here. It was added to clip the graphic and it
     silently killed the graphic's `position: sticky`: an ancestor with a
     non-visible overflow becomes the sticky element's scrollport, and since
     that box doesn't scroll, sticky never engages. Nothing overflows this
     section anyway — the graphic's track is inside it and the artwork is
     already cropped to its 195px window. */
  background: var(--c-cream);
  /* The design's section is a fixed 1006, which is just the artwork's 187px
     offset plus its 819px height. As a min-height it holds that shape without
     capping the copy — these pages will run far longer than the mock. */
  min-height: 1006px;
}
/* The two `page-thanks` pages (thank-you.html, subscribed.html) reuse
   `.page-content` for the same gutters and prose treatment as the policy
   pages, but carry none of the `.page-content__aside` fan artwork the
   1006px above is sized for — left as-is, a two-line confirmation would sit
   in a screenful of empty cream under it. Same fix as the mobile reset
   below, just scoped to these two pages at every width instead of to a
   breakpoint. */
.page-thanks .page-content { min-height: 0; }

/* Will's orange fan, hard against the left edge and behind the text. It never
   reaches the prose column (195px wide against a column starting at 380). */
/* The graphic's track: absolutely positioned down the left of the section,
   from the design's 187px offset to the section's bottom edge.
   No draw-in reveal here — it was tried and removed: the artwork spends most
   of its life pinned to the bottom of the screen, so the wipe played once,
   off to the side, and nobody saw it. */
.page-content__aside {
  position: absolute;
  left: 0;
  top: 187px;
  bottom: 0;
  width: 195px;
  z-index: 0;
  pointer-events: none;
  /* The graphic's natural home is the BOTTOM of the track. That is what makes
     `bottom: 0` stickiness work at all: sticky-bottom only ever pulls an
     element UP from below the fold — it cannot drag one back down once it has
     scrolled off the top. Parked at the top, the first attempt did nothing. */
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
}
/* Sticky INSIDE the track — `position: sticky` needs an in-flow element, so
   the graphic can't be the absolutely-positioned one. It draws downward as
   the section arrives (like the Join FAQ and Contact fans), rides the bottom
   of the screen through the copy, then settles at the section's foot. */
.page-content__graphic {
  position: sticky;
  bottom: 0;
  display: block;
  width: 100%;
  height: auto;
}
/* `.post-prose` carries `margin: 20px 0` for the article, where blocks
   alternate and each needs its own air. Here the prose is the only block, and
   because .page-content has no padding of its own that top margin COLLAPSED
   out through the section, pushing the whole cream band down and showing 20px
   of white under the hero. The section's own spacing is the padding below. */
.page-content .post-prose {
  margin: 0;
  position: relative;
  z-index: 1;
  padding-top: 40px;
  padding-bottom: 100px;
}
/* The article's prose column, with one change: headings take more air
   beneath them here (30px against the article's 15). The design also drew
   them in the body colour — Will confirmed that was a mistake, so they keep
   the article's dark green. */
.page-content h2 { margin-bottom: 0.72em; }   /* 30px at 42 */

/* ---- Responsive: mobile ------------------------------------------------- */
/* Breakpoint at 860px so the header nav + CTA never get squeezed */
@media (max-width: 859px) {
  /* ---- One band height for the whole site ----
     Every `.image-band` — page heroes, the news hero, the article bands — is
     250px on a phone. It used to be per-section: 300px here, 300px there, and
     one of them set `height` directly rather than `--band-h`, which beats the
     variable outright and would have silently ignored this rule. If a band
     needs to differ it overrides `--band-h` BELOW this line, with a reason. */
  .image-band { --band-h: 250px; }
  :root { --pad-x: 15px; --header-h: 66px; }

  /* The 12-column grid collapses to one column: every span goes full width
     and every start is dropped, so a grid row stacks in source order. */
  .grid-12 { grid-template-columns: minmax(0, 1fr); }
  .grid-12 > * { grid-column: 1 / -1; }

  .site-header__inner { padding: 5px var(--pad-x); min-height: 66px; }
  .site-header .logo__type { display: none; } /* footer keeps its logotype on mobile */
  .logo__mark { width: 59.4px; margin-bottom: 0; }
  .logo__mark .logomark { width: 59.4px; height: 25.8px; }
  /* No logotype here to trade away, and the mark is already the larger size —
     so the scroll transformation is a desktop-only move. */
  .site-header .logo__mark { transform: none; will-change: auto; }
  .nav-pill { display: none; }
  .site-header__cta { display: none; }
  .menu-btn { display: flex; }

  .section-title .content-wrapper {
    padding-top: 62px;
    padding-bottom: 20px;
  }
  .section-title__content { --grid-row-gap: 20px; align-items: stretch; }
  .section-title__aside {
    justify-content: flex-start;
    gap: 20px;
    padding: 15px 0;
  }
  .section-title__aside p { font-size: 16px; }

  /* Buttons step down site-wide on mobile (46px per the mobile Figma frame).
     The newsletter button is the exception — it pairs with its 56px input. */
  .btn { --btn-h: 46px; }
  .newsletter .btn { --btn-h: 56px; }

  /* Video — mobile only: pulled up so its top edge sits at the midpoint of
     the title button (btn half 23 + aside pb 15 + wrapper pb 20) */
  .section-video { height: 254px; margin-top: -58px; --wipe-run: 356px; }

  /* Intro */
  .eyebrow { font-size: 12px; }
  .section-intro .content-wrapper { padding-top: 70px; padding-bottom: 70px; }
  .section-intro__content { gap: 50px; }
  .section-intro__head { gap: 20px; }
  .section-intro__cols { --grid-row-gap: 20px; }
  .section-intro__aside { font-size: 16px; }
  .section-intro__body { gap: 25px; }
  /* Cards become a swipeable full-bleed carousel. width:auto is essential —
     with width:100% the negative margins shift the list without widening it,
     leaving a gap at the right instead of letting cards run off the edge. */
  /* In the carousel the cards sit off the right edge, so a per-card stagger
     means you swipe to a card that hasn't arrived yet. main.js reveals the
     whole [data-reveal-together] list at once here; this lands them together
     and sooner. */
  .section-intro .card,
  .about-values .card { --reveal-delay: 0.55s; }

  .card-list {
    width: auto;
    align-self: stretch; /* parent is align-items:flex-start, which would otherwise shrink-to-fit */
    margin: 0 calc(-1 * var(--pad-x));
    padding: 0 var(--pad-x);
    overflow-x: auto;
    scroll-snap-type: x mandatory;
    scrollbar-width: none;
  }
  .card-list::-webkit-scrollbar { display: none; }
  .card {
    flex: 0 0 296px;
    padding: 15px 15px 20px;
    gap: 10px;
    scroll-snap-align: start;
    scroll-margin-left: var(--pad-x);
  }
  .card__title { font-size: 21px; }
  .card__text { font-size: 16px; }
  .section-intro__graphic { height: 64px; }

  /* The page intros step up to 48px on a phone and drop their measure — the
     desktop clamps left them looking timid here. The homepage intro is NOT in
     this set: its heading is a full sentence rather than a short statement, so
     48px ran it to eight lines. The homepage h1 takes the bump instead. */
  .about-intro__heading,
  .partners-intro__heading,
  .join-intro__heading { font-size: 48px; max-width: none; }
  .section-title__heading { font-size: 48px; max-width: none; }

  /* Companies — half-scale items, quicker loop over the shorter track */
  .section-companies { gap: 40px; padding: 40px 0; }
  .marquee {
    --item-w: 125px;
    --item-h: 78px;
    --logo-scale: 0.5;
    --marquee-duration: 38s;   /* half-width items → shorter set */
  }

  /* Stats — 2×2 grid */
  .section-stats .content-wrapper { padding: 40px var(--pad-x); }
  .section-stats__content { gap: 90px; padding-bottom: 20px; }
  .section-stats__head { flex-direction: column; align-items: flex-start; gap: 20px; }
  .section-stats__heading { flex: none; width: 100%; }
  .stat-list {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    column-gap: 30px;
    row-gap: 20px;
  }
  .stat { align-self: start; }

  /* Testimonial */
  .section-testimonial .content-wrapper { padding-top: 40px; padding-bottom: 40px; }
  .section-testimonial__content { gap: 15px; }
  .section-testimonial__cols { flex-direction: column; gap: 30px; }
  .quote { flex: none; width: 100%; gap: 15px; }
  .quote__cite { font-size: 14px; }
  .section-testimonial__logo {
    flex: none;
    align-self: flex-end;
    padding: 5px 0;
  }
  .section-testimonial__logo img { width: 64px; }
  /* Button rises to sit alongside the Irwin logo, per the mobile design */
  .section-testimonial .btn { margin-top: -75px; }
  .angle-image {
    position: relative;
    width: 100%;
    margin: 30px 0 0 var(--pad-x);
    /* shifted by the widest stripe offset so the stripes start at the gutter
       and the photo bleeds off the right, as in the mobile design */
    transform: translateX(12.06%);
  }

  /* CTA — copy above the button */
  .section-cta .content-wrapper { padding-top: 40px; padding-bottom: 40px; }
  .section-cta__content { gap: 140px; }
  .section-cta__head { gap: 10px; }
  .section-cta__head { max-width: none; }
  /* Stacked, with the paragraph above the button as in the design */
  .section-cta__cols { --grid-row-gap: 30px; align-items: start; }
  .section-cta__col--btn { order: 2; }
  .section-cta__col--text { order: 1; }
  .section-cta__text { font-size: 16px; }

  /* News — stacks, with a small square thumbnail beside the copy */
  .section-news .content-wrapper { padding-top: 40px; padding-bottom: 40px; }
  .section-news__content { flex-direction: column; gap: 20px; }
  .section-news__aside {
    flex: none;
    width: 100%;
    display: contents;         /* heading above the article, button below */
  }
  .section-news__heading { order: 1; }
  .article { order: 2; flex: none; width: 100%; gap: 15px; padding-top: 15px; }
  .section-news__more { order: 3; margin-top: 20px; align-self: flex-start; }
  .article__media { flex: 0 0 93px; aspect-ratio: 93.4 / 93.75; border-radius: 1px; }
  .article__body { gap: 10px; }
  .article__date { font-size: 12px; }
  .article__excerpt { font-size: 14px; }


  /* Partners */
  .partners-intro .content-wrapper { padding-top: 30px; padding-bottom: 60px; }
  .partners-intro__content { gap: 20px; }
  .partners-intro__cols { --grid-row-gap: 40px; }
  .partners-intro__aside { font-size: 16px; max-width: none; }
  .partners-companies .content-wrapper { padding-top: 40px; padding-bottom: 80px; }
  /* No mobile frame exists for this page. One card per row, and the card turns
     landscape so the reverse copy has room without a 480px-tall card. */
  .partner-list { --grid-row-gap: 15px; }
  /* Full-height and much fainter here: at this ratio a half-height band would
     be a thin strip, so the photo becomes a wash behind the logotype. */
  .partner-card__photo { height: 100%; opacity: 0.15; }
  .partner-card__logo { width: calc(var(--logo-w, 60%) * 0.62); }
  .partner-card__text,
  .partner-card__hq { font-size: 16px; }
  /* The card grew when the HQ line was added (Sep 2026). At the old 4:2 the
     longest reverse needed 205px of a 192px card — it had been within 15px of
     overflowing before, and one more line tipped it over.

     A HEIGHT, not a ratio. A ratio on a full-width stacked card ties height to
     screen width, which is the wrong driver twice over: on a narrow phone the
     same copy wraps to more lines in LESS width, so the ratio shrinks the box
     exactly when it needs to grow; and at the top of this breakpoint an 859px
     card came out 494px tall around 200px of content. The clamp fixes both —
     a floor for small screens, a ceiling so it never becomes a mostly-empty
     panel. Measured at 320 / 390 / 480 / 700 / 859.

     The three partners still without a description will need re-checking when
     their copy lands. Desktop is untouched — it has 179px of headroom. */
  .partner-card__inner { height: clamp(260px, 62vw, 310px); aspect-ratio: auto; }
  .partner-card__title { gap: 6px; }
  .partner-card__back { gap: 12px; }


  /* Join */
  .join-intro .content-wrapper { padding-top: 30px; padding-bottom: 60px; }
  .join-intro__cols { --grid-row-gap: 40px; }
  .join-intro__aside { font-size: 16px; max-width: none; }
  .join-difference .content-wrapper { padding-top: 50px; }
  .join-difference__head { flex-direction: column; align-items: flex-start; gap: 25px; }
  .join-difference__title { max-width: none; gap: 15px; }
  /* Arrows are for pointers; a phone swipes the track instead */
  .carousel-nav { display: none; }
  .carousel { margin-top: 60px; padding-bottom: 110px; gap: 12px; }
  .carousel > * { flex-basis: 65%; }
  .difference-card { padding: 25px 20px; }
  /* Founders stacks the other way round here: the copy leads and the photo
     block follows it, so the section reads heading-first on a phone. */
  .join-founders { display: flex; flex-direction: column; }
  .join-founders .content-wrapper { padding-top: 50px; padding-bottom: 60px; }
  .join-founders__cols { --grid-row-gap: 0; }
  /* In the flow and flush to the left edge, below the copy, with room under
     it before the next section. Shallower than the desktop frame — the ratio
     is the desktop 474 : 583 with a quarter off the height (583 → 437), so
     the photo crops rather than squashes. */
  .founders-image {
    position: relative;
    top: auto;
    order: 2;
    width: 70%;
    aspect-ratio: 474 / 437;
    margin: 0 0 70px;
  }
  /* The shorter frame would squash the curve, so crop it from the top
     instead — the bottom sweep is the part that reads. */
  .founders-image__graphic { object-fit: cover; object-position: left bottom; }
  .join-founders__col { gap: 30px; }
  .join-founders__head { gap: 15px; }
  .join-founders__lead { font-size: 18px; max-width: none; }
  .tick-list li { font-size: 16px; max-width: none; }


  /* Join: process — no pinning on a phone. The scene collapses to its content,
     the panel scrolls normally and the steps stack, so the timeline (a
     horizontal device) is dropped rather than squashed. */
  .process-scene { height: auto; }
  .process-sticky { position: static; height: auto; display: block; }
  .process-list { flex-direction: column; gap: 30px; }
  .process-line { display: none; }
  .process-step { opacity: 1; }
  .process-step__marker { transform: none; }
  .process-step__text { font-size: 16px; }
  .process-box { min-height: 0; display: block; }

  /* Join: promises — one card per row */
  .join-promises .content-wrapper { padding-top: 60px; padding-bottom: 60px; }
  .join-promises__content { gap: 40px; }
  .join-promises__head { max-width: none; gap: 15px; }
  .promise-list { --grid-row-gap: 15px; }
  .promise-card { grid-column: 1 / -1; margin: 0 10vw; padding: 25px 20px; }
  .promise-card__text { font-size: 16px; }


  /* Join: FAQs stack, heading above the accordion */
  .join-faqs .content-wrapper { padding-top: 50px; padding-bottom: 150px; }
  /* Bigger share of a narrow screen, or it reads as a stray mark */
  .join-faqs__graphic { width: 34%; height: auto; top: auto; bottom: -6%; opacity: 0.35; }

  /* Standard page template — the title block tightens, the graphic shrinks so it does
     not crowd the copy, and the section's desktop min-height is dropped: it
     exists to fit the artwork, and the artwork is smaller here. */
  .page-intro .content-wrapper { padding-top: 40px; padding-bottom: 25px; }
  .page-intro__cols { --grid-row-gap: 15px; }
  .page-intro__title { font-size: 34px; max-width: none; }
  .page-content { min-height: 0; }
  /* Hidden, not shrunk. On desktop the fan lives in the 350px of empty margin
     to the left of the prose; on a phone the prose runs full width, so any
     size of it ends up crossing the copy — and orange hairlines behind legal
     text read as damage rather than decoration. The other page graphics stay
     on mobile because they sit in genuinely empty areas; this one can't. */
  .page-content__aside { display: none; }
  .page-content__prose { padding-top: 35px; padding-bottom: 60px; }

  /* Article — the 3/12 indent goes: on a phone the prose runs the full
     column, and all three media widths collapse to the same thing. The
     block variants stay in the markup so the desktop distinction survives. */
  .post-head .content-wrapper { padding-top: 40px; padding-bottom: 20px; }
  .post-head__cols { --grid-row-gap: 15px; }
  .post-head__date { padding-top: 0; }
  .post-head__title { font-size: 34px; }
  .post__body { padding-bottom: 30px; }
  .post-prose, .post-media, .post-media--wide { margin: 15px 0; }
  .post-media--full { margin: 25px 0; }
  .post-prose__col,
  .post-media__frame,
  .post-media figcaption { width: 100%; margin-left: 0; }
  /* the frame still breaks out; only the offset goes */
  .post-media--full .post-media__frame { width: 100vw; margin-left: calc(50% - 50vw); }
  .post-prose p, .post-prose li { font-size: 18px; }
  .post-prose h2 { font-size: 30px; }
  .post-prose h3 { font-size: 21px; }
  .post-prose blockquote { margin-top: 40px; margin-bottom: 30px; }
  .post-prose blockquote p { font-size: 26px; margin-bottom: 18px; }
  .post-media .post-media__frame,
  .post-media--wide .post-media__frame,
  .post-media--full .post-media__frame { aspect-ratio: 3 / 2; }
  .post-pager .content-wrapper { padding-top: 40px; padding-bottom: 50px; }
  .post-pager__nav { gap: 15px; }

  /* News — the intro heading takes the same 48px bump as the other page
     intros; the card goes from a row to a stack; the signup panel's two
     columns stack and the button goes full width of the form column. */
  .news-intro .content-wrapper { padding-top: 30px; padding-bottom: 50px; }
  .news-intro__cols { --grid-row-gap: 20px; }
  .news-intro__heading { font-size: 48px; }
  .news-list .content-wrapper { padding-top: 20px; }
  .news-card { flex-direction: column; gap: 20px; padding: 30px 0; }
  /* Wider than tall on a phone — a square photo above a stack of copy pushes
     the title too far down the screen. */
  .news-card__media { flex: none; width: 100%; aspect-ratio: 3 / 2; }
  .news-card__body { flex: none; width: 100%; gap: 15px; }
  .news-card__excerpt { font-size: 16px; }
  .news-pager .content-wrapper { padding-top: 20px; padding-bottom: 50px; }
  .news-pager__nav { gap: 15px; }
  .news-signup .content-wrapper { padding-bottom: 40px; }
  .news-signup__inner { --grid-row-gap: 35px; padding: 35px 20px; }
  .news-signup__head { gap: 20px; padding-left: 0; }
  .news-signup__heading { font-size: 34px; max-width: none; }
  .news-signup__form { gap: 15px; padding-right: 0; }
  .news-signup__input { height: 56px; padding: 0 24px; font-size: 16px; }
  .news-signup__btn { margin-top: 8px; }

  /* Contact — the intro follows the other page intros; the form column and
     the office details stack, and the two name fields drop out of their row
     because 187px each is not a field. */
  .contact-intro .content-wrapper { padding-top: 30px; padding-bottom: 60px; }
  .contact-intro__content { gap: 20px; }
  .contact-intro__cols { --grid-row-gap: 40px; }
  .contact-intro__heading { font-size: 48px; max-width: none; }
  .contact-intro__aside { font-size: 16px; }
  .contact-form-section .content-wrapper { padding-top: 40px; padding-bottom: 70px; }
  .contact-form-section__cols { --grid-row-gap: 50px; }
  /* Details lead on a phone — the address and email are what someone reaching
     for a contact page on a phone most often wants. */
  .contact-details { order: -1; }
  .form-field__row { flex-direction: column; gap: 10px; }
  .contact-form { gap: 20px; }
  .form-field__label { font-size: 18px; }
  .form-input { height: 56px; padding: 0 22px; font-size: 16px; }
  .form-input--area { height: 200px; padding: 18px 22px; }
  .contact-details { gap: 40px; }
  .contact-details__text, .contact-details__email { font-size: 18px; }
  /* Held back so it reads as a watermark behind the details rather than
     crossing the copy — the desktop version has empty cream to run through. */
  .contact-form__graphic { width: 40%; height: auto; top: auto; bottom: -4%; opacity: 0.35; z-index: 0; }
  .join-faqs__cols { --grid-row-gap: 30px; }
  .join-faqs__head { gap: 15px; }
  .faq-item__q { padding: 18px 0; }
  .faq-item__a { padding-right: 0; }

  /* Footer */
  .section-footer .content-wrapper { padding-top: 20px; padding-bottom: 20px; }
  .site-footer__content { --grid-row-gap: 30px; }
  .site-footer__col { justify-content: flex-start; gap: 30px; }
  .site-footer__tagline { max-width: none; }
  .newsletter { flex-direction: column; align-items: stretch; }
  .newsletter__input { flex: none; width: 100%; } /* keep the 56px height in column layout */
  .newsletter .btn { align-self: flex-start; }

  /* ---- About page (no mobile design — translated from the desktop frame
     using the homepage's conventions) ---- */
  .about-intro .content-wrapper { padding-top: 30px; padding-bottom: 40px; }
  /* Flex, not a grid, so this is `gap` — `--grid-row-gap` would be inert
     here. Same 40px as the three `__cols` grids above. */
  .about-intro__content { gap: 40px; }
  .about-intro__head { gap: 20px; }
  /* The columns stack, so the quote reads as the section's centrepiece with
     the mission line above it — same order as the desktop reading path. */
  .about-intro__cols { flex-direction: column; gap: 60px; }
  .about-intro__col--mission,
  .about-intro__quote { flex: none; width: 100%; }
  .about-intro__col--mission p { font-size: 16px; max-width: none; }
  .about-intro__quote { gap: 25px; }

  /* Vision: the bands shorten, the columns stack, and the graphic is given a
     bigger share of the width or it reads as a stray mark at this size. */
  /* Still sticky here, so it needs enough height for the section sliding over
     it to read as a reveal rather than a jump. */
  .about-vision-image { --band-h: clamp(280px, 50vh, 420px); }
  .about-vision__graphic { width: 75%; }
  .about-vision .content-wrapper { padding-top: 30px; padding-bottom: 180px; }
  .about-vision__cols { flex-direction: column; gap: 25px; }
  .about-vision__heading, .about-vision__col { flex: none; width: 100%; }
  .about-vision__col { font-size: 18px; }
  .about-vision__col p { max-width: none; }

  /* Team: two up, the label above the grid, and the plus always showing —
     there is no hover on a phone to reveal it with. */
  .about-team .content-wrapper { padding-bottom: 60px; }
  .about-team__grid {
    grid-template-columns: 1fr;
    gap: 25px;
    margin-top: 30px;
  }
  .about-team__lead { grid-column: 1; font-size: 16px; max-width: none; margin-bottom: 20px; }
  .about-team__label { grid-column: 1; grid-row: auto; }
  .team-list { grid-column: 1; grid-row: auto; grid-template-columns: repeat(2, 1fr); }
  .team-list__item:nth-child(2n + 1) { --reveal-delay: 0s; }
  .team-list__item:nth-child(2n) { --reveal-delay: 0.08s; }
  .team-card__open { opacity: 1; width: 28px; height: 28px; }
  .team-card__text { font-size: 14px; }

  /* Modal: the design drops the photograph and leads with the name */
  .team-modal { width: calc(100vw - 30px); max-height: calc(100dvh - 30px); padding: 30px; }
  .team-modal__cols { flex-direction: column; gap: 30px; }
  .team-modal__person { flex: none; width: 100%; padding-right: 60px; }
  .team-modal__photo { display: none; }
  .team-modal__body { padding-right: 0; }
  .team-modal__close { top: 30px; right: 30px; }

  /* Support: the image block moves above the copy and bleeds off the left,
     the way the homepage testimonial's angled block does on mobile. */
  .about-support .content-wrapper { padding-top: 50px; padding-bottom: 40px; }
  .about-support__grid { grid-template-columns: 1fr; }
  .about-support__col { grid-column: 1; gap: 25px; }
  .about-support__head { gap: 20px; }
  .about-support__text { font-size: 16px; max-width: none; }
  /* The photo reads as a full-stop under the copy here rather than a lead-in,
     so it comes out of the absolute placement and sits after the text. It
     still bleeds off the left edge. */
  .about-support { display: flex; flex-direction: column; }
  .about-support .content-wrapper { order: 1; }
  .support-image {
    order: 2;
    position: relative;
    top: auto;
    width: 78%;
    margin-bottom: 40px;
  }

  /* Testimonial: the round block drops below the button and bleeds off the
     right, keeping the left gutter — same treatment as the homepage. */
  .testimonial-block .content-wrapper { padding-top: 30px; padding-bottom: 30px; }
  .testimonial-block__content { gap: 40px; }
  .testimonial-block__cols { flex-direction: column; gap: 25px; align-items: flex-start; }
  .testimonial-block__quote, .testimonial-block__logo { flex: none; width: 100%; }
  .testimonial-block__logo { justify-content: flex-start; padding: 0; }
  .testimonial-block__cite { font-size: 16px; }
  .round-image { position: relative; right: auto; width: 100%; margin-left: 15px; }
  /* Both testimonial shapes go full width on a phone and bleed off the right,
     so the desktop cap comes off. One reset for both — .angle-image's own
     `max-width: none` was doing this job before the cap became shared. */
  .testimonial-image { max-width: none; }

  /* Alpine: rows stack, stats go 2×2 like the homepage */
  .about-alpine .content-wrapper { padding-top: 40px; padding-bottom: 40px; }
  .about-alpine__content { gap: 40px; }
  .about-alpine__cols, .about-alpine__foot { --grid-row-gap: 25px; align-items: start; }
  .about-alpine__foot { --grid-row-gap: 45px; }
  /* Full width stacked — the 15em cap is there to shape two lines beside the
     heading, which only happens on desktop */
  .about-alpine__label { max-width: none; }
  .about-alpine__col { gap: 30px; }
  .about-alpine__text { font-size: 16px; max-width: none; }
  /* Will's call: the desktop clamp bottoms out at 36px, which ran this
     two-sentence heading to six lines on a phone. */
  .about-alpine__heading { font-size: clamp(30px, 5vw, 72px); }
  /* Logo below the stats on mobile only. The row is still a grid once it
     collapses to one column, so `order` is all it takes — the stats have no
     order of their own and default to 0. */
  .about-alpine__logo { max-width: 140px; order: 2; }
  .about-alpine-image { --band-h: 220px; }

  /* Values: same full-bleed carousel as the homepage cards */
  .about-values .content-wrapper { padding-top: 40px; padding-bottom: 60px; }
  .about-values__content { gap: 40px; }
  .about-values__head { gap: 10px; }
  .about-values__sub { font-size: 16px; }
  .card-list--center .card { gap: 15px; }

  /* Legal strip */
  .section-legal .content-wrapper { padding-top: 20px; padding-bottom: 20px; }
  .section-legal__cols { flex-direction: column; gap: 20px; }
  .section-legal__col { flex: none; width: auto; }
}

/* Body scroll lock while menu is open */
body.menu-open { overflow: hidden; }

/* ---- Cookie notice ------------------------------------------------------ */
/* Figma 1621:49668 — a 462x149 cream card, 8px radius, 15px padding, sitting
   over the page bottom-left. Figma draws it as a loose frame with no page
   behind it, so the CORNER IS A CHOICE, not a measurement: bottom-left keeps
   it off the "Get in touch" pill at top-right and out of the way of the CTA
   sections, which all run their content right. Move it by swapping `left`
   for `right` here — nothing else depends on the side. */
.cookie-notice {
  position: fixed;
  z-index: 90;                    /* under the header (100), over the page */
  left: var(--pad-x);
  bottom: var(--pad-x);
  width: min(462px, calc(100vw - var(--pad-x) * 2));
  padding: 15px;
  border-radius: 8px;
  background: var(--c-cream);
  box-shadow: 0 4px 24px rgba(0, 0, 0, 0.25);
}
.cookie-notice[hidden] { display: none; }
/* The mobile menu is z-index 50 and the header drops to 60 over it, so at 90
   the notice would float on top of an open menu. Take it out while the menu
   is open; it comes back on close. */
body.menu-open .cookie-notice { display: none; }
/* Arrives after the page has settled rather than competing with the loader */
.cookie-notice { animation: cookie-in 0.45s 0.15s ease both; }
@keyframes cookie-in {
  from { opacity: 0; transform: translateY(12px); }
  to   { opacity: 1; transform: translateY(0); }
}
.cookie-notice__inner { display: flex; flex-direction: column; gap: 15px; }
.cookie-notice__text {
  margin: 0;
  font-size: 16px;
  line-height: 1.3;
  color: var(--c-green-80);
}
.cookie-notice__buttons { display: flex; gap: 10px; }

/* The notice's buttons are the design's SMALL size: 41px rather than the
   site's 56px. `--btn-h` drives the height AND the swipe geometry, so
   overriding the one variable keeps the 35.5deg angle correct at this size.
   Padding is Figma's 10/15/10/20; the hover nudge scales with it (+6px). */
.cookie-notice__btn {
  --btn-h: 41px;
  padding: 10px 15px 10px 20px;
  font-size: 16px;
}
.cookie-notice__btn:hover,
.cookie-notice__btn:focus-visible { padding-right: 21px; }

/* Cream primary — Figma's `button/cream/primary` variable: #ededE6 resting,
   #c9c9c4 hover. NOT `.btn--cream`, which is the lighter --c-cream with an
   orange swipe; on this cream card that button would vanish into the panel.
   A flat darken is what the design does, so there is no ::before swipe. */
.btn--cream-solid {
  background: #ededE6;
  color: var(--c-dark-green);
  transition: background 0.3s ease, padding-right 0.3s ease;
}
.btn--cream-solid:hover,
.btn--cream-solid:focus-visible { background: #c9c9c4; color: var(--c-dark-green); }

/* Footer link that reopens the notice. Sits in the legal strip with the
   policy links and inherits their type; it is a <a> for consistency there,
   so it needs the pointer a real link would have. */
.cookie-prefs-link { cursor: pointer; }
.cookie-prefs-link[hidden] { display: none; }

@media (prefers-reduced-motion: reduce) {
  .cookie-notice { animation: none; }
  .btn--cream-solid { transition: none; }
}

@media (max-width: 859px) {
  /* Full width at the gutter, and clear of the bottom of a phone screen */
  .cookie-notice { left: var(--pad-x); right: var(--pad-x); width: auto; }
}

/* ---- Email links -------------------------------------------------------- */
/* Sources author plain `mailto:` links; build-page.py strips the address out
   of the built HTML and js/main.js puts the href back on load. See the
   obfuscate_emails() note in build-page.py for why, and for what it does and
   doesn't buy.

   `.e-decoy` is the half that works with NO JavaScript: junk characters
   inserted into the visible address that browsers and screen readers both
   skip, so it reads and sounds correct while a harvester scraping text gets a
   broken address. `display: none` (not clip or 0-width) is deliberate — it is
   the one hiding method assistive technology also ignores, so nobody hears
   "REMOVE" read out mid-address. */
.e-decoy { display: none; }

/* Between parse and script the anchor has no href, so it is not a link yet
   and the UA would give it default text styling and no pointer. Both are
   restored here so nothing shifts when the script runs. */
a.email-link { color: inherit; cursor: pointer; }

/* ---- Philosophy page ---------------------------------------------------- */
/* Figma 1570:51637. Almost everything here is an existing block reused: the
   hero is `.image-band`, the why box is the Founders page's `.process-box`
   shell without the scroll-pinning, the accordion is the Founders FAQ
   untouched, and the testimonial is the homepage's. Only the wrappers below
   are new. */

/* Hero. Same band as every other inner page. `--band-pos` is derived from
   Will's crop (Figma 1601:31396) with the formula in the --band-pos note
   above: the band is 1440 x 423.096, the source 2500 x 1667 renders 960.1px
   tall at that width, and Figma draws the image 154px above the band's top:
       (-0.12 x 423.096 + 154) / (960.1 - 1.24 x 423.096) = 23.7%
   The article's lead image was cropped from the same frame, so the two pages
   show the building identically. */
.decentralised-hero { --band-pos: 23.7%; }

/* ---- Intro ---- */
.decentralised-intro .content-wrapper { padding-top: 40px; padding-bottom: 180px; }
/* Same three-part shape as Partners and Contact: a flex column holding the
   label and the grid, so the 20px under the label is the column's gap and is
   independent of the grid's row-gap. */
.decentralised-intro__content { display: flex; flex-direction: column; gap: 20px; }
.decentralised-intro__cols { align-items: start; }
.decentralised-intro__heading {
  font-family: var(--font-serif);
  font-size: clamp(40px, 5vw, 72px);
  line-height: 1;
  letter-spacing: -0.02em;
  color: var(--c-dark-green);
  text-wrap: balance;
}
.decentralised-intro__heading em { font-style: italic; font-weight: 400; }
.decentralised-intro__aside {
  display: flex;
  flex-direction: column;
  gap: 15px;
  font-size: 18px;
  line-height: 1.3;
  color: var(--c-green-80);
}

/* ---- Why box ----
   The `.process-box` gradient, radius and padding, but a fixed proportion
   rather than `min-height: 80vh` — this box holds a known amount of copy and
   is not pinned, so it should be as tall as its content. */
.decentralised-why .content-wrapper { padding-bottom: 40px; }
.why-box {
  --box-pad: clamp(30px, 4.9vw, 70px);
  position: relative;
  overflow: hidden;
  border-radius: 8px;
  background: linear-gradient(232.4deg, var(--c-teal) 1.6%, var(--c-dark-green) 72.9%);
}
/* Will's suggestion — the support graphic, clipped by the box so only its
   right-hand end shows, which is how the frame draws it. Sized off the box
   width so it holds its proportion; `left` is negative for the same reason
   the frame's own copy sits off the left edge. */
.why-box__graphic {
  position: absolute;
  left: -17%;
  bottom: 13%;
  width: 45%;
  height: auto;
  pointer-events: none;
  /* `data-reveal="from-left"` only WRITES --slide-x; the element has to
     compose it into its own transform, or it fades without moving. See the
     note on the reveal system, and .angle-image__stripe for the same pattern. */
  transform: translate(var(--slide-x, 0px), var(--slide-y, 0px));
}
.why-box__inner {
  position: relative;
  padding: clamp(50px, 7.6vw, 110px) var(--box-pad);
}
/* Two equal columns with a 20px gap — the frame's 1240 inner splits 610/610 */
.why-box__cols { display: flex; gap: 20px; }
.why-box__cols > * { flex: 1 1 0; min-width: 0; }
/* NO max-width here. It caps the flex COLUMN, not just the line length, so
   the two columns stop being equal — the body column grew to 718 against the
   frame's 610 and started 108px too far left. At 610 the heading wraps to two
   lines on its own, which is what the frame draws. */
.why-box__heading {
  font-family: var(--font-serif);
  font-size: clamp(30px, 3.9vw, 56px);
  line-height: 1.05;
  color: var(--c-cream);
}
.why-box__text { display: flex; flex-direction: column; gap: 1em; }
.why-box__text p {
  font-size: clamp(17px, 1.5vw, 21px);
  line-height: 1.35;
  color: var(--c-green-40);
}
.why-box__text strong { font-weight: 600; }

/* ---- What this means ----
   The FAQ accordion verbatim (`.faq-list` / `.faq-item`); only the section
   wrapper and its graphic are new. Columns are 6 : 6 here, not the FAQ's
   5 : 6 — the frame draws 680 + 680 in a 1380 wrapper, which is exactly six
   columns each on the site grid. */
.decentralised-means { position: relative; overflow: hidden; background: #fff; }
.decentralised-means .content-wrapper {
  position: relative;
  z-index: 1;
  padding-top: 40px;
  padding-bottom: 120px;
}
/* Will's arc, bottom-left and bleeding off the page edge. His export is
   already the visible crop — the same as graphic-support.svg — so there is no
   offset to compute, only a position. */
/* Sized off the section's HEIGHT, not the page width — the same lesson as the
   Founders FAQ fan above. Driven off width it grew until it crowded the
   heading; the frame leaves ~100px of clear air between the two. */
.decentralised-means__graphic {
  position: absolute;
  left: 0;
  bottom: 0;
  height: 56%;
  width: auto;
  z-index: 0;
  pointer-events: none;
}
.decentralised-means__cols { align-items: start; row-gap: 40px; }
.decentralised-means__heading {
  font-family: var(--font-serif);
  font-size: clamp(36px, 3.9vw, 56px);
  line-height: 1.05;
  color: var(--c-dark-green);
  max-width: 6.7em;          /* 376px at 56px — the frame's own break */
}

@media (max-width: 859px) {
  .decentralised-intro .content-wrapper { padding-top: 30px; padding-bottom: 40px; }
  .decentralised-intro__content { gap: 20px; }
  .decentralised-intro__cols { --grid-row-gap: 40px; }
  .decentralised-intro__heading { font-size: clamp(36px, 9vw, 48px); }
  .decentralised-intro__aside { font-size: 16px; max-width: none; }
  /* One column: heading, then copy */
  .decentralised-intro__cols > * { grid-column: 1 / -1; }

  .decentralised-why .content-wrapper { padding-bottom: 20px; }
  /* The two columns stack — the heading is a heading, not a sidebar */
  .why-box__cols { flex-direction: column; gap: 30px; }
  /* Stacked, the copy fills the box, so at its desktop position the graphic
     sat BEHIND the last two paragraphs — orange under cream, unreadable. It
     now hangs off the bottom edge (the box clips it) rather than sitting
     under live text. */
  .why-box__inner { padding-bottom: 120px; }
  .why-box__graphic { left: -10%; bottom: -56px; width: 72%; }

  .decentralised-means__cols > * { grid-column: 1 / -1; }
  .decentralised-means__heading { max-width: none; }
  /* Stacked, the accordion fills the width, so a height-driven arc sat
     straight across the last two rows — orange lines through the type. It
     hangs off the bottom edge instead; the section clips it. */
  .decentralised-means .content-wrapper { padding-bottom: 210px; }
  .decentralised-means__graphic { height: auto; width: 70%; bottom: -140px; }
}
