@font-face { font-family: 'Anton'; src: url('../assets/fonts/anton-latin.woff2') format('woff2'); font-display: swap; }
@font-face { font-family: 'Archivo'; src: url('../assets/fonts/archivo-latin.woff2') format('woff2'); font-weight: 400; font-display: swap; }
@font-face { font-family: 'Archivo'; src: url('../assets/fonts/archivo-700.woff2') format('woff2'); font-weight: 700; font-display: swap; }

/* Connected Sourcing brand palette, taken from the live site's own CSS.
   Two greens, and which one to use is a contrast decision, not a taste one:
   --c-green is the brand value and clears AA only as LARGE text on paper
   (3.75:1), so it fills surfaces whose label is >=19px bold and paints
   non-text accents. --c-green-deep is the same hue darkened; at 5.65:1 it is
   the only green safe for small text or small text ON it, and it doubles as
   the hover/pressed state. Measured against --c-paper, both directions.
   Ink moves off pure #111 to the brand near-black, everywhere — body copy on
   paper and dark-section backgrounds alike — so there is one ink, not two. */
:root {
  --c-paper: #F4F2EE; --c-ink: #12141D; --c-sea: #1B3FAA;
  --c-accent: #1F6FEA;
  --c-green: #128F36; --c-green-deep: #0E6F2A; --c-green-deeper: #0A5A22;
  --c-rust: #B5482A; --c-steel: #8C9196; --c-navy: #1E2A52;
  /* Muted TEXT ink, for muted text ON PAPER. --c-steel is a 2.88:1 grey on
     paper — it never cleared AA for body copy, and Version B leans on
     secondary text far harder than Version A did (hints, eyebrows, notes,
     captions), so the failure would have gone from one stat caption to most
     of the page. This is the same ink the body is set in, thinned against the
     same paper, at 5.5:1. --c-steel keeps the non-text work it was doing.
     --c-ink-mute-dark is the same idea mirrored for ink surfaces: paper
     thinned toward ink instead of ink thinned toward paper, at ~7.4:1 against
     --c-ink — clears 4.5:1 with room because dark surfaces have far more
     range to spend before the "muted" read disappears. .section--dark
     reassigns --c-ink-mute to this value for everything under it; a surface
     that goes back to paper WITHIN a dark section (the waybill card in the
     footer) reassigns --c-ink-mute back to the paper-surface formula itself. */
  --c-ink-mute: color-mix(in srgb, var(--c-ink) 65%, var(--c-paper));
  --c-ink-mute-dark: color-mix(in srgb, var(--c-paper) 65%, var(--c-ink));
  --font-display: 'Anton', Impact, 'Arial Narrow', sans-serif;
  --font-body: 'Archivo', Helvetica, Arial, sans-serif;
}

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

/* Green owns the UI accents now, so the brand BLUE's job in CSS is the pair of
   states nothing else should claim: the keyboard focus ring and the selection
   highlight. Neither carries body text, and both need to be unmistakably not
   green so they read as "the browser is talking to you", not as branding. */
:focus-visible { outline: 3px solid var(--c-accent); outline-offset: 3px; }

::selection { background: var(--c-accent); color: var(--c-paper); }

html { scroll-behavior: smooth; }

@media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } }

body { font-family: var(--font-body); color: var(--c-ink); background: var(--c-paper); line-height: 1.5; overflow-x: hidden; }

h1, h2, h3 { font-family: var(--font-display); font-weight: 400; text-transform: uppercase; letter-spacing: 0.02em; line-height: 1.05; }

h1 { font-size: clamp(2.4rem, 7vw, 6rem); }

h2 { font-size: clamp(1.8rem, 5vw, 4rem); }

/* One green rule under every section heading — the single repeated primitive
   that carries the brand colour through the page, rather than a different
   flourish per section. Decorative, so the 4.38:1 it lands at on ink is a
   non-issue; it never carries text. It follows the heading's own alignment
   because the sections do not agree on one. */
.wrap > h2::after,
.ocean-copy h2::after {
  content: "";
  display: block;
  width: 48px;
  height: 4px;
  border-radius: 2px;
  margin-top: clamp(1rem, 2vh, 1.5rem);
  background: var(--c-green);
}

/* #delivered is a centred section; the footer used to be one too, but Version
   B's footer is a four-office grid and reads left. Its rule follows. */
#delivered h2::after { margin-inline: auto; }

.ocean-copy h2::after { margin-left: auto; }

.section--dark { background: var(--c-ink); color: var(--c-paper); --c-ink-mute: var(--c-ink-mute-dark); }

.wrap { max-width: 1200px; margin: 0 auto; padding: 0 clamp(1rem, 4vw, 3rem); }

/* ---------- Nav ---------- */

.nav {
  position: sticky;
  top: 0;
  z-index: 50;
  background: var(--c-paper);
  border-bottom: 1px solid rgba(17, 17, 17, 0.1);
}

.nav > .wrap {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1.5rem;
  padding-top: 1.25rem;
  padding-bottom: 1.25rem;
}

/* The lockup ships at its native 210px; held at 150 it stays crisp on a 1x
   screen and is still only mildly soft at 2x, which is the trade the 210px
   source forces. width/height attributes carry the intrinsic ratio for CLS. */
.nav__logo img { display: block; width: 150px; height: auto; }

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

.nav a {
  color: var(--c-ink);
  text-decoration: none;
  font-weight: 700;
  font-size: 0.9rem;
  text-transform: uppercase;
  letter-spacing: 0.03em;
}

/* Brand green, but the DEEP one: this pill's label is 0.9rem bold (14.4px), well
   under the 18.66px that would let it count as large text, so --c-green's 3.75:1
   would not clear AA here. --c-green-deep is the same hue at 5.65:1. The big
   .cta can and does use the brand value, because its label is sized for it. */
a.nav__cta {
  background: var(--c-green-deep);
  color: var(--c-paper);
  padding: 0.6rem 1.25rem;
  border-radius: 999px;
}

a.nav__cta:hover { background: var(--c-green-deeper); }

/* ---------- Scenes / shared scene primitives ---------- */

.scene { position: relative; min-height: 100vh; display: grid; align-content: center; }

/* Stages are plain positioning boxes: every scene layer is an absolutely placed
   <img> sized in % of the stage, so the whole composition scales as one and the
   old SVG viewBox/wrapper-scale bookkeeping is gone. Class name kept for
   continuity with the scene ids/selectors. */
.svg-stage { position: relative; width: 100%; min-height: 0; overflow: hidden; }

/* height:auto so the width/height attributes (there for CLS) act as an aspect
   ratio rather than a fixed rendered height */
.svg-stage img { display: block; height: auto; }

/* every scene layer */
.svg-stage > img,
.svg-stage > .ly,
.crane-rig > img,
.crane-rig > .cable,
.rig > img { position: absolute; }

.stack-group img { width: 100%; height: auto; }

/* Reach stacker. The box carries the render's own aspect ratio, so the cable and
   spreader can be anchored at the boom tip in the image's fractional coordinates
   (tip = 95.06% across, 25.42% down — measured off the source render). */
.crane-rig {
  position: absolute;
  aspect-ratio: 1152 / 838;
  z-index: 4;
}

.crane-rig > .rig-crane { inset: 0; width: 100%; height: 100%; }

.cable {
  left: 95.06%;
  top: 25.42%;
  width: 3px;
  height: 0.9%;
  margin-left: -1.5px;
  background: #16181c;
  transform-origin: top center;
}

.rig-spreader {
  left: 95.06%;
  top: 26.2%;
  width: 39%;          /* the spreader's own share of the crane render's width */
  margin-left: -19.5%;
  z-index: 1;
}

.crane-rig--flip > .rig-crane { transform: scaleX(-1); }

.crane-rig--flip > .cable,
.crane-rig--flip > .rig-spreader { left: 4.94%; }

/* Truck + seated container, moving as one rig. The container is placed in the
   truck render's own fractional coordinates: flatbed surface at 28.3% up from
   the bottom, bed spanning 34%–99% of the truck's width. */
.rig { position: absolute; aspect-ratio: 1400 / 452; z-index: 2; }

.rig > .rig-truck { inset: 0; width: 100%; height: 100%; }

.rig > .rig-container { left: 35.5%; bottom: 28.3%; width: 62%; }

/* Container resting flush on a stack: normal flow does the seating, so it stays
   flush at any size. */
.stack-group { position: absolute; z-index: 2; }

/* yard surface */
.ground {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  background: linear-gradient(to bottom, rgba(140, 145, 150, 0.18), rgba(140, 145, 150, 0));
  border-top: 2px solid rgba(17, 17, 17, 0.4);
}

/* Yard backdrop. At 0.45 this read as a washed-out smudge at the edge of the
   frame rather than as containers; #loading keeps its own low value because
   there it is a far-distance wall behind the whole scene. */
.ly--farstack { opacity: 1; }


.ghost-word {
  position: absolute;
  inset: 0;
  display: grid;
  place-content: center;
  font-family: var(--font-display);
  font-size: 22vw;
  text-transform: uppercase;
  opacity: 0.06;
  pointer-events: none;
  white-space: nowrap;
}

/* The wake the truck leaves the word in. Same flat ink the section is painted
   in, twice the section's width so that its right edge can be dragged from
   beyond the right edge of the frame to beyond the left one and still cover
   everything the truck has not passed yet. No z-index: it comes after
   .ghost-word and before the stage and the card track in source order, which
   is exactly the layer it needs to be in.
   The parked transform IS the no-JS and reduced-motion state: shifted a full
   frame and a bit off to the left, covering nothing, so a page with no scrub
   shows the whole word the way it always did. The scene overwrites x in px. */
.ghost-mask {
  position: absolute;
  top: 0;
  bottom: 0;
  right: 0;
  width: 200%;
  background: var(--c-ink);
  pointer-events: none;
  transform: translateX(-110%);
}

/* ---------- Hero: the idea ---------- */

/* The page's opening beat, and the only scene on the page whose subject is
   people rather than plant. Same layout contract as every other scene — CSS
   parks everything at the SETTLED pose, so the no-JS and reduced-motion pages
   render a finished composition (three people at a table, a drawing lying on
   it) and the JS only has to move away from it.
   Editorial split rather than the yard's full-bleed band: type owns the left of
   the frame, the meeting sits in the bottom-right corner of it. A centred
   composition under a display headline needs more than one viewport at 900px
   tall, and the first thing a visitor sees must not be a headline with its
   picture cut off. */
#hero-idea {
  position: relative;
  display: block;
  align-content: normal;      /* same .scene grid reset #hero-yard needs */
  /* NOT 100vh while the section sits in normal flow: the sticky nav is in flow
     too, so a 100vh first section ends one nav-height below the fold and the
     table gets its legs cut off. The scrubbed version at the foot of this block
     pins, and a pinned section is flush to the viewport top by definition, so
     it takes the full 100vh back and pays for the nav in padding instead. */
  min-height: calc(100vh - 6rem);
  padding-top: clamp(2rem, 6vh, 4rem);
  overflow: hidden;           /* the sheet leaves through the section's edge */
}

#hero-idea .wrap { position: relative; z-index: 2; padding-top: clamp(1rem, 6vh, 4rem); }

#hero-idea .eyebrow { margin-bottom: 1rem; }

/* Held to the left of the frame so the meeting has a corner to sit in. 15ch is
   the width at which "SOURCING SHOULD NOT BE A" breaks where the sentence
   does, rather than mid-clause. */
#hero-idea h1 { max-width: 15ch; }

#hero-idea .lede {
  font-size: clamp(1.1rem, 2.2vw, 1.5rem);
  max-width: 34ch;
  margin-top: 1.5rem;
}

#hero-idea .svg-stage {
  position: absolute;
  right: clamp(-3rem, -2vw, 0rem);
  bottom: clamp(1rem, 4vh, 3.5rem);
  width: min(56%, 720px);
  /* The stage is not the sheet's clip any more — the travelling sheet is
     fixed-position and outside every section — but the stage box is smaller
     than the render's own shadow footprint, so this stays. */
  overflow: visible;
}

/* The composition box carries the render's own aspect ratio, so the blueprint
   can be placed — and posed — in the image's own fractional coordinates. */
.meeting { position: relative; aspect-ratio: 1180 / 800; }

.meeting__img { position: relative; z-index: 1; width: 100%; height: auto; }

/* ---------- The travelling blueprint ---------- */

/* One asset, one class, four scenes. Every pose is a transform on the SAME
   rectangle: the sheet is squared up in the asset itself, so "flat on a table"
   and "propped against a desk" are rotateX and rotateY rather than four
   separately rendered sheets that would each have to be trusted to look like
   the same object. Width is always a % of the scene box the sheet belongs to,
   so it scales with its scene and needs no intrinsic-size maths. */
.bp { position: absolute; height: auto; z-index: 2; }

/* ---- The one sheet that actually travels ---- */

/* Nothing at all unless the page is both scripted and allowed to animate:
   the four .bp poses below ARE the static compositions, and this element
   would be a second drawing lying on top of them. */
#bp-fly { display: none; }

@media (prefers-reduced-motion: no-preference) {
  /* Fixed, and outside every section, because the path it flies crosses
     section boundaries and two pins: in viewport coordinates the sheet simply
     does not care which section is under it, which is the whole reason it can
     be on screen in the unpinned gaps where the old per-scene sheets were not.
     z-index sits under the sticky header (50) — the drawing passes BEHIND the
     nav, the way a real sheet would pass behind something nearer the reader —
     and over every scene layer, which all live in their own sections.
     overflow: hidden makes the element's own bottom edge a clip line; the
     scene lays that edge along the folder's mouth for the insertion, and for
     the rest of the journey the sheet is parked clear of it and nothing is
     cut. transform-origin is that edge's midpoint, so the rotation the path
     carries happens about the point that has to land on the mouth.
     Hidden until the scene has rendered its first frame: an untransformed
     fixed element is a drawing in the top-left corner of the page, and that
     is what the first paint would otherwise be. */
  html.js #bp-fly {
    display: block;
    position: fixed;
    left: 0;
    top: 0;
    z-index: 40;
    overflow: hidden;
    visibility: hidden;
    pointer-events: none;
    transform-origin: 50% 100%;
    will-change: transform;
  }

  html.js #bp-fly img { position: absolute; height: auto; }

  /* The per-scene sheets stay in the layout — the flying one is AIMED at them
     (their live offset boxes are where the table pose, the desk pose and the
     folder's mouth actually are, so the stylesheet stays the only place those
     placements are stated) — but only one drawing may be visible at a time.
     visibility, not display: a display: none element has no box to measure. */
  html.js #bp-meeting,
  html.js #bp-wall,
  html.js #bp-desk,
  html.js #bp-deal { visibility: hidden; }

  /* The scene sets this on itself if it cannot build the path (a layer it
     aims at has been renamed or removed). The rule above hides the per-scene
     sheets on the assumption that something else is flying one; this is what
     happens when that assumption turns out to be false — the page falls back
     to the settled compositions rather than to no drawing at all. */
  html.js.no-bp-path #bp-fly { display: none; }

  html.js.no-bp-path #bp-meeting,
  html.js.no-bp-path #bp-wall,
  html.js.no-bp-path #bp-desk,
  html.js.no-bp-path #bp-deal { visibility: visible; }
}

/* Pose 1 — lying on the meeting table, on the erased sheet's own footprint:
   that footprint spans 29.9%–65.8% across the composition and 50%–62.9% down
   it, and its centre is therefore at 47.9%, 56.5%. rotateX foreshortens the
   sheet the way the render's own camera does; a plain scaleY squash would leave
   the far edge as wide as the near one, which is the tell that reads as "flat
   image pasted on top of a photo".
   Two consequences of doing it with a real perspective, both paid for here.
   The box is parked so its CENTRE lands on the footprint's centre, because
   rotateX about 50% 50% shrinks the element around that point and not about its
   top edge. And the near edge comes out ~9% WIDER than the element box (that is
   what perspective is), so the box is set from the footprint's near edge
   divided by that expansion, not from the footprint width — sized the naive way
   the sheet's near corner hung over the table's front edge.
   The far edge finishes just under the standing figure's pointing finger, which
   is why the erase behind it was colour-selective rather than a box fill: he
   has to still be pointing AT something. */
.bp--table {
  left: 31.6%;
  top: 41.8%;
  width: 32.6%;
  transform: perspective(760px) rotate(-1deg) rotateX(64deg);
}

/* No corner to sit in at 390px: the type takes the top of the frame and the
   meeting takes the bottom of it, full width, still one screen. */
@media (max-width: 767px) {
  #hero-idea { min-height: calc(100vh - 5rem); }   /* shorter nav down here */

  #hero-idea h1, #hero-idea .lede { max-width: none; }

  #hero-idea .svg-stage { right: 0; left: 0; bottom: clamp(1rem, 3vh, 2rem); width: auto; }
}

/* Scrubbed composition: the hero pins now, so it owns a whole viewport and has
   to clear the sticky nav on its own account. Gated exactly like the scene's
   matchMedia so the two can never disagree about the layout they are driving. */
@media (prefers-reduced-motion: no-preference) {
  html.js #hero-idea {
    min-height: 100vh;
    padding-top: clamp(6rem, 13vh, 9rem);
  }
}

/* ---------- Hero: the yard ---------- */

#hero-yard {
  display: block;
  align-content: normal; /* .scene sets align-content: center for its grid layout;
    modern browsers also apply align-content to block containers, so it must be
    reset here now that #hero-yard is display:block, or .wrap/.svg-stage get centered. */
  min-height: 100vh;
  /* Clears the sticky nav (~82px tall): once pinned, #hero-yard is fixed flush to the
     viewport top, so the nav (z-index: 50) would otherwise cover the headline. */
  padding-top: clamp(6rem, 14vh, 8rem);
  overflow: hidden;
}

/* above .crane-rig (z-index: 4): at scroll 0 the boom sweeps across the headline
   block, and anything lower let the crane paint over the .lede. */
#hero-yard .wrap { position: relative; z-index: 5; }

/* The z-index above puts the headline in front of the boom; the paper halo is
   what makes it *readable* there — ink glyphs sitting on the near-black boom are
   on top but have almost no contrast without it. Invisible over the paper bg.
   In Version A this was the lede's problem; here the section leads with the
   positioning h2 that moved down from #credibility, so the halo moves with the
   text that now sits in the boom's path. */
#hero-yard .wrap h2 {
  max-width: 20ch;
  text-shadow:
    0 0 3px var(--c-paper), 0 0 3px var(--c-paper), 0 0 7px var(--c-paper),
    0 0 14px var(--c-paper), 0 0 22px var(--c-paper), 0 0 30px var(--c-paper);
}

#hero-yard .svg-stage {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 66vh;
}

#hero-yard .ground { height: 5%; }

/* The rig's parked position is the pose at the END of the approach: spreader
   centred over the stack (boom tip = 95.06% across the crane render, so
   17.1% + 0.9506 x 52% = 66.5%, which is the container's centre below). The
   whole yard is shifted right of where it used to sit purely to buy the crane
   room to come from — it now enters from off the left edge. */
#hero-yard .crane-rig { left: 17.1%; bottom: 5%; width: 52%; }

#hero-yard .stack-group { left: 57%; bottom: 5%; width: 19%; }

#hero-yard .ly--farstack { right: 2%; bottom: 5%; width: 17%; }

/* ---------- Credibility / stats ---------- */

#credibility { padding: 6rem 0; }

.stats {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 2.5rem;
  margin-top: 3rem;
}

.stats > div { display: flex; flex-direction: column; gap: 0.5rem; }

/* Green at display size: 3.75:1 on paper is above the 3:1 large-text floor with
   room to spare at 48-96px. */
.stats dt.stat {
  font-family: var(--font-display);
  font-size: clamp(3rem, 8vw, 6rem);
  line-height: 1;
  color: var(--c-green);
}

.stats dd {
  font-size: 0.85rem;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  color: var(--c-ink-mute);
}

/* ---------- Development act: sourcing, hand-over, the line ---------- */

/* Three full scenes where a horizontal strip of four station renders used to
   pan. Every one of them is laid out the same way as the rest of the page's
   scenes — a stage that is a plain positioning box, layers sized in % of it,
   and a settled pose in CSS that the no-JS and reduced-motion pages render on
   their own. What is new is that one layer, .bp, appears in all three and in
   the hero: it is the same file at four poses, and the poses are the only
   thing that changes. */

/* Annotation on the scene it names, not a caption floating over a slide. Same
   type and the same green rule the station labels carried, and — like them —
   it is positioned INSIDE the moving composition, so it travels with the thing
   it is naming instead of sitting still while the scene slides out from under
   it. */
.scene-label {
  position: absolute;
  z-index: 4;
  max-width: 15ch;
  font-size: clamp(0.6rem, 0.8vw, 0.72rem);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.09em;
  line-height: 1.25;
  padding-bottom: 0.45rem;
  border-bottom: 2px solid var(--c-green);
}

/* ---- 1b. The development studio ---- */

#dev-studio {
  display: block;
  align-content: normal;
  min-height: auto;
  padding: clamp(4rem, 10vh, 7rem) 0;
  overflow: hidden;
}

/* Full bleed by WIDTH, and the box carries the render's own 3:2 with the image
   filling it exactly — so the box IS the painted frame, and the sheet pinned on
   the sketch wall can be placed (and measured) in the picture's own fractional
   coordinates with no object-fit to resolve. What the section's overflow takes
   at a window shorter than 3:2 is empty ceiling and empty floor, which is the
   crop this composition can afford; a height-fitted stage would crop sideways
   instead and the sketch wall is the first thing at that edge. */
.studio {
  position: relative;
  width: min(100%, 1536px);
  margin: 0 auto;
  aspect-ratio: 1536 / 1024;
}

/* Same dissolve, and for the same reason, as the warehouse's: on a window TALLER
   than 3:2 the picture no longer fills the section and its top and bottom edges
   meet paper. The render's own ceiling (244,238,234) and floor (246,240,237) are
   within a couple of levels of --c-paper, so there is no colour to mismatch —
   only a straight line to dissolve. On the image, not the box: the box's other
   children are the pinned sheet and the label. */
#dev-studio .studio__img {
  width: 100%;
  height: 100%;
  /* All four edges, not just the horizontal pair. Full bleed the left and right
     ones are off-screen and only the top and bottom can ever meet paper — but
     the width cap above (see the pinned block further down) narrows the picture
     on a short, wide window, and then both vertical edges meet paper too. Two
     gradients intersected, which is the only way to fade a box on every side
     with one mask. */
  -webkit-mask-image:
    linear-gradient(to bottom, transparent 0, #000 4%, #000 96%, transparent 100%),
    linear-gradient(to right, transparent 0, #000 1.5%, #000 98.5%, transparent 100%);
  mask-image:
    linear-gradient(to bottom, transparent 0, #000 4%, #000 96%, transparent 100%),
    linear-gradient(to right, transparent 0, #000 1.5%, #000 98.5%, transparent 100%);
  -webkit-mask-composite: source-in;
  mask-composite: intersect;
}

/* Pose 1b — pinned up on the sketch wall, over the material swatches at the
   board's right end (they run 427..550 across the render's own 1536 and
   265..365 down its 1024, and this sheet's 388..572 x 259..371 covers them with
   its own corners on board either side). Over THOSE and not over the headphone
   sketches: the sketches are what the room is a picture of, and a new drawing
   pinned over the reference swatches is what a studio wall actually accretes.
   Upright, because a wall is: no rotateX at all, a whisker of yaw for depth and
   a degree of in-plane tilt, which is the difference between pinned by a hand
   and positioned by a stylesheet. Written in GSAP's own composition order
   (perspective, rotate, rotateX, rotateY) because the travelling sheet arrives
   at this pose as numbers — see BP_WALL in main.js. */
.bp--wall {
  left: 25.3%;
  top: 25.3%;
  width: 12%;
  transform: perspective(1200px) rotate(-1.2deg) rotateY(3deg);
}

/* The studio's own empty upper wall — everything above the sketch board (the
   top 22% of the render, at every column) — which is the one band of this
   composition that is not a picture of something. devStudioScene() clamps it
   down that band if a short, wide window would otherwise put it under the
   sticky header. */
.scene-label--studio {
  left: 5%;
  top: 9%;
  /* One line, not the default three. This band is 22% of the picture's height
     and the clamp above can spend a good part of that pushing the label clear
     of the sticky header on a short, wide window — at 1900x1000 it lands 111px
     down, and a three-line label from there runs its green rule across the top
     corner of the sketch board itself. Wide enough for the whole name on one
     line, it cannot: the rule finishes clear of the board at every window this
     layout can produce. 40ch and not 34: at 1900 the name is 217px of type and
     34ch resolves to 197, which wrapped it back to two. */
  max-width: 40ch;
}

/* ---- 2. Sourcing evaluation ---- */

#sourcing-eval {
  display: block;
  align-content: normal;   /* same reset #hero-yard needs, for the same reason */
  /* .scene's 100vh is for the PINNED version only; in flow the desk is barely
     two thirds of that and a forced viewport leaves a screenful of empty paper
     under it. */
  min-height: auto;
  padding-top: clamp(6rem, 14vh, 8rem);
  padding-bottom: clamp(3rem, 8vh, 6rem);
  overflow: hidden;
}

#sourcing-eval .wrap { position: relative; z-index: 5; }

/* Nothing of this scene's own is allowed to clip: the label sits outside the
   desk box, and the render carries a soft shadow past its edges. The sheet
   that lands here is not clipped by anything — it is fixed-position and
   outside the section entirely. */
#sourcing-eval .svg-stage { overflow: visible; }

.desk {
  position: relative;
  width: min(92%, 1080px);
  margin: clamp(1rem, 4vh, 3rem) auto 0;
  aspect-ratio: 1536 / 1024;
}

.desk__img {
  position: relative;
  z-index: 1;
  width: 100%;
  height: auto;
  /* This render is a full rectangular frame, not a cutout, and its own paper
     (240,233,227) is four levels off the page's --c-paper — enough that its
     four straight edges read as a photograph pasted on the page. So all four
     dissolve: two gradients intersected, which is the only way to fade a box on
     every side with one mask. The composition's own content is comfortably
     inside the fade (the boards start 3% in, the shelf ends 4% short of the
     right edge), so nothing of the picture is lost to it. */
  -webkit-mask-image:
    linear-gradient(to bottom, transparent 0, #000 3.5%, #000 96.5%, transparent 100%),
    linear-gradient(to right, transparent 0, #000 2.5%, #000 97.5%, transparent 100%);
  mask-image:
    linear-gradient(to bottom, transparent 0, #000 3.5%, #000 96.5%, transparent 100%),
    linear-gradient(to right, transparent 0, #000 2.5%, #000 97.5%, transparent 100%);
  -webkit-mask-composite: source-in;
  mask-composite: intersect;
}

/* Pose 2 — LYING ON THE SAMPLE TABLE, set down among the swatches: the table's
   own top face runs from y=672 to y=717 in the render's 1024 rows (45 of them,
   which is what a 2.4m table 800mm deep comes to at this camera), and this
   sheet is parked inside that band with its centre at 1040, 695 — over the dark
   swatch and the loose paper already lying there, clear of the flasks standing
   behind and of the near edge in front.
   rotateX is 76deg here and not the meeting table's 64: the angle is not a
   taste setting, it is the table's own foreshortening, and this camera is much
   shallower than the hero's. Read off the render rather than guessed — a
   tabletop 45px deep against a top face 620px wide, over a real table's own
   800/2400 proportion, is an inclination of 77deg. Perspective and the yaw stay
   the meeting table's: it is the same sheet and the same hand put it down.
   Rotation order is GSAP's (perspective, rotate, rotateX, rotateY), not reading
   order, because the travelling sheet arrives at this pose as numbers — see
   BP_DESK in main.js — and the two spellings have to compose to one matrix. */
.bp--desk {
  left: 59.2%;
  top: 59.3%;
  width: 17%;
  transform: perspective(760px) rotate(1.8deg) rotateX(76deg);
}

/* In the band of empty wall above the scorecard. Both of this render's boards
   start 14.5% down it, and everything above that line is bare wall at every
   column — the same place the studio and the warehouse put their own labels,
   for the same reason. Left of centre would put it over the pipeline board's
   own title. */
.scene-label--desk { left: 58%; top: 4%; }

/* ---- 3. The hand-over ---- */

#handshake {
  display: block;
  align-content: normal;
  min-height: auto;
  padding: clamp(4rem, 10vh, 7rem) 0 clamp(3rem, 8vh, 6rem);
  overflow: hidden;
}

#handshake .svg-stage { overflow: visible; }

.pair {
  position: relative;
  width: min(52%, 420px);
  margin: 0 auto;
  aspect-ratio: 900 / 1069;
}

.pair__img {
  position: relative;
  z-index: 2;
  width: 100%;
  height: auto;
}

/* The folder's mouth, as a clipping box. Measured off handshake.webp: the blue
   folder's top edge runs from (595, 405) to (737, 331) in the render's own
   900x1069 pixels — 160px long, at -27.5deg — so its midpoint is 74% across and
   34.4% down, and that is where this box's BOTTOM EDGE is put, at that angle.
   transform-origin is the box's own bottom centre and the translate takes it to
   that point, so the rotation happens about the mouth's midpoint and the bottom
   edge lies along the mouth exactly.
   Everything below that edge is clipped away, which is the whole mechanic: the
   sheet is a child of this box, so travelling down the box's LOCAL y — which
   after the rotation is the folder's own axis, down and to the right — takes it
   through the mouth edge-first and it disappears into the folder progressively.
   Nothing about the clip is animated; it is static geometry, and the only thing
   that moves is the sheet's own transform.
   Sized 200% x 150% of the pair so the sheet can start well outside the frame
   in local coordinates without meeting the box's other three edges: the only
   edge that is ever supposed to cut anything is the bottom one. */
.bp-slot {
  position: absolute;
  z-index: 3;
  left: 74%;
  top: 34.4%;
  width: 200%;
  height: 150%;
  overflow: hidden;
  transform: translate(-50%, -100%) rotate(-27.5deg);
  transform-origin: 50% 100%;
}

/* Pose 3 — half into the folder. No transform of its own: the slot already
   carries the folder's angle, so square with the folder IS the identity here,
   and the sheet's resting attitude is a pure position.
   Every percentage below is of the SLOT, which is twice the pair's width and
   one and a half times its height, so they are not the fractions they look
   like. 8.6% of the slot is 17.2% of the pair, which is 155px against the
   folder mouth's own 160px in the render's coordinates: sized to the thing it
   is going into, and a hair narrower, so both its corners are inside the mouth.
   Sized to the mouth exactly (9%) it overhung the folder's left corner by ~5px
   and that overhang got cut by the clip line with no folder under it — the one
   way this mechanic can show itself.
   top is the mouth (100%) less half the sheet's height, so its centre sits ON
   the clip line and exactly half of it shows. Half its height as a fraction of
   the slot's: 8.6% x (439/720) of the slot's width, over 150% of the pair's
   height (which is 1069/900 of its width), halved = 2.94%. That half-in pose
   IS the static composition — a drawing caught mid-filing, which is the
   readable freeze-frame of this beat. */
.bp--deal {
  left: 45.7%;
  top: 97.06%;
  width: 8.6%;
}

/* Out at the page's own left margin rather than over the pair: the figures are
   a tall centred cutout with no corner to put type in, and the gutter beside
   them is where the rest of the page starts its lines. */
.scene-label--deal { left: -58%; top: 8%; }

/* ---- 4. The line ---- */

#factory-hall {
  display: block;
  align-content: normal;
  min-height: auto;
  padding: clamp(4rem, 10vh, 7rem) 0;
  overflow: hidden;
}

/* The hall is a backdrop the way the port's harbour is, not a cutout: its interior is
   the scene. It carries the render's own aspect ratio so the drawing pinned
   over the line and both labels can be placed in the image's own fractional
   coordinates and then travel with it — one element moves, and everything that
   belongs to the scene is inside it. */
.hall {
  position: relative;
  width: min(100%, 1680px);
  margin: 0 auto;
  aspect-ratio: 1680 / 840;
}

#factory-hall .hall__img { width: 100%; height: 100%; }

/* Pose 4 — pinned up on the wall at the head of the line. Small on purpose:
   by this scene the drawing is no longer the subject, it is the authority the
   line is working to, and a sheet sized to compete with the machines would say
   the opposite. */
.bp--line {
  left: 12%;
  top: 24%;
  width: 6.5%;
  transform: perspective(1200px) rotate(-1.5deg) rotateY(6deg);
}

/* The label rides the hall's own coordinates, so it arrives with the stretch of
   line it names. Set below the sheet and above the machines: the band of wall
   over the window row is the only part of this render that is empty at every
   point of the pan, and it is where a factory would put its own signage.
   26% is the highest it can sit and still clear the sticky header once the
   scene is pinned and the render is cropped to the frame.
   ONE label, where this scene used to carry two. Every beat in the act names a
   single service now — the drawing's own scenes took the two names that were
   stacked here (the studio takes Product Development & OEM / ODM, the warehouse
   takes Ongoing Supply Chain Support), and what is left on the line is the one
   that is actually a picture of it: this is the supplier's own floor, running
   the product that was sourced from them. */
.scene-label--line { left: 25%; top: 26%; }

/* ---- 5. The warehouse ---- */

#warehouse {
  display: block;
  align-content: normal;
  min-height: auto;
  padding: clamp(4rem, 10vh, 7rem) 0;
  overflow: hidden;
}

/* Full bleed by WIDTH, not by height. The render is 2:1 and its two ends carry
   the whole story of the beat — the racking the pallet came out of at one end,
   the roller door it is going to at the other — so a height-fitted stage, which
   is what would keep the picture off the frame's own top and bottom edges, is
   the one fit this composition cannot take: it crops sideways, and the door is
   the first thing to go. Width-fitted the crop is vertical instead, and what it
   takes is empty ceiling and empty floor.
   The box carries the render's own aspect ratio and the image fills it exactly,
   so the box IS the painted frame and the forklift can be positioned — and
   measured — in the picture's own fractional coordinates, with no object-fit to
   resolve. */
.aisle {
  position: relative;
  width: min(100%, 1774px);
  margin: 0 auto;
  aspect-ratio: 1774 / 887;
}

/* Both horizontal edges of the render dissolve into the page. The hall's
   ceiling (247,241,236) and its floor (244,238,233) are within a few levels of
   --c-paper, so there is no colour to mismatch — but a straight line at three
   levels across a flat field is still a line, and this is the one boundary in
   the scene that is not bled off-screen.
   On the IMAGE and not on the box that holds it: a mask on .aisle would apply
   to its children too, and at 390px the label sits ABOVE the picture, outside
   the box — where a mask would simply erase it. */
#warehouse .aisle__img {
  width: 100%;
  height: 100%;
  -webkit-mask-image: linear-gradient(to bottom, transparent 0, #000 4%, #000 96%, transparent 100%);
  mask-image: linear-gradient(to bottom, transparent 0, #000 4%, #000 96%, transparent 100%);
}

/* The forklift, its pallet and its shadow, as ONE layer at exactly the size and
   place the render drew them (tools/build-warehouse.py prints every number
   here). That is also the static composition: no JS, or reduced motion, and the
   picture is the photograph again, forklift mid-aisle. The drive is a pure
   translate off this rest pose, so the two can never disagree. */
#forklift {
  position: absolute;
  z-index: 3;
  left: 40.1353%;
  top: 57.9481%;
  width: 19.8422%;
  height: auto;
}

/* The empty upper wall — the top 28% of this render is nothing but it, at every
   column — which is where a warehouse hangs its own signage. warehouseScene()
   clamps it down the wall if a short, wide window would otherwise put it under
   the sticky header; it has 28% of the picture's height to be pushed into. */
.scene-label--aisle { left: 5%; top: 9%; }

/* Scrubbed compositions. Each of the three pins and therefore owns a full
   viewport; in flow they are as tall as their own content and no taller. Gated
   exactly like the scenes' matchMedia so the two can never disagree about the
   layout they are driving. */
@media (prefers-reduced-motion: no-preference) {
  html.js #sourcing-eval,
  html.js #handshake,
  html.js #factory-hall { min-height: 100vh; padding-bottom: 0; }

  /* The studio pins to a viewport and its picture is CENTRED in it: the stage is
     width-derived and the section is window-height, so the spare is whatever the
     window's aspect leaves, and splitting it keeps the sketch wall (upper left)
     and the shelf of variants (lower right) equally in frame. Flex rather than a
     translate, so nothing on this box carries a transform that the pinned
     sheet's own measurement would have to back out. */
  html.js #dev-studio { min-height: 100vh; padding: 0; }

  html.js #dev-studio .svg-stage {
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
  }

  /* flex: none, or the width the mobile override asks for (230%, so the phone
     shows the sketch-wall panel rather than the whole room) would be shrunk
     straight back to the frame by the flex line's default flex-shrink. */
  /* Full bleed, but never wider than the window can afford. The picture is
     width-derived and centred in a window-height section, so the taller it gets
     the more of it the section crops off both ends — and the first thing off
     the top is the band of empty wall the label lives in. Past about 2.3:1
     (2560x1100 is one, and every ultrawide is) that band is gone entirely and
     the clamp in devStudioScene() has nowhere to put the label but on the
     sketch board itself.
     So the width is capped at the point where the board's own top edge (22.85%
     down the render) still clears the nav plus a label: with the picture
     centred, that edge sits at (100vh - w/1.5)/2 + 0.2285 * w/1.5, and holding
     it at or below 149px gives w <= (50vh - 149px) * 5.525. At 1440x900 that is
     1663 and at 1900x1000 it is 1939 — both wider than the window, so the cap
     does nothing at any ordinary shape; it only ever bites on the ones that
     would otherwise put type over the artwork. */
  html.js .studio {
    width: 100%;
    max-width: calc((50vh - 149px) * 5.525);
    /* auto, not 0: on the shapes where the cap above actually bites, the box is
       narrower than the frame, and a flex line's default packing would park it
       against the left edge with all the spare paper in one strip down the
       right. */
    margin: 0 auto;
    flex: none;
  }

  html.js #sourcing-eval .svg-stage,
  html.js #handshake .svg-stage {
    position: absolute;
    left: 0;
    right: 0;
    bottom: clamp(1rem, 4vh, 3rem);
  }

  /* Sized by HEIGHT once it is pinned: the frame is a viewport, not a column,
     and what has to fit is the composition's height. */
  html.js .desk {
    width: auto;
    height: 66vh;
    aspect-ratio: 1536 / 1024;
    margin: 0 auto;
  }

  html.js .desk__img { width: 100%; height: 100%; }

  html.js .pair { width: auto; height: 66vh; margin: 0 auto; }

  html.js .pair__img { width: 100%; height: 100%; }

  /* Full bleed, and wider than the frame on purpose: the overflow is the pan.
     The vertical crop takes a slice off BOTH ends — dead ceiling off the top,
     dead floor off the bottom — rather than sitting the render on the frame's
     bottom edge. Bottom-aligned (which is what this was) the crop came entirely
     off the top and took the pinned-up drawing and both labels with it: they
     were placed in the render's own coordinates, rendered correctly in the
     static composition, and were simply above the frame once it was pinned.
     The -8% is what puts the render's ceiling band back on screen, clear of the
     sticky header, and it costs only floor nobody was reading. */
  html.js #factory-hall { padding: 0; }

  html.js #factory-hall .svg-stage { position: absolute; inset: 0; }

  html.js .hall {
    position: absolute;
    left: 0;
    bottom: -8%;
    width: auto;
    height: 118%;
    margin: 0;
  }

  /* The warehouse pins to a viewport like the rest of the act, and the picture
     is CENTRED in it rather than anchored: the stage is width-derived and the
     section is window-height, so the spare is whatever the window's own aspect
     leaves, and splitting it puts equal paper above and below a picture that is
     symmetrical about its own floor line. Flex rather than a translate, so
     nothing on this box carries a transform the forklift's own would compose
     with. */
  html.js #warehouse { min-height: 100vh; padding: 0; }

  html.js #warehouse .svg-stage {
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
  }

  html.js .aisle { width: 100%; margin: 0; flex: none; }
}

/* ---------- Gap copy ----------
   Shared mechanics for the short lines that ride in the gap a lifted or
   hovering container opens against whatever it just left or is about to
   land on (the yard's lift, the loading truck's hand-off, the port's strike-
   down). Position only: left/top here are a resting spot for the no-JS and
   reduced-motion pages, where there is no lift in progress and the container
   is wherever CSS alone seats it — "sensible", not exact, because there is no
   gap to be exact ABOUT without a scrub driving it.
   In the animated page, positionYardGap()/positionLoadingGaps()/
   positionPortGaps() in main.js zero left/top out to 0 once (a one-time
   reset, not part of the scrub) and drive the actual position every frame
   after that with xPercent/yPercent (set once, for centring) plus a live x/y
   translate — transform and opacity only from there on, same as every other
   scrubbed layer in these scenes. */
.gap-copy {
  position: absolute;
  z-index: 5;
  max-width: 22ch;
  text-align: center;
  pointer-events: none;
}

/* Yard: the tagline rides the lift itself, so its resting spot is open sky
   above the stack — there is nothing to hover it OVER until the crane moves
   it there. Centred on the stack-group's own horizontal centre (57% + 19%/2),
   high enough to clear the parked spreader sitting right over the container
   in this, the settled, no-lift-yet composition. */
#yard-gap-copy {
  left: 66.5%;
  top: 28vh;
  transform: translate(-50%, -50%);
}

/* ---------- Loading beat ---------- */

/* Sized to sit INSIDE a tracked gap rather than span the open sky the way
   this line used to (it was the whole beat's only line, banner-sized,
   before the gaps below carved this section into two smaller ones). 30ch at
   this size renders narrower than #loading-container's own ~375px box at
   1440, so a centred line never overhangs into the cab beside it, and three
   lines of it comes out well under the ~80-160px the tighter of the two
   tracked gaps actually opens. Same halo the yard's headline carries, for
   the same reason — the boom and the load sweep through this band, and ink
   glyphs on a near-black boom have almost no contrast without it. Invisible
   over paper. */
.loading-line {
  max-width: 30ch;
  font-family: var(--font-display);
  font-size: clamp(0.85rem, 1.6vw, 1.15rem);
  line-height: 1.15;
  text-transform: uppercase;
  margin-top: 0;
  text-shadow:
    0 0 3px var(--c-paper), 0 0 3px var(--c-paper), 0 0 7px var(--c-paper),
    0 0 14px var(--c-paper), 0 0 22px var(--c-paper), 0 0 30px var(--c-paper);
}

/* Resting spots for the no-JS / reduced-motion page, where the container is
   always already seated (there is no separate hover pose in CSS the way the
   port has one) — both lines simply take the band of open sky across the top
   the original single line used to own, stacked so neither is hidden. */
#loading-gap-below {
  left: clamp(1rem, 4vw, 3rem);
  top: clamp(5.5rem, 13vh, 8rem);
}

#loading-gap-above {
  left: clamp(1rem, 4vw, 3rem);
  top: calc(clamp(5.5rem, 13vh, 8rem) + 7rem);
}

/* Sized so the crane's own spreader (39% of its width) comes out the same width
   as the container the truck bed wants — one consistent scale across both rigs. */
#loading .svg-stage { aspect-ratio: 1400 / 780; }

#loading .ground { height: 6%; }

#loading .crane-rig { left: 32%; bottom: 6%; width: 68%; }

#loading .rig { left: 7.5%; bottom: 6%; width: 42%; }

/* stack.webp is a flat wall of container faces with no end caps, so butting it
   against x=0 read as a hard crop at the stage edge. Nudged inboard and
   feathered on both sides so it dissolves into the yard haze instead. */
#loading .ly--farstack {
  left: 1.5%;
  bottom: 6%;
  width: 10%;
  opacity: 0.32;
  -webkit-mask-image: linear-gradient(to right, transparent 0, #000 48%, #000 82%, transparent 100%);
  mask-image: linear-gradient(to right, transparent 0, #000 48%, #000 82%, transparent 100%);
}

/* ---------- Services ---------- */

/* .scene's implicit grid column defaults to `auto` (max-content) sizing; without a
   constraint the wide (width:max-content) desktop .service-track blows that column
   out and stretches the sibling .svg-stage to match, wrecking the truck SVG's scale. */
#services { overflow: hidden; grid-template-columns: minmax(0, 1fr); }

#services .svg-stage { aspect-ratio: 1400 / 380; }

#services .rig { left: 20%; bottom: 13%; width: 46%; }

/* The tractor render is near-black and #services is ink: with the cutout's light
   fringe now decontaminated away there is nothing left to separate the two, so
   the silhouette is given a static rim light instead. Static (not animated) so
   it never enters the scrubbed transform's compositing cost. */
#services .rig > img { filter: drop-shadow(0 0 14px rgba(244, 242, 238, 0.28)); }

/* …and the drive lane gets a soft wash so the wheels/chassis have something
   other than pure ink underneath them. */
#services .svg-stage::before {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 64%;
  background: linear-gradient(to top,
    rgba(244, 242, 238, 0.11), rgba(244, 242, 238, 0.035) 55%, rgba(244, 242, 238, 0));
  pointer-events: none;
}

.road {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 13%;
  background: rgba(244, 242, 238, 0.06);
  border-top: 3px solid rgba(244, 242, 238, 0.9);
}

.road::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  top: 45%;
  height: 4px;
  background-image: repeating-linear-gradient(to right,
    rgba(244, 242, 238, 0.35) 0 46px, transparent 46px 80px);
}

.service-track {
  position: relative;
  z-index: 1;
  display: grid;
  /* Seven cards, not six or three — a fixed 3-column track leaves the last
     card alone on a row of its own. auto-fit/minmax fills each row with
     whatever fits and wraps the rest, so the no-JS/reduced-motion fallback
     reads as a roster at any width instead of a lopsided grid. */
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 16rem), 1fr));
  gap: 1.5rem;
  max-width: 1200px;
  margin: 0 auto;
  padding: 2rem clamp(1rem, 4vw, 3rem);
}

.service-card {
  background: rgba(244, 242, 238, 0.05);
  border: 1px solid rgba(244, 242, 238, 0.15);
  padding: 1.75rem;
}

.service-card h3 { font-size: 1.2rem; margin-bottom: 0.75rem; }

.service-card p { font-size: 0.95rem; opacity: 0.85; }

/* Desktop pinned drive-by: JS converts the grid into a wide flex row that
   truckScene() scrolls horizontally via transform. Gated under html.js so
   the no-JS page keeps the Task 2 grid (content fully visible without JS).
   Also gated on prefers-reduced-motion: no-preference — under reduced motion
   main.js's matchMedia bails out of initScenes() before truckScene() ever
   runs, so nothing would translate this flex row and the last two cards
   would sit permanently clipped behind #services's overflow:hidden. Keeping
   the Task 2 grid under reduced motion instead makes all seven cards visible
   in normal document flow, matching the no-JS fallback. */
@media (min-width: 768px) and (prefers-reduced-motion: no-preference) {
  html.js .service-track {
    display: flex;
    gap: 2rem;
    width: max-content;
    max-width: none;
    padding: 2rem clamp(2rem, 8vw, 6rem);
  }

  html.js .service-card {
    width: min(360px, 60vw);
    flex: 0 0 auto;
  }
}

/* ---------- Process ---------- */

#process { position: relative; padding: 6rem 0 8rem; }

#process .wrap { position: relative; z-index: 1; background: var(--c-paper); }

.process-road {
  position: absolute;
  left: 50%;
  top: 0;
  bottom: 0;
  width: 64px;
  transform: translateX(-50%);
  background: var(--c-ink);
}

#process-truck {
  position: absolute;
  left: 50%;
  top: 18%;
  z-index: 2;
  width: 56px;
  height: auto;
  transform: translateX(-50%);
  /* the cab is near-black and the road is ink: a faint paper rim keeps its
     silhouette readable */
  filter: drop-shadow(0 0 2px rgba(244, 242, 238, 0.5));
}

.process-road::after {
  content: "";
  position: absolute;
  left: 50%;
  top: 0;
  bottom: 0;
  width: 4px;
  transform: translateX(-50%);
  background-image: repeating-linear-gradient(to bottom, var(--c-paper) 0 16px, transparent 16px 32px);
}

.process-steps {
  list-style: none;
  position: relative;
  z-index: 1;
  display: grid;
  grid-template-columns: 1fr 64px 1fr;
  column-gap: 3rem;
  row-gap: 4rem;
  max-width: 1200px;
  margin: 0 auto;
  padding: 4rem clamp(1rem, 4vw, 3rem);
}

.process-step:nth-child(odd) { grid-column: 1; text-align: right; }

.process-step:nth-child(even) { grid-column: 3; text-align: left; }

.process-step h3 {
  display: flex;
  align-items: baseline;
  gap: 0.75rem;
  font-size: clamp(1.2rem, 2vw, 1.6rem);
}

.process-step:nth-child(odd) h3 { justify-content: flex-end; }

/* Anton at 1.05rem is 16.8px at weight 400 — nowhere near large text — so these
   step numbers take --c-green-deep (5.65:1) rather than the brand value. */
.process-step h3 span { font-family: var(--font-display); color: var(--c-green-deep); font-size: 1.05rem; }

.process-step p { margin-top: 0.75rem; font-size: 0.95rem; color: var(--c-ink-mute); }

/* ---------- Port handoff ---------- */

/* The stage below is sized by WIDTH and allowed to run taller than the window,
   so the section is pinned to exactly one screen and takes the clip: without
   the height the section would grow to the render's own aspect instead and the
   bottom of the composition would sit under the fold for the whole pin. .scene
   centres its grid row where the row fits and starts it where it does not, so
   on a window wider than 3:2 the picture is flush with the section's top and
   what the clip takes is open water off the foot — which is the right end to
   lose, and is why the horizon, the quay, the deck and the drop all sit in the
   upper two thirds of the render. */
#port {
  height: 100vh;
  overflow: hidden;
  /* .scene's own grid (align-content: center) is for sections that just want
     their one child centred; this one needs an explicit ANCHOR (see below),
     so it opts out of the grid rather than fight it. */
  display: block;
}

/* Full-bleed, no cap. assets/img/port-front.webp is now the 2:1 OUTPAINT of
   the original 3:2 render — same ship, same machine, same hold in the middle,
   quay/pilings/water/sky extended seamlessly both sides (tools/build-port-
   harbor.py, re-run against the new source) — and a 2:1 stage is FLATTER than
   almost every real window (16:10 is 1.6, 16:9 1.78), not taller, which is
   what the old 3:2 stage (1.5, reliably taller than the window) needed a
   155vh width cap to survive: run 3:2 full-bleed on a wide window and the
   stage overflowed the section's foot, and what sat in that overflow was the
   WATER — the harbour the ship is moored in went off the bottom edge and the
   scene read as a ship parked on paper. A 2:1 stage does not have that
   problem at typical aspects: width:100% now runs edge to edge at every
   window with nothing to cap, because the picture itself was widened to match
   rather than the box being narrowed to fit it.
   ANCHORED TO THE FOOT (`bottom: 0` on a `position: absolute` stage, #port
   itself only needing the `position: relative` .scene already gives it), not
   left in flow. Two consequences, both wanted: (1) at the common aspect
   (window shorter than 2:1 — everything below), the stage's own foot lands
   exactly on the section's foot, which is where #port::after's fade into
   #ocean's blue already lives, and the SPARE height opens above the picture,
   in its own sky, where paper meeting paper is not a seam. (2) at the rare
   aspect (window WIDER than 2:1 — an ultra-wide monitor in a short browser
   window), the stage now overflows the section's TOP, not its foot, so the
   thing #port's own `overflow: hidden` trims off is sky, never the water. */
#port .svg-stage {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  width: 100%;
  aspect-ratio: 2 / 1;
  overflow: visible;
  /* The dissolve below used to be gated behind `max-aspect-ratio: 3/2` — the
     one case the old cap left uncovered. With the cap gone that case is now
     the DEFAULT (a 2:1 stage is shorter than most windows), so the mask is
     unconditional: the render's own last rows (water, then its reflection)
     fade into the paper above #port::after's own fade rather than stopping on
     a hard photo edge. Harmless in the rare overflow case too — the stage's
     foot is flush with the section's there, and dissolving the water into the
     next section's own paper is what #port::after is already doing. */
  -webkit-mask-image: linear-gradient(to bottom, #000 94%, transparent 100%);
  mask-image: linear-gradient(to bottom, #000 94%, transparent 100%);
}

/* The foot of the scene, at EVERY window shape. The stage is foot-anchored
   (see above), so its own bottom edge — open harbour water — is flush with
   the SECTION's own bottom edge at every aspect. That is fine while the
   section is pinned and its edge IS the screen's edge; the moment the pin
   releases and #port scrolls up, the same edge becomes an interior seam with
   #ocean's flat deep blue immediately under it — and a hard seam between two
   unrelated blues directly below a container is most of why this beat could
   be read as the box going into the sea. So the section closes its own last
   band into the paper it sits on: the harbour water fades out before the
   section's edge, #ocean's blue arrives against paper the way every other
   boundary on this page does, and the scene stays self-contained. Paper
   rather than water because the sky in this backdrop IS the paper — the fade
   closes the picture instead of introducing a colour to it, which is the same
   move the stage's own mask-image makes at its foot. Above every stage layer
   (the highest is the occluder at z-index 4), and inert.

   3.5vh, not the 13vh this started at. 13vh of a 994px window is 129px, and
   with the stage capped by height that is 125 of the render's own rows going
   to paper — the whole visible body of the water plus the bottom of the hull,
   which is what bleached the foot of the scene white. All this band has to do
   is stop one blue arriving hard against another: 35px, solid paper only in its
   last 6, water still water above it. */
#port::after {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 3.5vh;
  z-index: 6;
  pointer-events: none;
  background: linear-gradient(to bottom, transparent 0%, var(--c-paper) 82%);
}

/* One picture, one render. assets/img/port-front.webp is now a 1774x887 (2:1)
   photoreal harbour, shot HEAD-ON — an orange container handler on the quay
   directly behind a moored feeder with its spreader crossbar raised high on
   its mast, the ship running the full width in front of it with a long OPEN
   hold, calm water, flat paper sky, quay/pilings/water/sky OUTPAINTED
   seamlessly on both sides of the original 3:2 exposure so the same
   photograph can now run full-bleed at any window shape — and
   tools/build-port-harbor.py splits it into the three layers this scene
   composes: this backdrop (sky matted to transparent, crossbar cut away, the
   mast it rides rebuilt behind it), the crossbar on its own, and the ship's
   near hold wall as an occluder.

   Head-on is what makes the beat literal. The drop line is dead centre and
   vertical, so nothing has to be read at an angle. The two side-view renders
   this replaced could only ever perch the box on a coaming — there was
   nothing behind it to be inside of.

   Every number in this block is a fraction of that render's own frame, printed
   by the build script. Vertically both crops below show all 887 of its rows,
   so every top/height is ONE number; only the x values change. */
#port .port-bg,
#port .port-occluder {
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* The occluder — the ship's NEAR HOLD WALL: its top rail and the grey
   coaming/hull below it, i.e. everything of the ship that stands between the
   camera and a box in the hold. Cut at the rail's own top edge (row 649 of
   887, dead level across the open hold in the columns clear of the machine's
   own footprint — build script) and drawn OVER the container. It is the whole
   point of this render: the box does not land on a deck, it goes down INTO
   the hold and 85% of it disappears, leaving a strip of roof standing behind
   the rail.
   It is emitted full-canvas so it can take the same inset/object-fit the
   backdrop takes — one rule, above — which makes it pixel-registered with the
   backdrop at every viewport with no second set of percentages to drift. And
   because its pixels ARE the backdrop's pixels, compositing it over the
   backdrop is a no-op at any alpha: the layer cannot show a seam, it can only
   hide what passes behind it.

   z-index 4, above BOTH moving layers (spreader 3, container 2): the crossbar
   grips the box and rides down with it on one tween, and the wall has to take
   both of them the same way or the beat shows a machine that stops at the rim
   while its own load keeps going — which is a spreader with nothing in its
   jaws. Only the release-and-hoist, a separate tween on the crossbar alone
   once the box is down, ever lifts it back out from under this layer. */
#port .port-occluder { z-index: 4; }

/* Mobile crop. A 390px-wide phone under a 2:1 stage would get a 195px-tall
   strip — even thinner than the old 3:2 render's 260px one, which was already
   "a diagram of the beat rather than the beat". So the stage goes SQUARE and
   object-fit takes an 887-wide window out of the 1774-wide render instead —
   the same trick the 3:2 render used, just re-measured: this composition is
   CENTRED, so the window needs no object-position at all, and the default 50%
   takes source x 443.5..1330.5, which is the machine, the drop line and the
   whole open hold, with a great deal more of the quay to lose on both sides
   than the old crop had (the outpaint's whole reason to exist). Vertically
   centred rather than foot-anchored like the desktop stage — the square crop
   is much shorter than the phone's own viewport, and centring leaves paper
   above it for the two gap-copy lines exactly the way the pre-outpaint mobile
   layout did, rather than pinning the picture to the fold. */
@media (max-width: 767px) {
  #port .svg-stage {
    aspect-ratio: 1 / 1;
    bottom: auto;
    top: 50%;
    transform: translateY(-50%);
  }
}

/* The rig is two elements, not four: crossbar and box. There is no cable and no
   #port-cable, because this machine does not have one — its spreader rides a
   MAST, and the mast is the backdrop's own, rebuilt by the build script through
   the bite the crossbar's cut took out of it. So the link between the machine
   and its load is drawn steel at every frame of the descent, and there is one
   less element that can fall out of register with the photograph.

   Both left values below are the drop line itself — 49.7182% of the frame, the
   mid-point of the crossbar's two twistlock heads, which is also the mast's own
   centre. The drop is therefore dead vertical down the middle of the picture.

   The crossbar's REST top is NOT the photographed one: the render parks it low
   enough that a box hung from it covers the cab and the operator. 64 rows up
   (39.9098% rather than the photographed 47.2925% + jaws' own span) puts the
   box in open sky with the whole machine legible under it and gives the drop
   somewhere to travel from. Nothing about that shows as a seam — the mast
   behind it is rebuilt for the whole run, so the carriage simply sits higher on
   a mast that is continuous either way. */

/* The crossbar — the machine's own, cut out of the same render the backdrop is
   made of, so it lights and weathers exactly like the mast it rides, and drawn
   at exactly the size it was rendered at (11.9504% of the frame = its own
   212px) rather than rescaled to the box.
   It is a LAYER and not scenery because the container cannot be struck down by
   a spreader that stays where it is — what that reads as is a box coming loose.
   It descends with the box, on the same tween, and lets go only once the box is
   in the hold (portScene()).

   height is stated rather than left to auto because the file's 215x67 is a
   rounding of the crop's 212x66 and auto would put the jaws off by a fraction
   of a pixel; the sub-percent vertical stretch that costs is invisible and an
   exact jaw line is not. The crop's last row of ink IS its twistlock heads
   (jaws at 1.0000 of the layer's own height, measured), so the ELEMENT'S bottom
   edge is the jaws' underside, and the container's hang pose below is that line
   less its own 2/253 of transparent roof margin. margin-left is -5.9752% (of
   the stage, not of the element): the drop line sits that far right of the
   crop's own left edge.

   z-index 3: UNDER the occluder (4), OVER the container (2). Gripped, the
   crossbar's lower portion has to sink behind the hold wall exactly like the
   box in its jaws — a spreader that stayed drawn on top of the wall would read
   as stopping dead at the rim while the container it is holding kept going,
   which is a box with nothing above it. Above the container so the jaws still
   paint over the roof they are gripping, not the other way round. The
   occluder's own pixels are the backdrop's, so this costs nothing while the
   crossbar is above the rim (nothing there to hide behind) and only bites once
   the descent actually crosses it; release-and-hoist then lifts the crossbar
   back out from under this layer the same way the box went in. */
#port-spreader {
  left: 49.7182%;
  top: 39.9098%;
  width: 11.9504%;
  height: 7.4408%;
  margin-left: -5.9752%;
  z-index: 3;
}

/* The container. Its width is not a taste decision: 196px is the span of the
   crossbar's own two twistlock heads, so the box is exactly what this spreader
   can hold, and its roof lines up with the jaws that are gripping it.
   Static end state (no-JS / reduced motion): struck down and seated in the
   hold, the machine having already let go — top = the hold rim (73.1680% of the
   frame, the occluder's own measured top edge) plus 85% of its own 7.3560%
   height BELOW that rim, less the 250/253 of that height that separates its
   sole from its top edge: 72.1518%.
   The seat is stated against the box's own height, not as a row of the render,
   because the depth has to be the same picture at every width. 85% is a design
   constant carried over from the 3:2 render, unchanged: this render's own roof
   strip comes out to 9.8px, read against the quay surface behind the machine
   rather than the old render's separate "hold interior" band — both are simply
   "not sky", which is what "in the hold" needs to read as. Both crops put the
   rim at the same fraction of the STAGE's height (2:1 desktop, and the square
   phone crop, where cover scales on height and shows all 900 rows), so this
   one number is right on both.
   z-index puts it UNDER the occluder above. */
#port-container {
  left: 44.1939%;
  top: 72.1518%;
  width: 11.0485%;
  height: auto;
  z-index: 2;
}

/* Mobile takes an 887-wide window out of the same 1774, so every HORIZONTAL
   fraction is restated against 887 (which, since 887 is exactly half of 1774,
   is always exactly double the desktop fraction here) and every vertical one
   is unchanged. */
@media (max-width: 767px) {
  #port-spreader {
    left: 49.4363%;
    width: 23.9008%;
    margin-left: -11.9504%;
  }

  #port-container { left: 38.3878%; width: 22.0970%; }
}

/* The two gap lines. Same copy, same type, same two beats (see the markup);
   what these renders changed is that they ride their gaps SIDEWAYS, and what
   the head-on one changes again is that there are now TWO clear columns to ride
   in, one each side of the drop.

   Every gap this beat opens is open sky. Everything that is NOT sky in this
   picture is a photograph of a thing — hull, coaming, quay, the machine — and a
   line of type laid over any of it reads as painted onto it, which is the one
   thing a caption must never look like. So each line takes its x from a column
   that is clear of the artwork for the whole beat — measured off the built
   backdrop's own alpha WITH the rig's own column folded in, so "clear" means
   clear at every frame and not just at rest — and rides its gap's SIZE up and
   down inside that column rather than sitting level with the load (which would
   need a clear column that follows the box into the ship; see portScene()):

     desktop  the machine and the drop sit dead centre, which leaves a wide
              reach of sky on each side — even more of one now that the
              outpaint gave the render its own extra width to spend. Measured
              off the built backdrop's own alpha with the rig's column folded
              in: both chosen columns (x 100..450 and 1370..1700 of the 1774)
              run clear to 69.56% down. #port-gap-below takes the left one,
              centred 15.50%; #port-gap-above the right one, centred 86.53%.
     mobile   the square crop is 887 of the render's 1774 wide, so both those
              columns are entirely outside it — there is no clear column left
              to ride at that crop. Both lines go into the section's own paper
              ABOVE the stage instead, stacked, which is the one place on a
              phone that is guaranteed clear of the artwork at every frame
              rather than merely measured to be.

   portScene() writes those columns as a live translate off the stage's MEASURED
   box, clamping each line's tracked y by its own height so a gap can pull it to
   the edge of the clear zone and no further. The y has to be measured rather
   than declared because it is a fraction of the PICTURE and the picture is not
   the parent: the section is a window-height box, the stage a width-derived one.

   The declarations below are the resting spots for the no-JS and reduced-motion
   pages, restated as a `calc()` of vh and vw — which needs the parent's
   geometry, and unlike the old 3:2/capped render this now needs only ONE
   formula for every window shape, because the stage is foot-anchored rather
   than sometimes-clipped-sometimes-centred: its own top edge sits at
   `100vh - 50vw` (a 2:1 stage is `50vw` tall, flush with the section's foot),
   so a point `f` of the way down the picture rests at
   `100vh - (1 - f) * 50vw`. True at every aspect, including the rare
   wider-than-2:1 one, where the formula simply places it above the fold along
   with the rest of the (now top-overflowing) picture. */
#port-gap-below {
  left: 15.50%;
  top: calc(100vh - 32.5vw);
  max-width: 22ch;
  transform: translate(-50%, -50%);
}

#port-gap-above {
  left: 86.53%;
  top: calc(100vh - 36vw);
  max-width: 22ch;
  transform: translate(-50%, -50%);
}

/* Mobile: both lines into the paper above the picture, stacked, at f -0.30 and
   f -0.10 of the square stage's own height — i.e. 110px and 188px down a 844px
   screen, clear of the nav above them (81.5px) and of the stage's own top edge
   (227px) below. Nothing of the artwork can reach them: they are not over the
   picture at all. portScene() states the same two slots as WINDOW fractions
   rather than stage ones, with hard clamps against the nav and the stage edge,
   because on a short phone those two move independently of the picture, and it
   is them and not the picture that these lines have to keep out of. */
@media (max-width: 767px) {
  #port-gap-below { left: 50%; top: calc(50% - 80vw); }
  #port-gap-above { left: 50%; top: calc(50% - 60vw); }
  #port-gap-below, #port-gap-above { max-width: 26ch; }
}

/* Animated start state: the box hanging in the crossbar's jaws (47.2925% down,
   its own 2/253 of transparent roof margin already taken off — see
   #port-spreader above), which is the ONLY thing the animated page has to
   restate. The crossbar itself already sits at its raised pose in the static
   CSS, and the strike-down is a round trip from there: it descends gripping the
   box and comes back up empty, so where it rests is where the beat both starts
   and ends. Gated exactly like portScene()'s own matchMedia, so the two can
   never disagree about where the beat begins. */
@media (prefers-reduced-motion: no-preference) {
  html.js #port-container { top: 47.2925%; }
}

/* ---------- Ocean / freight ---------- */

/* Backdrop colour is ocean.webp's own mean pixel value (18, 52, 112), not the
   --c-sea brand token: the pull-away zoom exposes this behind the photo, and the
   lighter token left a visible rectangle of a different blue around the water. */
#ocean { background: #123470; color: var(--c-paper); min-height: 100vh; overflow: hidden; }

#ocean .svg-stage { position: absolute; inset: 0; min-height: 0; }

#ocean-bg { inset: 0; width: 100%; height: 100%; object-fit: cover; }

.ship-wrap { position: absolute; left: 12%; top: 2%; width: 12%; transform: rotate(-6deg); }

#ship { position: relative; }

.ship-img { width: 100%; height: auto; }

/* soft stern wake — a blurred translucent plume that travels with the ship */
.wake {
  position: absolute;
  left: 50%;
  top: 98%;
  width: 260%;
  height: 62%;
  margin-left: -130%;
  background: radial-gradient(ellipse 38% 100% at 50% 0%,
    rgba(255, 255, 255, 0.26), rgba(255, 255, 255, 0.05) 50%, rgba(255, 255, 255, 0) 80%);
  filter: blur(16px);
  pointer-events: none;
}

.ocean-copy {
  position: relative;
  z-index: 1;
  max-width: 720px;
  margin: 0 auto;
  padding: 2rem clamp(1rem, 4vw, 3rem);
  text-align: center;
}

.ocean-copy p { margin-top: 1.5rem; opacity: 0.9; }

/* keep the copy clear of the ship, which sits in the left third of the ocean stage */
@media (min-width: 900px) {
  .ocean-copy { margin-left: auto; margin-right: clamp(1rem, 4vw, 5rem); }
}

/* narrower viewports have no room beside the copy, so the ship crosses behind a
   bottom-sheet treatment instead of sitting next to it */
@media (max-width: 899px) {
  /* pushed right and narrowed so the hull clears the centred copy column: the
     ship crosses down the right margin rather than straight through the h2 */
  .ship-wrap { left: 66%; width: 24%; }

  /* Bottom sheet: pinned to the foot of the pinned #ocean viewport. The scrim
     has to reach above the h2 (not just the paragraph) and hold enough alpha
     under the whole text block to stay legible with the ship crossing behind
     it — a light foot-of-frame wash left the heading unreadable mid-crossing. */
  .ocean-copy {
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    margin: 0;
    max-width: none;
    padding: 3.5rem 1.5rem 2rem;
    background: linear-gradient(to top,
      rgba(8, 17, 40, .93) 0%,
      rgba(8, 17, 40, .9) 55%,
      rgba(8, 17, 40, .68) 82%,
      rgba(8, 17, 40, 0) 100%);
  }
}

.route-markers {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 1rem;
  list-style: none;
  margin-top: 2rem;
}

/* Green marks the port, paper carries the name. The dot is decorative so it can
   take the brand value; the label stays paper-white because nothing green reads
   at 0.8rem over open water. */
.route-markers li {
  display: inline-flex;
  align-items: center;
  gap: 0.55rem;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  font-size: 0.8rem;
  line-height: 1.3;
  padding: 0.4rem 1rem;
  border: 1px solid rgba(244, 242, 238, 0.4);
  border-radius: 999px;
}

.route-markers li::before {
  content: "";
  width: 7px;
  height: 7px;
  flex: 0 0 auto;
  border-radius: 50%;
  background: var(--c-green);
}

/* ---------- Delivered / CTA ---------- */

#delivered {
  position: relative;
  overflow: hidden;
  /* The top padding is the sky band: the jet's flight path has to clear the
     headline's cap height entirely, or the crossing reads as a collision
     rather than as altitude above the copy. */
  padding: clamp(9rem, 22vh, 15rem) 0 8rem;
  text-align: center;
}

/* Ambient sky behind the closing copy. Every layer is parked by CSS in a
   composition that already reads as finished, so the reduced-motion and no-JS
   pages get a still of the flyby rather than an empty band of paper.
   Height is explicit rather than inset:0 because #delivered now carries the
   industries/renewables band under the CTA: stretched over that, the sky puts
   a cloud behind body copy and hands deliveredSection() a climb height derived
   from how much text is in the section. The sky is the top band, full stop. */
.sky {
  position: absolute;
  inset: 0 0 auto;
  height: clamp(20rem, 44vh, 28rem);
  pointer-events: none;
}

.cloud { position: absolute; height: auto; opacity: 0.9; }

.cloud--1 { left: -5%; top: 4%; width: 30%; }

.cloud--2 { right: -4%; top: 10%; width: 27%; z-index: 2; }

.cloud--3 { left: 26%; bottom: 4%; width: 24%; z-index: 2; }

.cloud--4 { right: 30%; top: 1%; width: 11%; }

#plane {
  position: absolute;
  left: 50%;
  top: 9%;
  width: 20%;
  height: auto;
  z-index: 1;
  transform: translateX(-50%);
}

#delivered .wrap {
  position: relative;
  z-index: 3;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2rem;
}

/* The page's one primary action, in the brand green. The label is 1.2rem/700
   (19.2px bold) rather than the 0.95rem it was: past 18.66px bold WCAG counts it
   as large text, and --c-green's 3.75:1 against paper clears the 3:1 that
   applies there. The size is a contrast requirement, not a flourish. */
.cta {
  display: inline-block;
  background: var(--c-green);
  color: var(--c-paper);
  text-decoration: none;
  padding: 1rem 2.75rem;
  border-radius: 999px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  font-size: 1.2rem;
}

.cta:hover { background: var(--c-green-deep); }

/* ---------- Contact / footer ---------- */

footer#contact { padding: 6rem 0 3rem; }

footer#contact h2 { margin-bottom: 1rem; }

footer#contact a { color: var(--c-paper); }

footer#contact address { font-style: normal; margin-top: 1.5rem; opacity: 0.85; }

footer#contact .offices {
  margin-top: 1rem;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  font-size: 0.85rem;
  opacity: 0.7;
}

/* knockout lockup — the footer is ink, so the paper-surface variant used in the
   nav would lose its near-white "CONNECTED" wordmark here */
.footer-logo { display: block; width: 130px; height: auto; margin-top: 2.5rem; }

footer#contact .kinara { margin-top: 2rem; font-size: 0.8rem; opacity: 0.7; }

.footer-logo + .kinara { margin-top: 0.75rem; }

/* Visible text stays the site's own ("Read More"); the context a screen reader
   needs to tell five identical links apart is added, not substituted. */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* ---------- Long-form content system ----------
   Everything below styles the reference material Version B carries in full:
   the service drawers, the about/products/insights/FAQ bands and the footer.
   It introduces no new colour — the only surfaces are paper, ink and mixes of
   the two, and green stays where it already was (rules, ticks, the one CTA). */

/* Section rhythm. Long-form bands are a fixed vertical beat apart so the page
   reads as one document rather than as a stack of unrelated widgets. */
#positioning,
#services-depth,
#about,
#products,
#insights,
#faqs,
#assessment { padding: clamp(4rem, 10vh, 7rem) 0; }

/* Two paper tones, both mixed from the existing tokens: the alternation is what
   keeps six consecutive text bands from reading as one undifferentiated wall. */
#services-depth,
#products { background: color-mix(in srgb, var(--c-ink) 4%, var(--c-paper)); }

/* The positioning line runs to a sentence and a half. At the h2 clamp's top end
   it would set three lines of 64px display type, which is a poster, not a
   statement — so this one variant sizes down and holds a reading measure. */
.h2--statement { font-size: clamp(1.4rem, 2.6vw, 2.2rem); max-width: 24ch; }

.section-lede {
  max-width: 70ch;
  margin-top: 1.5rem;
  font-size: clamp(1rem, 1.4vw, 1.15rem);
}

.eyebrow {
  font-family: var(--font-body);
  font-size: 0.78rem;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--c-ink-mute);
}

/* The one body-copy container: every drawer body, every band of prose. 70ch is
   the measure the whole document is set to. */
.prose { max-width: 70ch; margin-top: 1.5rem; }

.prose > * + * { margin-top: 1rem; }

.prose h4 { font-size: clamp(1.2rem, 2vw, 1.6rem); margin-top: 2rem; }

.prose h5 {
  font-family: var(--font-body);
  font-size: 1.02rem;
  font-weight: 700;
  text-transform: none;
  letter-spacing: 0;
  margin-top: 2rem;
}

.prose h6 {
  font-family: var(--font-body);
  font-size: 0.95rem;
  font-weight: 700;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  color: var(--c-ink-mute);
  margin-top: 1.5rem;
}

.prose ol { padding-left: 1.25rem; }

.prose li + li { margin-top: 0.4rem; }

.prose ul { list-style: none; padding-left: 0; }

/* Green tick rules the list the way the h2 rule rules a heading — the same
   primitive, one step smaller. Decorative, so the brand value is safe here. */
.prose ul > li {
  position: relative;
  padding-left: 1.5rem;
}

.prose ul > li::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0.62em;
  width: 10px;
  height: 2px;
  border-radius: 1px;
  background: var(--c-green);
}

.prose .steps { list-style: none; padding-left: 0; }

.prose .steps > li + li { margin-top: 1.25rem; }

.prose .steps p { margin-top: 0.35rem; }

/* Content-team flags. Several live pages ship duplicated copy; it is reproduced
   verbatim (that is the parity requirement) and labelled, so a reader is never
   left thinking the duplication is ours. */
.note {
  font-size: 0.85rem;
  color: var(--c-ink-mute);
  /* Ink, not steel: the section comment above claims paper/ink mixes only, and
     --c-steel was the one hairline that didn't back that claim up. */
  border-left: 2px solid color-mix(in srgb, var(--c-ink) 30%, transparent);
  padding-left: 0.9rem;
}

/* ---------- Credibility additions ---------- */

.stat-notes {
  margin-top: 1.5rem;
  font-size: 0.85rem;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  color: var(--c-ink-mute);
}

.partners { margin-top: 3.5rem; }

.partner-list {
  display: flex;
  flex-wrap: wrap;
  gap: 0.6rem 0.75rem;
  list-style: none;
  margin-top: 1rem;
}

.partner-list li {
  padding: 0.4rem 0.9rem;
  border: 1px solid color-mix(in srgb, var(--c-ink) 18%, transparent);
  border-radius: 999px;
  font-size: 0.82rem;
  letter-spacing: 0.02em;
}

.risk { max-width: 70ch; margin-top: 3.5rem; }

.risk > * + * { margin-top: 1rem; }

.risk-list { list-style: none; }

.risk-list li { position: relative; padding-left: 1.5rem; }

.risk-list li + li { margin-top: 0.4rem; }

/* Rust, not green: this is the list of what goes wrong. The token is already in
   the palette and carries no other job on the page. Qualified with .risk (its
   actual parent in the markup, not just a scoping trick) rather than left as
   the bare class: .prose ul > li::before outranks a bare .risk-list li::before
   on specificity alone (3 type selectors vs. 2), so if .risk-list content is
   ever nested inside a .prose block the green tick would silently win over
   this rust one. The two-class form always wins regardless of nesting. */
.risk .risk-list li::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0.62em;
  width: 10px;
  height: 2px;
  border-radius: 1px;
  background: var(--c-rust);
}

/* ---------- Capabilities / column lists ---------- */

.capabilities,
.deflist { margin-top: 2rem; }

.deflist > div { max-width: 70ch; }

.deflist > div + div { margin-top: 1.5rem; }

/* The five homepage capabilities are short enough to pair up, and paired they
   read as a roster rather than as five stacked paragraphs. */
.capabilities {
  display: grid;
  gap: 1.75rem 2.5rem;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 24rem), 1fr));
}

.capabilities dt,
.deflist dt { font-weight: 700; }

.capabilities dd,
.deflist dd { margin-top: 0.3rem; }

.deflist--split { display: grid; gap: 1.5rem; grid-template-columns: repeat(auto-fit, minmax(min(100%, 22rem), 1fr)); }

.deflist--split > div + div { margin-top: 0; }

.col-lists {
  display: grid;
  gap: 2rem;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 15rem), 1fr));
  margin-top: 2.5rem;
}

.col-lists h4 {
  font-family: var(--font-body);
  font-size: 0.78rem;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--c-ink-mute);
  padding-bottom: 0.6rem;
  border-bottom: 2px solid var(--c-green);
}

.col-lists ul { list-style: none; margin-top: 1rem; }

.col-lists li + li { margin-top: 0.5rem; }

/* Anchor landings clear the sticky nav. Without this a deep link into a drawer
   puts the summary row exactly under the 95px header — the reader arrives at a
   drawer that opened somewhere they cannot see. scroll-margin only affects
   anchor/scrollIntoView landings, so ScrollTrigger's own maths is untouched. */
.drawer,
#positioning, #services-depth, #about, #products, #insights, #faqs, #assessment,
footer#contact { scroll-margin-top: clamp(5.5rem, 12vh, 7rem); }

/* ---------- Disclosures ---------- */

/* Native <details>: keyboard-operable and open-able with JavaScript off, which
   is the whole reason the long-form copy is carried this way rather than in a
   scripted accordion. */
/* Solid rgba first, color-mix() second: these hairlines carry the drawer
   roster's only visual separation, so a browser without color-mix() support
   needs a real fallback rather than an invalid declaration silently dropped
   (which would leave every drawer border-less). The later, valid declaration
   wins wherever color-mix() IS supported. */
.accordion { margin-top: 2rem; border-top: 1px solid rgba(18, 20, 29, 0.16); border-top: 1px solid color-mix(in srgb, var(--c-ink) 16%, transparent); }

.drawer { border-bottom: 1px solid rgba(18, 20, 29, 0.16); border-bottom: 1px solid color-mix(in srgb, var(--c-ink) 16%, transparent); }

.drawer > summary {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 1rem;
  padding: 1.1rem 2.5rem 1.1rem 0;
  position: relative;
  cursor: pointer;
  list-style: none;
}

.drawer > summary::-webkit-details-marker { display: none; }

.drawer__title { font-weight: 700; font-size: 1.05rem; }

.drawer__hint {
  font-size: 0.78rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--c-ink-mute);
  text-align: right;
}

/* The open/closed tell is a plus that loses its vertical stroke — one element,
   two pseudo-free rules, no icon font and nothing to load. */
.drawer > summary::after {
  content: "";
  position: absolute;
  right: 0.25rem;
  top: 1.45rem;
  width: 14px;
  height: 14px;
  background:
    linear-gradient(var(--c-green-deep) 0 0) center / 100% 2px no-repeat,
    linear-gradient(var(--c-green-deep) 0 0) center / 2px 100% no-repeat;
  /* [open] below animates background-size, not transform — the guard a few
     lines down was disabling the wrong property and guarding nothing. */
  transition: background-size 0.2s ease;
}

.drawer[open] > summary::after { background-size: 100% 2px, 0 100%; }

@media (prefers-reduced-motion: reduce) {
  .drawer > summary::after { transition: none; }
}

.drawer > summary:hover .drawer__title { color: var(--c-green-deep); }

.drawer__body { padding-bottom: 2rem; }

.drawer__body > :first-child { margin-top: 0; }

.accordion--dense .drawer__title { font-size: 0.98rem; }

/* ---------- Services cards ---------- */

.service-card__link {
  display: inline-block;
  margin-top: 1rem;
  font-size: 0.78rem;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--c-paper);
  text-decoration: none;
  border-bottom: 2px solid var(--c-green);
  padding-bottom: 0.15rem;
}

.service-card { display: flex; flex-direction: column; align-items: flex-start; }

.service-card p { flex: 1 1 auto; }

/* ---------- Process closing ---------- */

.process-closing {
  position: relative;
  z-index: 1;
  max-width: 70ch;
  margin-top: 2rem;
  background: var(--c-paper);
  padding: 1rem 0;
}

/* ---------- Ocean overlay copy ---------- */

/* Two freight-page paragraphs ride the ocean panel. They are a step down from
   the industries line so the panel still reads as a caption over a photograph
   rather than as an article printed on the sea. */
.ocean-detail {
  font-size: 0.88rem;
  line-height: 1.45;
  max-width: 62ch;
  margin-inline: auto;
}

/* ---------- Delivered band ---------- */

/* #delivered .wrap is a centred flex column (id + class beats a bare class), so
   the band has to out-specify it to become a grid at all. */
#delivered .wrap.delivered-band {
  position: relative;
  z-index: 3;
  display: grid;
  gap: 2.5rem;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 20rem), 1fr));
  margin-top: clamp(4rem, 10vh, 7rem);
  text-align: left;
}

.delivered-band h3 {
  font-family: var(--font-body);
  font-size: 0.78rem;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--c-ink-mute);
  padding-bottom: 0.6rem;
  border-bottom: 2px solid var(--c-green);
}

.delivered-band ul { list-style: none; margin-top: 1rem; }

.delivered-band li + li { margin-top: 0.45rem; }

.delivered-band p { margin-top: 1rem; font-size: 0.9rem; color: var(--c-ink-mute); }

/* ---------- Cards (insights / case studies) ---------- */

.cards {
  display: grid;
  gap: 1.5rem;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 18rem), 1fr));
  margin-top: 2rem;
}

.card {
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
  padding: 1.5rem;
  background: var(--c-paper);
  border: 1px solid color-mix(in srgb, var(--c-ink) 14%, transparent);
}

.card h3 { font-size: 1.15rem; }

.card h4 { font-size: 1rem; font-family: var(--font-body); text-transform: none; letter-spacing: 0; }

/* Removing the UA bullet with nothing to replace it left these list items
   reading as loose lines of text rather than a list — same green-tick
   primitive as .prose ul > li::before, one size down for the card's tighter
   type. */
.card ul { list-style: none; }

.card ul > li { position: relative; padding-left: 1.25rem; }

.card ul > li::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0.55em;
  width: 8px;
  height: 2px;
  border-radius: 1px;
  background: var(--c-green);
}

.card li + li { margin-top: 0.4rem; }

.card p { font-size: 0.92rem; }

.card__link {
  margin-top: auto;
  align-self: flex-start;
  font-size: 0.78rem;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--c-green-deep);
  text-decoration: none;
  border-bottom: 2px solid var(--c-green);
  padding-bottom: 0.15rem;
}

/* Sub-heads inside the long-form bands. They are display type like every other
   heading, but they have to sit BELOW a block of body copy, so the spacing is
   the rule that does the work: without it the h3 lands on the last line of the
   definition list above and the two read as one paragraph. */
#positioning .wrap > h3,
#services-depth .wrap > h3,
#about .wrap > h3,
#products .wrap > h3,
#insights .wrap > h3,
#faqs .wrap > h3 {
  margin-top: clamp(2.5rem, 6vh, 4rem);
  font-size: clamp(1.3rem, 2.4vw, 2rem);
}

/* Small off-site link roster closing #insights: same rhythm as the sub-heads
   above it, but the label stays an eyebrow rather than a heading — this is a
   footnote to the section, not another sub-head competing with "Case
   Studies" and "Insights for Better Sourcing Decisions". */
#insights .more-links { margin-top: clamp(2.5rem, 6vh, 4rem); }

.more-links__row { display: flex; flex-wrap: wrap; column-gap: 1.75rem; row-gap: 0.5rem; margin-top: 1rem; }

.more-links__row .card__link { margin-top: 0; }

/* ---------- Assessment CTA band ---------- */

#assessment { text-align: center; }

#assessment .section-lede { margin-inline: auto; }

#assessment h2::after { margin-inline: auto; }

.cta-row { margin-top: 2.5rem; }

.cta-alt {
  margin-top: 1.5rem;
  font-size: 0.8rem;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  opacity: 0.75;
}

/* ---------- Footer ---------- */

.offices-grid {
  display: grid;
  gap: 2rem;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 14rem), 1fr));
  margin-top: 3rem;
}

.offices-grid h3 {
  font-family: var(--font-body);
  font-size: 0.78rem;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  padding-bottom: 0.6rem;
  border-bottom: 2px solid var(--c-green);
}

.offices-grid address { font-style: normal; margin-top: 1rem; font-size: 0.9rem; opacity: 0.85; }

.footer-cols {
  display: grid;
  gap: 2rem;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 14rem), 1fr));
  margin-top: 3.5rem;
  padding-top: 2.5rem;
  border-top: 1px solid rgba(244, 242, 238, 0.18);
}

/* No longer needs its own override: .eyebrow's color: var(--c-ink-mute)
   already resolves to the dark-surface value here, inherited from
   .section--dark on the ancestor footer. */
.footer-cols p, .footer-cols address { margin-top: 1rem; font-size: 0.9rem; font-style: normal; }

.link-list { list-style: none; margin-top: 1rem; }

.link-list li + li { margin-top: 0.5rem; }

.link-list a { font-size: 0.9rem; }

.cert { opacity: 0.75; }

/* 0.45 put paper on ink at the 4.5:1 line and Lighthouse called it under;
   0.7 is 8.5:1 and still reads as fine print. */
footer#contact .legal { margin-top: 1.5rem; font-size: 0.78rem; opacity: 0.7; }

/* ---------- Booking note (contact form) ----------
   A shipping document rather than a form: paper laid on the dark footer, ruled
   in manifest lines, perforated, numbered and barcoded. Nothing here is wired
   to anything — the submit handler prevents default and stamps the document,
   which is the entire response. */

.waybill {
  position: relative;
  max-width: 46rem;
  margin-top: 3rem;
  padding: clamp(1.5rem, 4vw, 2.25rem);
  background: var(--c-paper);
  color: var(--c-ink);
  /* Paper again, inside a dark section: --c-ink-mute has to come back to the
     paper-surface formula here or .wb-key/.wb-ask/etc. would inherit the
     footer's dark-surface mute (light grey) onto this light card and fail
     contrast the other way round. */
  --c-ink-mute: color-mix(in srgb, var(--c-ink) 65%, var(--c-paper));
  /* Paper does not float: the drop shadow is what puts this on top of the ink
     section rather than in it. */
  box-shadow: 0 24px 60px rgba(0, 0, 0, 0.45);
}

/* Perforated tear edge, top and bottom: a row of paper-coloured half-circles
   punched out of the card's own edge. One gradient per edge, no assets. */
.waybill::before,
.waybill::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  height: 10px;
  background: radial-gradient(circle at 8px 0, var(--c-ink) 0 5px, transparent 5px) 0 0 / 16px 10px repeat-x;
}

.waybill::before { top: 0; }

.waybill::after { bottom: 0; transform: scaleY(-1); }

.waybill__head {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  justify-content: space-between;
  gap: 1rem;
  padding-bottom: 1rem;
  border-bottom: 2px dashed color-mix(in srgb, var(--c-ink) 30%, transparent);
}

.waybill__logo { display: block; width: 128px; height: auto; }

.waybill__meta { text-align: right; }

.waybill__kind {
  display: block;
  font-size: 0.7rem;
  font-weight: 700;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--c-ink-mute);
}

/* The document number. Archivo has tabular figures at this tracking, which is
   what stops the number reshuffling its own width between page loads. */
.waybill__docno {
  display: block;
  margin-top: 0.35rem;
  font-size: 1.05rem;
  font-weight: 700;
  letter-spacing: 0.14em;
  font-variant-numeric: tabular-nums;
}

.waybill__rows { margin-top: 0.5rem; }

/* One manifest line. The key column is fixed so every rule starts at the same
   x — that alignment is most of what makes this read as a printed document. */
.wb-row {
  display: grid;
  grid-template-columns: 11rem 1fr;
  align-items: baseline;
  gap: 0 1rem;
  padding: 0.9rem 0 0.5rem;
  /* Same fallback pattern as .accordion/.drawer above — this rule IS the
     field on a document that carries no other box around its inputs. */
  border-bottom: 1px solid rgba(18, 20, 29, 0.22);
  border-bottom: 1px solid color-mix(in srgb, var(--c-ink) 22%, transparent);
}

.wb-row label { display: block; cursor: pointer; }

.wb-key {
  display: block;
  font-size: 0.72rem;
  font-weight: 700;
  letter-spacing: 0.14em;
  text-transform: uppercase;
}

.wb-ask {
  display: block;
  margin-top: 0.15rem;
  font-size: 0.8rem;
  color: var(--c-ink-mute);
}

/* The field is the rule itself: no box, no fill, nothing that reads as a web
   form control sitting on a piece of paper. */
.wb-row input,
.wb-row select {
  width: 100%;
  padding: 0.35rem 0.25rem;
  font: inherit;
  font-size: 1rem;
  color: var(--c-ink);
  background: transparent;
  border: 0;
  border-radius: 0;
}

/* The select reads as a control on its own (the chevron below says so); a
   plain text <input> has nothing to distinguish it from empty paper at rest,
   so it gets its own low-contrast writing line — dotted, so it still reads as
   "where you write" rather than as a form field's box. */
.wb-row input {
  border-bottom: 1px dotted rgba(18, 20, 29, 0.32);
  border-bottom: 1px dotted color-mix(in srgb, var(--c-ink) 32%, transparent);
}

.wb-row input::placeholder { color: var(--c-ink-mute); opacity: 1; }

/* The select still has to say it is a select. */
.wb-row select {
  appearance: none;
  cursor: pointer;
  background-image:
    linear-gradient(45deg, transparent 50%, var(--c-ink) 50%),
    linear-gradient(135deg, var(--c-ink) 50%, transparent 50%);
  background-position: right 0.7rem center, right 0.35rem center;
  background-size: 6px 6px, 6px 6px;
  background-repeat: no-repeat;
  padding-right: 1.6rem;
}

.wb-row:focus-within { background: color-mix(in srgb, var(--c-green) 7%, transparent); }

.waybill__foot {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: 1.5rem;
  flex-wrap: wrap;
  margin-top: 1.75rem;
}

/* Corner barcode. Two stripe widths in one repeating gradient so it scans as a
   barcode rather than as a hatch pattern. */
.waybill__barcode {
  width: min(15rem, 100%);
  padding-bottom: 0.4rem;
  background:
    repeating-linear-gradient(90deg,
      var(--c-ink) 0 2px, transparent 2px 4px,
      var(--c-ink) 4px 5px, transparent 5px 8px,
      var(--c-ink) 8px 11px, transparent 11px 13px) top / 100% 44px no-repeat;
  padding-top: 52px;
}

.waybill__barcode-no {
  font-size: 0.62rem;
  letter-spacing: 0.3em;
  text-transform: uppercase;
  color: var(--c-ink-mute);
}

/* Same green as the page's one primary action, and the same reason it can take
   the brand value: the label is 1rem/700 uppercase, above the large-text floor
   once letterspaced, and it clears 3:1 on paper at that size. */
.wb-submit {
  font: inherit;
  font-size: 1rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--c-paper);
  background: var(--c-green);
  border: 0;
  border-radius: 999px;
  padding: 0.85rem 2.25rem;
  cursor: pointer;
}

.wb-submit:hover { background: var(--c-green-deep); }

.wb-submit[disabled] { opacity: 0.55; cursor: default; }

/* The stamp. Struck over the document, off-square, transform and opacity only
   so it never touches layout. */
/* display:grid out-specifies the UA's [hidden] rule, so the attribute the JS
   toggles needs saying again here or the stamp ships already struck. */
.waybill__stamp[hidden] { display: none; }

.waybill__stamp {
  position: absolute;
  right: clamp(1rem, 6%, 3rem);
  /* Struck across the manifest, not across the button: at the foot of the card
     the disc lands squarely on top of Lodge Booking. */
  top: 34%;
  width: 11rem;
  height: 11rem;
  display: grid;
  place-content: center;
  text-align: center;
  border: 5px solid var(--c-green-deep);
  border-radius: 50%;
  box-shadow: inset 0 0 0 3px var(--c-paper), inset 0 0 0 6px var(--c-green-deep);
  color: var(--c-green-deep);
  font-family: var(--font-display);
  font-size: 1.6rem;
  line-height: 1.05;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  opacity: 0.92;
  /* It lies ON the document, so it must not take the clicks meant for the
     fields it covers. */
  pointer-events: none;
  transform: rotate(-13deg);
  animation: stamp-strike 0.45s cubic-bezier(0.2, 1.4, 0.4, 1) both;
}

@keyframes stamp-strike {
  from { opacity: 0; transform: rotate(-34deg) scale(2.2); }
  to   { opacity: 0.92; transform: rotate(-13deg) scale(1); }
}

/* Reduced motion: the stamp still lands, it just does not fly in. */
@media (prefers-reduced-motion: reduce) {
  .waybill__stamp { animation: none; }
}

.waybill__ack {
  margin-top: 1.25rem;
  font-weight: 700;
  color: var(--c-green-deep);
}

/* Present in the DOM (and the accessibility tree) at all times, but takes no
   space until the submit handler gives it text — the same trick minus the
   `hidden`-toggle that broke the live-region announcement. */
.waybill__ack:empty { margin-top: 0; }

@media (max-width: 767px) {
  /* The key column cannot hold "Contact Channel" and leave a writable rule at
     390px, so the manifest line stacks: key over rule, same reading order. */
  .wb-row { grid-template-columns: 1fr; gap: 0; }

  .waybill__stamp {
    position: static;
    top: auto;
    width: 9rem;
    height: 9rem;
    margin: 1.5rem auto 0;
    font-size: 1.3rem;
  }

  .wb-submit { width: 100%; }
}

/* ---------- Mobile ---------- */

@media (max-width: 767px) {
  /* Long-form bands: one column, and the drawer hint drops — at 390px the
     summary row has to hold a service name and a control, and nothing else. */
  .drawer__hint { display: none; }

  .drawer > summary { padding-right: 2rem; }

  /* The bottom sheet now carries two freight paragraphs on top of what it
     already had, and at 844px tall it was pushing the last two route markers
     off the foot of the pinned viewport. Every value here is the same block of
     copy set tighter — nothing is dropped, because parity means nothing is. */
  .ocean-detail { font-size: 0.82rem; }

  /* Top padding matches the 899px breakpoint's 3.5rem, not a tightened value:
     the comment above the scrim (:866) requires the gradient reach above the
     h2, and cutting this by the 1rem it used to lose here undercut that. */
  .ocean-copy { padding: 3.5rem 1.25rem 1.5rem; }

  .ocean-copy p { margin-top: 1rem; }

  .ocean-copy .route-markers { margin-top: 1.25rem; }

  /* the industries line is five lines of body copy at 390px; a step down takes
     it to four and buys the fourth route marker its row back */
  .ocean-copy > p:first-of-type { font-size: 0.9rem; }

  .route-markers { gap: 0.5rem; }

  .stats { grid-template-columns: 1fr; gap: 2rem; }

  .service-track {
    display: flex;
    overflow-x: auto;
    scroll-snap-type: x mandatory;
    gap: 1rem;
    padding: 1.5rem clamp(1rem, 4vw, 3rem);
    /* clear the desktop base rule's max-width/margin so the scroll-snap
       container's own viewport matches what's actually visible on screen
       (without this, margin:auto collapses to 0 and the box shrink-fits up
       to 1200px wide, way past the clipped 390px viewport, and scroll-snap
       computes its port against that oversized box). */
    max-width: none;
    margin: 0;
  }

  /* min-width alone is a floor, not a ceiling: flex: 0 0 auto sizes the card to
     its content's preferred (max-content) width when that's wider than 80vw —
     unwrapped copy pushed cards as wide as 712px, clipped by #services's own
     overflow:hidden. max-width pins it back to the same 80vw. */
  .service-card { scroll-snap-align: start; min-width: 80vw; max-width: 80vw; flex: 0 0 auto; }

  .process-road { left: 24px; width: 4px; transform: none; }

  .process-road::after { left: 0; transform: none; }

  #process-truck { width: 30px; left: 2px; }

  /* Scenes are far narrower here, so every layer takes a bigger share of the
     stage — the drop geometry (boom tip clearing the landing surface) still
     resolves because it is derived from these same proportions. */
  #hero-yard .crane-rig { left: -12%; bottom: 5%; width: 92%; }

  #hero-yard .stack-group { left: 58.5%; width: 34%; }

  #hero-yard .ly--farstack { display: none; }

  /* The lift itself is much shorter at this width (the crane's own drop
     distance, measured off the same proportions the rig is scaled to,
     comes out under 45px here against ~240px at 1440) — three lines of the
     desktop eyebrow size overflow a gap that tight on both edges. Two lines
     at a smaller size and a wider (in ch) box clears it with room to spare. */
  #yard-gap-copy { font-size: 0.62rem; line-height: 1.2; max-width: 26ch; }

  /* Same story for the two beats' worth of sentence-length lines: the truck
     and port gaps are tighter here too, and .loading-line's desktop floor
     (0.85rem, three lines) is taller than either one comfortably holds. */
  .loading-line.gap-copy { font-size: 0.6rem; line-height: 1.25; max-width: 26ch; }

  #loading .svg-stage { aspect-ratio: 3 / 2; }

  #loading .crane-rig { left: 28.25%; width: 78%; }

  #loading .rig { left: 0; width: 49%; }

  #loading .ly--farstack { display: none; }

  #services .svg-stage { aspect-ratio: 900 / 330; }

  #services .rig { left: 12%; width: 76%; }

  /* The desk and the pair are the full column at this width; there is no corner
     left to hold either of them in. */
  .desk, .pair { width: 100%; }

  .scene-label { font-size: 0.62rem; max-width: 12ch; }

  /* The hand-over's label hangs in the gutter beside the pair on desktop. At
     390px there is no gutter — at -58% it is off the left edge of the phone
     entirely — so it goes above the figures instead, where the wide frames put
     it beside them. left is inset by the same 1rem the page's own .wrap gutter
     resolves to at this width, so it doesn't sit flush with the viewport edge. */
  .scene-label--deal { left: 1rem; top: -13%; }

  /* ---- Finding 1: factory-hall static composition at 390px ----
     .hall's base rule (568) is 2:1 (aspect-ratio: 1680/840); at 390px wide
     that is only 195px tall, which put both scene-labels' green rules across
     the machinery/worker and left .bp--line (6.5% wide, the desktop figure) a
     26x16px speck. This block only reaches the STATIC render — no JS, or JS
     with prefers-reduced-motion: reduce — because the pinned/panned composition
     (html.js + prefers-reduced-motion: no-preference) is driven by a wholly
     different, position:absolute .hall (see the general override above, and
     the mobile re-scale of it further down this file) that this selector
     pair does not touch. */
  html:not(.js) #factory-hall .svg-stage { padding: 3rem 0; }

  html:not(.js) .hall { aspect-ratio: 1680 / 1200; height: auto; }

  /* Cover-crop rather than stretch: the box is no longer the render's own 2:1,
     so filling it without object-fit would squash the artwork. Centered, so
     the crop takes equally off both sides rather than favouring one label.
     Unqualified (not html:not(.js)-scoped): in the animated case .hall keeps
     its native 1680/840 box (restored below), where cover is a no-op — the
     box ratio already matches the image, so this line only does anything in
     the static, re-ratio'd case above. */
  #factory-hall .hall__img { object-fit: cover; object-position: center; }

  /* Off the artwork and into the padding band reserved above, fixed rem
     offsets rather than %-of-hall so they don't scale back onto the image as
     .hall's own height changes. Full-width so the label — still carrying its
     green rule — reads on one line instead of wrapping across three. */
  html:not(.js) .scene-label--line { left: 1rem; right: 1rem; max-width: none; top: -2.4rem; }

  /* Sized up from the desktop figure it inherited (6.5%, a 26x16px speck at
     this width) to something that reads as a sheet pinned near the line. */
  html:not(.js) .bp--line { width: 14%; }

  /* ---- The development studio at 390px ----
     A 1536-wide three-panel diorama at 390px is 260px tall and each of its
     three panels is 130px across: a picture of a room rather than a room. So
     this one IS cropped, and cropped to the panel the beat belongs to — the
     sketch wall the drawing is pinned to, and the designer working under it.
     Done by SCALING THE BOX and letting the section's own overflow do the
     cutting (width 230%, pulled 10% left, which puts the frame's 4%..48% in
     the window) rather than by object-fit on the image: the box has to stay the
     painted frame, or every percentage the pinned sheet and the label are
     placed at would slide against the artwork. */
  html:not(.js) .studio,
  html.js .studio { width: 230%; margin-left: -10%; }

  /* ---- The warehouse at 390px ----
     A 2:1 diorama is 195px tall here, and both of its ENDS carry the beat (the
     racking the pallet came from, the door it is going to), so there is no crop
     of it that can be taken: it stays whole and letterboxed, top and bottom
     edges dissolved into the paper it is lying on, and the section's own height
     goes to air rather than to a bigger slice of a smaller picture.
     The label leaves the artwork entirely. On a 720px-tall picture the render's
     empty upper wall is a band a label sits comfortably inside; on a 195px one
     that same band is 55px, and a four-line label at 0.62rem runs its green
     rule straight across the racking. Above the picture, in the section's own
     paper, is clear by construction — the same move the hall's two labels make
     at this width. */
  .scene-label--aisle {
    left: 1rem;
    right: 1rem;
    top: -2.6rem;
    max-width: none;
  }

  /* JS is present but motion is off: same static composition as the no-JS
     case above, just under the html.js selector the animated rules (the
     general override above, and its mobile re-scale further down this file)
     also use. Re-asserted here rather than folded into the animated block so
     a later edit to that block can't accidentally start driving this static
     one too. */
  @media (prefers-reduced-motion: reduce) {
    html.js #factory-hall .svg-stage { padding: 3rem 0; }
    html.js .hall { aspect-ratio: 1680 / 1200; height: auto; }
    html.js .scene-label--line { left: 1rem; right: 1rem; max-width: none; top: -2.4rem; }
    html.js .bp--line { width: 14%; }
  }

  /* At 390px the lockup, "Our Services" and the Contact pill only just fit on
     one line; without the nowrap the link breaks in two and the whole sticky
     bar gets a row taller for the rest of the page. */
  .nav__logo img { width: 112px; }

  .nav > .wrap { gap: 0.75rem; }

  .nav nav { gap: 0.9rem; }

  .nav a { white-space: nowrap; font-size: 0.8rem; }

  a.nav__cta { padding: 0.55rem 1rem; }

  /* Scene sizes for the pinned versions at 390px. The desk and the pair are set
     by WIDTH here rather than height: a portrait viewport has height to spare
     and none to spare across, so the constraint that decides them is the
     opposite one to desktop's. The hall keeps being set by height, but a much
     smaller one — at desktop's 124% a 2:1 diorama on a 390px frame shows a
     slice about one machine wide, which is a pan through a keyhole. Gated
     exactly like the desktop overrides and the scenes' own matchMedia, so the
     mobile scrubbed layout can never disagree with the static composition
     reduced-motion actually renders. */
  @media (prefers-reduced-motion: no-preference) {
    /* Both scenes are landscape-ish objects in a tall frame, so sizing them by
       width leaves a lot of viewport under the heading. Lifting the stage off
       the bottom edge centres each composition in the space its section
       actually has, instead of parking it on the floor of the frame with the
       dead paper all above it. */
    html.js #sourcing-eval .svg-stage { bottom: 22vh; }

    html.js #handshake .svg-stage { bottom: 26vh; }

    /* Zoomed, and zoomed onto the half of the room this beat happens in: at
       390px the whole 1536-wide office is 260px tall and the sample table the
       sheet lands on is 100px of it. 170% pulled 55% left puts the frame's
       32%..91% in the window — the scorecard's right half, the sample table and
       the shelf — which is everything the sheet flies past and lands on.
       By SCALING THE BOX, not by object-fit on the image: the box has to stay
       the painted frame or the landing pose's own percentages would slide
       against the artwork. */
    html.js .desk { width: 170%; height: auto; margin-left: -55%; }

    html.js .desk__img { height: auto; }

    html.js .pair { width: 78%; height: auto; }

    html.js .pair__img { height: auto; }

    html.js .hall { height: 54%; bottom: 20%; }

    /* The pinned drawing inherits the desktop figure (6.5%, main.js:329) which
       on the panned mobile hall rendered at ~60x38px — legible as a shape but
       not as a sheet. Bumped, same as the static composition's .bp--line
       above, so both paths show a drawing that reads. */
    html.js .bp--line { width: 9%; }
  }

  .route-markers li { font-size: 0.7rem; padding: 0.35rem 0.8rem; }

  /* Fewer, larger-feeling clouds and a smaller jet: four cutouts at 390px read
     as clutter behind the closing headline. */
  .cloud--3, .cloud--4 { display: none; }

  .cloud--1 { left: -12%; top: 3%; width: 58%; }

  .cloud--2 { right: -10%; top: 12%; width: 52%; }

  #plane { top: 8%; width: 42%; }

  .process-steps { grid-template-columns: 1fr; padding-left: 4rem; column-gap: 0; }

  .process-step { grid-column: 1 !important; text-align: left !important; }

  .process-step h3 { justify-content: flex-start !important; }
}
