/* ============================================================
   Bold Bloom Marketing
   Implementation of "Bold Bloom Marketing Blend II.dc.html"
   ============================================================ */

/* --- 0. Fonts -------------------------------------------------
   Self-hosted, latin subset only, declared here rather than fetched from
   fonts.googleapis.com. That removes two third-party connections and a
   render-blocking stylesheet from the critical path — the browser used to have
   to resolve DNS, negotiate TLS and parse a stylesheet on another origin before
   it knew which font files it needed.

   Archivo is the variable file covering 400-600 in one 34KB download; three
   static weights were 102KB. Regenerate with the recipe in the README.

   `swap` over `block`: the text is readable immediately in the fallback and
   reflows once, rather than being invisible for up to three seconds. The
   fallback stacks in --font-* are chosen to be metrically close, so that reflow
   stays small. */

@font-face {
  font-family: 'Archivo';
  font-style: normal;
  font-weight: 400 600;
  font-display: swap;
  src: url('/fonts/archivo-variable.woff2') format('woff2');
}
@font-face {
  font-family: 'Instrument Serif';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('/fonts/instrument-serif.woff2') format('woff2');
}
@font-face {
  font-family: 'Instrument Serif';
  font-style: italic;
  font-weight: 400;
  font-display: swap;
  src: url('/fonts/instrument-serif-italic.woff2') format('woff2');
}
@font-face {
  font-family: 'IBM Plex Mono';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('/fonts/ibm-plex-mono-400.woff2') format('woff2');
}
@font-face {
  font-family: 'IBM Plex Mono';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url('/fonts/ibm-plex-mono-500.woff2') format('woff2');
}

/* --- 1. Tokens ------------------------------------------------ */

/* Sandstone. The page, and every surface that is not a card. --white is the
   card surface — off-white rather than white, so a card reads as lifted off
   the paper rather than punched through it. */
:root {
  --bg:        #F7F2EA;
  --bg-alt:    #EDE4D7;
  --white:     #FFFDF8;

  --ink:       #1C1613;
  --ink-2:     #4F453E;
  /* Both of these were darkened to clear 4.5:1 for small text — WCAG 1.4.3.
     --ink-3 is the tightest pair in the system: the brand sheet specifies
     #75675C, which is 4.33:1 against --bg-alt (the darker sand) where it
     carries 10px case tags and contact labels. Three steps darker clears it.
     --ink-4 follows it down to hold its own 4.5:1 on --bg under the step and
     process numbers. The gap between the two tiers narrows but the hierarchy
     holds; the ratios are checked by tools/theme.mjs and tools/a11y.mjs, so
     neither can drift back. */
  --ink-3:     #716355;
  --ink-4:     #796B5F;

  --line:      #E1D6C7;
  --line-2:    #CFC2AF;
  --line-3:    #DCD1C1;

  /* Rose clay, held dark enough to clear 4.5:1 on both page surfaces — it
     carries heading italics and 10px link hovers, including over the --bg-alt
     sections, so it has to read as text and not only as decoration. It is
     never a fill and never a background: --accent-tint and --accent-badge are
     separate tokens precisely so the accent itself stays type. */
  --accent:        #9C4A47;
  /* Failure. Kept redder and darker than the accent so an error does not read
     as emphasis — the rose clay cannot carry it, since it is the colour every
     heading already uses. */
  --warn:          #9E2A1C;
  --accent-tint:   #F2DCD9;
  --accent-badge:  #EBC9C5;

  --dark:      #1C1613;
  --dark-green:#34382E;
  --dark-warm: #3B3128;

  /* Copy set over a photograph. A photograph does not invert when the page
     does, so its scrim stays dark and its copy stays paper in both modes —
     these are deliberately absolute rather than aliases of --bg and --line,
     which is what they used to be and what broke when the page went dark. */
  --on-photo:   #F7F2EA;
  --on-photo-2: #E1D6C7;

  /* Copy and hairlines on the surfaces painted with --ink — the dark plan card
     and the case-study metric. Those surfaces flip with the page, so unlike
     the two above, these have to flip with it. */
  --on-ink-muted: #C9B7A3;
  --on-ink-rule:  rgba(247, 242, 234, 0.18);

  /* The sticky header is the page ground at 93%, not a colour of its own. */
  --header-bg: rgba(247, 242, 234, 0.93);

  --font-sans:  Archivo, Helvetica, Arial, sans-serif;
  --font-serif: 'Instrument Serif', Georgia, serif;
  --font-mono:  'IBM Plex Mono', ui-monospace, SFMono-Regular, Menlo, monospace;

  --gut:   clamp(20px, 4vw, 48px);  /* page gutter          */
  --wrap:  1320px;                  /* content max width    */
  --hdr:   78px;                    /* sticky header height */

  color-scheme: light;
}

/* --- 1b. Dark mode --------------------------------------------
   `data-mode` is set on <html> by the inline script in each <head>, from a
   stored choice or the OS preference, before the first paint. It is a separate
   attribute from the `data-theme` that themes.css uses, because the two are
   different axes: `data-theme` picks a hue, this picks a ground.

   The palette is one ink/paper pair and almost everything is drawn from it, so
   dark mode is mostly that pair swapped — every surface painted with --ink and
   every text set in --bg inverts together and keeps its contrast by
   construction. The dark plan card becomes a light card on a dark page, which
   is still the most emphatic card on the page.

   What cannot swap is anything sitting on a photograph, since the photograph
   does not invert. Those use --on-photo and the --dark-* frames, and neither
   is redeclared here. Gated by tools/theme.mjs like every other palette. */
[data-mode="dark"] {
  color-scheme: dark;

  --bg:        #17120F;
  --bg-alt:    #201A15;
  --white:     #241D18;

  --ink:       #F7F2EA;
  --ink-2:     #D6C9B9;
  /* Same relationship as in light mode, mirrored: --ink-4 sits a step nearer
     the ground than --ink-3, so it stays the fainter of the two tiers. */
  --ink-3:     #A2917F;
  --ink-4:     #9C8B79;

  --line:      #362C23;
  --line-2:    #46392E;
  --line-3:    #2E261F;

  /* The brand sheet's own reversed accent — the rose clay lifted to read as
     type on ink, which is exactly what it has to do here. */
  --accent:        #EE9691;
  --warn:          #FF8A72;
  --accent-tint:   #4A2B29;
  --accent-badge:  #5C332F;

  --dark:      #F7F2EA;

  --on-ink-muted: #6B5E52;
  --on-ink-rule:  rgba(28, 22, 19, 0.18);
  --header-bg:    rgba(23, 18, 15, 0.93);
}

/* The mark ships as one flat near-black file, so on a dark ground it has to be
   turned over. It is a single colour on transparency, which is the one case
   where `invert` is exact rather than approximate.

   Scoped to the two places the Bold Bloom mark appears, and deliberately not to
   `.shot--logo` at large: the client logos in the wall on home carry that class
   too, and inverting those would swap each brand's colours for their
   complements — the moodswingz red came out cyan when it was tried. They are
   transparent now rather than white-boxed, so the boxes that used to turn black
   are gone, but the colour objection stands and section 6e still handles them
   one mark at a time. */
[data-mode="dark"] .brand__mark .shot--logo,
[data-mode="dark"] .site-footer__mark .shot--logo,
[data-mode="dark"] .about-head::before { filter: invert(1) brightness(1.06); }

/* Placeholder hatching is a tint of the page, so it follows the page down. */
[data-mode="dark"] .hatch {
  background: repeating-linear-gradient(
    135deg,
    var(--bg-alt) 0 var(--hs),
    var(--bg) var(--hs) calc(var(--hs) * 2)
  );
}

/* --- 2. Reset / base ------------------------------------------ */

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

html { scroll-behavior: smooth; }

/* Anchor targets land clear of the sticky header instead of under it. The
   router used to do this arithmetic in JS on every route change; now that
   #anchors are the browser's job again, the same 24px of air below the
   header is stated here. `--hdr` is remeasured on load and on resize. */
[id] { scroll-margin-top: calc(var(--hdr) + 24px); }

body {
  margin: 0;
  background: var(--bg);
  color: var(--ink);
  font-family: var(--font-sans);
  -webkit-font-smoothing: antialiased;
}

a { color: var(--ink); text-decoration: none; }
a:hover { color: var(--accent); }

:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }

/* <main> carries tabindex="-1" so the skip link can actually move focus into it
   — without that, activating the link scrolls the page but leaves focus at the
   top, and the next Tab goes straight back into the header. The ring is
   suppressed only here: main is a landing target rather than a control, and a
   2px outline around the whole page reads as an error. The next Tab lands on a
   real control and shows its ring normally. */
#main:focus { outline: none; }
::selection { background: var(--accent-tint); }

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

button { font: inherit; }

[hidden] { display: none !important; }

.sr-only {
  position: absolute;
  width: 1px; height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

.skip-link {
  position: absolute;
  left: -9999px; top: 0;
  z-index: 200;
  background: var(--ink);
  color: var(--bg);
  padding: 12px 18px;
}
.skip-link:focus { left: 12px; top: 12px; color: var(--bg); }

/* --- 3. Utilities --------------------------------------------- */

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

/* Mono eyebrow / label text, the design's second voice. */
.mono {
  font-family: var(--font-mono);
  font-size: 10px;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--ink-3);
}
.mono--wide { letter-spacing: 0.22em; }
.mono--tight { letter-spacing: 0.16em; }
.mono--note {
  font-size: 11px;
  line-height: 1.9;
  letter-spacing: 0.16em;
  color: var(--ink-2);
}

/* Italic serif accent inside headings. Coloured wherever it appears in a
   heading — the two italics outside one, the counter's `+` and the pricing
   lead-in, stay in ink. */
/* 1.06em, not 1em. The serif carries a smaller x-height than Archivo at the
   same size, so matching the font-size makes the clause read as a step down
   mid-sentence. The nudge makes it read level rather than larger. */
h1 em,
h2 em,
h3 em { color: var(--accent); font-size: 1.06em; }

em {
  font-family: var(--font-serif);
  font-style: italic;
  font-weight: 400;
  letter-spacing: -0.01em;
}

.lede {
  margin: 0;
  max-width: 44ch;
  font-size: 16px;
  line-height: 1.75;
  color: var(--ink-2);
}

/* Arrow-prefixed intro paragraph. */
.arrow-lede {
  margin: clamp(22px, 3vw, 34px) 0 0;
  max-width: 52ch;
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 14px;
  font-size: clamp(17px, 1.9vw, 22px);
  line-height: 1.6;
}
.arrow-lede > span:first-child { color: var(--ink-4); }

/* Diagonal hatch used for every image placeholder. */
.hatch {
  --h1: #EBE3D5;
  --h2: #F7F2EA;
  --hs: 9px;
  background: repeating-linear-gradient(
    135deg,
    var(--h1) 0 var(--hs),
    var(--h2) var(--hs) calc(var(--hs) * 2)
  );
}

/* --- Supplied images ------------------------------------------
   Real assets dropped into the placeholder frames. The frame is in charge:
   it keeps its size, aspect ratio, radius, clipping, spacing and reveal, and
   the image conforms to it. Absolutely positioned rather than in flow so it
   fills the border box exactly — padding and all — and inherits the frame's
   corners. Later component rules still win on `position`, which is what lets
   the carousel cards stay absolute. */
.shot {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: var(--focus, 50% 50%);
  border-radius: inherit;
}
/* The photographs are served through <picture>, which offers AVIF first, then
   WebP, then the original format. That puts an element between the frame and
   the image, and two things depend on it not being there:
     - `.shot` is absolutely positioned against the frame, so the wrapper must
       generate no box — hence `display: contents`.
     - `.shot`'s `border-radius: inherit` reads its DOM parent, which is now the
       <picture> rather than the frame, so the radius is passed down explicitly.
   Without the second line every rounded frame gets square corners on its
   image. `tools/shots.mjs` is the check that catches it. */
picture {
  display: contents;
  border-radius: inherit;
}

/* Logos are shown whole, never cropped, with room to breathe inside the
   frame they were given. */
.shot--logo {
  object-fit: contain;
  padding: var(--logo-pad, 12%);
}
.brand__mark .shot, .site-footer__mark .shot { padding: 0; }
/* The hatch and the caption were the placeholder; with a real image in the
   frame they have nothing left to say. Everything else about the wrapper is
   untouched. */
.has-shot {
  position: relative;
  background-image: none;
}
/* Only the caption goes — a blanket `span` rule here also caught the culture
   banner's scrim, which is a span and is the thing keeping its text legible. */
.has-shot > .ph { display: none; }

/* Pill caption sitting on a placeholder. */
.ph {
  font-family: var(--font-mono);
  font-size: 10px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--ink-3);
  border: 1px solid var(--line-3);
  border-radius: 100px;
  padding: 8px 14px;
  background: var(--bg);
  text-align: center;
}

.slot { display: grid; place-items: center; }

/* Pill buttons. */
.btn {
  display: inline-flex;
  align-items: center;
  gap: 12px;
  min-height: 52px;
  padding: 0 26px;
  border: 0;
  border-radius: 100px;
  background: var(--ink);
  color: var(--bg);
  font-family: var(--font-sans);
  font-size: 15px;
  font-weight: 500;
  cursor: pointer;
  transition: background 0.2s ease, color 0.2s ease;
}
.btn:hover { background: var(--accent); color: var(--bg); }

.btn--lg {
  gap: 16px;
  min-height: 60px;
  padding: 0 34px;
  font-size: 17px;
  letter-spacing: -0.01em;
}

.btn--light { background: var(--bg); color: var(--ink); }
.btn--light:hover { background: var(--accent); color: var(--bg); }

.btn--block { display: flex; justify-content: space-between; gap: 16px; padding: 0 22px; }

/* Mono link with a trailing rule. */
.rule-link {
  display: inline-flex;
  align-items: center;
  gap: 14px;
  min-height: 44px;
  font-family: var(--font-mono);
  font-size: 10px;
  letter-spacing: 0.2em;
  text-transform: uppercase;
}
.rule-link i {
  width: 52px;
  height: 1px;
  background: currentColor;
  transition: width 0.25s ease;
}
.rule-link:hover i { width: 72px; }

/* Underlined inline link. */
.ul-link { border-bottom: 1px solid var(--line-2); }
.ul-link:hover { border-bottom-color: var(--ink); }

/* --- 4. Header ------------------------------------------------ */

.site-header {
  position: sticky;
  top: 0;
  z-index: 100;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 20px;
  flex-wrap: wrap;
  padding: 14px var(--gut);
  background: var(--header-bg);
  /* Prefixed as well, the same way the sweep mask below is. Safari only took
     `backdrop-filter` unprefixed in 18, so every iOS 17 and older device was
     getting the flat 93%-opaque header while the other engines got the frosted
     one. Readable either way — which is why it went unnoticed — but it is the
     one piece of chrome on every page. */
  -webkit-backdrop-filter: blur(10px);
          backdrop-filter: blur(10px);
}

.brand { display: flex; align-items: center; gap: 11px; min-height: 44px; }
/* No frame: the mark reads as part of the wordmark, not as a slot holding a
   picture of one. Sized a little above the 18px/1.1 title beside it. */
.brand__mark {
  width: 26px; height: 26px;
  flex: none;
}
.brand__text { display: flex; flex-direction: column; line-height: 1.1; }
.brand__name { font-size: 18px; font-weight: 600; letter-spacing: -0.02em; }
.brand__sub {
  font-family: var(--font-mono);
  font-size: 9px;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--ink-3);
}

.site-nav {
  display: flex;
  align-items: center;
  gap: clamp(13px, 2vw, 30px);
  flex-wrap: wrap;
  font-family: var(--font-mono);
  font-size: 10px;
  letter-spacing: 0.2em;
  text-transform: uppercase;
}
.site-nav a { display: inline-flex; align-items: center; min-height: 44px; }

/* Active section marker — the design has no state for this, but a real
   nav needs to say where you are. */
.site-nav a[aria-current='page'] { box-shadow: inset 0 -1px 0 0 currentColor; }
.site-nav .btn[aria-current='page'] { box-shadow: none; background: var(--accent); }

.site-nav .btn { min-height: 46px; padding: 0 22px; gap: 10px; font-size: inherit; letter-spacing: inherit; }

/* Dark mode toggle. A hairline circle rather than a pill, so it reads as a
   control and not as a second call to action beside the one pill in the bar.
   36px of visible circle inside a 44px touch target — the target is the
   element, the circle is drawn on it, so the hit area never falls below the
   2.5.8 minimum on a phone. */
.mode-toggle {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  padding: 0;
  border: 0;
  background: none;
  color: var(--ink);
  cursor: pointer;
  border-radius: 100px;
}
.mode-toggle::before {
  content: '';
  position: absolute;
  width: 36px;
  height: 36px;
  border: 1px solid var(--line-2);
  border-radius: 100px;
  transition: border-color .2s, background-color .2s;
}
.mode-toggle { position: relative; }
.mode-toggle svg { position: relative; width: 16px; height: 16px; display: block; }
.mode-toggle:hover { color: var(--accent); }
.mode-toggle:hover::before { border-color: var(--accent); }

/* Only one of the two icons is in the document at a time — the sun is what
   the button will do, not what the page currently is, which is the way round
   that stops it reading as a status light. */
.mode-toggle__icon--sun  { display: none; }
[data-mode="dark"] .mode-toggle__icon--sun  { display: block; }
[data-mode="dark"] .mode-toggle__icon--moon { display: none; }

/* --- 5. Views (client-side routing) --------------------------- */

.view { display: block; }

/* --- 6. Home — statement -------------------------------------- */

/* Settled state: the heading sits near the top of the page with the
   carousel pulled up under it, the cards tucking just behind the last
   line of the headline. The opening sequence (section 14) animates
   into this from a full-height, vertically centred statement. */
/* The hero always owns the first screen, so nothing that follows it can
   creep into view on a tall display. */
.hero {
  position: relative;
  overflow: clip;
  /* `svh` is the viewport at its *smallest* — the height with a phone's URL bar
     showing. `vh` is the height with it hidden, so a section sized to it is
     taller than what is actually on screen and its bottom edge is cut off until
     the visitor scrolls. The `vh` line stays first for browsers before Safari
     15.4 / Chrome 108, which ignore the second and are no worse off. */
  min-height: calc(100vh - var(--hdr));
  min-height: calc(100svh - var(--hdr));

  /* Cards are 1.2x the design's size, capped so the bottom edge always
     clears the fold. The cap has to subtract the headline as well as read
     the viewport height: the headline is sized in vw, so on a short, wide
     window it eats the space the card needs and a plain vh cap is not
     enough. The 215px is the fixed chrome above the cards (header, padding,
     eyebrow) and 1.73 is how much of the headline's height sits above them.
     Declared here because the overlap below is derived from both values. */
  --h1-size: clamp(34px, 5.8vw, 88px);
  --card-w: min(
    clamp(276px, 36vw, 800px),
    max(180px, 100svh - 210px - 1.91 * var(--h1-size))
  );
}

.statement {
  position: relative;
  z-index: 2;
  display: grid;
  place-items: center;
  padding: clamp(28px, 4.5vw, 64px) var(--gut) 0;
  text-align: center;
}
.statement__eyebrow { margin: 0 0 clamp(28px, 4vw, 48px); letter-spacing: 0.22em; }
.statement h1 {
  margin: 0;
  font-weight: 500;
  font-size: var(--h1-size);
  line-height: 1.02;
  letter-spacing: -0.03em;
  max-width: 22ch;
  text-wrap: pretty;
}
/* Instrument Serif sets optically smaller than Archivo at the same size, so
   1.06em reads as level with the rest of the line rather than larger — enough
   to give the phrase weight without breaking the headline's baseline. The
   colour comes from the shared heading rule in section 3. */
.statement h1 em { font-size: 1.06em; }

/* --- 6b. Home — 3D project wheel ------------------------------ */

.wheel-section {
  position: relative;
  z-index: 1;
  /* Pulls the card tops up behind the last line of the headline. Derived
     rather than hand-fitted: the cards sit (0.099 * card-w + 40px) below the
     top of the wheel box, and the headline's descender space is ~0.2 of its
     font size, so this leaves 19-23px of glyph over card at every size. It
     was a constant 21px before the accent went to 1.06em, which makes the
     last line's box height depend on whether the accent lands on it.
     Re-derive these two coefficients if the ring geometry changes. */
  margin-top: calc(-64px - 0.099 * var(--card-w) - 0.2 * var(--h1-size));
  padding: 0 0 clamp(32px, 4.5vw, 52px);
  overflow: hidden;
}

.wheel {
  /* --card-w is inherited from .hero. A wide ring — 2.65 card-widths of radius
     against the design's 2.048 — so the far side sits properly far back. The
     ring carries 15 cards at 24deg rather than the design's 12 at 30deg, which
     is what holds the flat gap between neighbours at 10% of a card width — it
     renders as ~9% at this card aspect.
     Radius and step are coupled — change one and you must change the other:
     gap = 2 * radius * sin(step / 2) - card-w. The 2.65 is what keeps that
     product at 1.10 card-widths now the step has widened from 360/17deg. */
  /* 3:5 — landscape. This also moves the ring's rotation origin, which is why
     the wheel height, the ring's top and .wheel-section's margin-top were all
     refitted with it. perspective-origin is written against --card-h so the
     camera keeps looking at the middle of the front card at any aspect. */
  --card-h: var(--card-w);
  --radius: calc(var(--card-w) * 2.65);
  /* Ring tilt and camera distance, both expressed against the card size so
     the ring reads identically at any scale. Tilt sets how much of the ring's
     far side shows below the near cards; perspective sets how much smaller
     that far side reads. Raising the tilt much past this detaches the back
     into a second row; lowering it hides the back entirely. */
  --tilt: 6deg;
  --persp: calc(var(--card-w) * 3.6);
  position: relative;
  /* Tall enough to hold the whole fan. Tilting the ring drops the outer
     cards well below the front one, and the design's `card-h + 120px` sliced
     them off in a straight line partway down the composition. Fitted to
     where the lowest front-facing card actually lands, so every card ends on
     its own rounded corner, with ~20px to spare. That clearance is why this
     box does not clip: `overflow: hidden` here sliced the featured card's
     box-shadow into a hard horizontal line across the page. Horizontal
     containment is `.wheel-section`'s, and nothing overflows the bottom — if
     you re-tilt the ring, check `tools/clip2.mjs` still reports a negative
     worst overflow before trusting that. */
  height: calc(var(--card-w) * 1.09 + 61px);
}
.wheel__stage {
  position: absolute;
  inset: 0;
  perspective: var(--persp);
  perspective-origin: 50% calc(var(--card-h) * 0.5 + 40px);
}
.wheel__ring {
  position: absolute;
  left: 0; right: 0;
  /* Fitted, not geometric: rotateX lifts the front card by ~0.61 card-widths,
     so the ring has to start that far down the box for the card tops to land
     0.088 * card-w + 41px inside it. Re-measure with tools/refit.mjs after any
     change to the radius, tilt or card aspect. */
  top: calc(var(--card-w) * 0.365 + 40px);
  height: var(--card-h);
  transform-style: preserve-3d;
  transform: translateZ(calc(var(--radius) * -1)) rotateX(var(--tilt));
}
/* The ring turns clockwise as seen from above, so the near cards travel to
   the left. The spin lives on its own element rather than on .wheel__ring so
   the keyframes carry nothing but the rotation — the ring keeps the fixed
   translateZ/rotateX, and neither has to repeat the other. Same box as the
   ring (inset: 0), so the cards' absolute positions are unchanged.

   It turns from the first frame, so the ring is already in motion as the
   carousel fades up and in. The negative delay is what keeps the featured
   card centred anyway: starting one turn minus 4.6s in means the ring
   completes that turn exactly as the opening sequence ends — 2.7s to beat 2
   plus the 1.9s the carousel takes to arrive — and lands back at 0. Written
   as a calc so the two stay tied if the turn is re-timed.

   One turn per 120s is 8s a card, slow enough to read as drift rather than
   motion. `prefers-reduced-motion` drops it with everything else, in
   section 14. */
.wheel__spin {
  --spin-turn: 120s;
  --intro-ends: 4.6s;
  position: absolute;
  inset: 0;
  transform-style: preserve-3d;
  animation: wheel-spin var(--spin-turn) linear infinite;
  animation-delay: calc(var(--intro-ends) - var(--spin-turn));
}
@keyframes wheel-spin {
  from { transform: rotateY(0deg); }
  to   { transform: rotateY(-360deg); }
}

.wheel__card {
  position: absolute;
  left: 50%; top: 0;
  width: var(--card-w);
  height: var(--card-h);
  margin-left: calc(var(--card-w) / -2);
  transform: rotateY(calc(var(--i) * 1deg)) translateZ(var(--radius));
  /* Backfaces stay visible: the far side of the ring showing through and
     below the near cards is what makes it read as a carousel in the round
     rather than a flat fan. */
  border-radius: calc(var(--card-w) * 0.065);
  opacity: var(--o, 1);
  --h1: var(--t1);
  --h2: var(--t2);
  display: grid;
  place-items: center;
}
.wheel__card--featured { box-shadow: 0 40px 70px -50px rgba(28, 22, 19, 0.45); }
.wheel__card .ph { background: var(--white); white-space: nowrap; padding: 8px 13px; }

.wheel-meta {
  max-width: var(--wrap);
  margin-inline: auto;
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 20px;
  flex-wrap: wrap;
  padding-inline: var(--gut);
}

/* --- 6c. Home — services list --------------------------------- */

.section { padding: clamp(64px, 10vh, 132px) var(--gut); }
.section--alt { background: var(--bg-alt); }

/* Sections that would otherwise share a screen with their neighbour get a
   screen of their own, so the page is read one section per scroll.
   `safe center` degrades to top-aligned rather than clipping when the
   content is taller than the viewport. */
.screen {
  min-height: calc(100vh - var(--hdr));
  min-height: calc(100svh - var(--hdr));
  display: grid;
  /* An explicit full-width track — an implicit `auto` one would size to
     max-content and collapse the auto-fit grids nested inside. */
  grid-template-columns: minmax(0, 1fr);
  align-content: safe center;
}
/* Auto inline margins stop a grid item stretching, so the centred
   wrappers would shrink to fit-content without this. */
.screen > * { width: 100%; }

/* Heading beside a supporting paragraph. */
.head-split {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  gap: clamp(20px, 3vw, 48px);
  align-items: end;
  margin-bottom: clamp(32px, 4.5vw, 56px);
}
.head-split h2 {
  margin: 0;
  font-weight: 500;
  font-size: clamp(28px, 4.2vw, 56px);
  line-height: 1.04;
  letter-spacing: -0.03em;
  max-width: 20ch;
}
.head-split p { margin: 0; max-width: 38ch; }

.svc-list {
  display: grid;
  gap: 1px;
  background: var(--line);
  border-top: 1px solid var(--line);
  border-bottom: 1px solid var(--line);
}
.svc {
  position: relative;
  background: var(--bg);
  padding: clamp(20px, 2.6vw, 30px) 0;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
  gap: clamp(16px, 2.6vw, 40px);
  align-items: baseline;
}
.svc > div { min-width: 0; }
.svc__no { margin: 0 0 12px; }
.svc h3 {
  margin: 0;
  font-weight: 600;
  font-size: clamp(19px, 2vw, 26px);
  letter-spacing: -0.02em;
}
/* Each row opens the matching card on the capabilities page. The link sits
   on the heading so its accessible name is the service itself, and a
   stretched pseudo-element hands the whole row the click. */
.svc h3 a { color: inherit; }
.svc h3 a::after { content: ''; position: absolute; inset: 0; }
.svc:hover h3 a { color: var(--accent); }
.svc p {
  margin: 0;
  min-width: 0;
  font-size: 15px;
  line-height: 1.75;
  color: var(--ink-2);
}

.link-row {
  display: flex;
  flex-wrap: wrap;
  gap: clamp(16px, 2.6vw, 32px);
  margin-top: clamp(28px, 4vw, 44px);
}

/* --- 6d. Home — recent work ----------------------------------- */

.work-preview { padding-bottom: clamp(56px, 8vw, 110px); }
.work-preview .head-split h2 { margin-left: clamp(0px, 3vw, 56px); max-width: 18ch; }

.preview-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
  gap: clamp(24px, 3vw, 56px);
  align-items: start;
}
.preview-card { min-width: 0; }
.preview-card__img {
  --h1: #E4DACA;
  --h2: #EDE4D7;
  display: grid;
  place-items: center;
  transition: transform 0.45s cubic-bezier(0.2, 0.7, 0.2, 1);
}
/* Grows a little under the pointer and settles back after it leaves. A
   transform, so the card's neighbours and the grid around it never move —
   and the supplied image, being a child, comes with it. Hovering the caption
   counts too: it is one card. */
.preview-card:hover .preview-card__img { transform: scale(1.03); }
.preview-card__img .ph { border-color: var(--line-2); padding: 8px 14px; }
.preview-card h3 {
  margin: 26px 0 10px;
  font-family: var(--font-serif);
  font-weight: 400;
  font-size: clamp(24px, 2.6vw, 34px);
  letter-spacing: -0.01em;
}
.preview-card__tag {
  margin: 0 0 16px;
  letter-spacing: 0.14em;
  color: var(--ink-3);
}
.preview-card p:last-child {
  margin: 0;
  max-width: 34ch;
  font-size: 15px;
  line-height: 1.75;
  color: var(--ink-2);
}

.preview-card--a { margin-right: clamp(0px, 3vw, 52px); }
.preview-card--a .preview-card__img { aspect-ratio: 4/5; border-radius: 26px 26px 26px 6px; }
.preview-card--b { margin-top: clamp(0px, 17vw, 260px); margin-left: clamp(0px, 2vw, 36px); }
.preview-card--b .preview-card__img { aspect-ratio: 1/1; border-radius: 6px 26px 26px 26px; }
.preview-card--c { margin-top: clamp(0px, 7vw, 104px); }
.preview-card--c .preview-card__img { aspect-ratio: 3/4; border-radius: 26px 6px 26px 26px; }

.work-preview__foot {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
  gap: clamp(24px, 3vw, 56px);
  align-items: center;
  margin-top: clamp(32px, 4vw, 56px);
}
.work-preview__foot p { margin: 0; max-width: 26ch; }
.work-preview__foot .btn { justify-self: center; }

/* --- 6e. Home — client logos ---------------------------------- */

.clients { padding: clamp(48px, 7vw, 96px) var(--gut) clamp(40px, 5vw, 64px); }
.clients .head-split { gap: clamp(16px, 3vw, 48px); margin-bottom: clamp(28px, 4vw, 48px); }
.clients .head-split h2 { font-size: clamp(24px, 3.2vw, 40px); line-height: 1.06; }
.clients .head-split p { max-width: 30ch; justify-self: end; }

/* Two bands of client marks running the full width of the page, drifting in
   opposite directions — the same edge-to-edge treatment as the about page's
   culture banner at a fraction of its height. That one is
   clamp(420px, 50vw, 680px) tall.

   The section keeps its gutter for the heading; the bands escape it by exactly
   that much on each side, which lands them on the viewport edges. The width
   has to be explicit because `.screen > *` sets `width: 100%` — this rule
   comes later and has the same weight, so it wins. */
.logo-walls {
  width: calc(100% + var(--gut) * 2);
  margin-inline: calc(var(--gut) * -1);
}
.logo-wall {
  overflow: hidden;
  border-top: 1px solid var(--line);
  border-bottom: 1px solid var(--line);
}
/* The two touch, so the first one's lower rule is the divider. */
.logo-wall + .logo-wall { border-top: 0; }

/* Each track holds the set twice over, which is what makes translating it by
   half its own width a seamless loop. The cell is a fixed width rather than a
   fraction of the viewport so the drift keeps one speed everywhere: eight
   cells of 200px in 20s is 80px/s, about what the carousel's front card
   travels at. `prefers-reduced-motion` stops them, in section 14 — the static
   state is the untransformed track, so they simply stand still. */
.logo-wall__track {
  --cell: 200px;
  display: flex;
  width: max-content;
  animation: logo-drift-right 20s linear infinite;
}
.logo-wall--back .logo-wall__track { animation-name: logo-drift-left; }

/* Static variant, used once the real client list is short enough to sit on one
   row. The cells share the width instead of holding a fixed 200px, so the row
   fills the wall exactly and there is nothing to loop. */
.logo-wall__track--static {
  width: 100%;
  animation: none;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
}
.logo-wall__track--static .logo-wall__cell { width: auto; }
/* Named clients who did not supply a mark. Set in the display face at logo
   scale so the cell reads as a brand alongside the real logos, not as the
   9px mono caption that labelled the empty placeholders. Two class selectors
   because the generic `.logo-wall span` rule below is (0,1,1) and would
   otherwise win. */
.logo-wall .logo-wall__name {
  padding: 0 16px;
  text-align: center;
  text-wrap: balance;
  font-family: var(--font-serif);
  font-size: clamp(15px, 1.5vw, 20px);
  line-height: 1.15;
  letter-spacing: -0.01em;
  color: var(--ink-2);
  text-transform: none;
}

@keyframes logo-drift-left  { to   { transform: translateX(-50%); } }
@keyframes logo-drift-right { from { transform: translateX(-50%); } }

.logo-wall__cell {
  flex: none;
  position: relative;
  width: var(--cell);
  min-height: clamp(96px, 9vw, 132px);
  display: grid;
  place-items: center;
  border-right: 1px solid var(--line);
}
@media (max-width: 700px) { .logo-wall__track { --cell: 150px; } }

.logo-wall .shot--logo { padding: 0; }
.logo-wall .has-shot > span:not(.shot) { display: none; }

/* Dark mode.

   This strip used to stay cream while the rest of the page went dark, and the
   reason was the logos rather than the design: every file the clients supplied
   is fully opaque with a solid white box baked in, so the strip leaned on
   `mix-blend-mode: multiply` to drop that box away. Multiply only subtracts, so
   it needs a light ground — over near-black it drove every mark to black.

   The box is now removed in the asset instead, by tools/img.mjs (`knockWhite`),
   which means one transparent file per client that is correct on either ground
   and needs no blend mode in either mode. That is what lets the band simply
   follow the theme: there is no rule here forcing it light any more.

   One mark still gets a treatment once the ground is dark, and it moves
   lightness only — no hue is rotated, so the red this file used to warn about
   coming out cyan stays red. */

/* Skin by Brooke is a single flat black on transparency — 18,873 ink pixels,
   0 of them non-grey — which is the one case where invert is exact rather than
   approximate, the same reason it is used on the Bold Bloom mark in section 2. */
[data-mode="dark"] .logo-wall .shot--logo-mono { filter: invert(1); }

/* Moodswingz gets no treatment, deliberately. Their mark is a dark maroon
   (#841212) which lands near 1.8:1 on the dark ground — legible as a shape,
   dim as a wordmark — and it was previously lifted with
   `brightness(2.1) saturate(1.12)` to bring it to roughly 5.5:1.

   That is reverted on purpose: the supplied colour is the brand, and rendering
   it as a brighter red than the client chose is still altering someone else's
   mark, even though brightness moves lightness rather than hue. Matching the
   ground and accepting that it recedes is the deliberate choice here.

   It is a defensible one rather than a contrast failure: the whole strip is
   `aria-hidden="true"` and decorative, and every client name appears as real
   text on the work page — so nothing here is the only way to get the
   information. If it is ever wanted back, restore the one-line filter above;
   the class hook was removed from the markup with it. */
.logo-wall span {
  font-family: var(--font-mono);
  font-size: 9px;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--ink-4);
}

/* --- 6f. Home — team ------------------------------------------ */

.team-home { padding: clamp(24px, 4vw, 56px) var(--gut) clamp(56px, 8vw, 110px); }
.team-home h2 {
  /* Above the arch, which now reaches up behind this line. */
  position: relative;
  z-index: 1;
  margin: 0 0 clamp(28px, 4vw, 56px);
  text-align: center;
  font-weight: 500;
  font-size: clamp(40px, 9vw, 132px);
  line-height: 0.94;
  letter-spacing: -0.04em;
}

.team-home__row {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: clamp(20px, 3vw, 40px);
  align-items: center;
}
.team-home__row > p { position: relative; z-index: 1; margin: 0; max-width: 26ch; }

/* The arch runs wider than its column and taller than its row, tucking behind
   the heading above it and the button beside it. Its bottom edge does not
   move: the negative margin-top is exactly the height the extra width buys
   (aspect 4/5, so 2 * grow wider is 2.5 * grow taller), which leaves the
   item's outer box the size it always was — the row, the chips over it and
   everything below stay put. Change --grow alone and it all still holds;
   change the aspect ratio and the 2.5 has to change with it. */
.team-photo {
  --grow: clamp(24px, 11.15vw, 162px);   /* extra width, each side */
  justify-self: center;
  align-self: end;
  width: calc(100% + var(--grow) * 2);
  /* Tracks --grow so it only ever binds once the row has collapsed, where
     the cap is the original 440px. */
  max-width: calc(440px + var(--grow) * 2);
  /* Give back exactly the height the wider frame saves, so the item's outer
     box stays the 1.25 columns the original 4/5 frame occupied and nothing
     below it moves. Percentage margins resolve against the grid area's width,
     so 100% here is the column — which is what makes this whole thing
     expressible without knowing the column's px width. */
  margin-top: calc(1.25 * 100% - (100% + var(--grow) * 2) / 1.15);
  /* Wider than tall now, where it began at 4/5: `cover` crops by aspect, not
     by size, so a wider frame is what actually shows more of the photograph —
     scaling the old frame up just enlarged the same crop. Width is up ~44% on
     the original in two passes, height unchanged throughout. */
  aspect-ratio: 23/20;
  /* Half the width on both axes keeps the top a true semicircle at any size,
     where the old fixed 220px was only exact at one width. Half the width is
     57.5% of the height at this aspect. */
  border-radius: 50% 50% 26px 26px / 57.5% 57.5% 26px 26px;
  overflow: hidden;
  --h1: #EADFD1;
  --h2: #F4EEE4;
  display: grid;
  place-items: center;
  padding: 24px;
}

.team-cta {
  position: relative;
  z-index: 1;
  justify-self: stretch;
  align-self: center;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 24px;
  padding: clamp(20px, 2.4vw, 28px) clamp(22px, 2.6vw, 30px);
  border-radius: 18px;
  background: var(--bg-alt);
  font-size: clamp(18px, 1.9vw, 24px);
  font-weight: 500;
  letter-spacing: -0.02em;
  min-height: 76px;
  transition: background 0.2s ease, color 0.2s ease;
}
.team-cta:hover { background: var(--ink); color: var(--bg); }

.team-chips {
  position: relative;
  z-index: 2;
  margin-top: clamp(-64px, -4vw, -24px);
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: 14px;
}
.chip {
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 14px 18px;
  border-radius: 16px;
  background: var(--white);
  box-shadow: 0 10px 30px rgba(28, 22, 19, 0.06);
}
.chip__avatar {
  width: 52px; height: 52px;
  flex: none;
  border-radius: 100px;
  --h1: #EADFD1;
  --h2: #F4EEE4;
  --hs: 5px;
}
.chip__name { display: block; font-size: 16px; font-weight: 500; }
.chip__role {
  display: block;
  font-family: var(--font-mono);
  font-size: 10px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink-3);
}

/* --- 6g. Home — clients-served counter ------------------------ */

.counter { padding: 0 var(--gut) clamp(56px, 8vw, 110px); }
.counter__inner {
  max-width: var(--wrap);
  margin-inline: auto;
  border-radius: 18px;
  background: var(--white);
  padding: clamp(24px, 3.4vw, 44px);
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: clamp(16px, 3vw, 40px);
  align-items: end;
}
.counter__value {
  margin: 0;
  font-weight: 500;
  font-size: clamp(48px, 7vw, 104px);
  line-height: 0.92;
  letter-spacing: -0.04em;
  /* Same-width digits, so the `+` beside them does not shuffle as the figure
     counts up. */
  font-variant-numeric: tabular-nums;
  color: var(--ink-4);
  /* JS drives --count-t from 0 to 1 as the figure climbs, carrying the colour
     from muted ink to the accent with it. The plain declaration above is the
     fallback if color-mix is unavailable. */
  color: color-mix(in oklab, var(--accent) calc(var(--count-t, 0) * 100%), var(--ink-4));
}
.counter__note { margin: 0; justify-self: end; letter-spacing: 0.14em; }

/* --- 6h. Home — needs picker ---------------------------------- */

.needs { background: var(--bg-alt); padding: clamp(56px, 8vw, 110px) var(--gut); }
.needs__eyebrow { margin: 0 0 20px; text-align: center; letter-spacing: 0.22em; }
.needs h2 {
  margin: 0;
  text-align: center;
  font-family: var(--font-serif);
  font-weight: 400;
  font-size: clamp(46px, 9.5vw, 140px);
  line-height: 0.92;
  letter-spacing: -0.025em;
}

.needs__bar {
  max-width: 900px;
  margin: clamp(32px, 5vw, 64px) auto 0;
  border-radius: 100px;
  background: var(--bg);
  padding: 10px 10px 10px clamp(18px, 2.4vw, 28px);
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
}
.needs__bar > span:first-child { font-size: clamp(15px, 1.7vw, 19px); color: var(--ink-2); }
.needs__pill {
  min-height: 52px;
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 0 18px;
  border-radius: 100px;
  background: var(--white);
  font-size: clamp(15px, 1.7vw, 19px);
  font-weight: 500;
  white-space: nowrap;
}
.needs__pill--empty { padding: 0 20px; color: var(--ink-4); }
/* Wrapper for the selected pills — dissolves so they lay out as
   direct children of the flex bar. */
.needs__pills { display: contents; }
.needs__bar .btn { margin-left: auto; }

.needs__options {
  max-width: 1100px;
  margin: clamp(36px, 5vw, 72px) auto 0;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(270px, 1fr));
  gap: 4px clamp(28px, 4vw, 64px);
}
.need {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 18px;
  min-height: 56px;
  padding: 0 14px;
  border: 0;
  border-bottom: 1px solid var(--line);
  border-radius: 10px 10px 0 0;
  background: transparent;
  color: var(--ink);
  font-size: clamp(15px, 1.6vw, 18px);
  font-weight: 400;
  text-align: left;
  white-space: nowrap;
  cursor: pointer;
  box-shadow: inset 0 -2px 0 0 transparent;
  transition: background 0.15s ease, box-shadow 0.15s ease;
}
.need:hover { background: var(--bg-alt); }
.need__label { display: flex; align-items: center; gap: 12px; }
.need__label i { color: var(--accent); font-style: normal; }
.need__mark { font-size: 18px; color: var(--ink-4); }

.need[aria-pressed='true'] {
  background: var(--white);
  font-weight: 500;
  box-shadow: inset 0 -2px 0 0 var(--accent);
}
.need[aria-pressed='true'] .need__mark { color: var(--accent); }

/* --- 7. Page heads (inner pages) ------------------------------ */

.page-head { padding: clamp(44px, 6vw, 88px) var(--gut) 0; }

/* Scroll cue — a caret near the foot of every title screen, bobbing to say
   the page carries on below. Taken out of flow so it sits at the bottom of
   the screen rather than joining the block the section centres; without that
   it would ride up under the heading. Fades out as soon as the page moves,
   and JS hides it outright if there is nothing to scroll to. */
.page-head, .about-head { position: relative; }
.scroll-cue {
  position: absolute;
  left: 0; right: 0;
  bottom: clamp(18px, 4vh, 42px);
  display: grid;
  place-items: center;
  opacity: 1;
  transition: opacity 0.45s ease;
}
.scroll-cue.is-past { opacity: 0; }
.scroll-cue span {
  width: 13px;
  height: 13px;
  border-right: 2px solid var(--ink-4);
  border-bottom: 2px solid var(--ink-4);
  /* Rotated 45deg, so translating equally on both local axes reads as
     straight down the page. */
  transform: rotate(45deg);
  animation: cue-bob 2.6s ease-in-out infinite;
}
@keyframes cue-bob {
  0%, 100% { transform: rotate(45deg) translate(0, 0); }
  50%      { transform: rotate(45deg) translate(4px, 4px); }
}
.page-head p:first-child { margin: 0 0 16px; }
.page-head h1 {
  margin: 0;
  font-weight: 500;
  font-size: clamp(32px, 5.2vw, 76px);
  line-height: 1;
  letter-spacing: -0.03em;
  max-width: 20ch;
}
.page-head .lede { margin-top: clamp(22px, 3vw, 34px); }

/* --- 8. Capabilities ------------------------------------------ */

.caps { padding: clamp(48px, 7vw, 96px) var(--gut) 0; }
.cap {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
  gap: clamp(28px, 4vw, 64px);
  align-items: center;
  padding-bottom: clamp(48px, 6vw, 88px);
}
.cap__body { min-width: 0; }
.cap__no { margin: 0 0 16px; }
.cap h2 {
  margin: 0 0 18px;
  font-weight: 600;
  font-size: clamp(22px, 2.4vw, 32px);
  letter-spacing: -0.02em;
}
.cap__body > p {
  margin: 0 0 26px;
  max-width: 44ch;
  font-size: 15px;
  line-height: 1.75;
  color: var(--ink-2);
}

/* Each item is now a named deliverable with a sentence explaining it, not the
   two-word label this was built for. That rules out the old two-up grid: at
   375px an auto-fit track lands at ~150px, which a sentence cannot fit. One
   column, and the text gets a measure. */
.dash-list {
  list-style: none;
  margin: 0 0 30px;
  padding: 0;
  display: grid;
  gap: 14px;
  max-width: 62ch;
  font-size: 13px;
  line-height: 1.7;
  color: var(--ink-2);
}
/* The dash hangs in the margin rather than being a flex sibling. A flex row
   splits `<strong>label</strong> — sentence` into two items, the second of them
   anonymous and so impossible to give `min-width: 0` — it then refuses to
   shrink and pushes the sentence out of the column. Absolute positioning keeps
   the same look with the content in ordinary inline flow, which wraps. */
.dash-list li { position: relative; padding-left: 22px; }
.dash-list li::before {
  content: '\2014';
  position: absolute;
  left: 0;
  top: 0;
  color: var(--ink-4);
}
.dash-list strong { font-weight: 500; color: var(--ink); }

.cap__media {
  position: relative;
  min-width: 0;
  aspect-ratio: 4/5;
  border-radius: 26px 8px 26px 8px;
  overflow: hidden;
  background: var(--dark-green);
  display: flex;
  align-items: flex-end;
  padding: clamp(20px, 3vw, 32px);
}
.cap__media::before {
  content: '';
  position: absolute;
  inset: 0;
  background: repeating-linear-gradient(135deg, rgba(255, 255, 255, 0.05) 0 8px, rgba(0, 0, 0, 0.05) 8px 16px);
}
.cap__media--warm { background: var(--dark-warm); border-radius: 8px 26px 8px 26px; }

/* Behind the card's texture overlay and its caption, above its flat colour.
   The isolation is what makes that possible: without a stacking context here,
   a negative z-index child drops behind the card's own background instead of
   just behind its contents, and the photograph never appears. */
.cap__media.has-shot { isolation: isolate; }
.cap__media .shot { z-index: -1; }
.has-shot > .cap__stamp { display: none; }
/* The weave was there to give an empty card some life; over a photograph it
   just sits on top of it, so the overlay becomes purely a bottom-weighted
   scrim that keeps the caption readable. */
.cap__media.has-shot::before {
  background: linear-gradient(to top, rgba(28, 22, 19, 0.86) 0%, rgba(28, 22, 19, 0.38) 45%, rgba(28, 22, 19, 0.12) 100%);
}

.cap__stamp {
  position: absolute;
  top: 50%; left: 50%;
  transform: translate(-50%, -50%);
  margin: 0;
  font-family: var(--font-mono);
  font-size: 10px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--on-photo);
  background: rgba(28, 22, 19, 0.6);
  border: 1px solid rgba(247, 242, 234, 0.4);
  border-radius: 100px;
  padding: 8px 14px;
  text-align: center;
}
.cap__caption { position: relative; }
.cap__caption p:first-child { margin: 0 0 8px; color: #C6C9B6; }
.cap__media--warm .cap__caption p:first-child { color: #D3C6B4; }
.cap__caption p:last-child {
  margin: 0;
  max-width: 26ch;
  font-size: clamp(16px, 1.6vw, 20px);
  line-height: 1.35;
  color: var(--white);
}

/* Rows 02 and 04 lead with the image — that is DOM order, not a
   flex reorder, so it holds when the grid collapses to one column. */

/* --- 9. Work -------------------------------------------------- */

.work-section { padding-bottom: clamp(56px, 8vw, 110px); }
.work-section .head-split p {
  margin: 0;
  max-width: 30ch;
  line-height: 1.9;
  color: var(--ink-2);
}

.work-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  gap: clamp(24px, 3vw, 56px);
  align-items: start;
}
.case { min-width: 0; }
.case__img {
  --h1: #E7DCCD;
  --h2: #F1E8DB;
  --hs: 8px;
  display: grid;
  place-items: center;
}
.case__img .ph { padding: 7px 13px; }

/* Boxed variant — white card with a metric footer. */
.case--boxed {
  border-radius: 22px;
  background: var(--bg);
  padding: clamp(18px, 2.4vw, 26px);
}
.case--boxed h2 {
  margin: 20px 0 6px;
  font-size: 18px;
  font-weight: 600;
  letter-spacing: -0.01em;
  line-height: 1.45;
}
/* Some of these titles are links now, and an inline link's hit box is its font
   metrics — 20px here, under the 24px WCAG 2.2 minimum. Leading on the heading
   does not change that, because line-height does not grow an inline box.
   Vertical padding does, and unlike inline-block it survives wrapping. */
.case--boxed h2 a { padding-block: 3px; }
.case--boxed .case__tag { margin: 0 0 18px; letter-spacing: 0.14em; }
/* A case with a page behind it gets a way in below the figures. */
.case__more { margin: 16px 0 0; }

.metrics {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 16px;
  border-top: 1px solid var(--line);
  padding-top: 16px;
}
.metrics__value { margin: 0 0 4px; font-weight: 500; font-size: 28px; }
.metrics__label {
  margin: 0;
  font-family: var(--font-mono);
  font-size: 9px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink-3);
}

/* Plain variant — serif title with a one-line brief. */
.case--plain h2 {
  margin: 22px 0 8px;
  font-family: var(--font-serif);
  font-weight: 400;
  font-size: clamp(22px, 2.2vw, 30px);
  letter-spacing: -0.01em;
}
.case--plain .case__tag { margin: 0; letter-spacing: 0.14em; }
.case--plain .case__brief {
  margin: 14px 0 0;
  max-width: 32ch;
  font-size: 15px;
  line-height: 1.75;
  color: var(--ink-2);
}

.case-1 .case__img { aspect-ratio: 3/4; border-radius: 16px 16px 16px 4px; }
.case-2 { margin-top: clamp(0px, 9vw, 140px); width: 94%; }
.case-2 .case__img { aspect-ratio: 16/11; border-radius: 22px; }
.case-3 { margin-left: auto; width: 90%; }
.case-3 .case__img { aspect-ratio: 1/1; border-radius: 4px 16px 16px 16px; }
.case-4 { margin-top: clamp(0px, 3vw, 48px); }
.case-4 .case__img { aspect-ratio: 4/5; border-radius: 180px 180px 20px 20px; }
.case-5 { width: 96%; }
.case-5 .case__img { aspect-ratio: 5/4; border-radius: 16px; }
.case-6 { margin-top: clamp(0px, 2vw, 36px); margin-left: auto; width: 88%; }
.case-6 .case__img { aspect-ratio: 2/3; border-radius: 22px 4px 22px 22px; }

/* --- 10. About ------------------------------------------------ */

.about-head { padding: clamp(20px, 2.4vw, 34px) var(--gut) 0; }
/* The mark, washed into the background of the title screen. Isolated so the
   negative z-index keeps it under the headline rather than under the page,
   and clipped to the section so it does not bleed into the story below.
   Pale enough that the headline and lede read normally straight over it. */
.about-head { overflow: hidden; isolation: isolate; }
.about-head::before {
  content: '';
  position: absolute;
  z-index: -1;
  top: 50%;
  left: 56%;
  translate: -50% -50%;
  height: 78%;
  /* Sized off the section's height, so on a narrow screen it also has to be
     told not to outrun the width — `contain` then fits the mark inside
     whichever axis is the binding one. */
  max-width: 88%;
  aspect-ratio: 119.86 / 120.8;
  background: url('images/boldBloomLogos/BoldBloonBlack.svg') center / contain no-repeat;
  opacity: 0.07;
  pointer-events: none;
}
.about-head h1 {
  margin: 0;
  font-weight: 500;
  font-size: clamp(30px, 4.6vw, 68px);
  line-height: 1;
  letter-spacing: -0.035em;
  max-width: 16ch;
}
.about-head .arrow-lede { margin-top: clamp(22px, 3vw, 36px); }

.story {
  padding: clamp(48px, 7vw, 104px) var(--gut) clamp(48px, 7vw, 96px);
}
.story__inner {
  max-width: var(--wrap);
  margin-inline: auto;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
  gap: clamp(28px, 5vw, 88px);
  align-items: start;
}
.story__aside { min-width: 0; position: sticky; top: 104px; }
.story__aside p { margin: 0 0 18px; letter-spacing: 0.22em; }
.story__aside h2 {
  margin: 0;
  font-family: var(--font-serif);
  font-weight: 400;
  font-size: clamp(46px, 7.5vw, 110px);
  line-height: 0.9;
  letter-spacing: -0.02em;
}
.story__body { min-width: 0; }
.story__body > p:first-child {
  margin: 0 0 26px;
  font-size: clamp(18px, 2vw, 24px);
  line-height: 1.6;
  letter-spacing: -0.01em;
}
.story__body > p {
  margin: 0 0 22px;
  max-width: 52ch;
  font-size: 16px;
  line-height: 1.8;
  color: var(--ink-2);
}

.steps { border-bottom: 1px solid var(--line); margin-top: clamp(32px, 4vw, 52px); }
.step {
  display: grid;
  grid-template-columns: auto minmax(0, 140px) minmax(0, 1fr);
  gap: clamp(12px, 2vw, 28px);
  padding: 18px 0;
  border-top: 1px solid var(--line);
}
.step p { margin: 0; }
.step__no { font-family: var(--font-mono); font-size: 10px; letter-spacing: 0.16em; color: var(--ink-4); }
.step__name {
  font-family: var(--font-mono);
  font-size: 10px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--ink-2);
}
.step__desc { font-size: 15px; line-height: 1.7; color: var(--ink-2); }

.about-team { padding: 0 var(--gut) clamp(56px, 8vw, 110px); }
.about-team .head-split { margin-bottom: clamp(28px, 4vw, 56px); }
.about-team .head-split h2 { max-width: 18ch; }
.about-team .head-split p { max-width: 30ch; }

/* Three people, so three columns: at a 420px minimum the third portrait wrapped
   to a second row on its own, next to an empty half. 300px lets all three sit
   on one row inside the wrap and still collapses to one column on a phone. */
.portrait-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 300px), 1fr));
  gap: clamp(24px, 3.4vw, 56px);
  align-items: start;
}
.portrait { min-width: 0; }
.portrait__img {
  aspect-ratio: 4/5;
  border-radius: 24px;
  --h1: #EADFD1;
  --h2: #F4EEE4;
  --hs: 10px;
  display: grid;
  place-items: center;
}
.portrait__img .ph { border-color: var(--line-2); }
.portrait h3 {
  margin: 24px 0 8px;
  font-family: var(--font-serif);
  font-weight: 400;
  font-size: clamp(28px, 3vw, 44px);
  letter-spacing: -0.015em;
}
.portrait p {
  margin: 0;
  font-family: var(--font-mono);
  font-size: 11px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink-3);
}
/* The stagger that keeps the row from reading as a grid of passport photos.
   Retuned for three across — the old offsets were built for a 2x2. */
.portrait:nth-child(2) { margin-top: clamp(0px, 3vw, 48px); }
.portrait:nth-child(3) { margin-top: clamp(0px, 6vw, 96px); }

.culture { background: var(--bg-alt); padding: clamp(56px, 8vw, 110px) 0; }
.culture__head {
  max-width: var(--wrap);
  margin-inline: auto;
  padding-inline: var(--gut);
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
  gap: clamp(16px, 3vw, 48px);
  align-items: end;
  margin-bottom: clamp(28px, 4vw, 52px);
}
.culture__head p { margin: 0; max-width: 32ch; color: var(--ink-2); }
.culture__head h2 {
  margin: 0;
  justify-self: end;
  text-align: right;
  font-family: var(--font-serif);
  font-weight: 400;
  font-size: clamp(40px, 7vw, 96px);
  line-height: 0.96;
  letter-spacing: -0.02em;
}

.culture__banner {
  position: relative;
  overflow: hidden;
  min-height: clamp(420px, 50vw, 680px);
  display: flex;
}
/* The four image panes. Scoped to their own class so the absolutely
   positioned scrim and caption stay out of the flex flow. */
.culture__pane {
  position: relative;
  --hs: 10px;
  border-left: 1px solid rgba(247, 242, 234, 0.14);
}
.culture__pane:nth-of-type(1) { flex: 1.2; --h1: #2A2E24; --h2: #32362B; }
.culture__pane:nth-of-type(2) { flex: 1;   --h1: #2F3329; --h2: #373B30; }
.culture__pane:nth-of-type(3) { flex: 1.7; --h1: #282C22; --h2: #303428; }
.culture__pane:nth-of-type(4) { flex: 1;   --h1: #32362B; --h2: #3A3F33; }
.culture__pane span {
  position: absolute;
  top: 14px; left: 14px;
  font-family: var(--font-mono);
  font-size: 9px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: rgba(247, 242, 234, 0.55);
}
.culture__scrim {
  position: absolute;
  inset: 0;
  background: linear-gradient(90deg, rgba(28,22,19,0.78) 0%, rgba(28,22,19,0.55) 55%, rgba(28,22,19,0.45) 100%);
}
/* One photograph behind all four panes: they stop being image slots and
   become the banner's dividing lines. */
.culture__banner.has-shot .culture__pane { background-image: none; }
.culture__banner.has-shot .culture__pane > span { display: none; }
/* Holds the banner's original dark tone over a real photograph, and keeps
   the copy legible against it. A layer above the image, not a change to it. */
.culture__banner.has-shot .culture__scrim {
  background:
    linear-gradient(90deg, rgba(28,22,19,0.80) 0%, rgba(28,22,19,0.60) 55%, rgba(28,22,19,0.50) 100%),
    rgba(28,22,19,0.34);
}
.culture__copy {
  position: absolute;
  left: 0; bottom: 0;
  max-width: min(52ch, 92vw);
  padding: clamp(24px, 4vw, 56px);
  color: var(--on-photo);
  z-index: 2;
}
.culture__copy p:first-child { margin: 0 0 16px; color: var(--on-photo-2); }
.culture__copy h3 {
  margin: 0;
  font-family: var(--font-serif);
  font-weight: 400;
  font-size: clamp(24px, 3.2vw, 44px);
  line-height: 1.2;
  letter-spacing: -0.01em;
}
.culture__copy p:last-child {
  margin: 18px 0 0;
  max-width: 46ch;
  font-size: 15px;
  line-height: 1.8;
  color: var(--on-photo-2);
}

/* Wordmark variant. With no heading above the banner and no paragraph on it,
   the section is one photograph with the name set across it, so the scrim
   drops to what a wordmark needs rather than what body copy needs. */
.culture--mark { padding-top: 0; }
.culture--mark .culture__banner.has-shot .culture__scrim {
  background:
    linear-gradient(180deg, rgba(28,22,19,0.34) 0%, rgba(28,22,19,0.18) 45%, rgba(28,22,19,0.46) 100%),
    rgba(28,22,19,0.16);
}
.culture__wordmark {
  position: absolute;
  inset: 0;
  z-index: 2;
  margin: 0;
  display: grid;
  place-items: center;
  padding: var(--gut);
  text-align: center;
  color: var(--on-photo);
  font-family: var(--font-serif);
  font-weight: 400;
  font-size: clamp(52px, 12vw, 190px);
  line-height: 1;
  letter-spacing: -0.03em;
  /* The photograph behind this is busy and its tone varies across the frame;
     the shadow is what guarantees the edge of every letter, not the scrim. */
  text-shadow: 0 2px 40px rgba(28, 22, 19, 0.5);
}

/* --- 10b. Case study ------------------------------------------
   One long-form page per project, linked from the work grid. Unlike every
   other page here it is a single measured column of running prose, so it gets
   its own narrower wrap rather than the site-wide one — a case study that
   sets to the full 1200px would be unreadable. */

.case-study { --wrap: 760px; }
.case-study__head { padding: clamp(40px, 6vw, 88px) var(--gut) clamp(28px, 4vw, 48px); }
.case-study__head .mono { margin: 0 0 clamp(20px, 3vw, 32px); }
.case-study__head h1 {
  margin: 0;
  font-size: clamp(34px, 5.2vw, 66px);
  line-height: 1.04;
  letter-spacing: -0.03em;
  text-wrap: balance;
}
.case-study__head .lede {
  margin: clamp(20px, 3vw, 30px) 0 0;
  max-width: 54ch;
  font-size: clamp(17px, 1.9vw, 21px);
  line-height: 1.6;
  color: var(--ink-2);
}

/* The at-a-glance strip. A description list because that is what it is:
   four labelled facts, not a table and not a paragraph. */
.case-study__facts {
  margin: clamp(28px, 4vw, 44px) 0 0;
  padding-top: clamp(20px, 3vw, 28px);
  border-top: 1px solid var(--line);
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  gap: clamp(16px, 2.4vw, 28px);
}
.case-study__facts div { min-width: 0; }
.case-study__facts dt {
  margin: 0 0 8px;
  font-family: var(--font-mono);
  font-size: 9px;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--ink-4);
}
.case-study__facts dd { margin: 0; font-size: 15px; line-height: 1.5; }

.case-study__hero {
  position: relative;
  overflow: hidden;
  min-height: clamp(280px, 42vw, 560px);
  background: var(--bg-alt);
}

.case-study__body { padding: clamp(40px, 6vw, 88px) var(--gut) clamp(56px, 8vw, 110px); }
.case-study__body .wrap { max-width: var(--wrap); }
.case-study__body p {
  margin: 0 0 22px;
  font-size: 16px;
  line-height: 1.85;
  color: var(--ink-2);
}
/* The opening paragraph carries the dateline, so it sets a size up from the
   body and stays in the primary ink. Scoped through .case-study__body rather
   than declared bare, because the `.case-study__body p` rule above is (0,1,1)
   and a bare class would lose to it on every property they share. */
.case-study__body .case-study__dateline {
  margin-bottom: 26px;
  font-size: clamp(17px, 1.8vw, 20px);
  line-height: 1.65;
  color: var(--ink);
}
.case-study__dateline strong { font-weight: 500; }
.case-study__body h2 {
  margin: clamp(36px, 5vw, 56px) 0 18px;
  font-family: var(--font-serif);
  font-weight: 400;
  font-size: clamp(24px, 3vw, 36px);
  line-height: 1.2;
  letter-spacing: -0.01em;
}

/* A single figure pulled out of the prose. Sized like a heading rather than
   like body copy, because it is the thing a skimming reader should catch. */
.case-study__metric {
  margin: clamp(32px, 4vw, 48px) 0;
  padding: clamp(24px, 3vw, 36px);
  border-radius: 22px;
  background: var(--ink);
  color: var(--bg);
}
.case-study__body .case-study__metric-value {
  margin: 0;
  font-weight: 500;
  font-size: clamp(44px, 7vw, 76px);
  line-height: 1;
  letter-spacing: -0.04em;
  color: var(--bg);
}
.case-study__body .case-study__metric-label {
  margin: 12px 0 0;
  max-width: 34ch;
  font-family: var(--font-mono);
  font-size: 10px;
  line-height: 1.6;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--on-ink-muted);
}

.case-study__foot {
  margin-top: clamp(40px, 6vw, 72px);
  padding-top: clamp(24px, 3vw, 36px);
  border-top: 1px solid var(--line);
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 20px;
}

/* --- 10b. Long-form prose ------------------------------------
   Continuous text with no images in it — the privacy notice, and anything
   like it later. The type scale is the case study's, because a reader should
   not be able to tell which page they are on from the paragraphs alone; what
   differs is the measure. `.case-study__body` runs to the full 1320px wrap
   because photographs sit in the column with the text and want the width.
   Here there is nothing but sentences, and a 1320px line of 16px type is
   roughly 160 characters — about twice what anyone tracks comfortably. 70ch
   holds it to the usual 60-75. */

.prose-section { padding: clamp(40px, 6vw, 88px) var(--gut) clamp(56px, 8vw, 110px); }
.prose-section .wrap { max-width: 70ch; }
.prose-section p {
  margin: 0 0 22px;
  font-size: 16px;
  line-height: 1.85;
  color: var(--ink-2);
}
.prose-section h2 {
  margin: clamp(36px, 5vw, 56px) 0 18px;
  font-family: var(--font-serif);
  font-weight: 400;
  font-size: clamp(24px, 3vw, 36px);
  line-height: 1.2;
  letter-spacing: -0.01em;
  color: var(--ink);
}
/* Emphasis inside prose is a field name or a party, not a shout — it stays the
   body weight the rest of the site uses for strong. */
.prose-section strong { font-weight: 500; color: var(--ink); }
.prose-section code {
  font-family: var(--font-mono);
  font-size: 0.92em;
  padding: 2px 5px;
  border-radius: 5px;
  background: var(--bg-alt);
  color: var(--ink);
}
.prose-section .dash-list { margin: 0 0 22px; }

/* --- 11. Pricing ---------------------------------------------- */

.plans { padding: clamp(44px, 6vw, 88px) var(--gut) 0; }
.plans__grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  gap: clamp(20px, 2.6vw, 36px);
  align-items: start;
}
.plan {
  border-radius: 22px;
  background: var(--white);
  color: var(--ink);
  padding: clamp(24px, 3vw, 36px);
  display: flex;
  flex-direction: column;
  gap: 22px;
}
.plan__top { display: flex; align-items: center; justify-content: space-between; gap: 14px; }
.plan__top p { margin: 0; }
.plan__badge {
  font-family: var(--font-mono);
  font-size: 9px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--ink);
  background: var(--accent-badge);
  border-radius: 100px;
  padding: 6px 12px;
}
.plan h2 {
  margin: 0;
  font-weight: 600;
  font-size: clamp(21px, 2.2vw, 28px);
  letter-spacing: -0.02em;
}
.plan__desc { margin: 0; font-size: 15px; line-height: 1.75; color: var(--ink-2); }
.plan__price { padding-top: 20px; border-top: 1px solid var(--line); }
.plan__price > p:first-child { margin: 0; display: flex; align-items: baseline; gap: 10px; }
.plan__figure {
  font-weight: 500;
  font-size: clamp(34px, 4vw, 54px);
  letter-spacing: -0.035em;
  color: var(--ink-4);
}
.plan__unit {
  font-family: var(--font-serif);
  font-style: italic;
  font-size: clamp(16px, 1.6vw, 20px);
  color: var(--ink-2);
}
.plan__note { margin: 8px 0 0; font-size: 9px; letter-spacing: 0.14em; }
/* "from" rides ahead of the figure: these are starting prices, not fixed ones,
   and the qualifier has to be read as part of the number rather than as a
   footnote under it. */
.plan__from {
  font-family: var(--font-serif);
  font-style: italic;
  font-size: clamp(15px, 1.5vw, 19px);
  color: var(--ink-2);
}
.plan__price > p:first-child { gap: 8px; }
.plan__list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 10px;
  font-size: 14px;
  color: var(--ink-2);
}
.plan__list li { display: flex; gap: 12px; }
.plan__list li::before { content: '\2014'; color: var(--ink-4); }
/* A deliverable list still being written by the client. It keeps the card the
   same shape as its neighbours without dressing an unknown up as a feature. */
.plan__list--pending { font-style: italic; color: var(--ink-3); }
.plan .btn { margin-top: auto; }

.plan--dark { background: var(--ink); color: var(--bg); }
.plan--dark .plan__top p:first-child,
.plan--dark .plan__desc,
.plan--dark .plan__from,
.plan--dark .plan__unit,
.plan--dark .plan__note,
.plan--dark .plan__list { color: var(--on-ink-muted); }
.plan--dark .plan__price { border-top-color: var(--on-ink-rule); }
.plan--dark .plan__figure { color: var(--bg); }

.plan--b { margin-top: clamp(0px, 3vw, 44px); }
.plan--c { margin-top: clamp(0px, 1.5vw, 20px); }

/* Inclusions matrix */
.matrix-section { padding: clamp(56px, 8vw, 110px) var(--gut) 0; }
.matrix-section .head-split { gap: clamp(16px, 3vw, 48px); margin-bottom: clamp(28px, 4vw, 48px); }
.matrix-section .head-split h2 { font-size: clamp(26px, 3.6vw, 48px); line-height: 1.05; max-width: 18ch; }
.matrix-section .head-split p { max-width: 30ch; }

.matrix-card {
  border-radius: 22px;
  background: var(--white);
  padding: clamp(20px, 3vw, 38px);
  overflow-x: auto;
  /* `position: relative` is load-bearing, not decoration. The table is wider
     than a phone and scrolls inside this card, but every cell also carries an
     absolutely-positioned `.sr-only` span naming its dot for a screen reader.
     `overflow-x: auto` does not make an element the containing block for
     absolutely-positioned descendants, so without this those 25 one-pixel boxes
     were laid out against the page instead of the card, escaped the clip, and
     dragged the whole document's scroll width out to 564px — the entire pricing
     page slid sideways on every phone. */
  position: relative;
}
.matrix {
  width: 100%;
  min-width: 560px;
  border-collapse: collapse;
}
.matrix th, .matrix td { padding: 16px 0; }
.matrix thead th {
  padding-bottom: 18px;
  border-bottom: 1px solid var(--line);
  font-family: var(--font-mono);
  font-size: 10px;
  font-weight: 400;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  vertical-align: bottom;
}
.matrix thead th:first-child {
  text-align: left;
  letter-spacing: 0.16em;
  color: var(--ink-3);
  width: 38%;
}
.matrix tbody tr + tr th, .matrix tbody tr + tr td { border-top: 1px solid var(--line); }
.matrix tbody th {
  text-align: left;
  font-size: 15px;
  font-weight: 400;
  color: var(--ink-2);
  padding-right: clamp(12px, 2vw, 28px);
}
.matrix td { text-align: center; }

.dot {
  display: inline-block;
  width: 12px; height: 12px;
  border-radius: 100px;
  background: var(--accent);
  vertical-align: middle;
}
.dot--off { background: transparent; border: 1.5px solid var(--line-2); }

/* Quote process */
.process { padding: clamp(56px, 8vw, 110px) var(--gut); }
.process h2 {
  margin: 0 0 clamp(32px, 4.5vw, 64px);
  margin-left: clamp(0px, 3vw, 56px);
  font-weight: 500;
  font-size: clamp(26px, 3.6vw, 48px);
  line-height: 1.05;
  letter-spacing: -0.03em;
  max-width: 16ch;
}
.process__grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
  gap: clamp(24px, 3vw, 48px);
  border-top: 1px dashed var(--line-2);
  padding-top: clamp(24px, 3vw, 40px);
}
.process__item { min-width: 0; }
.process__no {
  margin: 0 0 14px;
  font-weight: 500;
  font-size: clamp(30px, 3.4vw, 44px);
  letter-spacing: -0.03em;
  color: var(--ink-4);
}
.process__name {
  margin: 0 0 8px;
  font-family: var(--font-mono);
  font-size: 10px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
}
.process__desc { margin: 0; max-width: 26ch; font-size: 15px; line-height: 1.7; color: var(--ink-2); }

.pricing-cta {
  margin-top: clamp(44px, 6vw, 88px);
  border-radius: 22px;
  background: var(--bg-alt);
  padding: clamp(26px, 4vw, 52px);
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
  gap: clamp(20px, 3vw, 44px);
  align-items: end;
}
.pricing-cta p {
  margin: 0;
  font-weight: 500;
  font-size: clamp(22px, 3vw, 40px);
  line-height: 1.05;
  letter-spacing: -0.03em;
  max-width: 18ch;
}
.pricing-cta .btn { justify-self: end; padding: 0 32px; font-size: 16px; }

/* --- 12. Contact ---------------------------------------------- */

/* One screen: title top left, ways to reach us bottom left, form on the
   right. The form is the whole point of the page, so it should never be
   something you have to scroll to find. */
/* Vertical padding off the viewport height, not its width: on a short window
   the gutter is what pushes the Send button under the fold. */
.contact { padding: clamp(20px, 2.6vh, 48px) var(--gut) clamp(20px, 2.6vh, 48px); }
.contact__inner {
  max-width: var(--wrap);
  margin-inline: auto;
  display: grid;
  /* `min(320px, 100%)` rather than a bare 320px: a bare minimum wider than the
     space available cannot shrink, so at 320px the column stayed 320px inside a
     280px area and pushed the page sideways. */
  grid-template-columns: repeat(auto-fit, minmax(min(320px, 100%), 1fr));
  gap: clamp(28px, 4vw, 72px);
  align-items: stretch;
}
/* Heading at the top of the column, the ways to reach us pushed to its foot. */
.contact__intro {
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  gap: clamp(24px, 4vh, 56px);
  min-width: 0;
}
.contact__head p:first-child { margin: 0 0 16px; }
.contact__head h1 {
  margin: 0;
  font-weight: 500;
  /* The same scale as the other title screens, even though this one shares
     its row with the form. */
  font-size: clamp(34px, 5.2vw, 78px);
  line-height: 1;
  letter-spacing: -0.03em;
  max-width: 14ch;
}
/* The standing arrow-lede is sized for a screen it has to itself. Here it sits
   under a heading twice its old size and beside a form of 16px fields, so it
   reads as supporting text rather than competing with either. */
.contact__head .arrow-lede {
  margin-top: clamp(18px, 2.4vw, 28px);
  max-width: 40ch;
  font-size: clamp(15px, 1.15vw, 17px);
  line-height: 1.65;
}
/* These three read as separate ways to reach us, so they need air between
   them — at the old spacing an eyebrow sat closer to the paragraph above it
   than to its own value. */
.contact__details { display: grid; gap: clamp(26px, 4vh, 48px); min-width: 0; }

.form { display: grid; gap: clamp(14px, 2.2vh, 24px); min-width: 0; align-content: start; }
/* The `rows` attributes are the no-CSS fallback; on the page the two text
   areas give up height on a short window so the Send button stays in view. */
.form textarea[rows='4'] { min-height: clamp(64px, 10vh, 110px); height: auto; }
.form textarea[rows='3'] { min-height: clamp(52px, 7vh, 84px); height: auto; }
.field { display: grid; gap: 8px; }
.field > span {
  font-family: var(--font-mono);
  font-size: 10px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--ink-3);
}
.field input,
.field textarea {
  font-family: var(--font-sans);
  font-size: 16px;
  padding: 12px 0;
  border: 0;
  border-bottom: 1px solid var(--line-2);
  background: transparent;
  color: var(--ink);
}
.field textarea { line-height: 1.6; resize: vertical; }
/* The two required fields say so. `required` on the input already tells a screen
   reader; this is the visible half of the same information, so it is aria-hidden
   to avoid announcing it twice. Style, not an asterisk: an asterisk needs a
   legend somewhere to mean anything. */
.field__req {
  font-style: normal;
  color: var(--ink-4);
  letter-spacing: 0.16em;
}
.field input:focus,
.field textarea:focus { border-bottom-color: var(--accent); }

.form__actions {
  display: flex;
  align-items: center;
  gap: 20px;
  flex-wrap: wrap;
  margin-top: 8px;
}
.form__actions .btn { padding: 0 28px; font-size: 16px; }
/* The privacy link sits in the 10px mono note beside Send, so its text box is
   about 14px tall — under WCAG 2.2's 24px minimum on its own. The padding
   stretches the hit area to 44px and the negative margin gives the height back,
   so the row is laid out exactly as it was before the link existed. */
.form__actions .ul-link {
  display: inline-block;
  padding-block: 15px;
  margin-block: -15px;
}
/* One element, three states. Colour is never the only signal — the wording
   differs too, and the live region's politeness changes with it. */
.form__status { margin: 0; font-size: 14px; color: var(--accent); }
.form__status[data-state='sending'] { color: var(--ink-3); }
.form__status[data-state='error'] { color: var(--warn); }

/* While a submission is in flight the button has to look unavailable, or it
   invites a second click that would send a second enquiry. */
.btn:disabled,
.btn[aria-busy='true'] {
  opacity: 0.55;
  cursor: progress;
  pointer-events: none;
}

.contact__card {
  border-radius: 18px;
  background: var(--bg-alt);
  padding: clamp(22px, 3vw, 32px);
}
.contact__card p:first-child { margin: 0 0 14px; }
.contact__card p:nth-child(2) {
  margin: 0 0 10px;
  font-size: clamp(19px, 2.2vw, 26px);
  font-weight: 500;
  letter-spacing: -0.02em;
}
.contact__card p:last-child { margin: 0; font-size: 14px; color: var(--ink-2); }
.contact__block p:first-child { margin: 0 0 14px; }
.contact__block p:nth-child(2) { margin: 0 0 10px; font-size: 16px; }
.contact__block p:last-child { margin: 0; font-size: 14px; color: var(--ink-2); }
.contact__social { display: flex; gap: 20px; font-size: 15px; }
/* These two are 17px tall, and the underline is a border — padding would push
   it off the text. So the hit area is an overlay instead, reaching into the
   20px gap without moving anything that shows. Same trick as the service rows
   on home, which stretch their link across the whole row. */
.contact__social .ul-link { position: relative; }
.contact__social .ul-link::after {
  content: '';
  position: absolute;
  inset: -6px -5px;
}

/* --- 13. Footer ----------------------------------------------- */

.site-footer { padding: clamp(40px, 5vw, 64px) var(--gut) 28px; }
.site-footer__cols {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
  gap: clamp(24px, 4vw, 52px);
  padding-bottom: clamp(32px, 4vw, 56px);
}
.site-footer__brand { display: flex; align-items: center; gap: 11px; }
.site-footer__mark {
  width: 24px; height: 24px;
  flex: none;
}
.site-footer__name { font-size: 17px; font-weight: 600; letter-spacing: -0.02em; }
.site-footer__blurb {
  margin: 16px 0 0;
  max-width: 26ch;
  font-size: 14px;
  line-height: 1.7;
  color: var(--ink-3);
}
.site-footer nav {
  display: grid;
  gap: 11px;
  align-content: start;
  font-family: var(--font-mono);
  font-size: 10px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
}
.site-footer nav > span { color: var(--ink-3); }
/* 10px uppercase mono is a 13px-tall line, and a 13px-tall link stacked 11px
   from its neighbours is a coin toss on a phone — under the 24px minimum in
   WCAG 2.5.8. The padding grows the hit area into the gap that was already
   there and the negative margin takes the growth back out of the layout, so
   every link is 25px tall to a thumb and the footer looks untouched. */
.site-footer nav a {
  padding-block: 6px;
  margin-block: -6px;
}
.site-footer__legal {
  display: flex;
  justify-content: space-between;
  gap: 20px;
  flex-wrap: wrap;
  border-top: 1px solid var(--line);
  padding-top: 20px;
  font-family: var(--font-mono);
  font-size: 10px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink-3);
}
/* The privacy link sits in the legal row and reads as part of it, not as a
   navigation item — so it takes the row's colour rather than --ink. The
   padding is the stretched hit area the footer nav links already use: at 10px
   type the text box is about 13px tall, which clears WCAG's 24px minimum only
   with this, and a thumb wants more than the minimum. The negative margin
   keeps the row's height exactly where it was. */
.site-footer__legal-link {
  color: inherit;
  padding-block: 10px;
  margin-block: -10px;
}
.site-footer__legal-link:hover { color: var(--accent); }

/* --- 14. Motion ----------------------------------------------- */

/* Scroll reveal — JS adds .is-armed before observing, .is-in on entry. */
.is-armed {
  opacity: 0;
  transform: translateY(16px);
  transition: opacity 0.7s ease, transform 0.7s cubic-bezier(0.2, 0.7, 0.2, 1);
}
.is-in { opacity: 1; transform: none; }

/* Section headings arrive left to right as well as up: a gradient mask three
   times the heading's width slides across it, so the first word resolves
   before the last. The hero statement's fade, turned 90 degrees. Body copy
   keeps the plain lift — the sweep is what marks a heading as a heading.
   The transition has to restate opacity and transform: this rule's shorthand
   replaces .is-armed's outright. */
[data-reveal='sweep'].is-armed {
  -webkit-mask-image: linear-gradient(to right, #000 0 33.333%, transparent 66.666%);
          mask-image: linear-gradient(to right, #000 0 33.333%, transparent 66.666%);
  -webkit-mask-size: 300% 100%;
          mask-size: 300% 100%;
  -webkit-mask-repeat: no-repeat;
          mask-repeat: no-repeat;
  -webkit-mask-position: 100% 0;
          mask-position: 100% 0;
  transition:
    opacity 0.7s ease,
    transform 0.7s cubic-bezier(0.2, 0.7, 0.2, 1),
    -webkit-mask-position 1.1s cubic-bezier(0.2, 0.7, 0.2, 1),
            mask-position 1.1s cubic-bezier(0.2, 0.7, 0.2, 1);
}
[data-reveal='sweep'].is-in {
  -webkit-mask-position: 0 0;
          mask-position: 0 0;
}
/* Dropped once it has arrived, so no heading is left carrying a mask —
   the same housekeeping the opening sequence does. JS adds this on the
   sweep's transitionend. */
[data-reveal='sweep'].is-swept {
  -webkit-mask-image: none;
          mask-image: none;
}

/* Hero statement fades as it scrolls away. */
[data-anim='fade-out-on-scroll'] { will-change: opacity, transform; }

/* Opening sequence, in two beats:
   1. the statement fades in, centred in a full-height screen of its own;
   2. it slides up to the top of the page while the carousel rises to meet
      it, the card tops finishing just behind the last line of the headline.

   `.js-intro` is set on <html> by an inline script in the head, so the
   opening state is in place before the first paint — and never applies
   at all without scripting or with reduced motion asked for. The two
   travel distances are measured at runtime into --intro-lift and
   --intro-wheel; the fallbacks are only used if that has not run yet. */
.statement__inner {
  transition:
    opacity 1.6s ease,
    -webkit-mask-position 2.6s ease,
    mask-position 2.6s ease,
    transform 1.5s cubic-bezier(0.2, 0.7, 0.2, 1);
}
.wheel-section {
  transition: opacity 1.2s ease, transform 1.9s cubic-bezier(0.2, 0.7, 0.2, 1);
}

/* Held still while the travel distances are measured. */
.intro-measuring .statement__inner,
.intro-measuring .wheel-section { transition: none !important; }

@media (prefers-reduced-motion: no-preference) {
  /* The fade runs top to bottom: a gradient mask three times the height of
     the block slides up through it, so the first line resolves before the
     last. Dropped once the sequence is over so nothing is left masked. */
  .js-intro:not(.intro-2) .statement__inner {
    -webkit-mask-image: linear-gradient(to bottom, #000 0 33.333%, transparent 66.666%);
            mask-image: linear-gradient(to bottom, #000 0 33.333%, transparent 66.666%);
    -webkit-mask-size: 100% 300%;
            mask-size: 100% 300%;
    -webkit-mask-repeat: no-repeat;
            mask-repeat: no-repeat;
  }

  .js-intro .statement__inner {
    opacity: 0;
    -webkit-mask-position: 0% 100%;
            mask-position: 0% 100%;
    transform: translateY(var(--intro-lift, 26vh));
  }
  .js-intro .wheel-section {
    opacity: 0;
    transform: translateY(var(--intro-wheel, 70vh));
  }

  /* Beat 1 — fade in top to bottom, still centred. */
  .js-intro.intro-1 .statement__inner {
    opacity: 1;
    -webkit-mask-position: 0% 0%;
            mask-position: 0% 0%;
  }

  /* Beat 2 — both travel to their settled positions. */
  .js-intro.intro-2 .statement__inner { transform: translateY(0); }
  .js-intro.intro-2 .wheel-section { opacity: 1; transform: translateY(0); }
}

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  * { transition: none !important; animation: none !important; }
  .is-armed { opacity: 1; transform: none; }
}

/* --- 15. Narrow screens --------------------------------------- */

@media (max-width: 900px) {
  /* Three columns end about here. Below it the arch sits in the right-hand
     column, where spilling sideways would run it off the page — and there is
     no heading or button beside it to sit behind anyway. No growth means no
     height given back either, so the margin goes with it. */
  .team-photo { --grow: 0px; margin-top: 0; }
  .story__aside { position: static; }
  .clients .head-split p,
  .counter__note,
  .pricing-cta .btn { justify-self: start; }
  .culture__head h2 { justify-self: start; text-align: left; }
}

@media (max-width: 640px) {
  :root { --hdr: 110px; }
  /* Two rows: brand and the Contact pill share the top line, the
     section links sit underneath. */
  .site-header { position: sticky; gap: 10px; }
  .brand__sub { display: none; }
  .site-nav { gap: 14px; width: 100%; }
  .site-nav .btn {
    position: absolute;
    top: 12px;
    right: var(--gut);
    min-height: 40px;
    padding: 0 16px;
    gap: 8px;
  }
  .team-chips { margin-top: 24px; }
  .preview-card--a,
  .preview-card--b,
  .preview-card--c,
  .case-2, .case-3, .case-4, .case-5, .case-6,
  .plan--b, .plan--c,
  .portrait:nth-child(2),
  .portrait:nth-child(3),
  .portrait:nth-child(4) {
    margin: 0;
    width: 100%;
  }
  .work-preview__foot .btn { justify-self: start; }
  .process h2 { margin-left: 0; }
  .work-preview .head-split h2 { margin-left: 0; }
  .culture__pane:nth-of-type(2),
  .culture__pane:nth-of-type(4) { display: none; }
}
