/* ==========================================================================
   HUGHES MECHANICAL CONSULTING — main.css
   Light (white paper), premium, GSAP scroll-driven single page.
   The fixed nav and the footer stay dark navy; everything between them
   sits on white paper. Blueprint-blue linework replaces the dark
   theme's glow accents.
   ========================================================================== */

/* ==========================================================================
   1. TOKENS
   ========================================================================== */
:root {
  --ink-950: #0a141f;
  --ink-900: #0e1a26;
  --ink-800: #16273b;
  --steel-700: #24466b;
  --steel-500: #33608f;
  --steel-400: #4a7fb5;
  --steel-300: #7fa8cd;
  --chrome-200: #c8d2da;
  --chrome-100: #e8edf2;
  --white: #f5f8fa;
  /* --glow and the --glass pair are for the DARK bars only (nav, mobile
     menu, footer). Never use them on the white page body. */
  --glow: #55a8ff;
  --glass: rgba(255, 255, 255, 0.05);
  --glass-brd: rgba(255, 255, 255, 0.10);

  /* Page theme (2026-08-26). The page base is now --ink-950 (#0a141f),
     the same value the nav and footer sit on, so the whole page reads as
     one continuous dark navy surface; the bars are separated by their
     hairlines and shadows rather than by a tonal step. */
  --paper: #060e17;         /* page base */
  --paper-alt: #0a141f;     /* alternating sections */
  --paper-tint: #101d2d;    /* wells and image frames */

  /* THE CANVAS — one continuous drafting surface (colour + grid) behind
     the whole of <main>, replacing what used to be a per-section colour
     block each with its own seam gradient. Every section between the nav
     and the footer is transparent and sits ON this; photos are the only
     thing that covers it, and they fade back into it at their own edges
     rather than the canvas stopping at theirs. The footer, on --ink-950,
     ends it with a hard edge and no fade — deliberately the one seam on
     the page that reads as a cut. Anything that needs to dissolve into
     the page background fades to --canvas, never to --paper/--paper-alt,
     so there is a single value to change. */
  --canvas-rgb: 10, 20, 31;
  --canvas: rgb(var(--canvas-rgb));
  --canvas-grid: rgba(127, 168, 205, 0.06);
  --text-strong: #f5f8fa;   /* headings — 14.6:1 on --paper */
  --text: #c8d2da;          /* body — 10.1:1 on --paper */
  --accent: #7fa8cd;        /* labels + small marks — 6.2:1 on --paper */
  --accent-mid: #7fa8cd;    /* CAD linework, icons */
  --line: rgba(255, 255, 255, 0.12);
  /* The only non-steel hue on the page: form validation, nothing else. */
  --alert: #ef9a8a;
  --line-strong: rgba(255, 255, 255, 0.22);
  --shadow-sm: 0 1px 3px rgba(3, 8, 14, 0.30);
  --shadow-md: 0 10px 30px rgba(3, 8, 14, 0.42);
  --shadow-lg: 0 30px 70px rgba(3, 8, 14, 0.55);

  --font-display: "Space Grotesk", "Helvetica Neue", Arial, sans-serif;
  --font-body: "Inter", "Helvetica Neue", Arial, sans-serif;

  --pad: clamp(20px, 5vw, 72px);
  --nav-h: 84px;
  --ease-out: cubic-bezier(0.22, 1, 0.36, 1);
  /* The unfold curve, shared by everything that moves when a service is
     opened or shut: the copy's own window, the copy inside it, and the
     height of the whole Services section (which is what carries Process
     down the page and back up). Leaves fast, decelerates hard, and runs
     about 5% past its mark before settling — that overshoot is the bounce.
     One token because the pieces have to travel together: matched curve
     and matched duration is what stops the copy and the section below it
     arriving at different times, which is when they overlap.

     The 1s they all share is in the rules themselves rather than a token,
     since the fades that go with them are deliberately shorter. A full
     second because at half of it the whole move was over before it
     registered as a move: the copy, the section and Process all arrived
     while the eye was still on the heading that had just been clicked. */
  --ease-unfold: cubic-bezier(0.16, 1.4, 0.36, 1);
}

/* ==========================================================================
   2. RESET / BASE
   ========================================================================== */
*,
*::before,
*::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

html {
  scroll-behavior: smooth;
  /* Two declarations, and the order is the point. `clip` is what we want —
     it contains without turning the root into a scroll container, so
     position: sticky and the pinned sections keep working — but it is only
     Safari 16 and up. An older iOS drops it as an invalid value and is
     left containing nothing at all, which is a page you can drag sideways
     into empty canvas. `hidden` is understood everywhere and is read
     first; anything that understands `clip` overwrites it on the next
     line, so no current browser changes behaviour. */
  overflow-x: hidden;
  overflow-x: clip;
}

html.lenis,
html.lenis body {
  height: auto;
}

.lenis.lenis-smooth {
  scroll-behavior: auto !important;
}

body {
  background: var(--canvas);
  color: var(--text);
  font-family: var(--font-body);
  font-size: 16px;
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
  /* Same pair, same reason as <html> above. */
  overflow-x: hidden;
  overflow-x: clip;
}

body.menu-open {
  overflow: hidden;
}

/* The canvas itself. The colour is on <main>; ::before carries the grid
   as one unbroken layer from the top of the page to the last section's
   final pixel, so the ruling lines up across every section boundary
   instead of restarting per block. Sections sit above it (z-index: 1)
   and are transparent, so this is what shows between and behind them. */
main {
  position: relative;
  background: var(--canvas);
}

main::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  background-image:
    linear-gradient(var(--canvas-grid) 1px, transparent 1px),
    linear-gradient(90deg, var(--canvas-grid) 1px, transparent 1px);
  background-size: 48px 48px;
  /* --grid-drift is written by main.js from the scroll position: the
     horizontal ruling is pushed back down the page by most of the
     distance scrolled, so it crawls instead of racing past. The vertical
     ruling keeps a fixed origin — sideways drift would just look like
     wobble. Defaults to 0, so with no JS the grid is simply static. */
  background-position:
    0 var(--grid-drift, 0px),
    0 0;
}

main > section {
  position: relative;
  z-index: 1;
  /* Anchor targets stop clear of the fixed nav. This is the native path —
     a link from another page (Start a Project points at index.html#contact
     from Relationships and FAQ), a bookmark, a browser restoring a
     position — none of which go through the smooth scroll in js/main.js,
     and all of which would otherwise put the top of the section behind the
     bar. The script re-takes the landing afterwards, once the page has
     stopped changing shape (5a); this is what the visitor sees until it
     does, and what they get if the script never runs at all. */
  scroll-margin-top: calc(var(--nav-h) + 12px);
}

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

a {
  color: inherit;
  text-decoration: none;
}

ul,
ol {
  list-style: none;
}

address {
  font-style: normal;
}

h1,
h2,
h3 {
  font-family: var(--font-display);
  color: var(--text-strong);
  line-height: 1.05;
  font-weight: 600;
  letter-spacing: -0.01em;
}

h2 {
  font-size: clamp(34px, 4.5vw, 56px);
  max-width: 18ch;
  text-wrap: balance;
}

h3 {
  font-size: clamp(18px, 1.6vw, 22px);
  line-height: 1.2;
}

p {
  max-width: 62ch;
}

::selection {
  background: var(--steel-500);
  color: #fff;
}

.skip-link {
  position: absolute;
  top: -60px;
  left: 16px;
  z-index: 200;
  background: var(--chrome-100);
  color: var(--ink-950);
  padding: 10px 18px;
  border-radius: 999px;
  font-weight: 600;
  transition: top 0.2s var(--ease-out);
}

.skip-link:focus {
  top: 16px;
}

/* ==========================================================================
   3. UTILITIES — labels, container, buttons, glass, image frames
   ========================================================================== */
/* Visually hidden but read by screen readers and indexed by crawlers.
   Used for the intro h1's text, where the heading is rendered as the logo
   PNG. Clipped rather than display:none (which AT skips) and margin:-1px
   + overflow:hidden so it can never open a horizontal scrollbar. */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

.container {
  max-width: 1280px;
  margin: 0 auto;
  padding-left: var(--pad);
  padding-right: var(--pad);
}

.label {
  font-family: var(--font-body);
  font-size: 12px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.14em;
  color: var(--accent);
}

.section-head {
  margin-bottom: clamp(40px, 6vw, 72px);
}

.section-head .label {
  margin-bottom: 14px;
}

/* Services is the one section whose header was built without an eyebrow,
   and the rest of it is authored around that absence: from 900px up the
   header is a two-column grid where the lede is pinned to ROW 1 because
   the h2 is row 1 (see the note at the 900px rule), and on top of that
   both the h2 and the lede carry hand placement from the editor. Put a
   label in the flow and it takes row 1, the h2 drops to row 2 and the
   lede ends up beside the label instead of beside the heading — the whole
   header rearranges to accommodate one line of 12px type.

   So this one is set in the padding that is already above the heading
   rather than in the header's flow: 144px of clear space on a desktop,
   104 on a phone, against a label that needs about 33. bottom:100% plus
   the same 14px the other sections use puts it exactly where an in-flow
   eyebrow would sit, and because it is out of flow the h2, the lede, the
   two service headings, the drawing and everything below the section stay
   where they are at every width. */
.services .section-head {
  position: relative;
}

.services .section-head .label {
  position: absolute;
  bottom: 100%;
  left: 0;
}

.section-lede {
  margin-top: 18px;
  color: var(--text);
  max-width: 54ch;
}

/* Buttons — pill */
.btn {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  border-radius: 999px;
  padding: 14px 28px;
  font-family: var(--font-body);
  font-size: 13px;
  font-weight: 600;
  /* 0.08em was doing the job of a wordmark, not a button — at this size
     it read as gaps between letters rather than tracking. */
  letter-spacing: 0.035em;
  text-transform: uppercase;
  line-height: 1;
  border: 1px solid transparent;
  cursor: pointer;
  transition: transform 0.3s var(--ease-out), box-shadow 0.3s var(--ease-out),
    background-color 0.3s var(--ease-out), border-color 0.3s var(--ease-out);
}

.btn-primary {
  background: var(--chrome-100);
  color: var(--ink-950);
}

.btn-primary:hover {
  transform: translateY(-2px);
  box-shadow: 0 0 26px rgba(85, 168, 255, 0.30);
}

.btn-ghost {
  border-color: var(--line-strong);
  background: var(--glass);
  color: var(--text-strong);
}

.btn-ghost:hover {
  transform: translateY(-2px);
  border-color: var(--steel-400);
  box-shadow: var(--shadow-md);
}

.btn-sm {
  padding: 10px 20px;
  font-size: 11px;
}

/* Image frame — keeps layout solid even when the asset is missing */
.img-frame {
  position: relative;
  overflow: hidden;
  border-radius: 16px;
  background: linear-gradient(160deg, var(--paper-tint) 0%, var(--paper-alt) 85%);
}

.img-frame::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: linear-gradient(180deg, rgba(5, 12, 20, 0.28), transparent 45%);
}

.img-frame img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* Hover-to-colour: every duotoned photo carries a true-colour twin that
   crossfades in on hover, while the blueprint/duotone washes fade out. */
.img-frame .img-color {
  position: absolute;
  inset: 0;
  z-index: 2;
  opacity: 0;
  transition: opacity 0.45s var(--ease-out);
  pointer-events: none;
}

.img-frame:hover .img-color {
  opacity: 1;
}

.about-photo-frame::before {
  transition: opacity 0.45s var(--ease-out);
}

.about-photo-frame:hover::before {
  opacity: 0;
}

/* ==========================================================================
   4. NAV
   ========================================================================== */
.nav {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 100;
  /* Transparent at rest — .nav::before carries the actual fill (a fade
     down into the photo below, the same dark-to-clear treatment used at
     the foot of full-bleed sections elsewhere on the page), so the bar
     reads as a graded vignette over the hero rather than a hard bar.
     Solid only lands once scrolled, below. */
  background: transparent;
  border-bottom: 1px solid transparent;
  transition: background-color 0.4s var(--ease-out),
    border-color 0.4s var(--ease-out), box-shadow 0.4s var(--ease-out);
}

.nav::before {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: linear-gradient(
    180deg,
    rgba(10, 20, 31, 0.85) 0%,
    rgba(10, 20, 31, 0.55) 55%,
    rgba(10, 20, 31, 0.18) 85%,
    transparent 100%
  );
  transition: opacity 0.4s var(--ease-out);
}

.nav.is-scrolled {
  /* Same dark bar as before, now only once there's real content behind it
     to separate from. */
  background: var(--ink-950);
  border-bottom-color: rgba(255, 255, 255, 0.08);
  box-shadow: 0 12px 34px rgba(3, 8, 14, 0.55);
}

.nav.is-scrolled::before {
  opacity: 0;
}

.nav-inner {
  position: relative;
  max-width: 1380px;
  margin: 0 auto;
  height: var(--nav-h);
  padding: 0 var(--pad);
  display: flex;
  align-items: center;
  gap: 32px;
}

.nav-logo {
  /* Flex, not the default inline: an inline anchor around an inline-
     replaced img leaves baseline gap under it, so its own box would be a
     few pixels taller than the 41px img. */
  display: inline-flex;
  align-items: center;
}

.nav-logo img {
  height: 41px;
  width: auto;
  filter: drop-shadow(0 2px 8px rgba(0, 0, 0, 0.5));
}

.nav-links {
  display: flex;
  gap: 34px;
  margin-left: auto;
}

.nav-links a {
  font-size: 12px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.14em;
  color: var(--chrome-200);
  transition: color 0.25s var(--ease-out);
}

.nav-links a:hover {
  color: var(--glow);
}

.nav-cta {
  flex-shrink: 0;
}

/* Burger */
.nav-burger {
  display: none;
  flex-direction: column;
  justify-content: center;
  gap: 5px;
  width: 44px;
  height: 44px;
  padding: 10px;
  margin-left: auto;
  background: transparent;
  border: 1px solid rgba(255, 255, 255, 0.18);
  border-radius: 10px;
  cursor: pointer;
}

.nav-burger span {
  display: block;
  height: 2px;
  width: 100%;
  background: var(--chrome-100);
  border-radius: 2px;
  transition: transform 0.3s var(--ease-out), opacity 0.3s var(--ease-out);
}

body.menu-open .nav-burger span:nth-child(1) {
  transform: translateY(7px) rotate(45deg);
}

body.menu-open .nav-burger span:nth-child(2) {
  opacity: 0;
}

body.menu-open .nav-burger span:nth-child(3) {
  transform: translateY(-7px) rotate(-45deg);
}

/* ==========================================================================
   5. MOBILE MENU (full-screen overlay)
   ========================================================================== */
.mobile-menu {
  position: fixed;
  inset: 0;
  z-index: 90;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: flex-start;
  gap: 40px;
  padding: calc(var(--nav-h) + 24px) var(--pad) 48px;
  background: rgba(10, 20, 31, 0.97);
  backdrop-filter: blur(18px);
  -webkit-backdrop-filter: blur(18px);
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  transition: opacity 0.35s var(--ease-out), visibility 0.35s var(--ease-out);
}

.mobile-menu.is-open {
  opacity: 1;
  visibility: visible;
  pointer-events: auto;
}

.mobile-menu-links {
  display: flex;
  flex-direction: column;
  gap: 18px;
}

.mobile-menu-links a {
  font-family: var(--font-display);
  font-size: clamp(30px, 8vw, 44px);
  font-weight: 600;
  color: var(--white);
  transition: color 0.25s var(--ease-out);
}

.mobile-menu-links a:hover {
  color: var(--glow);
}

.mobile-menu-meta {
  color: var(--steel-300);
  line-height: 2;
}

/* ==========================================================================
   6. INTRO — CAD draw-on opener
   ========================================================================== */
.intro {
  position: relative;
  min-height: 100svh;
  display: flex;
  /* The inner grid fills the section so the sign can hang from the top
     while the copy stays centred in the remaining height. */
  align-items: stretch;
  /* The card leads the flow again, so the top padding has to clear the nav. */
  /* Symmetric block padding: the inner grid uses align-content: center, so
     an asymmetric pair (100px top vs 56px bottom) centred the lockup 22px
     BELOW the viewport centre. Equal padding makes "centred" true against
     the viewport, and the nav clearance still holds because the top value
     is unchanged. */
  padding: calc(var(--nav-h) + 16px) var(--pad);
  overflow: clip;
  /* Transparent onto the shared canvas (see main::before). The seam
     gradient that used to live here — paper fading to paper-alt for
     Services below — has nothing left to hand off to: both sections now
     sit on the same surface. The photo wall above still fades out at its
     own bottom edge, and what it fades into is the canvas. */
  background: transparent;
}

/* Legibility scrim — weighted to the lower half, where the headline and
   lede now sit, and clear across the top so the sign reads against real
   photography. Edge-anchored and full-bleed, so it has no visible boundary;
   it also releases in the last few percent so the seam into Services
   doesn't band. Sits above the tiles, below .intro-inner. */
.intro::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  background: linear-gradient(
    180deg,
    transparent 0%,
    rgba(10, 20, 31, 0.08) 26%,
    rgba(10, 20, 31, 0.5) 44%,
    rgba(10, 20, 31, 0.62) 60%,
    rgba(10, 20, 31, 0.46) 80%,
    rgba(10, 20, 31, 0.26) 93%,
    transparent 100%
  );
}

/* The blueprint grid that used to sit here was removed once the photo
   wall took over the intro background — two overlaid textures fought each
   other. The Process section still carries the drafting grid. */

/* Photo wall — a single full-bleed photo behind the headline. Sits
   above the section background, below the vignette and the copy; the
   bottom edge fades back to the section background so the Services
   seam still reads. */
.intro-wall {
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  opacity: 1;
  -webkit-mask-image: linear-gradient(
    180deg,
    black 0%,
    black 80%,
    transparent 100%
  );
  mask-image: linear-gradient(180deg, black 0%, black 80%, transparent 100%);
}

.intro-tile {
  position: relative;
  width: 100%;
  height: 100%;
  overflow: hidden;
  /* Matches the canvas so a photo that hasn't decoded yet reads as bare
     surface rather than a differently-toned hole in it. */
  background: var(--canvas);
}

/* Graded, not raw: a steel gradient over a desaturated, darkened photo
   so whatever's cycling through still reads as this site's surface
   rather than a bare snapshot. */
.intro-tile::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(
    165deg,
    rgba(18, 37, 58, 0.08) 0%,
    rgba(18, 37, 58, 0.36) 100%
  );
}

.intro-tile-img {
  position: absolute;
  inset: 0;
  background-position: center;
  background-size: cover;
  background-repeat: no-repeat;
  filter: grayscale(0.26) brightness(1.02) contrast(1.04);
  /* Layer A starts visible; JS only ever fades the layer it just loaded. */
  transition: opacity 2.4s var(--ease-out);
}

.intro-tile-img:last-child {
  opacity: 0;
}

.intro-inner {
  position: relative;
  z-index: 2;
  width: 100%;
  max-width: 1380px;
  margin: 0 auto;
  display: grid;
  /* Sign, lede — centred as a block in the section's full height. */
  grid-template-columns: minmax(0, 1fr);
  grid-template-rows: auto auto;
  align-content: center;
  gap: 0;

  /* ---- LOCKUP GEOMETRY -------------------------------------------
     logo.png is 1159x471, but the wordmark drawn inside it is not
     centred in that canvas: the artwork occupies x 103..1141, y 63..391,
     leaving 103px of dead space on the left against 17px on the right
     (measured off the file: alpha jumps 116 -> 255 at x 103, and the
     H's bottom flange is the leftmost ink, not the stem).
     Centring anything on the <img> box therefore lands it ~4.3% of the
     logo's width left of where the wordmark actually reads, which is
     what made the rule and the lede look off.

     So the box is normalised to the artwork rather than the file: the
     dead space is pulled off with negative margins below, and every
     side is expressed as a fraction of --logo-w (the rendered width),
     since dead-px / 1159 * --logo-w gives the rendered inset for the
     vertical sides too — the image keeps its aspect ratio, so the file
     width is the single denominator for all four.

     After that, .intro-logo shrink-wraps to the wordmark itself and
     ordinary centring is honest centring. --lockup-w is that visible
     width, for anything that wants to span the sign exactly. */
  --logo-w: clamp(320px, 42vw, 640px);
  /* Numerators are the measured dead-space in FILE pixels; the shared
     denominator is the file's width. Left as division rather than
     pre-computed decimals so the numbers in this file are the raw
     measurement and there is no rounding step to get wrong.

     125, not 103 or 117. Sampled off the PNG's pixels: the H's structural
     flange edge — a near-black outline, rgb(7,7,7) — is at x 125 and holds
     there for ~250 rows. Ink does reach x 103 at full alpha, but ONLY on
     rows 380-386 (7 rows), and it is the pale blue baseplate glow under
     the H, rgb(131,160,204). Cropping to that sliver instead of the flange
     drags the whole sign ~5px right of centre, because nothing on the
     right-hand side answers it. The eye reads the dark flange as the edge,
     so that is what gets centred; 117 is just where the sliver tapers. */
  --logo-dead-l: calc(125 / 1159);
  --logo-dead-r: calc(17 / 1159);
  --logo-dead-t: calc(63 / 1159);
  --logo-dead-b: calc(79 / 1159);
  /* The single gap between the sign and the tagline. It used to be spent
     twice — once above the dividing rule and once below it — but the rule
     is gone, so this is now the whole distance between the two. */
  --lockup-gap: clamp(16px, 1.9vw, 24px);
}

.intro-lede {
  /* Centred under the sign, squared to the same width as the logo image
     above it — width, not max-width, and the same clamp() .intro-logo
     img uses, so the measure is stable at every size instead of
     shrinking to fit two short lines of text. Set in the wordmark's own
     subtitle treatment — uppercase, tracked, Space Grotesk — so the two
     read as one lockup. The hairline rule that used to divide them is
     gone: sign and tagline now sit on one gap of clear space with
     nothing drawn between them. */
  grid-row: 2;
  align-self: start;
  justify-self: center;
  margin: 0 auto;
  /* One gap now instead of two: the space used to be split above and
     below the hairline rule, and with the rule gone that pair would read
     as an unexplained double-height void.
     .intro-logo's box ends exactly where the wordmark ends, but the
     tagline's line box carries leading above its cap height — the same
     ~7px the rule pass measured as 24px above vs 31px below. With
     nothing drawn between them any more, the distance that has to look
     like --lockup-gap is ink to ink, so that leading comes back off the
     top or the tagline reads as sitting half a line further away. */
  --lockup-lead: 0.25em;
  /* 1.9, not 1. With nothing drawn between them, the sign's own subtitle
     ("MECHANICAL CONSULTING") and this tagline are both uppercase tracked
     Space Grotesk at similar weight, and a single gap let them read as one
     three-line block instead of a sign with a caption under it. The rule
     used to hold them about two gaps apart; this restores that separation
     without putting anything back in it. */
  --lockup-open: 1.9;
  margin-top: calc(
    var(--lockup-gap) * var(--lockup-open) - var(--lockup-lead) * 2
  );
  /* Still the logo's full rendered width, not the wordmark's: the tagline
     needs the measure to stay on one line where it can (narrowing it to
     the wordmark wraps "ALBERTA." onto a second line at every desktop
     width). Centred on the artwork, same as the sign above it. */
  width: var(--logo-w);
  /* Global `p { max-width: 62ch }` would otherwise still clamp this
     narrower than the logo above it and undo the width set above. */
  max-width: none;
  text-align: center;
  font-family: var(--font-display);
  text-transform: uppercase;
  font-weight: 500;
  /* Tracking is added after the last glyph as well, so the line box ends
     up one letter-space wider than the ink it holds and centring the box
     leaves the glyphs half a space to the left. text-indent on a centred
     line shifts the content by half the indent, so an indent of one full
     letter-space cancels exactly that half — measured on the rendered
     pixels: 0 -> 1.5px left of the rule, 0.07em -> 1.0px, 0.14em -> dead
     on. Both properties read the same token so they cannot drift. */
  --lede-track: 0.14em;
  letter-spacing: var(--lede-track);
  text-indent: var(--lede-track);
  /* The measure is not wide enough for one line at any desktop width the
     42vw clamp actually produces, so the default greedy wrap dropped a
     single 78px word ("ALBERTA.") onto line 2 under a 469px line 1.
     Balancing splits the two lines evenly instead of leaving that orphan. */
  text-wrap: balance;
  font-size: 14px;
  line-height: 1.9;
  color: var(--text);
}

/* Legibility comes from the section-wide vignette on .intro::after, not a
   scrim boxed around this column — a floating radial read as a dark smudge
   sitting on the photos. */

/* Full lockup standing in for the old drawn headline. Same file as the
   footer, just large, with a drop shadow doing the legibility work the
   vignette alone doesn't finish over the busier tiles. */
.intro-logo {
  grid-row: 1;
  justify-self: center;
}

.intro-logo img {
  display: block;
  /* Bigger than the first pass. */
  width: var(--logo-w);
  /* The negative margins below make .intro-logo shrink-wrap narrower
     than this image, and the global `img { max-width: 100% }` would then
     clamp the image to that narrower parent — which re-shrinks the
     parent, and so on. The image's width is set explicitly here, so the
     reset's cap has nothing useful to do. */
  max-width: none;
  height: auto;
  /* Crop the file's dead space out of the LAYOUT only — the artwork is
     untouched and the soft blueprint glow still paints outside the box,
     which is what it is there for. See .intro-inner for the numbers. */
  margin-left: calc(var(--logo-w) * var(--logo-dead-l) * -1);
  margin-right: calc(var(--logo-w) * var(--logo-dead-r) * -1);
  margin-top: calc(var(--logo-w) * var(--logo-dead-t) * -1);
  margin-bottom: calc(var(--logo-w) * var(--logo-dead-b) * -1);
  filter: drop-shadow(0 6px 24px rgba(3, 8, 14, 0.55));
}

/* ==========================================================================
   7. SERVICES
   ========================================================================== */
.services {
  position: relative;
  /* Tighter than the old 3-up grid needed: two cards of three lines each
     leave far less to breathe around.

     The foot is a third of the head, and Process's run-up above is cut to
     the same figure: the two are read as one gap, so they are set as one.
     Smaller than a straight halving on purpose — the tail is measured off
     the model's canvas box (5e in js/main.js), and the drawing stops some
     50px above the bottom of that box, so half the numbers still left an
     eyeful more air than half the gap. What is halved is what the eye
     actually reads: from the lowest thing in the section to the Process
     label. */
  padding: clamp(64px, 8vw, 104px) 0 clamp(22px, 2.6vw, 34px);
  /* Transparent onto the shared canvas — no seam gradient needed now
     that the section above and below are the same surface. */
  background: transparent;
  contain: layout paint;
  /* The section is held open for whichever service is unfolded (5e in
     js/main.js writes the min-height), so its foot moves as the copy opens
     and closes — and Process, which sits under it, rides down with it. The
     curve and the duration are the copy's own, so the two travel as one
     movement instead of the section catching up afterwards. */
  transition: min-height 1s var(--ease-unfold);
}

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

/* No seam shadow here. This section used to cast one up from the Process
   section below it (paired with .sectors::before on the far side), back
   when Process was its own colour block and the two shadows sold it as a
   raised slab. Process sits on the same canvas as everything else now, so
   there is no edge to imply — a dark gradient at this boundary reads as a
   grubby horizontal band across an otherwise continuous surface, and the
   grid running straight through is what makes the page feel like one
   sheet. Sections are separated by their own padding, nothing else. */

.services-grid {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: clamp(16px, 2vw, 28px);
}

/* No card box and no rule: the grid alone carries the structure. */
.service-card {
  padding-top: clamp(18px, 2vw, 26px);
}

/* --- Slideshow mode -------------------------------------------------
   JS adds .is-slides and pages the six services two at a time. All six
   stay in the DOM and in source order; they're stacked into two columns
   (odds left, evens right, every card on row 1) so the row height is the
   tallest card and nothing shifts as pairs swap. Without JS, or under
   reduced motion, the plain grid above is what renders. */
.services-grid.is-slides {
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: clamp(24px, 3vw, 48px);
}

.services-grid.is-slides > .service-card {
  grid-row: 1;
  opacity: 0;
  visibility: hidden;
  /* --slide-x is set from JS per item, right before it enters or leaves,
     to +/-100% depending on travel direction — see initSlideshow() in
     main.js. The 24px fallback only ever shows on first paint, before JS
     has picked a direction. */
  transform: translateX(var(--slide-x, 24px));
  pointer-events: none;
  transition: opacity 0.6s var(--ease-out), transform 0.6s var(--ease-out),
    visibility 0.6s step-end;
}

.services-grid.is-slides > .service-card:nth-child(odd) {
  grid-column: 1;
}

.services-grid.is-slides > .service-card:nth-child(even) {
  grid-column: 2;
}

.services-grid.is-slides > .service-card.is-current {
  opacity: 1;
  visibility: visible;
  transform: none;
  pointer-events: auto;
  transition: opacity 0.6s var(--ease-out), transform 0.6s var(--ease-out),
    visibility 0s;
}

/* Slightly wider measure than the 3-up grid, since each card now has
   half the row to itself. */
.services-grid.is-slides .service-card p {
  max-width: 42ch;
}

/* --- The chevron lanes as a gradient wall ---------------------------
   A slide travels a full card-width in and out, so before this it stayed
   fully lit the whole way across the arrow rail and then simply stopped
   dead at the container edge. The grid is instead widened by
   --slide-fade on both sides with matching padding, so resting copy sits
   exactly where it always did while the box itself now reaches under
   both chevrons — and a mask ramps that added lane to nothing at its
   outer edge. Copy on its way out dies into the chevron beside it rather
   than sliding past it.

   mask-repeat: no-repeat is what handles the rest of the travel: outside
   the single mask tile there is no mask at all, so everything beyond the
   widened box goes unpainted — no second, fully-opaque copy of the ramp
   tiling out across the section. The ramp is deliberately uneven (a
   little over a third of the way lit at just past half the lane) so it
   reads as falling away into the wall rather than as a flat linear
   dissolve. */
.services-grid.is-slides {
  --slide-fade: clamp(38px, 5vw, 78px);
  padding-inline: var(--slide-fade);
  margin-inline: calc(var(--slide-fade) * -1);
  -webkit-mask-image: linear-gradient(
    90deg,
    transparent 0,
    rgba(0, 0, 0, 0.38) calc(var(--slide-fade) * 0.56),
    #000 var(--slide-fade),
    #000 calc(100% - var(--slide-fade)),
    rgba(0, 0, 0, 0.38) calc(100% - var(--slide-fade) * 0.56),
    transparent 100%
  );
  mask-image: linear-gradient(
    90deg,
    transparent 0,
    rgba(0, 0, 0, 0.38) calc(var(--slide-fade) * 0.56),
    #000 var(--slide-fade),
    #000 calc(100% - var(--slide-fade)),
    rgba(0, 0, 0, 0.38) calc(100% - var(--slide-fade) * 0.56),
    transparent 100%
  );
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
  /* The widened box now sits over both chevrons, so it hands hit-testing
     back: the empty lanes are inert and only the current card takes the
     pointer (it re-enables its own above). */
  pointer-events: none;
}

/* Above the grid, so a slide crossing the lane passes behind the chevron
   as it dissolves — the mask alone would fade it out in front of the
   glyph, which reads as the text evaporating rather than ducking under
   something. */
.services-arrow {
  position: relative;
  z-index: 1;
}

/* Slider row — wide arrow rails flanking the slide(s), full height, so
   the hit target is generous without crowding the copy. Shared with
   Sectors below: same controls, same behaviour, different content. */
.services-slider,
.sectors-slider {
  display: flex;
  align-items: stretch;
  gap: clamp(10px, 1.4vw, 20px);
}

.services-slider > .services-grid,
.sectors-slider > .sectors-grid {
  flex: 1 1 auto;
  min-width: 0;
}

/* Bare chevrons, no card behind them — just a tall, wide hit target
   floating over the section background. One size variable feeds both the
   button and its icon, so the two can never drift into a non-square,
   slightly-stretched box between them (that mismatch — width and height
   resolving a fraction of a pixel apart — was reading as a soft, faintly
   "pixelated" edge on the diagonal stroke). aspect-ratio backs that up by
   forcing the button itself to be a true square regardless of layout. */
.services-arrow,
.sectors-arrow,
.process-arrow {
  --arrow-size: clamp(48px, 4.4vw, 68px);
  flex: 0 0 auto;
  width: var(--arrow-size);
  aspect-ratio: 1;
  border: 0;
  background: none;
  color: var(--text);
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: color 0.3s var(--ease-out);
}

/* Sized off --arrow-size directly, not a percentage of the button's own
   box: Services' button is deliberately stretched tall (full card height),
   and a percentage of THAT height would distort the icon right back into
   a non-square rectangle. Deriving both axes from the same variable is
   what actually guarantees a square icon. */
.services-arrow svg,
.sectors-arrow svg,
.process-arrow svg {
  width: calc(var(--arrow-size) * var(--arrow-icon, 0.85));
  height: calc(var(--arrow-size) * var(--arrow-icon, 0.85));
  transition: transform 0.3s var(--ease-out);
}

/* The two overlay arrows get a roomier hit target than the Services rail
   without the chevron itself getting bigger: --arrow-size grows and
   --arrow-icon shrinks by the same factor, so the drawn stroke stays put
   and the extra area is pure padding around it. (Services is excluded —
   its button is stretched to the full card height, so widening it eats
   into the slide beside it.) */
.sectors-arrow,
.process-arrow {
  --arrow-size: clamp(58px, 5.3vw, 82px);
  --arrow-icon: 0.7;
}

.services-arrow:hover,
.sectors-arrow:hover,
.process-arrow:hover {
  color: var(--text-strong);
}

/* Grows off the whole hit target, not the glyph — the chevron reacts as
   soon as the pointer is anywhere in the (now larger) square. Scaling the
   svg rather than the button keeps the buttons' own translateY(-50%)
   centring transform intact. */
.services-arrow:hover svg,
.sectors-arrow:hover svg,
.process-arrow:hover svg,
.services-arrow:focus-visible svg,
.sectors-arrow:focus-visible svg,
.process-arrow:focus-visible svg {
  transform: scale(1.16);
}

.services-arrow:focus-visible,
.sectors-arrow:focus-visible,
.process-arrow:focus-visible {
  outline: 2px solid var(--accent-mid);
  outline-offset: 2px;
}

/* Header, pager and slide(s) read as one block, so the two gaps around
   the pager are tighter than the site's default section rhythm. */
.services .section-head,
.sectors .section-head,
.process .section-head {
  margin-bottom: clamp(26px, 3.4vw, 42px);
}

/* Services runs a touch looser than that shared floor: with the lede
   moved up beside the heading, the block ends on a line of body copy
   that sat too close to the first card's H3. */
.services .section-head {
  margin-bottom: clamp(34px, 4.2vw, 56px);
}

/* SERVICES + SECTORS HEADERS — two columns from 900px up. The heading
   owns the left column and the lede sits beside it in the right one,
   dropped to the foot of the heading block so the two read as one line
   of type rather than a stacked paragraph. Below 900px both fall back
   to the stacked .section-head everything else uses.

   The two headers differ only in row count: Sectors carries a .label
   above its h2, Services doesn't — so the lede's row is set per section
   below rather than shared. */
@media (min-width: 900px) {
  .services .section-head,
  .sectors .section-head,
  .process .section-head {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 0.9fr);
    column-gap: clamp(32px, 5vw, 88px);
  }

  .services .section-head h2,
  .sectors .section-head .label,
  .sectors .section-head h2,
  .process .section-head .label,
  .process .section-head h2 {
    grid-column: 1;
  }

  .services .section-head .section-lede,
  .sectors .section-head .section-lede,
  .process .section-head .section-lede {
    grid-column: 2;
    align-self: end;
    margin-top: 0;
    /* Narrower measure than the 54ch default — this column is roughly
       45% of the container, and 54ch would overrun it. */
    max-width: 44ch;
  }

  /* Services: h2 is row 1 (no label). Sectors: label is row 1, h2 row 2.
     Services centres its lede against the heading rather than dropping
     it to the foot — the heading runs three lines there, and bottoming
     the lede left it hanging off the last one. Sectors keeps the
     bottom alignment; its label row makes centring read as drift. */
  .services .section-head .section-lede {
    grid-row: 1;
    align-self: center;
  }

  .sectors .section-head .section-lede,
  .process .section-head .section-lede {
    grid-row: 2;
    /* Single spacer to the bands below — .sectors-grid drops its own
       margin-top to match (see section 8), so the gap under the header
       is the .section-head margin and nothing more. */
  }

  .sectors .section-head {
    margin-bottom: clamp(30px, 3.6vw, 46px);
  }
}

.services-pager,
.sectors-pager {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 12px;
  margin: clamp(26px, 3.2vw, 40px) 0 0;
}

.services-pager button,
.sectors-pager button,
.process-pager button {
  position: relative;
  width: 34px;
  height: 3px;
  padding: 0;
  border: 0;
  border-radius: 999px;
  background: var(--line);
  cursor: pointer;
  transition: background-color 0.4s var(--ease-out);
}

/* The pill stays a hairline; the actual hit target is a 44px square
   centred on it, matching the usual touch-target minimum. */
.services-pager button::before,
.sectors-pager button::before,
.process-pager button::before {
  content: "";
  position: absolute;
  inset: -20.5px -5px;
}

.services-pager button:hover,
.sectors-pager button:hover,
.process-pager button:hover {
  background: var(--line-strong);
}

.services-pager button[aria-current="true"],
.sectors-pager button[aria-current="true"],
.process-pager button[aria-current="true"] {
  background: var(--accent-mid);
}

.services-pager button:focus-visible,
.sectors-pager button:focus-visible,
.process-pager button:focus-visible {
  outline: 2px solid var(--accent-mid);
  outline-offset: 4px;
}

/* Two-up slideshow gives each card far more room than the old 3-up grid,
   so the type steps up ~5.5px at both ends of the scale. */
.service-card h3 {
  margin-bottom: 12px;
  font-size: clamp(23.5px, 2vw, 27.5px);
}

.service-card p {
  font-size: 20px;
  line-height: 1.55;
  color: var(--text);
  max-width: 34ch;
}

/* A combined card carries two paragraphs (the two services it merged), so
   they need a beat between them rather than running together. */
.service-card p + p {
  margin-top: 0.7em;
}

/* The heading of each service is the model's control (see 5d in main.js),
   so main.js swaps it for a button in place. Same face, same size, same
   colour as the heading it replaces — the only thing added is a plus sign
   in front of it, which says there is copy folded away underneath and
   turns into an X while that copy is open. The negative margin keeps the
   text itself flush with the copy below, sign and all. */
.service-switch {
  display: inline-block;
  position: relative;
  margin-left: -26px;
  padding: 0 0 0 26px;
  border: 0;
  background: none;
  font: inherit;
  color: inherit;
  letter-spacing: inherit;
  text-align: left;
  cursor: pointer;
  transition: color 0.35s var(--ease-out);
}

/* Two bars on the same centre: one flat, one stood on end. Closed they
   read as a plus; opening rotates the pair 45deg so the same two bars
   become an X, and closing unwinds it. Both are rotated about their own
   centre, so the sign never drifts off the baseline of the heading. */
.service-switch::before,
.service-switch::after {
  content: "";
  position: absolute;
  left: 0;
  top: 0.6em;
  width: 15px;
  height: 2px;
  background: var(--accent-mid);
  transition: transform 0.45s var(--ease-out),
    background-color 0.4s var(--ease-out);
}

.service-switch::after {
  transform: rotate(90deg);
}

/* A breath of light every four seconds, so the sign reads as something to
   press rather than punctuation. Both bars carry the same animation, so
   the whole plus lights together; only on a closed card, because an open
   one has already been pressed and its X is lit by .is-current below.

   The light takes a second and a half to come up and nearly two to go, and
   it is the CURVE that does the work: ease-in-out on every step, so it
   arrives and leaves at nothing and is only quick through the middle. An
   earlier version ran the same swell on the site's ease-out, which starts
   at full speed — with only a 2px bar to light, that reads as a blink
   rather than a breath. Peaks a little past the middle and falls off
   longer than it rose, which is how a breath goes.

   Services only. The FAQ rows carry the same sign (.faq-mark) and are
   deliberately left still: a page that is nothing but a list of rows would
   have seven of these breathing at once. */
@keyframes service-plus-pulse {
  0%,
  100% {
    box-shadow: 0 0 0 rgba(127, 168, 205, 0);
    background: var(--line-strong);
  }

  38% {
    box-shadow: 0 0 10px 1px rgba(127, 168, 205, 0.45);
    background: var(--accent-mid);
  }

  88% {
    box-shadow: 0 0 0 rgba(127, 168, 205, 0);
    background: var(--line-strong);
  }
}

.service-card:not(.is-current) .service-switch::before,
.service-card:not(.is-current) .service-switch::after {
  animation: service-plus-pulse 4s ease-in-out infinite;
}

/* Hovering is the visitor already knowing; the hint stands down. */
.service-switch:hover::before,
.service-switch:hover::after,
.service-switch:focus-visible::before,
.service-switch:focus-visible::after {
  animation: none;
  background: var(--accent-mid);
}

/* --- The same breath, on the words ------------------------------------
   The plus was doing this work alone, and a 15px bar is a small thing to
   hang "this is a control" on. The heading is the control — it is what the
   thumb actually lands on — so it takes the same hint: the same 4s cycle,
   the same ease-in-out curve, the same peak at 38% and fall through 88%,
   so the sign and the words breathe as one thing rather than two.

   It scales from the left edge, which is where the plus sits: the sign
   holds its place and its size while the words grow away from it, so the
   two hints never fight over the same pixels. Scaling from the centre
   would have walked the first letter back and forth across the gap.

   A transform never reflows, so none of this touches the layout — the
   heading's box, the copy under it and the model beside it are all
   measured off the unscaled span.

   --- Why the glow is a layer and not a text-shadow ---------------------
   The first cut animated text-shadow and colour on the words themselves,
   alongside the scale. Every one of those is a paint property: the glyphs
   had to be re-rasterised on the main thread on every frame, and because
   the scale was landing on unpromoted text the browser was also re-fitting
   the type at 1.0004, 1.0011, 1.0021 … all the way up. On a desktop that
   is merely wasteful. On a phone or tablet, sharing a frame budget with
   the WebGL drawing right above it, it is the stutter and the soft,
   half-resolution type the client saw.

   So nothing here paints per frame any more. The words move on transform
   and the light arrives on opacity — the two properties a compositor can
   animate on its own thread without asking the main one for a single
   repaint. The glyphs are rasterised once, at one size, and the GPU does
   the rest.

   The light is the same text-shadow it always was, on the same glyphs. It
   simply lives on its own twin of the words now (see upgradeHeadings in
   main.js): same type, same box, laid under the real ones in transparent
   ink, so the only thing it contributes is the shadow. Fading that layer
   in and out is an opacity change, which composites; fading a shadow on
   the words themselves was a repaint, which does not.

   What is gone is the colour lift. It was the other half of the per-frame
   repaint, and it cannot be had for free — the glow under the words does
   the brightening instead. */
@keyframes service-label-pop {
  0%,
  100% {
    transform: scale(1);
  }

  38% {
    transform: scale(1.03);
  }

  88% {
    transform: scale(1);
  }
}

@keyframes service-label-glow {
  0%,
  100% {
    opacity: 0;
  }

  38% {
    opacity: 1;
  }

  88% {
    opacity: 0;
  }
}

/* inline-block is what makes the span transformable at all; an inline box
   ignores scale. Shrink-to-fit, so a heading that wraps to two lines on a
   phone wraps inside the span exactly as it wrapped inside the button. */
.service-switch-label {
  display: inline-block;
  position: relative;
  transform-origin: left center;
}

/* Promoted only while it is actually breathing. will-change is a standing
   instruction to keep a layer alive, so it is scoped to the same selector
   that starts the animation rather than sitting on every label on the
   page: an open card, a hovered one and a reduced-motion visitor all get
   their plain text back with no layer behind it.

   This is the half that fixes the resolution. Promoted, the type is
   rasterised once and the GPU scales that; unpromoted, the browser was
   re-rendering the glyphs at every fractional step of the swell.

   .is-settling is a card that has just been opened and whose hint is
   being let down gently rather than cut (see settleHint in main.js). It
   is on the selector so that the animation keeps running for the rest of
   its cycle even though the card is now current. The class comes off at
   the cycle boundary, where the swell is back at rest and the light is
   back at nothing — so what removes the animation removes a value
   identical to the one underneath it, and nothing moves. */
.service-card:not(.is-current) .service-switch-label,
.service-card.is-settling .service-switch-label {
  animation: service-label-pop 4s ease-in-out infinite;
  will-change: transform;
}

/* The one thing a wind-down cannot survive is having its timing changed
   underneath it, and every stand-down below is written to avoid that.
   This transition is the belt to that braces: if an animation is ever
   pulled off these two mid-swell — a hover, a focus, anything added
   later — the value it was holding eases back to rest over a third of a
   second instead of snapping there. It cannot touch the animation
   itself; an animated value outranks a transition for as long as the
   animation is running. */
.service-switch-label {
  transition: transform 0.35s var(--ease-out);
}

/* The light itself: the twin, in ink that paints nothing, carrying the
   shadow that does. inset:0 hands it the label's own box, so a heading
   that wraps to two lines wraps identically here — same width, same font,
   same line-height, same break. z-index puts it under the real words, and
   the label's transform makes the stacking context, so "under" means
   under the text and nothing else; it cannot fall through to the section.

   The shadow is carried a little heavier than the old one (0.62 against
   0.5) because it is no longer stacked on top of the words' own colour
   change — this is now the whole of the effect. */
.service-switch-glow {
  position: absolute;
  inset: 0;
  z-index: -1;
  color: transparent;
  text-shadow: 0 0 14px rgba(127, 168, 205, 0.62);
  opacity: 0;
  pointer-events: none;
  user-select: none;
  /* The same safety net as the label above, on the property this one
     animates. */
  transition: opacity 0.35s var(--ease-out);
}

.service-card:not(.is-current) .service-switch-glow,
.service-card.is-settling .service-switch-glow {
  animation: service-label-glow 4s ease-in-out infinite;
  will-change: opacity;
}

/* --- Staggered, so the two do not breathe in unison -------------------
   Two headings swelling on the same frame reads as the page doing
   something to itself; offset, each one draws the eye on its own and the
   section feels alive rather than mechanical. Half the cycle apart is as
   far apart as two can get.

   Negative, so it starts already two seconds in rather than standing
   still for two seconds first — on a section the visitor may only look at
   for a moment, a hint that has not begun yet is a hint that never
   happened.

   The card's plus sign takes the same offset. It has to: the sign and the
   words are one hint on one control, and leaving the sign on the old
   phase would have set the second heading arguing with its own plus.
   Nothing about either animation changes — same keyframes, same curve,
   same four seconds — only when this one card starts.

   The settling card carries the delay too, and this matters more than it
   looks: a running animation whose delay changes does not stop, it jumps
   — two seconds of phase, in one frame, which on this card is the swell
   snapping to full size. Letting the label and its light keep the offset
   while they wind down is what keeps the second card's exit as quiet as
   the first card's. */
.service-card:nth-of-type(2):not(.is-current) .service-switch-label,
.service-card:nth-of-type(2).is-settling .service-switch-label,
.service-card:nth-of-type(2):not(.is-current) .service-switch-glow,
.service-card:nth-of-type(2).is-settling .service-switch-glow,
.service-card:nth-of-type(2):not(.is-current) .service-switch::before,
.service-card:nth-of-type(2):not(.is-current) .service-switch::after {
  animation-delay: -2s;
}

/* --- A pointer on it holds it still -----------------------------------
   The hint stands down under a cursor, because the visitor has already
   found the control and no longer needs telling. It used to stand down by
   being deleted — animation:none, transform:none — which threw away
   whatever the swell was holding and put the words back at full stop on
   the next frame. Arriving at a heading should not make it flinch.

   Paused instead. The animation keeps its place in the cycle and simply
   stops advancing, so the words hold the size they had when the cursor
   reached them, however far up or down the swell that was. Take the
   cursor away again and it carries on from the same place — there is no
   frame anywhere in that exchange where a value changes by more than the
   animation itself was going to change it.

   Both layers pause together, so the light holds with the size.
   will-change stays: a paused animation is still an animation, and this
   layer is about to be either resumed or eased away. */
.service-switch:hover .service-switch-label,
.service-switch:focus-visible .service-switch-label,
.service-switch:hover .service-switch-glow,
.service-switch:focus-visible .service-switch-glow {
  animation-play-state: paused;
}

@media (prefers-reduced-motion: reduce) {
  .service-card:not(.is-current) .service-switch::before,
  .service-card:not(.is-current) .service-switch::after,
  .service-card:not(.is-current) .service-switch-label,
  .service-card:not(.is-current) .service-switch-glow,
  .service-card.is-settling .service-switch-label,
  .service-card.is-settling .service-switch-glow {
    animation: none;
    will-change: auto;
  }

  /* Nothing to ease back from, either: the wind-down never starts here
     (main.js checks for a running animation first), and these two should
     not answer a class change with a glide. */
  .service-switch-label,
  .service-switch-glow {
    transition: none;
  }
}

.service-card:not(.is-current) .service-switch {
  color: var(--text);
}

.service-card:not(.is-current) .service-switch:hover {
  color: var(--text-strong);
}

.service-card.is-current .service-switch::before {
  transform: rotate(45deg);
}

.service-card.is-current .service-switch::after {
  transform: rotate(135deg);
}

/* --- The copy each heading folds away ---------------------------------
   Collapsed with a grid row that animates from 0fr to 1fr, which needs no
   measured pixel height and so survives a resize, a font swap or a text
   edit in the visual editor mid-open. .is-collapsible is added by main.js
   (5d) only once the model is live: without it every card is simply open,
   which is what a no-JS visitor gets. */
.service-body {
  display: grid;
  grid-template-rows: 1fr;
}

/* Out of flow once it can collapse, and this is what stops one service's
   copy shoving the other service's heading down the page as it opens. In
   flow the open copy is real height, and every heading below it moves —
   which was fine when the two cards were stacked in a column and read as
   one list, and is meaningless now that the headings are placed by hand
   wherever they need to be. Absolute, so opening one changes nothing but
   itself.

   It costs no layout: collapsed, the grid row is already 0fr and the body
   is already 0 tall, so taking it out of flow leaves every heading exactly
   where it sits today. top: 100% keeps it against the foot of its own card,
   which is where it was, and js/main.js (5e) carries the heading's own
   offset across to it so the two travel together. */
.service-card {
  position: relative;
}

.service-card.is-collapsible .service-body {
  position: absolute;
  top: 100%;
  left: 0;
  width: 100%;
  /* 0px rather than 0fr, and this matters: the open state is a measured
     pixel height (below), and a track cannot be interpolated from an fr to
     a length — the browser gives up and swaps the two values at the
     halfway mark, which is a fold that jumps open with no travel at all.
     Both ends in the same units, and it interpolates. */
  grid-template-rows: 0px;
  opacity: 0;
  overflow: hidden;
  /* Shut, the fade runs late and slow: the copy stays readable for the
     first stretch of the roll-up and dissolves on the way, rather than
     blinking out and leaving an empty blind to close. */
  transition: grid-template-rows 1s var(--ease-unfold),
    opacity 0.55s var(--ease-out) 0.2s;
}

/* Open on the measured height of the copy rather than 1fr, because an fr
   track cannot be pushed past its content: interpolated beyond 1fr it
   simply stops, which would have the copy arrive early and hold still
   while the section under it was still moving. In pixels the window can
   run the same few percent past its mark and settle back, so the copy and
   the section bounce as one thing. js/main.js (5e) keeps
   --service-body-h measured; the 1fr fallback only stands in if the copy
   is somehow opened before it has been measured once, and opens without
   travelling (no length to interpolate against).

   Opening, the fade is short and immediate: the text is legible almost as
   soon as the window starts to move, so what you watch is copy rolling
   down rather than a gap opening and text appearing in it afterwards. */
.service-card.is-collapsible.is-current .service-body {
  grid-template-rows: var(--service-body-h, 1fr);
  opacity: 1;
  transition: grid-template-rows 1s var(--ease-unfold),
    opacity 0.42s var(--ease-out);
}

/* A grid item will not shrink below its content unless told to. */
.service-body-inner {
  min-height: 0;
}

/* The copy rides down inside its own window instead of being stretched
   into place: shut, it sits a few pixels above where it belongs and is
   clipped by the blind above it, so opening reads as the paragraphs
   rolling out from under the heading and settling. Same curve and the
   same duration as the window and the section — all three are one
   movement, and retiming one means retiming all of them. */
.service-card.is-collapsible .service-body-inner {
  transform: translateY(-14px);
  transition: transform 1s var(--ease-unfold);
}

.service-card.is-collapsible.is-current .service-body-inner {
  transform: none;
}

.service-switch:focus-visible {
  outline: 2px solid var(--accent-mid);
  outline-offset: 6px;
}

/* The outdent has to stay inside the page gutter, and --pad bottoms out at
   20px on a phone — a 26px sign would hang off the edge of the screen. */
@media (max-width: 599px) {
  .service-switch {
    margin-left: -15px;
    padding-left: 15px;
  }

  .service-switch::before,
  .service-switch::after {
    width: 11px;
  }
}

/* --- Copy left, model right ------------------------------------------
   The two combined services sit in the left column and the right column
   carries the blueprint model, which draws whichever of the two services
   the visitor last picked. No frame and no fill behind it — the canvas is
   transparent, so the section's CAD grid shows straight through the void
   around the drawing. The slot keeps its aspect ratio whether or not the
   canvas ever lands, so the layout around it never moves. */
.services-layout {
  display: grid;
  grid-template-columns: minmax(0, 0.92fr) minmax(0, 1.08fr);
  column-gap: clamp(32px, 5vw, 88px);
  align-items: start;
}

/* One column inside the left half: the cards stack instead of running
   3-up across the container.

   Stacked above the model, too. The canvas is wider than its own column
   (see .services-model .ng-kitchen below) and the headings can be placed
   anywhere over it, so the copy has to be the thing that answers the
   pointer where the two overlap — a click on a heading picks the heading,
   and a click anywhere else on the drawing picks the drawing. */
.services-layout .services-grid {
  position: relative;
  z-index: 1;
  grid-template-columns: minmax(0, 1fr);
  gap: clamp(26px, 3.4vw, 46px);
}

/* The measure comes from the column now, not the old 3-up card width. */
.services-layout .service-card p {
  max-width: 42ch;
}

/* The slot the kitchen block sits in. The block brings its own shape (16:9,
   floored at 420px), so there is nothing to set here but where it sits in
   the row — see section 16 for the block itself. */
.services-model {
  align-self: center;
}

/* Theming the block, through the three custom properties it reads. The block
   ships with an opaque navy ground of its own; here there is no ground at
   all. Even matched exactly to --canvas it still read as a panel, because a
   flat fill covers the CAD grid the rest of the page is drawn on. The model
   now clears to nothing and the grid runs through the void around the
   drawing, exactly as it does behind the Process model. The WebGL side of
   this is in js/kitchen-model.js (the renderer takes alpha and the scene has
   no background) — the two have to stay in step. */
.services-model .ng-kitchen {
  --site-model-ground: transparent;
  --site-model-ink: var(--text-strong);
  --site-model-muted: var(--accent);

  /* --- Size. The model is drawn 20% bigger than the column would give it,
     and the 20% is taken as canvas, not as zoom.

     The engine fits the room's half-WIDTH to the frame here (the drawing is
     2:1 and the frame is 1.4:1, so width is always the binding dimension —
     see resize() in js/kitchen-model.js). Two consequences, and they are the
     whole reason this rule looks the way it does:

       - How big the model is drawn depends on the canvas's width and
         nothing else. Extra height only adds empty air above and below it.
       - Zooming the camera in instead would not make it bigger, it would
         make it CROPPED: anything outside the frustum is never drawn, and
         no amount of CSS can show what the renderer did not draw. That is
         the boundary the model gets cut off at.

     So the canvas itself is 20% bigger. The width spills 10% into the column
     gap on one side and 10% into the page gutter on the other — both of them
     empty space the copy never uses — through negative margins, so the grid
     column and the copy beside it do not move at all. Symmetric on purpose:
     at the tightest desktop width (a viewport the size of the 1280px
     container itself) that still leaves the drawing clear of both the copy
     and the screen edge, and every wider viewport has more room, not less.

     The height goes up by the same 20% (420 -> 504, the block's own floor
     scaled), and that is not optional. Which dimension binds is decided per
     VIEW, not per model: the whole kitchen is 2:1 and binds on width, but
     the HVAC close-up is 1.4:1 and binds on height in a wider frame. Widen
     the canvas alone and that view grows by 0.6% while the other two grow by
     20%. Scaling both dimensions keeps the frame's aspect exactly as it was,
     so all three views come out at 1.2x and the framing of each is
     untouched. Keep the two numbers in step if either is retuned. */
  width: 120%;
  margin-inline: -10%;
  min-height: 504px;

  /* The block clips itself to its own box. That is the bounding box the
     model disappears into the moment anything moves it — nudge the stage in
     the editor and the far side is cut off against an edge that is not even
     visible, because this copy of the block has no ground and no frame to
     be an edge. Nothing here needs clipping, so nothing is clipped. */
  overflow: visible;
}

/* No pointer-events: none on the stage. It was here to stop the widened
   canvas swallowing clicks meant for the copy underneath it, but an element
   that does not answer the pointer cannot be picked up in the editor
   either — which is what locked the model in place. The copy wins the
   overlap by stacking order now (see .services-layout .services-grid
   above), and the drawing is grabbable everywhere else. */

/* --- Where the drawing stops -------------------------------------------
   The camera zooms into the hood and the water heater, and a close-up fills
   the canvas edge to edge — so wherever the canvas reaches, ink reaches.
   Sized and placed by hand the way this one is, that means ink running
   under the lede above it and the service headings below it.

   ONLY a close-up, though. Wide, the whole kitchen sits inside its canvas
   with room to spare and touches nothing, so there is nothing to crop and
   nothing to soften — a fade there just eats the top of the ductwork for
   no reason. All three values are driven to zero in the wide view (5f in
   js/main.js reads the block's data-view), which leaves the gradient
   opaque from edge to edge: a mask that does nothing.

   The crop is a mask rather than overflow: hidden, for two reasons. It can
   sit anywhere (the cut lines are measured off the actual text, in 5f of
   js/main.js, so they follow the copy when it is moved), and it can be
   soft — a hard edge across a drawing that has no frame reads as a
   rendering fault, where a fade reads as the drawing passing behind the
   page. --ngk-crop-top and --ngk-crop-bottom are written from there; the
   sides have no text to clear, so they fade at the canvas edge.

   Two elements, one axis each, deliberately: the vertical mask on the
   stage and the horizontal one on the canvas inside it. Masking both axes
   on one element needs mask-composite, which is the one part of masking
   that is still spelt differently in different engines. */

/* Registered, so the three of them are lengths the browser can interpolate
   rather than opaque strings it can only swap. That is what lets the crop
   arrive WITH the camera over half a second instead of snapping on under a
   drawing that has not started moving yet. Engines without @property skip
   the block and simply snap, which is the behaviour these had before. */
@property --ngk-crop-top {
  syntax: "<length>";
  inherits: false;
  initial-value: 0px;
}

@property --ngk-crop-bottom {
  syntax: "<length>";
  inherits: false;
  initial-value: 0px;
}

@property --ngk-fade {
  syntax: "<length>";
  inherits: true;
  initial-value: 0px;
}

.services-model .ng-kitchen__stage {
  --ngk-crop-top: 0px;
  --ngk-crop-bottom: 0px;
  --ngk-fade: 0px;
  /* Coming IN: arrive with the camera, which takes 1.4s but has covered
     most of the ground early (cubic ease-in-out). Half a second in is the
     crop already waiting for the drawing that is expanding into it. */
  transition: --ngk-crop-top 1.1s var(--ease-out),
    --ngk-crop-bottom 1.1s var(--ease-out), --ngk-fade 1.1s var(--ease-out);
  -webkit-mask-image: linear-gradient(
    to bottom,
    transparent var(--ngk-crop-top),
    #000 calc(var(--ngk-crop-top) + var(--ngk-fade)),
    #000 calc(100% - var(--ngk-crop-bottom) - var(--ngk-fade)),
    transparent calc(100% - var(--ngk-crop-bottom))
  );
  mask-image: linear-gradient(
    to bottom,
    transparent var(--ngk-crop-top),
    #000 calc(var(--ngk-crop-top) + var(--ngk-fade)),
    #000 calc(100% - var(--ngk-crop-bottom) - var(--ngk-fade)),
    transparent calc(100% - var(--ngk-crop-bottom))
  );
}

.services-model .ng-kitchen__stage canvas {
  -webkit-mask-image: linear-gradient(
    to right,
    transparent 0,
    #000 var(--ngk-fade),
    #000 calc(100% - var(--ngk-fade)),
    transparent 100%
  );
  mask-image: linear-gradient(
    to right,
    transparent 0,
    #000 var(--ngk-fade),
    #000 calc(100% - var(--ngk-fade)),
    transparent 100%
  );
}

/* Going back OUT is not the same move in reverse. The camera takes the same
   1.4 seconds either way, but on the way out the drawing is still close-up
   sized for most of them — release the crop on the way-in timing and it is
   gone half a second before the model has shrunk, which puts ink back over
   the copy for the rest of the pull-back.

   So the crop is held at full strength while the camera does the work, and
   only lets go at the end: the second of delay covers the tween (a second
   in, the eased camera is ~93% of the way home and the drawing is all but
   wide again), and then it takes a second more to dissolve. That last
   second is the part anyone sees, which is why it is a slow one — a crop
   that vanishes quickly reads as a glitch, where one that dissolves reads
   as the drawing coming back out from behind the page. The state flips at
   the click — the block writes data-view the moment focus() is called — so
   this counts from the same instant the camera starts. */
.services-model .ng-kitchen[data-view="wide"] .ng-kitchen__stage {
  transition: --ngk-crop-top 1s var(--ease-out) 1s,
    --ngk-crop-bottom 1s var(--ease-out) 1s,
    --ngk-fade 1s var(--ease-out) 1s;
}

@media (prefers-reduced-motion: reduce) {
  .services-model .ng-kitchen__stage,
  .services-model .ng-kitchen[data-view="wide"] .ng-kitchen__stage {
    transition: none;
  }
}

@media (max-width: 899px) {
  .services-layout {
    grid-template-columns: minmax(0, 1fr);
  }

  /* Stacked under the copy, with a beat between the two. */
  .services-model {
    margin-top: clamp(28px, 5vw, 44px);
  }

  /* Hand placement is width-agnostic: the editor writes one pixel offset
     that applies at every screen size, so a heading dropped 668px to the
     right of its column — which is where it belongs beside a model on a
     desktop — is 300px off the side of a phone, and <html> is
     overflow-x: clip, so it does not even scroll to. The service simply
     would not exist on mobile.

     Below the breakpoint the headings therefore go back to their own
     places and stack with their copy, which is the layout this column was
     built for anyway. Delete this rule if the placement is ever made
     narrow-safe; nothing else depends on it. */
  .services-grid .service-card h3 {
    translate: none !important;
    width: auto !important;
    max-width: 100% !important;
  }

  /* And with the headings back in a stack, the copy belongs back in the
     flow with them: out of flow it would open straight over the heading
     below it. Pushing what is underneath down the page is the right
     behaviour for a list of two — it is only wrong when the headings have
     been placed by hand, which is a thing that only happens above this
     breakpoint. */
  .service-card.is-collapsible .service-body {
    position: static;
  }

  /* The model is hand-sized in the same width-agnostic pixels: an 826px
     stage nudged 373px to the left is a model on a desktop and a model
     hanging most of the way off the left of a phone. It goes back to
     filling its own box down here, which is the box the section already
     sizes to the screen. Same deal as the headings above — delete this
     with that one. */
  .services-model .ng-kitchen__stage {
    translate: none !important;
    width: auto !important;
    min-height: 0 !important;
  }

  /* Stacked, the model already has the full column, so the 20% has nowhere
     to go but off the side of the screen — and <html> is overflow-x: clip,
     which would cut it off for real. Edge to edge instead: the widest the
     canvas can be without being cropped, which is the same trade made the
     same way, just against a smaller budget.

     The height is a ratio here rather than the desktop floor, and it is the
     one number on this model that has to be derived rather than chosen. The
     engine will only pull the camera back to 1.6x its view height (see
     resize() in js/kitchen-model.js); past that it stops fitting the width
     and starts cutting into it, which is why the model arrived on a phone
     with its side walls sliced off. The whole kitchen is 345 half-units
     wide against a 172 half-unit view, so the frame has to stay wider than
     345 / (172 * 1.6) = 1.2536:1 for the room to fit inside it at all.
     1.26 is that, with the rounding on the safe side. Anything squarer
     crops, however much width the canvas is given. */
  .services-model .ng-kitchen {
    width: calc(100% + var(--pad) * 2);
    margin-inline: calc(var(--pad) * -1);
    aspect-ratio: 1.26;
    min-height: 0;
  }
}

/* --- Tablet: model first, services under it ---------------------------
   A tablet has the room to show the kitchen at a size worth looking at,
   and once it is that big it wants to be the thing you meet — the two
   services read as captions to it rather than a list the drawing happens
   to follow. Order only: same markup, same source order for a screen
   reader and for a crawler, and the model keeps the full-bleed width and
   the 1.26 frame the stacked layout gives it.

   Only between these two widths. Below 600 the model is small enough that
   leading with it pushes the copy off the first screen, and from 900 up
   the layout is two columns and the question does not arise. */
@media (min-width: 600px) and (max-width: 899px) {
  .services-model {
    order: -1;
    /* Both bands taken back, not just the lower one.

       The frame is 1.26:1 and the room inside it is 2:1, so the canvas
       carries a band of empty air above the ceiling line as well as below
       the floor — the note below works out both at
       (1 / 1.26 - 172 / 345) / 2 = 14.76% of the width, about 121px at
       820. Only the lower one was ever claimed, which left the drawing
       hanging that far under the lede with nothing in between: the gap
       is inside the canvas, which is why no amount of margin on the text
       above ever closed it.

       +6px because the canvas already starts 6px above the lede's last
       line (the section head's margin collapses against it), so the
       corner needs 6px less than the full band to land ON that line
       rather than through it. */
    margin-top: calc(-14.76vw + 6px);
    /* Pulled up under the DRAWING rather than under the model's box, which
       are 120-odd pixels apart: the frame is 1.26:1 and the room inside it
       is 2:1, so the canvas carries a band of empty air below the floor
       line. Left alone, the two services sat below all of it and read as
       an unrelated list further down the page.

       The band is a fixed fraction of the canvas, and the canvas is the
       full width of the screen down here, so it can be named in vw and it
       tracks every width in this range: the room is 345 half-units wide
       against 172 high, which in a frame of 1.26 leaves
       (1 / 1.26 - 172 / 345) / 2 = 14.76% of the width above the room and
       the same below. Take that back, leave 12px, and the services sit
       just under the floor line wherever the tablet's width falls. */
    margin-bottom: calc(-14.76vw - 12px);
  }

  /* The canvas now reaches up over the heading's box. It is transparent
     there — that band is the empty air we just reclaimed — but it would
     still be the thing under the pointer, so the text is lifted over it
     the same way .services-grid already is at the other edge. */
  .services .section-head {
    z-index: 1;
  }

  /* Side by side, because there is width for it here and stacked they
     pushed each other down the page. In one row they open independently:
     the copy unfolds under its own heading and the other heading does not
     move at all. */
  .services-layout .services-grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    align-items: start;
  }
}

/* ==========================================================================
   8. SECTORS — full-bleed photo bands, one at a time
   ========================================================================== */
.sectors {
  position: relative;
  /* Transparent onto the shared canvas. Each band's photo fades back to
     --canvas at its own top and bottom edge (see .sector-band::after),
     so the grid runs right up to the photography and dissolves into it
     rather than being interrupted by a section-coloured block. */
  background: transparent;
  /* Matches .services' rhythm — this is now ordinary section padding,
     not clearance for a shadow that used to darken the top of the
     section (see the note below). */
  padding-top: clamp(64px, 8vw, 104px);
  padding-bottom: clamp(24px, 3vh, 44px);
  /* This section is the one that genuinely parks content outside the
     screen: the four bands that are not on are held a slide-width to the
     side, at +24px before the script picks a direction and +/-100% after.
     They are meant to be out there, so the fix is to contain them rather
     than move them — but on both properties, because `overflow: clip` on
     its own leaves an older iOS containing nothing and those bands then
     ARE the sideways drag. Shorthand first, so `clip` still wins wherever
     it is understood. */
  overflow: hidden;
  overflow: clip;
}

/* No seam shadow here either — the other half of the pair removed from
   .services above. See that note: with Process on the same canvas as its
   neighbours there is no raised edge for these to cast from, and the
   gradient only muddied the surface at the boundary. */

/* Progressive-fallback layout: without JS (or under reduced motion) every
   sector band is simply visible, stacked full-bleed top to bottom — the
   same experience the section had before any slideshow controls existed. */
.sectors-grid {
  position: relative;
  display: flex;
  flex-direction: column;
  margin-top: clamp(28px, 4vw, 48px);
}

/* Two-column header above (section 7) already carries the whole gap to
   the bands, so the grid adds none of its own — stacking both left a
   dead band of empty canvas between the lede and the first photo. */
@media (min-width: 900px) {
  .sectors-grid {
    margin-top: 0;
  }
}

.sector-band {
  position: relative;
  min-height: clamp(400px, 62vh, 620px);
  display: flex;
  align-items: flex-end;
  overflow: hidden;
  /* Paint containment, not just overflow. The media layer inside is
     promoted to its own compositor layer by the parallax transform, and a
     plain overflow:hidden on an unpromoted ancestor is a clip the
     compositor has to re-apply per frame — on a fast flick it can lag a
     frame behind the transform and let the layer's 12% overdraw (blown
     ceiling/sky on several of these photos) paint OUTSIDE the band, above
     its top edge, as a pale strip. contain:paint makes the band its own
     clipping/painting box so the overdraw can never escape it. */
  contain: paint;
  isolation: isolate;
}

/* Photo layer. Its own stacking context, so the frame's internal
   z-indexes (blueprint wash, colour hover twin) stay beneath the band's
   scrim and edge fades. */
.sector-band-media {
  position: absolute;
  /* No bleed by default. Below 900px the parallax does not run (see the
     matchMedia block in main.js), so overdraw here would only crop the
     photo tighter on the narrow layouts for nothing. The desktop rule
     below adds it back where it is actually needed. */
  inset: 0;
  z-index: 0;
  border-radius: 0;
  isolation: isolate;
  /* OPAQUE, and a flat colour rather than .img-frame's gradient. This is
     the fix for the white flash. Nothing on this page is white — but an
     unrastered compositor tile is. This layer is transformed every frame
     and clipped by the band, and Chrome will only backfill a tile it has
     not rastered yet with the layer's background when that background is
     a solid colour; the inherited gradient gave it nothing to use, so a
     tile that lost the race with a fast scroll came back blank. Painting
     it --canvas costs nothing visually (the photo covers this completely)
     and means the worst case is a frame of bare canvas instead of white. */
  background: var(--canvas);
  backface-visibility: hidden;
}

/* Promotion is scoped to the one band on screen. Holding all five
   promoted (which is what a bare will-change on .sector-band-media does)
   pins five full-bleed layers in raster memory at once so the slideshow
   can page between them — and under memory pressure the compositor
   evicts tiles, which is the very thing that produces the blank frame
   this is meant to prevent. GSAP's force3D:"auto" otherwise adds
   translate3d for the duration of a tween and strips it after, and that
   create/destroy churn is its own source of a one-frame flash on a
   scrubbed tween that is constantly starting and settling. */
.sector-band.is-current .sector-band-media {
  will-change: transform;
}

/* Desktop bleed for the parallax. The tween travels this layer ±6% of ITS
   OWN height, and at 12% bleed the layer is 1.24x the band, so the travel
   is ~7.4% of the band — the old 7% bleed left a measured 0.94px of cover
   at each extreme, i.e. a sub-pixel hairline at the band edge on a fast
   flick. 12% leaves ~27px of real margin at full travel. */
@media (min-width: 900px) {
  .sector-band-media {
    inset: -12% 0;
  }
}

.sector-band-media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* No filter. This used to carry saturate(1) contrast(0.94) brightness(1)
     to keep clipped ceilings off paper-white — but saturate(1) and
     brightness(1) are literal no-ops, and contrast(0.94) moves a 215
     highlight to 210, about 2%. For that it forced a filter render
     surface on the single largest element on the page, re-rastered on a
     layer that is already being transformed every frame. The edge fade
     below is what actually holds the highlights, and it does it without
     putting a full-viewport filter pass in the scroll path. */
}

/* Legibility scrim behind the copy — from the left on desktop. */
.sector-band::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  background: linear-gradient(
    90deg,
    rgba(18, 37, 58, 0.88) 0%,
    rgba(18, 37, 58, 0.66) 34%,
    rgba(18, 37, 58, 0.18) 68%,
    rgba(18, 37, 58, 0.14) 100%
  );
}

/* HARD EDGE. The band used to dissolve into the canvas top and bottom
   behind a long alpha ramp. That ramp is gone: the photo now ends on a
   clean cut at the band's own box, which is what .sector-band's
   contain:paint clips it to.

   Two things this makes load-bearing rather than cosmetic. The 12%
   parallax overdraw is now the only thing standing between a fast scroll
   and a visible hairline of canvas along the edge — there is no fade left
   to hide a shortfall in, so that margin must not be trimmed. And the
   opaque background on .sector-band-media is what the cut resolves to if
   a tile has not rastered yet, instead of white.

   The left-to-right legibility scrim (.sector-band::before) is untouched;
   it is what keeps the copy readable, and it never had anything to do
   with the edges. */


.sector-band-inner {
  position: relative;
  z-index: 3;
  width: 100%;
  padding-bottom: clamp(32px, 5vw, 56px);
}

.sector-band-inner h3 {
  font-size: clamp(28px, 3.6vw, 44px);
  max-width: 18ch;
  margin-bottom: 12px;
  text-wrap: balance;
}

.sector-band-inner p {
  font-size: 17px;
  color: var(--text);
  max-width: 42ch;
}

/* True colour always on — no hover reveal, no duotone wash underneath.
   The blueprint-tinted default photo and its wash are gone entirely; this
   is the same permanent look the hover state used to unlock. */
.sector-band .img-color {
  opacity: 1;
}

/* Slideshow mode — JS adds .is-slides and stacks every sector into the
   same grid cell, exactly like the services pair does with two columns;
   here it's one full-bleed slide at a time, so there's just the one. */
.sectors-grid.is-slides {
  display: grid;
}

.sectors-grid.is-slides > .sector-band {
  grid-area: 1 / 1;
  opacity: 0;
  visibility: hidden;
  /* --slide-x is set from JS per item, right before it enters or leaves,
     to +/-100% depending on travel direction — see initSlideshow() in
     main.js. Same slide+fade combo as the Services pager. */
  transform: translateX(var(--slide-x, 24px));
  transition: opacity 0.7s var(--ease-out), transform 0.7s var(--ease-out),
    visibility 0.7s step-end;
}

.sectors-grid.is-slides > .sector-band.is-current {
  opacity: 1;
  visibility: visible;
  transform: none;
  pointer-events: auto;
  transition: opacity 0.7s var(--ease-out), transform 0.7s var(--ease-out),
    visibility 0s;
}

/* The chevrons used to float on top of the full-bleed photo, pinned to its
   left and right edges — which, for an edge-to-edge band, means pinned to
   the two edges of the SCREEN: stepping through the sectors meant crossing
   the whole viewport between clicks. They sit in the control row under the
   band now (prev — dashes — next, .sectors-controls), so both directions
   are one small movement apart and all three of this page's slideshows put
   their controls in the same place. No box behind them either way. (A
   drop-shadow filter sat here briefly for contrast, but CSS filters
   rasterize the element they're applied to, and that rasterized copy is
   what was reading as soft/pixelated — dropped in favour of the same flat,
   unfiltered treatment Services uses.) */
.sectors-controls {
  display: flex;
  align-items: center;
  justify-content: center;
  /* Wider than the gap between the dashes themselves, so the row reads as
     [chevron] [the dash group] [chevron] rather than six evenly spaced
     controls. */
  gap: clamp(14px, 1.8vw, 28px);
  margin: clamp(20px, 2.6vw, 32px) 0 0;
}

/* The row owns the top margin now; the dashes inside it must not add a
   second one. */
.sectors-controls .sectors-pager {
  margin: 0;
}

/* Same hit target as before — only the drawn chevron steps down. Against a
   full-bleed photo the glyph had a whole band to hold its own against; in a
   row with 3px dashes that same size reads as two oversized brackets around
   the pager. The hit square is untouched, so nothing gets harder to click. */
.sectors-controls .sectors-arrow {
  --arrow-icon: 0.56;
}

/* ==========================================================================
   9. PROCESS — full-bleed 4-phase HVAC model. Phase advances on click
      (see .process-pager in main.js) rather than off scroll position —
      the section is a normal-height block now, not a pinned runway.
   ========================================================================== */
.process {
  position: relative;
  /* A third of the run-up the other sections get, and the matching half of
     the gap Services now leaves under itself (see .services). Services is
     the one section whose foot moves — it opens to let a service's copy
     out and shuts again — so the air between the two is one distance, and
     a full section's rhythm on both sides of it was several times what it
     should be. Keep the two figures identical: the gap is meant to read as
     evenly split between the section that ends and the one that starts. */
  padding-top: clamp(22px, 2.6vw, 34px);
  /* This section's colour + drafting grid are what the whole page now
     wears — they moved to main / main::before, so there is nothing left
     to declare here. */
  background: transparent;
}

/* The header sits in the site's centred container, like Services' and
   Sectors' — so it stays optically centred on the page at any window width
   instead of stretching to the gutters with the full-bleed stage below it.
   That does mean its left edge and the caption column's don't share a line
   past 1280px; centring the header block was the call. */

.process-pin {
  position: relative;
}

/* Pulls the stage up under the header. The distance from the last line of
     the lede to the top of the drawing is the header's own bottom margin
     plus the model's nav clearance inside the stage — around 140px, which
     read as a break between two sections rather than a header and the thing
     it introduces. This halves it. The clearance itself can't be the part
     that gives: the stage still reaches the top of the viewport on scroll,
     and the fixed nav would clip the top of the drawing without it.
   Desktop only: the stacked layout below 900px carries no nav clearance
   inside the stage, so there is no surplus there to take back. */
@media (min-width: 900px) {
  .process-pin {
    margin-top: calc(-1 * clamp(56px, 4.6vw, 70px));
  }
}

.process-stage-sticky {
  position: relative;
  /* A viewport less a glimpse — not the full 100svh it held when the
     section header was overlaid inside it. The header is a block above the
     stage now, so a full-height stage would put the two exactly one
     viewport apart: every scroll position either shows the header with the
     drawing running off the bottom of the screen, or the drawing framed
     with no sign of the section it belongs to. Trimming the stage brings
     the foot of the header back into the frame with it, and costs the
     drawing nothing it was using — the box is width-limited from 1440 down
     anyway. */
  height: clamp(520px, 84svh, 940px);
  overflow: hidden;
  /* The band of clear sheet under the drawing. It was the dash pager's
     clearance; the pager is gone (prev/next moved up under the caption)
     and the band stays as the frame's own foot, so the drawing keeps the
     height and place it had. fitProcessModel mirrors these numbers when it
     measures the model's bottom inset — keep the two in step. */
  --foot-band: clamp(72px, 10.4vh, 116px);
}

/* Both the fill and the drafting grid this element used to carry are now
   the page-wide canvas (see main / main::before). Kept as an empty,
   transparent layer rather than deleted so the stage's z-order and the
   model's "no card, no frame" comment below still describe the real
   stack. Its parallax drift went with it: a grid that scrolled at its
   own rate here would visibly break alignment with the same grid running
   through every section above and below. */
.process-bg {
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
}

/* The model itself — no card, no frame, no background fill. Transparent
   canvas (alpha:true in hvac-scene.js), so .process-bg / the grid show
   straight through the void around the drawing. */
.process-model {
  position: absolute;
  /* The left-hand side of the stage: the phase caption owns the right
     column (.process-steps) and the dashes the foot, so the model gets
     everything short of the one and above the other. fitProcessModel() in
     main.js sets the real insets, measured off those two boxes and
     trimmed to the plan's own aspect; these values only cover the instant
     before it runs and are kept in step with the column width above. */
  left: var(--pad);
  right: min(38vw, 460px);
  top: clamp(96px, 13vh, 152px);
  bottom: clamp(120px, 16vh, 180px);
  z-index: 1;
}

.process-model canvas {
  display: block;
  cursor: grab;
  touch-action: pan-y;
}

.process-model.is-dragging canvas {
  cursor: grabbing;
}

.hvac-labels {
  position: absolute;
  inset: 0;
  pointer-events: none;
  overflow: hidden;
}

.hvac-tag {
  font-family: "Roboto Mono", ui-monospace, monospace;
  font-size: 8.5px;
  line-height: 1.25;
  letter-spacing: 0.06em;
  white-space: pre;
  text-transform: uppercase;
  color: #9fd4f5;
  opacity: var(--tag-op, 0);
  padding: 1px 0 1px 5px;
  border-left: 1px solid rgba(120, 200, 245, 0.45);
  text-shadow: 0 0 6px rgba(6, 22, 40, 0.95), 0 0 2px rgba(6, 22, 40, 1);
  transition: opacity 0.1s linear;
}

.hvac-tag.is-supply { color: #7fe4ff; border-color: rgba(79, 216, 255, 0.5); }
.hvac-tag.is-return { color: #ffd190; border-color: rgba(255, 194, 102, 0.5); }
.hvac-tag.is-exhaust { color: #ff9a80; border-color: rgba(255, 111, 77, 0.5); }
.hvac-tag.is-water { color: #8ff5dd; border-color: rgba(92, 240, 207, 0.5); }
.hvac-tag.is-waste { color: #cbbcff; border-color: rgba(182, 156, 255, 0.5); }
.hvac-tag.is-gas { color: #ffe999; border-color: rgba(255, 224, 102, 0.5); }
.hvac-tag.is-term { color: #a9eeff; font-size: 8px; }
.hvac-tag.is-equip { color: #ffd9a3; font-weight: 500; }
.hvac-tag.is-note { color: #7ba8c9; font-size: 7.5px; opacity: calc(var(--tag-op, 0) * 0.8); }

/* No internal top/bottom scrim on the stage itself any more — the shadow
   that used to darken this section's own edges now lives on Services'
   and Sectors' facing edges instead (.services::after, .sectors::before),
   so the grid section reads as the one casting shadow over its neighbours
   rather than sitting in a shadow cast onto it. process-steps/-pager
   already clear 10-14:1 contrast on --paper alone (see token comments
   above), so they don't need a scrim behind them to stay legible. */

/* Step captions — one at a time, stacked in the same grid cell like the
   Services/Sectors slideshows, so all four stay in the DOM for crawlers
   and only the current one is visible. main.js toggles .is-current to
   match the model's phase.

   Top-left of the stage — the column the section header used to occupy,
   before it moved out to the container above. The caption is the only copy
   left on the stage now, so it takes the header's old place beside the
   drawing rather than sitting under it: the model runs the full height of
   the stage out on the right, and the phase it is showing is named at eye
   level next to it instead of down at the foot of the frame.

   Fixed-width column (not shrink-to-fit) on purpose: fitProcessModel
   measures the model's left edge off this element, so a width that changed
   with the length of the current step's heading would resize the drawing on
   every phase change. */
.process-steps {
  position: absolute;
  /* Aligned to the CONTAINER's right gutter, not the stage's. The stage is
     full-bleed, so right:0 + var(--pad) would push the caption ~320px past
     the section header above it on a 1920 screen — the copy would read as
     belonging to the page edge rather than to the header and the drawing it
     sits between. This walks the column in to where the header's measure
     ends (the .container's 1280px measure, centred, plus its own gutter) and
     costs the model nothing: its box is trimmed to the plan's aspect and
     anchored left, so the surplus this takes was empty canvas. Inert
     below 900px, where the column goes position:static and stacks.

     Right, not left: the drawing takes the left of the sheet and the copy
     answers it from the right. The gutter therefore sits on the column's
     OUTER (right) edge, and the copy still starts at the column's inner
     edge — which is what fitProcessModel() measures the model's right-hand
     limit against, and what the chevron row under the caption lines up on. */
  right: max(0px, calc((100% - 1280px) / 2));
  top: 0;
  z-index: 3;
  width: min(38vw, 460px);
  padding: 0 var(--pad) 0 0;
  /* Centred on the model: fitProcessModel sets `top` to the middle of the
     drawing's box and this lifts the block back by half its own height.
     The fallback below covers the instant before that runs and reduced
     motion, where the model is never measured — the middle of the stage is
     within a dozen pixels of the middle of the model's box. */
  top: 50%;
  transform: translateY(-50%);
  display: grid;
  justify-items: start;
}

/* Click-through for the 4 phases — built by main.js. Prev and next, in one
   row directly under the phase caption in the right column, so the control
   sits with the copy it steps through instead of straddling the drawing
   from the stage's two edges. fitProcessModel() hands this element its
   left/top as inline styles, measured off the caption's real box (and
   clears this `right` when it does, so the row is placed from one edge
   rather than stretched between two); the fallback below covers the instant
   before that runs. Above the model's z-index so a click lands on a chevron
   rather than starting a drag on the canvas. */
.process-controls {
  position: absolute;
  right: max(0px, calc((100% - 1280px) / 2));
  top: 50%;
  z-index: 4;
  display: flex;
  align-items: center;
  gap: clamp(4px, 0.8vw, 12px);
}

/* In flow inside that row, not overlaid on the stage — the absolute
   placement belongs to the row now, and the buttons just sit in it. */
.process-controls .process-arrow {
  position: static;
  transform: none;
}

/* The index and the heading are one line (.process-step-line) above the
   paragraph, and the whole block hangs off the page gutter — the same left
   edge the section header above it and every other block on the page use. */
.process-step {
  grid-area: 1 / 1;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  /* The column's own width is the measure now (see .process-steps). */
  max-width: none;
  opacity: 0;
  visibility: hidden;
  transition: opacity 0.28s var(--ease-out), visibility 0.28s step-end;
}

.process-step-line {
  display: flex;
  gap: 18px;
  align-items: flex-start;
}

/* 0.28s, down from 0.5s: the caption and the model are answering the same
   click, so the copy crossfade has to sit inside the phase animation rather
   than still resolving after the model has finished drawing. */
.process-step.is-current {
  opacity: 1;
  visibility: visible;
  transition: opacity 0.28s var(--ease-out), visibility 0s;
}

/* Sized up alongside the caption heading below it — at 15px against a
   56px h3 the number read as a stray footnote mark rather than the
   caption's index. line-height puts its baseline near the heading's cap
   height, which is what makes the pair sit as one unit. */
.process-num {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: clamp(15px, 1.5vw, 20px);
  letter-spacing: 0.02em;
  color: var(--accent-mid);
  line-height: 2.4;
  flex-shrink: 0;
  min-width: 2ch;
}

/* A tier under the section h2 above it, not level with it: the heading and
   this caption used to face each other across the model as equals, but the
   header sits directly above the stage now and two blocks of identical
   display type stacked that close read as two competing titles rather than
   a section and the phase inside it. Still well clear of the body copy
   beneath it, so the caption keeps its weight beside the drawing. */
.process-step h3 {
  font-size: clamp(28px, 3.1vw, 42px);
  line-height: 1.05;
  letter-spacing: -0.01em;
  margin-bottom: 10px;
}

.process-step p {
  font-size: 16px;
  color: var(--text);
  max-width: 50ch;
}

/* The caption's parts carry no offsets of their own: .process-steps places
   the column and the four captions stack in its single grid cell, so a
   number, heading or paragraph nudged out of that stack is out of the
   caption.

   Held against per-element translates on purpose, and the reason is worth
   writing down. The copy used to sit on the LEFT of the stage and was
   dragged ~775px across it by hand, element by element, to try the
   right-hand arrangement; the column was then moved right properly in CSS
   (see .process-steps). The hand-drags outlive that — they are in the
   visual editor's own store, and it re-emits them into mb-overrides.css
   after any edit, so deleting them there lasts until the next save. Landing
   on top of the column's new place they put three of the four captions,
   and Consult's paragraph, out past x≈1676 on a 1440 sheet, where the
   stage's overflow:hidden clips them: the section showed "01 Consult" and
   nothing else for the other three phases.

   Specificity, not just !important — the editor's rules are
   [data-mb="..."] { translate: … !important }, which a bare .process-step p
   would tie with and lose to on order. */
.process-steps .process-step,
.process-steps .process-step .process-step-line,
.process-steps .process-step .process-num,
.process-steps .process-step h3,
.process-steps .process-step p {
  translate: none !important;
}

/* ==========================================================================
   10. ABOUT
   ========================================================================== */
.about {
  position: relative;
  overflow: clip;
  /* Tight top: this section continues straight out of Sectors, which
     now fades to the same paper tone this section sits on, rather than
     sitting after a gap and a colour jump. */
  padding: clamp(24px, 3vw, 44px) 0 clamp(90px, 12vw, 150px);
  /* Transparent onto the shared canvas. The rooftop photo below is
     masked out on every edge, so the grid shows through around it and
     the picture reads as texture rising out of the surface. */
  background: transparent;
  contain: layout paint;
}

/* Rooftop photograph as a quiet backdrop for the principal's section,
   fading out on every edge so it reads as texture, not as an image. */
.about-bg {
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
}

.about-silhouette {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  width: 100%;
  height: 62%;
  object-fit: cover;
  opacity: 0.18;
  /* Fades on BOTH edges: anchored to the section bottom, a one-sided
     mask left a hard horizontal cut where Testimonials begins. */
  -webkit-mask-image: linear-gradient(
    180deg,
    transparent 0%,
    #000 42%,
    #000 68%,
    transparent 100%
  );
  mask-image: linear-gradient(
    180deg,
    transparent 0%,
    #000 42%,
    #000 68%,
    transparent 100%
  );
}

.about-grid {
  position: relative;
  z-index: 2;
  display: grid;
  grid-template-columns: minmax(0, 0.85fr) minmax(0, 1.15fr);
  gap: clamp(32px, 6vw, 88px);
  align-items: center;
}

.about-photo-frame {
  aspect-ratio: 3 / 4;
  /* Frameless: the photograph is the content, it needs no chrome. */
  border-radius: 4px;
  /* Contain the duotone blend below to this frame's own subtree */
  isolation: isolate;
}

/* :not(.img-color) matters — the hover twin must NOT be desaturated,
   or hovering the portrait would reveal a grayscaled colour photo and
   look like nothing happened. */
.about-photo-frame img:not(.img-color) {
  /* Slight lift so the B&W portrait separates from the blue page */
  filter: grayscale(1) contrast(1.03) brightness(1.06);
}

/* The portrait stays black & white as the client asked; this is only a
   whisper of steel so it belongs to the same family as the blue-duotone
   sector photos. Static element; the reveal tween animates the parent
   figure once, not this frame's paint. */
.about-photo-frame::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
  background: var(--steel-500);
  mix-blend-mode: color;
  opacity: 0.16;
  pointer-events: none;
}

.about-copy {
  display: flex;
  flex-direction: column;
  gap: 18px;
  align-items: flex-start;
}

.about-copy p {
  color: var(--text);
}

.about-creds {
  margin-top: 8px;
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.about-creds li {
  position: relative;
  padding-left: 26px;
  color: var(--text-strong);
  font-weight: 500;
  font-size: 15px;
}

.about-creds li::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0.52em;
  width: 12px;
  height: 2px;
  background: var(--accent-mid);
}

/* ==========================================================================
   10b. PARTNERS CAROUSEL (index.html) — the relationships logo wall as a
   single looping lane, sitting between About and Contact.

   Pure CSS, no JS: the track holds the same 22 logos twice and slides
   exactly one set-width (-50%) before restarting, so the seam never
   shows. Every logo is a real link in the markup, so the content is there
   with or without motion — under reduced motion the animation is dropped,
   the duplicate set is hidden and the lane falls back to a wrapped static
   wall, the same treatment the relationships page uses. Logos reuse
   .partner / .partner-mono / .partner-colour from section 15, so a tile
   here rests at 60% white and swaps to full colour on hover exactly as
   it does there.
   ========================================================================== */
.partners {
  padding: clamp(10px, 2vw, 28px) 0 clamp(24px, 3vw, 40px);
  contain: layout paint;
}

/* The lane leads with the heading, so the header block is tighter than
   the site's default section rhythm. */
.partners-head {
  margin-bottom: clamp(24px, 3vw, 38px);
}

/* Full-bleed lane: the logos run out past the container on both sides and
   dissolve into the page rather than stopping at a hard edge. */
.partners-marquee {
  overflow: hidden;
  padding: clamp(6px, 1vw, 14px) 0;
  -webkit-mask-image: linear-gradient(
    90deg,
    transparent 0,
    #000 clamp(48px, 8vw, 140px),
    #000 calc(100% - clamp(48px, 8vw, 140px)),
    transparent 100%
  );
  mask-image: linear-gradient(
    90deg,
    transparent 0,
    #000 clamp(48px, 8vw, 140px),
    #000 calc(100% - clamp(48px, 8vw, 140px)),
    transparent 100%
  );
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
}

.partners-track {
  display: flex;
  width: max-content;
  /* The lap time, not the pace, is what this number sets: one -50% pass is
     exactly one set, so a longer set at the same duration travels faster.
     The lane's pace is 58.7px/s — 22% slower than the original 60s pass
     (60 / 0.78 = 76.9s) over the 4513px set it was written for. The set is
     6511px now that it carries all 32 firms, so the duration follows it:
     6511 / 58.7 = 110.9s. Add a logo and this has to be re-derived, or the
     belt quietly speeds up.

     One duration covers every width. The set scales with the breakpoints
     (--logo-scale drops to 0.72 under 900px, and the gap with it), so the
     same number of seconds is the same px/s everywhere. */
  animation: partners-scroll 110.9s linear infinite;
}

/* Each set carries its own trailing gap so the spacing across the seam
   matches the spacing inside a set. */
.partners-set {
  --partners-gap: clamp(40px, 5vw, 76px);
  /* .partner sizes itself off --lw * --logo-scale (section 15). .rel-grid
     declares the scale on the relationships page; the lane has to declare
     its own or the width calc goes invalid and every mark falls back to
     its natural pixel size. */
  --logo-scale: 1;
  display: flex;
  align-items: center;
  gap: var(--partners-gap);
  padding-right: var(--partners-gap);
}

@keyframes partners-scroll {
  from {
    transform: translateX(0);
  }
  to {
    transform: translateX(-50%);
  }
}

/* The lane never stops. No hover pause, no focus pause, nothing that can
   leave it parked.

   There was a :focus-within pause here, kept for a keyboard user whose
   focused logo would otherwise slide out from under its ring. It had to go,
   because focus is not only a keyboard thing: clicking a mark focuses it,
   so press-and-hold stopped the belt, and following a logo out to its own
   site and coming back restored that focus — which left the lane stopped
   for good, with nothing on screen explaining why or how to restart it.

   The only thing that still stands the belt down is
   prefers-reduced-motion, which replaces it with a static wrapped logo wall
   (section 14) rather than pausing it mid-travel. That is a project rule
   and an accessibility one, and is not what this note is about. */

/* Same 0.72 step-down the relationships grid takes below 900px, so a
   phone gets two or three marks in the lane instead of one and a half.
   The edge fades shrink with it — 140px of dissolve on a 390px screen is
   most of the lane. */
@media (max-width: 899px) {
  .partners-set {
    --logo-scale: 0.72;
    --partners-gap: clamp(28px, 7vw, 44px);
  }

  .partners-marquee {
    -webkit-mask-image: linear-gradient(
      90deg,
      transparent 0,
      #000 26px,
      #000 calc(100% - 26px),
      transparent 100%
    );
    mask-image: linear-gradient(
      90deg,
      transparent 0,
      #000 26px,
      #000 calc(100% - 26px),
      transparent 100%
    );
  }
}

/* --- "See who we work with" bar -------------------------------------
   A hairline-ruled strip under the lane, the full width of the page and
   clickable end to end, pointing at the relationships page where the
   same firms are listed by trade. */
.partners-bar {
  display: block;
  border-top: 1px solid var(--line);
  border-bottom: 1px solid var(--line);
  text-decoration: none;
  transition: background-color 0.3s var(--ease-out);
}

.partners-bar:hover,
.partners-bar:focus-visible {
  background: rgba(127, 168, 205, 0.07);
}

.partners-bar:focus-visible {
  outline: 2px solid var(--accent-mid);
  outline-offset: -2px;
}

.partners-bar-inner {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 12px;
  padding-top: clamp(18px, 2.4vw, 28px);
  padding-bottom: clamp(18px, 2.4vw, 28px);
  font-family: var(--font-display);
  font-size: clamp(14px, 1.5vw, 18px);
  font-weight: 600;
  letter-spacing: 0.02em;
  color: var(--text-strong);
  transition: color 0.3s var(--ease-out);
}

.partners-bar-inner svg {
  flex: 0 0 auto;
  color: var(--accent);
  transition: transform 0.3s var(--ease-out);
}

.partners-bar:hover .partners-bar-inner svg,
.partners-bar:focus-visible .partners-bar-inner svg {
  transform: translateX(5px);
}

/* ==========================================================================
   10b. FAQ — its own page (faq.html)
   ========================================================================== */
/* Ruled rows on the shared canvas — the same hairline grouping the
   contact details use, no boxes and no fills. Every row ships open in the
   markup, so every answer is in the DOM and on the page with JS off or
   disabled.

   The list used to be a section on the home page; it is a page of its own
   now, so the heading it sat under is the page's <h1> in .faq-hero and the
   rows themselves carry the <h2>s. Top padding is smaller than the old
   section's because the hero above it already opens the page. */
.faq {
  position: relative;
  padding: clamp(24px, 3vw, 40px) 0 clamp(24px, 3vw, 40px);
}

/* Same measure and rhythm as the Relationships page hero, so the two
   subpages open the same way under the nav. */
.faq-hero {
  padding: calc(var(--nav-h) + clamp(56px, 9vw, 120px)) 0 clamp(32px, 4vw, 56px);
}

.faq-hero h1 {
  font-size: clamp(38px, 5.4vw, 68px);
  max-width: 22ch;
  text-wrap: balance;
  margin-top: 14px;
}

.faq-hero .section-lede {
  max-width: 58ch;
  margin-top: 20px;
}

.faq-list {
  max-width: 900px;
  /* The closing hairline under the last row. */
  border-bottom: 1px solid var(--line);
}

.faq-item {
  border-top: 1px solid var(--line);
}

/* The whole question row is the control — a real <button>, filling the
   heading that wraps it, so the row is operable from the keyboard without
   a line of script. The reset is the usual one a button needs before it
   can be a row of type: the UA's own chrome off, the heading's type
   inherited in. */
.faq-q {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: clamp(16px, 3vw, 40px);
  width: 100%;
  margin: 0;
  padding: clamp(18px, 2.2vw, 26px) 0;
  border: 0;
  background: none;
  -webkit-appearance: none;
  appearance: none;
  font: inherit;
  color: inherit;
  text-align: left;
  cursor: pointer;
  transition: color 0.3s var(--ease-out);
}

/* h2 since the FAQ became a page of its own — the <h1> is the hero above,
   so each question is the next level down and nothing is skipped. The
   heading is the wrapper now and the button inside it inherits this type;
   .faq-q h2/h3 stay in the selector so the rows still look right if the
   list is ever dropped back inside a section that has its own h2. */
.faq-q-h,
.faq-q h2,
.faq-q h3 {
  font-family: var(--font-display);
  font-size: clamp(16px, 1.5vw, 20px);
  font-weight: 600;
  line-height: 1.35;
  letter-spacing: -0.01em;
  color: var(--text-strong);
  transition: color 0.3s var(--ease-out);
  /* Both undo the global h2 rule, which is written for section headings:
     18ch would fold a question onto three lines inside a 900px row, and
     balancing a one-line question just moves the break around. */
  max-width: none;
  text-wrap: normal;
}

/* .is-open is the one open state now: it is in the markup on every row
   that ships open, and main.js takes it off and puts it back as the fold
   runs. The colour is set on the button rather than on the heading around
   it because the question text lives in the button, and the heading hands
   its type down to it. */
.faq-q:hover,
.faq-item.is-open .faq-q,
.faq-q:hover h2,
.faq-q:hover h3,
.faq-item.is-open .faq-q h2,
.faq-item.is-open .faq-q h3 {
  color: var(--accent);
}

.faq-q:focus-visible {
  outline: 2px solid var(--steel-400);
  outline-offset: 4px;
  border-radius: 2px;
}

/* The same sign the service headings carry (see .service-switch above):
   two bars on one centre, 15x2, held at one constant blue, rotating 45deg
   into an X while the row is open and unwinding as it shuts. Same bars,
   same weight, same curve — the only difference is that here they hang off
   a real <span> at the end of the question rather than a pseudo. */
.faq-mark {
  position: relative;
  flex: none;
  width: 15px;
  height: 15px;
  /* Nudges the mark onto the cap-height of the first line of the
     question rather than its baseline. */
  align-self: flex-start;
  margin-top: 0.28em;
}

.faq-mark::before,
.faq-mark::after {
  content: "";
  position: absolute;
  top: 50%;
  left: 0;
  width: 100%;
  height: 2px;
  margin-top: -1px;
  background: var(--accent-mid);
  transition: transform 0.45s var(--ease-out);
}

.faq-mark::after {
  transform: rotate(90deg);
}

.faq-item.is-open .faq-mark::before {
  transform: rotate(45deg);
}

.faq-item.is-open .faq-mark::after {
  transform: rotate(135deg);
}

/* --- The answer each question folds away ------------------------------
   The same fold the service copy uses (see .service-body above), down to
   the curve and the durations: a grid row animated between 0px and the
   measured height of the copy, the copy itself riding down inside its own
   window, and the fade timed differently opening and shutting.

   Collapsed with a grid row rather than a height so nothing here depends
   on a measured pixel value to be CORRECT — only to overshoot.
   .is-collapsible is written by main.js (5f) and means "this row folds";
   .is-open ships in the markup and is then owned by the script. A row
   without .is-collapsible does not fold at all: it shows its answer, or
   hides it outright in the rule below, which is what a no-JS visitor gets
   and what a reduced-motion visitor is given deliberately.

   One difference from Services, and it is a layout difference rather than
   a motion one: this window stays in the flow. The service copy is out of
   flow because its headings are placed by hand and an opening card would
   otherwise shove the one below it; a list of ruled rows is the opposite —
   the rows below SHOULD travel down as one opens, and they carry the
   window's overshoot with them. */
.faq-body {
  display: grid;
  grid-template-rows: 1fr;
}

/* The no-fold path, and the one thing <details> used to do for nothing:
   a row that is not collapsible is either showing its answer or not
   showing it, with no travel in between. Reduced motion lands here, and
   so does the frame before main.js has marked the rows. Since every row
   ships .is-open, "not showing it" is only ever a row a visitor has shut
   themselves — there is no state in which the page arrives with an answer
   hidden. */
.faq-item:not(.is-collapsible):not(.is-open) .faq-body {
  display: none;
}

.faq-item.is-collapsible .faq-body {
  /* 0px rather than 0fr: the open state is a measured pixel height, and a
     track cannot be interpolated from an fr to a length — the browser
     swaps the two at the halfway mark, which is a fold that jumps open
     with no travel at all. Both ends in the same units, and it
     interpolates. Same reasoning as the service fold. */
  grid-template-rows: 0px;
  opacity: 0;
  overflow: hidden;
  /* Shut, the fade runs late and slow: the answer stays readable for the
     first stretch of the roll-up and dissolves on the way, rather than
     blinking out and leaving an empty blind to close. */
  transition: grid-template-rows 1s var(--ease-unfold),
    opacity 0.55s var(--ease-out) 0.2s;
}

/* Open on the measured height rather than 1fr, because an fr track cannot
   be pushed past its content: interpolated beyond 1fr it simply stops, and
   the overshoot in --ease-unfold would be thrown away. In pixels the
   window runs a few percent past its mark and settles back, which is the
   bounce the service copy has. main.js (5f) keeps --faq-body-h measured;
   the 1fr fallback only stands in if a row is somehow opened before it has
   been measured once, and opens without travelling.

   Opening, the fade is short and immediate: the text is legible almost as
   soon as the window starts to move, so what you watch is copy rolling
   down rather than a gap opening and text appearing in it afterwards. */
.faq-item.is-collapsible.is-open .faq-body {
  grid-template-rows: var(--faq-body-h, 1fr);
  opacity: 1;
  transition: grid-template-rows 1s var(--ease-unfold),
    opacity 0.42s var(--ease-out);
}

.faq-a {
  padding: 0 clamp(16px, 3vw, 40px) clamp(22px, 2.6vw, 30px) 0;
  max-width: 68ch;
  /* A grid item will not shrink below its content unless told to. start
     rather than the default stretch so the copy is only ever as tall as
     itself: stretched, it grows with the row, and main.js measures its
     scrollHeight — which would feed the overshoot straight back in as a
     new target and never settle. */
  min-height: 0;
  align-self: start;
}

/* The copy rides down inside its own window instead of being stretched
   into place: shut, it sits a few pixels above where it belongs and is
   clipped by the blind above it, so opening reads as the answer rolling
   out from under the question and settling. Same curve and the same
   duration as the window — the two are one movement, and retiming one
   means retiming both. */
.faq-item.is-collapsible .faq-a {
  transform: translateY(-14px);
  transition: transform 1s var(--ease-unfold);
}

.faq-item.is-collapsible.is-open .faq-a {
  transform: none;
}

.faq-a p {
  color: var(--text);
}

.faq-a a {
  color: var(--accent);
  text-decoration: underline;
  text-underline-offset: 3px;
  text-decoration-thickness: 1px;
  transition: color 0.25s var(--ease-out);
}

.faq-a a:hover {
  color: var(--text-strong);
}

/* No fold at all: main.js leaves .is-collapsible off under reduced
   motion, so the rows show and hide their answers instantly. This is
   belt and braces for the case where the class is on the element and the
   preference is switched on afterwards, mid-session. */
@media (prefers-reduced-motion: reduce) {
  .faq-item.is-collapsible .faq-body,
  .faq-item.is-collapsible.is-open .faq-body {
    transition: none;
  }

  .faq-item.is-collapsible .faq-a {
    transform: none;
    transition: none;
  }
}

@media (max-width: 700px) {
  .faq-q {
    align-items: flex-start;
  }
}

/* ==========================================================================
   11. CONTACT
   ========================================================================== */
/* The page's last section before the footer's hard cut. The steel glow
   at its foot stays — it sits ON the canvas now instead of on top of an
   opaque --paper fill, so the grid still reads through it. */
.contact {
  padding: clamp(64px, 8vw, 108px) 0 clamp(56px, 7vw, 96px);
  background:
    radial-gradient(70% 60% at 50% 110%, rgba(51, 96, 143, 0.12), transparent 70%);
  contain: layout paint;
}

/* The headline sits in the left column now, level with the top of the
   form, so it is sized to the column rather than to the page: big enough
   to still open the section, small enough that only the explicit <br> in
   the markup breaks the line. It goes back up to page scale once the
   grid stacks and it has the full width again. */
.contact-big {
  font-size: clamp(30px, 3.7vw, 52px);
  margin: -0.12em 0 clamp(18px, 2vw, 26px);
}

/* Copy + direct details on the left, the inquiry form on the right,
   divided by the same hairline rule the rest of the page groups with —
   no box, no fill. */
.contact-grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  column-gap: clamp(28px, 3.4vw, 56px);
}

.contact-intro {
  display: flex;
  flex-direction: column;
  /* Not stretched by the row, which is the default and was the bug. The
     ruled rows below are built to fill the height the form leaves them
     (see .contact-links), and while that height is the form's own that is
     exactly right — but the visitor can drag the project details box
     taller, and a stretched column grew with it: the three hairlines
     drifting apart on the far side of the page while they dragged.

     The height it fills is pinned by js/main.js (5b-2) to the form's own
     height instead, so the rows sit where they always did and stay there
     however far the box is pulled down. */
  align-self: start;
}

/* One column of ruled rows that fills whatever height the form leaves
   below the lede: three equal rows, so the hairlines land on even
   thirds and the last row's content sits on the form's bottom edge. */
.contact-links {
  flex: 1;
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  grid-template-rows: repeat(3, minmax(min-content, 1fr));
  padding-top: clamp(28px, 3.4vw, 44px);
}

.contact-item {
  display: flex;
  flex-direction: column;
  gap: 6px;
  padding-top: 18px;
  border-top: 1px solid var(--line);
  transition: color 0.3s var(--ease-out), border-color 0.3s var(--ease-out);
}

.contact-item-key {
  font-size: 13px;
  color: var(--text);
}

.contact-item-value {
  font-family: var(--font-display);
  font-size: clamp(17px, 1.7vw, 22px);
  font-weight: 600;
  color: var(--text-strong);
  line-height: 1.35;
}

a.contact-item:hover {
  border-top-color: var(--line-strong);
}

a.contact-item:hover .contact-item-value {
  color: var(--accent);
}

/* --- Inquiry form --------------------------------------------------
   Fields are ruled lines, not boxes: one hairline under each input,
   which brightens on focus. Same grouping logic as the rest of the
   page, and it keeps the form from reading as a card. */
.contact-form {
  padding-left: clamp(28px, 3.4vw, 56px);
  border-left: 1px solid var(--line);
}

.contact-form-title {
  margin-bottom: clamp(22px, 2.6vw, 30px);
}

.form-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: clamp(22px, 2.4vw, 34px) clamp(18px, 2.2vw, 30px);
}

.field {
  display: flex;
  flex-direction: column;
  gap: 5px;
  min-width: 0;
}

.field-wide {
  grid-column: 1 / -1;
}

.field > label {
  font-family: var(--font-body);
  font-size: 12px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.14em;
  color: var(--accent);
}

.field-optional {
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 0;
  text-transform: none;
  color: var(--text);
  opacity: 0.7;
}

.field input,
.field select,
.field textarea {
  width: 100%;
  padding: 5px 0 9px;
  border: 0;
  border-bottom: 1px solid var(--line-strong);
  border-radius: 0;
  background: transparent;
  color: var(--text-strong);
  font-family: var(--font-body);
  font-size: 15px;
  line-height: 1.5;
  transition: border-color 0.3s var(--ease-out), box-shadow 0.3s var(--ease-out);
}

/* Fixed box: no drag handle in the bottom corner. The height is the
   stylesheet's to set, not the visitor's — a dragged-open textarea
   stretches the form's grid row and pushes the submit button down the
   page. Long messages scroll inside the box instead. */
.field textarea {
  height: 116px;
  resize: none;
}

.field input:hover,
.field select:hover,
.field textarea:hover {
  border-bottom-color: var(--steel-300);
}

.field input:focus,
.field select:focus,
.field textarea:focus {
  outline: none;
  border-bottom-color: var(--accent);
  /* Reads as a 2px rule without shifting the field by a pixel. */
  box-shadow: 0 1px 0 0 var(--accent);
}

/* Native select chrome is opaque white on this page — swap it for the
   page's own chevron, drawn in --accent. */
.field select {
  appearance: none;
  -webkit-appearance: none;
  padding-right: 26px;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'%3E%3Cpath d='M1 1.5 6 6.5l5-5' fill='none' stroke='%237fa8cd' stroke-width='1.5'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 2px center;
  cursor: pointer;
}

.field select option {
  background: var(--ink-900);
  color: var(--text-strong);
}

/* Autofill paints its own opaque background; this keeps the ruled-line
   field looking like a ruled line. */
.field input:-webkit-autofill,
.field input:-webkit-autofill:hover,
.field input:-webkit-autofill:focus,
.field textarea:-webkit-autofill {
  -webkit-text-fill-color: var(--text-strong);
  caret-color: var(--text-strong);
  box-shadow: 0 0 0 1000px var(--canvas) inset;
  transition: background-color 9999s ease-out;
}

.field [aria-invalid="true"] {
  border-bottom-color: var(--alert);
}

.field-error {
  font-size: 12px;
  line-height: 1.4;
  color: var(--alert);
}

.field-error:empty {
  display: none;
}

/* Honeypot — off-screen, never focusable, never announced. */
.field-hp {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

.contact-form-foot {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 14px 20px;
  margin-top: clamp(26px, 3vw, 36px);
}

.contact-form-status:empty {
  display: none;
}

.contact-form-status {
  font-size: 13px;
  color: var(--text);
  opacity: 0.72;
}

.contact-form-status[data-state="error"] {
  color: var(--alert);
  opacity: 1;
}

.contact-form-status[data-state="sent"] {
  color: var(--accent);
  opacity: 1;
}

.contact-form[data-busy="true"] {
  pointer-events: none;
  opacity: 0.6;
}

/* ==========================================================================
   12. FOOTER
   ========================================================================== */
/* The footer keeps the dark navy treatment, bookending the nav. Text
   inside it therefore stays light — including .label, which is dark ink
   everywhere else on the page. */
/* Where the canvas ends. Opaque --ink-950 butted straight against the
   last section with no fade between them: this is deliberately the one
   hard seam on the page — the drafting surface stops at a cut line, it
   doesn't dissolve. Nothing here may gain a gradient into --canvas. */
.footer {
  background: var(--ink-950);
  padding: clamp(56px, 7vw, 88px) 0 32px;
  contain: layout paint;
}

.footer .label {
  color: var(--steel-300);
}

.footer h3 {
  color: var(--chrome-100);
}

.footer-grid {
  display: grid;
  /* brand + 3 link columns. Track count is explicit, so it must be kept in
     step with the number of .footer-col children — a stale extra track
     leaves a dead gap on the right. The 768px/480px overrides below are
     count-agnostic (repeat(2) / 1fr) and need no change. */
  grid-template-columns: 2fr 1fr 1fr 1fr;
  gap: clamp(28px, 4vw, 48px);
  padding-bottom: clamp(40px, 5vw, 64px);
}

.footer-brand img {
  /* Full logo (1159x471, transparent) at a dignified size */
  width: clamp(200px, 16vw, 240px);
  height: auto;
  margin-bottom: 16px;
}

.footer-brand p {
  font-size: 14px;
  color: var(--steel-300);
  max-width: 34ch;
}

.footer-col .label {
  display: block;
  margin-bottom: 16px;
}

.footer-col ul {
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.footer-col a,
.footer-col address {
  font-size: 14px;
  color: var(--chrome-200);
  transition: color 0.25s var(--ease-out);
}

.footer-col a:hover {
  color: var(--glow);
}

.footer-bottom {
  display: flex;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 12px;
  border-top: 1px solid var(--glass-brd);
  padding-top: 24px;
  font-size: 13px;
  color: var(--steel-300);
}

/* ==========================================================================
   13. RESPONSIVE
   ========================================================================== */
@media (max-width: 1080px) {
  .nav-links {
    gap: 24px;
  }
}

@media (max-width: 899px) {
  .nav-links,
  .nav-cta {
    display: none;
  }

  .nav-burger {
    display: flex;
  }

  /* Row gap stays 0 here too — the lede's own 6px margin is the whole gap,
     and a grid gap would stack on top of it. */

  .intro-inner {
    /* One knob: the lede's width and the logo's dead-space crop are both
       derived from it. */
    --logo-w: clamp(210px, 66vw, 360px);
  }

  .intro-lede {
    font-size: 12.5px;
    /* letter-spacing and text-indent both follow this. */
    --lede-track: 0.1em;
  }

  /* Same 4-panel row as desktop, just dimmed further since the stacked
     copy covers more of it on a phone. */
  .intro-wall {
    opacity: 0.44;
  }

  /* Grids collapse */
  .services-grid {
    grid-template-columns: 1fr;
  }

  /* Slideshow keeps showing two at a time on a phone, stacked instead of
     side by side. */
  .services-grid.is-slides {
    grid-template-columns: 1fr;
  }

  .services-grid.is-slides > .service-card:nth-child(even) {
    grid-column: 1;
    grid-row: 2;
  }

  /* Sectors: copy drops to the foot of each band over a bottom scrim */
  .sector-band {
    min-height: clamp(340px, 52vh, 460px);
    align-items: flex-end;
    padding-bottom: clamp(20px, 4vh, 34px);
  }

  .sector-band::before {
    /* Same floor as the desktop scrim: it thins out but never releases
       completely, so a clipped highlight in the photo can't read as bare
       page showing through the band. */
    background: linear-gradient(
      0deg,
      rgba(18, 37, 58, 0.90) 0%,
      rgba(18, 37, 58, 0.74) 44%,
      rgba(18, 37, 58, 0.20) 80%,
      rgba(18, 37, 58, 0.26) 100%
    );
  }

  /* The control row's chevrons come down with everything else on a narrow
     band; the icon keeps its scoped ratio, so it stays square. */
  .sectors-arrow {
    --arrow-size: clamp(40px, 9.6vw, 52px);
  }

  /* Process: the side-by-side composition (steps left, model right) only
     has room to breathe at desktop widths — narrowing either box to fit a
     portrait column just pushes the overlap onto the other. A plain
     top-to-bottom stack is the only layout that's overlap-safe regardless
     of how long the copy runs. The nav clearance that used to sit on this
     padding went with the header, which is in normal flow above the stage
     now and carries the section's own top padding. */
  .process-stage-sticky {
    height: auto;
    overflow: visible;
    display: flex;
    flex-direction: column;
    padding: 0 0 40px;
  }

  .process-stage-sticky::before,
  .process-stage-sticky::after {
    display: none;
  }

  .process-steps,
  .process-controls {
    position: static;
    width: auto;
    max-width: none;
    padding: 0 var(--pad);
    transform: none;
  }

  .process-model {
    /* relative, not static — .hvac-labels and the CSS2DRenderer output
       inside it are position:absolute against this element specifically
       (see hvac-scene.js). static would drop that positioning context
       and let them fall back to the next positioned ancestor up the
       tree (.process-stage-sticky, now the full stacked section), which
       scatters the HVAC callout tags up near the head copy instead of
       over the drawing. relative keeps the anchor without pulling it
       out of the static flow this stack relies on. */
    position: relative;
    /* ...but relative also means the desktop rule's top/right/bottom/left
       keep applying, as an OFFSET from the flow position rather than as
       edges of a containing box. Left in, `top: clamp(96px, 13vh, 152px)`
       slid the model ~100px down the stack while its siblings stayed laid
       out around where it should have been, dropping the drawing on top
       of the phase caption below it. Offsets cleared; the width/height/
       margin below are what place it here. */
    inset: auto;
    order: 1;
    width: calc(100% - 2 * var(--pad));
    /* The plan's own proportions (152ft x 94ft), same as the measured box on
       desktop: a frame taller than the drawing needs is empty canvas above
       and below it, and a frame taller than it is WIDE would make the scene
       turn the sheet on its side. It shrinks with the column instead. */
    aspect-ratio: 152 / 94;
    height: auto;
    margin: 32px auto;
  }

  .process-steps {
    order: 2;
    justify-items: stretch;
    padding-bottom: 0;
    top: auto;
  }

  /* Stacked and full-width here, so the block aligns to the page's left
     gutter with everything else in the column. */
  .process-step {
    max-width: none;
    align-items: flex-start;
  }

  /* Still under the caption, still the only phase control — the stack just
     puts the two chevrons below the copy rather than beside the drawing. */
  .process-controls {
    order: 3;
    justify-content: flex-start;
    margin-top: 12px;
  }

  /* About / contact stack */
  .about-grid {
    grid-template-columns: 1fr;
  }

  .about-photo-frame {
    max-width: 420px;
  }

  /* Single column: copy and direct details, then the form under them —
     the hairline moves from the form's left edge to its top. */
  .contact-grid {
    grid-template-columns: 1fr;
  }

  .contact-big {
    font-size: clamp(36px, 6.4vw, 58px);
  }

  .contact-links {
    flex: none;
    grid-template-rows: none;
    gap: clamp(18px, 2.4vw, 26px);
  }

  .contact-form {
    padding-left: 0;
    padding-top: clamp(32px, 5vw, 44px);
    margin-top: clamp(32px, 5vw, 44px);
    border-left: 0;
    border-top: 1px solid var(--line);
  }

  .contact-links {
    padding-top: clamp(30px, 4.5vw, 40px);
  }

  .footer-grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }

  .footer-brand {
    grid-column: 1 / -1;
  }
}

@media (max-width: 480px) {
  .form-grid {
    grid-template-columns: 1fr;
  }

  .footer-grid {
    grid-template-columns: 1fr;
  }
}

/* ==========================================================================
   14. REDUCED MOTION
   ========================================================================== */
@media (prefers-reduced-motion: reduce) {
  html {
    scroll-behavior: auto;
  }

  *,
  *::before,
  *::after {
    animation-duration: 0.001s !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001s !important;
  }

  /* Partners lane stops travelling and becomes a static, wrapped logo
     wall inside the container — the duplicate set only ever existed to
     make the loop seamless, so it goes. */
  .partners-track {
    animation: none !important;
    transform: none !important;
    width: auto;
    flex-wrap: wrap;
  }

  .partners-set + .partners-set {
    display: none;
  }

  .partners-set {
    flex-wrap: wrap;
    justify-content: center;
    row-gap: clamp(24px, 3vw, 40px);
    padding-right: 0;
    padding-inline: var(--pad);
  }

  .partners-marquee {
    overflow: visible;
    -webkit-mask-image: none;
    mask-image: none;
  }

  /* The relationships-page category lanes stand down the same way: back to
     the static wrapped grid, clones gone. relationships.js already refuses
     to build the belt under this preference — these rules cover a visitor
     who flips the setting mid-visit. Explicit animation/transform resets,
     not just the global 0.001s clamp above: a one-iteration run would
     otherwise leave the track parked at translateX(-50%), half off-screen. */
  .rel-marquee.is-looping {
    overflow: visible;
    margin-inline: 0;
    -webkit-mask-image: none;
    mask-image: none;
  }

  .rel-marquee .rel-track {
    animation: none !important;
    transform: none !important;
    width: auto;
  }

  .rel-marquee .rel-set {
    flex-wrap: wrap;
    padding-right: 0;
  }

  .rel-marquee .rel-set[aria-hidden="true"] {
    display: none;
  }
}

/* ==========================================================================
   15. RELATIONSHIPS PAGE (relationships.html)
   Working Relationships with Trades / with Professionals. Logo tiles sit
   on white plates so every partner's own colours read on the dark canvas;
   relationships.js shuffles the tiles inside each category on every load
   so no company is ever fixed in one spot (alphabetical in the HTML is
   the no-JS fallback).
   ========================================================================== */
/* This section carries the page's <h1> and the jump links under it. All it
   owes the page above that is the fixed nav's clearance. */
.rel-hero {
  padding: calc(var(--nav-h) + 40px) 0 0;
}

.rel-hero h1 {
  font-size: clamp(38px, 5.4vw, 68px);
  max-width: 16ch;
  text-wrap: balance;
  margin-top: 14px;
}

/* --- Desktop: the page opens under the nav, like FAQ does -----------------
   <main> carries a hand offset of -30px -242px from the visual editor, and
   the 242px of it is why this page opened wrong: the whole document was
   lifted most of a screen, which put the <h1> at -194px — off the top of the
   window entirely, never seen — and left the first group's label sitting
   behind the nav bar. The 34px of hero padding that used to be here was an
   attempt to live with that rather than undo it.

   The vertical half of the offset is dropped and the horizontal half is
   kept exactly as it is. -30px is load-bearing: two sections further down
   carry +29px and +31px of their own, which only line up against it, so
   cancelling both axes would push those two out by 30px. Nothing on this
   page moves sideways here — this is the vertical fix only.

   The padding is then FAQ's expression verbatim (.faq-hero, section 12), so
   the two subpages open on the same line at every desktop width: 204px at
   1440, which is 119px of air under an 85px bar. The h1's 14px top margin
   goes with it — that margin exists to separate the h1 from a label above
   it, and this hero has no label. It has to be cleared AFTER the rule that
   sets it, which is why this block sits below rather than above. */
@media (min-width: 1024px) {
  #top:has(> .rel-hero) {
    translate: -30px 0 !important;
  }

  .rel-hero {
    padding-top: calc(var(--nav-h) + clamp(56px, 9vw, 120px));
  }

  .rel-hero h1 {
    margin-top: 0;
  }
}

.rel-hero .section-lede {
  max-width: 58ch;
}

.rel-jump {
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
  margin-top: 32px;
}

.rel-group {
  --group-pad: clamp(48px, 7vw, 104px);
  /* At a seam between two groups the two paddings stack either side of one
     hairline, so the open value read as a hole. Each side carries about two
     thirds of it instead; the rule still stands clear of the copy. */
  --seam-pad: clamp(36px, 4.6vw, 66px);
  padding: var(--group-pad) 0;
  border-top: 1px solid var(--line);
}

.rel-group + .rel-group {
  padding-top: var(--seam-pad);
}

.rel-group:has(+ .rel-group) {
  padding-bottom: var(--seam-pad);
}

/* The group's own bottom padding already closes the group. The last logo
   lane inside it was adding its own 44px on top, which put the best part of
   250px of nothing between the last row of logos and the next heading. */
.rel-group .rel-category:last-child {
  padding-bottom: 0;
}

/* No rule between the page's own hero and the first group. The hairline
   earns its place BETWEEN two groups, where it separates Trades from
   Professionals; up against the hero it was ruling off the page title
   from the page, which the whitespace already does.

   And with the rule gone the padding that stood clear of it has nothing
   to stand clear of: the group's own 96px sat on top of the hero's 72px,
   which read as a gap once there was no line in it to explain it. The
   first group therefore starts at the hero's own foot and the air above
   Trades is the hero's bottom padding alone. Only the first group —
   between Trades and Professionals the rule is still drawn, and the
   padding on both sides of it is what keeps it off the copy. */
.rel-hero + .rel-group {
  border-top: 0;
  padding-top: 0;
}

.rel-group .section-head {
  display: grid;
  grid-template-columns: minmax(0, 1.1fr) minmax(0, 1fr);
  gap: clamp(24px, 4vw, 64px);
  align-items: start;
}

.rel-group .section-head h2 {
  grid-column: 1;
}

.rel-group .section-head .label {
  grid-column: 1 / -1;
}

.rel-intro {
  grid-column: 2;
  grid-row: 2 / span 2;
  margin-top: 0;
}

.rel-intro p {
  color: var(--text);
  font-size: clamp(16px, 1.2vw, 18px);
  line-height: 1.7;
}

.rel-intro p + p {
  margin-top: 16px;
}

.rel-intro cite {
  display: block;
  margin-top: 18px;
  font-style: normal;
  font-size: 13px;
  letter-spacing: 0.06em;
  color: var(--accent);
}

.rel-category {
  padding: clamp(28px, 4vw, 44px) 0;
  border-top: 1px solid var(--line);
}

.rel-category-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  flex-wrap: wrap;
  margin-bottom: 22px;
}

.rel-category-head h3 {
  font-size: clamp(20px, 2vw, 26px);
}

.rel-count {
  font-size: 12px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--accent);
}

.rel-empty {
  max-width: none;
  color: var(--steel-300);
  font-size: 14px;
  padding: 18px 0 4px;
  border-top: 1px dashed var(--line);
}

/* Logo row: no plates. Every firm ships two marks of identical bounds —
   a white mono version (rest) and a colour version (hover) — stacked in
   one box and cross-faded. Boxes are scaled to roughly equal visual area
   (--lw, set inline per logo from its aspect ratio) so a wide wordmark
   and a square badge read as the same weight. At rest the white mark
   sits at 60%; hover or keyboard focus fades it out and the full-colour
   mark in. Nothing else on the tile changes. */
.rel-grid {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 32px clamp(32px, 4vw, 56px);
  padding: 12px 0 4px;
  --logo-scale: 1;
}

/* Each category's tiles sit one level down now, in a .rel-set inside a
   .rel-track. At rest (JS off, or reduced motion) the set is simply the
   wrapped grid this page always had — same flex, same gaps, inheriting
   --logo-scale from .rel-grid above — so no logo ever depends on the
   script to be seen. */
.rel-set {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 32px clamp(32px, 4vw, 56px);
}

/* --- The looping lanes -------------------------------------------------
   The home page's partners lane, per category: relationships.js shuffles
   each set, clones it (aria-hidden, stripped of ids) until the belt is at
   least two lanes long and the copy count is even, then adds .is-looping —
   at which point the track stops wrapping and slides left by half its own
   width on the same partners-scroll keyframes. An even number of identical
   sets makes -50% land exactly on a seam, so the loop never jumps. The
   script also writes the real animation-duration from the belt's measured
   width, so a two-logo category travels at the same pace as a nine-logo
   one; the 60s here only covers the frame before it does. */
.rel-marquee.is-looping {
  overflow: hidden;
  /* The belt keeps the category's own width — the same measure as the
     hairline that rules off each category and the heading above it — so
     a logo never travels out past the end of that line. It was previously
     full-bleed to the viewport, which read as a row much wider than the
     page it sits in. The fade ramps sit at the lane's own two ends, so the
     belt dissolves exactly where the rule stops. */
  --rel-fade: clamp(40px, 6vw, 96px);
  margin-inline: 0;
  -webkit-mask-image: linear-gradient(
    90deg,
    transparent 0,
    #000 var(--rel-fade),
    #000 calc(100% - var(--rel-fade)),
    transparent 100%
  );
  mask-image: linear-gradient(
    90deg,
    transparent 0,
    #000 var(--rel-fade),
    #000 calc(100% - var(--rel-fade)),
    transparent 100%
  );
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
}

.rel-marquee.is-looping .rel-track {
  display: flex;
  width: max-content;
  animation: partners-scroll 60s linear infinite;
}

/* Nowrap, and each set carries its own trailing gap so the spacing across
   the seam matches the spacing inside a set — same trick as .partners-set. */
.rel-marquee.is-looping .rel-set {
  --marquee-gap: clamp(40px, 5vw, 76px);
  flex-wrap: nowrap;
  gap: var(--marquee-gap);
  padding-right: var(--marquee-gap);
}

/* Never stops, for the same reasons the home lane never stops — see the
   note on .partners-marquee. These category lanes carry the identical
   pause and so carried the identical bug: a click parked them, and coming
   back from a firm's site parked them for good. */

@media (max-width: 899px) {
  /* Same step-downs the home lane takes on a phone: tighter gaps, and edge
     fades that don't eat most of a 390px screen. */
  .rel-marquee.is-looping .rel-set {
    --marquee-gap: clamp(28px, 7vw, 44px);
  }

  .rel-marquee.is-looping {
    --rel-fade: 26px;
  }
}

/* --- The carousels -----------------------------------------------------
   Every lane that is not looping is a carousel. The set stops wrapping and
   the lane scrolls, so a category that outgrows its width slides sideways
   instead of breaking onto a second row.

   This is the still page: reduced motion, or the script blocked. With the
   script running and motion allowed, every category is the belt above —
   all eight, at all three widths, whether or not their logos would have
   fitted. Nothing lands here by being short or by being narrow.

   Which makes the arrows a reduced-motion control, and only that. A
   visitor who has asked for no movement gets the rows standing still, and
   the ones too long for the frame are theirs to scroll — by hand, one
   press at a time. It is the same markup either way: the chevrons ship
   hidden and relationships.js only reveals a pair on a lane that is both
   static and actually scrollable.

   A set that still fits its lane doesn't scroll and looks exactly as it
   always did: the rule costs nothing until there is something to scroll.
   That is the whole point of doing it in CSS — overflow works out for
   itself whether a lane needs to move, at every width, with no measuring
   and no script. Snap is `proximity`, not `mandatory`: a swipe that ends
   mid-lane is left where the thumb put it.

   The arrows and the edge fade are the enhancement on top, and they are
   the only part that waits for relationships.js. */
.rel-grid:not(.is-looping) {
  flex-wrap: nowrap;
  overflow-x: auto;
  overscroll-behavior-x: contain;
  scroll-snap-type: x proximity;
  /* No scrollbar under the logos — the fade and the arrows are the cue. */
  scrollbar-width: none;
  -ms-overflow-style: none;
  /* Room inside the scrollport for a focused tile's outline (offset 10px),
     which the scroll container would otherwise clip. */
  padding-bottom: 12px;
}

.rel-grid:not(.is-looping)::-webkit-scrollbar {
  width: 0;
  height: 0;
}

.rel-grid:not(.is-looping) .rel-track {
  flex: 0 0 auto;
  width: max-content;
}

.rel-grid:not(.is-looping) .rel-set {
  flex-wrap: nowrap;
}

.rel-grid:not(.is-looping) .partner {
  flex: 0 0 auto;
  scroll-snap-align: start;
}

/* The fade is drawn on the side that has more logos behind it, and only
   while the lane is actually scrollable — relationships.js sets data-edge
   from the scroll position. A lane that fits carries no attribute and so
   no mask, which keeps the first logo hard against the heading above it. */
.rel-grid[data-edge="start"] {
  -webkit-mask-image: linear-gradient(
    90deg,
    #000 calc(100% - 44px),
    transparent 100%
  );
  mask-image: linear-gradient(
    90deg,
    #000 calc(100% - 44px),
    transparent 100%
  );
}

.rel-grid[data-edge="both"] {
  -webkit-mask-image: linear-gradient(
    90deg,
    transparent 0,
    #000 44px,
    #000 calc(100% - 44px),
    transparent 100%
  );
  mask-image: linear-gradient(
    90deg,
    transparent 0,
    #000 44px,
    #000 calc(100% - 44px),
    transparent 100%
  );
}

.rel-grid[data-edge="end"] {
  -webkit-mask-image: linear-gradient(90deg, transparent 0, #000 44px);
  mask-image: linear-gradient(90deg, transparent 0, #000 44px);
}

/* Arrows. They ship in the HTML hidden and stay hidden unless the script
   finds the lane scrollable, so they never promise a move the lane can't
   make. Touch gets the swipe instead — see the phone rule further down. */
.rel-nav {
  display: flex;
  gap: 8px;
  flex: 0 0 auto;
}

.rel-nav[hidden] {
  display: none;
}

.rel-nav-btn {
  width: 44px;
  height: 44px;
  display: grid;
  place-items: center;
  padding: 0;
  border: 1px solid var(--line);
  border-radius: 50%;
  background: transparent;
  color: var(--steel-300);
  cursor: pointer;
  transition: color 0.3s var(--ease-out), border-color 0.3s var(--ease-out),
    opacity 0.3s var(--ease-out);
}

.rel-nav-btn svg {
  width: 15px;
  height: 15px;
}

.rel-nav-btn:hover:not(:disabled),
.rel-nav-btn:focus-visible {
  color: var(--white);
  border-color: var(--steel-400);
}

.rel-nav-btn:disabled {
  opacity: 0.28;
  cursor: default;
}

.partner {
  position: relative;
  display: block;
  width: calc(var(--lw, 160px) * var(--logo-scale));
  overflow: hidden;
  outline: none;
  border-radius: 6px;
}

.partner img {
  display: block;
  width: 100%;
  height: auto;
  transition: opacity 0.5s var(--ease-out);
}

/* 60% is the rest state for a mark drawn at the weight most of these are.
   A few are not: Amini's wordmark is a hairline serif, and at the shared 60%
   it read as a watermark beside its neighbours rather than a logo. --mono-op
   is the per-tile escape hatch, set inline next to --lw on the tiles that
   need it, so the exception travels with the logo it belongs to instead of
   pulling the whole lane up. */
.partner-mono {
  opacity: var(--mono-op, 0.6);
}

.partner-colour {
  position: absolute;
  inset: 0;
  opacity: 0;
}

.partner:hover .partner-mono,
.partner:focus-visible .partner-mono {
  opacity: 0;
}

.partner:hover .partner-colour,
.partner:focus-visible .partner-colour {
  opacity: 1;
}

.partner:focus-visible {
  outline: 1px solid var(--steel-400);
  outline-offset: 10px;
}

/* --plate: the light card a logo drawn for paper needs.
   Jute Studios' wordmark is a hairline serif in their own gold (JUTE) over
   a pale warm grey (STUDIOS) — drawn for a white page, and at ~1.2:1 on
   --canvas the lower line disappears into the ground entirely. Every other
   mark here carries white or a light tint and survives the dark.
   The card fades in WITH the colour twin rather than sitting there at
   rest, so the lane still reads as one row of mono marks until you touch
   one. --lw carries the card, not the artwork: the tile is the logo's
   width plus the padding (118 + 22 = 140), so the mark itself stays the
   size it was and only the card is new. */
.partner--plate {
  /* Scaled with the lane so the card keeps its proportions where
     --logo-scale drops the mark (see .rel-grid and the phone block). */
  --plate-x: calc(11px * var(--logo-scale, 1));
  --plate-y: calc(10px * var(--logo-scale, 1));
  box-sizing: border-box;
  padding: var(--plate-y) var(--plate-x);
}

.partner--plate::before {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: 6px;
  background: var(--white);
  opacity: 0;
  transition: opacity 0.5s var(--ease-out);
}

.partner--plate:hover::before,
.partner--plate:focus-visible::before {
  opacity: 1;
}

/* .partner img is width: 100%, which on an absolutely positioned twin
   resolves against the PADDING box — so `inset` alone would place the
   mark correctly and then let it overhang the card by the padding. Width
   and position both come off the content box by hand instead. */
.partner--plate .partner-colour {
  top: var(--plate-y);
  right: auto;
  left: var(--plate-x);
  width: calc(100% - var(--plate-x) * 2);
  height: auto;
}

/* Shared by the Relationships and FAQ pages: the same closing band, so
   both subpages hand off to the contact form the same way. */
.rel-cta,
.faq-cta {
  padding: clamp(56px, 8vw, 110px) 0;
  border-top: 1px solid var(--line);
}

.rel-cta-inner {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 28px;
}

.rel-cta h2,
.faq-cta h2 {
  font-size: clamp(28px, 3.4vw, 42px);
}

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

/* ==========================================================================
   15b. LEGAL PAGES (privacy.html) — long-form prose on the shared canvas

   No new furniture: the hero is the FAQ hero's measure and rhythm, the
   sections are separated by the same hairline the FAQ rows and the contact
   details use, and the type comes from the tokens. The one thing a page
   like this needs that the rest of the site does not is a reading measure
   — 40-odd characters of heading over 70-odd of body — because it is the
   only page anyone reads top to bottom rather than scans.
   ========================================================================== */
.legal-hero {
  padding: calc(var(--nav-h) + clamp(56px, 9vw, 120px)) 0 clamp(32px, 4vw, 56px);
}

.legal-hero h1 {
  font-size: clamp(38px, 5.4vw, 68px);
  max-width: 18ch;
  text-wrap: balance;
  margin-top: 14px;
}

.legal-hero .section-lede {
  max-width: 58ch;
  margin-top: 20px;
}

/* Date line under the lede. Smaller and quieter than body copy — it is
   metadata about the document, not part of it. */
.legal-updated {
  margin-top: 20px;
  font-size: 14px;
  color: var(--steel-300);
}

.legal {
  position: relative;
  padding: clamp(24px, 3vw, 40px) 0 clamp(40px, 5vw, 72px);
}

.legal-body {
  max-width: 76ch;
}

/* Hairline between clauses, matching .faq-item. The first section does not
   get one: the hero above it already ends on air, and a rule there reads as
   a lid on the page. */
.legal-section + .legal-section {
  border-top: 1px solid var(--line);
  margin-top: clamp(32px, 4vw, 52px);
  padding-top: clamp(32px, 4vw, 52px);
}

.legal-section h2 {
  font-size: clamp(22px, 2.2vw, 30px);
  max-width: 24ch;
  text-wrap: balance;
}

.legal-section h3 {
  margin-top: clamp(24px, 3vw, 34px);
  font-family: var(--font-body);
  font-size: 15px;
  font-weight: 600;
  letter-spacing: 0.01em;
  color: var(--text-strong);
}

.legal-section p,
.legal-section li {
  color: var(--text);
  max-width: 72ch;
}

.legal-section > p {
  margin-top: 18px;
}

.legal-section h2 + p,
.legal-section h3 + p {
  margin-top: 12px;
}

.legal-section ul {
  margin-top: 18px;
  padding-left: 0;
  list-style: none;
}

/* The same blueprint-blue tick the About page's credentials list uses:
   a short rule rather than a bullet, hung in the gutter. */
.legal-section li {
  position: relative;
  padding-left: 26px;
  margin-top: 10px;
}

.legal-section li::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0.7em;
  width: 12px;
  height: 1px;
  background: var(--accent-mid);
}

.legal-section strong {
  color: var(--text-strong);
  font-weight: 600;
}

.legal-section em {
  font-style: italic;
}

.legal-section a {
  color: var(--accent);
  text-decoration: underline;
  text-underline-offset: 3px;
  text-decoration-thickness: 1px;
  transition: color 0.25s var(--ease-out);
}

.legal-section a:hover {
  color: var(--text-strong);
}

/* Contact block: the label sits above its value rather than beside it, so
   the three rows line up on one left edge like the footer columns do. */
.legal-contact li {
  padding-left: 0;
  margin-top: 20px;
}

.legal-contact li::before {
  display: none;
}

.legal-contact .label {
  display: block;
  margin-bottom: 6px;
}

.legal-contact address {
  font-style: normal;
  line-height: 1.6;
}

/* The privacy link in the bottom bar of every page. .footer-bottom has no
   link styling of its own — it was two paragraphs until now. */
.footer-bottom a {
  color: var(--steel-300);
  text-decoration: underline;
  text-underline-offset: 3px;
  transition: color 0.25s var(--ease-out);
}

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

@media (max-width: 899px) {
  .rel-group .section-head {
    grid-template-columns: 1fr;
  }

  .rel-intro {
    grid-column: 1;
    grid-row: auto;
  }
}

/* Phone: the carousels are swiped, not clicked. The arrows come out of the
   category head — two 44px circles beside a heading that already wants the
   full gutter — and the fade narrows so it isn't eating a tile's width of a
   390px screen. */
@media (max-width: 767px) {
  .rel-nav {
    display: none;
  }

  .rel-grid[data-edge="start"] {
    -webkit-mask-image: linear-gradient(
      90deg,
      #000 calc(100% - 26px),
      transparent 100%
    );
    mask-image: linear-gradient(
      90deg,
      #000 calc(100% - 26px),
      transparent 100%
    );
  }

  .rel-grid[data-edge="both"] {
    -webkit-mask-image: linear-gradient(
      90deg,
      transparent 0,
      #000 26px,
      #000 calc(100% - 26px),
      transparent 100%
    );
    mask-image: linear-gradient(
      90deg,
      transparent 0,
      #000 26px,
      #000 calc(100% - 26px),
      transparent 100%
    );
  }

  .rel-grid[data-edge="end"] {
    -webkit-mask-image: linear-gradient(90deg, transparent 0, #000 26px);
    mask-image: linear-gradient(90deg, transparent 0, #000 26px);
  }
}

@media (max-width: 480px) {
  .rel-grid {
    --logo-scale: 0.72;
    gap: 28px 32px;
  }

  /* The tiles live in .rel-set now; this is the wrapped-fallback gap only.
     A looping set's own gap rule is more specific, so the belt keeps
     --marquee-gap. */
  .rel-set {
    gap: 28px 32px;
  }
}

/* ==========================================================================
   16. CUSTOM CURSOR — "ughes" chrome arrow
   An ordinary arrow pointer, machined out of the same metal as the letters
   in the wordmark. The gradient stops are sampled straight off a letter
   stroke in assets/img/logo.png (a vertical scan through the "U"): dark
   navy edge, a bright top lip, mid steel blue, the specular band about two
   thirds down, then slate blue falling back to navy at the base.

   Two nested elements: .cursor-pos is a bare positioning shim JS moves
   every pointermove with no transition — a real cursor has zero lag, so
   position is never eased. .cursor-arrow is the visible SVG inside it,
   which IS free to transition, since only its scale/rotation change (on
   hover, on grab) and those read better with a little give.

   Only ever active when JS has added .has-cursor to <html> AND the device
   has a fine pointer, so touch and no-JS visitors keep the native cursor.
   Nothing on the page depends on it; it is decoration over the top.
   ========================================================================== */
:root {
  /* One letter stroke, top to bottom. */
  --ughes-edge: #0b1e3a;
  --ughes-lip: #9fc7e2;
  --ughes-steel: #3f7fb8;
  --ughes-mid: #6ea4cb;
  --ughes-spec: #eaf3f5;
  --ughes-slate: #6f8492;
  --ughes-deep: #46647c;
  --ughes-base: #1c3856;
}

.cursor-pos {
  position: fixed;
  top: 0;
  left: 0;
  z-index: 9999;
  pointer-events: none;
  will-change: transform;
}

.cursor-arrow {
  display: block;
  width: 21px;
  height: 28px;
  /* The tip — the hotspot a real cursor clicks from — sits at roughly
     (12%, 9%) of the SVG's own box. Pinning transform-origin there keeps
     the tip planted under the pointer while hover/grab scale or rotate
     the rest of the arrow around it. */
  transform-origin: 12% 9%;
  opacity: 0;
  filter: drop-shadow(0 1px 2px rgba(3, 8, 14, 0.45))
    drop-shadow(0 0 8px rgba(85, 168, 255, 0.25));
  transition: opacity 0.2s var(--ease-out),
    transform 0.22s var(--ease-out);
}

.has-cursor .cursor-arrow {
  opacity: 1;
}

/* Native pointer off only once the replacement is live. Interactive
   elements are listed too so their own `cursor: pointer` is overridden. */
.has-cursor,
.has-cursor a,
.has-cursor button,
.has-cursor summary,
.has-cursor label,
.has-cursor select,
.has-cursor .btn,
.has-cursor .process-model canvas,
.has-cursor .services-model canvas {
  cursor: none;
}

/* Where the native cursor still carries meaning — the text caret in a
   form field — it wins and the chrome arrow steps aside. */
.has-cursor input,
.has-cursor textarea {
  cursor: text;
}

.has-cursor.cursor-text .cursor-arrow,
.has-cursor.cursor-out .cursor-arrow {
  opacity: 0;
}

/* Over a link or button: the arrow leans in, a touch larger. */
.has-cursor.cursor-hot .cursor-arrow {
  transform: scale(1.16);
}

/* Over the draggable HVAC model: a slight tilt, as if reaching for it. */
.has-cursor.cursor-grab .cursor-arrow {
  transform: rotate(-10deg);
}

/* While it is being dragged: tilts further and tightens, like a grip. */
.has-cursor.cursor-grabbing .cursor-arrow {
  transform: rotate(-18deg) scale(0.92);
}

/* Coarse pointers never get it, whatever the script decided. */
@media (hover: none), (pointer: coarse) {
  .cursor-pos {
    display: none;
  }

  .has-cursor,
  .has-cursor a,
  .has-cursor button,
  .has-cursor summary,
  .has-cursor label,
  .has-cursor select,
  .has-cursor .btn,
  .has-cursor .process-model canvas,
  .has-cursor .services-model canvas {
    cursor: auto;
  }

  .has-cursor a,
  .has-cursor button,
  .has-cursor summary,
  .has-cursor label,
  .has-cursor select,
  .has-cursor .btn {
    cursor: pointer;
  }
}


/* ==========================================================================
   16. COMMERCIAL KITCHEN MODEL — the delivered .ng-kitchen block's own
       stylesheet, lifted verbatim out of the block's inline <style> so it
       lives with the rest of the site's CSS. Every rule is scoped to
       .ng-kitchen and nothing here touches anything else on the page.

       The block's own heading, its three view buttons and its colour
       legend are NOT in the markup (see index.html): the Services headings
       beside the model are its buttons, and the drawing is read without a
       key. Their rules are left in place regardless, so the block still
       stands up if any of them is ever put back.
   ========================================================================== */
.ng-kitchen {
    --ngk-ground: var(--site-model-ground, #061525);
    --ngk-ink: var(--site-model-ink, #bfe4ff);
    --ngk-muted: var(--site-model-muted, #6d93b3);
    --ngk-cold: #2fb4ff; --ngk-hot: #ff5a2a; --ngk-gas: #ffa32e; --ngk-flue: #b59a76; --ngk-air: #86b7d8;
    --ngk-flame: #ff8a2a; --ngk-smoke: #b8c4cc; --ngk-fan: #9fd4ff;
    position: relative; display: block; width: 100%; aspect-ratio: 16 / 9; min-height: 420px; max-height: 92vh;
    overflow: hidden; background: var(--ngk-ground); color: var(--ngk-ink);
    font: 400 14px/1.4 Barlow, "Helvetica Neue", Arial, sans-serif; box-sizing: border-box;
  }
  .ng-kitchen *, .ng-kitchen *::before, .ng-kitchen *::after { box-sizing: border-box; }
  .ng-kitchen__stage { position: absolute; inset: 0; }
  .ng-kitchen__stage canvas { display: block; width: 100%; height: 100%; }

  .ng-kitchen__head { position: absolute; left: 28px; top: 24px; max-width: 36ch; margin: 0; }
  .ng-kitchen__head h2 { margin: 0; font: 600 22px/1.15 Barlow, "Helvetica Neue", Arial, sans-serif; letter-spacing: -0.005em; color: var(--ngk-ink); }
  .ng-kitchen__head p { margin: 8px 0 0; font-size: 14px; line-height: 1.45; color: var(--ngk-muted); }

  .ng-kitchen__nav { position: absolute; right: 28px; top: 24px; display: flex; gap: 22px; margin: 0;
    font: 500 15px/1.2 "Barlow Semi Condensed", Barlow, "Helvetica Neue", Arial, sans-serif; }
  .ng-kitchen__nav button { appearance: none; background: none; border: 0; padding: 6px 0 8px; margin: 0; cursor: pointer;
    color: var(--ngk-muted); font: inherit; border-bottom: 2px solid transparent; transition: color 160ms, border-color 160ms; }
  .ng-kitchen__nav button:hover { color: var(--ngk-ink); }
  .ng-kitchen__nav button:focus-visible { outline: 2px solid var(--ngk-cold); outline-offset: 4px; border-radius: 2px; }
  .ng-kitchen__nav button[aria-pressed="true"] { color: var(--ngk-ink); border-bottom-color: var(--ngk-ink); }

  .ng-kitchen__key { position: absolute; left: 28px; bottom: 24px; margin: 0; padding: 0; list-style: none;
    font: 500 13px/1.3 "Barlow Semi Condensed", Barlow, "Helvetica Neue", Arial, sans-serif; }
  .ng-kitchen__key li { display: flex; align-items: center; gap: 10px; padding: 3px 0; color: var(--ngk-ink); }
  .ng-kitchen__key i { display: block; width: 22px; height: 4px; border-radius: 2px; background: currentColor; box-shadow: 0 0 8px currentColor; opacity: 0.9; }
  .ng-kitchen__key .cold i { color: var(--ngk-cold); }   .ng-kitchen__key .hot i { color: var(--ngk-hot); }
  .ng-kitchen__key .gas i { color: var(--ngk-gas); }     .ng-kitchen__key .flue i { color: var(--ngk-flue); }
  .ng-kitchen__key .air i { color: var(--ngk-air); box-shadow: none; opacity: 0.7; }
  .ng-kitchen__key .flame i { color: var(--ngk-flame); } .ng-kitchen__key .smoke i { color: var(--ngk-smoke); } .ng-kitchen__key .fan i { color: var(--ngk-fan); }
  .ng-kitchen[data-view="plumbing"] .ng-kitchen__key li.hvac { display: none; }
  .ng-kitchen[data-view="hvac"] .ng-kitchen__key li.plumbing { display: none; }

  @media (max-width: 720px) {
    .ng-kitchen { min-height: 360px; }
    .ng-kitchen__head { left: 18px; top: 16px; max-width: 26ch; }
    .ng-kitchen__head h2 { font-size: 18px; }
    .ng-kitchen__head p { display: none; }
    .ng-kitchen__nav { right: 18px; top: 16px; gap: 14px; font-size: 14px; }
    .ng-kitchen__key { left: 18px; bottom: 16px; font-size: 12px; }
  }

/* ==========================================================================
   17. PHONE & TABLET — three layouts, one site

   Desktop (1440) is signed off, so nothing in this section touches it.
   Almost everything below sits inside a max-width: 1023px query. The one
   exception is 17a-ii, which reaches up to 1179px to catch the 12.9" iPad
   held upright — 1024px wide, above every breakpoint here and still far
   short of the 1280px container the desktop composition is measured
   against. It ends at 1179; from 1180px up the rendering is exactly what it
   was.

   The breakpoints are the editor's own lines — 767 / 768 / 1023 — so what
   a visitor edits in the phone view is what these rules govern there. The
   site's older 899/900 pair is left alone; it still does the heavy
   structural stacking, and this section sits after it, tuning what the
   phone and the tablet need on top of it.
   ========================================================================== */

/* --------------------------------------------------------------------------
   17a. Everything below desktop (max-width: 1023px)
   -------------------------------------------------------------------------- */
@media (max-width: 1023px) {
  /* --- Hand-placed desktop geometry, unpicked ---------------------------
     The visual editor writes a pixel placement into all three of its
     breakpoint blocks at once, so a heading positioned to sit beside the
     kitchen model at 1440 arrives on a 390px phone as a 1051px-wide box,
     and a section lede arrives 627px off the left edge. mb-overrides.css
     is the editor's file and not ours to rewrite, so the placements are
     unpicked here instead.

     `html [data-mb=...]` rather than plain `[data-mb=...]`: the override
     is !important and loads after this file, so only a more specific
     selector — one element deep — takes it back. Desktop never reads this
     block, so every one of those placements still lands at 1024px up. */
  html [data-mb="h2-52yvm"],
  html [data-mb="p-52yvn"],
  html [data-mb="h2-hmi071"],
  html [data-mb="h3-52yvp"],
  html [data-mb="h3-52yvs"] {
    width: auto !important;
    max-width: 100% !important;
    translate: none !important;
  }

  /* The same thing a size smaller, on the Relationships page. A standing
     quote pulled up 21 and 26 pixels tightens it against a heading that
     sits in the next column over at 1440; in one column the heading is
     directly above it, and the two paragraphs are dragged up through it.
     Closing the gap is the desktop's business, not the phone's. */
  html [data-mb="blockquote-52z2n"],
  html [data-mb="p-52z2o"],
  html [data-mb="p-52z2p"] {
    translate: none !important;
  }

  /* Same problem one element bigger: the kitchen model's stage is hand-
     sized to 826px and nudged 373px left. It fills its own box down here,
     which is the box the section already sizes to the screen. */
  html [data-mb="div-ngk02"] {
    width: auto !important;
    max-width: 100% !important;
    min-height: 0 !important;
    translate: none !important;
  }

  /* --- The header collapses to a drawer ---------------------------------
     Seven destinations plus a call to action is more than an 820px bar
     holds, so the burger comes down to 1023 rather than stopping at the
     old 899 line. From 1024px up the links are still inline. */
  .nav-links,
  .nav-cta {
    display: none;
  }

  .nav-burger {
    display: flex;
  }

  /* The panel slides in from the right instead of dissolving over the
     whole screen: the page stays visible beside it, which is what makes a
     drawer read as a drawer. The bar sits above it (z-index 100 against
     90), so the burger — an X while the menu is open — is the close
     control, on screen for as long as the panel is. */
  .mobile-menu {
    inset: 0 0 0 auto;
    width: min(360px, 88vw);
    justify-content: flex-start;
    gap: 20px;
    padding: calc(var(--nav-h) + 8px) 24px calc(20px + env(safe-area-inset-bottom));
    border-left: 1px solid var(--glass-brd);
    overflow-y: auto;
    overscroll-behavior: contain;
    transform: translateX(100%);
    transition: transform 0.38s var(--ease-out), opacity 0.38s var(--ease-out),
      visibility 0.38s var(--ease-out);
  }

  .mobile-menu.is-open {
    transform: translateX(0);
  }

  /* Rows, not a poster: 19px links in 52px rows, each ruled off, so the
     tap area is drawn rather than implied. The clamp(30px, 8vw, 44px)
     display type it had was sized for a full-screen overlay and would run
     two words to the line inside a 360px panel. */
  .mobile-menu-links {
    width: 100%;
    gap: 0;
  }

  .mobile-menu-links a {
    display: flex;
    align-items: center;
    min-height: 52px;
    font-size: 19px;
    letter-spacing: 0;
    border-bottom: 1px solid var(--glass-brd);
  }

  /* The call to action is the foot of the panel with the direct details
     under it — margin-top: auto holds the pair down there however short
     the link list above them is. */
  .mobile-menu-cta {
    margin-top: auto;
    width: 100%;
    justify-content: center;
  }

  .mobile-menu-meta {
    width: 100%;
    line-height: 1.3;
  }

  .mobile-menu-meta a {
    display: inline-flex;
    align-items: center;
    min-height: 44px;
  }

  /* --- Touch targets ----------------------------------------------------
     A partner mark is as tall as its own artwork — 19px for a wide
     wordmark — which is a fine drawing and an impossible tap. The box
     opens to 44px and centres the mark in it; the width, and with it the
     lane's spacing and travel, is untouched. */
  .partner {
    display: flex;
    align-items: center;
    min-height: 44px;
  }

  /* The colour twin is positioned at inset: 0, and an absolutely
     positioned replaced element ignores `bottom` once it has an intrinsic
     height — so in a box taller than the artwork it would hang from the
     top while the mono mark it replaces sits centred, and the hover swap
     would jump. Centred against the same line instead. */
  .partner-colour {
    top: 50%;
    bottom: auto;
    transform: translateY(-50%);
  }

  /* Same centring for a plated tile, which sets its own `top` off the
     card's padding at desktop and would otherwise hang from the top here. */
  .partner--plate .partner-colour {
    top: 50%;
    transform: translateY(-50%);
  }

  /* Footer columns: 14px links on a 10px rhythm are a 17px-tall target.
     The rows open to 44px and the gap comes off, since each row now
     carries its own height. */
  .footer-col ul {
    gap: 0;
  }

  /* min-width as well as min-height: "FAQ" is three letters, and a 27px
     wide target fails for the same reason a 17px tall one does. */
  .footer-col ul a {
    display: inline-flex;
    align-items: center;
    min-width: 44px;
    min-height: 44px;
  }

  /* Telephone and email links inside running copy stay inline — they are
     part of a sentence — but vertical padding on an inline box grows the
     hit area without touching the line box, so the sentence sets exactly
     as it did and the number becomes a 47px target. */
  main p a[href^="tel:"],
  main p a[href^="mailto:"] {
    padding-block: 14px;
  }

  /* Every link in the policy prose, by the same means. The rule above does
     not reach them: the clauses put links in <li> as well as <p>, and most
     of them are ordinary page and site links rather than tel:/mailto:.
     14px takes a 20px line box to a 48px target without moving a word, and
     the clauses are far enough apart vertically that no two of these
     targets can overlap. */
  .legal-section a {
    padding-block: 14px;
  }

  /* The privacy link in the bottom bar of every page: 17px of type, and
     the only link down there. Left inline on purpose — padding on an
     inline box grows the hit area, not the footer row. */
  .footer-bottom a {
    padding-block: 16px;
  }

  /* Form controls: 44px of height and a 16px face, which is also the size
     below which iOS zooms the page on focus. */
  .field input,
  .field select,
  .field textarea {
    min-height: 44px;
    padding: 11px 0 12px;
    font-size: 16px;
  }

  .field textarea {
    height: 132px;
  }

  /* The spam honeypot is visually hidden but still carried a 149x38 box,
     which measures as a tap target no thumb can hit. Collapsed to nothing
     — it stays in the DOM, stays fillable by a bot, and submits with the
     form exactly as before. */
  .field-hp input {
    width: 0;
    height: 0;
    min-height: 0;
    padding: 0;
    border: 0;
  }

  /* --- Small type -------------------------------------------------------
     12px eyebrows and 13px legal lines are desktop fine print; on a phone
     they are under the floor of what is readable. Both come up to 14px,
     and the tracking eases off so the labels still hold one line. */
  .label {
    font-size: 14px;
    letter-spacing: 0.1em;
  }

  .field > label {
    font-size: 14px;
  }

  .footer-bottom {
    font-size: 14px;
  }

  /* The intro h1 is the logo artwork with its text in an .sr-only span.
     white-space: nowrap inside a 1px box makes that text 468px wider than
     the box holding it — invisible, but still a line that cannot fit, and
     it is measured as one. Allowed to break anywhere instead; the clip is
     what hides it, never the nowrap. */
  .sr-only {
    white-space: normal;
    overflow-wrap: anywhere;
  }
}

/* Script blocked or broken: the drawer has no opener, so it stops being a
   drawer and becomes what it already is in the markup — a list of links
   under the header. Nothing here needs JavaScript to be read. */
@media (max-width: 1023px) and (scripting: none) {
  .nav-burger {
    display: none;
  }

  .mobile-menu {
    position: static;
    width: auto;
    padding: calc(var(--nav-h) + 24px) var(--pad) 8px;
    background: transparent;
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
    border-left: 0;
    opacity: 1;
    visibility: visible;
    pointer-events: auto;
    transform: none;
  }
}

/* --------------------------------------------------------------------------
   17a-ii. Tablet portrait (1024–1179px) — the gap 17a left open

   17a stops at 1023px because that is where the header stops being a
   drawer. The hand-placed geometry it unpicks does NOT stop there: those
   offsets and widths are pixels measured against the 1280px container, and
   a viewport between 1024 and 1280 gives the container less than 1280 —
   `.container` is max-width, so below 1280 it tracks the window and every
   fixed offset inside it drifts left with it.

   A 12.9" iPad held upright is exactly 1024px wide, which is why this
   showed up as a tablet bug and not a desktop one. At that width the
   Services lede lands at -66px — the first six characters of
   "Comprehensive" are off the left edge of the screen — the h2's 1051px box
   runs 78px past the right edge, and the plumbing copy, which follows its
   heading (5e in js/main.js), runs 75px past it.

   Measured rather than assumed: the composition is whole at 1180px and up
   (an iPad on its side is 1180 or 1194, and both read correctly), and the
   first thing crosses an edge below that. So the band ends at 1179 and
   nothing at or above 1180 — including every desktop width — reads a line
   of this block.

   Unpicked, not re-placed. Services falls back to the layout the hand
   placement was applied on top of: the two cards in the left column, the
   drawing in the right one, which is `.services-layout` doing what section
   7 already says it does. Same five selectors and the same one-element-deep
   `html [data-mb=...]` specificity as 17a, for the same reason — the
   editor's declarations are !important and load after this file.
   -------------------------------------------------------------------------- */
@media (min-width: 1024px) and (max-width: 1179px) {
  html [data-mb="h2-52yvm"],
  html [data-mb="p-52yvn"],
  html [data-mb="h3-52yvp"],
  html [data-mb="h3-52yvs"] {
    width: auto !important;
    max-width: 100% !important;
    translate: none !important;
  }

  /* The stage is hand-sized to 826px and nudged 349px left to sit under
     headings that are no longer there. It fills its own slot instead, and
     the slot is the one .services-model already sizes to the column. */
  html [data-mb="div-ngk02"] {
    width: auto !important;
    max-width: 100% !important;
    min-height: 0 !important;
    translate: none !important;
  }

  /* The model turns each service heading into a button, and the plumbing
     one carries a 74.5px nudge that lines its label up against the
     hand-placed drawing. With the drawing back in its column there is
     nothing to line up against, and the nudge only indents that one label
     50-odd pixels past the other.

     An id, because `html [data-mb=…] > button.service-switch` is not
     enough here: the editor writes this one as
     `[data-mb=…] > button.service-switch:nth-child(1 of button.service-switch)`,
     three class-level selectors deep, and the `html` trick 17a uses only
     buys two. #services outranks all of it. Both headings rather than the
     one, since neither is placed by hand inside this band. */
  html #services .service-card h3 > button.service-switch {
    translate: none !important;
  }

  /* And with the headings back in a stack, the copy belongs back in the
     flow with them — the same move the 899px block makes, for the same
     reason. Out of flow (section 7) each body is absolute at top: 100% of
     its own card, which is correct only while the two headings are placed
     apart by hand: stacked, opening HVAC drops its copy straight over the
     "Plumbing & Gas Piping" heading underneath it. In flow it pushes that
     heading down instead, which is the right behaviour for a list of two.

     No !important needed: this is the same specificity as the rule it
     overrides and it comes later in the file. */
  .service-card.is-collapsible .service-body {
    position: static;
  }
}

/* --------------------------------------------------------------------------
   17b. Phone (max-width: 767px) — balanced density
   -------------------------------------------------------------------------- */
@media (max-width: 767px) {
  /* 17px/1.55 body, per the brief's balanced density. Set on <body> so
     every block that inherits its size comes up with it. */
  body {
    font-size: 17px;
    line-height: 1.55;
  }

  /* The hero is the photograph, full bleed: it runs to all four edges and
     up under the nav, with the lockup centred on it and the tagline small
     underneath — the arrangement this site opened with on a phone.

     A full screen, not content height. This is a deliberate exception to
     the brief's compact phone hero (headline first, image below, height
     follows content): the ask is for the photo to carry the whole first
     screen. 100svh, never 100vh — vh on iOS is the tall measure that
     assumes the address bar and toolbar are gone, so a hero sized in it
     is taller than the screen it is actually on the whole time the chrome
     is showing.

     Symmetric padding is what makes "centred" true: .intro-inner centres
     its rows with align-content, so an uneven pair would centre the
     lockup against the padding box rather than the screen. Measured off
     the old hero, that lands the sign's ink within 4px of where it sat
     (centre at 45% of the screen — the lede below it carries the block's
     lower half, so the sign itself reads just above the middle). */
  .intro {
    min-height: 100svh;
    padding-top: calc(var(--nav-h) + 16px);
    padding-bottom: calc(var(--nav-h) + 16px);
  }

  /* Undimmed. At 0.44 the photo read as a grey wash behind the sign
     rather than as the photograph the hero is built on; the scrim on
     .intro::after is what buys the copy its legibility, and it is
     weighted to the lower half where the lockup and tagline sit. Phone
     only — the tablet keeps the dimmed wall it shares with 899px. */
  .intro-wall {
    opacity: 1;
  }

  /* Small under the sign, the way the tagline reads in the old hero: a
     tracked caption to the lockup, not a line of body copy. 14px is the
     floor rather than the ~12px it was measured at — below 14 the fit
     protocol counts a <p> as unreadable on a phone — so the measure
     widens to hold the two lines the balance gives it at that size
     instead of spilling to three. */
  .intro-lede {
    font-size: 14px;
    --lede-track: 0.1em;
    width: min(310px, 100%);
  }

  /* Section headings: 26px is the brief's phone ceiling, and 4.5vw of a
     390px screen is 17px, so the clamp is rewritten against the narrow
     column instead of scaled off the desktop one. A long compound word
     breaks rather than pushing its line past the gutter. */
  h2 {
    font-size: clamp(22px, 6.2vw, 26px);
    max-width: none;
    overflow-wrap: anywhere;
  }

  /* The two subpage titles: 36px is the ceiling, 34 leaves the descenders
     their room. */
  .rel-hero h1,
  .faq-hero h1 {
    font-size: clamp(28px, 8.4vw, 34px);
    max-width: none;
    overflow-wrap: anywhere;
  }

  /* Contact's standing statement is display type rather than a section
     heading, but 36px still ran three words to the line. */
  .contact-big {
    font-size: clamp(28px, 7.6vw, 32px);
  }

  /* A question is a body-sized line here, not a small heading. */
  .faq-q-h,
  .faq-q h2,
  .faq-q h3 {
    font-size: 17px;
  }

  .rel-intro p {
    font-size: 17px;
    line-height: 1.6;
  }

  /* 48px between groups is the desktop clamp bottoming out; the brief's
     phone rhythm is 56 to 64 — taken at the bottom of that range, since a
     group here is a heading, a standing quote and five logo lanes, and
     the rule between two of them is already doing the separating. */
  .rel-group {
    --group-pad: 44px;
    --seam-pad: 34px;
  }

  /* One column: every field runs the full width of the form. */
  .form-grid {
    grid-template-columns: 1fr;
  }

  /* Band height, measured against the small viewport. vh on iOS is the
     tall measure — the one that assumes the address bar and the toolbar
     are gone — so a band sized in it is taller than the screen it is
     actually on for as long as the chrome is showing.

     The five bands take turns in one frame again (17d), so this is the
     height of the ONE band on screen, and it is whatever the rest of the
     section leaves: 56px of top padding, a 200px heading block, the 40px
     control row and the gaps, inside a 664px screen that the fixed 65px
     nav is sitting on top of. ~34svh is what is left, and it is still a
     photograph with its caption on it rather than a strip. */
  .sector-band {
    min-height: clamp(190px, 34svh, 246px);
  }

  /* --- Footer, centred ---------------------------------------------------
     Stacked in one column on a phone, the brand blurb, the three headings
     and their links all read as a single centred block rather than a row
     of columns that happen to be on top of each other.

     Scoped to .footer-grid, not .footer: the legal line and the credit
     below it are a sibling (.footer-bottom), and they stay exactly where
     and how they are — left-aligned, same wrap, same rhythm. Nothing here
     can reach them. */
  .footer-grid {
    text-align: center;
  }

  /* text-align only moves inline content, so the two block boxes that are
     narrower than the column need centring in their own right: the logo
     is a block-level image with a set width, and the blurb carries a
     34ch measure. Margin, not width — neither box changes size. */
  .footer-brand img {
    margin-inline: auto;
  }

  .footer-brand p {
    margin-inline: auto;
  }

  /* The link lists are column flex containers, so their items stretch the
     full width by default and text-align does the centring inside each
     row. That holds for the 44px tap targets too — .footer-col ul a is an
     inline-flex box (17a), and it is the line box inside the stretched li
     that centres, so every target keeps the width it was given. */
  .footer-col ul {
    align-items: center;
  }

  /* --- Footer pages, two columns ----------------------------------------
     Seven links stacked one-per-row ran the footer long on a phone, so
     they sit two abreast instead: Services / Process / Sectors / About
     down the left, Relationships / FAQ / Contact down the right. Four
     rows, filled column-first, which is that order.

     This is the arrangement roughed out in the visual editor by dragging
     each link, and the editor records a drag as a per-element
     `translate`. Translate is a paint-time transform — it moves the ink
     and leaves the box behind — so the list went on reserving all seven
     rows while only four of them had anything in them: 144px of dead
     space between About and the heading under it. That gap is the whole
     bug, and laying the columns out for real is what closes it, because
     the list then stands exactly as tall as the rows it uses.

     The drags have to come off as part of the same change, or they apply
     on top of the grid and throw the links back out of their cells. The
     columns land where the drags put them (centres 107.5px and 282.5px
     against the dragged 109 and 283), so this keeps the arrangement and
     only takes away the offsets holding it up.

     Specificity, not just !important: the editor writes
     [data-mb="..."] { translate: … !important }, which a bare
     .footer-col ul a would tie with and lose to on source order. Phone
     only — the editor holds no offsets for these links at the other two
     widths, and both keep their single column. */
  .footer-col[aria-label="Footer pages"] ul {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    grid-template-rows: repeat(4, auto);
    grid-auto-flow: column;
  }

  .footer-col[aria-label="Footer pages"] ul a {
    translate: none !important;
    /* The FAQ link was dragged narrow (27px) as well as across. min-width
       on .footer-col ul a (17a) was already holding its tap target at
       44px, so this only clears the stale declaration behind it. */
    width: auto !important;
  }
}

/* --------------------------------------------------------------------------
   17c. Tablet (768–1023px) — the desktop design reflowed
   -------------------------------------------------------------------------- */
@media (min-width: 768px) and (max-width: 1023px) {
  /* 32px gutters, per the brief. --pad is the one number the whole page
     measures its edges by — the container, the nav bar, the full-bleed
     breakouts that pull back out of it — so it is set once here rather
     than patched section by section, and it is scoped to this width:
     :root keeps its own clamp() at every other size. */
  :root {
    --pad: 32px;
  }

  /* The lockup has the room for the lower half of the desktop clamp
     rather than the phone's 66vw. */
  .intro-inner {
    --logo-w: clamp(320px, 46vw, 420px);
  }

  .intro-lede {
    font-size: 14px;
  }

  /* Two columns at most. The 899px stack already puts most of this page in
     one; the footer and the form are the two grids with the width for a
     pair. */
  .footer-grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }

  .form-grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

/* --------------------------------------------------------------------------
   17d. MOBILE-FIRST PHONE LAYOUT (max-width: 767px)

   The phone is a design in its own right, laid out against a 390x664
   screen: every section reads straight down, and the target for each one
   is that it fits that screen.

   Most of the desktop's one-thing-at-a-time compositions are ways of
   buying width, which a phone does not have and cannot spend, so they
   stand down here:

     - Services folds its two bodies of copy behind their headings, so the
       drawing can stand beside them. Stacked, the fold hides both
       services and buys nothing.
     - Process shows one of four steps at a time, and the FAQ one of seven
       answers. Same trade, same answer: all of them, in order.

   Each is undone in CSS first — so it holds with the script blocked —
   and stood down in js/main.js behind the same 767px query, so the
   markup's own state (aria-hidden, aria-expanded) stays honest about what
   is on screen.

   Sectors is the deliberate exception and keeps its carousel here. What
   its paging buys is HEIGHT, not width: five full-bleed photographs in a
   column is five screens, and paged it is one — heading, lede, band and
   controls together, which is this layout's own goal for a section. The
   partners lane is the other thing still moving sideways, and it is a
   decorative belt of logos rather than a section's content; every firm on
   it is also on the Relationships page.

   Sizes are the brief's phone floors throughout: body copy at 16px or
   more, labels at 13px or more, headings clamped with a phone ceiling,
   and every link, button, summary and form control at least 48px on its
   short side. Sections are then as compact as the copy they carry allows
   — which for several of them is still more than one screen, and that is
   reported rather than solved by hiding half of it.

   Nothing in this block is written above 767px. The 1440 layout is signed
   off and never reads a line of it.
   -------------------------------------------------------------------------- */
@media (max-width: 767px) {
  /* The page does not move sideways, full stop.

     The containment above (<html>, <body>, .sectors) decides how far a
     drag COULD go; this decides whether the browser starts one at all.
     pan-y means a touch drag on the page scrolls it vertically and is
     never interpreted as a horizontal pan, so a finger that wanders off
     the vertical cannot peel the page off to the side — which is the
     empty canvas on the right of the screen recording.

     pinch-zoom is kept deliberately. pan-y on its own would also drop
     pinch, and a page a visitor cannot zoom is a phone accessibility
     failure; this does not touch the zoomed state either, where panning
     is the visual viewport's job and nothing here governs it.

     The sectors carousel is unaffected: it already declares exactly this
     pair on itself (17d, below) and drives its swipe from pointer events
     rather than from a browser pan. */
  html,
  body {
    touch-action: pan-y pinch-zoom;
    overscroll-behavior-x: none;
  }

  /* An 84px bar is a desktop measure. Fixed at the top of a 664px screen
     it is an eighth of everything the visitor can see, and every section
     that clears the header pays for it again. The lockup inside it is
     41px tall, so 64 keeps its air and hands ~20px back to each of them. */
  :root {
    --nav-h: 64px;
  }

  /* The burger is the only control in the bar down here, so it carries
     the phone's target size rather than the 44px floor. */
  .nav-burger {
    width: 48px;
    height: 48px;
  }

  /* --- Section rhythm ---------------------------------------------------
     Set once, at the top, rather than section by section: the brief's
     balanced density is 56-64px of padding with 24-32px between the
     blocks inside a section, and what follows only departs from it where
     a section has a reason to. The desktop's clamps mostly bottom out
     near the top of that range already; the header block is the one that
     does not — 40px under a heading is a desktop distance, and on a phone
     it is a tenth of the screen spent on a gap. */
  .section-head {
    margin-bottom: 24px;
  }

  .section-lede {
    margin-top: 12px;
    line-height: 1.5;
  }

  /* --- Touch targets ----------------------------------------------------
     48px on the short side, which is the phone rule; 17a's 44 is the
     floor for a tablet with a mouse as well as a thumb. */
  .btn {
    min-height: 48px;
    padding-block: 15px;
  }

  .btn-sm {
    min-height: 48px;
    padding: 13px 20px;
    font-size: 12px;
  }

  /* Both axes: the narrowest mark in the lane is a 66px square badge taken
     down to 47 by the phone's --logo-scale, and a 47px target fails for
     the same reason a 19px one does. The artwork keeps its size and is
     centred in the box. */
  .partner {
    min-width: 48px;
    min-height: 48px;
    justify-content: center;
  }

  .footer-col ul a {
    min-width: 48px;
    min-height: 48px;
  }

  /* The lockup is 41px of artwork; the link around it takes the target. */
  .nav-logo {
    min-height: 48px;
  }

  .skip-link {
    min-height: 48px;
    display: inline-flex;
    align-items: center;
  }

  .mobile-menu-meta a {
    min-height: 48px;
  }

  main p a[href^="tel:"],
  main p a[href^="mailto:"] {
    padding-block: 16px;
  }

  .field input,
  .field select,
  .field textarea {
    min-height: 48px;
  }

  /* --- 2. SERVICES ------------------------------------------------------
     Drawing first, then the two services folded under it.

     This is the one composition on the phone that keeps its fold, and it
     is kept for the opposite of the reason the desktop has it. On a
     desktop the fold buys width, so the drawing can stand BESIDE the copy.
     Here it buys height: the model is 278px of a 664px screen, and with
     both bodies of copy open underneath it the second service is already
     off the bottom. Folded, the drawing and both headings are on one
     screen together — you can see the kitchen, and reach either service,
     without scrolling in either direction. Asking for one drops its copy
     in underneath, the way it does at every other width.

     Order only: the markup still reads services-then-model, so the source
     order a screen reader and a crawler get is unchanged. */
  /* 2px, not 32. The section's trailing padding IS the seam into Process,
     and it is the only place to take 30px out of the page without moving
     anything inside this section — the drawing, the two headings and the
     fold all keep the positions they already have, and everything from
     Process down comes up by 30.

     The number is the lift asked for on the two service buttons in this
     view. What is left below the last heading is this 2px plus Process's
     own 22px of top padding, which is a closed seam between two sections
     on the same canvas rather than a band of empty page between them. */
  .services {
    padding-bottom: 2px;
  }

  /* The drawing leads. The two headings read as its controls when they sit
     under it, which is what they are.

     The -58px is the lift the editor was asked for on this view, moved from
     the override into the layout, and it is here rather than there for two
     reasons.

     It holds. The rule 1200 lines up that strips hand placement off this
     stage below 900px (translate/width/min-height, all !important) outranks
     the editor's [data-mb] selector, so the drag was being cancelled on
     every load — moved in the editor, back on reload, however many times it
     was re-done. Nothing overrules a margin in the phone block.

     And it closes the gap instead of opening one. The editor moves things
     with `translate`, which paints an element somewhere else without
     telling the layout — the drawing rises and its box stays, so the two
     headings under it, and Process under them, sit exactly where they were
     and the lift shows up as a hole. A negative top margin takes the 58px
     out of the flow, so the drawing goes up and everything below it comes
     with it. The frame carries ~52px of empty canvas above the room (the
     1.26 aspect against a 2:1 room — see the tablet note on .services-model)
     so what this actually closes is that dead air, and the wireframe still
     clears the lede above it. */
  .services-model {
    order: -1;
    margin-top: -58px;
    margin-bottom: 16px;
  }

  /* One column, always. The 600-899px rule two-ups the cards for a tablet,
     and 600-767 is a phone by this site's breakpoints — a large one, but
     still a column. Stacked, opening the first service pushes the second
     down rather than stretching a row neither heading asked to grow. */
  .services-layout .services-grid {
    grid-template-columns: minmax(0, 1fr);
    gap: 4px;
  }

  /* In the flow, not out of it. The desktop lifts the open copy out of the
     column because its headings are placed by hand over the drawing; here
     they are an ordinary stack, so the copy belongs under its own heading,
     pushing what follows down. (The 899px rule already says this — it is
     repeated because it is load-bearing for this layout, not incidental.) */
  .service-card.is-collapsible .service-body {
    position: static;
  }

  /* The sign is back, because the copy really is folded away behind it
     again. The heading is still the model's control as well as its own
     disclosure — one tap does both, as on every other width. */
  .service-switch {
    /* A 17px heading is a 22px box, and it is a button. Padding takes the
       hit area to 48px and the negative margin gives most of that back
       to the layout, so the target is a thumb's and the heading still sits
       where the column puts it.

       Named, because the sign below has to be placed against it: the pad
       is the distance from the top of the button to the top of the text,
       and getting the two out of step is exactly the bug this fixes. */
    --sw-pad: 13px;
    padding-block: var(--sw-pad);
    margin-block: -8px;
  }

  /* --- The seam into Process ---------------------------------------------
     Both service labels were dragged 112px up in the visual editor, to sit
     them closer under the drawing. A drag is recorded as `translate`, which
     moves the ink and leaves the box behind, so the lift did not shorten
     this section by a pixel: the two buttons painted up over the foot of
     the kitchen and left 112px of reserved, empty column beneath them. That
     dead height, plus the two sections' own padding, is the gap between the
     last service and PROCESS.

     Cleared, the labels fall back into their own boxes — which is 8px under
     the drawing, close to where the drag was reaching for anyway — and the
     empty column goes with them. The seam is then just the section padding
     either side of it, which is already under the brief's phone range and
     is not tightened further here.

     The copy needs no rule of its own: the drop-down body follows whatever
     the button computes to (js/main.js 5d), so blanking the button here
     clears the body with it.

     #services to win the cascade: the editor writes
     [data-mb="h3-…"] > button.service-switch:nth-child(1 of button.service-switch),
     four classes deep and !important. An id outranks it. Phone only — the
     button placements at 1024px up are positioning the sign against the
     drawing in a layout that wants them, and this block is never read
     there. */
  #services .service-switch {
    translate: none !important;
  }

  /* Centred on the first line of the label, rather than the flat 0.6em the
     wider layouts use. That 0.6em is measured from the top of the button
     and knows nothing about the padding above — which is zero on a desktop
     and 13px here, so the sign floated a clear 11px above the text it
     belongs to. The line box is 1.3em (set on the h3 below), so its middle
     is 0.65em under the top of the text, less half the 2px bar.

     Phone only. Above 767px the button has no vertical padding and 0.6em
     already lands on the cap-height, which is what that layout wants. */
  .service-switch::before,
  .service-switch::after {
    top: calc(var(--sw-pad) + 0.65em - 1px);
  }

  .service-card {
    padding-top: 0;
  }

  /* Down from 19px. Both labels still set on one line at 390px, and the
     step down puts a little more air between the two buttons without
     touching the column's rhythm. The 1.3 is what the sign above measures
     itself against — retiming one means retiming both. */
  .service-card h3 {
    font-size: 17px;
    line-height: 1.3;
  }

  .service-card p {
    font-size: 16px;
    line-height: 1.5;
  }

  .service-card p + p {
    margin-top: 10px;
  }

  /* The drawing inside its gutters rather than edge to edge. Full-bleed
     it is 310px of a 664px screen, which is a section's worth of screen
     spent on the illustration to a section that also has to say what the
     practice does. Back inside the column it is 278, and the frame keeps
     the 1.26 ratio the model needs to fit its own room (see the note at
     the 899px rule): anything squarer crops the kitchen, however much
     width the canvas is given.

     Placement is up with the .services-model rule above, which leads this
     section now — both live in one place so the order and the spacing
     cannot drift apart. */
  .services-model .ng-kitchen {
    width: 100%;
    margin-inline: 0;
  }

  /* --- 3. PROCESS -------------------------------------------------------
     One step at a time, the same way the wider views show it: the caption
     sits directly under the drawing and changes with it, so what you are
     reading is always what the model is drawing. The four of them stacked
     in a column — which is what a phone used to get — made the section
     four screens long and left the copy answering a drawing that had
     moved on three phases ago.

     Nothing here re-implements that: the stack into a single grid cell and
     the crossfade between captions are the base rules (section 9), and the
     phone simply stops opting out of them. What it sets is the order —
     drawing, caption, chevrons — and the fallback below. */
  /* Half of what it was. The air above the drawing is this margin plus the
     drawing's own margin-top below, and the two together read as a hole
     between the lede and the plan — measured at 40px, which is a lot of a
     664px screen to spend on nothing. 10 and 10 halves it to 20 and the
     lede still has room to breathe. */
  .process .section-head {
    margin-bottom: 10px;
  }

  .process-stage-sticky {
    padding-bottom: 28px;
  }

  .process-steps {
    order: 2;
  }

  /* Unless nothing is driving the model. No script, or reduced motion, and
     the scene never mounts, the phases never advance and the chevrons are
     never built — so the caption would be stuck on step 01 and three
     quarters of this section's copy would be unreachable. js/main.js marks
     the list paged only once the model is live; without that mark all four
     steps are a plain column, exactly as they were. */
  .process-steps:not(.is-paged) {
    display: flex;
    flex-direction: column;
    gap: 20px;
  }

  .process-steps:not(.is-paged) .process-step,
  .process-steps:not(.is-paged) .process-step.is-current {
    opacity: 1;
    visibility: visible;
    transition: none;
  }

  .process-step-line {
    gap: 12px;
    align-items: baseline;
  }

  .process-num {
    font-size: 14px;
    line-height: 1.3;
  }

  .process-step h3 {
    font-size: 20px;
    line-height: 1.2;
    margin-bottom: 6px;
  }

  .process-step p {
    font-size: 16px;
    line-height: 1.5;
  }

  /* The plan is 152ft x 94ft and keeps that shape. It was capped at 300px
     so that the heading and all four captions could be on screen with it;
     with one caption under it that cap is just a smaller drawing, so it
     takes the column the copy takes and lines up with it. */
  .process-model {
    /* Both gaps halved, measured with the model settled rather than
       mid-draw: 40px from the lede's last line down to the plan and 10px
       from the plan down to the caption, now 20 and 5.

       The 20px above is not spare space on its own — .process-pin is
       pulled up under the header by a negative margin, and an earlier
       attempt at 4px here put the drawing through the lede's last line by
       16px. What made halving safe is that the section-head margin above
       came down with it, so the clearance is shared between the two
       instead of resting on this one number. */
    margin: 10px auto 5px;
  }

  /* Under the caption, where they page it — the same order the markup
     reads in and the same order every other view puts them in. They sat
     above it while all four steps were on screen, because then they only
     turned the drawing; now they turn the drawing and the words with it,
     and they belong with the words. */
  /* Centred, which is the placement that was being asked for by hand. The
     899px rule above starts this row at the left gutter, where it lines up
     with the caption; the drag recorded against it was 115px, and 115px is
     exactly what it takes to centre the pair at 390 (20px gutter, two 58px
     chevrons and a 4px gap = 120px of controls in a 350px content box).

     So it is written as centring rather than as that measurement. A pixel
     offset is only correct at the width it was dragged at — it would sit
     off-centre on every other phone — and it has to survive the reset
     below, which clears hand offsets from these two buttons. Justifying
     the row does both: nothing is translated, so there is nothing for the
     reset to take back, and it re-centres at any width.

     Horizontal only. The vertical position is untouched, which is where
     the drag left it too — its Y offset was 0. */
  .process-controls {
    order: 3;
    justify-content: center;
    margin-top: 10px;
    margin-bottom: 0;
  }

  /* --- The stack closes up, and does it in the layout ---------------------
     The drawing, the caption and the chevrons were each dragged upwards in
     the visual editor to take this empty space out, and a drag is recorded
     as a per-element `translate` — a paint-time transform that moves the
     ink and leaves the box behind. That cannot close a gap: it only moves
     the thing across one, and the gap reappears wherever the thing came
     from. Here it made the section worse, not shorter — the drawing went
     up 20px and opened a 44px hole beneath itself, and the chevrons went
     up 29px into the last line of the paragraph.

     The captions could not move with them at all: all four stack in one
     grid cell and are held against exactly this kind of offset (section 9),
     because three of them were once dragged clean off the sheet. So the
     drags were also inconsistent between phases — 222px, 264px, 29px and
     nothing — which is why only some of the section appeared to shift.

     The margins above are the fix: taken out of the layout, every phase
     closes up by the same amount, and the section really does get shorter
     rather than just moving its parts around. These two clear the drags
     that were standing in for them. Phone only, and the editor holds no
     offsets for either element at the other two widths.

     #process to win the cascade: the chevron rules the editor writes are
     [data-mb="…"] > div.process-controls:nth-child(1 of button.process-arrow)
     … — seven classes deep and !important, which no practical class
     selector outranks. One id does. */
  #process .process-model,
  #process .process-controls .process-arrow {
    translate: none !important;
  }

  /* --- 4. SECTORS -------------------------------------------------------
     The exception to the paragraph above: this is the one composition that
     keeps paging on a phone. Five full-bleed photographs stacked is five
     screens of scrolling for a section that is meant to be a quick read of
     who the practice builds for; paged, the heading, the lede, one band and
     the controls are all on the 664px screen at once, which is the phone
     layout's own test for a section. The width argument doesn't apply —
     nothing here sits side by side at any size, one band is one band.

     The desktop rules are what drive it (.sectors-grid.is-slides, section
     8); this block only sets the phone's measurements. If the script is
     blocked .is-slides is never added, .sectors-grid stays a flex column
     and all five bands read straight down — the fallback is unchanged. */
  .sectors {
    padding: 56px 0 28px;
  }

  .sectors-grid {
    margin-top: 20px;
  }

  /* Swipe (initSwipe in main.js). This one declaration is what splits the
     two axes between the browser and the script: pan-y leaves vertical
     dragging to the page, exactly as it is everywhere else, and reserves
     horizontal for the carousel — so the script never has to preventDefault
     a scroll to keep a swipe, and a drag down the page is handed back to us
     as a pointercancel rather than a half-finished gesture. It is scoped to
     .is-slides so that a phone with the script blocked, where the five
     bands are an ordinary column, keeps plain auto touch behaviour.

     pinch-zoom is kept: pan-y alone would drop it, and a photograph the
     visitor cannot zoom is a phone accessibility failure. */
  .sectors-grid.is-slides {
    touch-action: pan-y pinch-zoom;
  }

  /* The band on screen is pinned at transform:none by .is-current (section
     8) — which is right for every other moment, and wrong for the one where
     a finger is on it. Without this the drag writes --slide-x and nothing
     moves: the paging still lands, but the gesture has no answer while it
     is happening, which is the whole reason for having it. The transform
     only comes back while a drag is live, so the paging transition either
     side of it is untouched. */
  .sectors-grid.is-dragging > .sector-band.is-current {
    transform: translateX(var(--slide-x, 0px));
    /* Promoted for the duration of the drag only. The band is repainted on
       every pointermove here, and unlike the paging transition there is no
       tween for the compositor to own — same reasoning as the scoped
       will-change on .sector-band-media, same scope. */
    will-change: transform;
  }

  /* Nothing here animates a drag — the band tracks the finger 1:1 off an
     inline transition:none — but while one is live the caption must not be
     selectable, or a slow swipe across the h3 starts a text selection and
     the browser's handles come up over the photograph instead. */
  .sectors-grid.is-dragging .sector-band-inner {
    user-select: none;
    -webkit-user-select: none;
  }

  /* The control row is the section's last line on a phone, so it sits
     closer to the band than the desktop's clamp puts it and keeps the
     bottom of the section inside the screen. */
  .sectors-controls {
    margin-top: 12px;
    gap: 14px;
  }

  /* The chevrons were hidden at this width until now, so they never had to
     meet the 48px floor every other control in this block holds to. They
     are the section's only control now — 40px was a desktop-derived clamp
     landing under a thumb. (The dashes keep their 44px ::before square.) */
  .sectors-controls .sectors-arrow {
    --arrow-size: 48px;
  }

  /* The band's own foot and the caption block's stack up to 58px of dead
     photograph under the last line of copy — a fifth of the band, spent on
     nothing, in a section that has to close inside one screen. One 22px
     foot between the two of them instead. */
  .sector-band {
    padding-bottom: 0;
  }

  .sector-band-inner {
    padding-bottom: 22px;
  }

  .sector-band-inner h3 {
    font-size: 20px;
    line-height: 1.2;
    margin-bottom: 6px;
    max-width: none;
  }

  .sector-band-inner p {
    font-size: 16px;
    line-height: 1.45;
  }

  /* --- 5. ABOUT ---------------------------------------------------------
     A portrait at the head of the column, then the principal's copy. */
  .about {
    padding: 32px 0 48px;
  }

  .about-grid {
    gap: 22px;
  }

  /* 420px of portrait is 466px of screen before a word of the copy. At
     242 it is still a photograph of a person and the heading under it is
     on the same screen. (220 + 10%, asked for in chat.) */
  .about-photo-frame {
    max-width: 242px;
    /* Both photographs — the B&W and its colour twin — are children of
       this one frame and fill it, so sizing the frame scales the pair
       together and they stay registered to the pixel. */
    margin-inline: auto;
  }

  /* The figure was dragged off-centre in the visual editor, which pinned
     its width and a +64.5px nudge in mb-overrides.css. Chat asks for it
     centred, so the phone view takes its geometry back: full column
     width, no nudge, and the frame above centres inside it. Higher
     specificity than the editor's [data-mb] rule, which is why it wins.
     Drop these three lines if the portrait should be draggable on phone
     again. */
  .about-grid figure.about-photo {
    width: auto !important;
    max-width: 100% !important;
    translate: none !important;
  }

  .about-copy {
    gap: 12px;
  }

  .about-copy p {
    font-size: 16px;
    line-height: 1.5;
  }

  /* 15px is fine print on a desktop list and under the floor here. */
  .about-creds {
    margin-top: 4px;
    gap: 8px;
  }

  .about-creds li {
    font-size: 16px;
    line-height: 1.4;
    padding-left: 22px;
  }

  /* --- 6. PARTNERS ------------------------------------------------------
     The one lane left, and the exception the phone layout allows: a
     decorative belt rather than a section's content. Every firm on it is
     also a link on the Relationships page, in a heading-and-list layout
     that reads straight down. */
  .partners {
    padding: 8px 0 24px;
  }

  .partners-head {
    margin-bottom: 20px;
  }

  /* --- 7. CONTACT -------------------------------------------------------
     The standing line, the direct details, then the form. */
  .contact {
    padding: 48px 0;
  }

  .contact-big {
    margin-bottom: 14px;
  }

  /* The ruled rows are built to fill whatever height the form leaves
     beside them, which is a two-column arrangement's problem. Stacked,
     three equal rows only stretch the gaps between three short lines. */
  .contact-links {
    grid-template-rows: none;
    gap: 14px;
    padding-top: 20px;
  }

  .contact-item {
    padding-top: 12px;
    gap: 2px;
  }

  .contact-item-key {
    font-size: 13px;
  }

  .contact-item-value {
    font-size: 18px;
  }

  .contact-form {
    padding-top: 26px;
    margin-top: 26px;
  }

  .contact-form-title {
    margin-bottom: 18px;
  }

  .form-grid {
    gap: 18px;
  }

  .field textarea {
    height: 104px;
  }

  .contact-form-foot {
    margin-top: 22px;
  }

  /* --- RELATIONSHIPS ----------------------------------------------------
     Six lanes of logos under two headings; the lanes are the same
     decorative belt the home page carries, and everything else on the
     page is copy that simply stacks. */
  .rel-hero {
    padding: calc(var(--nav-h) + 28px) 0 0;
  }

  .rel-hero h1 {
    margin-top: 10px;
  }

  .rel-jump {
    margin-top: 20px;
    gap: 10px;
  }

  .rel-group .section-head {
    margin-bottom: 18px;
    gap: 16px;
  }

  .rel-intro p {
    line-height: 1.55;
  }

  .rel-intro p + p {
    margin-top: 12px;
  }

  .rel-intro cite {
    margin-top: 12px;
    font-size: 13px;
  }

  .rel-category {
    padding: 22px 0;
  }

  .rel-category-head {
    margin-bottom: 14px;
    gap: 10px;
  }

  .rel-category-head h3 {
    font-size: 19px;
  }

  .rel-count {
    font-size: 13px;
    letter-spacing: 0.1em;
  }

  .rel-cta,
  .faq-cta {
    padding: 44px 0;
  }

  .rel-cta-inner {
    gap: 20px;
  }

  .rel-cta h2,
  .faq-cta h2 {
    font-size: clamp(22px, 6.2vw, 26px);
  }

  /* --- LEGAL (privacy.html) ---------------------------------------------
     A page of running prose, which on a phone is the easiest thing to get
     right and the easiest to get wrong: the body is already the 17px/1.55
     this block sets on <body>, so all that is left is the heading scale and
     the space between clauses.

     The h1 comes down to 34px. The shared clamp bottoms out at 38, which is
     over the brief's 36px ceiling at 390 — the other two subpages predate
     that ceiling and are signed off as they are, but a page being written
     now should meet it. */
  .legal-hero {
    padding: calc(var(--nav-h) + 28px) 0 20px;
  }

  .legal-hero h1 {
    font-size: 34px;
    line-height: 1.1;
    margin-top: 10px;
  }

  .legal-updated {
    margin-top: 14px;
  }

  .legal {
    padding: 4px 0 28px;
  }

  /* Two thirds of the desktop gap. The hairline is doing the separating
     here; on a column this narrow the desktop's 52px reads as the document
     coming apart into unrelated pages. */
  .legal-section + .legal-section {
    margin-top: 28px;
    padding-top: 28px;
  }

  .legal-section h2 {
    font-size: 21px;
    line-height: 1.25;
    max-width: none;
  }

  .legal-section h3 {
    margin-top: 22px;
  }

  /* --- FAQ --------------------------------------------------------------
     Seven questions, each folded behind its own row, exactly as they are
     at every other width. An earlier pass held every answer open here on
     the reasoning that a phone should not hide content behind a tap; in
     practice seven open answers is a very long column to scroll past to
     reach the one you came for, and the fold is what makes the list
     scannable. So the row is a real disclosure again: same <details>, same
     sign, same drop-down.

     Nothing about the fold is phone-specific — the rules in section 10b
     already do all of it — so this block only sets the phone's sizes and
     spacing and otherwise gets out of the way. With JavaScript off the row
     is still a plain <details> that opens on tap, which is the fallback
     section 10b is built around. */
  .faq-hero {
    padding: calc(var(--nav-h) + 28px) 0 20px;
  }

  .faq-hero h1 {
    margin-top: 10px;
  }

  .faq {
    padding: 4px 0 24px;
  }

  /* The row is the control again, so it takes a pointer and a thumb's
     worth of height. align-items stays at the shared baseline rule's
     value: the sign is back and it lines up on the first line of the
     question, not on the middle of a two-line one. */
  .faq-q {
    min-height: 48px;
    padding: 14px 0;
  }

  .faq-a {
    padding: 0 0 18px;
  }

  .faq-a p {
    font-size: 16px;
    line-height: 1.5;
  }
}
