/* ── DBM Global · prototype ─────────────────────────────────────────
   Fluid canvas · content on a 1200 rail that shrinks with the viewport
   Type: Archivo (display/body) + Inter (labels)
   ------------------------------------------------------------------ */

:root {
  /* viewport CONTENT width - app.js keeps this at documentElement.clientWidth,
     which excludes a classic (Windows-style) scrollbar. 100vw does not, so any
     layout derived from it drifts ~8px against the auto-margin-centred rail
     and can overflow the viewport by the scrollbar's width. Every horizontal
     100vw calc in this file reads this var; the fallback is JS-off only. */
  --vwc: 100vw;
  --navy-deep: #0A1A32;
  --navy-900:  #0D1F3B;
  --navy-700:  #12294D;
  --navy-rule: #24405E;

  --blue:      #007CBB;
  --blue-lt:   #7FC4EA;
  --blue-pale: #9FD4F0;
  --blue-soft: #68A6DB;

  --ink:   #12294D;
  --body:  #43505F;
  --muted: #8095AC;
  --dim:   #A9BCD0;

  --mist:  #E9EEF4;
  --mist2: #EDF2F8;
  --rule:  #C9D4E1;
  --rule-lt: #D5DEE9;

  --rail-max: 1200px;
  --gut: 120px;

  --f-display: 'Archivo', system-ui, sans-serif;
  --f-mono: 'Inter', ui-monospace, monospace;

  --ease: cubic-bezier(.22,.61,.36,1);
  /* the header fold travels 138px both ways, so it wants a slow both-ends curve
     rather than the snappy ease-out the rest of the interface uses */
  --hdr-dur: 620ms;
  --hdr-ease: cubic-bezier(.33,.02,.18,1);
}

@media (max-width: 1280px) { :root { --gut: 64px; } }
@media (max-width: 900px)  { :root { --gut: 24px; } }

* { box-sizing: border-box; }
html { scroll-behavior: smooth; }

body {
  margin: 0;
  overflow-x: hidden;
  font-family: var(--f-display);
  font-weight: 400;
  color: var(--body);
  background: #fff;
  -webkit-font-smoothing: antialiased;
}

img { display: block; }
/* figure carries a 40px UA side margin that silently offset the carousel */
figure { margin: 0; }
a { text-decoration: none; color: inherit; }

.rail { width: min(var(--rail-max), 100% - var(--gut) * 2); margin-inline: auto; }

.mono { font-family: var(--f-mono); font-weight: 400; letter-spacing: .12em; font-size: 11px; line-height: 18px; }
.lbl { color: var(--muted); }

.h2 { margin: 0; font-weight: 300; font-size: clamp(28px, 2.8vw, 40px); line-height: 1.3; letter-spacing: -.02em; color: var(--ink); }
.h2--accent { color: var(--blue); }
.h2--onnavy { color: #fff; }
.h3 { margin: 0; font-weight: 300; font-size: 32px; line-height: 40px; letter-spacing: -.02em; color: var(--ink); }
.body { margin: 0; font-size: 16px; line-height: 28px; color: var(--body); }
.body--onnavy { color: var(--dim); }

.split { display: flex; align-items: flex-start; gap: 80px; }
.split__l { flex: 0 1 520px; }
.split__r { flex: 0 1 600px; display: flex; flex-direction: column; gap: 26px; }
/* Once the row stacks, flex-basis stops meaning width and starts meaning HEIGHT,
   so the 520/600 bases were padding every split section with ~1120px of nothing.
   Reset the basis and let both columns take the full width. */
@media (max-width: 1000px) {
  .split { flex-direction: column; gap: 32px; }
  .split__l, .split__r { flex: 0 0 auto; width: 100%; }
}

/* ── button ─────────────────────────────────────────────────────── */
.btn { display: inline-flex; align-items: center; gap: 8px; padding: 6px 6px 6px 24px; border-radius: 2px; width: max-content; }
.btn--white { background: #fff; }
.btn--white .btn__label { color: var(--ink); }
.btn__label { font-size: 14px; line-height: 18px; }
.btn__label.mono { font-size: 11px; line-height: 16px; font-weight: 500; }
.btn__tile {
  width: 40px; height: 40px; flex-shrink: 0; border-radius: 2px; background: var(--blue);
  display: flex; align-items: center; justify-content: center; color: #fff; font-size: 15px; line-height: 1;
  transition: background-color .18s var(--ease), transform .18s var(--ease);
}
.btn__tile--sm { width: 30px; height: 30px; font-size: 13px; }
.btn:hover .btn__tile { background: var(--navy-700); transform: translateX(3px); }

/* Unplated CTA — label, arrow, rule. No box, so it inherits whatever ground it
   sits on and reads as a peer to the nav rather than the only chrome on the page.
   The whole block is the target, not the words: 48px tall clears the touch
   minimum and gives the pointer somewhere to land above and below the text. */
.cta {
  display: inline-flex; flex-direction: column; justify-content: center;
  gap: 11px; height: 48px; width: max-content; color: #fff;
}
.cta__row { display: flex; align-items: center; gap: 14px; }
.cta__label { font-size: 15px; line-height: 22px; }
.cta__arrow { font-size: 14px; line-height: 22px; transition: transform .18s var(--ease); }
.cta__rule {
  display: block; height: 1px; width: 100%; background: currentColor; opacity: .5;
  transition: opacity .18s var(--ease), height .18s var(--ease);
}
.cta:hover .cta__arrow { transform: translateX(6px); }
.cta:hover .cta__rule { opacity: 1; height: 2px; }
/* keyboard focus gets its own outline rather than borrowing the rule, so it is
   never mistaken for a hover */
.cta:focus-visible { outline: 2px solid var(--blue); outline-offset: 6px; }

/* ── header ─────────────────────────────────────────────────────── */
/* Fixed from the first pixel. It used to start absolute and switch to fixed at
   200px, so scrolling from the top let it ride away with the page and then snap
   back — a disappear/reappear rather than a move. Same element throughout now;
   only the backdrop plate changes, and that just fades. */
.hdr { position: fixed; top: 0; left: 0; width: 100%; z-index: 60; }
/* Scrolling down folds it away; scrolling up or hovering the top edge brings it back.
   The IES / investor bar stays put through all of it: the header travels up by its own
   height and the bar is pushed back down by the same amount, so it never leaves. */
/* the fold is a live value now, not a binary state: it tracks scroll directly for
   the first nav-row's worth of travel, then the directional reveal drives it */
.hdr { transform: translateY(calc(-1 * var(--fold, 0px))); }
.hdr.is-eased { transition: transform var(--hdr-dur) var(--hdr-ease); }

.topbar { transform: translateY(var(--fold, 0px)); }
.hdr.is-eased .topbar { transition: transform var(--hdr-dur) var(--hdr-ease); }

/* A transformed element becomes the containing block for its position:fixed
   descendants. The header is transformed for the scroll fold, so the mobile
   takeover — fixed, inset 0, and a child of the header — was resolving against
   the header's own 138px box instead of the viewport. It opened as a sliver
   tucked under the folded bar with the page showing through everywhere else.
   Opening the menu drops the fold, which removes the transform and hands the
   takeover back to the viewport. The header belongs at the top with a menu
   open regardless. */
.hdr.is-open, .hdr.is-open .topbar { transform: none; }

/* invisible catcher along the very top so a hover can summon the folded header */
.peek { position: fixed; top: 0; left: 0; width: 100%; height: 24px; z-index: 59; pointer-events: none; }
.peek.is-armed { pointer-events: auto; }
.hdr__plate {
  position: absolute; inset: 0 0 auto 0; height: 138px; background: var(--navy-deep);
  opacity: 0; pointer-events: none; transition: opacity .22s var(--ease);
}
/* at the very top the bar floats over the hero; hovering it brings the plate in
   so the links stay legible, and it leaves again when the pointer does */
.hdr.is-stuck .hdr__plate, .hdr.is-peeking .hdr__plate { opacity: 1; }

/* the bar outranks the nav row so the fold slides away UNDER it, never across it */
.topbar { position: relative; z-index: 3; height: 34px; background: var(--navy-700); }
.topbar__inner { display: flex; align-items: center; justify-content: space-between; height: 34px; }
.topbar__lede { font-size: 9px; line-height: 12px; color: #8FA6BC; }
.topbar__links { display: flex; gap: 26px; }
.topbar__links a { font-size: 9px; line-height: 12px; color: #fff; opacity: .9; transition: color .16s var(--ease); }
.topbar__links a:hover { color: var(--blue-lt); }

.navrow { position: relative; z-index: 2; height: 104px; display: flex; align-items: center; justify-content: space-between; }
.navrow__logo img { width: 104px; height: 52px; }
.nav { display: flex; align-items: center; gap: 30px; }
.nav a { font-size: 15px; line-height: 18px; color: #fff; padding: 8px 0; position: relative; transition: color .16s var(--ease); }
.nav a::after {
  content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 2px; background: var(--blue);
  transform: scaleX(0); transform-origin: left; transition: transform .22s var(--ease);
}
/* the underline is a hover affordance — nothing opens from the nav any more */
.nav a:hover, .nav a.is-on { color: var(--blue-lt); }
.nav a:hover::after, .nav a.is-on::after { transform: scaleX(1); }
@media (max-width: 1100px) { .nav { display: none; } }


/* ── hero ───────────────────────────────────────────────────────── */
/* Full viewport, and pinned: the cover holds at the top of the screen while every
   section after it scrolls up and covers it. That is the whole effect — the copy
   and the logo strip stay put rather than travelling with the page. Sections are
   already z-index 0 and paint in document order, so each one rides over this. */
.hero { position: sticky; top: 0; width: 100%; height: 100vh; height: 100vh; height: 100svh; min-height: 640px; overflow: hidden; background: var(--navy-900); }
.hero__stage {
  position: absolute; left: 0; top: -30%; width: 100%; height: 160%;
  background-size: cover; background-position: 50%;
  opacity: 0; transition: opacity .8s var(--ease);
  will-change: transform, opacity;
}
.hero__stage.is-on { opacity: 1; }
/* The cross-fade puts an inline z-index on the incoming layer. Everything that sits
   over the photograph therefore needs to outrank it explicitly, or the image paints
   over the headline, dots, credit and logo strip. */
.hero__scrim, .hero__topfade { z-index: 3; }
.hero__head, .hero__dots, .credit, .powered { z-index: 4; }
.hero__scrim {
  position: absolute; inset: 0;
  /* Taken from the Paper board, not approximated. The bottom stop is a LIGHT BLUE,
     not navy — that lift is what gives the carousel its cast. Rebuilding it in navy
     alone read as a grey wash and lost the colour entirely. */
  background: linear-gradient(0deg,
    rgba(23,108,168,.94) 0%,
    rgba(13,31,59,.62) 46%,
    rgba(13,31,59,.30) 66%,
    rgba(13,31,59,.10) 84%,
    rgba(13,31,59,0) 100%);
  background: linear-gradient(0deg,
    oklab(44.9% -0.035 -0.130 / 94%) 0%,
    oklab(21.9% -0.011 -0.051 / 62%) 46%,
    oklab(21.9% -0.011 -0.051 / 30%) 66%,
    oklab(21.9% -0.011 -0.051 / 10%) 84%,
    oklab(21.9% -0.011 -0.051 / 0%) 100%);
}
.hero__topfade { position: absolute; left: 0; top: 0; width: 100%; height: 190px; background: linear-gradient(180deg, rgba(13,31,59,.58) 0%, rgba(13,31,59,.22) 60%, rgba(13,31,59,0) 100%); }
.hero__head { position: absolute; left: 50%; translate: -50%; top: 30%; }
.hero__title { margin: 0; font-weight: 200; font-size: clamp(64px, 7.8vw, 112px); line-height: 1.07; letter-spacing: -.025em; color: #fff; }
/* the two lines are deliberately far apart in weight — the qualifier recedes, the word carries it */
.hero__title--bold, .ftr__we--bold { font-weight: 500; }
.hero__body { margin: 20px 0 0; max-width: 620px; min-height: 84px; font-size: 17px; line-height: 28px; color: #fff; }

/* Slide changes are a real out-then-in: the copy leaves completely before it is
   replaced, so two slides' text is never on screen at once, then it rises back. */
.hero__body, .credit { transition: opacity .26s var(--ease), transform .26s var(--ease); }
.hero.is-out .credit { opacity: 0; transform: translateY(-10px); }
/* the sub-copy's words now rise out of their own masks, so a block-level rise on
   top of that would be two animations doing one job — the credit keeps it */
.hero.is-in .credit { animation: copyIn .52s var(--ease) both; }
.hero.is-in .credit { animation-delay: 90ms; }
@keyframes copyIn { from { opacity: 0; transform: translateY(16px); } to { opacity: 1; transform: none; } }
.hero__dots { position: absolute; left: 50%; translate: -50%; bottom: 168px; display: flex; align-items: center; gap: 12px; }
.hero__dots button { width: 34px; height: 3px; border: 0; padding: 0; cursor: pointer; background: rgba(255,255,255,.34); transition: background-color .25s var(--ease); }
.hero__dots button.is-active { background: #fff; }

/* label above, logos as a seamless marquee below */
/* no backplate — the logos ride on the photograph, with only enough tonal
   protection under them to keep white marks legible */
.powered {
  position: absolute; left: 0; bottom: 0; width: 100%; height: 158px;
  background: linear-gradient(180deg, rgba(10,26,50,0) 0%, rgba(10,26,50,.30) 34%, rgba(10,26,50,.42) 100%);
  padding-top: 26px; overflow: hidden;
}
.powered__label { display: block; color: var(--blue-lt); letter-spacing: .14em; }
.marquee { position: relative; margin-top: 30px; overflow: hidden; }
/* the track holds the sequence twice; translating by exactly one copy loops seamlessly */
.marquee__track { display: flex; align-items: center; width: max-content; animation: marquee var(--dur, 42s) linear infinite; }
.marquee__track img { width: 108px; height: 36px; margin-right: 132px; object-fit: contain; opacity: .92; }
@keyframes marquee { to { transform: translate3d(calc(var(--loop) * -1), 0, 0); } }
.powered:hover .marquee__track { animation-play-state: paused; }
/* No edge fades on the logo scroll. They were meant to soften where the marquee
   leaves the viewport, but over a photograph they read as two navy panels sitting
   on the image rather than as a fade. The track just clips at the edge. */

/* subtle project credit on the hero photograph */
.credit {
  position: absolute; right: var(--gut); bottom: 186px; margin: 0; z-index: 3;
  text-align: right; font-family: var(--f-mono); font-size: 10px; line-height: 16px; letter-spacing: .12em;
  transition: opacity .5s var(--ease);
}
.credit b { display: block; font-weight: 400; color: rgba(255,255,255,.72); }
.credit span { display: block; color: rgba(255,255,255,.4); }

/* ── knockout quote: scroll-linked word highlight ───────────────── */
/* Sections layer in document order. Without this a positioned child — a portfolio
   cell, which needs position for its hover reveal — paints over the section that
   follows it, so the grid animated in on top of Markets instead of under it. */
.page > section { position: relative; z-index: 0; }
/* .page > section outranks .hero on specificity, so the pin has to match it */
.page > .hero { position: sticky; top: 0; }

.statement { background: var(--blue); padding: clamp(64px, 8vw, 110px) 0; }
.statement__copy { margin: 0; max-width: 900px; font-weight: 300; font-size: clamp(28px, 3.4vw, 46px); line-height: 1.35; letter-spacing: -.02em; }
.statement__copy w {
  color: rgba(255,255,255,.28);
  transition: color .32s var(--ease);
}
.statement__copy w.is-lit { color: #fff; }

/* ── integrated delivery · pinned scroll ────────────────────────── */
.delivery { position: relative; background: var(--mist); }
.delivery__pin { position: sticky; top: 0; height: 100vh; min-height: 720px; display: flex; flex-direction: column; justify-content: center; overflow: hidden; }
.delivery__head { padding-bottom: 48px; }
/* scroll-linked handoff at both ends of the pin — see the delivery module */
.delivery__head, .delivery__stage {
  opacity: var(--enter, 1);
  transform: translate3d(0, var(--shift, 0px), 0);
  will-change: opacity, transform;
}
.delivery__stage {
  position: relative; display: flex; align-items: center;
  padding-right: max(var(--gut), calc((100% - var(--rail-max)) / 2));
}
/* The phase names line up with the body copy above them, and the photograph stops
   90px short of the rule so the two never crowd each other. */
.delivery__visuals { position: relative; flex: 1 1 auto; min-width: 0; margin-right: 90px; aspect-ratio: 5 / 4; max-height: 54vh; overflow: hidden; }
.delivery__visuals figure { position: absolute; inset: 0; margin: 0; background-size: cover; background-position: 50%; opacity: 0; transform: scale(1.06); transition: opacity .55s var(--ease), transform .8s var(--ease); }
.delivery__visuals figure.is-on { opacity: 1; transform: none; }
/* 600, not 630 — the stage is padded to the rail on the right, so a 600 column
   puts the rule exactly on the viewport centre line at every width. It was landing
   30px left of centre, which put it out of step with the services rail below. */
.delivery__phases { flex: 0 1 600px; display: flex; flex-direction: column; }

.phase__list { position: relative; padding-left: 30px; }
.phase__rule { position: absolute; left: 0; top: 0; bottom: 0; width: 2px; background: var(--rule); }
/* the marker spans the open phase: its name plus the copy beneath it */
.phase__marker {
  position: absolute; left: 0; top: 0; width: 2px; background: var(--blue);
  height: calc(var(--row, 67px) + var(--dh, 0px));
  transition: transform .58s var(--ease), height .5s var(--ease);
}
.phase { cursor: pointer; border-bottom: 1px solid var(--rule-lt); }
.phase:first-child { border-top: 1px solid var(--rule-lt); }
.phase__row { display: flex; align-items: center; height: 66px; }
.phase__t {
  font-size: 26px; line-height: 34px; letter-spacing: -.02em; color: #8095AC;
  transition: color .3s var(--ease);
}
.phase.is-on .phase__t { color: var(--ink); }
.phase:hover .phase__t { color: var(--ink); }

/* every phase opens to the same reserved height, set at runtime from the longest
   paragraph, so the list is one size and each opens at one rate */
.phase__d {
  height: 0; overflow: hidden; opacity: 0;
  transition: height .5s var(--ease), opacity .32s var(--ease);
}
.phase.is-on .phase__d { height: var(--dh, 0px); opacity: 1; }
.phase__inner { padding-bottom: 0; }
.phase__d p { margin: 0; max-width: 500px; font-size: 16px; line-height: 28px; color: var(--body); }
.phase__pending { display: block; font-size: 10px; letter-spacing: .12em; color: var(--blue); }

/* measuring must see the real content height, not the collapsed state */
.phase__list.is-measuring .phase__d { height: auto; opacity: 1; transition: none; }

@media (max-width: 1000px) {
  /* The pin holds here too — image and rail lock to the screen while the phases
     step through, same as desktop. It used to go static, which is why the section
     just scrolled past on a phone. The stage stacks instead of splitting, and the
     photograph is capped so image + list still fit one screen. --dh stays reserved
     (see lockHeight) so the pinned block is one height and never reflows. */
  /* The sticky header sits over the top of the pin, and now that the photograph
     takes every spare pixel the head is pushed right under it — the first line of
     the title was hidden. Reserve the folded header's height. */
  .delivery__pin { height: 100vh; height: 100svh; min-height: 0; padding: 46px 0 0; }
  .delivery__stage { flex-direction: column; align-items: stretch; padding-right: 0; }
  .delivery__phases { flex: 0 0 auto; margin-left: 0; padding-top: 24px; }
  .delivery__visuals { margin-right: 0; aspect-ratio: 16 / 11; max-height: 28vh; max-height: 28svh; }
}

/* ── portfolio wall ─────────────────────────────────────────────── */
.portfolio { background: var(--navy-900); padding-top: 96px; }
.split--onnavy { padding-bottom: 56px; }
.wall { display: grid; grid-template-columns: repeat(4, 1fr); }
@media (max-width: 1000px) { .wall { grid-template-columns: repeat(2, 1fr); } }
.wall__cell { position: relative; aspect-ratio: 1 / 1; overflow: hidden; }
.wall__bg { position: absolute; inset: 0; background-size: cover; background-position: 50%; transform: scale(1); transition: transform .7s var(--ease); }
.wall__cell:hover .wall__bg { transform: scale(1.06); }
/* rest gradient — light, only where the logo and location sit */
.wall__cell::after {
  content: ''; position: absolute; inset: 0; pointer-events: none;
  background: linear-gradient(0deg, rgba(10,26,50,.92) 0%, rgba(10,26,50,.72) 30%, rgba(10,26,50,.12) 66%, rgba(10,26,50,0) 84%);
  transition: opacity .4s var(--ease);
}
/* hover gradient — deeper and taller, so the revealed copy has something to sit on */
.wall__cell::before {
  content: ''; position: absolute; inset: 0; z-index: 1; pointer-events: none; opacity: 0;
  background: linear-gradient(0deg, rgba(10,26,50,.95) 0%, rgba(10,26,50,.88) 44%, rgba(10,26,50,.5) 74%, rgba(10,26,50,.18) 100%);
  transition: opacity .4s var(--ease);
}
.wall__cell:hover::before { opacity: 1; }
.wall__cell:hover::after { opacity: 0; }

.wall__copy { position: absolute; left: 32px; bottom: 32px; right: 32px; z-index: 2; display: flex; flex-direction: column; gap: 14px; }
.wall__copy img { width: 150px; height: 34px; object-fit: contain; object-position: left center; }
.wall__loc { font-size: 10px; color: #B9CBDD; }

/* the block is bottom-anchored, so the reveal grows upward without shifting the logo lane */
.wall__reveal {
  display: grid; grid-template-rows: 0fr; opacity: 0;
  transition: grid-template-rows .42s var(--ease), opacity .3s var(--ease);
}
.wall__cell:hover .wall__reveal,
.wall__cell:focus-within .wall__reveal { grid-template-rows: 1fr; opacity: 1; }
.wall__revealed { min-height: 0; overflow: hidden; display: flex; flex-direction: column; gap: 14px; }
.wall__desc { margin: 0; font-size: 15px; line-height: 24px; color: #D3DFEC; }
.wall__cta { display: inline-flex; align-items: center; gap: 10px; width: max-content; cursor: pointer; }
.wall__cta:hover .mono { color: var(--blue-lt); }
.wall__cta:hover i { transform: translate(2px, -2px); }
.wall__cta .mono { font-size: 10px; font-weight: 500; color: #fff; }
/* no tile behind the arrow — it was the only filled block left on the wall and
   read as a button pasted onto the photograph */
.wall__cta i {
  flex-shrink: 0; font-style: normal; font-size: 13px; color: #fff;
  transition: transform .2s var(--ease);
}
.wall__cell:hover .wall__cta i { transform: translate(2px, -2px); }

/* ── markets ────────────────────────────────────────────────────── */
.markets { background: var(--mist2); padding: clamp(72px, 8vw, 118px) 0 clamp(72px, 8vw, 110px); overflow: hidden; }
.markets__head { padding-bottom: 64px; }
/* the copy column narrows so the photography can run wider, and both start level */
.markets__body { display: flex; align-items: flex-start; gap: 56px; padding-left: max(var(--gut), calc((100% - var(--rail-max)) / 2)); }
.markets__col { width: 344px; flex-shrink: 0; display: flex; flex-direction: column; }

.switch { display: flex; align-items: flex-end; gap: 36px; margin-top: 26px; }
.switch__tab {
  border: 0; background: none; padding: 0; cursor: pointer; display: flex; flex-direction: column; gap: 12px;
  font-family: var(--f-display); font-size: 22px; line-height: 30px; letter-spacing: -.01em; color: var(--muted); font-weight: 400;
  transition: color .2s var(--ease);
}
.switch__tab i { display: block; height: 2px; background: var(--rule); transition: background-color .2s var(--ease); }
.switch__tab:hover { color: var(--ink); }
.switch__tab.is-active { color: var(--ink); font-weight: 300; }
.switch__tab.is-active i { background: var(--blue); }

.markets__project { margin: -6px 0 26px; font-size: 24px; line-height: 32px; }
.spec { margin: 0; display: flex; flex-direction: column; transition: opacity .25s var(--ease); }
.spec__row { display: flex; flex-direction: column; gap: 5px; padding: 15px 0; border-top: 1px solid var(--rule-lt); }
.spec__row:last-child { border-bottom: 1px solid var(--rule-lt); }
.spec__row dt { font-family: var(--f-mono); font-size: 10px; line-height: 18px; letter-spacing: .12em; color: var(--muted); flex-shrink: 0; }
.spec__row dd { margin: 0; font-size: 15px; line-height: 24px; color: var(--ink); }
/* project copy: out, replace, then rise back with the rows staggered */
/* the counter holds still — only the project copy transitions */
.markets__project, .spec__row { transition: opacity .26s var(--ease), transform .26s var(--ease); }
.markets__col.is-out .markets__project,
.markets__col.is-out .spec__row { opacity: 0; transform: translateY(-10px); }
.markets__col.is-in .markets__project,
.markets__col.is-in .spec__row { animation: copyIn .52s var(--ease) both; animation-delay: var(--d, 0ms); }

.pager { display: flex; align-items: center; gap: 16px; margin-top: 32px; }
.circ {
  width: 48px; height: 48px; flex-shrink: 0; border-radius: 24px; border: 1px solid #B9C7D6; background: none; cursor: pointer;
  display: flex; align-items: center; justify-content: center; font-size: 15px; color: var(--ink);
  transition: background-color .2s var(--ease), color .2s var(--ease), border-color .2s var(--ease);
}
.circ:hover { background: var(--navy-700); border-color: var(--navy-700); color: #fff; }
.circ--fill { background: var(--blue); border-color: var(--blue); color: #fff; }
.circ--fill:hover { background: var(--navy-700); border-color: var(--navy-700); }
.pager__count { margin-left: 12px; color: var(--muted); }

/* carousel — a real track that slides, with the next slide bleeding off-canvas */
.carousel { flex: 1; min-width: 0; overflow: hidden; cursor: grab; }
.carousel.is-dragging { cursor: grabbing; }
.carousel__track { display: flex; gap: 20px; will-change: transform; transition: transform .68s var(--ease); }
.carousel__slide {
  position: relative; width: 760px; height: 500px; flex-shrink: 0; overflow: hidden;
  background-size: cover; background-position: 50%;
  filter: saturate(.92); transition: filter .5s var(--ease);
}
.carousel__slide.is-on { filter: none; }
.carousel__slide figcaption {
  position: absolute; left: 28px; bottom: 24px; z-index: 2;
  font-family: var(--f-mono); font-size: 10px; letter-spacing: .12em; color: #fff;
  opacity: 0; transform: translateY(6px); transition: opacity .4s var(--ease), transform .4s var(--ease);
}
.carousel__slide::after {
  content: ''; position: absolute; inset: 0;
  background: linear-gradient(0deg, rgba(10,26,50,.7) 0%, rgba(10,26,50,0) 42%);
  opacity: 0; transition: opacity .4s var(--ease);
}
.carousel__slide.is-on figcaption { opacity: 1; transform: none; }
.carousel__slide.is-on::after { opacity: 1; }
/* stretch again once stacked, or flex-start collapses the column to its content */
@media (max-width: 1000px) { .markets__body { flex-direction: column; align-items: stretch; padding-right: var(--gut); } .markets__col { width: 100%; } }

/* ── fast facts ─────────────────────────────────────────────────── */
.facts { display: grid; grid-template-columns: 1fr 1fr; background: var(--navy-900); }
@media (max-width: 1000px) { .facts { grid-template-columns: 1fr; } }
.facts__hero { position: relative; min-height: 620px; overflow: hidden; }
.facts__img { position: absolute; left: 0; top: -30%; width: 100%; height: 160%; background-size: cover; background-position: 50%; transition: opacity .5s var(--ease); will-change: transform; }
.facts__scrim { position: absolute; inset: 0; background: linear-gradient(0deg, rgba(10,26,50,.94) 0%, rgba(10,26,50,.82) 32%, rgba(10,26,50,.2) 58%, rgba(10,26,50,0) 78%); }
/* sits on the page gutter like every other block, not on its own 60px inset */
.facts__herocopy { position: absolute; left: var(--gut); right: 60px; bottom: 56px; display: flex; flex-direction: column; gap: 16px; }
.facts__grid { display: grid; grid-template-columns: 1fr 1fr; }
.facts__cell {
  padding: 44px; display: flex; flex-direction: column; gap: 12px; cursor: pointer;
  border-bottom: 1px solid var(--navy-rule); border-right: 1px solid var(--navy-rule);
  transition: background-color .28s var(--ease);
}
/* the outer column stops on the rail rather than running to the page edge */
.facts__cell:nth-child(2n) { border-right: 0; padding-right: var(--gut); }
.facts__cell:nth-child(n+3) { border-bottom: 0; }
.facts__cell:hover { background: rgba(255,255,255,.04); }
.facts__cell:hover .num { color: var(--blue-pale); }

.num { margin: 0; font-weight: 300; letter-spacing: -.04em; color: #fff; font-size: 72px; line-height: 80px; display: flex; align-items: baseline; gap: 8px; transition: color .25s var(--ease); }
.num--xl { font-size: clamp(88px, 9vw, 140px); line-height: 1.06; gap: 10px; }
.num__unit { font-weight: 400; font-size: 26px; line-height: 34px; color: var(--blue-lt); letter-spacing: -.02em; }
.num--xl .num__unit { font-size: 44px; line-height: 52px; }
.sub { color: #8FA6BC; }
.facts .lbl--bright { color: var(--blue-lt); }
.facts__herocopy .lbl--bright { color: var(--blue-pale); }
.facts__herocopy .sub { color: #B9CBDD; }
.facts__herocopy > * { animation: fadeUp .5s var(--ease); }
@keyframes fadeUp { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: none; } }

/* ── safety ─────────────────────────────────────────────────────── */
.safety { background: #fff; }
.safety__band { position: relative; width: 100%; height: clamp(420px, 46vw, 560px); overflow: hidden; }
/* the layer overhangs its frame top and bottom — that surplus is the parallax travel */
.safety__img { position: absolute; left: 0; top: -35%; width: 100%; height: 170%; background: url('assets/img/safety.jpg?a=0.71.7') 50% / cover; will-change: transform; }
.safety__scrim { position: absolute; inset: 0; background: linear-gradient(180deg, rgba(18,41,77,.16) 0%, rgba(18,41,77,.22) 34%, rgba(18,41,77,.62) 66%, rgba(18,41,77,.76) 100%); }
.safety__copy { position: absolute; left: 50%; translate: -50%; bottom: 60px; }
.safety__t1, .safety__t2 { margin: 0; font-size: clamp(34px, 3.7vw, 52px); line-height: 1.2; letter-spacing: -.02em; color: #fff; }
.safety__t1 { font-weight: 300; }
.safety__t2 { font-weight: 200; }
.safety__detail { padding: 88px 0 96px; }

/* ── footer ─────────────────────────────────────────────────────── */
/* the page sits above the footer; scrolling down slides it off and uncovers the footer */
.page { position: relative; z-index: 2; background: #fff; }

.ftr {
  position: fixed; left: 0; bottom: 0; width: 100%; z-index: 1;
  background: var(--navy-900); color: #fff; overflow: hidden;
}
.ftr__bg { position: absolute; inset: 0; overflow: hidden; }
.ftr__img {
  position: absolute; left: 0; top: -18%; width: 100%; height: 136%;
  background: url('assets/img/field-crew.jpg?a=0.71.7') 50% 40% / cover;
  will-change: transform;
}
.ftr__inner, .ftr__legal { will-change: transform, opacity; }
.ftr::before {
  content: ''; position: absolute; inset: 0; z-index: 1; pointer-events: none;
  background: linear-gradient(180deg, rgba(10,26,50,.86) 0%, rgba(13,31,59,.94) 44%, rgba(18,41,77,.985) 100%);
}
/* five proportional columns that fill the 1200 rail exactly, so the last one ends
   on the rail edge instead of wherever its content happened to run to */
/* Four columns per the board: brand lockup, one link column, companies, address.
   Connect sits under the brand and Follow under the address, on a second row. */
.ftr__inner { position: relative; z-index: 2; display: grid; grid-template-columns: 1.7fr .8fr 1.05fr 1.25fr; gap: 60px 48px; padding: 88px 0 64px; }
/* The lockup sits at the TOP of the row, not centred in it — otherwise the mark
   floats below the columns beside it. Its own three parts stay centred on each
   other, so the logo top lines up with the labels on the right. */
.ftr__brand { grid-column: 1; grid-row: 1; align-self: start; display: flex; align-items: center; gap: 29px; }
.ftr__links { grid-column: 2; grid-row: 1; }
.ftr__cos   { grid-column: 3; grid-row: 1; }
.ftr__addr  { grid-column: 4; grid-row: 1; }
.ftr__links a { white-space: nowrap; }
/* Hairline between the mark and the statement. Measured off the board: 103px,
   the height of the statement block — not the full column. Stretching it made it
   read as a long thin ruler running past both sides of the lockup. */
.ftr__divide { width: 1px; height: 103px; flex-shrink: 0; background: rgba(255,255,255,.38); }
@media (max-width: 1100px) { .ftr__inner { grid-template-columns: repeat(2, 1fr); } }
/* The reveal parks the footer with position: fixed. Once it is taller than the
   viewport that clips its top off with no way to reach it, so below the desktop
   layout it goes back to being an ordinary block that scrolls. */
/* The reveal is gated on FIT, not on width: app.js parks the footer only when
   it is shorter than the viewport, and falls back to a plain block when it is
   not. `.ftr.is-flow` is what it sets in that case. */
.ftr.is-flow { position: relative; }   /* relative, NOT static: the gradient ::before and bg are absolute and must stay contained */
.page.is-flow { margin-bottom: 0 !important; }
.ftr__we { margin: 0; font-weight: 200; font-size: 42px; line-height: 50px; letter-spacing: -.02em; color: #fff; }
/* the mark tops out with the labels on the right rather than centring against
   the taller statement beside it */
.ftr__logo { width: 183px; flex-shrink: 0; align-self: flex-start; }
.ftr__logo img { width: 183px; height: 88px; }
/* The other columns carry a mono label above their content. The links have none,
   so they get that label's height plus its gap as padding — otherwise the column
   sits one line proud of everything beside it. */
.ftr__links { display: flex; flex-direction: column; gap: 6px; padding-top: 30px; }
.ftr__links a { font-size: 14px; line-height: 20px; color: #C6CACE; transition: color .16s var(--ease); }
.ftr__links a:hover { color: #fff; }
.ftr__addr p, .ftr__cos li { margin: 0; font-size: 14px; line-height: 20px; color: #C6CACE; }
.ftr__addr { display: flex; flex-direction: column; gap: 12px; }
.ftr__cos { display: flex; flex-direction: column; gap: 12px; }
.ftr__cos ul { margin: 0; padding: 0; list-style: none; display: flex; flex-direction: column; gap: 6px; }
.ftr__connect { grid-column: 1; grid-row: 2; display: flex; flex-direction: column-reverse; align-items: flex-start; gap: 14px; }
.ftr__connectline { margin: 0; width: 298px; font-size: 14px; line-height: 20px; color: #C6CACE; }
/* footer sits on navy, so the CTA inherits white from .cta and needs no variant */
.ftr__cta { width: 170px; }
.ftr__follow { grid-column: 4; grid-row: 2; display: flex; flex-direction: column; gap: 16px; }
.ftr__social { display: flex; gap: 12px; }
.ftr__social a { width: 34px; height: 34px; border: 1px solid rgba(255,255,255,.28); display: flex; align-items: center; justify-content: center; color: #fff; transition: background-color .18s var(--ease), border-color .18s var(--ease); }
.ftr__social a:hover { background: var(--blue); border-color: var(--blue); }
.ftr__legal { position: relative; z-index: 2; height: 68px; border-top: 1px solid rgba(255,255,255,.14); }
.ftr__legalrow { display: flex; align-items: center; justify-content: space-between; height: 67px; font-family: var(--f-mono); font-size: 11px; letter-spacing: .12em; color: #8FA6BC; }

@media (prefers-reduced-motion: reduce) {
  * { animation: none !important; transition-duration: .01ms !important; }
}

/* ═══ MOBILE ═══════════════════════════════════════════════════════
   Matches the Paper artboard `DBMG — Landing page · MOBILE @390`
   ================================================================ */

.burger { display: none; }
.mobnav { display: none; }

@media (max-width: 760px) {
  :root { --gut: 20px; }

  /* header ------------------------------------------------------- */
  .topbar__links { display: none; }
  .topbar__lede { font-size: 8px; }
  .topbar, .topbar__inner { height: 30px; }
  .navrow { height: 82px; }
  .navrow__logo img { width: 84px; height: 42px; }
  .navrow__cta { display: none; }
  .hdr__plate { height: 112px; }

  .burger {
    display: flex; flex-direction: column; justify-content: space-between;
    width: 36px; height: 22px; padding: 0; border: 0; background: none; cursor: pointer;
  }
  .burger i { display: block; height: 2px; background: #fff; transition: transform .3s var(--ease), opacity .2s var(--ease), width .3s var(--ease); }
  .burger i:nth-child(1) { width: 36px; }
  .burger i:nth-child(2) { width: 36px; }
  .burger i:nth-child(3) { width: 24px; }
  .burger[aria-expanded="true"] i:nth-child(1) { transform: translateY(10px) rotate(45deg); }
  .burger[aria-expanded="true"] i:nth-child(2) { opacity: 0; }
  .burger[aria-expanded="true"] i:nth-child(3) { width: 36px; transform: translateY(-10px) rotate(-45deg); }

  /* Full-screen takeover, matching the Paper board: hero photograph under a heavy
     navy wash, five rules, then the contact CTA and the standing details. */
  .mobnav {
    position: fixed; inset: 0; z-index: 1;
    display: flex; flex-direction: column; justify-content: flex-end;
    background: var(--navy-deep); overflow: hidden;
  }
  .mobnav[hidden] { display: none; }

  .mobnav__bg {
    position: absolute; inset: 0;
    background: url('assets/img/hero.jpg?a=0.71.7') 60% 40% / cover;
  }
  .mobnav__scrim {
    position: absolute; inset: 0;
    background: linear-gradient(180deg, rgba(10,26,50,.97) 0%, rgba(10,26,50,.93) 42%, rgba(10,26,50,.86) 72%, rgba(10,26,50,.94) 100%);
  }
  /* the topbar and logo stay above the takeover, and the burger itself is the
     close control — it already morphs into the X the board shows */
  .topbar { z-index: 3; }
  .navrow { z-index: 2; }
  .navrow { position: relative; }

  .mobnav__inner {
    position: relative; z-index: 2;
    padding: 136px var(--gut) 34px; display: flex; flex-direction: column;
    height: 100%; overflow-y: auto;
  }
  .mobnav__list { list-style: none; margin: 0; padding: 0; }
  .mobnav__list a {
    display: block; padding: 20px 0; border-bottom: 1px solid var(--navy-rule);
    font-size: 32px; line-height: 40px; font-weight: 300; letter-spacing: -.02em; color: #fff;
  }
  /* scaled up in the takeover so it reads as a peer to the 32px nav items and
     keeps a comfortable touch target */
  .mobnav__cta { margin-top: 34px; align-self: flex-start; height: 52px; min-width: 170px; }
  .mobnav__cta .cta__label { font-size: 19px; line-height: 26px; }
  .mobnav__cta .cta__arrow { font-size: 17px; line-height: 26px; }

  .mobnav__media { margin-top: 40px; display: flex; flex-direction: column; gap: 14px; }
  .mobnav__lbl { color: var(--blue-lt); font-size: 10px; line-height: 16px; }
  .mobnav__sub { display: flex; flex-wrap: wrap; column-gap: 28px; row-gap: 10px; }
  .mobnav__sub a { font-size: 15px; line-height: 24px; color: var(--dim); }

  .mobnav__foot {
    margin-top: auto; padding-top: 22px; border-top: 1px solid var(--navy-rule);
    display: flex; align-items: flex-end; justify-content: space-between; gap: 20px;
  }
  .mobnav__where { display: flex; flex-direction: column; gap: 6px; }
  .mobnav__where .mono, .mobnav__lang .mono { font-size: 10px; line-height: 16px; color: var(--muted); }
  .mobnav__lang { display: flex; align-items: center; gap: 14px; }
  .mobnav__lang .mono.is-on { color: #fff; font-weight: 500; }
  .mobnav__lang i { width: 1px; height: 12px; background: #3A5878; }

  /* hero --------------------------------------------------------- */
  .hero__head { top: auto; bottom: 208px; }
  .hero__title { font-size: 52px; line-height: 58px; }
  .hero__body { margin-top: 18px; font-size: 15px; line-height: 26px; min-height: 104px; }
  .hero__dots { left: var(--gut); translate: none; bottom: 158px; gap: 8px; }
  .credit { bottom: 166px; }
  .powered { height: 140px; padding-top: 20px; }
  .marquee__track img { width: 92px; height: 30px; margin-right: 56px; }
  .hero__dots button { width: 28px; }


  /* statement ---------------------------------------------------- */
  .statement { padding: 52px 0; }
  .statement__copy { font-size: 28px; line-height: 38px; }

  /* delivery ----------------------------------------------------- */
  /* the runway set in JS has to survive here — clearing the height was what
     unpinned the section on phones */
  .delivery__pin { min-height: 0; }
  /* Everything in the pin has to clear one phone screen: head, photograph, the
     four rows and the open paragraph. The head goes compact and the photograph
     becomes a band rather than a panel so the rail keeps its room. */
  .delivery__head { padding-bottom: 18px; gap: 18px; }
  .delivery__head .h2 { font-size: 24px; }
  .delivery__head .split__r { font-size: 14px; line-height: 22px; }
  .delivery__stage { padding-right: 0; }
  /* Edge to edge, and the height is measured in JS from whatever the head and the
     rail leave behind — a fixed svh cap turned the photograph into a letterbox on
     every phone. The value here is only the pre-measurement fallback. */
  .delivery__visuals { margin-right: 0; aspect-ratio: 3 / 2; max-height: 26vh; max-height: 26svh; }
  .delivery__phases { padding: 16px var(--gut) 0; }
  .phase__row { height: 52px; }
}

/* The fourth phase was running off the bottom on every phone shorter than 844
   once the real copy arrived — the reserved open height grew with it. This trades
   image height and type size, never the number of rows, so all four stay on one
   screen. Threshold raised from 700 to 860 because almost no phone cleared it. */
@media (max-width: 760px) and (max-height: 860px) {
  .delivery__head .h2 { font-size: 21px; }
  .delivery__head .split__r { font-size: 13px; line-height: 20px; }
  .delivery__head { padding-bottom: 12px; gap: 12px; }
  .delivery__phases { padding-top: 12px; }
  .phase__row { height: 46px; }
  /* These two were declared twice, and the looser pair won — which is why the
     tightening this block exists for never actually happened. */
  .phase__t { font-size: 19px; line-height: 26px; }
  .phase__d p { font-size: 13px; line-height: 21px; }

  /* portfolio ---------------------------------------------------- */
  .portfolio { padding-top: 56px; }
  .split--onnavy { padding-bottom: 36px; }
  .wall { grid-template-columns: repeat(2, 1fr); }
  .wall__cell { aspect-ratio: 1 / 1; }
  .wall__copy { left: 20px; right: 20px; bottom: 20px; gap: 10px; }
  .wall__copy img { width: 104px; height: 26px; }
  .wall__loc { font-size: 9px; }
  .wall__desc { font-size: 13px; line-height: 21px; }
  .wall__cta .mono { font-size: 9px; }

  /* markets ------------------------------------------------------ */
  .markets { padding: 56px 0; }
  .markets__head { padding-bottom: 32px; }
  .markets__body { flex-direction: column; gap: 28px; padding-left: var(--gut); padding-right: 0; }
  .markets__col { width: auto; padding-right: var(--gut); }
  .switch { margin-top: 18px; margin-bottom: 0; gap: 28px; }
  .switch__tab { font-size: 19px; line-height: 26px; }
  .markets__project { font-size: 22px; line-height: 30px; }
  .spec__row dt { font-size: 9px; }
  .spec__row dd { font-size: 14px; line-height: 22px; }
  .circ { width: 44px; height: 44px; }
  .carousel__slide { width: 300px; height: 360px; }
  .carousel__track { gap: 12px; }

  /* facts -------------------------------------------------------- */
  .facts { grid-template-columns: 1fr; }
  .facts__hero { min-height: 340px; }
  .facts__herocopy { left: var(--gut); right: var(--gut); bottom: 28px; gap: 10px; }
  .num--xl { font-size: 82px; line-height: 88px; }
  .num--xl .num__unit { font-size: 28px; line-height: 34px; }
  .facts__cell { padding: 24px var(--gut); gap: 8px; }
  .num { font-size: 44px; line-height: 50px; }
  .num__unit { font-size: 18px; line-height: 24px; }
  .facts .mono { font-size: 9px; }

  /* safety ------------------------------------------------------- */
  .safety__band { height: 320px; }
  .safety__copy { bottom: 28px; }
  .safety__t1, .safety__t2 { font-size: 32px; line-height: 40px; }
  .safety__detail { padding: 48px 0 56px; }

  /* footer ------------------------------------------------------- */
  .ftr__inner { grid-template-columns: 1fr 1fr; gap: 36px 40px; padding: 52px 0 40px; }
  .ftr__logo { width: 152px; }
  .ftr__logo img { width: 152px; height: 73px; }
  .ftr__statement { grid-column: 1 / -1; }
  .ftr__we { font-size: 34px; line-height: 42px; }
  .ftr__addr, .ftr__cos { grid-column: 1 / -1; }
  .ftr__connect { grid-column: 1 / -1; }
  .ftr__follow { grid-column: 1 / -1; }
  .ftr__legalrow { font-size: 9px; }
}

@media (max-width: 380px) {
  .carousel__slide { width: 260px; height: 320px; }
  .hero__title { font-size: 46px; line-height: 52px; }
}

/* ── services accordion ─────────────────────────────────────────── */
/* Text on the rail's left edge, photograph bleeding to the right edge of the
   viewport, and the rule sitting in the gutter between them — 30px off the copy,
   which puts it on the frame's centre line at 1440. */
.services { background: var(--mist); padding: 96px 0 104px; overflow: hidden; }
.services__inner {
  display: flex; align-items: flex-start;
  padding-left: max(var(--gut), calc((100% - var(--rail-max)) / 2));
}
/* the column, not the section, owns the rail — so the rule is exactly as tall as
   the list and never runs on past the last row */
.services__col { position: relative; flex: 0 0 570px; min-width: 0; }
.services__rail {
  position: absolute; top: 0; left: calc(100% + 30px);
  width: 2px; height: 100%; background: var(--rule); display: block;
}
/* the lit segment belongs to its row, so it is exactly as tall as that row with
   no measurement at all */
.sv { position: relative; }
.sv__mark {
  position: absolute; top: 0; left: calc(100% + 30px);
  width: 2px; height: 100%; background: var(--blue); display: block;
  opacity: 0; transition: opacity .34s var(--ease);
}
.sv.is-on .sv__mark { opacity: 1; }
.services__visual {
  flex: 1 1 auto; margin-left: 150px; align-self: stretch; min-height: 482px;
  background-size: cover; background-position: 50%;
}

.sv { border-bottom: 1px solid var(--rule); cursor: pointer; }
.sv:first-child { border-top: 1px solid var(--rule); }
.sv__t {
  display: block; padding: 22px 0; font-size: 26px; line-height: 34px;
  letter-spacing: -.02em; color: #5A6C82;
  transition: color .3s var(--ease), font-size .3s var(--ease);
}
.sv.is-on .sv__t { font-size: 32px; color: var(--ink); font-weight: 500; padding-bottom: 0; }
.sv:hover .sv__t { color: var(--ink); }
/* every row opens to the same reserved height, taken from the longest entry, so
   the rail and the rows beneath it move the same distance whichever you pick */
.sv__d { height: 0; overflow: hidden; opacity: 0; transition: height .5s var(--ease), opacity .32s var(--ease); }
.sv.is-on .sv__d { height: var(--sh, 0px); opacity: 1; }
.sv__d p { margin: 16px 0 30px; font-size: 16px; line-height: 28px; color: var(--body); }
.sv__list.is-measuring .sv__d { height: auto; opacity: 1; transition: none; }

@media (max-width: 1000px) {
  .services { padding: 64px 0 72px; }
  .services__inner { flex-direction: column; gap: 32px; padding-left: 0; }
  .services__col { flex: 0 0 auto; width: 100%; padding: 0 var(--gut); }
  .services__rail { display: none; }
  .services__visual { width: 100%; margin-left: 0; min-height: 0; aspect-ratio: 5 / 4; order: -1; }
  .sv__t { font-size: 22px; line-height: 30px; }
  .sv.is-on .sv__t { font-size: 26px; }
  .sv__d p { font-size: 15px; line-height: 26px; }
}
/* measuring pass: no transitions, so the reserved open height can be read in one
   frame instead of guessed from padding arithmetic */
.sv__list.is-snap .sv__d, .sv__list.is-snap .sv__t { transition: none; }

/* ══════════════════════════════════════════════════════════════════
   V2 · VAR F adjustments — applied on top of the V1 system.
   New sections get new class names; V1 rules stay untouched for alt.html.
   ══════════════════════════════════════════════════════════════════ */

/* ── platform band: lit words, "Delivered." in navy when lit ────── */
.statement--band { padding: 78px 0; }
.statement--band .statement__copy { max-width: 760px; font-weight: 500; font-size: clamp(30px, 3vw, 42px); line-height: 1.28; }
.statement--band .statement__copy w:last-child.is-lit { color: var(--navy-deep); }

/* ── intro photo + copy ─────────────────────────────────────────── */
.introphoto { position: relative; height: 680px; overflow: hidden; }
.introphoto__img { position: absolute; inset: 0; background: url("assets/img/site-crane.jpg?a=0.71.7") center / cover no-repeat; }
.introphoto__panel { position: absolute; inset: auto 0 0 0; background: linear-gradient(180deg, transparent, rgba(10,26,50,.92) 38%); padding: 220px 0 64px; }
.introphoto__copy { display: block; max-width: 900px; font-size: 18px; line-height: 32px; color: rgba(255,255,255,.94); }

/* ── mission split ──────────────────────────────────────────────── */
.mission { display: grid; grid-template-columns: 1fr 1fr; min-height: 600px; }
.mission__l {
  background: var(--navy-900); color: #fff;
  display: flex; flex-direction: column; justify-content: center;
  padding: 72px 72px 72px max(24px, calc((var(--vwc, 100vw) - var(--rail-max, 1200px)) / 2 + 24px));
}
.mission__label { color: var(--blue-lt, #7FC4EA); margin-bottom: 28px; }
.mission__title { margin: 0; font-size: clamp(26px, 2.6vw, 36px); line-height: 1.33; font-weight: 500; letter-spacing: -0.015em; max-width: 460px; }
.mission__r { background: url("assets/img/h-plant.jpg?a=0.71.7") center / cover no-repeat; min-height: 320px; }

/* ── services, VAR F layout: visual left, accordion right + head ── */
.services__inner--varf { display: grid; grid-template-columns: minmax(0, 1.05fr) minmax(0, 1fr); gap: clamp(40px, 6vw, 96px); align-items: center; }
.services__inner--varf .services__visual { order: 0; }
.services__inner--varf .services__col { order: 1; }
.services__head { margin-bottom: 28px; }
.services__title { margin: 0; font-size: 28px; font-weight: 600; letter-spacing: -0.01em; }
.services__sub { margin: 12px 0 0; font-size: 15px; line-height: 26px; color: var(--body, #43505F); }
.services__inner--varf .services__visual { position: relative; }
.services__inner--varf .services__visual::after { content: ""; position: absolute; inset: 0; background: rgba(0,124,187,.25); mix-blend-mode: multiply; pointer-events: none; }

/* ── blue quote band ────────────────────────────────────────────── */
.bandquote { background: var(--blue); padding: 88px 0; }
.bandquote__copy { display: block; margin: 0; max-width: 1013px; font-size: clamp(22px, 2.2vw, 30px); line-height: 1.45; font-weight: 500; letter-spacing: -0.01em; color: #fff; }

/* ── markets: index row + sector card rail ──────────────────────── */
.markets__indexrow { display: flex; justify-content: space-between; align-items: center; gap: 32px; margin-top: 56px; padding-bottom: 18px; border-bottom: 1px solid var(--line, #D9E2EC); }
.mindex { display: flex; gap: 26px; overflow-x: auto; scrollbar-width: none; }
.mindex::-webkit-scrollbar { display: none; }
.mindex button { white-space: nowrap; font-size: 13px; font-weight: 500; color: var(--muted, #8095AC); padding: 0 0 8px; border: 0; border-bottom: 2px solid transparent; background: none; cursor: pointer; transition: color .15s; }
.mindex button:hover { color: var(--ink, #12294D); }
.mindex button.is-active { color: var(--ink, #12294D); border-color: var(--blue); }
.markets__scroll { overflow-x: auto; margin-top: 36px; scrollbar-width: none; cursor: grab; }
.markets__scroll::-webkit-scrollbar { display: none; }
.markets__scroll:active { cursor: grabbing; }
.mcards { display: flex; gap: 20px; width: max-content; padding: 0 max(24px, calc((var(--vwc, 100vw) - var(--rail-max, 1200px)) / 2 + 24px)) 80px; }
.mcard { width: 420px; flex-shrink: 0; }
.mcard__img { height: 300px; background-size: cover; background-position: center; }
.mcard__name { margin: 20px 0 0; font-size: 19px; font-weight: 600; letter-spacing: -0.01em; }
.mcard__desc { margin: 8px 0 0; font-size: 14px; line-height: 23px; color: var(--body, #43505F); }

/* ── fast facts: simple centered row ────────────────────────────── */
.facts--row { display: block; background: #fff; padding: 60px 0; border-top: 1px solid var(--line, #D9E2EC); }
.facts__rowgrid { display: grid; grid-template-columns: repeat(4, 1fr); }
.factcell { text-align: center; }
.factcell__num { margin: 0; font-size: clamp(36px, 3.6vw, 52px); font-weight: 500; letter-spacing: -0.02em; color: var(--ink, #12294D); }
.factcell__lbl { display: block; margin-top: 10px; font-size: 11px; letter-spacing: .12em; color: var(--muted, #8095AC); line-height: 18px; }

/* ── safety: headline on white above the band (VAR F order) ─────── */
.safety__headline { padding: 72px 24px 48px; }
.safety__headline .safety__t1 { color: var(--ink, #12294D); font-weight: 500; }
.safety__headline .safety__t2 { color: var(--muted, #8095AC); font-weight: 400; font-size: clamp(20px, 2vw, 26px); margin-top: 10px; }

/* ── careers ────────────────────────────────────────────────────── */
.careers { background: #fff; padding: 72px 0 120px; border-top: 1px solid var(--line, #D9E2EC); }
.careers__grid { display: grid; grid-template-columns: minmax(0, 1.1fr) minmax(0, 1fr); gap: clamp(48px, 6vw, 96px); }
.careers__l .h2 { margin-bottom: 36px; }
.careers__img { height: 400px; background: url("assets/img/field-crew.jpg?a=0.71.7") center / cover no-repeat; }
.careers__head { margin: 0; font-size: clamp(24px, 2.2vw, 30px); line-height: 1.33; font-weight: 500; letter-spacing: -0.015em; }
.careers__r .body { margin-top: 22px; }
.careers__link { display: inline-flex; gap: 10px; margin-top: 32px; font-size: 15px; font-weight: 600; color: var(--blue); }
.careers__link:hover { text-decoration: underline; }

/* ── V2 mobile ──────────────────────────────────────────────────── */
@media (max-width: 1000px) {
  .mission { grid-template-columns: 1fr; min-height: 0; }
  .mission__l { padding: 72px 24px; }
  .services__inner--varf { grid-template-columns: 1fr; }
  .services__inner--varf .services__visual { min-height: 320px; }
  .mcard { width: 320px; }
  .mcard__img { height: 240px; }
  .careers__grid { grid-template-columns: 1fr; }
  .facts__rowgrid { grid-template-columns: repeat(2, 1fr); gap: 36px 0; }
  .introphoto { height: 540px; }
}

@media (max-width: 760px) {
  .mcard { width: 280px; }
  .mcard__img { height: 210px; }
  .markets__indexrow { flex-direction: column; align-items: flex-start; gap: 18px; }
  .safety__headline { padding: 56px 24px 36px; }
  .bandquote { padding: 64px 0; }
  .introphoto { height: 460px; }
  .introphoto__copy { font-size: 16px; line-height: 28px; }
}

/* ══════════════════════════════════════════════════════════════════
   V2 · VAR F FIDELITY PASS — exact values from the Paper selection.
   Wins over the earlier V2 block by cascade order.
   ══════════════════════════════════════════════════════════════════ */

/* ── hero composition (static composition per the board) ────────── */
.hero__head { padding-top: 217px; }
.hero__dots { display: none; }
.hero__scrim { background: linear-gradient(0deg, rgba(18,84,157,.94) 0%, rgba(13,31,59,.62) 46%, rgba(13,31,59,.30) 66%, rgba(13,31,59,.10) 84%, rgba(13,31,59,0) 100%); }
.powered { border-top: 0.5px solid rgba(255,255,255,.37); padding-bottom: 0; height: 158px; display: flex; flex-direction: column; justify-content: center; }
.powered__label { color: #7FC4EA; font-family: "Inter", sans-serif; font-size: 11px; letter-spacing: .14em; margin-bottom: 18px; }
.marquee { border-top: 0; padding-top: 0; }

/* ── platform band: 60px light + semibold "Delivered." + watermark ── */
.statement--band { position: relative; padding: 78px 0; overflow: clip; }
.statement--band .statement__copy { max-width: 681px; font-weight: 300; font-size: clamp(34px, 4.2vw, 60px); line-height: 1.18; letter-spacing: -0.02em; }
.statement--band .statement__copy w.is-lit { color: #fff; }
.statement--band .statement__copy w:last-child.is-lit { color: #fff; font-weight: 600; }
.band__watermark { position: absolute; left: 322px; top: -62px; width: 1191px; opacity: .1; pointer-events: none; }

/* ── intro photo + statement: left-darkened overlay, 36px light ─── */
.introphoto__img { background: url("assets/img/site-crane.jpg?a=0.71.7") 50% 40% / cover no-repeat; }
.introphoto__panel { background: linear-gradient(0deg, rgba(13,31,59,.94) 0%, rgba(13,31,59,.80) 34%, rgba(13,31,59,.24) 62%, rgba(13,31,59,.04) 86%); padding: 280px 0 96px; }
.introphoto__copy { max-width: 900px; font-weight: 300; font-size: clamp(24px, 2.5vw, 36px); line-height: 1.39; letter-spacing: -0.02em; color: #fff; padding: 0 24px; margin: 0 auto; }

/* ── mission split: 40px regular, no eyebrow ────────────────────── */
.mission__title { font-weight: 400; font-size: clamp(28px, 2.8vw, 40px); line-height: 1.4; letter-spacing: -0.02em; }

/* ── preconstruction services: board-exact rows ─────────────────── */
.services--varf { position: relative; }
.services__inner--varf { grid-template-columns: 658px minmax(0, 1fr); gap: clamp(48px, 6vw, 124px); padding-block: 89px 78px; align-items: start; }
.services__inner--varf .services__visual { height: 482px; }
.svc__rule { position: absolute; left: calc(50% - 600px + 599px); width: 2px; pointer-events: none; }
.svc__rule--blue { top: 78px; height: 186px; background: #007CBB; }
.svc__rule--gray { top: 147px; height: 434px; background: #C9D4E1; left: calc(50% - 600px + 599px + 20px); }
.services__inner--varf .services__head { display: none; } /* head copy lives in the open row now */
.services__inner--varf .sv__t { font-weight: 400; font-size: 26px; line-height: 34px; letter-spacing: -0.02em; color: #5A6C82; }
.services__inner--varf .sv.is-on .sv__t { font-weight: 500; font-size: 32px; color: #12294D; }
.services__inner--varf .sv { border-bottom: 1px solid #C9D4E1; }
.services__inner--varf .sv__d p { font-size: 16px; line-height: 28px; color: #43505F; }

/* ── portfolio: 96px head, 32px copy, 4×360 tiles ───────────────── */
.portfolio__head { padding-top: 50px; padding-bottom: 48px; }
.portfolio__title { font-weight: 300; font-size: clamp(56px, 6.7vw, 96px); line-height: 1.04; letter-spacing: -0.035em; color: #fff; }
.portfolio__copy { font-size: clamp(20px, 2.2vw, 32px); line-height: 1.25; color: #fff; max-width: 555px; }
.wall { grid-template-columns: repeat(4, 1fr); }
.wall__cell { aspect-ratio: 1 / 1; height: auto; }

/* ── quote band: 42px light + watermark ─────────────────────────── */
.bandquote { position: relative; padding: 60px 0; overflow: clip; }
.bandquote__copy { display: block; margin: 0 auto; max-width: 900px; padding: 0 24px; font-weight: 300; font-size: clamp(26px, 2.9vw, 42px); line-height: 1.43; letter-spacing: -0.02em; color: #fff; }
.band__watermark--quote { left: 427px; top: -62px; width: 1013px; }

/* ── markets head: 96px title + 60px tabs + 20px copy ───────────── */
.markets__title { margin: 0; font-weight: 300; font-size: clamp(56px, 6.7vw, 96px); line-height: 1.04; letter-spacing: -0.035em; color: #1A3768; }
.markets__r { max-width: 555px; padding-top: 8px; }
.markets__copy { font-size: 20px; line-height: 24px; color: #43505F; }
.switch { gap: 36px; border-bottom: 0; margin-bottom: 22px; }
.switch__tab { font-weight: 300; font-size: clamp(34px, 4.2vw, 60px); line-height: 1.26; letter-spacing: -0.035em; color: #D6E2F0; padding-bottom: 0; }
.switch__tab.is-active { color: #1A3768; }
.switch__tab.is-active::after { display: none; }

/* ── market index: 17px tabs + progress rule ────────────────────── */
.mindex button { font-size: 17px; line-height: 28px; font-weight: 400; color: rgba(26,55,104,.35); border-bottom: 0; padding-bottom: 16px; }
.mindex button.is-active { color: #1A3768; }
.markets__indexrow { position: relative; padding-bottom: 0; border-bottom: 0; margin-top: 64px; }
.markets__indexrow::before { content: ""; position: absolute; left: 24px; right: 24px; bottom: 0; height: 1px; background: #C9D4E1; }
.markets__indexrow::after { content: ""; position: absolute; left: 24px; bottom: 0; height: 2px; width: 246px; background: #007CBB; }

/* ── market cards: 420×520 dark card, chip + title + body ───────── */
.mcards { gap: 24px; padding-block: 24px 80px; }
.mcard { position: relative; width: 420px; height: 520px; background: #14161A; overflow: clip; cursor: pointer; }
.mcard__img { position: absolute; inset: 0; height: 100%; }
.mcard__scrim { position: absolute; left: 0; right: 0; top: 150px; height: 370px; background: linear-gradient(180deg, rgba(20,22,26,0) 0%, rgba(20,22,26,.58) 42%, rgba(20,22,26,.95) 100%); }
.mcard__chip { position: absolute; left: 24px; bottom: 158px; max-width: 372px; display: inline-flex; align-items: center; height: 32px; padding-inline: 16px; background: rgba(255,255,255,.20); border: 1px solid rgba(255,255,255,.36); border-radius: 2px; font-size: 12px; line-height: 16px; color: #fff; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.mcard__text { position: absolute; left: 24px; right: 24px; bottom: 34px; }
.mcard__name { margin: 0 0 12px; font-size: 20px; line-height: 28px; font-weight: 500; letter-spacing: -0.01em; color: #fff; }
.mcard__desc { margin: 0; font-size: 12px; line-height: 19px; color: rgba(255,255,255,.68); }

/* ── fast facts: 68px + suffix, two-tone labels, column rules ───── */
.factcell { padding: 32px 24px 34px; }
.factcell + .factcell { border-left: 1px solid #C9D4E1; }
.factcell__num { font-weight: 300; font-size: clamp(44px, 4.7vw, 68px); line-height: 1.03; letter-spacing: -0.035em; color: #1A3768; }
.factcell__suffix { font-size: 30px; letter-spacing: -0.02em; color: #007CBB; margin-left: 4px; }
.factcell__l1, .factcell__l2 { display: block; font-family: "Inter", sans-serif; font-weight: 500; font-size: 13px; line-height: 18px; letter-spacing: .11em; }
.factcell__l1 { color: #5A6C82; margin-top: 14px; }
.factcell__l2 { color: #1A3768; }

/* ── safety: three bands per the board ──────────────────────────── */
.safety__bandA { background: linear-gradient(180deg, #0E8ED0, #0A6EAE); padding: 72px 0 48px; }
.safety__bandA .safety__t1 { color: #fff; font-weight: 300; font-size: clamp(56px, 6.7vw, 96px); line-height: 1.04; letter-spacing: -0.035em; }
.safety__copy .safety__t2 { color: #fff; font-weight: 300; font-size: clamp(34px, 3.6vw, 52px); line-height: 1.19; letter-spacing: -0.02em; }
.safety__bandC { background: #E9EEF4; }
.safety__bandC .safety__detail { background: linear-gradient(180deg, #0A6EAE, #084E80); padding-top: 88px; padding-bottom: 96px; }
.safety__claim { font-weight: 300; font-size: clamp(28px, 2.8vw, 40px); line-height: 1.3; letter-spacing: -0.02em; color: #fff; }
.safety__copyr p { font-size: 18px; line-height: 28px; color: #fff; }

/* ── careers: board-exact ───────────────────────────────────────── */
.careers { padding: 72px 0 120px; }
.careers__title { margin: 0 0 40px; font-weight: 300; font-size: clamp(56px, 6.7vw, 96px); line-height: 1.04; letter-spacing: -0.035em; color: #1A3768; }
.careers__head { font-weight: 400; font-size: clamp(28px, 2.8vw, 40px); line-height: 1.3; color: #1A3768; }
.careers__body { margin-top: 34px; display: grid; gap: 20px; }
.careers__body p { font-size: 18px; line-height: 24px; color: #43505F; }
.careers__link { position: relative; display: inline-flex; gap: 14px; margin-top: 54px; font-size: 17px; line-height: 24px; font-weight: 400; color: #1A3768; }
.careers__link i { position: absolute; left: 0; right: 0; bottom: -8px; height: 1px; background: rgba(26,55,104,.5); }
.careers__link:hover { text-decoration: none; }
.careers__r .body { display: none; } /* replaced by careers__body */

/* ── footer: #131517 board colors ───────────────────────────────── */
.ftr { background: #131517; }
.ftr__legal { background: #0B182E; }

/* ── V2 fidelity mobile ─────────────────────────────────────────── */
@media (max-width: 1000px) {
  .services__inner--varf { grid-template-columns: 1fr; }
  .svc__rule { display: none; }
  .wall { grid-template-columns: repeat(2, 1fr); }
  .mcard { width: 320px; height: 420px; }
  .mcard__chip { bottom: 130px; }
  .factcell + .factcell { border-left: 0; }
}
@media (max-width: 760px) {
  .wall { grid-template-columns: 1fr; }
  .mcard { width: 280px; height: 380px; }
  .switch__tab { font-size: 34px; }
}

/* mobile: chip rides the top of the card so it can't collide with wrapped titles */
@media (max-width: 1000px) {
  .mcard__chip { bottom: auto; top: 16px; }
}

/* ── VAR F reconciliation pass ──────────────────────────────────── */
/* page ground is light blue-gray; sections carry their own fills */
.page { background: #E9EEF4; }
.services--varf { background: #fff; }
.facts--row { border-top: 0; }
.introphoto__panel { margin-bottom: 0; }

/* services: photo bleeds to the canvas left edge per the board */
.services__inner--varf { max-width: none; padding-left: 0; grid-template-columns: 658px minmax(0, 570px); justify-content: start; column-gap: 124px; padding-right: 24px; }

/* footer: darker navy blue. The legal bar used to go a step darker again at
   #060F1F, which put a near-black strip under the footer and read as a third
   colour rather than the end of one. It shares the footer's ground; the 14%
   rule above it is what separates them. */
.ftr { background: #0B182E; }
.ftr__legal { background: #0B182E; }
.ftr__divide, .ftr__follow { border-color: rgba(255,255,255,.12); }

/* ══════════════════════════════════════════════════════════════════
   V2 · VAR F fix pass 2 — principal review items
   ══════════════════════════════════════════════════════════════════ */

/* platform band: rises once via motion split — no scroll-lit words ── */
.statement--band .statement__copy { color: #fff; }

/* intro photo: lit words live here; parallax surplus; exact metrics ─ */
.introphoto__img { inset: -18% 0; height: 136%; }
.introphoto__panel { padding: 280px 0 96px; }
.introphoto__copy { padding: 0; }
.introphoto__copy w { color: rgba(255,255,255,.32); transition: color .28s ease; }
.introphoto__copy w.is-lit { color: #fff; }

/* safety: bands per the board ────────────────────────────────────── */
.safety__bandA { padding: 72px 0 48px; }
.safety__bandA .rail { padding-inline: 0; }
.safety__copy { left: 0; right: 0; translate: none; bottom: 64px; }
.safety__copy .safety__t2 { text-align: left; }
.safety__bandC { background: #E9EEF4; padding: 56px 0; }
.safety__bandC .safety__detail {
  max-width: none; margin: 0; padding: 88px 120px 96px;
  background: linear-gradient(180deg, #0A6EAE, #084E80);
}
.safety__bandC .safety__detail .split__l,
.safety__bandC .safety__detail .split__r { padding: 0; }

/* careers: spec rails — 120 left / 162 right, 560 + 555 columns ──── */
.careers { padding: 72px 0 120px; }
.rail.careers__grid { max-width: none; margin: 0; padding: 0 162px 0 120px; display: flex; justify-content: space-between; gap: 0; }
.careers__l { width: 560px; flex-shrink: 0; }
.careers__r { width: 555px; padding-top: 8px; }
.careers__img { height: 400px; }

/* footer: no photo, board gradient ───────────────────────────────── */
.ftr__bg { display: none; }
.ftr::before { background: linear-gradient(180deg, rgba(0,0,0,.36) 0%, rgba(18,84,157,.84) 100%); }

/* powered-by strip: board metrics + edge fades ───────────────────── */
.powered { height: 158px; }
.powered__label { margin-bottom: 30px; }
.marquee__track img { height: 36px; }
.marquee::before, .marquee::after {
  content: ""; position: absolute; top: 0; bottom: 0; width: 120px; z-index: 2; pointer-events: none;
}
.marquee::before { left: 0; background: linear-gradient(90deg, rgba(13,31,59,.62), rgba(13,31,59,0)); }
.marquee::after { right: 0; background: linear-gradient(270deg, rgba(13,31,59,.62), rgba(13,31,59,0)); }
.marquee { position: relative; }

/* header: persistent compact mode — scales down, never hides ─────── */
.hdr .navrow { transition: height .25s ease; }
.hdr .navrow__logo img { transition: width .25s ease, height .25s ease; }
.hdr .nav a { transition: font-size .25s ease, color .16s ease; }
/* The plate is the header's own ground. It was a fixed 138 (34 bar + 104 row),
   so once the row shrank to 60 it hung 44px below the bar it was backing. It
   tracks the compact height now, and the compact row went 60 → 76 because at
   60 the logo had 12px above and below it and read pinched. */
.hdr__plate { transition: opacity .22s var(--ease), height .25s ease; }
.hdr.is-compact .navrow { height: 76px; }
.hdr.is-compact .hdr__plate { height: 110px; }   /* 34 bar + 76 row */
.hdr.is-compact .navrow__logo img { width: 80px; height: 40px; }
.hdr.is-compact .nav a { font-size: 14px; }
.hdr.is-compact .cta { font-size: 14px; }

/* ══════════════════════════════════════════════════════════════════
   V2 · VAR F fix pass 3 — principal review, 2026-08-07
   Numbers are read off the Paper build, not estimated: hero head
   ZE6-0, powered strip 119K-0, intro photo ZQI-0/ZQN-0, safety
   10KK-0/ZKY-0, careers 10RX-0/10SD-0.
   ══════════════════════════════════════════════════════════════════ */

/* ── a control is not a paragraph ─────────────────────────────────
   <button> does not inherit font-family, so the market index and the
   carousel arrows were rendering in Arial next to Archivo everywhere
   else. This is the "JetBrains is still showing up" report: it was
   never JetBrains, it was the UA default. */
button, input, select, textarea { font: inherit; letter-spacing: inherit; }
.mindex button, .circ, .switch__tab { font-family: var(--f-display); }

/* one label face, one fallback chain — the strip and the fact cells
   had each declared their own */
:root { --f-mono: 'Inter', system-ui, sans-serif; }

/* ── the rail origin, as a value ──────────────────────────────────
   --gut is a fixed 120 and only equals the rail on a 1440 canvas. The
   board IS 1440, so anything positioned at var(--gut) looked correct
   here and drifted the moment the window was wider: at 1920 the rail
   centres at 360 while a hard 120 stays at 120. Everything that has to
   sit on the rail but cannot inherit .rail's auto margins — absolute
   boxes, section padding, full-bleed plates — reads this instead. */
:root { --rail-x: max(var(--gut), calc((var(--vwc, 100vw) - var(--rail-max)) / 2)); }
.powered__label, .factcell__l1, .factcell__l2 { font-family: var(--f-mono); }

/* ── hero: head clears the logo strip ─────────────────────────────
   The board puts the head at top 217 of a 775-tall hero with 88px of
   air above the strip. Ours is 100svh, so top-anchoring at 217 (as a
   padding, on top of the existing top: 30%) drove the sub-copy straight
   through "POWERED BY". Anchor from the bottom instead and the 88px
   gap holds at any viewport height. */
.hero__head {
  left: 0; right: 0; translate: none;     /* .rail's own auto margins place it */
  top: auto; bottom: 246px;               /* 158 strip + 88 air */
  padding-top: 0;
}
/* The board sets this at 17/28 on a 620 measure. Against a 112 display it
   reads undersized on screen, so it is up to 20/32 on a 730 measure, which
   keeps the three-line wrap the board has. Director's call over the board. */
.hero__body { max-width: 730px; font-size: 20px; line-height: 32px; }

/* "Builders." at 500 carries a 9.1px left sidebearing where "We Are" at 200
   carries 1.6px, so setting both flush to the rail leaves the second line
   optically indented by ~7.5px at 112. Correct in em so it holds down the
   clamp. Same pair, same correction, in the footer. */
.hero__title--bold, .ftr__we--bold { margin-left: -0.067em; }

/* ── markets: index rule travels, strip starts on the rail ────────
   Both the rule and the card strip were inset by a leftover 24px from
   before the rail moved to 120, so the index read at 120 while the blue
   rule and the first card sat at 144. The rule is also no longer a fixed
   246px — app.js writes the active item's box into --mk-x / --mk-w. */
.markets__indexrow::before { left: 0; right: 0; }
.markets__indexrow::after {
  left: var(--mk-x, 0px); width: var(--mk-w, 246px);
  transition: left .42s var(--ease, cubic-bezier(.22,.61,.36,1)), width .42s var(--ease, cubic-bezier(.22,.61,.36,1));
}
.mcards { padding-left: var(--rail-x); }

/* ── preconstruction: anchored to the centre, not to x=0 ──────────
   On the board the photo bleeds off the canvas edge to 658, the rule sits
   at 719 and the accordion starts at 782 — all of which read against the
   1440 centre at 720. The grid was laid from x=0 with fixed tracks, so
   past 1440 the rule (already centre-based) walked away from the column
   it belongs to. Track one now ends 62 short of centre and the 124 gap
   puts the accordion 62 past it, at any width. */
.services__inner--varf { grid-template-columns: calc(50% - 62px) 570px; column-gap: 124px; }
.services__inner--varf .services__visual { width: 100%; margin: 0; }

/* ── powered-by strip: board metrics ──────────────────────────────
   label at 26 from the strip top, logos at 82, which leaves 40px of
   air under the logos instead of the 8px the centred flex column gave. */
.powered { display: block; padding: 0; }
.powered::before {
  content: ""; position: absolute; inset: 0; z-index: 0; pointer-events: none;
  background: linear-gradient(180deg, rgba(13,31,59,0) 0%, rgba(13,31,59,.34) 30%, rgba(13,31,59,.46) 100%);
}
.powered__label {
  position: absolute; left: var(--rail-x); top: 26px; width: auto; margin: 0;
  font-size: 11px; line-height: 16px; letter-spacing: .14em;
}
.powered .marquee { position: absolute; left: 0; right: 0; top: 82px; height: 36px; margin: 0; }
.marquee__track img { margin-right: 72px; }

/* ── intro photo: copy on the rail, gradient over the whole frame ──
   ZQJ-0 is a full-frame 1440×680 overlay and ZQN-0 sits at left 120 /
   bottom 96 / width 900. The copy was centred by an inherited
   margin-inline: auto, which put it 270 off the rail. */
.introphoto__img { background-image: url("assets/img/intro-deck.jpg?a=0.71.7"); background-position: 50% 46%; }
.introphoto__panel { position: absolute; inset: 0; padding: 0; }

/* ── intro photo pins while the copy lights ───────────────────────
   The section is a scroll track; the frame inside it sticks to the top.
   The words light over the first stretch of that track, then the mission
   is pulled up by exactly one screen so it rises over the held frame
   instead of pushing it off. app.js drives both off the same progress. */
.introphoto__pin { position: relative; height: 100%; overflow: clip; }
@media (min-width: 901px) {
  /* overflow: hidden on the track would make it the sticky child's scroll
     container, and a container that never scrolls never lets it stick.
     The clipping belongs on the frame now, which is where the photo is. */
  .introphoto { height: 330vh; height: 330svh; overflow: visible; }
  .introphoto__pin { position: sticky; top: 0; height: 100vh; height: 100svh; }
  .mission { position: relative; z-index: 1; margin-top: -100vh; margin-top: -100svh; }
}
.introphoto__copy {
  position: absolute; left: var(--rail-x); bottom: 96px;
  width: 900px; max-width: calc(100% - var(--rail-x) * 2);
  margin: 0; padding: 0;
}

/* ── safety: band A gets its height, band C its full-bleed plate ───
   .safety__detail carries .rail, and .rail is width + auto margins.
   Zeroing the margin alone left the plate 1200 wide pinned to x=0. */
.safety__bandA { min-height: 261px; }
/* ZKX-0 and ZKY-0 are both 1440×434 — the plate fills the wrapper and the
   #E9EEF4 never shows, so the 56px band was a seam the board doesn't have */
.safety__bandC { padding: 0; }
.safety__bandC .safety__detail {
  width: auto; max-width: none; margin: 0;
  padding: 88px var(--rail-x) 96px;
  justify-content: space-between; gap: 0;
}
.safety__bandC .safety__detail .split__l { flex: 0 0 645px; }
.safety__bandC .safety__detail .split__r { flex: 0 0 555px; gap: 26px; }

/* ── careers: on the rail ─────────────────────────────────────────
   The previous selector was .rail.careers__grid and the element never
   carried .rail, so none of it applied and the section ran edge to
   edge. The photograph is an absolute 580×400 at left 120 / top 273
   on the board, not a flow item under the title. */
.careers { padding: 0; }
.careers__grid {
  position: relative; max-width: none; width: 100%; margin: 0;
  display: flex; align-items: flex-start; justify-content: space-between; gap: 0;
  /* the board's 162 right rail is the 120 gutter plus 42, so it stays
     42 inside the rail rather than 162 off the window at any width */
  padding: 72px calc(var(--rail-x) + 42px) 120px var(--rail-x);
  /* the photograph is out of flow, so the section has to reserve its box:
     273 top + 400 tall + the board's 56 below it */
  min-height: 729px;
}
/* intrinsic, not 645 — 645 + 555 exceeds the 1158 content box and the
   space-between collapses, pushing the copy column off the right rail */
.careers__l { flex: 0 1 auto; }
.careers__r { width: 555px; flex-shrink: 0; padding-top: 8px; }
.careers__img {
  position: absolute; left: var(--rail-x); top: 273px;
  width: 580px; height: 400px;
  background-image: url("assets/img/careers-crew.jpg?a=0.71.7"); background-position: 50% 44%;
}
.careers__body { gap: 26px; }
.careers__body p { margin: 0; }

/* ── mobile parity for the pass above ─────────────────────────────── */
@media (max-width: 1100px) {
  .careers__grid { flex-direction: column; padding: 56px var(--gut) 88px; gap: 40px; }
  .careers__l, .careers__r { width: 100%; }
  .careers__img { position: static; width: 100%; height: clamp(240px, 46vw, 400px); margin-top: 36px; }
  .safety__bandC .safety__detail { flex-direction: column; padding: 56px var(--gut) 64px; gap: 32px; }
  .safety__bandC .safety__detail .split__l,
  .safety__bandC .safety__detail .split__r { flex: 0 0 auto; width: 100%; }
  .safety__bandA { min-height: 0; padding: 56px 0 36px; }
}
@media (max-width: 900px) {
  /* NOT width: 100%. .hero__head carries .rail, and .rail is a width plus auto
     margins — overriding the width killed the inset outright, so the hero title
     and its sub-copy sat flush at x=0 on a phone while every other block held
     the 20px gutter. Let .rail size it. */
  .hero__head { bottom: 200px; }
  .hero__body { max-width: none; font-size: 17px; line-height: 28px; }
  .powered { height: 132px; }
  .powered__label { top: 20px; }
  /* 30px flat clipped the descenders on the stacked wordmarks */
  .powered .marquee { top: 60px; height: 34px; }
  .marquee__track img { height: 28px; margin-right: 44px; }
  /* the copy is ten lines on a 350 rail — at 460 it ate the whole frame and
     the photograph read as a texture behind text rather than a photograph */
  .introphoto { height: 640px; }
  .introphoto__copy { bottom: 40px; width: 100%; font-size: clamp(19px, 5.1vw, 24px); line-height: 1.42; }
}

/* ══════════════════════════════════════════════════════════════════
   V2 · VAR F fix pass 4 — principal review, 2026-08-07 (second batch)
   ══════════════════════════════════════════════════════════════════ */

/* ── preconstruction pins and steps through its rows ──────────────
   Same track/frame pattern as the intro photo: the section is a scroll
   track, the frame sticks, and scroll position picks the open row rather
   than a click. The stage inside the frame is the board's own 663-tall
   composition, so the rail coordinates stay board-exact instead of being
   re-derived against a viewport-height box. */
.services__pin { position: relative; }
.services__stage { position: relative; }

@media (min-width: 901px) {
  /* 100svh longer than the five row-steps need: app.js reserves that last
     screen so the portfolio can rise over the still-held frame rather than
     arriving exactly as it releases, which reads as a push. */
  .services--varf { height: 450vh; height: 450svh; padding: 0; overflow: visible; }
  .portfolio { margin-top: -100vh; margin-top: -100svh; }
  .services__pin {
    position: sticky; top: 0; height: 100vh; height: 100svh; overflow: clip;
    display: grid; align-content: center; background: #fff;
  }
  .services__stage { width: 100%; }
}

/* The rail is one gray rule down the accordion with a blue marker over the
   open row. The board draws both at the same x (719 on a 1440 frame); ours
   had nudged the gray 20px clear of the blue, which read as two rails. */
/* measuring pass: every transition off so the marker can be given each row's
   destination box instead of a frame from the middle of the animation */
.sv__list.is-nofx, .sv__list.is-nofx * { transition: none !important; }
.svc__rule { left: calc(50% - 1px); }
.svc__rule--gray { left: calc(50% - 1px); top: 89px; height: 482px; background: #C9D4E1; }
.svc__rule--blue {
  top: 0; height: 186px; background: #007CBB; z-index: 2;
  transition: transform .42s var(--ease), height .42s var(--ease);
}

/* ── portfolio: the board's rhythm, not ours ──────────────────────
   1273 tall against the board's 1046. The section carried a 96px top pad
   the board does not have, the head an extra 56 on top of its own 48, and
   the wall was missing the board's -36 pull into the head. The copy also
   sat top-aligned against a two-line 96px headline, which is the unevenness
   — the board centres the two columns against each other. */
.portfolio { padding-top: 0; }
.portfolio .portfolio__head { align-items: center; padding: 50px 0 48px; }
.portfolio .wall { margin-top: -36px; }

/* Display heads and section copy were still carrying the UA's em-based
   block margins — 79.7px above and below a 96px h2, 31.7 around a 24px p.
   That is where 160 of the portfolio's excess height came from. */
.portfolio__title, .portfolio__copy,
.markets__title, .markets__copy,
.mission__title, .careers__title, .careers__head,
.safety__t1, .safety__t2, .safety__claim, .safety__copyr p,
.statement__copy, .bandquote__copy { margin: 0; }

/* the board's left column is a 264 box whichever way its title wraps */
.portfolio .split__l, .markets__head .split__l { min-height: 264px; }

/* ── markets: same story, 1401 against the board's 1050 ───────────
   All of the excess was section padding the board puts inside its own
   blocks: head 72/28, index row a flat 118 with the hairline at 46 from
   its top, card strip 24 above and below. */
.markets { padding: 0; }
.markets__head { padding: 72px 0 28px; }
.markets__indexrow { height: 118px; align-items: flex-start; margin-top: 0; padding: 0; }
.mindex { column-gap: 34px; }
.markets__indexrow::before { top: 46px; bottom: auto; }
.markets__indexrow::after { top: 45px; bottom: auto; }
.markets__pager { position: absolute; right: 0; top: 0; }
.markets__scroll { margin-top: 0; }
.mcards { padding-block: 24px; }

/* A card opens up as you point at it; the strip is a scroll rail, so the
   others keep their width and the rail simply gets longer. The selector has
   to out-specify motion.css's [data-m-rise], which loads after this file and
   replaces the whole transition shorthand — width would never animate. */
.markets__scroll .mcards .mcard {
  transition-property: opacity, transform, width;
  transition-duration: 1.15s, 1.15s, .42s;
  transition-timing-function: cubic-bezier(.16,.84,.24,1), cubic-bezier(.16,.84,.24,1), cubic-bezier(.22,.61,.36,1);
}
/* Gated on a real pointer. :hover latches on touch — a tap left the card stuck
   at 520 with no way to release it — and the earlier mobile override was a
   two-class selector losing to this one's five, so it never applied at all.
   Scoping the rule itself is the fix; overriding it afterwards was not. */
@media (hover: hover) and (pointer: fine) and (min-width: 901px) {
  .markets__scroll:not(:active) .mcards .mcard:hover { width: 520px; }
}
/* nothing to animate where nothing can hover */
@media (hover: none), (pointer: coarse) {
  .markets__scroll .mcards .mcard { transition-property: opacity, transform; }
}

@media (max-width: 900px) {
  .services--varf { height: auto; }
  .services__pin { position: relative; height: auto; display: block; }
  .markets__indexrow { height: auto; padding-bottom: 18px; }
  .markets__indexrow::before, .markets__indexrow::after { top: auto; bottom: 0; }
  .markets__pager { position: static; }
}

/* the board draws one rail beside the accordion, not two — the column's own
   right-hand rule is a leftover from the pre-VAR-F layout */
.services--varf .services__rail { display: none; }

/* ══════════════════════════════════════════════════════════════════
   V2 · VAR F fix pass 5 — principal review, 2026-08-07 (third batch)
   ══════════════════════════════════════════════════════════════════ */

/* the accordion's old right-hand lit segment. The board's rail and its
   travelling marker are on the LEFT of the column now, so this drew a second
   blue line down the right of every open row. */
.services--varf .sv__mark { display: none; }

/* intro statement runs the full rail rather than the board's 900 — at 900 it
   stopped 300 short of the grid it sits in */
.introphoto__copy { width: var(--rail-max); max-width: calc(100% - var(--rail-x) * 2); }

/* ── the two watermarked bands ────────────────────────────────────
   .bandquote had no inline padding at all, so its copy started at x=0 while
   every other section began on the rail. Both ghosted marks are pushed off
   the right edge instead of sitting mid-frame over the copy. */
.bandquote { padding-inline: var(--rail-x); }
.bandquote__copy { margin: 0; max-width: 900px; padding: 0; }
/* Halfway between the board (quote mark at 427, band mark at 322) and the
   first correction, which read too far right. Right-anchored so both keep the
   same crop off the edge whatever the window is doing. */
.band__watermark { left: auto; right: -165px; }
.band__watermark--quote { left: auto; right: -110px; }

/* ── sections layer over one another ──────────────────────────────
   Each band holds at the top while the next one rises over it. Every one of
   them needs an opaque ground or the section beneath shows through, and the
   z-order has to climb in document order so later always covers earlier. */
/* `.page > section` sets position and z-index at (0,1,1), so these have to be
   written the same way or none of it lands — the same trap the hero pin hit. */
/* top is NOT 0. These bands are taller than the viewport, and a 0 stick would
   hold each one from its first screen — the rest of it would be covered by the
   next section before it ever scrolled into view. app.js writes
   min(0, viewport - height) into --stick-top, so a tall band scrolls through
   normally and only holds once its LAST screen is on show.

   A section is held only when the NEXT one should rise over it. Two are
   deliberately free:
   · portfolio — the wall and the quote band travel as one pass, so the quote
     must not cover the wall. The quote is what gets held instead, and Markets
     is what rises over it.
   · markets   — fast facts belongs to Markets We Serve and reads as its last
     row. Holding markets would make facts arrive as a layer on top of it
     rather than locking to the bottom of the card strip. Facts is held, so
     Safety still layers over the pair. */
.page > .mission, .page > .bandquote, .page > .markets,
.page > .safety, .page > .careers {
  position: sticky; top: var(--stick-top, 0px);
}
/* facts rides WITH markets: it holds at the foot of the screen, where the held
   band ends, so the pair reads as one view. app.js writes its --stick-top. */
.page > .facts { position: sticky; top: var(--stick-top, 0px); }
.page > .mission        { z-index: 1; }
.page > .services--varf { z-index: 2; }
.page > .portfolio      { position: relative; z-index: 3; }
.page > .bandquote      { z-index: 4; }
.page > .markets        { z-index: 5; background: #E9EEF4; }
.page > .facts          { z-index: 6; background: #fff; }
.page > .safety         { z-index: 7; background: #fff; }
.page > .careers        { z-index: 8; background: #fff; }

@media (max-width: 900px) {
  .introphoto__copy { width: 100%; }
  .bandquote { padding-inline: var(--gut); }
  .band__watermark, .band__watermark--quote { right: -40%; }
  /* the layering runs on a phone too — superseded by the --stick-top block
     further down, which holds a tall band only once its last screen shows */
}

/* ── logo strip back to V1's spacing ──────────────────────────────
   Measured off localhost:4321, not guessed: strip 158 with 26 of padding, the
   label on the rail at 26, the marquee at 74 (26 + an 18 label + 30), logos
   108×36 on a 132 gap for a 240 pitch, and no edge fades — V1 dropped those
   deliberately. The board's 72 gap packed the marks together and the fades ate
   the first and last one. */
.powered {
  border-top: 0;
  background: linear-gradient(180deg, rgba(10,26,50,0) 0%, rgba(10,26,50,.30) 34%, rgba(10,26,50,.42) 100%);
}
.powered::before { content: none; }
.powered .marquee { top: 74px; }
.marquee::before, .marquee::after { content: none; }
.marquee__track img { width: 108px; height: 36px; margin-right: 132px; opacity: .92; }

@media (max-width: 900px) {
  .powered { height: 140px; }
  .powered__label { top: 20px; }
  .powered .marquee { top: 68px; height: 30px; }
  .marquee__track img { width: 92px; height: 30px; margin-right: 56px; }
}

/* ── ticker marks link out ────────────────────────────────────────
   Spacing moves onto the anchor so the track's loop measurement — one
   sequence's width, read off the first child of the second copy — is unchanged. */
.marquee__link { display: inline-flex; align-items: center; flex-shrink: 0; margin-right: 132px; }
.marquee__link img { margin-right: 0; opacity: .92; transition: opacity .2s ease; }
.marquee__link:hover img { opacity: 1; }

/* ── preconstruction photo cross-fades ────────────────────────────
   background-image cannot be transitioned, so rewriting it cut straight from one
   photograph to the next. Two stacked layers fade instead. */
.services__visual { position: relative; overflow: hidden; }
.sv__layer {
  position: absolute; inset: 0; display: block;
  background-size: cover; background-repeat: no-repeat;
  opacity: 0; transition: opacity .55s var(--ease);
}
.sv__layer.is-on { opacity: 1; }

/* intro statement steps up — 36 read small on a 1200 measure */
.introphoto__copy { font-size: clamp(22px, 2.6vw, 36px); line-height: 1.38; }

@media (max-width: 900px) {
  .marquee__link { margin-right: 56px; }
  .introphoto__copy { font-size: clamp(21px, 5.6vw, 26px); line-height: 1.4; }
}

/* ── the lit sweep, continuous ────────────────────────────────────
   app.js writes a 0–1 --lit per word and the colour is mixed from it, so the
   edge of the sweep moves with the scroll rather than snapping a word at a
   time. No transition: the value already changes every frame, and easing it
   would only make the sweep lag the scroll it is supposed to follow. */
.introphoto__copy w {
  transition: none;
  color: color-mix(in oklab, #fff calc(var(--lit, 0) * 100%), rgba(255,255,255,.28));
}
.introphoto__copy w.is-lit { color: inherit; }

/* ── mission: the photograph slides in from the right ─────────────
   Was a 50/50 grid where both halves rose 42px on the same curve, which read
   as two blocks nudging up together. The photograph now takes the larger share
   and travels horizontally into place while the copy still rises, so the two
   move on different axes and the panel reads as arriving rather than settling.
   The frame clips, so the image is oversized inside it and only its own drift
   is visible — the slide never exposes an edge. */
.mission { grid-template-columns: minmax(0, 42fr) minmax(0, 58fr); min-height: 620px; }
.mission__r { position: relative; overflow: clip; background: none; }
.mission__r::before {
  content: ""; position: absolute; inset: 0 -12% 0 0;
  background: url("assets/img/h-plant.jpg?a=0.71.7") 50% 44% / cover no-repeat;
}
/* Two motions, not one wipe. A clip-path uncovers a panel that was always
   there; the backplate is meant to arrive, so it TRAVELS — the whole navy
   block slides in from the left with its copy aboard. The photograph does not
   move at all: it scales down into its frame from the right edge, so the two
   halves read as different gestures meeting in the middle rather than one
   effect applied twice.

   The section carries the page's own ground so there is something for the
   plate to slide across. Without it the plate would be sliding over whatever
   the mission is currently covering, which is the pinned intro photo. */
.mission { background: #E9EEF4; }

.m-ready .mission__l[data-m-rise] {
  opacity: 1; clip-path: none;
  transform: translateX(-100%);
  transition: transform 1.25s cubic-bezier(.16,.84,.24,1) var(--m-delay, 0ms);
}
.m-ready .mission__l[data-m-rise].is-in { transform: none; }

.m-ready .mission__r[data-m-rise] { opacity: 1; clip-path: none; transform: none; }
.mission__r::before { transform: scale(1.18); transform-origin: right center; }
.m-ready .mission__r[data-m-rise]::before {
  transition: transform 1.6s cubic-bezier(.16,.84,.24,1) var(--m-delay, 0ms);
}
.m-ready .mission__r[data-m-rise].is-in::before { transform: scale(1); }
.mission__title { max-width: 520px; }

@media (max-width: 900px) {
  .mission { grid-template-columns: 1fr; }
  .mission__r::before { inset: 0 -8% 0 0; transform: scale(1.1); }
}

/* ── preconstruction: the composition stops recentring ────────────
   The frame centres its stage, and the stage was sized by its content. Open
   rows are not all the same height — row one's title wraps at 32 where the
   others do not, so the list ran 215 to 249 — and every change re-centred the
   whole composition by half the difference. That is the jump between rows.
   The stage is a fixed box now: the list still grows inside it, nothing moves
   around it. */
.services__stage { height: 663px; }
.services__inner--varf { align-items: start; height: 100%; }

/* photograph sits above the accordion's top line rather than level with it */
.services__inner--varf .services__visual { align-self: start; margin-top: -56px; }

@media (max-width: 900px) {
  .services__stage { height: auto; }
  .services__inner--varf { height: auto; }
  .services__inner--varf .services__visual { margin-top: 0; }
}

/* The mission panel starts translated 18% right, which pushed the page's scroll
   width out by 150px before it revealed. Clipping at the section contains the
   travel and gives the panel somewhere to slide in from. */
.mission { overflow: clip; }

/* the bar travels as one plate; .ftr already clips, so it rises from under its
   own bottom edge without opening a gap */

/* ══════════════════════════════════════════════════════════════════
   V2 · VAR F fix pass 6 — mobile parity + section rhythm
   ══════════════════════════════════════════════════════════════════ */

/* ── careers: one rhythm instead of three margins ─────────────────
   The board's copy frame is a flex column on a single 54 gap. Ours stacked a
   34 margin on the body and a 54 on the link, so the head sat closer to the
   copy than the copy sat to the CTA and the column read lopsided. */
.careers__r { display: flex; flex-direction: column; align-items: flex-start; gap: 54px; }
.careers__r > * { margin: 0; }
.careers__body { display: grid; gap: 26px; margin: 0; }
.careers__link { margin: 0; }
.careers__l { display: flex; flex-direction: column; gap: 39px; }

/* ── markets: the right column on the board's 32 ──────────────────
   The switch carried a 22 bottom margin and the copy its own leading, which
   put the body closer to the tabs than the board's 32 and left the column
   feeling top-heavy against a 96 display head. */
.markets__r { display: flex; flex-direction: column; align-items: flex-start; gap: 32px; }
.markets__r > * { margin: 0; }
.switch { margin: 0; gap: 36px; }
.markets__copy { font-size: 20px; line-height: 30px; max-width: 555px; }

/* ── mobile ───────────────────────────────────────────────────────
   Preconstruction was still running the desktop grid on a phone: a fixed 570
   second track on a 390 screen pushed the whole accordion off the right edge.
   It is a plain stack there. */
@media (max-width: 900px) {
  .services__inner--varf {
    display: block; grid-template-columns: none;
    max-width: none; padding: 0 0 56px; height: auto;
  }
  .services__inner--varf .services__visual {
    width: 100%; height: clamp(230px, 58vw, 340px); margin: 0 0 34px;
  }
  .services--varf .services__col { width: 100%; padding: 0 var(--gut); }
  .services--varf .svc__rule { display: none; }

  /* the 264 column box is a desktop measure; on a phone it opened a dead
     screen-third between the head and the switch */
  .portfolio .split__l, .markets__head .split__l { min-height: 0; }
  .markets__head { padding: 56px 0 24px; }
  .markets__r { gap: 24px; }
  .careers__r { gap: 36px; }
  .careers__l { gap: 24px; }
}

/* Careers photograph breaks the rail to the left, the way the preconstruction
   photo does. It kept its right edge where the board put it (rail + 580) and
   runs off the canvas edge instead of starting flush with the title. */
.careers__img { left: 0; width: calc(var(--rail-x) + 580px); }
@media (max-width: 1100px) { .careers__img { left: auto; width: 100%; } }

/* ── carousel controls: square, and out of the index's way ────────
   Round buttons were the one curve left in a build whose whole rule is square
   corners. They were also positioned absolutely, so the index list ran
   underneath them and the last label sat behind the arrows. Back in flow with
   the index as the flexible track, so the two can never collide. */
.markets__indexrow { display: flex; align-items: flex-start; gap: 32px; }
.mindex { flex: 1 1 auto; min-width: 0; }
.markets__pager { position: static; flex: 0 0 auto; margin: 0; gap: 12px; }
.circ {
  width: 44px; height: 44px; border-radius: 0;
  display: inline-flex; align-items: center; justify-content: center;
}

/* two lines, as written: 520 was narrower than the first line needs at 40px,
   so the <br> gave three lines rather than two */
.mission__title { max-width: 660px; }

/* ── preconstruction: photo matches the column it sits beside ─────
   It was 482 tall and pulled 56 above the accordion, so the two columns
   started and finished at different heights. It is now the same box as the
   list's tallest state and starts on the same line, which is what "equally
   aligned" asks for. Padding comes in to keep the stage at the board's 663. */
.services__inner--varf { padding-block: 48px; }
.services__inner--varf .services__visual { align-self: start; margin-top: 0; height: 566px; }
.svc__rule--gray { top: 48px; height: 566px; }

@media (max-width: 900px) {
  .services__inner--varf { padding-block: 0 56px; }
  .services__inner--varf .services__visual { height: clamp(230px, 58vw, 340px); }
}

/* The copy panel's 144 left padding left the title only 389px to wrap in, so
   the <br> produced four lines rather than two. Back to the board's 50/50
   split with the copy starting on the rail. */
.mission { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }
.mission__l { padding: 72px 64px 72px var(--rail-x); }
.mission__title { max-width: none; }

/* portfolio wall marks up a step — 150 read undersized in a 360 cell */
.wall__cell img { width: 172px; height: 40px; }

/* At 40px the first line measures 566 against 536 of column, so the <br> still
   bought a third line. 38 on a 576 column clears it with room to spare, and two
   points off a 40px head is not a size anyone reads as different. */
.mission__l { padding-right: 24px; }
.mission__title { font-size: clamp(26px, 2.6vw, 38px); line-height: 1.4; }

@media (max-width: 900px) {
  /* The 50/50 split is a desktop rule and, being written later in the file, it
     was beating the old single-column mobile rule — the copy panel was 195px
     wide on a 390 screen and the title ran to six lines. */
  .mission { grid-template-columns: 1fr; }
  .mission__l { padding: 56px var(--gut) 56px var(--rail-x); }
  .mission__title { font-size: 26px; line-height: 1.38; }
}

/* ── platform band: a line per sentence, the last one carrying the weight ──
   The 681 measure existed to force the wrap; the breaks are explicit now, so
   it only risked wrapping a line that is meant to be whole. The bold sits on
   its own element rather than on a :last-child of the lit words, which is a
   hook that stopped existing when the band moved off the scroll-lit treatment. */
.statement--band .statement__copy { max-width: none; }
.statement__strong { font-weight: 600; }

/* ── two-column sections all split 645 / 555 ──────────────────────
   The design system is explicit: 1200 rail, left 645, right 555 starting at
   765. Neither of these obeyed it. Markets ran on the generic .split defaults
   (520 + an 80 gap) so its right column began at 720, and careers carried the
   board's 162 right padding, so both columns stopped ~42 short of the rail's
   right edge while every head above them ran the full 1200. Same split, same
   765, same 1320 for all of them now. */
.split { gap: 0; justify-content: space-between; }
.split .split__l { flex: 0 0 645px; }
.split .split__r { flex: 0 0 555px; max-width: none; }
.markets__r { flex: 0 0 555px; max-width: none; }
.markets__copy { max-width: none; }

.careers__grid { padding-right: var(--rail-x); }
.careers__l { flex: 0 0 645px; }
.careers__r { width: 555px; flex: 0 0 555px; }

@media (max-width: 1100px) {
  .split { gap: 32px; }
  .split .split__l, .split .split__r, .markets__r { flex: 0 0 auto; width: 100%; }
  .careers__grid { padding-right: var(--gut); }
  .careers__l, .careers__r { flex: 0 0 auto; width: 100%; }
}

/* Careers photograph gains 62px of height and is bottom-anchored rather than
   dropped from a fixed 273. It ran 315→715 against a section whose content
   bottom is 609, so it overshot its own padding while still reading short.
   Anchoring from the base keeps the board's 56 of clearance whatever the
   title does above it. */
.careers__img { top: auto; bottom: 56px; height: 462px; }
@media (max-width: 1100px) { .careers__img { bottom: auto; height: clamp(240px, 46vw, 400px); } }

/* ── footer ───────────────────────────────────────────────────────
   The footer Connect had no rule of its own at all — it rendered as raw inline
   text with the arrow jammed against the word and its <i> invisible, while the
   header's identical markup got the full label · gap · arrow · rule treatment.
   It is the same component; it now reads as one. */
/* It now carries .cta and uses the header's own markup, so it inherits the
   tested component rather than reimplementing it. A second implementation is
   what left it unstyled in the first place. */

/* and the block itself tightens: 88/64 of padding and a 60 row gap left the
   follow row stranded 60px below columns that had already run out of content */
.ftr__inner { padding: 64px 0 48px; gap: 40px 48px; }
.ftr__follow { gap: 12px; }
.ftr__social { gap: 10px; }
.ftr__social a { width: 32px; height: 32px; }

/* FOLLOW was parked in column 4 against the right rail while Connect sat in
   column 1, so the second row stretched the full 1200 with nothing between
   them. Moving it in a column pulls the block narrower and brings the social
   marks closer at the same time, and a tighter row gap lifts them. */
.ftr__inner { gap: 28px 48px; }
.ftr__follow { grid-column: 3; }
@media (max-width: 1100px) { .ftr__follow { grid-column: 1 / -1; } }

/* the investor lede reads as a standing statement, not a caption */
.topbar__lede { font-weight: 700; }

/* ── markets switch: the rule belongs to the tab above it ─────────
   At 1.26 line-height on 60px type the rule sat in a pocket of descender space
   roughly as deep as the gap below it, so it read as floating between the tabs
   and the copy rather than underlining either. Tighter leading pulls it up
   under the letterforms; the copy gains a little air so the two gaps are
   clearly unequal and the grouping reads. */
.switch { gap: 44px; }
.switch__tab { line-height: 1.06; padding-bottom: 16px; }
.markets__r { gap: 44px; }
@media (max-width: 900px) {
  .switch { gap: 28px; }
  .switch__tab { padding-bottom: 10px; }
  .markets__r { gap: 28px; }
}

/* ── portfolio wall stays a grid on a phone ───────────────────────
   Below 760 it collapsed to one column, so the eight companies became eight
   full-width photographs and the section ran to 3553px. Two up × four keeps it
   readable as a grid and the marks scale with the cell. */
@media (max-width: 760px) {
  .wall { grid-template-columns: repeat(2, 1fr); }
  .wall__cell { aspect-ratio: 1 / 1; }
  .wall__cell img { width: 96px; height: 22px; }
  .wall__copy { left: 12px; right: 12px; bottom: 14px; gap: 6px; }
  .wall__loc { font-size: 8px; letter-spacing: .1em; }
  .wall__desc { display: none; }        /* no room for it in a half-width cell */
}

/* ── mobile menu slides ───────────────────────────────────────────
   It used to be a 0.3s opacity keyframe fired by the `hidden` attribute coming
   off, so it had no motion and no exit at all. The panel travels in from the
   right; its contents follow on a short stagger so the menu assembles rather
   than arriving as one slab. Everything keys off `.is-on`, which app.js adds a
   frame after the panel is displayed and removes before it is hidden again. */
@media (max-width: 760px) {
  .mobnav {
    transform: translateX(100%);
    transition: transform .46s cubic-bezier(.16,.84,.24,1);
    will-change: transform;
  }
  .mobnav.is-on { transform: none; }

  .mobnav__scrim, .mobnav__bg {
    opacity: 0; transition: opacity .5s ease .06s;
  }
  .mobnav.is-on .mobnav__scrim, .mobnav.is-on .mobnav__bg { opacity: 1; }

  .mobnav__list li, .mobnav__cta, .mobnav__media, .mobnav__foot {
    opacity: 0; transform: translateX(26px);
    transition: opacity .4s ease var(--d, 0ms), transform .5s cubic-bezier(.16,.84,.24,1) var(--d, 0ms);
  }
  .mobnav.is-on .mobnav__list li,
  .mobnav.is-on .mobnav__cta,
  .mobnav.is-on .mobnav__media,
  .mobnav.is-on .mobnav__foot { opacity: 1; transform: none; }

  .mobnav__list li:nth-child(1) { --d: 120ms; }
  .mobnav__list li:nth-child(2) { --d: 165ms; }
  .mobnav__list li:nth-child(3) { --d: 210ms; }
  .mobnav__list li:nth-child(4) { --d: 255ms; }
  .mobnav__list li:nth-child(5) { --d: 300ms; }
  .mobnav__cta   { --d: 345ms; }
  .mobnav__media { --d: 390ms; }
  .mobnav__foot  { --d: 430ms; }
}

@media (prefers-reduced-motion: reduce) {
  .mobnav, .mobnav__scrim, .mobnav__bg,
  .mobnav__list li, .mobnav__cta, .mobnav__media, .mobnav__foot {
    transition: none; transform: none; opacity: 1;
  }
}

/* ── mobile footer ────────────────────────────────────────────────
   The desktop layout places every block by explicit grid-column AND grid-row.
   The old mobile block reset some of the columns and none of the rows, so four
   blocks stayed pinned to row 1 and two to row 2 and the whole footer printed
   on top of itself. A flex column has no placement to leak — the blocks simply
   follow DOM order. */
@media (max-width: 760px) {
  .ftr__inner {
    display: flex; flex-direction: column; align-items: stretch;
    gap: 36px; padding: 48px 0 36px;
  }
  .ftr__brand, .ftr__links, .ftr__cos, .ftr__addr,
  .ftr__connect, .ftr__follow { grid-column: auto; grid-row: auto; width: 100%; }
  .ftr__brand { display: flex; align-items: center; gap: 20px; }
  .ftr__logo, .ftr__logo img { width: 128px; height: auto; }
  .ftr__we { font-size: 28px; line-height: 34px; }
  .ftr__links { display: grid; grid-template-columns: 1fr 1fr; gap: 12px 20px; }
  .ftr__connect { gap: 18px; }
  .ftr__connectline { width: 100%; }
}

/* ── sections layer on a phone too ────────────────────────────────
   These were held to `position: relative` under 900 on the assumption that
   stacking tall sections would bury their lower halves. That was true of a
   `top: 0` stick; it is not true of --stick-top, which is min(0, viewport −
   height) and so holds a tall band only once its LAST screen is showing.
   app.js recomputes it per viewport, so the phone gets the same behaviour. */
@media (max-width: 900px) {
  .page > .mission, .page > .bandquote, .page > .markets,
  .page > .safety, .page > .careers {
    position: sticky; top: var(--stick-top, 0px);
  }
}

/* the investor lede is a standing statement, set as one */
.topbar__lede { text-transform: uppercase; letter-spacing: .09em; }

/* ── card text holds still while the card grows ───────────────────
   The chip and copy were pinned left AND right, so widening the card from 420
   to 520 rewrapped every line under the cursor. They are now a fixed 372
   measure anchored to the left edge: the frame and the photograph grow, the
   words do not move. */
.mcard__text { right: auto; width: 372px; }
.mcard__chip { max-width: 372px; }
@media (max-width: 1000px) { .mcard__text { width: auto; right: 24px; } }

/* Connect leads, the line supports it. column-reverse put the CTA under its own
   supporting sentence, which reads as a footnote rather than an invitation. */
.ftr__connect { flex-direction: column; }

/* FOLLOW back on the right rail under the address it belongs with, and both
   rows pulled in — 64/48 of padding on a 28 row gap left the second row
   floating well clear of columns that had already run out of content. */
.ftr__follow { grid-column: 4; }
.ftr__inner { gap: 18px 48px; padding: 56px 0 40px; }
@media (max-width: 1100px) { .ftr__follow { grid-column: 1 / -1; } }

/* The two tabs already measured 516 of a 555 column, so the 44 gap was pushing
   the switch 6px past its own rail — there was no room left to open it up.
   54px buys 91px of headroom and the pair sits at 72 apart, comfortably inside
   the column. Two points under the board's 60, in exchange for the separation. */
.switch { gap: 72px; }
.switch__tab { font-size: clamp(32px, 3.8vw, 54px); }
@media (max-width: 900px) { .switch { gap: 32px; } .switch__tab { font-size: 30px; } }

/* The board's −36 pull of the wall into the head is a desktop move: there the
   copy sits in the right column, clear of the grid below. Stacked on a phone
   the copy lands directly above the cells, so the pull put the last line hard
   against the first photograph. */
@media (max-width: 900px) {
  .portfolio .wall { margin-top: 0; }
  .portfolio .portfolio__head { padding: 48px 0 40px; }
  .portfolio__copy { font-size: 17px; line-height: 26px; }
}

/* ── markets category dropdown ────────────────────────────────────
   Replaces the horizontally scrolling index. Square corners throughout, the
   trigger set in the same 17px the list items used, and the panel sized to its
   own content rather than a fixed drawer. */
.mksel { position: relative; flex: 0 0 auto; }
.mksel__trigger {
  display: inline-flex; align-items: center; gap: 14px;
  padding: 0 0 16px; border: 0; background: none; cursor: pointer;
  font-family: var(--f-display); font-size: 17px; line-height: 28px;
  color: #1A3768; transition: color .15s var(--ease);
}
.mksel__trigger:hover { color: #007CBB; }
.mksel__chev {
  width: 9px; height: 9px; border-right: 1.5px solid currentColor; border-bottom: 1.5px solid currentColor;
  transform: translateY(-3px) rotate(45deg); transition: transform .28s var(--ease);
}
.mksel.is-open .mksel__chev { transform: translateY(1px) rotate(-135deg); }

.mksel__list {
  position: absolute; left: 0; top: calc(100% + 8px); z-index: 20;
  margin: 0; padding: 8px 0; list-style: none; min-width: 300px;
  background: #fff; border: 1px solid #C9D4E1;
  box-shadow: 0 18px 44px rgba(13,31,59,.14);
  opacity: 0; transform: translateY(-8px);
  transition: opacity .22s var(--ease), transform .26s var(--ease);
}
.mksel.is-open .mksel__list { opacity: 1; transform: none; }
.mksel__list[hidden] { display: none; }
.mksel__list button {
  display: block; width: 100%; text-align: left;
  padding: 9px 22px; border: 0; background: none; cursor: pointer;
  font-family: var(--f-display); font-size: 17px; line-height: 26px;
  color: rgba(26,55,104,.62); transition: color .14s var(--ease), background-color .14s var(--ease);
}
.mksel__list button:hover { color: #1A3768; background: #F2F6FA; }
.mksel__list button.is-active { color: #1A3768; }

/* the rule now reads as carousel progress rather than an item marker */
.markets__indexrow::after { transition: width .42s var(--ease); }

@media (max-width: 900px) {
  .mksel__trigger { font-size: 16px; padding-bottom: 12px; }
  .mksel__list { min-width: min(300px, calc(var(--vwc, 100vw) - 40px)); }
}

/* ── a card cycles its own projects ───────────────────────────────
   Markets that hold more than one project stack their photographs in the same
   card and cross-fade between them, chip included, instead of taking a second
   slot in the strip and repeating the title. The fade is long on purpose: this
   is the card breathing, not a slideshow. */
.mcard__img { opacity: 0; transition: opacity 1.4s var(--ease); }
.mcard__img.is-on { opacity: 1; }
.mcard__chip { opacity: 0; pointer-events: none; transition: opacity .7s var(--ease); }
.mcard__chip.is-on { opacity: 1; }

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

/* ── mobile footer, compacted so it can be revealed ───────────────
   Two problems at once. The brand lockup is a horizontal one — logo, rule,
   statement — and on a 350 rail that left "We Are Builders." 98px to live in,
   so it wrapped to nothing. And at 1025 tall the footer could not be parked
   behind the page on an 844 screen without burying its own top, which is why
   the reveal was switched off below 1000 in the first place. Pairing the four
   list blocks into two rows brings it inside the viewport and the reveal comes
   back on its own. */
@media (max-width: 760px) {
  .ftr__inner { display: grid; grid-template-columns: 1fr 1fr; gap: 30px 24px; padding: 40px 0 32px; }

  .ftr__brand {
    grid-column: 1 / -1; flex-direction: column; align-items: flex-start; gap: 14px;
  }
  .ftr__divide { display: none; }
  .ftr__logo, .ftr__logo img { width: 150px; height: auto; }
  .ftr__we { font-size: 30px; line-height: 36px; }

  .ftr__links { grid-column: 1; display: flex; flex-direction: column; gap: 7px; }
  .ftr__cos   { grid-column: 2; }
  .ftr__cos ul { display: flex; flex-direction: column; gap: 7px; }
  .ftr__addr  { grid-column: 1; }
  .ftr__follow{ grid-column: 2; align-self: start; }
  .ftr__connect { grid-column: 1 / -1; }

  .ftr__links a, .ftr__cos li, .ftr__addr p { font-size: 14px; line-height: 21px; }
  .ftr__addr p { margin: 0 0 10px; }
  .ftr__legalrow { height: 52px; }
}

/* the safety photograph carries the brand blue rather than a neutral navy
   scrim — the band above and the plate below are both blue, and a grey-navy
   wash between them read as a different section wearing the same headline */
.safety__scrim {
  background: linear-gradient(180deg,
    rgba(0,124,187,.30) 0%,
    rgba(0,124,187,.44) 46%,
    rgba(10,110,174,.74) 100%);
}

/* "Builders." carries the weight in the footer as it does in the hero. .ftr__we
   sets 200 after the shared bold rule, so the bold needs to come later. */
.ftr__statement .ftr__we--bold { font-weight: 500; }

/* The ghosted mark is sized to overrun its band top and bottom rather than
   sitting inside it with air above and below — it reads as a watermark the
   band is cut out of, not a graphic placed on it. */
.band__watermark, .band__watermark--quote {
  width: auto; height: 168%; top: -34%; bottom: auto;
}

@media (max-width: 760px) {
  /* rows sized to their content, not stretched to the tallest cell — that is
     what left FOLLOW stranded at the bottom of a row the address block sized */
  .ftr__inner { align-content: start; }
  .ftr__inner > * { align-self: start; }
  .ftr__addr p { margin: 0 0 8px; }
  .ftr__cos .lbl, .ftr__addr .lbl { margin-bottom: 10px; display: block; }
  .ftr__follow { gap: 10px; }
  .ftr__social { gap: 8px; }
  .ftr__social a { width: 30px; height: 30px; }
}

@media (max-width: 760px) {
  /* Explicit rows. Naming only the column let auto-placement drop FOLLOW into
     a row of its own below the address rather than beside it, which is where
     ~180px of the footer's height was going. */
  .ftr__brand   { grid-row: 1; }
  .ftr__links   { grid-row: 2; }
  .ftr__cos     { grid-row: 2; }
  .ftr__addr    { grid-row: 3; }
  .ftr__follow  { grid-row: 3; }
  .ftr__connect { grid-row: 4; }
}

/* the board's own mission photograph (11JU-0), not the plant shot that was
   standing in for it */
.mission__r::before { background-image: url("assets/img/mission-crew.jpg?a=0.71.7"); background-position: 50% 50%; }

/* ── accordion: weight carries the state, not size ────────────────
   Opening a row took its title from 26 to 32, which wrapped the two long ones
   to a second line and changed the row's height with them — the rail, the
   marker and every row below it all moved for a reason that had nothing to do
   with the content. Same size in both states, weight and colour do the work,
   and the titles stay on one line. */
/* Padding was landing three times over: 22 top and bottom on .sv__t, another
   22 on the row I had added, and then the open row dropped its bottom 22 — so
   a closed row carried 88px of air around 34px of type and changed height when
   it opened. One source now, on the title, and the same in both states.

   Colour is the only thing transitioned. font-size no longer changes, and
   font-weight is not smoothly interpolable — Chrome re-synthesises and re-lays
   out the text every frame, which is the glitch. */
.services__inner--varf .sv__t {
  padding: 15px 0; font-size: 26px; line-height: 34px; font-weight: 400; color: #5A6C82;
  transition: color .26s var(--ease);
}
.services__inner--varf .sv.is-on .sv__t { font-size: 26px; font-weight: 500; color: #12294D; padding-bottom: 15px; }
.services__inner--varf .sv { padding-block: 0; }

/* the drawer is what animates, and it eases the same way in both directions */
.services__inner--varf .sv__d { transition: height .52s var(--ease), opacity .3s var(--ease); }
.services__inner--varf .sv__d p { margin: 4px 0 22px; font-size: 16px; line-height: 28px; color: #43505F; }

/* ── mobile footer brand keeps its horizontal lockup ──────────────
   Stacking it was the wrong fix for the statement being crushed to 98px — the
   answer was a smaller mark, not a different lockup. Logo 110 leaves the rail
   ~220 for "We Are Builders.", which is more than the 150 it needs. Connect
   follows directly under the brand; the four list blocks come after it. */
@media (max-width: 760px) {
  .ftr__brand {
    grid-row: 1; grid-column: 1 / -1;
    flex-direction: row; align-items: center; gap: 16px;
  }
  .ftr__divide { display: block; width: 1px; align-self: stretch; background: rgba(255,255,255,.18); }
  .ftr__logo, .ftr__logo img { width: 110px; height: auto; }
  .ftr__we { font-size: 26px; line-height: 32px; }

  .ftr__connect { grid-row: 2; grid-column: 1 / -1; }
  .ftr__links   { grid-row: 3; grid-column: 1; }
  .ftr__cos     { grid-row: 3; grid-column: 2; }
  .ftr__addr    { grid-row: 4; grid-column: 1; }
  .ftr__follow  { grid-row: 4; grid-column: 2; }
}

/* copy sits on the same 15px rhythm as the title above it, so the space over
   the paragraph matches the space under it */
.services__inner--varf .sv__d p { margin: 12px 0 15px; }

/* hero head lifts a little off the logo strip */
.hero__head { bottom: 272px; }
@media (max-width: 900px) { .hero__head { bottom: 216px; } }

/* ── menu groups + focused card ───────────────────────────────────
   The menu carries both markets, so it needs to say which is which. Picking
   one opens that card wide and knocks the rest back, which is the only way a
   selection reads when the card you asked for is mid-strip. */
.mksel__group {
  padding: 14px 22px 6px; font-family: var(--f-mono);
  font-size: 11px; line-height: 16px; letter-spacing: .11em; text-transform: uppercase;
  color: #8FA6BC; pointer-events: none;
}
.mksel__list li:first-child .mksel__group, .mksel__group:first-child { padding-top: 6px; }

.markets__scroll .mcards .mcard.is-focus { width: 520px; }
.markets__scroll .mcards .mcard.is-back { opacity: .42; }
.mcard { transition-property: opacity, transform, width; }

/* The title's own 15px of bottom padding sits inside the gap, so a 12px
   paragraph margin on top of it read as ~34 between the header and its copy.
   The padding is the whole gap now. */
.services__inner--varf .sv__d p { margin: 0 0 15px; }

/* ── preconstruction: breathing room and a directional swap ───────
   15px of row padding was tighter than the section wanted once the titles
   stopped resizing. 19 gives it back. The stage and the photograph are sized
   to the list's tallest state rather than to an inherited 566, which is where
   ~100px of slack under the list was coming from. */
.services__inner--varf .sv__t { padding: 19px 0; }
.services__inner--varf .sv.is-on .sv__t { padding-bottom: 19px; }
.services__inner--varf .sv__d p { margin: 0 0 19px; }
.services__inner--varf .services__visual { height: 498px; }
.svc__rule--gray { height: 498px; }
.services__stage { height: 594px; }

/* the incoming frame travels in from the direction the list moved */
.sv__layer {
  transform: translateY(var(--from, 0));
  transition: opacity .5s var(--ease), transform .78s var(--ease);
}
.sv__layer.is-on { transform: none; }

@media (max-width: 900px) {
  .services__stage { height: auto; }
  .services__inner--varf .services__visual { height: clamp(230px, 58vw, 340px); }
}

/* held across a re-split so the rebuild cannot replay the rise */
.hero__body.m-nofx .m-line > span { transition: none !important; }

/* ── mission: the two halves are offset, not simultaneous ─────────
   The photograph starts wider and holds a beat behind the plate, so the plate
   arrives first and the image is still settling behind it. Both moving on the
   same clock read as one effect applied twice. */
.mission__r::before { transform: scale(1.28); transform-origin: right center; }
.m-ready .mission__r[data-m-rise]::before {
  transition: transform 1.9s cubic-bezier(.16,.84,.24,1) 260ms;
}

.mksel__all { font-weight: 500; color: #1A3768 !important; }
.mksel__list li:first-child { border-bottom: 1px solid #E4EAF1; padding-bottom: 4px; margin-bottom: 4px; }

/* a wider open — 520 next to 420 read as a nudge rather than a focus */
.markets__scroll .mcards .mcard.is-focus { width: 620px; }
@media (hover: hover) and (pointer: fine) and (min-width: 901px) {
  .markets__scroll:not(:active) .mcards .mcard:hover { width: 620px; }
}

/* The index row carried a hairline and a blue progress segment from when the
   categories were a scrolling rail. With a dropdown there is no rail for them
   to measure, so they are gone rather than left as decoration. */
.markets__indexrow::before, .markets__indexrow::after { content: none; }

/* The project cross-fade inside a card runs slower — at 1.4s against a 2.4s
   dwell the card was settled for barely a second before the next swap started,
   which read as churn rather than a slow reveal. */
.mcard__img { transition: opacity 2.2s var(--ease); }
.mcard__chip { transition: opacity 1.1s var(--ease); }

/* ── a sliver of the next section under the held frame ────────────
   The pin filled the viewport, so while preconstruction was locked there was
   no hint of what came next. It is 88px short of the screen now and the track's
   own ground is the portfolio's navy, so that band reads as the next section
   waiting underneath — and when the portfolio actually rises it lands exactly
   on the colour that was already there, with no seam. */
@media (min-width: 901px) {
  .services--varf { background: var(--navy-900); }
  .services__pin { height: calc(100vh - 88px); height: calc(100svh - 88px); background: #fff; }
}

/* ── the panel is a sibling of the header, not a child ────────────
   .hdr carries transform: translateY(--fold), and a transformed ancestor
   becomes the containing block for position: fixed. So the panel was being
   sized to the header's 137px box — it filled the top bar — until `is-open`
   set transform: none, at which point it snapped to the viewport. Moving it
   out of the header is the fix; no ancestor can contain it now. It sits just
   under the header in the stack so the logo and the burger stay on top. */
.mobnav { z-index: 55; }
.hdr { z-index: 60; }

/* ── controls sit together under the head ─────────────────────────
   Arrows first, then the market menu beside them, both on the left rail
   directly beneath "Markets We Serve". They were split to opposite ends of
   the rail by the space-between left over from when a scrolling index sat
   between them — with the index gone there is nothing to hold apart. */
.markets__indexrow {
  justify-content: flex-start; align-items: center; gap: 24px; height: auto;
  padding: 0 0 8px;
}
.markets__pager { order: 0; }
.mksel { order: 1; }
/* the trigger's 16px was clearance for the underline that has been removed */
.mksel__trigger { padding-bottom: 0; }
.mksel__list { top: calc(100% + 14px); }

@media (max-width: 900px) { .markets__indexrow { gap: 16px; } }

/* Markets sits on white rather than the page's blue-grey ground */
.page > .markets { background: #fff; }

/* The stage was centred inside the PIN, and the pin is 88px short of the screen
   so the next section's sliver can show. That put 157 of air above the
   composition and 245 below it. Centring against the full viewport instead —
   the sliver is part of what you see, so it has to be part of the sum. */
@media (min-width: 901px) {
  .services__pin { align-content: start; padding-top: calc((100vh - 594px) / 2); padding-top: calc((100svh - 594px) / 2); }
}

/* The ghosted mark carries whitespace under the lockup in its own artwork, so
   bleeding it evenly top and bottom still left a visible gap at the base.
   Dropped lower and grown so the letterforms themselves run past both edges. */
.band__watermark, .band__watermark--quote { height: 190%; top: -14%; }

/* ── markets head, tightened ──────────────────────────────────────
   364 tall for 200px of title. The 264 minimum on the left column was doing
   nothing except holding a floor no content reached, the switch sat 44 clear
   of its own copy, and the block carried 100px of padding. Nothing here needed
   that much room. */
.markets__head { padding: 56px 0 18px; }
.markets__head .split__l { min-height: 0; }
.markets__r { gap: 30px; }
.markets__indexrow { padding: 0 0 6px; }
@media (max-width: 900px) { .markets__head { padding: 44px 0 14px; } }

/* ── mobile preconstruction: the photograph locks, the list runs under it ──
   Desktop pins the whole composition and steps the rows in place. A phone has
   no room for that, so the photograph is what sticks — cropped to a band at the
   top — and the list scrolls beneath it, each row changing the image as it
   passes under. A white gradient rides with the photo so the type fades out
   rather than being guillotined by its edge. */
@media (max-width: 900px) {
  .services--varf { overflow: visible; }
  .services__pin { position: static; height: auto; display: block; }
  .services__stage { height: auto; }
  .services__inner--varf { display: block; padding: 0 0 40px; }

  .services__inner--varf .services__visual {
    position: sticky; top: 0; z-index: 2;
    width: 100%; height: 42vh; height: 42svh; margin: 0;
  }
  /* No fade under the photo. A sticky pseudo-element with a negative margin
     re-composites against moving content every frame, and the jitter cost more
     than the softening was worth — the photo's own edge does the job. */
  .services--varf .services__col { width: 100%; padding: 56px var(--gut) 0; }
  .services--varf .svc__rule { display: none; }
  .services__inner--varf .sv__t { padding: 17px 0; font-size: 22px; line-height: 30px; }
  .services__inner--varf .sv.is-on .sv__t { padding-bottom: 17px; font-size: 22px; }
}

/* The head and the numbers sit on white; only the rail the cards ride on
   carries the off-white, so the strip reads as a tray rather than the whole
   section changing colour. */
.page > .markets { background: #fff; }
.markets__scroll { background: #E9EEF4; }
.page > .facts { background: #fff; }

/* ── markets head on the grid ─────────────────────────────────────
   Three things were floating. The switch was 537 wide in a 555 column, so its
   right edge stopped 18px short of the copy beneath it and the two underlines
   bracketed nothing. The right column carried an 8px top pad that pushed it off
   the title's line. And the tab gap was a fixed 72 rather than whatever it
   takes to reach the rail. Both tabs now sit on the column's own edges, so the
   underlines, the copy and the rail all agree. */
.markets__r { padding-top: 0; }
.switch { width: 100%; justify-content: space-between; gap: 24px; }
.switch__tab { flex: 0 0 auto; }
@media (max-width: 900px) { .switch { justify-content: flex-start; gap: 28px; } }

/* Mobile controls: one row, arrows left and menu right, not a centred stack.
   An older narrow-screen rule was still turning this row into a column. */
@media (max-width: 900px) {
  .markets__indexrow {
    flex-direction: row; align-items: center; justify-content: space-between;
    gap: 16px; padding: 0 0 6px;
  }
  .markets__pager { order: 0; margin: 0; }
  .mksel { order: 1; }
  .mksel__list { left: auto; right: 0; }
}

/* Building Safely carries the 270 Park erection frame — an ironworker
   harnessed on the beam guiding a hoisted girder, which is the section's whole
   argument in one picture. */
.safety__img { background-image: url("assets/img/safety-270park.jpg?a=0.71.7"); background-position: 50% 44%; }

/* The rail scrolls sideways only. Declaring overflow on one axis makes the
   browser compute the other as auto, so the strip could be nudged vertically
   inside its own box and the cards drifted off their line. */
.markets__scroll { overflow-x: auto; overflow-y: hidden; overscroll-behavior-x: contain; }

@media (max-width: 900px) {
  /* the controls were pinched between the copy above and the rail below */
  .markets__indexrow { padding: 18px 0 22px; }
}

@media (max-width: 900px) {
  /* the copy's line box was running past the head's own padding, so the
     controls started five pixels above it */
  .markets__head { padding: 44px 0 28px; }
  .markets__indexrow { padding: 8px 0 24px; }
}

/* Desktop controls had the same pinch as mobile: 17px above and sitting flush
   on the card rail, so the row read as part of the strip rather than as the
   thing that drives it. */
.markets__head { padding-bottom: 30px; }
.markets__indexrow { padding: 10px 0 32px; }

/* Building Safely's photograph carries the navy scrim, not the blue one — it
   matches the board's photo band and lets the two blue plates above and below
   it read as the blue, rather than three blues in a row. */
.safety__scrim {
  background: linear-gradient(180deg,
    rgba(13,31,59,.16) 0%,
    rgba(13,31,59,.24) 34%,
    rgba(18,41,77,.64) 66%,
    rgba(18,41,77,.80) 100%);
}

/* ── Building Safely: both headlines over the photograph ──────────
   The board dropped the blue plate that used to carry "Building Safely" and
   set it on the picture with "Raising the Standard, Nationwide" beneath — one
   statement in two sizes rather than a heading in one band and a subhead in
   the next. Frame ZKU-0: left 120, bottom 64, 26 between them. */
.safety__bandA { display: none; }
.safety__band { height: 480px; }
.safety__copy {
  display: flex; flex-direction: column; align-items: flex-start; gap: 26px;
  bottom: 64px;
}
.safety__copy .safety__t1 {
  margin: 0; font-weight: 300; font-size: clamp(48px, 6.7vw, 96px);
  line-height: 1.04; letter-spacing: -0.035em; color: #fff;
}
.safety__copy .safety__t2 {
  margin: 0; font-weight: 300; font-size: clamp(28px, 3.6vw, 52px);
  line-height: 1.19; letter-spacing: -0.02em; color: #fff; text-align: left;
}
@media (max-width: 900px) {
  .safety__band { height: 420px; }
  .safety__copy { bottom: 40px; gap: 16px; }
}

/* On a phone the careers photograph breaks the rail and runs edge to edge,
   the way the preconstruction and careers photos do on desktop. Negative
   margins against the column's own padding rather than a computed width, so
   it stays correct whatever the gutter is at that breakpoint. */
@media (max-width: 1100px) {
  .careers__img {
    width: auto; margin-inline: calc(var(--gut) * -1);
    height: clamp(260px, 62vw, 420px);
  }
}

/* ── Building Safely, second board pass ───────────────────────────
   The board grew the photograph to 929 inside a 480 band and hid the old
   scrim in favour of a bottom-up navy wash (11WR-0). The oversize is what
   makes the picture travel under the section above it as you scroll, so the
   site matches it — centred rather than top-offset, because the parallax pass
   moves this layer and a static offset would fight the transform. */
.safety__img { top: -47%; height: 194%; background-position: 50% 44%; }
.safety__scrim {
  background: linear-gradient(0deg,
    rgba(13,31,59,.94) 0%,
    rgba(13,31,59,.80) 24%,
    rgba(13,31,59,.24) 73%,
    rgba(13,31,59,.04) 86%,
    rgba(13,31,59,0) 100%);
}

/* ── markets: a wider right column, tighter controls ──────────────
   The 645/555 split leaves the title's column two-thirds empty while the tabs
   and the paragraph are squeezed into 555. Markets carries two elements on the
   right where the other three sections carry one, so it gets 620 against 520 —
   the tabs and the copy both gain 65 and the two halves finally weigh the same.
   The gap under the head comes down with it; the arrows were sitting 64 below
   the title for no reason. */
.markets__head .split__l { flex: 0 0 520px; min-height: 0; }
.markets__r { flex: 0 0 620px; }
.markets__head { padding-bottom: 12px; }
.markets__indexrow { padding: 0 0 32px; }

@media (max-width: 1100px) {
  .markets__head .split__l, .markets__r { flex: 0 0 auto; width: 100%; }
}

/* `.split .split__r` is a two-class selector and .markets__r is one, so the
   555 kept winning. Same weight, later in the file. */
.markets__head .markets__r { flex: 0 0 620px; max-width: none; }
.markets__head .markets__copy { max-width: none; }

/* The intro statement sits on the shop floor now — a grinder throwing sparks
   off a pipe flange, which is the "self-perform" claim in one frame. Board
   crop, 50% 40%. */
.introphoto__img { background-image: url("assets/img/intro-grind.jpg?a=0.71.7"); background-position: 50% 40%; }

/* The industrial chips are a solid blue plate with Inter Medium, not the glass
   treatment the commercial project chips use — the board draws the two
   differently because one names a market and the other names a building. */
.mcard__chip--solid {
  background: #007CBB; border-color: #007CBB;
  font-family: var(--f-mono); font-weight: 500;
}

/* ── markets, facts and safety scroll ─────────────────────────────
   Nothing holds through this stretch any more: the cards scroll up, the
   numbers follow them, and Building Safely comes up underneath rather than
   layering over. Only mission and the quote band stay held, because those are
   the two that something is meant to rise over. */
.page > .markets { position: relative; top: auto; }
.page > .facts   { position: relative; top: auto; }
.page > .safety  { position: relative; top: auto; }

/* the safety detail plate is navy, matching the scrim above it — three blues
   stacked was one blue too many */
.safety__bandC .safety__detail {
  background: linear-gradient(180deg, #12294D 0%, #0D1F3B 100%);
}

/* The board leads with the portfolio line in Medium and follows with the long
   paragraph in Light — the order and the weights both changed. Board gap 28. */
.introphoto__lead, .introphoto__body { display: block; }
.introphoto__lead { font-weight: 300; }
.introphoto__body { font-weight: 300; margin-top: 28px; }

/* Flat navy, no gradient. The plate is a ground, not a wash — the gradient was
   carried over from when it was blue and reads as a seam against the scrim
   above it once both are navy. */
.safety__bandC .safety__detail { background: #12294D; }

/* ── mission: no horizontal move ──────────────────────────────────
   The plate travelling in from the left and the photograph scaling from the
   right are both off. Both halves fall back to the same fade-and-rise every
   other block on the page uses, so nothing here is a special case any more.
   The clip stays because the photo still overhangs its frame by 12%. */
.m-ready .mission__l[data-m-rise],
.m-ready .mission__r[data-m-rise] {
  opacity: 0; clip-path: none; transform: translateY(42px);
  transition: opacity 1s var(--m-ease, cubic-bezier(.16,.84,.24,1)) var(--m-delay, 0ms),
              transform 1s var(--m-ease, cubic-bezier(.16,.84,.24,1)) var(--m-delay, 0ms);
}
.m-ready .mission__l[data-m-rise].is-in,
.m-ready .mission__r[data-m-rise].is-in { opacity: 1; transform: none; }

.mission__r::before { transform: none; }
.m-ready .mission__r[data-m-rise]::before,
.m-ready .mission__r[data-m-rise].is-in::before { transform: none; transition: none; }

/* ── safety photo: keep the worker in frame ───────────────────────
   At 194% the layer had 225px of slack each way, which means the band that is
   visible at BOTH extremes of the travel is only 30px tall — everything else
   scrolls off at one end or the other, including the ironworker. 125% leaves
   60px of slack, so a 360px band is on screen throughout, and the crop is set
   on him rather than on the middle of the plate. */
.safety__img { top: -12.5%; height: 125%; background-position: 50% 58%; }

/* The ghosted mark fades up with its band rather than being there from the
   start. It rests at 10%, so the shared rise rule's is-in opacity of 1 has to
   be overridden — and it holds its position, since the mark is placed against
   the band edges and any translate would break that. */
.m-ready .band__watermark[data-m-rise] {
  opacity: 0; transform: none;
  transition: opacity 1.6s cubic-bezier(.16,.84,.24,1);
}
.m-ready .band__watermark[data-m-rise].is-in { opacity: .1; transform: none; }

/* ══════════════════════════════════════════════════════════════════
   Mission — the plate pushes the photograph into place
   ══════════════════════════════════════════════════════════════════
   The photograph is full-bleed at rest and holds most of the width once
   settled. The navy plate arrives from the left and shoves it right by exactly
   its own width, so the two edges travel together and it reads as a push
   rather than one thing sliding over another. Both halves are absolutely
   placed and moved by transform — animating a grid track would relayout on
   every frame, which is what made the old version feel wrong. */
@media (min-width: 901px) {
  .mission {
    display: block; position: relative; height: 600px; overflow: clip;
    background: var(--navy-900);
  }
  .mission__l {
    position: absolute; left: 0; top: 0; bottom: 0; width: 38%; z-index: 2;
    display: flex; flex-direction: column; justify-content: center;
    padding: 72px 48px 72px var(--rail-x);
  }
  .mission__r { position: absolute; left: 0; top: 0; bottom: 0; width: 100%; z-index: 1; }
  .mission__r::before { inset: 0; }

  /* the push */
  .m-ready .mission__l[data-m-rise] {
    opacity: 1; clip-path: none; transform: translateX(-100%);
    transition: transform 1.15s cubic-bezier(.16,.84,.24,1) var(--m-delay, 0ms);
  }
  .m-ready .mission__l[data-m-rise].is-in { transform: none; }

  .m-ready .mission__r[data-m-rise] {
    opacity: 1; clip-path: none; transform: none;
    transition: transform 1.15s cubic-bezier(.16,.84,.24,1) var(--m-delay, 0ms);
  }
  .m-ready .mission__r[data-m-rise].is-in { transform: translateX(38%); }

  .mission__title { max-width: 460px; }
}

/* The plate is a 40% column with the rail's 120 on its left, which leaves 432
   for the copy — the 38px head needed 538 and broke to four lines. 30px fits
   the two the <br> asks for, and the photograph still holds 60% of the width. */
@media (min-width: 901px) {
  .mission__l { width: 40%; padding-right: 24px; }
  .m-ready .mission__r[data-m-rise].is-in { transform: translateX(40%); }
  .mission__title { font-size: clamp(24px, 2.2vw, 30px); line-height: 1.36; max-width: none; }
}

/* crop lifted — more of the hoisted beam and sky, less of the plate below */
.safety__img { background-position: 50% 42%; }

/* Wrong way — 42% moved the crop toward the top of the frame and took the
   ironworker out of it. Down the image instead, which brings him back. */
.safety__img { background-position: 50% 70%; }

/* Split down the middle: navy plate and photograph take half each, and the
   plate pushes the image by exactly that half. A 50% column leaves 576 after
   the rail, which is what the 38px head needs for the two lines the line break
   asks for — at 40% it only had 432 and had to drop to 30. */
@media (min-width: 901px) {
  .mission__l { width: 50%; }
  .m-ready .mission__r[data-m-rise].is-in { transform: translateX(50%); }
  .mission__title { font-size: clamp(26px, 2.7vw, 38px); line-height: 1.32; }
}

/* ══════════════════════════════════════════════════════════════════
   Platform band — a little more air, and a mark that reads as arriving
   ══════════════════════════════════════════════════════════════════ */

/* 78 left the three lines sitting close to both edges of the plate. */
.statement--band { padding: 104px 0; }

/* The fade was already running and already correct — it just could not be
   seen. Nothing that travels from 0 to 10% opacity in 1.6s reads as an
   arrival; it reads as having always been there. The delay holds it off
   until the band is properly on screen rather than clipping the bottom
   edge, and the longer curve keeps it moving while you are looking at it. */
.m-ready .band__watermark[data-m-rise] {
  transition: opacity 2.2s cubic-bezier(.16,.84,.24,1) .42s;
}

/* The mark was 190% of the band and hung 165px past the right edge, so it read
   as bleeding off rather than as placed. Smaller, and its right edge sits on
   the content rail like everything else in the page. It still overruns the
   band top and bottom, which is what keeps it a watermark.

   Scoped two ways on purpose: to .statement--band so the quote band keeps its
   own treatment, and to desktop so the mobile rule that pushes it off-edge at
   right: -40% still wins on a phone (--rail-x floors at 120, which on a 390
   screen would drag the mark almost entirely off the left). */
@media (min-width: 901px) {
  .statement--band .band__watermark {
    height: 132%; top: -16%; left: auto; right: var(--rail-x);
  }
  .statement--band { padding: 104px 0; }
}

/* 104 is desktop air. On a phone the band is already 44% of the screen. */
@media (max-width: 900px) { .statement--band { padding: 88px 0; } }

/* ══════════════════════════════════════════════════════════════════
   Header compaction — every property that moves, moves together
   ══════════════════════════════════════════════════════════════════
   Five things change between the full and compact bar: the row height, the
   plate height, the logo box, the nav size and the CTA size. Only three of
   them were transitioned, so the plate and the CTA jumped to their end value
   while the row was still easing. That mismatch is what read as a snap — not
   the duration. All five now share one curve, and .25s ease goes to a longer
   decelerating one so the bar settles rather than clicks. */
.hdr .navrow,
.hdr .hdr__plate       { transition: height .58s cubic-bezier(.22,.61,.36,1); }
.hdr .navrow__logo img { transition: width .58s cubic-bezier(.22,.61,.36,1),
                                     height .58s cubic-bezier(.22,.61,.36,1); }
.hdr .nav a            { transition: font-size .58s cubic-bezier(.22,.61,.36,1),
                                     color .16s ease; }
.hdr .cta              { transition: font-size .58s cubic-bezier(.22,.61,.36,1); }

@media (prefers-reduced-motion: reduce) {
  .hdr .navrow, .hdr .hdr__plate, .hdr .navrow__logo img,
  .hdr .nav a, .hdr .cta { transition: none; }
}

/* The quote band carries the same ghosted mark and was left hanging 230px past
   the rail, which read as an inconsistency once the platform band terminated
   on it. Same treatment, same reasoning, same desktop-only scope. */
@media (min-width: 901px) {
  .bandquote .band__watermark--quote {
    height: 132%; top: -16%; left: auto; right: var(--rail-x);
  }
}

/* 132% put the mark's left edge at x=170 against a right edge at 1320 — near
   symmetric margins, which reads as centred no matter that the right edge is
   correct. The mark wants to run off the left and terminate on the right, so
   the rail is the thing it resolves to. 166% of the 420 band reproduces the
   size it had before it was reduced, now anchored rather than overhanging. */
@media (min-width: 901px) {
  .statement--band .band__watermark { height: 166%; top: -20%; }
}

/* ══════════════════════════════════════════════════════════════════
   Platform band — taken from the Paper board, not derived
   ══════════════════════════════════════════════════════════════════
   Board values, read off the node rather than reasoned about:
     band  padding 78px, inline 120 (the rail at 1440)
     mark  1191x593 box at left 322 / top -62, background-size contain,
           opacity .1
   The box is 2.008:1 and the artwork is 2.072:1, so `contain` fits it by
   width and letterboxes 9px top and bottom — object-fit reproduces that on
   an <img>. Its right edge lands at 1513, which is 73px PAST the canvas: the
   board bleeds the mark off the right, it does not stop it on the rail.

   Anchored from the right so the bleed is preserved above 1440, where a fixed
   left: 322 would drift inward instead. 193 = 1513 - 1320, the mark's overhang
   past the rail on the board's own canvas.

   This supersedes every earlier attempt at this element. */
@media (min-width: 901px) {
  .statement--band { padding: 78px 0; }
  .statement--band .band__watermark {
    left: auto; right: calc(var(--rail-x) - 193px);
    top: -62px; bottom: auto;
    width: 1191px; height: 593px;
    object-fit: contain; object-position: 50% 50%;
  }
}

/* ══════════════════════════════════════════════════════════════════
   Mission — static half-and-half, vertical motion only
   ══════════════════════════════════════════════════════════════════
   The plate used to arrive from the left (translateX -100% to 0) while the
   photograph was shoved right (0 to 50%). Two things were moving sideways
   past each other and the seam never settled. The composition is now simply
   built where it belongs and only the entry is animated: the copy fades and
   rises, the photograph rises behind its own clip.

   Every transform below is stated explicitly rather than left to inherit,
   because the horizontal rules live at three breakpoints above this one and
   an unset property would let the old translateX through. */
@media (min-width: 901px) {
  .mission__l,
  .mission__l[data-m-rise],
  .m-ready .mission__l[data-m-rise],
  .m-ready .mission__l[data-m-rise].is-in {
    left: 0; width: 50%; transform: none; opacity: 1; transition: none;
  }

  /* the right half is a real box now, not a full-width box pushed halfway */
  .mission__r,
  .m-ready .mission__r[data-m-rise],
  .m-ready .mission__r[data-m-rise].is-in {
    left: 50%; width: 50%; transform: none; opacity: 1; transition: none;
  }
  .mission__r { overflow: clip; }

  /* copy rises */
  .m-ready .mission__title {
    opacity: 0; transform: translateY(26px);
    transition: opacity .9s var(--ease), transform 1.05s cubic-bezier(.16,.84,.24,1);
  }
  .m-ready .mission__l.is-in .mission__title { opacity: 1; transform: none; }

  /* photograph rises behind the clip. 106% and a 3% offset so the bottom edge
     never lifts into view while it travels. */
  .mission__r::before { inset: -3% 0; height: 106%; background-size: cover; }
  .m-ready .mission__r[data-m-rise]::before {
    transform: translateY(5%);
    transition: transform 1.25s cubic-bezier(.16,.84,.24,1);
  }
  .m-ready .mission__r[data-m-rise].is-in::before { transform: none; }
}

/* The is-in state above still carried `transition: none` from an earlier rule,
   and restating only `transform` let that through, so the photograph snapped
   from 5% to 0 in one frame instead of travelling. The transition has to be
   declared on BOTH states or the end state cancels it. */
@media (min-width: 901px) {
  .m-ready .mission__r[data-m-rise].is-in::before {
    transform: none;
    transition: transform 1.25s cubic-bezier(.16,.84,.24,1);
  }
}

/* Making the right column a true half-width box re-cropped the photograph:
   `cover` on a 720 box picks the centre of the frame, which is three workers
   and a much busier picture than the open one this section had. The image box
   stays full-width and simply starts at the seam, so what shows is its left
   half — the same crop as before, just arrived at without a translate. The
   section already clips what runs past the canvas. */
@media (min-width: 901px) {
  .mission__r,
  .m-ready .mission__r[data-m-rise],
  .m-ready .mission__r[data-m-rise].is-in { left: 50%; width: 100%; }
}

/* ══════════════════════════════════════════════════════════════════
   Markets head on mobile — the desktop flex basis was acting as a height
   ══════════════════════════════════════════════════════════════════
   The two head columns are sized with `flex: 0 0 520px` and `flex: 0 0 620px`
   for the desktop row. Below 901 the head stacks into a column, and a
   flex-basis on a column axis is a HEIGHT — so the right column reserved 620px
   for 266px of content and left ~325px of white gap above the pager. Neither
   column should be fixed once they stack.

   The paragraph also stayed at the desktop 20/30. It is body copy in a stacked
   column now, so it takes the same 17/26 the portfolio paragraph uses. */
@media (max-width: 900px) {
  .markets__head .split__l,
  .markets__head .markets__r,
  .markets__head .split__r { flex: 0 0 auto; height: auto; min-height: 0; }

  .markets__copy { font-size: 17px; line-height: 26px; }
}

/* With the basis gone the head stacks properly; this states the column and its
   rhythm explicitly rather than relying on .split being reinterpreted, and sets
   the 40px it should breathe between the title and the switch. */
@media (max-width: 900px) {
  .markets__head {
    display: flex; flex-direction: column; align-items: stretch;
    flex-wrap: nowrap; height: auto; gap: 40px;
  }
}

/* ══════════════════════════════════════════════════════════════════
   Mobile market cards — open, but nothing reflows
   ══════════════════════════════════════════════════════════════════
   The copy was pinned left AND right, so widening the card widened the text
   box and every line re-wrapped. Giving it a fixed width off the left edge
   means the card can grow behind copy that never moves - the same reasoning
   that fixed this on desktop.

   620 is a desktop open width; on a 390 screen it is wider than the viewport.
   330 opens clearly and still leaves the next card peeking. */
@media (max-width: 900px) {
  .markets__scroll .mcards .mcard__text { right: auto; width: 232px; }

  .markets__scroll .mcards .mcard.is-focus { width: 330px; }

  /* the touch rule above drops width from the transition list, which made the
     card jump to its open size in one frame instead of opening */
  .markets__scroll .mcards .mcard {
    transition-property: width, opacity, transform;
    transition-duration: .62s, 1.15s, 1.15s;
    transition-timing-function: cubic-bezier(.22,.61,.36,1);
  }
}

/* ══════════════════════════════════════════════════════════════════
   Header compaction on mobile
   ══════════════════════════════════════════════════════════════════
   The .is-compact block is unscoped, so on a phone it was taking the 82px row
   to 76 and the 84x42 logo to 80x40 - a 6px change nobody can see. The mobile
   bar needs its own set, cut by roughly the same proportion the desktop bar is
   (row to ~75 percent, logo to ~78). The topbar stays 30 either way, so the
   plate is 30 + the compact row. */
@media (max-width: 900px) {
  .hdr.is-compact .navrow { height: 62px; }
  .hdr.is-compact .hdr__plate { height: 92px; }        /* 30 bar + 62 row */
  .hdr.is-compact .navrow__logo img { width: 66px; height: 33px; }

  /* the burger rides up with the row rather than staying on the old centre */
  .hdr.is-compact .burger { transform: scale(.88); }
  .hdr .burger { transition: transform .58s cubic-bezier(.22,.61,.36,1); }
}

/* The plate carried `transition: opacity .22s, height .25s`. Unifying the
   header curve replaced that whole shorthand with `height .58s`, which
   silently dropped the opacity transition - so the plate grew smoothly and
   its colour arrived in one frame. Both properties, one curve, so the ground
   fades up as it sizes. Listing every animated property explicitly, because a
   shorthand here is what caused the problem in the first place. */
.hdr .hdr__plate {
  transition: height .58s cubic-bezier(.22,.61,.36,1),
              opacity .58s cubic-bezier(.22,.61,.36,1);
}

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

/* ══════════════════════════════════════════════════════════════════
   Mission on mobile — no overlap under preconstruction
   ══════════════════════════════════════════════════════════════════
   Mission is sticky so later sections rise over it. On a phone the section it
   is held under is preconstruction, which is a white plate at z-index 2, and
   it was reaching the mission photograph while ~215px of that photograph was
   still on screen - the image simply got sliced in half by a flat white edge.

   The photograph is the point of the section, so mission scrolls normally here
   and the two stack. The quote band and careers keep their overlap: they are
   flat colour and copy, so nothing gets cut. The layering script reads computed
   position and drops --stick-top on its own once this is no longer sticky. */
@media (max-width: 900px) {
  .page > .mission { position: relative; top: auto; }
}

/* ══════════════════════════════════════════════════════════════════
   Mobile footer — brought under the height the reveal needs
   ══════════════════════════════════════════════════════════════════
   The reveal only runs when the footer fits the viewport (height <= vh - 24).
   At 908 against 844 it fell back to flow, which is why it sat at the end of
   the page with its top cut off by the fixed bar instead of being uncovered.

   Nothing here removes content. The savings are all doubled-up space: the
   links column carried a 30px padding-top ON TOP of the 30px grid gap, the
   inner block had 40/32 of its own, and the row gap was set for a wider
   column count than a phone actually shows. */
@media (max-width: 900px) {
  .ftr__inner { padding-top: 30px; padding-bottom: 24px; row-gap: 22px; }
  .ftr__links { padding-top: 0; }
  .ftr__legal { padding-block: 14px; }
  .ftr__links a, .ftr__cos a { line-height: 30px; }
}

/* Still 46 over the budget after the doubled-up padding came out. The rest is
   seam space rather than type: the link lists carried a 7px gap on top of a
   30px line, the labels a 10px margin, and the connect block an 18px gap
   between its CTA and its line. Rows land at 33px, which is still a
   comfortable footer target. */
@media (max-width: 900px) {
  .ftr__links ul, .ftr__cos ul { gap: 3px; }
  .ftr__cos .lbl, .ftr__addr .lbl, .ftr__follow .lbl { margin-bottom: 6px; }
  .ftr__connect { gap: 8px; }
  .ftr__legal { padding-block: 10px; }
}

/* 820 against a 820 budget is exactly on the line, and innerHeight moves on a
   real phone as the URL bar collapses - one pixel the wrong way drops it back
   to flow. A little headroom so it stays revealed either way. */
@media (max-width: 900px) {
  .ftr__inner { padding-top: 24px; padding-bottom: 20px; row-gap: 18px; }
}

/* The image box is full-width and starts at the seam, so only its left half is
   ever on screen. `cover` fits a 1.78 photograph across a 2.19 box by WIDTH,
   which means the whole photo spans the full 1440 and the visible window is a
   fixed 0-50 percent of it. The second ironworker sits at 57-69 percent, so no
   amount of background-position could ever reach him - there was no horizontal
   overflow to pan.

   Cropping to the visible half instead makes the box 1.10, so cover fits by
   HEIGHT and the photo overflows sideways by 38 percent. That overflow is what
   background-position can move. 47% puts the window at roughly 18-80 percent
   of the frame, which holds both kneeling workers. */
@media (min-width: 901px) {
  .mission__r::before {
    left: 0; right: auto; width: 50%;
    background-position: 47% 50%;
  }
}

/* The cards are selectable now, so they read as controls. The focus ring is
   drawn inside because the card clips its own overflow. */
.markets__scroll .mcards .mcard { cursor: pointer; }
.markets__scroll .mcards .mcard:focus-visible {
  outline: 2px solid #007CBB; outline-offset: -2px;
}

/* The quote band's mark sat at -16%, which on a 299 band is a 48px overhang
   top and bottom - dead symmetric, so it read as centred in the plate rather
   than sitting under the copy. -8% drops it 24px: 24 above, 72 below. */
@media (min-width: 901px) {
  .bandquote .band__watermark--quote { top: -8%; }
}

/* The gap under the mark is the logo's own construction: the DBM letterforms
   bottom out at 73.7 percent of the artwork and the rest is the GLOBAL
   wordmark and its clear space, so the box ran to the band edge while the
   letters stopped 35px short. Scaled so the DBM baseline clears the band
   rather than tangenting it - 152% of 299 puts it about 12px past. */
@media (min-width: 901px) {
  .bandquote .band__watermark--quote { height: 152%; }
}

/* ══════════════════════════════════════════════════════════════════
   Preconstruction on mobile — the photo stops pinning
   ══════════════════════════════════════════════════════════════════
   The photo was sticky at top 0 with z-index 2, so it pinned flush against
   the viewport top (behind the fixed bar, which is why it rode up too high)
   and the accordion scrolled BEHIND it. That was meant to be softened by a
   gradient riding with the photo, but that was removed for jitter, so the
   type just got cut off by the photo's edge.

   On a 390 column there is no second column for the list to live in, so
   pinning buys nothing and costs the copy. Photo, then list, both scrolling.
   NOT static: the .sv__layer photographs are absolutely positioned inside
   this box, so removing the containing block let them escape to the section
   and stretch across all 1159px of it, behind the copy. Relative keeps the
   box and the clip, it just stops the pinning. */
@media (max-width: 900px) {
  .services__inner--varf .services__visual {
    position: relative; z-index: auto;
  }
}

/* ══════════════════════════════════════════════════════════════════
   Preconstruction on mobile — the photo locks, the list covers it
   ══════════════════════════════════════════════════════════════════
   Unpinning it in 0.50.0 threw out the behaviour to fix the symptom. The
   photo should lock at the top of the screen; what was wrong is that the list
   had no ground of its own, so the locked photo showed straight through the
   copy and read as overlap.

   So: the photo goes back to sticky, but BELOW the list rather than above it,
   and the list carries the section's white. The photo holds at the top while
   the list rides up and covers it — the copy is always on white, and the photo
   is never cut by an edge because it is simply behind.

   Still relative-not-static reasoning applies to the box itself: .sv__layer is
   absolutely positioned inside it, so it must stay a containing block. */
@media (max-width: 900px) {
  .services__inner--varf .services__visual {
    position: sticky; top: 0; z-index: 0;
    width: 100%; height: 42vh; height: 42svh; margin: 0;
  }
  .services--varf .services__col {
    position: relative; z-index: 1; background: #fff;
  }
}

/* ══════════════════════════════════════════════════════════════════
   Preconstruction on mobile — a real pinned track, as on desktop
   ══════════════════════════════════════════════════════════════════
   Covering the photo with the list was still the wrong shape: the photo has to
   be there the whole way through, not be scrolled past. Mobile had the pin
   disabled entirely and was faking it by sticking the photo alone, which is
   why every version of this either cut the copy or buried the picture.

   Same structure as desktop now. The section is a tall track, the frame inside
   it is sticky and one screen tall, and the frame holds the photo above the
   list. Nothing inside the frame scrolls — the rows open and close as the
   track is consumed, which is what app.js's services() already drives from
   scroll progress once the track gives it a non-zero step. */
@media (max-width: 900px) {
  .services--varf { height: 400vh; height: 400svh; padding: 0; overflow: visible; }

  .services__pin {
    position: sticky; top: 0; height: 100vh; height: 100svh;
    display: block; background: #fff;
  }
  .services__stage { height: 100%; }
  .services__inner--varf { display: block; padding: 0; height: 100%; }

  .services__inner--varf .services__visual {
    position: relative; z-index: 0;
    width: 100%; height: 34vh; height: 34svh; margin: 0;
  }
  .services--varf .services__col {
    position: relative; z-index: 1; background: #fff;
    width: 100%; padding: 22px var(--gut) 0;
  }

  /* the next section rises over the held frame instead of pushing it off */
  .portfolio { margin-top: -100vh; margin-top: -100svh; }
}

/* The frame has to hold the tallest row state without clipping, and at 34svh
   the list ran 20px past the bottom on every one. Nothing inside the frame
   scrolls, so anything past the edge is simply lost. */
@media (max-width: 900px) {
  /* min-height: 320px from an earlier rule was flooring this, so 34svh and
     30svh both rendered at 320 and the height had no effect at all. */
  .services__inner--varf .services__visual { height: 30vh; height: 30svh; min-height: 0; }
  .services--varf .services__col { padding-top: 18px; }
}

/* Preconstruction's own description is the longest of the five and still ran
   42px past the frame. Taken out of both sides: a little off the photo and
   3px off each row's padding, which is 30 across the five. */
@media (max-width: 900px) {
  .services__inner--varf .services__visual { height: 26vh; height: 26svh; }
  .services__inner--varf .sv__t { padding: 14px 0; }
  .services__inner--varf .sv.is-on .sv__t { padding-bottom: 14px; }
}

/* ══════════════════════════════════════════════════════════════════
   Preconstruction on mobile — photo sticks under the bar, list runs under it
   ══════════════════════════════════════════════════════════════════
   The pinned-frame version was the wrong model. It forced every row to fit one
   screen, which is why the type had to keep shrinking, and it stuck the photo
   at top 0 — behind the 92px bar, so most of the picture was never visible.

   The list scrolls normally now and the photo holds below the bar with the
   rows passing UNDER it, so the photo is on screen the whole way through and
   only the open row needs to be in view. No fit constraint, so the rows get
   their original rhythm back.

   top is the compact bar: 30 topbar + 62 row. The photo carries its own bottom
   edge so where the copy slides beneath it reads as a deliberate split rather
   than a cut. */
@media (max-width: 900px) {
  .services--varf { height: auto; padding: 0 0 40px; overflow: visible; }
  .services__pin { position: static; height: auto; display: block; background: none; }
  .services__stage { height: auto; }
  .services__inner--varf { display: block; padding: 0; height: auto; }

  .services__inner--varf .services__visual {
    position: sticky; top: 92px; z-index: 2;
    width: 100%; height: 34vh; height: 34svh; min-height: 0; margin: 0;
    box-shadow: 0 1px 0 var(--rule, #C9D4E1), 0 14px 22px -18px rgba(13,31,59,.45);
  }

  .services--varf .services__col {
    position: relative; z-index: 1; background: none;
    width: 100%; padding: 28px var(--gut) 0;
  }

  /* fitting no longer governs, so the rows go back to their own rhythm */
  .services__inner--varf .sv__t { padding: 17px 0; }
  .services__inner--varf .sv.is-on .sv__t { padding-bottom: 17px; }

  /* the pinned track's hand-off is gone with the track */
  .portfolio { margin-top: 0; }
}

/* The two photo layers cross-fade, so mid-swap the outgoing is at 1-x and the
   incoming at x — their combined alpha never reaches 1, and each also travels
   7 percent, which leaves an uncovered strip at the edge. With no ground on
   the container, whatever is behind shows through both gaps. On mobile that is
   the copy sliding under the sticky photo, which is the flash of overlap.

   A ground under the layers closes it. Navy rather than white so the dip reads
   as photographic shadow instead of a blown-out frame. Not scoped to mobile:
   the container should never be see-through anywhere. */
.services__visual { background-color: #0D1F3B; }

/* A row that has been passed fades out rather than sliding behind the photo,
   where its last line kept showing at the edge. It keeps its box, so nothing
   below it shifts and the scroll position stays honest — only the ink goes. */
@media (max-width: 900px) {
  /* Four class selectors on purpose. motion.css loads AFTER this file and sets
     `.m-ready [data-m-rise].is-in { opacity: 1 }` at three, so anything at
     three or below here loses on order and the fade silently does nothing. */
  .services--varf .services__inner--varf .sv,
  .services--varf .services__inner--varf .sv.is-in {
    transition: opacity .5s cubic-bezier(.22,.61,.36,1);
  }
  .services--varf .services__inner--varf .sv.is-past,
  .services--varf .services__inner--varf .sv.is-in.is-past {
    opacity: 0; pointer-events: none;
  }
}

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

/* ══════════════════════════════════════════════════════════════════
   Preconstruction on mobile — a locked frame that steps between rows
   ══════════════════════════════════════════════════════════════════
   The section locks for its whole length and scrolling advances the open row
   rather than moving anything. No in-between state: photo, the open row
   directly under it, the rows still to come below.

   The spent rows COLLAPSE rather than just fading. Fading alone left their
   boxes in place, which pushed the open row further down the screen at every
   step; collapsing them keeps it anchored under the photo, which is what makes
   each step read as the same composition with new content.

   Rows below the open one may run past the bottom edge and that is fine — the
   photo and the open row are what has to be in frame. */
@media (max-width: 900px) {
  .services--varf { height: 400vh; height: 400svh; padding: 0; overflow: visible; }
  .services__pin {
    position: sticky; top: 0; height: 100vh; height: 100svh;
    display: block; background: #fff; overflow: hidden;
  }
  .services__stage { height: 100%; }
  .services__inner--varf { display: block; padding: 0; height: 100%; }

  /* margin, not sticky: the frame is already held, and 92 is the compact bar */
  .services__inner--varf .services__visual {
    position: relative; z-index: 0; top: auto;
    margin: 92px 0 0; width: 100%; height: 44vh; height: 44svh; min-height: 0;
    box-shadow: none;
  }

  .services--varf .services__col {
    position: relative; z-index: 1; background: none;
    width: 100%; padding: 26px var(--gut) 0;
  }

  .services--varf .services__inner--varf .sv,
  .services--varf .services__inner--varf .sv.is-in {
    max-height: 520px; overflow: hidden;
    transition: max-height .52s cubic-bezier(.22,.61,.36,1),
                opacity .34s cubic-bezier(.22,.61,.36,1);
  }
  .services--varf .services__inner--varf .sv.is-past,
  .services--varf .services__inner--varf .sv.is-in.is-past {
    max-height: 0; opacity: 0; pointer-events: none;
  }

  .portfolio { margin-top: -100vh; margin-top: -100svh; }
}

/* The list's leading rule sat directly under the photo once the spent rows
   collapsed, drawing a line between the picture and its own heading. The photo
   edge is the divider there. Collapsed rows lose their borders too, or their
   1px each would stack up in the same place. */
@media (max-width: 900px) {
  .services__inner--varf .sv:first-child { border-top: 0; }
  .services--varf .services__inner--varf .sv.is-past,
  .services--varf .services__inner--varf .sv.is-in.is-past { border-width: 0; }
}

/* Rows near the bottom of the locked frame could never reveal. The observer
   wants an element above 743 (viewport less its 12 percent margin), and in a
   held frame rows 4 and 5 sit at 757 and 852 and never move, so they stayed at
   the rise transform's opacity 0 forever. The frame drives its own visibility
   through is-on and is-past, so the generic reveal is redundant here.

   :not(.is-past) matters - without it this comes after the collapse rule and
   switches the spent rows back on. */
@media (max-width: 900px) {
  .services--varf .services__inner--varf .sv[data-m-rise]:not(.is-past) {
    opacity: 1; transform: none;
  }
}

/* The 92px above the photograph is the bar's worth of clearance, and it was
   sitting on the frame's white — a white gap between the section before and
   the picture. It takes the header's own navy so it reads as the bar's ground
   continuing rather than a gap. A hard-stop gradient rather than a second
   element: the rest of the frame has to stay white or the copy would be navy
   on navy. */
@media (max-width: 900px) {
  .services__pin {
    background: linear-gradient(180deg, #0A1A32 0 92px, #FFFFFF 92px);
  }
}

/* The footer brand row on mobile inherited two desktop values. The logo
   carries align-self: flex-start, so it pinned to the top while the statement
   centred — the two never sat on a common axis. And the divider has a fixed
   103px height, which on a 64px row overshot both by 20 top and bottom.

   Logo centres like everything else, and the divider takes the row instead of
   a number from the wider layout. */
@media (max-width: 900px) {
  .ftr__brand { align-items: center; }
  .ftr__logo { align-self: center; }
  .ftr__divide { height: auto; align-self: stretch; }
}

/* ══════════════════════════════════════════════════════════════════
   Preconstruction rail on mobile
   ══════════════════════════════════════════════════════════════════
   The rail is hidden on mobile because .svc__rule is positioned from the
   section centre, which is meaningless in a single column. Rebuilt against the
   list instead: the grey runs the list's own height and the blue segment is
   carried by the open row.

   Row-carried rather than the JS marker on purpose — the rows collapse as they
   are passed, so their offsets move constantly and placeMark() would be
   measuring a layout that has already changed. A ::before on the open row is
   always exactly beside it. */
@media (max-width: 900px) {
  .services--varf .services__col { padding-left: calc(var(--gut) + 22px); }

  .services--varf .services__list { position: relative; }
  .services--varf .services__list::before {
    content: ''; position: absolute; left: -22px; top: 0; bottom: 0;
    width: 2px; background: #C9D4E1; pointer-events: none;
  }

  .services--varf .services__inner--varf .sv.is-on::before {
    content: ''; position: absolute; left: -22px; top: 0; height: 100%;
    width: 2px; background: #007CBB; pointer-events: none; z-index: 1;
  }

  /* the centre-positioned pair stays out of it */
  .services--varf .svc__rule { display: none; }
}

/* The collapse needs overflow: hidden, but the blue segment sits at -22px —
   outside the row's box — so it was being clipped by the very rule that makes
   the rows collapse. Only a COLLAPSING row needs the clip; the rest can let
   the rail through. Their content tops out around 308 against a 520 cap, so
   nothing else relies on it. */
@media (max-width: 900px) {
  .services--varf .services__inner--varf .sv,
  .services--varf .services__inner--varf .sv.is-in { overflow: visible; }
  .services--varf .services__inner--varf .sv.is-past,
  .services--varf .services__inner--varf .sv.is-in.is-past { overflow: hidden; }
}

/* The forward arrow carried .circ--fill, so it sat permanently blue while the
   back arrow was outlined — reading as a state rather than an affordance. Both
   rest outlined and take the accent on hover. Scoped to the markets pager so
   the filled treatment stays wherever else it is doing real work.

   Keyboard gets the same, or the control would be invisible to anyone tabbing.
   Touch has no hover, so the rest state has to stand on its own — it does, the
   arrow and the rule are the affordance. */
.markets__pager .circ,
.markets__pager .circ--fill {
  background: none; border-color: #B9C7D6; color: var(--ink);
}
@media (hover: hover) and (pointer: fine) {
  .markets__pager .circ:hover,
  .markets__pager .circ--fill:hover {
    background: var(--blue); border-color: var(--blue); color: #fff;
  }
}
.markets__pager .circ:focus-visible,
.markets__pager .circ--fill:focus-visible {
  background: var(--blue); border-color: var(--blue); color: #fff;
}

/* Portfolio wall on touch: the reveal is hover- and focus-within-driven, so a
   phone only ever saw the logo and the location. A tap opens the cell instead.

   The :hover variant is neutralised here first — on touch it sticks after a tap
   and would leave a cell open with no way to close it, fighting the class. The
   is-open rule follows so it wins on order. */
@media (hover: none), (pointer: coarse) {
  .wall__cell:hover .wall__reveal { grid-template-rows: 0fr; opacity: 0; }
  .wall__cell.is-open .wall__reveal { grid-template-rows: 1fr; opacity: 1; }
  .wall__cell { cursor: pointer; }
}

/* The description is hidden on mobile because a 195px square has no room for
   it at rest — true, but it is the thing the tap is for. It comes back on the
   OPEN cell only, clamped so it cannot overrun the square, and the scrim
   deepens to carry it. */
@media (hover: none), (pointer: coarse) {
  .wall__cell.is-open .wall__desc {
    display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical;
    overflow: hidden; font-size: 12px; line-height: 18px;
  }
  .wall__cell.is-open .wall__revealed { gap: 10px; }
  .wall__cell.is-open::after {
    background: linear-gradient(0deg, rgba(10,26,50,.96) 0%, rgba(10,26,50,.88) 46%, rgba(10,26,50,.42) 78%, rgba(10,26,50,.12) 100%);
  }
}

/* The card scrim was pinned at top: 150px with height: 370px — cut for the
   520-tall desktop card. On a 380-tall mobile card the ramp still ran 370, so
   its deepest 95 percent landed BELOW the card and the copy only ever sat on
   the middle of the gradient. That is why mobile looked washed out against
   desktop rather than differently graded.

   Proportional instead, and anchored to the base: 28.8 percent of 520 is the
   same 150 it always was, so desktop is unchanged to the pixel, and the ramp
   now reaches full strength at the bottom edge whatever the card height. */
.mcard__scrim {
  top: 28.8%; bottom: 0; height: auto;
}

/* Anchoring the ramp fixed where it ends but not where the copy meets it. The
   card is shorter on mobile while the copy block is not, so the text starts at
   34 percent of the scrim where desktop starts at 65 — on the desktop curve
   that is 0.47 opacity against 0.73. Same ramp, different sample point.

   Steeper here so the copy meets the same density it does on desktop: 0.62 by
   20 percent and full by 60, which puts ~0.74 under the first line. */
@media (max-width: 900px) {
  .mcard__scrim {
    background: linear-gradient(180deg,
      rgba(20,22,26,0) 0%, rgba(20,22,26,.62) 20%, rgba(20,22,26,.95) 60%, rgba(20,22,26,.95) 100%);
  }
}

/* 0.57.3 went too far. Chasing a target opacity under the copy produced a ramp
   that hit .95 at 71 percent of the card and then sat flat for the last 110px
   — a solid slab with the photograph lost inside it.

   Three changes, all pulling the same way: start the scrim at 38 percent
   instead of 29 so more of the picture is untouched, cap the darkest point at
   .86 rather than .95 so the photograph still reads through it, and let the
   ramp reach that maximum only at the very base so there is no flat run.

   The copy still lands with cover: its first line sits near .48 and the body
   near .62, which is enough for 20px medium and 12px regular on white. */
@media (max-width: 900px) {
  .mcard__scrim {
    top: 38%;
    background: linear-gradient(180deg,
      rgba(20,22,26,0) 0%,
      rgba(20,22,26,.42) 18%,
      rgba(20,22,26,.72) 50%,
      rgba(20,22,26,.86) 100%);
  }
}

/* Each row was getting 337px of scroll — about 0.4 of a screen for a full
   change of photograph and copy, which reads as the section running away from
   you. A longer track spends ~540px per row instead, without changing what the
   section does. */
@media (max-width: 900px) {
  .services--varf { height: 560vh; height: 560svh; }
}

/* ══════════════════════════════════════════════════════════════════
   Mission + preconstruction share one frame on mobile
   ══════════════════════════════════════════════════════════════════
   The mission photograph is folded into the preconstruction stack (app.js
   carries it as the lead frame), so its own block below the navy plate goes.
   The plate keeps the title and the section ends there.

   The frame is 3:4 and holds that ratio at every width, matching the market
   cards, instead of a viewport fraction that changes shape per device. */
@media (max-width: 900px) {
  /* min-height: 620px is a floor, so height: auto alone left 318px of empty
     plate where the photograph used to be. */
  .mission { height: auto; min-height: 0; }
  .mission__r { display: none; }

  .services__inner--varf .services__visual {
    height: auto; aspect-ratio: 3 / 4; min-height: 0;
  }
}

/* 3:4 at full bleed is 520px tall on a 390 screen, and with the 92px bar
   clearance that leaves 232 for a row whose tallest state is 308 — four of the
   five clipped, and nothing scrolls inside a locked frame, so those were lost
   words rather than hidden ones.

   Driving the frame from its HEIGHT keeps the ratio exact and lets the copy
   fit: 50svh is 422 tall, so 3:4 makes it 316 wide, inset on the rail. Reads
   as a deliberate plate rather than a full-bleed photo that got cropped. */
@media (max-width: 900px) {
  .services__inner--varf .services__visual {
    height: 47vh; height: 47svh; width: auto; aspect-ratio: 3 / 4;
    margin: 92px 0 0 var(--gut);
  }
}

/* 4:3 landscape, edge to edge. The earlier 3:4 portrait read the ratio the
   wrong way round and the inset was only there to buy back the height a
   portrait frame ate. Full width at 4:3 is 292 tall on a 390 screen, so the
   list gets 460 against a tallest row of 308 — the fit problem disappears with
   the correct orientation. */
@media (max-width: 900px) {
  .services__inner--varf .services__visual {
    width: 100%; height: auto; aspect-ratio: 4 / 3;
    margin: 92px 0 0;
  }
}

/* The bar-clearance strip took the header navy back when the section above it
   was white and it needed to read as the bar's ground continuing. Now the
   mission plate sits directly on top of it, and #0A1A32 against the plate's
   #0D1F3B is a visible step darker — a band between the title and the photo.
   It takes the plate's navy instead, so the two are one surface. Once the
   section locks the strip is behind the fixed bar and its colour stops
   mattering, so nothing is lost by matching the plate rather than the header. */
@media (max-width: 900px) {
  .services__pin {
    background: linear-gradient(180deg, #0D1F3B 0 92px, #FFFFFF 92px);
  }
}

/* One row at a time on mobile - REMOVED at 0.68.0.

   That rule collapsed every row that was not the open one, which belonged to
   the accordion architecture. Mobile is a sticky card stack now: every card is
   a visible plate and none of them collapse.

   It was still shipping, and it carried one class more than the stack block
   that replaced it, so `.sv.is-in:not(.is-on) { opacity: 0 }` outranked the
   stack's `opacity: 1`. Any card the reveal observer had already marked went
   invisible while it was sliding into view. Its `min-height` kept the box the
   right size, so the card was there, just unpainted. */

/* The 92px of bar clearance was a margin on the photograph, so it only does
   its job while the frame is locked — before that it is just dead navy between
   the mission plate and the picture, which is the padding you can see.

   Moved onto the sticky offset instead: the frame parks at 92 rather than 0,
   so the photo clears the bar for exactly the same reason, and in normal flow
   there is no offset at all — the plate runs straight into the photograph. The
   frame loses that 92 from its height so it still ends at the viewport base,
   and the navy stop in its gradient goes with the margin. */
@media (max-width: 900px) {
  .services__pin {
    top: 92px; height: calc(100vh - 92px); height: calc(100svh - 92px);
    background: #FFFFFF;
  }
  .services__inner--varf .services__visual { margin: 0; }
}



/* The plate carried 56 top and bottom around a 108px title, which was fine
   when a photograph sat below it inside the same section. Now the plate runs
   straight into the preconstruction frame, so that bottom 56 is the only
   breathing room between the headline and a picture — too tight for a section
   that is otherwise just a statement. */
@media (max-width: 900px) {
  .mission__l { padding-top: 88px; padding-bottom: 88px; }
}

/* Hero headline carries the page on a phone and 52 read small against a
   full-bleed photograph. 64/68 with the tracking held at -0.025em. */
@media (max-width: 900px) {
  /* 64 put "Builders." at 354 against a 350 rail; 62 is the largest that
     clears it with the display tracking in place. */
  .hero__title { font-size: 62px; line-height: 66px; }
}



/* The glitch is the step distance, not the snapping. At 626px a swipe has to
   travel 313 before the browser will release it toward the next row; anything
   shorter is pulled back to where it started, which is snapping behaving
   correctly and feeling like it is fighting you.

   Sized so one step is exactly one viewport — the distance a phone swipe is
   built around, and what full-screen slide sites use. A normal flick now clears
   the halfway line and advances instead of rubber-banding.

   700svh - 92px: stepScroll is (track - pin - vh) / 5, and the pin is
   100svh - 92, so this resolves to exactly one viewport per step. */
@media (max-width: 900px) {
  .services--varf { height: calc(700vh - 92px); height: calc(700svh - 92px); }
}



/* ══════════════════════════════════════════════════════════════════
   Preconstruction on mobile — sticky photo, stacking copy cards
   ══════════════════════════════════════════════════════════════════
   The photograph holds at the bar and swaps as before. What changed is
   underneath it: each heading and its copy ride on their own white card, and
   the cards stack — the next slides up over the last.

   Same construction as the technoprofil reference, which I inspected rather
   than guessed at: sticky cards at INCREMENTING tops, uniform height, stacked
   by DOM order rather than z-index. Each card parks a few pixels below the one
   before it, so the pile reads as depth instead of a single swap.

   No track, no step index, no landing to arbitrate. Position is the browser's
   the whole way; the only thing script does is read which card is on top so the
   photograph can follow it. */
@media (max-width: 900px) {
  .services--varf { height: auto; padding: 0 0 40px; overflow: visible; }
  /* overflow: visible is load-bearing. The pin still carries overflow: hidden
     from when it was the locked frame, and an overflow: hidden ancestor becomes
     the scroll container for any sticky descendant - so neither the photograph
     nor the cards would stick at all. */
  .services__pin { position: static; height: auto; background: none; overflow: visible; }
  .services__stage { height: auto; }
  .services__inner--varf { display: block; height: auto; padding: 0; }

  /* the photograph stays, 4:3 edge to edge, and keeps swapping */
  .services__inner--varf .services__visual {
    display: block; position: sticky; top: 92px; z-index: 0;
    width: 100%; height: auto; aspect-ratio: 4 / 3; min-height: 0; margin: 0;
  }

  /* rail and rules belong to a shared list; a card carries its own edge */
  .services--varf .svc__rule,
  .services--varf .services__rail,
  .services--varf .services__list::before,
  .services--varf .sv__mark { display: none; }

  .services--varf .services__col { position: static; width: 100%; padding: 0; background: none; }

  .services--varf .services__inner--varf .sv,
  .services--varf .services__inner--varf .sv.is-in,
  .services--varf .services__inner--varf .sv:not(.is-on) {
    position: sticky;
    display: block; max-height: none; overflow: visible;
    opacity: 1; border: 0; margin: 0;
    padding: 30px var(--gut) 34px;
    background: #FFFFFF;
    min-height: calc(100vh - 92px - 75vw); min-height: calc(100svh - 92px - 75vw);
    transition: opacity .38s cubic-bezier(.22,.61,.36,1);
  }
  /* +6 each, so a sliver of the card beneath stays visible as they pile */
  .services--varf .sv:nth-child(1) { top: calc(92px + 75vw); }
  .services--varf .sv:nth-child(2) { top: calc(92px + 75vw + 6px); }
  .services--varf .sv:nth-child(3) { top: calc(92px + 75vw + 12px); }
  .services--varf .sv:nth-child(4) { top: calc(92px + 75vw + 18px); }
  .services--varf .sv:nth-child(5) { top: calc(92px + 75vw + 24px); }

  /* a card that has been covered fades rather than sitting under the pile */
  .services--varf .services__inner--varf .sv.is-spent { opacity: 0; }

  .services--varf .services__inner--varf .sv__t,
  .services--varf .services__inner--varf .sv.is-on .sv__t {
    display: block; padding: 0;
    font-size: 26px; line-height: 34px; font-weight: 500; color: #12294D;
  }
  .services--varf .services__inner--varf .sv__d,
  .services--varf .services__inner--varf .sv.is-on .sv__d {
    max-height: none; opacity: 1; overflow: visible; padding: 14px 0 0;
  }
  .services--varf .sv__d p { font-size: 16px; line-height: 28px; color: #43505F; }

  .portfolio { margin-top: 0; }
}

/* Three fixes on the mobile stack.

   1. The blue behind the photographs. .services__visual carries a
      rgba(0,124,187,.25) multiply overlay, and the two photo layers cross-fade
      through a moment where their combined alpha is under 1 — so mid-swap you
      were seeing the ground through them, tinted blue by that overlay. The
      overlay goes on mobile and the ground stays opaque.

   2. The mission plate stops animating. It is a flat colour behind a headline;
      there is nothing for a rise to reveal, and it read as the background
      moving. The headline still arrives.

   3. Portfolio rises over the last card again. The -100svh was sized for the
      old track and became wrong when the track went; this overlaps the tail of
      the stack instead. */
@media (max-width: 900px) {
  .services__inner--varf .services__visual::after { display: none; }
  .services__inner--varf .services__visual { background-color: #0D1F3B; }

  .m-ready .mission__l[data-m-rise],
  .m-ready .mission__l[data-m-rise].is-in {
    opacity: 1; transform: none; transition: none;
  }

  .portfolio { margin-top: -46vh; margin-top: -46svh; position: relative; z-index: 3; }
}

/* Each card needs to read as its own plate, not as more white page. Without an
   edge, one white card sliding over another white card is invisible - the
   motion was happening and there was nothing to see it against.

   A soft shadow cast upward off the card's top edge does it: as the card rises
   it darkens the one beneath just before it covers it, so the slide reads.
   Square corners throughout, per the sheet: the edge is the shadow, not a
   radius. */
@media (max-width: 900px) {
  .services--varf .services__inner--varf .sv,
  .services--varf .services__inner--varf .sv.is-in,
  .services--varf .services__inner--varf .sv:not(.is-on) {
    box-shadow: 0 -20px 36px -24px rgba(13, 31, 59, .34);
  }
  /* the first card sits directly under the photograph, where a shadow would
     read as a seam rather than depth */
  .services--varf .services__inner--varf .sv:first-child { box-shadow: none; }
}

/* Portfolio is pulled up 46svh so it rides over the section below it. On mobile
   the section below is the card stack, and the last card had only 40px of tail
   behind it, so the pull-up landed straight on the card that was still being
   read. Give the stack the same 46svh back as trailing space: the overlap now
   consumes the tail instead of the copy, and the last card gets a full park
   before the portfolio edge reaches it. */
@media (max-width: 900px) {
  .services--varf { padding-bottom: calc(46vh + 40px); padding-bottom: calc(46svh + 40px); }
}

/* 0.68.2 - the tail belongs INSIDE the list, not on the section.

   A sticky child can only stick within its parent's content box, and the last
   card is the list's last child, so the list ended with it: the card locked for
   an instant and then scrolled away, leaving the portfolio to arrive over blank
   white. Padding on the section could not help, since it sits outside the box
   that bounds the sticking.

   A block ::after inside the list extends that box. The last card now holds its
   lock through the whole tail, and the portfolio's -46svh rides up over it the
   same way each card rides over the one before. */
@media (max-width: 900px) {
  .services--varf { padding-bottom: 0; }
  .services--varf .services__list::after {
    content: ''; display: block; height: calc(46vh + 40px); height: calc(46svh + 40px);
  }
}

/* ══════════════════════════════════════════════════════════════════
   TABLET · 761–1100
   A dedicated middle layout: neither an enlarged phone stack nor a clipped
   desktop rail. Mobile and desktop remain unchanged outside this range.
   ══════════════════════════════════════════════════════════════════ */
@media (min-width: 761px) and (max-width: 1100px) {
  :root { --gut: clamp(36px, 5vw, 56px); }

  /* Preserve Connect and restore access to the nav hidden below 1100. */
  .topbar, .topbar__inner { height: 32px; }
  .topbar__lede { font-size: 8px; line-height: 12px; }
  .topbar__links { gap: 20px; }
  .navrow { height: 88px; }
  .navrow__logo img { width: 90px; height: 45px; }
  .navrow__cta { margin-left: auto; }
  .burger {
    display: flex; flex-direction: column; justify-content: space-between;
    width: 34px; height: 22px; margin-left: 34px; padding: 0;
    border: 0; background: none; cursor: pointer;
  }
  .burger i { display: block; height: 2px; background: #fff; transition: transform .3s var(--ease), opacity .2s var(--ease), width .3s var(--ease); }
  .burger i:nth-child(1), .burger i:nth-child(2) { width: 34px; }
  .burger i:nth-child(3) { width: 23px; }
  .burger[aria-expanded="true"] i:nth-child(1) { transform: translateY(10px) rotate(45deg); }
  .burger[aria-expanded="true"] i:nth-child(2) { opacity: 0; }
  .burger[aria-expanded="true"] i:nth-child(3) { width: 34px; transform: translateY(-10px) rotate(-45deg); }
  .hdr__plate { height: 120px; }
  .hdr.is-compact .navrow { height: 68px; }
  .hdr.is-compact .hdr__plate { height: 100px; }
  .hdr.is-compact .navrow__logo img { width: 72px; height: 36px; }

  .mobnav {
    position: fixed; inset: 0; z-index: 55; display: flex;
    flex-direction: column; justify-content: flex-end;
    background: var(--navy-deep); overflow: hidden;
  }
  .mobnav[hidden] { display: none; }
  .mobnav__bg { position: absolute; inset: 0; background: url('assets/img/hero.jpg?a=0.71.7') 60% 40% / cover; }
  .mobnav__scrim { position: absolute; inset: 0; background: linear-gradient(180deg, rgba(10,26,50,.97), rgba(10,26,50,.90) 58%, rgba(10,26,50,.96)); }
  .mobnav__inner {
    position: relative; z-index: 2; height: 100%; overflow-y: auto;
    padding: 136px var(--gut) 36px;
    display: grid; grid-template-columns: minmax(0, 1fr) minmax(240px, .72fr);
    grid-template-rows: auto auto 1fr auto; column-gap: 72px;
  }
  .mobnav__list { grid-column: 1; grid-row: 1 / 4; list-style: none; margin: 0; padding: 0; }
  .mobnav__list a { display: block; padding: 18px 0; border-bottom: 1px solid var(--navy-rule); font-size: 36px; line-height: 44px; font-weight: 300; letter-spacing: -.02em; color: #fff; }
  .mobnav__cta { grid-column: 2; grid-row: 1; align-self: start; min-width: 160px; }
  .mobnav__cta .cta__label { font-size: 18px; }
  .mobnav__media { grid-column: 2; grid-row: 2; align-self: start; margin-top: 38px; display: flex; flex-direction: column; gap: 14px; }
  .mobnav__lbl { color: var(--blue-lt); font-size: 10px; line-height: 16px; }
  .mobnav__sub { display: flex; gap: 24px; }
  .mobnav__sub a { color: var(--dim); font-size: 15px; line-height: 24px; }
  .mobnav__foot { grid-column: 1 / -1; grid-row: 4; padding-top: 24px; border-top: 1px solid var(--navy-rule); display: flex; align-items: flex-end; justify-content: space-between; gap: 24px; }
  .mobnav__where { display: flex; flex-direction: column; gap: 6px; }
  .mobnav__where .mono, .mobnav__lang .mono { font-size: 10px; line-height: 16px; color: var(--muted); }
  .mobnav__lang { display: flex; align-items: center; gap: 14px; }
  .mobnav__lang .is-on { color: #fff; }
  .mobnav__lang i { width: 1px; height: 12px; background: #3A5878; }

  main > section { scroll-margin-top: 100px; }
  .hero__head { bottom: 232px; }
  .hero__title { font-size: clamp(66px, 8.4vw, 86px); line-height: 1.04; }
  .hero__body { max-width: 650px; font-size: 18px; line-height: 29px; }
  .powered { height: 144px; }
  .powered__label { top: 22px; }
  .powered .marquee { top: 70px; }
  .statement--band { padding: 96px 0; }
  /* The lit copy still uses its rect-based mobile path; the long desktop pin is
     unnecessary on a touch-first middle viewport. */
  .introphoto { height: clamp(600px, 80vh, 720px); height: clamp(600px, 80svh, 720px); overflow: hidden; }
  .introphoto__pin { position: relative; top: auto; height: 100%; }

  /* A tablet has room for the mission image; do not fold it into Services. */
  .mission { min-height: 0; margin-top: 0; overflow: clip; }
  .mission__title { font-size: clamp(26px, 3.2vw, 34px); line-height: 1.34; }

  /* Compact two-column pin instead of an enlarged phone card stack.
     The frame mirrors the desktop composition: the photograph bleeds from the
     viewport's left edge and runs the full height of the held frame - no inset
     tile - and the list column keeps the rail gutter on its side only. */
  .services--varf { height: 360vh; height: 360svh; padding: 0; overflow: visible; background: #fff; }
  .services__pin {
    /* below the compact tablet header, not under it */
    position: sticky; top: 100px; height: calc(100vh - 100px); height: calc(100svh - 100px);
    display: grid; overflow: clip; background: #fff;
  }
  .services__stage { width: 100%; height: 100%; }
  .services__inner--varf {
    display: grid; grid-template-columns: minmax(280px, 42%) 1fr;
    column-gap: clamp(32px, 5vw, 56px); align-items: center;
    width: 100%; height: 100%; padding: 0 var(--gut) 0 0;
  }
  .services__inner--varf .services__visual {
    display: block; position: relative; top: auto; z-index: 0;
    width: 100%; height: 100%; min-height: 0; align-self: stretch;
    aspect-ratio: auto; margin: 0; box-shadow: none;
  }
  .services__inner--varf .services__visual::after {
    display: block; mix-blend-mode: normal; background: rgba(0,124,187,.12);
  }
  .services--varf .services__col {
    position: relative; z-index: 1; width: 100%; padding: 0 0 0 26px;
    border-left: 2px solid #C9D4E1; background: none; align-self: center;
  }
  .services--varf .services__list { position: relative; }
  .services--varf .services__list::before,
  .services--varf .services__list::after,
  .services--varf .svc__rule,
  .services--varf .services__rail,
  .services--varf .sv__mark { display: none; }
  .services--varf .services__inner--varf .sv,
  .services--varf .services__inner--varf .sv.is-in,
  .services--varf .services__inner--varf .sv:not(.is-on) {
    position: relative; top: auto; display: block;
    min-height: 0; max-height: none; margin: 0; padding: 0;
    overflow: visible; opacity: 1; background: #fff; box-shadow: none;
    border: 0; border-bottom: 1px solid #C9D4E1;
  }
  .services--varf .services__inner--varf .sv:first-child { border-top: 1px solid #C9D4E1; }
  .services--varf .services__inner--varf .sv.is-past,
  .services--varf .services__inner--varf .sv.is-spent { opacity: 1; }
  .services--varf .services__inner--varf .sv.is-on::before {
    content: ''; position: absolute; left: -28px; top: 0; width: 2px; height: 100%;
    background: #007CBB; z-index: 2;
  }
  .services--varf .services__inner--varf .sv__t,
  .services--varf .services__inner--varf .sv.is-on .sv__t {
    display: block; padding: 14px 0; font-size: 21px; line-height: 28px;
  }
  .services--varf .services__inner--varf .sv__d { overflow: hidden; }
  .services--varf .services__inner--varf .sv__d p { margin: 0 0 15px; font-size: 14px; line-height: 23px; }
  /* The last viewport of the track is the portfolio's to ride over, exactly as
     on desktop - leaving it at 0 made a full screen of dead scroll after the
     fifth row. */
  .portfolio { margin-top: -100vh; margin-top: -100svh; position: relative; z-index: 2; }

  /* Section heads stack inside the available rail instead of overflowing it. */
  .split, .portfolio .portfolio__head, .markets__head {
    flex-direction: column; align-items: stretch; gap: 28px;
  }
  .split .split__l, .split .split__r,
  .markets__head .split__l, .markets__head .markets__r {
    flex: 0 0 auto; width: 100%; max-width: none; min-height: 0;
  }
  .portfolio .portfolio__head { padding: 56px 0 44px; }
  .portfolio__title, .markets__title, .careers__title { font-size: clamp(60px, 8vw, 78px); }
  .portfolio__copy { max-width: 720px; font-size: 19px; line-height: 28px; }
  .portfolio .wall { grid-template-columns: repeat(2, 1fr); margin-top: 0; }
  .wall__copy { left: 26px; right: 26px; bottom: 26px; }

  .bandquote { padding-block: 72px; }
  .bandquote__copy { font-size: clamp(28px, 3.7vw, 38px); line-height: 1.4; }

  .markets__head { padding: 56px 0 28px; gap: 36px; }
  .markets__r { gap: 24px; }
  .switch { width: auto; justify-content: flex-start; gap: 38px; }
  .switch__tab { font-size: clamp(34px, 4.8vw, 46px); }
  .markets__copy { max-width: 760px; font-size: 17px; line-height: 27px; }
  .markets__indexrow { padding: 8px 0 28px; }
  .mcard { width: clamp(320px, 39vw, 380px); height: 460px; }
  .markets__scroll .mcards .mcard.is-focus { width: min(500px, calc(var(--vwc, 100vw) - var(--gut) * 2)); }
  .mcard__chip { top: 20px; bottom: auto; }
  .mcard__text { left: 24px; right: 24px; width: auto; }

  .facts__rowgrid { grid-template-columns: repeat(2, 1fr); }
  .factcell { padding: 38px 24px 40px; }
  .factcell + .factcell { border-left: 0; }
  .factcell:nth-child(even) { border-left: 1px solid #C9D4E1; }
  .factcell:nth-child(n+3) { border-top: 1px solid #C9D4E1; }

  .safety__band { height: 440px; }
  .safety__copy { bottom: 48px; }
  .safety__bandC .safety__detail { padding: 64px var(--gut) 72px; }

  .careers__grid { padding: 64px var(--gut) 88px; }
  .careers__img { height: clamp(340px, 54vw, 460px); }
  .careers__r { max-width: 760px; }

  /* A compact four-column footer still fits a landscape tablet viewport. The
     old two-column fallback created implicit tracks, then placed Connect and
     Follow in the same lower-left area. Every block has an explicit cell now. */
  .ftr__inner {
    display: grid;
    grid-template-columns: minmax(0, 1.5fr) minmax(0, .75fr) minmax(0, 1fr) minmax(0, 1fr);
    gap: 24px; padding: 36px 0 28px;
  }
  .ftr__brand { grid-column: 1; grid-row: 1; }
  .ftr__links { grid-column: 2; grid-row: 1; padding-top: 28px; }
  .ftr__cos { grid-column: 3; grid-row: 1; }
  .ftr__addr { grid-column: 4; grid-row: 1; }
  .ftr__connect { grid-column: 1 / 3; grid-row: 2; align-self: start; }
  .ftr__follow {
    grid-column: 4; grid-row: 2; justify-self: end; align-self: start;
    align-items: flex-end; width: max-content;
  }
  .ftr__logo, .ftr__logo img { width: 88px; height: auto; }
  .ftr__brand { gap: 14px; }
  .ftr__divide { height: 58px; }
  .ftr__we { font-size: 26px; line-height: 32px; }
  .ftr__links a, .ftr__cos li, .ftr__addr p { font-size: 13px; line-height: 19px; }
  .ftr__connectline { width: min(360px, 100%); }
  .ftr__follow .lbl { text-align: right; }
}

@media (min-width: 761px) and (max-width: 900px) {
  .mission { display: grid; grid-template-columns: 1fr; height: auto; }
  .mission__l { padding: 72px var(--gut); }
  .mission__r { display: block; min-height: 360px; }
}

@media (min-width: 960px) and (max-width: 1100px) {
  .portfolio .wall { grid-template-columns: repeat(3, 1fr); }
}

/* 0.68.5 - portfolio head aligns its two columns.

   The head was the only .split on the site overriding the sitewide flex-start
   to center, so the 96px display head and the 32px copy were centred against
   each other and neither first line agreed with the other.

   flex-start alone is not enough. Aligning the BOXES leaves the text visibly
   off, because a 96px line box carries 20.1px above its cap height and a 32px
   line box carries 10.2px. The 10px below is that difference, measured from
   Archivo's own metrics at both sizes, so the two first lines start on the
   same optical line rather than the same box edge. */
@media (min-width: 901px) {
  .portfolio .portfolio__head { align-items: flex-start; }
  .portfolio .portfolio__head .portfolio__copy { padding-top: 10px; }
}

/* 0.69.2 - every card overlaps at the SAME place.

   The cards were staggered 6px apart, copied from the reference where the stack
   is meant to read as a visible pile of edges. Here it just meant the seam sat
   6px lower on each section and 24px lower by the last, which reads as drift
   rather than as design. One shared top: the overlap lands identically every
   time, and later cards still cover earlier ones by DOM order alone. */
@media (max-width: 760px) {
  .services--varf .sv:nth-child(1),
  .services--varf .sv:nth-child(2),
  .services--varf .sv:nth-child(3),
  .services--varf .sv:nth-child(4),
  .services--varf .sv:nth-child(5) { top: calc(92px + 75vw); }

  /* White edge instead of the navy one. */
  .services--varf .services__inner--varf .sv,
  .services--varf .services__inner--varf .sv.is-in,
  .services--varf .services__inner--varf .sv:not(.is-on) {
    box-shadow: 0 -20px 30px -12px rgba(255, 255, 255, .92);
  }
  .services--varf .services__inner--varf .sv:first-child { box-shadow: none; }
}

/* 0.69.3 - the feather moves INSIDE the card.

   A box-shadow paints outside the box, so the white glow landed on the bottom of
   the photograph whenever a card was parked flush under it. It also could not
   soften the edge it was drawn from: the card's own background still ended in a
   hard line, and the glow simply sat above that line.

   Making the top of the card's background a gradient does both jobs. The card
   fades in from transparent over 44px, so the outgoing card reads through it and
   dissolves rather than being cut, and because it is the card's own background it
   can never bleed past the card's box onto the photograph.

   Top padding grows to clear the fade, so a title never sits in the translucent
   band with the previous card's copy showing through it. */
@media (max-width: 760px) {
  .services--varf .services__inner--varf .sv,
  .services--varf .services__inner--varf .sv.is-in,
  .services--varf .services__inner--varf .sv:not(.is-on) {
    box-shadow: none;
    background: linear-gradient(180deg, rgba(255, 255, 255, 0) 0, rgba(255, 255, 255, .86) 46px, #FFFFFF 78px);
    padding-top: 88px;
  }
  /* The first card has nothing behind it but the photograph it parks under, so a
     translucent band there would show the navy ground, not a card. */
  .services--varf .services__inner--varf .sv:first-child {
    background: #FFFFFF;
  }
  /* The first card keeps the SAME top padding as the rest even though it has no
     fade to clear. The padding is what puts every title on one line as the stack
     runs; dropping it back to 30 on the one card that does not need the fade put
     Preconstruction 58px above every heading that followed it. */
}

/* 0.69.5 - back to the original Preconstruction position, all five matched.

   The 88px top padding only ever existed to keep a title out of the fade band.
   Restoring the original 30px means the fade has to end above the title instead,
   so it runs 30px rather than 78. Still a gradient rather than an edge, so there
   is no line to see; it is just a shorter dissolve. The alternative was leaving
   every heading 58px lower than it was drawn to buy a longer fade. */
@media (max-width: 760px) {
  .services--varf .services__inner--varf .sv,
  .services--varf .services__inner--varf .sv.is-in,
  .services--varf .services__inner--varf .sv:not(.is-on) {
    padding-top: 30px;
    background: linear-gradient(180deg,
      rgba(255, 255, 255, 0) 0,
      rgba(255, 255, 255, .78) 16px,
      #FFFFFF 30px);
  }
  .services--varf .services__inner--varf .sv:first-child { background: #FFFFFF; }
}

/* 0.70.1 - footer social row. X and YouTube have no destination yet, so their
   icons are hidden rather than shipped dead; the hidden attribute alone loses
   to the row's display:flex, which is why the rule is explicit. Facebook joined
   from the client's live site. */
.ftr__social a[hidden] { display: none; }

/* 0.70.2 - the footer company names are links now, so they take the same
   resting grey and white hover the page-link column beside them already has. */
.ftr__cos li a { color: #C6CACE; transition: color .16s var(--ease); }
.ftr__cos li a:hover { color: #FFFFFF; }

/* 0.70.6 - a focused strip belongs to the focused card.

   With one card focused, hovering any other card still ran the hover widen, so
   the strip reflowed and the card you had deliberately opened gave up its room
   to whatever the pointer grazed. While a focus is held, the other cards no
   longer respond to hover at all; the focused card keeps its width, and the
   knocked-back state stays knocked back. Selecting a different card is still a
   click away, so nothing is lost but the accidental reflow. */
@media (hover: hover) and (pointer: fine) and (min-width: 1001px) {
  .markets__scroll:not(:active) .mcards:has(.is-focus) .mcard:hover:not(.is-focus) { width: 420px; }
}
/* 901-1000px: the rest width is 320 there, so the suppressed hover is too. */
@media (hover: hover) and (pointer: fine) and (min-width: 901px) and (max-width: 1000px) {
  .markets__scroll:not(:active) .mcards:has(.is-focus) .mcard:hover:not(.is-focus) { width: 320px; }
}

/* 0.70.13 - the hero head centres in its actual zone.

   Bottom-anchoring at a fixed 272 put the block optically low: the space that
   matters runs from the header's bottom edge (34 bar + 104 row = 138) down to
   the top of the powered strip (158), and the head now sits at the middle of
   exactly that zone, whatever the viewport height. */
.hero__head {
  bottom: auto;
  top: calc(138px + (100vh - 138px - 158px) / 2); top: calc(138px + (100svh - 138px - 158px) / 2);
  /* NOT transform: the hero parallax writes style.transform every frame and
     would erase it. The translate property is separate and composes. */
  translate: 0 -50%;
}
@media (max-width: 760px) {
  /* The FULL mobile header - 30 bar + 82 row = 112 - not the compact 92: at
     the hero nothing has compacted yet, and the 92 was reading 10px high. */
  .hero__head { top: calc(112px + (100vh - 112px - 140px) / 2); top: calc(112px + (100svh - 112px - 140px) / 2); }
}

/* Tablet band header is 32+88=120 with a 144 strip - the centring formula gets
   the band's own numbers, matching the header block above. */
@media (min-width: 761px) and (max-width: 1100px) {
  .hero__head { top: calc(120px + (100vh - 120px - 144px) / 2); top: calc(120px + (100svh - 120px - 144px) / 2); }
}

/* The legacy phone-accordion passes fade and collapse passed rows through
   five-class selectors (.sv.is-in.is-past), which out-specify the tablet
   block's four-class resets - so on a tablet the passed row vanished and its
   box collapsed onto the next. Matched specificity, appended last: in the
   tablet band a passed row is simply a closed row. */
@media (min-width: 761px) and (max-width: 1100px) {
  .services--varf .services__inner--varf .sv.is-in.is-past,
  .services--varf .services__inner--varf .sv.is-in.is-spent {
    opacity: 1; max-height: none; overflow: visible; pointer-events: auto;
    padding: 0; margin: 0; border: 0; border-bottom: 1px solid #C9D4E1;
  }
  .services--varf .services__inner--varf .sv.is-in.is-past .sv__t {
    opacity: 1; transform: none;
  }
}

/* 0.70.17 - portfolio wall logos read the same SIZE.

   One 172x40 contain-box treated every mark alike, but the marks are nothing
   alike: GW Modular is a 4.9:1 wordmark and Aitken is a 1.3:1 crest, so inside
   the same box the wordmarks spanned 172px while the crests shrank to 53. The
   eye reads area, not box - so each mark gets its own height, chosen so the
   rendered areas come out even, wordmarks running wider and shallower, crests
   taller and narrower. --lh carries the height per logo and the mobile block
   scales all of them with one factor, so the balance survives the breakpoint. */
.wall__cell img {
  width: auto; height: calc(var(--lh, 40) * 1px);
  object-fit: contain; object-position: left center;
}
.wall__cell img[src*="gwmodular"]  { --lh: 28; }
.wall__cell img[src*="graywolf"]   { --lh: 28; }
.wall__cell img[src*="dbmvircon"]  { --lh: 31; }
.wall__cell img[src*="banker"]     { --lh: 34; }
.wall__cell img[src*="milco"]      { --lh: 36; }
.wall__cell img[src*="schuff"]     { --lh: 38; }
.wall__cell img[src*="nyc"]        { --lh: 46; }
.wall__cell img[src*="aitken"]     { --lh: 46; }
@media (max-width: 900px) {
  .wall__cell img { height: calc(var(--lh, 40) * 0.58px); }
}

/* ══════════════════════════════════════════════════════════════════
   0.70.19 · Density band - 901px to 1439px
   ══════════════════════════════════════════════════════════════════
   The 13-inch class (1280x800, 1366x768) was rendering close to the full
   1440-canvas scale: display type near its cap and fixed vertical paddings
   sized for a 900-tall viewport. Everything here pulls the scale down roughly
   a step inside the band and no further - at 1440 and above nothing changes,
   under 901 the tablet and phone architectures own the layout. Type stays
   fluid; these are lower slopes and caps, not new fixed sizes. */
@media (min-width: 901px) and (max-width: 1439px) {
  :root { --gut: clamp(56px, 7vw, 120px); }
  @media (max-width: 1280px) { :root { --gut: 56px; } }

  .hero__title { font-size: clamp(58px, 7.2vw, 100px); }
  .hero__body { max-width: 660px; font-size: 18px; line-height: 29px; }

  .statement--band { padding: clamp(56px, 5.5vw, 80px) 0; }
  .statement--band .statement__copy { font-size: clamp(32px, 3.85vw, 55px); }

  .introphoto__copy { font-size: clamp(22px, 2.3vw, 33px); }
  .mission__title { font-size: clamp(24px, 2.5vw, 34px); }

  .services__inner--varf .sv__t,
  .services__inner--varf .sv.is-on .sv__t { font-size: 23px; line-height: 30px; }

  .portfolio { padding-top: clamp(64px, 6.7vw, 96px); }
  .portfolio__title, .markets__title, .careers__title { font-size: clamp(54px, 6.1vw, 88px); }
  .portfolio .portfolio__head { padding: 40px 0 36px; }

  .bandquote__copy { font-size: clamp(24px, 2.65vw, 38px); }
  .switch__tab { font-size: clamp(30px, 3.5vw, 50px); }

  .facts--row { padding: clamp(44px, 4.2vw, 60px) 0; }
  .factcell__num { font-size: clamp(33px, 3.3vw, 47px); }
  .num--xl { font-size: clamp(80px, 8.2vw, 118px); }
  .num { font-size: 64px; line-height: 72px; }
  .num__unit { font-size: 23px; line-height: 30px; }

  .safety__bandC .safety__detail { padding: 48px var(--gut) 56px; }
  .careers__grid { padding: 52px var(--gut) 72px; }
}

/* 0.70.20 - the hero subtext keeps a measure on wide-mobile widths. The phone
   layout let it run the full rail, which at iPad-mini-class widths (744-900)
   meant lines running effectively edge to edge. A 540px cap restores a read-
   able measure; true phones sit well under it already. */
@media (max-width: 900px) {
  .hero__body { max-width: 540px; }
}

/* 0.70.21 - the preconstruction pin respects the header in the band.

   The desktop frame centres a fixed-height composition against the FULL
   viewport (padding-top: (100vh - 594px) / 2) with an 88px reveal sliver
   below. On a 1440x900 canvas that clears the header by 91px; padding-top: (100svh - 594px) / 2) with an 88px reveal sliver
   below. On a 1440x900 canvas that clears the header by 91px; at 1200x700 the
   first row goes 9px UNDER it, and every in-between width shows the sliver as
   a dead band. In the 1101-1439 band the pin now starts below the compact
   header and runs to the viewport bottom, the composition centres in what
   remains, and the photo column goes fluid so it stops squeezing the list.
   At 1440+ nothing changes. */
@media (min-width: 1101px) and (max-width: 1439px) {
  .services__pin {
    top: 110px; height: calc(100vh - 110px); height: calc(100svh - 110px);
    align-content: center; padding-top: 0;
  }
  .services__inner--varf {
    grid-template-columns: minmax(300px, 44%) minmax(0, 1fr);
    padding-block: clamp(36px, 5.5vh, 80px) clamp(30px, 5vh, 70px); padding-block: clamp(36px, 5.5svh, 80px) clamp(30px, 5svh, 70px);
  }
  .svc__rule--gray { top: clamp(36px, 5.5vh, 80px); top: clamp(36px, 5.5svh, 80px); height: auto; bottom: clamp(30px, 5vh, 70px); bottom: clamp(30px, 5svh, 70px); }
}

/* 0.70.22 - the sticky photo yields to the text on wide phones.

   The stack sized the photograph purely by width - 75vw at 4:3 - which at the
   wide end of the phone band (600-760px) is 450-570px of image. On anything
   but a tall screen that pushed the card past the fold: photo held, title
   buried "below the lip". The photo now takes whichever is SHORTER, its 4:3
   height or 46% of the viewport - on narrow phones nothing changes (75vw of
   390 is 293, well under), and on wide-short screens the photograph crops
   tighter instead of burying the text. Every card stop and the card's minimum
   height ride the same expression, so the geometry stays one system. */
@media (max-width: 760px) {
  .services__inner--varf .services__visual {
    aspect-ratio: auto; height: min(75vw, 46vh); height: min(75vw, 46svh);
  }
  .services--varf .services__inner--varf .sv,
  .services--varf .services__inner--varf .sv.is-in,
  .services--varf .services__inner--varf .sv:not(.is-on) {
    min-height: calc(100vh - 92px - min(75vw, 46vh)); min-height: calc(100svh - 92px - min(75vw, 46svh));
  }
  .services--varf .sv:nth-child(1),
  .services--varf .sv:nth-child(2),
  .services--varf .sv:nth-child(3),
  .services--varf .sv:nth-child(4),
  .services--varf .sv:nth-child(5) { top: calc(92px + min(75vw, 46vh)); top: calc(92px + min(75vw, 46svh)); }
}

/* 0.70.28 - mobile footer: equal link rhythm, and small enough to reveal.

   The page-links column ran a 7px gap where the companies column runs 3, so
   the two lists read as different rhythms. And the footer measured 840 against
   a 844 viewport - 20px over the reveal's fit margin - so phones always fell
   back to flow mode: no reveal, no parallax. Trimmed the legal band and inner
   padding to buy the margin back; the reveal machinery needs no changes, it
   simply starts passing its own fit check. */
@media (max-width: 900px) {
  .ftr__links { gap: 3px; }
  .ftr__legal { height: 64px; }
  /* The tight trim existed to squeeze under the old fixed-reveal fit check.
     The curtain works at any height, so the footer breathes again - the
     measure pass picks the new height up automatically. */
  .ftr__inner { padding-top: 52px; padding-bottom: 38px; row-gap: 32px; }
}

/* 0.70.31 - the real curtain reveal on mobile.

   The desktop reveal parks the footer with position: fixed behind the page,
   which requires the footer to FIT the viewport - and an 806px footer never
   fits a real phone's ~660px small viewport, so phones fell back to plain
   flow: footer stuck to the bottom of careers, no under-reveal, no power.

   The flow fallback is now a true curtain that works at ANY footer height:
   the footer box stays in normal flow (reserving its height) and clips with
   clip-path - which also makes it the containing block for fixed descendants
   - while the curtain inside is fixed to the viewport bottom. As the page
   scrolls its last stretch, the box's top edge rises and progressively
   unclips the stationary curtain: revealed from beneath, exactly like
   desktop, immune to the URL bar because nothing measures the viewport. */
.ftr.is-flow {
  position: relative;
  clip-path: inset(0);
  /* The box reserves the curtain PLUS the overflow runway: on a phone the
     curtain is taller than the screen, and the extra height is the scroll
     distance phase two uses to walk the rest of it into view. */
  height: calc(var(--ftr-h, 0px) + var(--ftr-over, 0px));
  overflow: visible;
}
.ftr.is-flow .ftr__curtain {
  /* TOP-anchored and stacked ABOVE the box gradient: on desktop the inner
     carries z-index 2 over the ::before scrim, and the fixed wrapper was
     flattening that order on mobile - the gradient painted over the text.
     The clip box does all the masking; the drive locks and releases this via
     transform. */
  position: fixed; left: 0; top: 0; bottom: auto; width: 100%; z-index: 2;
}

/* 0.71.7 - the market cards'
   entrance slide is retired (see motion.js REVEAL list): the alt marquee
   had to neutralise it on every rebuild, and cards should simply be
   present when the section arrives. */


/* ── markets switch + controls, review pass ───────────────────────
   The underline is a selection marker, so only the selected market carries
   it — the inactive tab was still showing the gray rule, which read as two
   underlines. And on desktop the category dropdown sat 24px off the pager
   arrows, close enough to read as one control; doubled to 48. Mobile keeps
   its space-between row, so the gap change is desktop-scoped. */
.switch__tab i { background: transparent; }
.switch__tab.is-active i { background: var(--blue); }
@media (min-width: 901px) { .markets__indexrow { gap: 48px; } }

/* ── markets switch: the rule travels ─────────────────────────────
   One marker slides between the tabs on selection. It replaces the active
   tab's static rule the moment JS installs it (has-marker); without JS the
   static rule still marks selection. */
.switch { position: relative; }
.switch__marker {
  position: absolute; height: 2px; background: var(--blue); pointer-events: none;
  transition: left .45s var(--ease), width .45s var(--ease);
}
.switch.has-marker .switch__tab.is-active i { background: transparent; }

/* ── markets switch: tabs sit together ────────────────────────────
   The space-between from 0.63.x was stretching the two tabs across the full
   555 column - the "awkward gap" was the column width, not the gap value.
   Back to a natural pair on the left rail. Desktop-scoped; mobile already
   sits flex-start at 38. */
@media (min-width: 901px) {
  .switch { width: auto; justify-content: flex-start; gap: 44px; }
}

/* ── footer: desktop grid rebuilt on wrappers ─────────────────────
   Four true columns. The outer two are flex stacks (brand over Connect,
   address over FOLLOW) so nothing waits on a shared grid row held tall by
   the companies list - Connect pulls up under the logo, FOLLOW up under the
   phone numbers. Columns 2-4 are content-sized and the leftover width all
   lands between the brand and the links, so the address block's right edge
   sits exactly on the rail. The links column gets the EXPLORE label, which
   levels all three column headers on one line. Below 1100 the wrappers
   dissolve (display: contents) and order restores the old stacking. */
@media (min-width: 1101px) {
  .ftr__inner { grid-template-columns: 1fr max-content max-content max-content; gap: 18px 56px; }
  .ftr__left { grid-column: 1; display: flex; flex-direction: column; align-items: flex-start; gap: 48px; }
  .ftr__right { grid-column: 4; display: flex; flex-direction: column; gap: 28px; }
  .ftr__links { grid-column: 2; padding-top: 0; }
  .ftr__cos { grid-column: 3; }
}
.ftr__links .lbl { margin-bottom: 6px; }
@media (max-width: 1100px) {
  .ftr__left, .ftr__right { display: contents; }
  .ftr__brand { order: 0; } .ftr__links { order: 1; } .ftr__cos { order: 2; }
  .ftr__addr { order: 3; } .ftr__connect { order: 4; } .ftr__follow { order: 5; }
}
@media (max-width: 900px) { .ftr__links .lbl { grid-column: 1 / -1; } }

/* ── footer: taller ground, tighter statement ─────────────────────
   The block earns more air now that the columns are ordered - top and bottom
   padding up from 56/40. And the two statement lines close ranks: 50px
   leading on 42px type left a visible slot between We Are and Builders. */
@media (min-width: 1101px) {
  .ftr__inner { padding: 96px 0 72px; }
  .ftr__we { line-height: 44px; }
}

/* ── Connect CTA geometry ─────────────────────────────────────────
   The mobnav takeover forced the CTA to a min-width for touch, but the rule
   is width: 100%, so it ran past the label and arrow - the 52px height is
   the touch target, the width can be honest. And the CTA's internal air
   tightens everywhere it appears: label-to-arrow 14 to 10, row-to-rule
   11 to 8. */
.mobnav__cta { min-width: 0; }
.cta { gap: 8px; }
.cta__row { gap: 10px; }

/* ── careers: Apply online hovers like the Connect CTA ────────────
   Same anatomy, same behavior: arrow slides 6, rule wakes to full
   strength and 2px. The rule moves to solid navy + opacity so hover
   can brighten it. */
.careers__arrow { transition: transform .18s var(--ease); }
.careers__link i { background: #1A3768; opacity: .5; transition: opacity .18s var(--ease), height .18s var(--ease); }
.careers__link:hover .careers__arrow { transform: translateX(6px); }
.careers__link:hover i { opacity: 1; height: 2px; }

/* the rule hugs the label - 8px below the descender line read as detached */
.careers__link i { bottom: -4px; }

/* ── mobile legal band: truly centered ────────────────────────────
   height 64 + padding-block 10 + a fixed 52px row = the row overflowed
   into the bottom padding and the type sat 5px low. The row just fills
   the band and centers. */
@media (max-width: 900px) {
  .ftr__legal { padding-block: 0; }
  .ftr__legalrow { height: 100%; }
}

/* Connect's rule tucks in to match Apply online's 4px seat */
.cta { gap: 5px; }

/* ── preconstruction: header-aware centering at every desktop width ──
   Above 1440 the stage still centered against the raw viewport, which
   tucked its top under the fixed header once the screen was short (13"
   laptops: 92px padding vs a 110px header). Same treatment the 1101-1439
   band already has: the pin lives in the zone below the header and the
   stage centers inside it, so composition and rule sit identically at
   every size. The 88px next-section sliver goes; consistency wins. */
@media (min-width: 1440px) {
  .services--varf .services__pin {
    top: 110px; height: calc(100vh - 110px); height: calc(100svh - 110px);
    align-content: center; padding-top: 0;
  }
}

/* ── header Connect compacts with the nav ─────────────────────────
   .is-compact set font-size on .cta, but the label and arrow carry their
   own explicit sizes, so the rule never reached them - the nav links
   stepped down to 14 and Connect stayed at 15. Same step, same curve. */
.hdr .cta__label, .hdr .cta__arrow { transition: font-size .58s cubic-bezier(.22,.61,.36,1); }
.hdr.is-compact .cta__label { font-size: 14px; }
.hdr.is-compact .cta__arrow { font-size: 13px; }
@media (prefers-reduced-motion: reduce) { .hdr .cta__label, .hdr .cta__arrow { transition: none; } }

/* ── mobile menu Connect returns to standard CTA scale ────────────
   It was scaled up to read as a peer to the 32px nav items; review call
   is the opposite - it is an action, not a menu item, so it wears the
   same 15/14 the header CTA wears. Height stays a full touch target. */
.mobnav__cta { height: 48px; flex-shrink: 0; }
.mobnav__cta .cta__label { font-size: 15px; line-height: 22px; }
.mobnav__cta .cta__arrow { font-size: 14px; line-height: 22px; }

/* ── preconstruction: one centering convention, every desktop width ──
   The 901-1100 band still centered the stage against the raw viewport
   while everything wider centers in the zone below the header. The pin
   already starts at the header's edge there - drop its padding math and
   let it center like the rest. */
@media (min-width: 901px) and (max-width: 1100px) {
  .services--varf .services__pin { align-content: center; padding-top: 0; }
}

/* ── preconstruction rule ends with the photo ─────────────────────
   In the 1101-1439 band the rule's clamped inset math ran it 12-20px past
   the image's base. The image is a fixed 498 in this band - the rule
   matches it exactly, as it already does at 1440+. */
@media (min-width: 1101px) and (max-width: 1439px) {
  .svc__rule--gray { height: 498px; }
}

/* ── preconstruction text ends on the rail ────────────────────────
   The inner grid is full-bleed for the photo's sake, but its right padding
   was a flat 24px, so the text column ran past the content rail - 20px at
   1440+, up to 72px mid-band. The rail gutter is what belongs there. */
@media (min-width: 1101px) {
  .services__inner--varf { padding-right: var(--rail-x); }
}

/* At 1440+ the photo column was a viewport calc that predated the rail
   padding, stranding the 570px text column short of the rail. The photo
   simply absorbs whatever the rail leaves - it bleeds left by design, so
   growing it wider is the design language, and the text lands exactly on
   the rail at any width. */
@media (min-width: 1440px) {
  .services__inner--varf { grid-template-columns: minmax(0, 1fr) 570px; }
}

/* the rule reads its seat from the measured text column (svcRuleSeat in
   app.js); the old viewport-center calc stays as the no-JS fallback */
@media (min-width: 1101px) {
  .svc__rule--gray, .svc__rule--blue { left: var(--svc-rule-x, calc(50% - 1px)); }
}

/* ── preconstruction photo grows into tall viewports ──────────────
   The photo was a fixed 498 no matter how much zone the screen offered.
   One variable now drives photo, rule, and stage: viewport minus the
   header (110), the stage's own 96 of padding, and 96 of breathing -
   floored at the current 498 so short screens (13") keep today's fit,
   capped at 680 so big displays stay composed. */
@media (min-width: 1101px) {
  :root { --svc-img-h: clamp(498px, calc(100vh - 302px), 560px); --svc-img-h: clamp(498px, calc(100svh - 302px), 560px); }
  .services__stage { height: calc(var(--svc-img-h) + 96px); }
  .services__inner--varf .services__visual { height: var(--svc-img-h); }
  .svc__rule--gray { height: var(--svc-img-h); }
}

/* ── CTA hover: the fly-through (option C from review) ────────────
   The arrow exits up-right through a mask and a twin re-enters from the
   lower left - motion in the direction the glyph points. The window takes
   over the arrow's type sizing (including the header's compact step) so
   both twins inherit; the old 6px slide dies with the main arrow's own
   hover transform being overridden below. */
.cta__win {
  position: relative; display: inline-block; overflow: hidden;
  width: 1em; height: 1lh; font-size: 14px; line-height: 22px;
}
.hdr .cta__win { transition: font-size .58s cubic-bezier(.22,.61,.36,1); }
.hdr.is-compact .cta__win { font-size: 13px; }
.cta__win .cta__arrow, .careers__win .careers__arrow {
  position: absolute; inset: 0; font-size: inherit; line-height: inherit;
  transition: transform .3s var(--ease), opacity .3s var(--ease);
}
.cta__arrow--ghost, .careers__arrow--ghost { transform: translate(-110%, 110%); opacity: 0; }
.cta:hover .cta__arrow--main { transform: translate(110%, -110%); opacity: 0; }
.cta:hover .cta__arrow--ghost { transform: none; opacity: 1; }
.careers__win { font-size: 17px; line-height: 24px; }
.careers__link:hover .careers__arrow--main { transform: translate(110%, -110%); opacity: 0; }
.careers__link:hover .careers__arrow--ghost { transform: none; opacity: 1; }
@media (prefers-reduced-motion: reduce) {
  .cta__win .cta__arrow, .careers__win .careers__arrow { transition: none; }
}

/* Fly-through softened: longer travel on an expo-style tail, with the
   incoming twin landing a beat after the exit starts so it reads as one
   continuous motion instead of a swap. */
.cta__win .cta__arrow, .careers__win .careers__arrow {
  transition: transform .55s cubic-bezier(.19, 1, .22, 1), opacity .38s cubic-bezier(.19, 1, .22, 1);
}
.cta__win .cta__arrow--ghost, .careers__win .careers__arrow--ghost { transition-delay: 70ms; }

/* ── the arrow matches the text height ────────────────────────────
   The glyph rode one size below its label (14 vs 15). The window is the
   single sizing authority now: same size as the label at rest and through
   the compact step, and the legacy per-arrow compact rule is neutralized
   so the twins always inherit. Apply online already sits at its label's
   17 via inherit. */
.cta__win { font-size: 15px; }
.hdr.is-compact .cta__win { font-size: 14px; }
.hdr.is-compact .cta__win .cta__arrow { font-size: inherit; }
/* Apply online keeps its label's 17 - re-asserted after the general 15 */
.careers__win { font-size: 17px; }

/* Optical seat: the arrow glyph hangs high against the label's x-height,
   so the whole window drops one pixel (the twins keep their own
   transforms free for the flight). */
.cta__win { translate: 0 1px; }
.careers__win { translate: 0 1.5px; }


/* ── mobile menu: Investors lives in the foot ─────────────────────
   The media block is gone; the Investors link takes the language
   toggle's seat in the bottom row, in the same quiet mono voice.
   Still an intentional dead link until the client sends the URL. */
.mobnav__inv { font-size: 10px; line-height: 16px; letter-spacing: .11em; color: var(--muted); transition: color .16s var(--ease); }
.mobnav__inv:hover { color: #fff; }
/* Investors sits beside the language toggle, one right-hand group */
.mobnav__footright { display: flex; align-items: center; gap: 24px; }

/* ── mobile menu: Connect is one of the links now ─────────────────
   Review call reversing the small-CTA pass: in the takeover it reads as
   the last menu item - same size and weight as the list, seated below
   it. The underline and fly-through stay. */
@media (max-width: 760px) {
  .mobnav__cta { height: auto; }
  .mobnav__cta .cta__label { font-size: 32px; line-height: 40px; font-weight: 300; }
  .mobnav__cta .cta__win { font-size: 32px; line-height: 40px; }
}
@media (min-width: 761px) and (max-width: 1100px) {
  .mobnav__list { grid-row: 1; }
  .mobnav__cta { grid-column: 1; grid-row: 2; align-self: start; height: auto; }
  .mobnav__cta .cta__label { font-size: 36px; line-height: 44px; font-weight: 300; }
  .mobnav__cta .cta__win { font-size: 36px; line-height: 44px; }
}

/* ── footer brand: one lockup, one knob ───────────────────────────
   Logo, divider, gap, and We Are Builders were sized independently at
   six breakpoints and their proportions drifted. Everything now derives
   from the brand's font-size in ems at the approved desktop ratio
   (183 logo : 42 type : 103 divider : 29 gap), so the unit scales as
   one piece and can never come apart. */
.ftr__brand { display: flex; align-items: center; font-size: 42px; gap: .69em; }
.ftr__logo, .ftr__logo img { width: 4.357em; height: auto; align-self: center; }
.ftr__divide { display: block; width: 1px; height: 2.452em; align-self: center; flex-shrink: 0; background: rgba(255,255,255,.24); }
.ftr__we { font-size: 1em; line-height: 1.048; }
@media (min-width: 1101px) and (max-width: 1439px) { .ftr__brand { font-size: 34px; } }
@media (min-width: 901px) and (max-width: 1100px) { .ftr__brand { font-size: 28px; } }
@media (max-width: 900px) { .ftr__brand { font-size: 26px; } }


/* ── hero: a real dissolve between cover slides ───────────────────
   Same symmetric curve as the services frame - the incoming photograph
   breathes in over the held one instead of stepping. */
.hero__stage { transition: opacity 1.2s cubic-bezier(.45, .05, .35, .95); }

/* ── careers grid: fluid below its 1440 design width ──────────────
   Fixed columns (560 + 555) plus fixed padding demand 1397px, so a
   half-screen window forced the whole document wider - which shifted
   every centered rail right and clipped every right-hand column on the
   page. Both columns flex within the rail gutter until the 1440 canvas
   can afford the fixed composition. */
@media (min-width: 901px) and (max-width: 1439px) {
  /* the white plate runs past the absolute photo: 52 top + 201 offset +
     400 tall + a 64px seat */
  .careers__grid { padding-top: 52px; padding-bottom: 72px; padding-inline: var(--rail-x); gap: clamp(40px, 5vw, 72px); }
  .careers__l { width: auto; flex: 1 1 460px; min-width: 0; }
  .careers__r { width: auto; flex: 1 1 420px; min-width: 0; }
}

/* ── splits go fluid below the 1440 canvas ────────────────────────
   The board-exact 645/555 columns carry flex-shrink 0, so below ~1310
   the rail cannot hold them and the whole DOCUMENT went wider than the
   window - every centered rail then shifted right and clipped its right
   column (portfolio, safety, markets, careers all at once). In the band
   the columns keep the 645:555 proportion but share whatever the rail
   actually has. */
@media (min-width: 901px) and (max-width: 1439px) {
  .split .split__l, .safety__bandC .safety__detail .split__l { flex: 645 645 0px; min-width: 0; }
  .split .split__r, .safety__bandC .safety__detail .split__r { flex: 555 555 0px; min-width: 0; max-width: none; }
  .markets__r { flex: 555 555 0px; min-width: 0; max-width: none; }
}

/* ── careers photo tracks its column in the band ──────────────────
   The absolute image kept a 1440-era width (rail-x + 580) while the
   columns went fluid, so at some widths it slid under the text. Anchored
   to the column itself now: full-bleed to the window's left edge, ending
   exactly at the column's right edge - the grid gap is the guaranteed
   clearance. top compensates for the section's 72px padding the old
   section-anchored offset included. */
@media (min-width: 1101px) and (max-width: 1439px) {
  /* in flow, not absolute: the column carries the photo, so the white
     plate grows around it and the padding is the guaranteed seat */
  .careers__img {
    position: static; top: auto;
    width: calc(100% + var(--rail-x)); margin-left: calc(-1 * var(--rail-x));
    margin-top: 44px; height: 400px;
  }
}

/* ── careers stacked: the photo takes the full rail ───────────────
   Below 1100 the grid stacks, but align-items: flex-start from the
   two-column pass let the columns shrink to content width - the photo
   rendered a third of the window. Both columns hold the full rail. */
@media (max-width: 1100px) {
  .careers__l, .careers__r { width: 100%; flex: 0 0 auto; min-width: 0; align-self: stretch; }
  /* full bleed: the photo breaks the rail gutters edge to edge */
  .careers__img { position: static; width: auto; margin-inline: calc(-1 * var(--gut)); height: clamp(240px, 46vw, 400px); }
}

/* ── portfolio + markets heads stack in the tablet band ───────────
   Between 901-1100 the two-column heads have no room to sit side by
   side - the title collided with the controls and copy. They stack:
   title first, controls and copy under it, full width, and the title's
   held min-height goes (it existed to balance the taller right column). */
@media (min-width: 901px) and (max-width: 1100px) {
  .portfolio__head.split, .markets__head.split { flex-direction: column; gap: 26px; }
  .portfolio__head .split__l, .portfolio__head .split__r,
  .markets__head .split__l, .markets__head .split__r { flex: 0 0 auto; width: 100%; min-width: 0; }
  .portfolio .split__l, .markets__head .split__l { min-height: 0; }
  .markets__r { flex: 0 0 auto; width: 100%; max-width: none; }
}

/* ── safety detail stacks on its plate in the tablet band ─────────
   Side-by-side columns wrapped unpredictably at 901-1100 and the copy
   ran off the navy plate - white text onto white ground, reading as a
   hidden section. Stacked in flow, the plate grows around everything. */
@media (min-width: 901px) and (max-width: 1100px) {
  .safety__bandC .safety__detail { flex-direction: column; gap: 28px; }
  .safety__bandC .safety__detail .split__l,
  .safety__bandC .safety__detail .split__r { flex: 0 0 auto; width: 100%; min-width: 0; }
}

/* ── facts 2x2: the rules join ────────────────────────────────────
   The 36px row gap severed the vertical divider at the horizontal
   crossing. No gap; the cells carry the air as padding, so the lines
   run continuous through the cross. */
@media (max-width: 1000px) {
  .facts__rowgrid { gap: 0; }
  .facts__cell { padding-top: 44px; padding-bottom: 44px; }
}

/* EXPLORE owns a label now at every width - the 30px top padding that
   used to fake alignment against the labeled columns sinks it instead */
.ftr__links { padding-top: 0; }

/* the panel's Connect is a menu item now - no CTA underline in the takeover */
@media (max-width: 1100px) { .mobnav__cta .cta__rule { display: none; } }

/* Connect enters the list rhythm: the same air the rows carry above
   their labels (their own padding), not an extra block margin */
@media (max-width: 760px) { .mobnav__cta { margin-top: 20px; } }
@media (min-width: 761px) and (max-width: 1100px) { .mobnav__cta { margin-top: 18px; } }

/* ── services: dwell after the last row ───────────────────────────
   Extra 50svh of track paired with stepScroll's desktop dwell reserve:
   the last paragraph gets a half-screen of quiet reading before the
   portfolio begins to rise. */
@media (min-width: 901px) {
  .services--varf { height: 500vh; height: 500svh; }
}

/* ── wall Visit link: blue voice, seated arrow, the fly-through ───
   The Visit line joins the accent voice at rest (it was white), the
   arrow matches the label size on the same optical seat, and hover runs
   the same masked exit/return the site's CTAs use. The legacy nudge
   rules on .wall__cta i are out-cascaded below. */
.wall__cta .mono { color: var(--blue-lt); }
.wall__win {
  position: relative; display: inline-block; overflow: hidden;
  width: 1.1em; height: 1.3em; font-size: 10px; translate: 0 0.5px;
}
.wall__cta .wall__win i {
  position: absolute; inset: 0; font-style: normal;
  font-size: inherit; line-height: 1.3; color: var(--blue-lt);
  transition: transform .55s cubic-bezier(.19, 1, .22, 1), opacity .38s cubic-bezier(.19, 1, .22, 1);
  transform: none;
}
.wall__cta .wall__win .wall__arr--ghost { transform: translate(-110%, 110%); opacity: 0; transition-delay: 70ms; }
.wall__cell:hover .wall__cta .wall__win .wall__arr--main { transform: none; }
.wall__cta:hover .wall__win .wall__arr--main { transform: translate(110%, -110%) !important; opacity: 0; }
.wall__cta:hover .wall__win .wall__arr--ghost { transform: none; opacity: 1; }
@media (max-width: 900px) { .wall__win { font-size: 9px; } }
