/* tokens.css */
/* =========================================================================
   Tokens — the whole visual system in one place.

   Colour comes ONLY from the eight swatches in "Mosagate colors + styles" —
   Colour A/A+, B/B+, C/C+, D/D+. Every surface, ink and rule below resolves to
   one of those eight, or to an alpha-blend of one of them. Nothing here is an
   invented hex value, and nothing is borrowed from the layout reference sites.
   ========================================================================= */

:root {
  /* --- Brand, exactly as specified ------------------------------------- */
  --a-plus: #00311f;   /* Colour A+ — set by user request, overrides the #004d2f in "Mosagate colors + styles" */
  --a: #00643f;        /* Colour A   MAIN */
  --b-plus: #2c7f27;   /* Colour B+  */
  --b: #38a032;        /* Colour B   MAIN */
  --c-plus: #cdd0b4;   /* Colour C+  */
  --c: #e5e5cc;        /* Colour C   MAIN */
  --d-plus: #3c3c3b;   /* Colour D+  */
  --d: #575756;        /* Colour D   MAIN */

  /* --- Surfaces ----------------------------------------------------------
     Dark grounds are Colour A itself, not a manufactured near-black: A+ for
     the hero/teasers/footer, A for the service band, so the two read as
     distinct bands while both stay literal brand colour. */
  --surface-dark: var(--a-plus);
  --surface-dark-2: var(--a);
  --surface-cream: var(--c);
  --surface-cream-2: var(--c-plus);

  /* --- Ink on those surfaces -----------------------------------------------
     Colour A/A+ are far brighter than a manufactured near-black, which leaves
     little contrast headroom: Colour C is the only swatch in the palette light
     enough to clear 4.5:1 against both, so it carries all on-dark body text
     directly (no alpha dimming). Colour C+ is the one swatch below it that
     still clears 4.5:1 against Colour A — the muted and faint tiers collapse
     into it, since there is no compliant third light tone below that. */
  --on-dark: var(--c);
  --on-dark-muted: var(--c-plus);
  --on-dark-faint: var(--c-plus);
  --on-cream: var(--d-plus);
  --on-cream-strong: var(--a); /* standard title colour on every cream section */
  --on-cream-muted: var(--d);

  /* --- Hairlines. The grid is drawn in rules, so these carry the layout --
     Decorative dividers, not text — held to Colour C/D's rgb at low alpha
     rather than the 4.5:1 text bar. */
  --rule-dark: rgba(229, 229, 204, 0.16);
  --rule-dark-strong: rgba(229, 229, 204, 0.32);
  --rule-cream: rgba(60, 60, 59, 0.18);
  --rule-cream-strong: rgba(60, 60, 59, 0.34);

  /* --- Accent. Used sparingly: active states, arrows, the open rule ----- */
  --accent: var(--b);
  --accent-deep: var(--b-plus);

  /* --- Type ------------------------------------------------------------- */
  --font: "Montserrat", system-ui, -apple-system, "Segoe UI", sans-serif;

  /* Sizes are the specified desktop anchors; they scale down through the
     viewport and stop at the anchor, never above it. */
  --t-hero: clamp(2.1005rem, 1.0815rem + 3.328vw, 3.1875rem); /* 51 / 46 */
  --t-hero-lh: 1;
  --t-page-title: clamp(1.75rem, 1.237rem + 1.692vw, 2.625rem); /* 42 / 46 */
  --t-page-title-lh: 1.095;
  --t-subtitle: clamp(1.146rem, 1.008rem + 0.458vw, 1.375rem);  /* 22 / 30 */
  --t-subtitle-lh: 1.364;
  --t-head: 1.25rem;                                     /* 20 / 26 · 600 */
  --t-head-lh: 1.3;
  --t-body: 0.9375rem;                                   /* 15 / 22 */
  --t-body-lh: 1.467;
  --t-disclaim: 0.8125rem;                               /* 13 / 19 */
  --t-disclaim-lh: 1.462;
  --t-micro: 0.6875rem;                                  /* 11 / 16 · 500 */
  --t-micro-lh: 1.455;

  --w-medium: 500;
  --w-semi: 600;

  /* Eyebrows and nav: the only tracked-out type on the site. */
  --track-eyebrow: 0.14em;
  --track-nav: 0.08em;

  /* --- Space ------------------------------------------------------------ */
  --gutter: clamp(1.25rem, 0.4rem + 3.4vw, 4rem);
  --measure: 1440px;
  --section-y: 90px;
  --header-h: 76px;

  /* --- Motion ----------------------------------------------------------- */
  /* One easing for everything that opens or moves. The accordion's 600ms is
     the reference; smaller things borrow the shorter step. */
  --ease: cubic-bezier(0.22, 0.61, 0.36, 1);
  --dur-panel: 600ms;
  --dur: 320ms;
  --dur-fast: 180ms;
}

@media (prefers-reduced-motion: reduce) {
  :root {
    --dur-panel: 1ms;
    --dur: 1ms;
    --dur-fast: 1ms;
  }
}

/* Fixed floors layered on top of the fluid clamp()s above — desktop stays
   on the clamp untouched; these breakpoints just raise the mobile/tablet
   plateau where the fluid ramp was reading too small. */
@media (max-width: 700px) {
  :root {
    --t-hero: 2.1875rem;      /* ~35px */
    --t-hero-lh: 1.15;
    --t-page-title: 1.9375rem; /* ~31px */
    --t-page-title-lh: 1.15;
    --t-subtitle: 1.25rem;    /* ~20px */
  }
}

@media (min-width: 701px) and (max-width: 900px) {
  :root {
    --t-hero-lh: 1.15;
    --t-page-title: 1.9375rem; /* ~31px */
    --t-page-title-lh: 1.15;
    --t-subtitle: 1.3125rem;  /* ~21px */
  }
}


/* base.css */
/* =========================================================================
   Base — reset, fonts, and the type scale from "Mosagate colors + styles".
   ========================================================================= */

/* Montserrat only. All three weights are subset to latin + latin-ext from
   the client's own TTFs; `swap` so text is readable before the font lands. */
@font-face {
  font-family: "Montserrat";
  src: url("/assets/fonts/montserrat-500.woff2") format("woff2");
  font-weight: 500;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Montserrat";
  src: url("/assets/fonts/montserrat-600.woff2") format("woff2");
  font-weight: 600;
  font-style: normal;
  font-display: swap;
}
/* Loaded for one spot only — the nav dropdown's hover state (header.css) —
   which is exactly why it's the only weight NOT also declared as a --w-*
   token in tokens.css: nothing else should reach for it. Before this, a
   sublink's hover already asked for font-weight:900, but with no face at
   700 or 900 loaded, the browser silently rendered it as this same 600 file
   — hover and rest were pixel-identical in weight, the "thicker" half of the
   effect was never actually visible. */
@font-face {
  font-family: "Montserrat";
  src: url("/assets/fonts/montserrat-700.woff2") format("woff2");
  font-weight: 700;
  font-style: normal;
  font-display: swap;
}

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

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
  /* Anchors land below the fixed header rather than under it. */
  scroll-padding-top: var(--header-h);
}

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

body {
  margin: 0;
  /* Was var(--surface-dark) (dark green) — light grey fallback instead, by
     request: this only ever shows in gaps/overscroll/letterboxing outside
     the page's own sections, never behind real content. */
  background: #eaeaea;
  color: var(--on-dark);
  font-family: var(--font);
  font-weight: var(--w-medium);
  font-size: var(--t-body);
  line-height: var(--t-body-lh);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  /* Was overflow-x: hidden — with no overflow-y set, that silently computes
     overflow-y to auto too (CSS's own rule: one axis non-visible forces the
     other out of visible), turning body into its own scroll container
     instead of the plain viewport. position:sticky descendants then stick
     relative to that instead of the real scroller — inconsistent across
     engines, and the kind of thing that can pass everywhere except real
     mobile Safari. clip clips the same horizontal overflow without
     establishing a scroll container at all, so there's nothing for the
     other axis to react to. */
  overflow-x: clip;
  /* Montserrat condensed to 95% width. Both self-hosted Montserrat files are
     static (see the @font-face rules above) — no 'wdth' axis for the browser
     to actually apply this against, so it is currently a no-op in every
     engine. It's set anyway so it takes effect the moment a variable
     Montserrat (or a true condensed static face) replaces the current files,
     with nothing to remember to come back and add then. .t-hero and
     .t-page-title reset it to normal: they're IBM Plex, not Montserrat. */
  font-stretch: 95%;
}

h1, h2, h3, h4, p, ul, ol, figure, blockquote {
  margin: 0;
}

ul, ol {
  padding: 0;
  list-style: none;
}

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

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

button {
  font: inherit;
  color: inherit;
  background: none;
  border: 0;
  padding: 0;
  cursor: pointer;
}

input, textarea {
  font: inherit;
  color: inherit;
}

/* Focus is never removed, only shaped. Sections set --focus per tone. */
:focus-visible {
  outline: 2px solid var(--focus, var(--accent));
  outline-offset: 3px;
  border-radius: 1px;
}

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

/* ------------------------------------------------------------------ type -- */

.t-hero {
  font-size: var(--t-hero);
  line-height: var(--t-hero-lh);
  font-family: "IBM Plex Serif", Georgia, serif;
  font-weight: var(--w-medium);
  letter-spacing: -0.4px;
  font-stretch: normal;
}

.t-page-title {
  font-size: var(--t-page-title);
  line-height: var(--t-page-title-lh);
  font-family: "IBM Plex Serif", Georgia, serif;
  font-weight: var(--w-medium);
  letter-spacing: -0.012em;
  text-wrap: balance;
  font-stretch: normal;
}

.t-subtitle {
  font-size: var(--t-subtitle);
  line-height: var(--t-subtitle-lh);
  font-weight: var(--w-medium);
  letter-spacing: -0.008em;
}

.t-head {
  font-size: var(--t-head);
  line-height: var(--t-head-lh);
  font-weight: var(--w-semi);
}

.t-body {
  font-size: var(--t-body);
  line-height: var(--t-body-lh);
  font-weight: var(--w-medium);
}

.t-body-strong {
  font-size: var(--t-body);
  line-height: var(--t-body-lh);
  font-weight: var(--w-semi);
}

.t-disclaim {
  font-size: var(--t-disclaim);
  line-height: var(--t-disclaim-lh);
  font-weight: var(--w-medium);
}

.t-micro {
  font-size: var(--t-micro);
  line-height: var(--t-micro-lh);
  font-weight: var(--w-medium);
}

/* The eyebrow: the one recurring signal that a new section has started. */
.eyebrow {
  display: flex;
  align-items: center;
  gap: 0.75em;
  font-size: var(--t-body);
  line-height: var(--t-body-lh);
  font-weight: var(--w-semi);
  letter-spacing: var(--track-eyebrow);
  text-transform: uppercase;
  color: var(--eyebrow-color, var(--accent));
}

/* --------------------------------------------------------------- helpers -- */

.container {
  width: 100%;
  max-width: var(--measure);
  margin-inline: auto;
  padding-inline: var(--gutter);
}

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

.skip-link {
  position: absolute;
  top: 0;
  left: 50%;
  translate: -50% -110%;
  z-index: 100;
  padding: 0.75rem 1.25rem;
  background: var(--c);
  color: var(--a-plus);
  font-size: var(--t-disclaim);
  font-weight: var(--w-semi);
}

.skip-link:focus-visible {
  translate: -50% 0;
}

.icon {
  width: 1em;
  height: 1em;
  flex: none;
  fill: none;
  stroke: currentColor;
}


/* sections.css */
/* =========================================================================
   Sections — tone switching, the editorial head, the rule grid, CTA bands.

   Every section declares a tone rather than hard-coding colours. That is what
   lets the fixed header invert itself as it passes over each one, and it keeps
   dark/cream variants of a component down to a single rule.
   ========================================================================= */

[data-tone="dark"] {
  --surface: var(--surface-dark);
  --ink: var(--on-dark);
  --ink-muted: var(--on-dark-muted);
  --ink-faint: var(--on-dark-faint);
  --rule: var(--rule-dark);
  --rule-strong: var(--rule-dark-strong);
  --eyebrow-color: var(--ink-muted);
  --focus: var(--ink);
}

[data-tone="dark-2"] {
  --surface: var(--surface-dark-2);
  --ink: var(--on-dark);
  --ink-muted: var(--on-dark-muted);
  --ink-faint: var(--on-dark-faint);
  --rule: var(--rule-dark);
  --rule-strong: var(--rule-dark-strong);
  --eyebrow-color: var(--ink-muted);
  --focus: var(--ink);
}

[data-tone="cream"] {
  --surface: var(--surface-cream);
  --ink: var(--on-cream);
  --ink-muted: var(--on-cream-muted);
  --ink-faint: var(--d);
  --rule: var(--rule-cream);
  --rule-strong: var(--rule-cream-strong);
  --eyebrow-color: var(--a);
  --focus: var(--a-plus);
}

[data-tone="cream-2"] {
  --surface: var(--surface-cream-2);
  --ink: var(--on-cream);
  --ink-muted: var(--on-cream-muted);
  --ink-faint: var(--d);
  --rule: var(--rule-cream);
  --rule-strong: var(--rule-cream-strong);
  --eyebrow-color: var(--a);
  --focus: var(--a-plus);
}

.section {
  background: var(--surface);
  color: var(--ink);
  padding-block: var(--section-y);
  position: relative;
}

.section--flush { padding-block: 0; }
.section--tight { padding-block: clamp(3rem, 1.6rem + 5vw, 6rem); }

/* Pulled up closer to the pinned hero above it than a full --section-y gap
   would sit. */
#chain {
  position: relative;
  overflow: hidden;
  /* Nav-click landing point (see main.js), pushed 50px lower than the
     header's own height (76px) — a click lands with 50px of the section
     already tucked behind the header instead of starting flush beneath it. */
  scroll-margin-top: 26px;
  padding-top: calc(clamp(2.5rem, 1.5rem + 3.5vw, 5rem) + 10px);
  /* Fixed 30px clearance below the CTA (10px base + 20px net added to grow
     the section's overall height while leaving the CTA's own position
     alone — the CTA itself carries the big push down toward the bottom of
     the photo, see #chain .cta). */
  padding-bottom: 30px;
}

/* Parallax image background — the STANDARD pattern for this site; see the
   header of js/parallax.js before adding another one.

   Real <img>, deliberately NOT object-fit:cover. Cover would scale to fill
   the section's height and spill the width — measured at 18.7% of the photo
   cropped off the sides. The photo's left/right edges are mid-green (#3c6b5f)
   so side gaps would show, but its top edge is #00311e — the section's own
   --a-plus ground — so a gap ABOVE the photo is invisible, no gradient
   needed to hide the join. That asymmetry is what this layout trades on:
   full width always, top gap free, bottom pinned (its bottom edge is bright
   floor, a gap there would be glaring).

   A video (mosagate-home-CTA.mp4) stood in for this photo for a while —
   its own top edge didn't match --a-plus the way structure.jpg's does, so
   a whole dynamically-measured gradient overlay existed just to fake that
   match back in (see git history on this file for the wreckage). Back on
   the static photo, none of that machinery is needed: the top just is the
   right colour. */
.chain__bg {
  position: absolute;
  inset: 0;
  overflow: hidden;
  z-index: 0;
}

.chain__bg-img {
  position: absolute;
  /* max-width overrides the sitewide `img { max-width: 100% }` reset in
     base.css, which would otherwise silently cap the 118% below. */
  max-width: none;
  /* 135.7% wide (118% + 15%), centred by the matching -17.85% offset (half
     the 35.7% overflow) rather than transform:translateX(-50%) — parallax.js
     writes `transform` on this element every frame and would clobber it.
     height:auto keeps the photo's own aspect, so the sides are never
     cropped. */
  width: 135.7%;
  height: auto;
  left: -17.85%;
  /* Sunk below the section's bottom edge — this is purely position (how
     high the photo's content sits in the frame), not the drift budget.
     Those two used to be the same number (bottom's own magnitude, read
     straight into parallax.js as maxTravel), which meant repositioning the
     photo silently resized the parallax effect as a side effect. They're
     independent now: --parallax-travel below is the drift budget,
     parallax.js reads that instead of `bottom`.

     -5px is where the photo's own content (the CTA's group of men) sits
     high enough in the frame that it doesn't sink out from under the CTA
     text near the bottom of the scroll-through (see git history — -100px
     and -50px both pushed it too low; this has moved up twice more since,
     30px then 25px, with no gap problems — see the note on
     --parallax-travel for why moving it further up keeps being safe). */
  bottom: -5px;
  /* The drift budget, at 4x the original 30px per explicit request,
     position and size left untouched. On paper this exceeds the sink
     above, which should risk lifting the photo's bottom edge above the
     section's during the upward half of the swing. Checked empirically
     rather than left as theory: that upward extreme only occurs at low
     scroll-progress, which is exactly when the section's bottom edge is
     still far below the viewport (the section is taller than the
     viewport, so its bottom can't be on-screen yet while its top is only
     just arriving). The unsafe number and the visible moment never
     coincide, confirmed by sampling gapAtBottom against secBottom's
     on-screen state across the full scroll range. Re-check this the same
     way if the section's height-vs-viewport relationship changes. */
  --parallax-travel: 120px;
  display: block;
  will-change: transform;
}

/* Tablet-only: the photo spans the frame exactly rather than the 135.7%
   overspill above. That overspill exists to give the horizontal parallax
   drift somewhere to travel without exposing an edge, so at this width the
   drift has no margin left — positioning first, per request; the parallax
   consequence is a known, deliberate trade to revisit after. left resets to
   0 in step with it, since -17.85% only existed to re-centre the overspill.
   Paired with structure-responsive.jpg (see the <picture> in home.html),
   whose taller crop still covers the section at this width. */
@media (min-width: 701px) and (max-width: 900px) {
  /* 100px taller than the shared 30px. Added as padding-bottom so the extra
     height lands below the CTA and nothing above it moves. Note .chain__bg
     is inset:0 on this section, so the photo (bottom-anchored, below) drops
     with the new bottom edge rather than staying put. */
  #chain { padding-bottom: 110px; }

  .chain__bg-img {
    /* 22% of overspill, re-centred by the matching -11% (half of it). */
    width: 122%;
    left: -11%;
    /* 25px above the section's bottom edge. Dropped by the same 30px the
       section's padding-bottom above lost: this photo is anchored to that
       bottom edge, so shrinking the section alone would just carry it up
       and leave the same green strip. Moving both keeps the photo where it
       is on screen and takes the strip from 55px to 25px. */
    bottom: -5px;
  }
}

/* Mobile-only: 20% larger than the shared 135.7% (162.8%), then a further
   5% by request (162.8% * 1.05 = 170.9%), re-centred by the matching
   -35.45% (half the 70.9% overspill). bottom kept at 0 by request — flush
   with the section's own bottom edge, not sunk below it like the shared
   -5px. */
@media (max-width: 700px) {
  .chain__bg-img {
    width: 170.9%;
    left: -35.45%;
    bottom: 0px;
  }
}

#chain > .container {
  position: relative;
  z-index: 1;
}

/* Moves the whole CTA block — divider included — down near the bottom of
   the background photo instead of sitting right under the KPIs. */
/* Scoped to #chain only — the other four .cta blocks sit on flat dark
   surfaces, not this section's photo, and don't need lifting off anything. */
#chain .cta {
  margin-top: calc(clamp(6rem, 3rem + 10vw, 12rem) + 40px);
}

/* ID beats the sitewide [data-tone="dark"] :is(h1,h2,h3){color:#fff} rule.
   margin-top: 1.25rem matches the sitewide eyebrow-to-title gap (see
   .split-50__title and #presence-title). */
#chain-title { color: var(--c); margin-top: 1.25rem; padding-bottom: 50px; word-spacing: 0.3em; }

/* Tablet-only: get the four words onto one line. At 768px the line needs
   711px against 703px of container — it wraps by a mere 8px, putting "Grow."
   alone on a second line. Dropping the sitewide 0.3em word-spacing back to
   normal reclaims ~35px on its own, and -4px on the type another ~60px, so
   it lands with room to spare rather than sitting on the exact edge where a
   font swap or a longer translation would wrap it again. Both are scoped
   here: desktop keeps the tracked-out look the 0.3em was chosen for, and
   phones (where it wraps regardless) are untouched. */
@media (min-width: 701px) and (max-width: 900px) {
  #chain-title {
    font-size: calc(var(--t-hero) - 4px);
    word-spacing: normal;
  }
}

/* Presence keeps the default --section-y padding on every side, same "head
   space" as every other section (no hairline, no shared cream ground with
   the Why Mosagate content — now folded into #mosagate, see home.html).
   Same gradient formula as #mosagate, computed independently on its own
   box. */
#presence { background: linear-gradient(to top, color-mix(in srgb, var(--c) 60%, white) 0%, #fff 100%); }

/* Same gradient formula as #presence/#mosagate — Contacts is the last cream
   section on the page and gets the same soft lift off its own top edge. */
#contacts { background: linear-gradient(to top, color-mix(in srgb, var(--c) 60%, white) 0%, #fff 100%); }

/* -------------------------------------------------------------- services -- */
/* Full-bleed background photo, fixed section height (1000px), image at
   100% cover — simpler than #chain's oversized/non-cover approach (see
   .chain__bg / .chain__bg-img above) because there's no parallax drift
   here needing spare width/height to move around in, and no bottom-pinned
   content whose edges would show through crop the way #chain's do. Plain
   object-fit: cover fills the fixed box directly. */
/* Top-aligned at --section-y (90px), the same gap #mosagate's own eyebrow
   sits at below its section top — not vertically centered in the fixed
   height. Centering measured out mathematically equal (verified directly,
   not guessed), but the photo's own weight sits low in its frame, so equal
   px above/below still read as unbalanced; anchoring to the sitewide top
   gap fixes the feel and matches every other section's rhythm at the same
   time. */
#services {
  overflow: hidden;
  height: 1030px;
  padding-top: var(--section-y);
}

/* Mobile: the fixed 1030px above is desktop-scale — on a narrow column the
   text/list stack takes far less vertical room, leaving ~490px of dead
   background below "Grow" with nothing in it. Cut down to fit. */
@media (max-width: 700px) {
  #services {
    height: 1000px;
  }
}

.services__bg {
  position: absolute;
  inset: 0;
  overflow: hidden;
  z-index: 0;
}

/* width:100%/height:100%/object-fit:cover removed for now — that forced
   crop-to-fill was what pushed the photo's bottom row away from its true
   edge (see git history / parallax.js), exposing a hard seam against
   #consulting below that no amount of scale/shift tuning fixed cleanly.
   Plain natural-aspect width instead: no forced vertical crop, so nothing
   to mismatch. Revisit once the photo itself (or a replacement) has a
   bottom edge actually meant to blend into #consulting's tone.

   132% wide, centred by the matching -16% offset (half the 32% overflow)
   — same technique as .chain__bg-img — top-aligned (no vertical shift), so
   height:auto just grows the image down from the section's own top. Was
   120%/-10% originally: the image's own aspect-locked height fell ~176px
   short of the section's fixed 1030px even at rest, worse (up to 296px) at
   the far end of the parallax swing — flat background colour showing below
   the photo. Tried 140%/-20% to close most of that, but that overshot —
   the image now large enough that its top edge, at the swing's other
   extreme, pushed into the section above instead. 132% targets a ~200px
   worst-case gap specifically, smaller than the original 296px but with
   margin on both ends rather than closing it as far as possible. */
.services__bg-img {
  position: absolute;
  top: 0;
  left: -16%;
  /* max-width overrides the sitewide `img { max-width:100% }` reset in
     base.css, which would otherwise silently cap the 132% below — same
     fix .chain__bg-img needs, missed here the first time (the gap on the
     right at 100%-capped-but-shifted-left offset was exactly this bug). */
  max-width: none;
  width: 132%;
  height: auto;
  display: block;
  /* Same drift budget as .chain__bg-img, by request — parallax.js writes
     transform directly here now (see its own #services block), same
     translate3d pattern, since there's no static base transform left to
     compose with any more (removed along with object-fit:cover above). */
  --parallax-travel: 120px;
  will-change: transform;
}

/* Tablet: 7% smaller — left kept at half the (now smaller) overflow so it
   stays centred the same way as the base rule above. */
@media (min-width: 701px) and (max-width: 900px) {
  .services__bg-img {
    width: 122.76%;
    left: -11.38%;
  }
}

/* Mobile: 25% larger — left kept at half the (now larger) overflow so it
   stays centred the same way as the base rule above. */
@media (max-width: 700px) {
  .services__bg-img {
    width: 165%;
    left: -32.5%;
    top: 130px;
  }
}

#services > .container {
  position: relative;
  z-index: 1;
}

/* Centred intro + the "choose your interest" menu below it, replacing the
   old two-column .section-head (eyebrow beside title) — everything here
   reads as one stack over the photo, not an aside/argument split. */
.services__inner {
  display: grid;
  justify-items: center;
  text-align: center;
  max-width: 900px;
  margin-inline: auto;
}

/* Eyebrow through "Choose Your Interest" pushed 10px lower, by request —
   the interest list right after it keeps its own original position via a
   matching -10px on its own margin-top below, so only the text above it
   moves. */
#services .eyebrow {
  margin-top: 10px;
}

/* .services__inner widened to 900px so the title (52px serif) has room to
   settle into 2 lines instead of 3 — but the lead/list below it stay
   readable at that width via their own narrower max-width (see
   .services__lead's 46ch; the interest words are short regardless). */
.services__title {
  max-width: 22ch;
}

.services__title { margin-top: 1.25rem; }

/* Forced breaks inside the title (see content.json), mobile-only — off
   everywhere else so it wraps naturally there. services-intro.js's
   scramble effect walks past <br> elements untouched (see its wrapIntoWords
   comment), so this is safe to combine with that. */
.services__title-break {
  display: none;
}

@media (max-width: 700px) {
  .services__title-break {
    display: inline;
  }

  /* The 22ch cap above was sized for natural wrapping — with explicit
     breaks now doing that job, it could clip a manually-broken line
     that's slightly over 22ch's own measured width. */
  .services__title {
    max-width: none;
  }
}

/* ID beats the sitewide [data-tone="dark"] :is(h1,h2,h3){color:#fff} rule —
   same reason #chain-title needs the ID and not just its class. */
#services-title { color: var(--c); }

.services__lead {
  margin-top: 1rem;
  color: #fff;
  max-width: 46ch;
  /* Overrides .t-head's own var(--w-semi) (600) — 500 here specifically. */
  font-weight: var(--w-medium);
}

/* Forced break inside the lead text (see content.json), tablet-only — off
   everywhere else so the text just wraps naturally there. */
.services__lead-break {
  display: none;
}

@media (min-width: 701px) and (max-width: 900px) {
  .services__lead-break {
    display: inline;
  }
}

/* Eyebrow overridden to full white here — the sitewide [data-tone="dark"]
   default (--ink-muted, a dimmed grey) is deliberately muted everywhere
   else, but this section wants eyebrow + subtitle reading as one strength
   of white together. */
#services .eyebrow { color: #fff; }

/* Sizing/weight now come from the .t-body class on the element itself (see
   home.html) rather than being set here — this rule only carries what's
   specific to it: the label's own spacing, its caps treatment, and its
   colour. */
/* flex column, not text-align:center on a plain <p> — the arrow icon wraps
   onto its own line below the text, and an inline element alone on a
   wrapped line does NOT reliably center under a block's text-align in
   every case; measured directly here and it was landing ~111px off the
   label's own center. Flex with align-items:center centers each child
   (the text span, then the icon) independently and reliably. */
.services__interest-label {
  margin-top: calc(clamp(1.25rem, 0.8rem + 2vw, 2.25rem) - 10px);
  display: flex;
  flex-direction: row;
  align-items: center;
  justify-content: center;
  gap: 0.4em;
  letter-spacing: 0;
  /* opacity, not a translucent text colour — applies uniformly to the
     icon too (stroke:currentColor would otherwise need its own alpha
     kept in sync by hand). */
  opacity: 0.8;
  color: #fff;
}

/* Thicker than the sitewide default (1.25, see icons.svg) — at this icon's
   small size next to the label text, the default stroke read as too thin. */
.services__interest-label .icon {
  --icon-stroke-width: 2.5;
}

.services__interest-list {
  /* Was 0.75rem + 10px (added for breathing room below the arrow), then
     10px less again — back to the original 0.75rem, offset -10px to cancel
     out #services .eyebrow's own +10px so this list didn't drift down with
     the text above it. +15px now on top of that, by request — this group
     itself moving down, independent of the text above it. */
  margin-top: calc(0.75rem - 10px + 15px);
  display: grid;
  gap: 0.25em;
  justify-items: center;
}

/* Tablet: label lowered 20px (the list follows it in flow, so this moves
   both as one group). */
@media (min-width: 701px) and (max-width: 900px) {
  .services__interest-label {
    margin-top: calc(clamp(1.25rem, 0.8rem + 2vw, 2.25rem) - 10px + 20px);
  }
}

.services__interest-link {
  display: block;
  text-decoration: none;
  /* The flip rotates the face in 3D rather than crossfading — perspective
     on the link (not the flipping element itself) is what gives that
     rotation its vanishing point instead of looking like a flat squash. */
  perspective: 800px;
}

.services__interest-flip {
  display: block;
  position: relative;
  transform-style: preserve-3d;
  transition: transform 420ms var(--ease);
}

.services__interest-link:hover .services__interest-flip,
.services__interest-link:focus-visible .services__interest-flip {
  transform: rotateX(180deg);
}

/* text-shadow lives here, on the shared base class, so both faces get it
   from one declaration rather than repeating it per style — same shadow
   #chain's own CTA text uses (see #chain .cta__title), since this sits
   over the same kind of busy photo and needs the same lift. */
.services__interest-face {
  display: block;
  backface-visibility: hidden;
  /* 8px below the sitewide --t-page-title now (4px, then another 4px on
     top per follow-up), here only — calc() off the token keeps the same
     responsive clamp curve, just shifted down by a constant offset at
     every viewport width instead of picking new clamp anchors by hand. */
  font-size: calc(var(--t-page-title) - 8px);
  /* Pinned to the PREVIOUS (-4px) size × the sitewide ratio, not the
     current -8px size — "keep the existing line-height" on the second
     reduction meant the line-height that was in place before this size
     drop, not one that shrinks again in step with it. line-height's
     unitless var(--t-page-title-lh) always multiplies against this
     element's own font-size, so keeping it in lockstep would need it
     computed off the old size explicitly, not the live one. */
  line-height: calc((var(--t-page-title) - 4px) * var(--t-page-title-lh));
  font-family: "IBM Plex Serif", Georgia, serif;
  font-weight: var(--w-medium);
  color: var(--c);
  text-shadow:
    0 4px 16px rgba(0, 49, 31, 1),
    0 2px 6px rgba(0, 49, 31, 1);
}

/* Tablet: button text sized up 3px. Line-height bumped further past the
   same-relationship value (var(--t-page-title) - 1px) — a bit looser than
   strictly proportional, by request. Placed after the base rule above so
   equal-specificity cascade order doesn't let it win back over this at
   tablet widths. */
@media (min-width: 701px) and (max-width: 900px) {
  .services__interest-face {
    font-size: calc(var(--t-page-title) - 2px);
    line-height: calc((var(--t-page-title) + 8px) * var(--t-page-title-lh));
  }
}

/* Mobile: button text sized up 3px, with line-height shifted by the same
   3px so it stays in the same relationship to the font-size as the base
   rule above (same idea as the tablet block above, its own numbers). */
@media (max-width: 700px) {
  .services__interest-face {
    font-size: calc(var(--t-page-title) - 5px);
    line-height: calc((var(--t-page-title) - 1px) * var(--t-page-title-lh));
  }
}

/* Absolutely positioned over the front face (not stacked in flow) so the
   link's height comes from the front word alone — the two words are
   different sizes/cases and letting the back one contribute height too
   would make the list jump around as it flips. Pre-rotated 180deg so that
   after the parent's own 180deg flip it lands right-reading, not mirrored. */
.services__interest-face--back {
  position: absolute;
  inset: 0;
  /* flex, not grid: an overflowing nowrap text node inside display:grid;
     place-items:center measured out NOT centered here — the anonymous
     item just overflowed entirely to one side instead of splitting past
     the box symmetrically (checked directly against the rendered text's
     own bounding box, not just the element's). justify-content:center on
     a flex container centers overflowing content correctly. */
  display: flex;
  align-items: center;
  justify-content: center;
  transform: rotateX(180deg);
  /* The box itself is only as wide as the front word (see the comment
     above) — "Global Commerce" is wider than "Supply", so without this
     it wraps to two lines inside that narrower box. nowrap keeps every
     division name on one line, overflowing centered past the box's own
     width rather than breaking. */
  white-space: nowrap;
  font-size: var(--t-subtitle);
  line-height: var(--t-subtitle-lh);
  /* Overrides the serif from .services__interest-face — the front face is
     .t-page-title's IBM Plex Serif, but this back face is .t-subtitle's
     styling, which is the sitewide default (Montserrat), not serif. */
  font-family: var(--font);
  font-weight: 700;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  color: #fff;
}

/* Headings sit brighter than body copy on both tones. */
[data-tone="cream"] :is(h1, h2, h3),
[data-tone="cream-2"] :is(h1, h2, h3) { color: var(--on-cream-strong); }
[data-tone="dark"] :is(h1, h2, h3),
[data-tone="dark-2"] :is(h1, h2, h3) { color: #fff; }

/* ------------------------------------------------------------ section head */
/* Eyebrow in a narrow left column, the argument in a wide right one. The copy
   document is written eyebrow → title → lead, so the markup follows it. */

.section-head {
  display: grid;
  grid-template-columns: minmax(0, 3fr) minmax(0, 8fr);
  gap: clamp(1.5rem, 0.6rem + 3vw, 4rem);
  align-items: start;
}

.section-head__aside { padding-top: 0.4rem; }

.section-head__title {
  max-width: 20ch;
}

.section-head__lead {
  margin-top: clamp(1.25rem, 0.8rem + 1.4vw, 2rem);
  max-width: 62ch;
  color: var(--ink-muted);
  display: grid;
  gap: 1.1em;
}

.section-head__lead > p:first-child {
  color: var(--ink);
  font-size: var(--t-subtitle);
  line-height: var(--t-subtitle-lh);
  max-width: 34ch;
}

.section-head__lead--plain > p:first-child {
  color: var(--ink-muted);
  font-size: var(--t-body);
  line-height: var(--t-body-lh);
  max-width: none;
}

/* International Presence reuses Why Mosagate's lead-highlight treatment: the
   first paragraph reads in the accent green, not just larger and dark. */
#presence .section-head__lead > p:first-child,
#contacts .section-head__lead > p:first-child,
#privacy .section-head__lead > p:first-child {
  color: var(--b-plus);
}

@media (max-width: 860px) {
  .section-head { grid-template-columns: 1fr; gap: 1.25rem; }
  .section-head__title { max-width: none; }
}

/* -------------------------------------------------------------- split-50 */
/* Two even columns. The right column starts level with the subtitle, not the
   eyebrow — a second grid row carries the subtitle and the body text side by
   side, so the alignment holds without hand-measured offsets. */

.split-50 {
  display: grid;
  grid-template-columns: 45fr 55fr;
  grid-template-areas:
    "head  .   "
    "sub   body";
  column-gap: clamp(1.5rem, 0.75rem + 3vw, 3.75rem);
  row-gap: clamp(1rem, 0.6rem + 1.5vw, 1.75rem);
}

.split-50__head { grid-area: head; }

.split-50__title {
  margin-top: 1.25rem;
  max-width: 18ch;
}

.split-50__subtitle {
  grid-area: sub;
  align-self: start;
  color: var(--b-plus);
  font-size: var(--t-subtitle);
  line-height: var(--t-subtitle-lh);
  font-weight: var(--w-medium);
  letter-spacing: -0.008em;
  max-width: 30ch;
}

/* Tablet: widened so the lead wraps to 2 lines instead of 3. */
@media (min-width: 701px) and (max-width: 900px) {
  .split-50__subtitle {
    max-width: 42ch;
  }
}

.split-50__body {
  grid-area: body;
  align-self: start;
  display: grid;
  gap: 1.1em;
  color: var(--ink-muted);
  max-width: 60ch;
}

@media (max-width: 860px) {
  .split-50 {
    grid-template-columns: 1fr;
    grid-template-areas: "head" "sub" "body";
  }
}

/* Mosagate, Why Mosagate and Presence each carry their own independent
   bottom-to-top gradient (Colour C at 60%, mixed with white, fading to
   white) — not one gradient spanning the three, each section computes its
   own from its own box. Mosagate's softens the solid cream ground into the
   dark section above it instead of cutting hard; body background (dark)
   shows through where it goes transparent, so the blend reads as
   intentional rather than as a missing background. */
#mosagate {
  background: linear-gradient(to top, color-mix(in srgb, var(--c) 60%, white) 0%, #fff 100%);
}

/* The pattern divider is this section's own first child now (see home.html)
   so the section starts at the top of the pattern rather than the container
   below it. Cancel the section's own top padding so the divider (wrapped in
   #mosagate-gap, see .pattern-divider--reveal below) sits flush against
   #chain above, and put a gap back between the divider and the container —
   45px (half the sitewide --section-y) by request, not the full value. */
#mosagate { padding-top: 0; }
#mosagate > .container { margin-top: 45px; }

/* Overrides the sitewide scroll-padding-top (base.css) fallback of one
   header height, lowering a nav-click landing by that same 76px. */
#mosagate { scroll-margin-top: 0; }

#mosagate-title {
  color: var(--a);
}

/* ---------------------------------------------------- mosagate-intro.js === */
/* Same two effects as the hero/chain entrances, reused here under
   section-neutral names since this drives both the title scramble and the
   KPI value scramble below (see mosagate-intro.js). */

/* Letter-scramble mechanism — same as .hero-scramble (hero.css) /
   .carousel__scramble (carousel.css): each character flickers through
   random glyphs and locks into its real one as a sweep passes it. Opacity is
   set inline by mosagate-intro.js, not by a class pair, so this only carries
   the transition that turns each glyph swap into an ease rather than a snap. */
.scramble-word { display: inline-block; }
.scramble-char { transition: opacity 150ms var(--ease); }

/* Fade-up entrance — same underlying shape as .hero__lead--intro (hero.css)
   / .carousel__card--intro (carousel.css): opacity finishes a beat before
   the rise settles, rather than the two locked to one timeline. Made more
   pronounced than those two by request (About Us only, not the hero) — a
   taller rise and a longer settle, rather than just a quicker/snappier
   version of the same distance. If mosagate-intro.js never runs (or is
   skipped, reduced motion), nothing here ever gets this class, so the
   element stays exactly as styled above — never silently stuck hidden. */
.fade-up-intro {
  opacity: 0;
  translate: 0 40px;
  transition: opacity 750ms var(--ease), translate 1050ms var(--ease);
}

.fade-up-intro.is-lit {
  opacity: 1;
  translate: 0 0;
}

/* Horizontal counterparts, same shape as .fade-up-intro just on the X axis —
   used by service-intro.js for the carousel's image/copy pair: the image
   comes in from the right, the copy from the left, crossing past each other
   into their resting columns rather than both rising from below. */
.fade-right-intro {
  opacity: 0;
  translate: 60px 0;
  transition: opacity 750ms var(--ease), translate 1050ms var(--ease);
}

.fade-right-intro.is-lit {
  opacity: 1;
  translate: 0 0;
}

.fade-left-intro {
  opacity: 0;
  translate: -60px 0;
  transition: opacity 750ms var(--ease), translate 1050ms var(--ease);
}

.fade-left-intro.is-lit {
  opacity: 1;
  translate: 0 0;
}

/* Presence, Contacts and the standalone Privacy Policy page all get the
   exact same divider-as-first-child treatment as #mosagate above:
   padding-top cancelled so the divider sits flush at the section's own top,
   that same 45px put back between the divider and the container, and
   scroll-margin-top: 0 so a nav-click (or, for #privacy, the page simply
   loading under the header) lands with the pattern flush under the header
   instead of 76px short. None of the three needs its own gate — nothing
   photographic sits directly above any of them. */
#presence,
#contacts,
#privacy {
  padding-top: 0;
  scroll-margin-top: 0;
}

#presence > .container,
#contacts > .container,
#privacy > .container {
  margin-top: 45px;
}

/* ---------------------------------------------------------------- why-grid */

.why-grid__lead {
  display: grid;
  gap: 1.1em;
  color: var(--ink-muted);
  max-width: 60ch;
}

.why-grid__lead-highlight {
  color: var(--b-plus);
  max-width: none;
}

/* Former "Why Mosagate" block, now living inside #mosagate alongside the
   About Us copy above it (see home.html). No .split-50 grid any more —
   title, KPI row and lead paragraphs stack as plain block-flow children of
   .why-block, each centred on its own (see the rules below) now that
   there's no accordion beside the lead to justify a two-column template.
   It has no eyebrow of its own (see home.html) since "Why Us" is no longer
   a separate destination. */
.why-block {
  margin-top: calc(var(--section-y) / 2 + 20px);
}

.why-block .split-50__head,
.why-block .why-grid__lead {
  text-align: center;
  margin-inline: auto;
  padding-inline: 60px;
}

.why-block .split-50__title {
  max-width: 34ch;
  /* text-align:center (above) only centers the TEXT inside this box, not the
     box itself; a block element with its own max-width needs its own
     margin-inline:auto to sit centred in its (full-width) parent. */
  margin-inline: auto;
}

.why-block .why-grid__lead {
  max-width: 90ch;
}

@media (max-width: 700px) {
  .why-block .split-50__head,
  .why-block .why-grid__lead {
    padding-inline: 20px;
  }
}

/* ------------------------------------------------------------ why-pillars */
/* Five items, 3-then-2, the shorter second row centered under the first
   rather than left-aligned. Items are still fixed-width, not flex-grow
   (see .why-pillars__item below) — that's what keeps each item's own
   internal content (icon, title, text) a consistent size across both rows,
   centering aside. Flex-wrap rather than a grid so the row still reflows
   cleanly at any width, mobile's single-column fallback included. */
.why-pillars {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 58px 46px;
  /* Wide enough for 3 x 280px fixed-width items plus their gaps
     (3 x 280 + 2 x 46 = 932px) — 90ch (the lead paragraph's own width,
     right above this) is too narrow and was wrapping to 2-per-row even on
     a wide desktop viewport. */
  max-width: 1060px;
  margin: calc(var(--section-y) / 2) auto 0;
  padding-inline: 60px;
}

@media (max-width: 700px) {
  .why-pillars { padding-inline: 30px; row-gap: 43px; }
}

/* Tablet: switched to an explicit 2-column grid instead of flex-wrap —
   with only 5 items, a 2-col grid auto-places the leftover 5th one in
   column 1 (left) of its own row rather than centering it like flex-wrap
   does. Columns narrowed from the base 280px too. */
@media (min-width: 701px) and (max-width: 900px) {
  .why-pillars {
    display: grid;
    grid-template-columns: repeat(2, 250px);
    justify-content: center;
    column-gap: 20px;
  }
}

.why-pillars__item {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  /* Fixed, not 1 1 240px — no grow, no shrink, so every item is the same
     width whether it's sharing a row with two others or just one. That's
     what keeps the second row's items under the first row's columns. */
  flex: 0 0 280px;
}

.why-pillars__icon {
  width: 44px;
  height: 44px;
  color: var(--b-plus);
  margin-top: 15px;
  margin-bottom: 14px;
}

.why-pillars__title {
  color: var(--a-plus);
  margin-bottom: 0.4em;
}

.why-pillars__text {
  color: var(--d);
}

/* ------------------------------------------------------------ figure slot */
/* PLACEHOLDER IMAGERY. The tint is applied here rather than baked into the
   files, so final photography drops in without inheriting a treatment. */

.figure {
  position: relative;
  overflow: hidden;
  background: var(--a-plus);  /* image-loading backdrop; never actually seen */
}

.figure img,
.figure svg {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

.figure--tinted img {
  filter: saturate(0.72) contrast(1.04);
}

/* A green wash ties the blue-hour footage to the brand without recolouring
   the source material. */
.figure--tinted::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(180deg, rgba(0, 49, 31, 0.24), rgba(0, 49, 31, 0.4));
  mix-blend-mode: multiply;
  pointer-events: none;
}

.figure--wide { aspect-ratio: 16 / 7; }

.figure--portrait { aspect-ratio: 3 / 4; }

/* International Presence's map is client-supplied artwork (an <img>, not
   this .figure system) — see #presence in home.html. Right-aligned in its
   column rather than stretched, since it doesn't share the text column's
   left edge; top-aligned with the lead paragraph beside it (grid-area
   "map"), not the eyebrow above. Sized ~21% over the source file's native
   589px (max-width:100% from the sitewide img reset still caps it on
   narrow columns). margin-right (not padding — padding on a replaced
   element with box-sizing:border-box would shrink the visible image into
   the gap instead of just spacing it) holds it off the container edge. */
.presence__map {
  display: block;
  grid-area: map;
  justify-self: end;
  align-self: start;
  width: 784px;
  margin-top: -30px;
  margin-right: 15px;
}

/* Mobile: drop the right-aligned/gutter-boxed treatment above for a flat
   15px left edge and a ~20% larger map — deliberately left unbounded on the
   right so it runs past the viewport edge; body's overflow-x:clip (base.css)
   trims it rather than a scrollbar appearing. */
@media (max-width: 700px) {
  #presence {
    padding-bottom: calc(var(--section-y) / 2);
  }

  #presence .split {
    /* Bare 1fr (set at the 860px breakpoint above) sizes the track to fit
       its widest child's content — including the map's own explicit width
       below — which was stretching the head/lead text's cell out to match
       and breaking their wrapping. minmax(0, 1fr) caps the track at the
       container width regardless of content, the standard fix for this
       "grid blowout" behaviour. */
    grid-template-columns: minmax(0, 1fr);
    /* Belt-and-suspenders alongside .presence__map's min-width:0 below —
       clips the oversized map locally instead of relying on the
       sitewide body overflow-x:clip to contain it. */
    overflow-x: hidden;
  }

  .presence__map {
    justify-self: start;
    max-width: none;
    /* Pre-mobile-fix this was already filling 100% of the column (784px
       always exceeded the container width, so the sitewide max-width:100%
       cap was permanently active) — so this is relative to that column,
       not to the uncapped 784px. */
    width: 113%;
    /* The img's HTML height="417" attribute otherwise wins as the default
       CSS height once width is overridden without a matching height,
       locking it at the desktop-scale 417px regardless of the new width
       and stretching the map out of proportion. */
    height: auto;
    min-width: 0;
    /* The base rule's -30px pulls the map up to align with the lead text
       when they share a row (desktop/tablet's side-by-side "lead map"
       area) — on mobile "map" is its own stacked row, so that pull just
       eats into the row's auto height instead and throws off the section's
       bottom padding. */
    margin-top: 0;
    margin-left: calc(35px - var(--gutter));
    margin-right: 0;
  }
}

/* Tablet: centered instead of right-aligned/gutter-boxed, and 15% smaller. */
@media (min-width: 701px) and (max-width: 900px) {
  #presence {
    padding-bottom: calc(var(--section-y) - 20px);
  }

  .presence__map {
    justify-self: center;
    width: 85%;
    /* Same fix as mobile above — without this the img's HTML height="417"
       attribute wins once width is overridden, stretching the map. */
    height: auto;
    min-width: 0;
    margin-right: 0;
    /* Base rule's -30px, lowered a total of 50px. */
    margin-top: 20px;
    /* Shifts right off center by 20px. */
    margin-left: 20px;
  }
}

/* ------------------------------------------------------------- split band */

/* Same "head ." / "sub body" two-row template as .split-50 (#mosagate) — the
   right column (here, the map) starts level with the lead paragraph, not
   the eyebrow — but its own 40/60 column ratio, since the map needs more
   room than Mosagate's text column does. */
.split {
  display: grid;
  grid-template-columns: 40fr 60fr;
  grid-template-areas:
    "head .  "
    "lead map";
  column-gap: clamp(0.75rem, 0.375rem + 1.5vw, 1.875rem);
  row-gap: clamp(1rem, 0.6rem + 1.5vw, 1.75rem);
}

.split__head { grid-area: head; }
/* No margin-top here (unlike .section-head__lead's usual spot right under a
   title) — it's a separate grid row now, and row-gap already spaces it from
   .split__head; the margin would only throw off the top-alignment with the
   map beside it. */
.split__lead { grid-area: lead; align-self: start; margin-top: 0; }

@media (max-width: 860px) {
  .split {
    grid-template-columns: 1fr;
    grid-template-areas: "head" "lead" "map";
  }
}

/* #contacts stacks lead and details under the head, in the same left
   column. The form spans the lead + details rows in the right column, so
   its top aligns with the lead paragraph rather than the address block —
   raised up from level-with-address by request. */
.split--contacts {
  grid-template-areas:
    "head    .   "
    "lead    form"
    "details form";
}

@media (max-width: 860px) {
  .split--contacts { grid-template-areas: "head" "lead" "details" "form"; }
}

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

/* One line — title then button, side by side rather than stacked — aligned
   on text baseline, not box edge. flex-end lines up the button's and
   title's *boxes*, but the title's line-height leading extends visibly
   below its own last glyph, so the box-bottom sits lower than the text
   looks — baseline lines up where the text itself actually sits, which is
   what "bottom aligned" looks like to the eye. flex-wrap so a long title +
   button still wraps to stacked on narrow viewports instead of
   overflowing. */
.cta {
  border-top: 2px solid var(--rule);
  padding-top: clamp(3rem, 1.8rem + 4.5vw, 5.5rem);
  /* 60% of the top padding — title/button/bottom sit closer together than
     the divider does to the title. */
  padding-bottom: clamp(1.8rem, 1.08rem + 2.7vw, 3.3rem);
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  align-items: baseline;
  gap: clamp(0.6rem, 0.32rem + 1vw, 1.4rem);
  text-align: center;
}

.cta .cta__title {
  max-width: 60ch;
  color: #fff;
  font-weight: var(--w-medium);
}

/* #chain's .cta sits directly on the photo (the KPI row that used to precede
   it, and absorb this border, now lives in Mosagate) — a hard rule across
   the image reads as a stray line rather than a divider, so it's dropped
   here only. */
#chain .cta { border-top: 0; }

/* Only #chain sits on a photo — the other four .cta titles are on flat dark
   surfaces and don't need lifting. Two layers, not one bigger blur: the
   original 4px/10px alone was already at full opacity (rgba alpha 1), so
   there was no more darkness to add by turning that shadow up — a tight
   2px/6px shadow underneath adds density right at the letterforms instead,
   stacking with the wider one rather than replacing it. */
#chain .cta__title {
  text-shadow:
    0 4px 16px rgba(0, 49, 31, 1),
    0 2px 6px rgba(0, 49, 31, 1);
}

/* Tablet-only. Down here rather than with the other #chain tablet rules
   earlier in this file: both selectors below also exist above at the same
   specificity, so an override has to follow them to win. */
@media (min-width: 701px) and (max-width: 900px) {
  /* 155px lower than the shared value — same clamp curve, bigger constant. */
  #chain .cta {
    margin-top: calc(clamp(6rem, 3rem + 10vw, 12rem) + 155px);
    /* Same arrangement as .service__cta (services.css): a centred ROW —
       title and button side by side, not stacked. Centring is horizontal
       only (justify-content, inherited from the base rule); align-items
       just replaces the shared `baseline`, which had pinned the button to
       the first line's baseline and sat it high against a two-line title.
       gap matches .service__cta's own 15px rather than the base clamp. */
    align-items: center;
    gap: 15px;
  }

  /* Forces the break after "GCC" instead of running as one 550px line.
     Done with a width rather than a <br> in content.json because that
     string is shared with desktop, which must keep its single line.
     Re-measured after a --t-subtitle size change moved these numbers:
     "Access to European and GCC" is 302.8px at this size and adding
     "Markets" takes it to 392.7px, so anything between those two breaks
     in exactly one place. If this cap ever drifts stale again (a wrap
     landing before "GCC" instead of after it, or a third line appearing),
     it's this pair of numbers that moved — re-measure both and update the
     cap below, don't just nudge it. */
  #chain .cta__title {
    /* .service__cta-title is flex:none too — keeps the title at its own
       width instead of flexing against the button. */
    flex: none;
    /* max-content + the cap below: the cap is what forces the break after
       "GCC", but on its own it also leaves the box a full 330px wide while
       the shorter second line fills only ~235px — so the 15px gap measured
       from the BOX edge read as ~110px of space before the button.
       max-content shrinks the box back onto the longest line, which is how
       .service__cta-title (no cap, spans sized by content) already behaves. */
    width: max-content;
    /* 303px, not some rounder number that merely forces the break:
       max-content is capped by this value, so any cap wider than the
       longest RENDERED line (302.8px, "Access to European and GCC") leaves
       that much dead box hanging past the text — and the flex gap measures
       from the box edge, so an oversized cap reads as extra space before
       the button instead of the intended 15px. Sized onto the line itself:
       still well under the 392.7px that adding "Markets" would need, so
       the break stays exactly where it belongs. Services avoids this with
       per-line spans, which can't be done here without splitting the
       string in content.json — and check-copy.mjs asserts that exact
       string. */
    max-width: 303px;
    /* Left, matching .service__cta-title's own `start` — the base .cta
       rule centres its text, which left the two lines centred against each
       other. The BLOCK stays centred in the row (justify-content above);
       only the text inside it aligns left. */
    text-align: left;
    /* The one type value that differed from .service__cta-title — size and
       weight already matched (both are .t-subtitle at 500). Colour and the
       two-layer text-shadow above stay as they are: this sits on a photo,
       services sits on flat --a-plus. */
    line-height: 1.2;
  }
}

/* Mobile: same technique as the tablet override above (a width cap, not a
   <br> in content.json, since that string is shared with desktop). Measured
   at this breakpoint's own .t-subtitle size: "Access to European and GCC" is
   292.6px and adding "Markets" takes it to 379.5px, so 294px forces the
   break in exactly the same place without leaving dead box past the text.
   Unlike tablet, .cta stays stacked and centred here (no flex row to keep
   left-aligned against), so text-align is left at its base centred value —
   width: max-content already shrinks the box onto the longest line, so the
   shorter second line still centres under it. */
@media (max-width: 640px) {
  #chain .cta__title {
    width: max-content;
    max-width: 294px;
  }
}

/* ---------------------------------------------------------------- buttons -- */

.button {
  display: inline-flex;
  align-items: center;
  gap: 0.7em;
  padding: 10px 18px;
  border: 2px solid var(--rule-strong);
  color: var(--ink);
  font-size: var(--t-disclaim);
  font-weight: var(--w-semi);
  letter-spacing: var(--track-nav);
  white-space: nowrap;
  transition: background var(--dur) var(--ease), color var(--dur) var(--ease),
              border-color var(--dur) var(--ease);
}

/* Nudged over the sprite's 1.25 default so the arrow's own weight keeps
   pace with the button label's bold text, not read as thin next to it. */
.button .icon {
  width: 1.15em;
  height: 1.15em;
  --icon-stroke-width: 1.75;
  transition: translate var(--dur) var(--ease);
}

/* Same outline as the carousel cards elsewhere on this page: 2px, --rule
   (not --button's own --rule-strong), plus a 12px radius the plain .button
   doesn't have. This block sits after .button in the cascade and redeclares
   several of its properties (font-weight, padding, letter-spacing) — any
   change meant to reach the CTA "Get in touch" buttons has to be made
   here, not on .button, or it's silently overridden. */
.cta__button {
  border-radius: 20px;
  border: none;
  background: #fff;
  color: var(--a-plus);
  padding: 8px 15px;
  font-size: 15px;
  line-height: var(--t-body-lh);
  font-weight: 600;
  letter-spacing: normal;
}

.button:hover {
  background: var(--accent-deep);
  border-color: var(--accent-deep);
  color: #fff;
}

.button:hover .icon { translate: 0.25em 0; }

[data-tone="cream"] .button:hover,
[data-tone="cream-2"] .button:hover {
  background: var(--a-plus);
  border-color: var(--a-plus);
  color: var(--c);
}

.link-arrow {
  display: inline-flex;
  align-items: center;
  gap: 0.55em;
  font-weight: var(--w-semi);
  font-size: var(--t-disclaim);
  letter-spacing: var(--track-nav);
  text-transform: uppercase;
  color: var(--accent);
}

.link-arrow .icon {
  width: 1.2em;
  height: 1.2em;
  transition: translate var(--dur) var(--ease);
}

/* Decorative section divider — a seamless pattern tiled edge-to-edge at a
   fixed 128px band height (the tile's own viewBox is 128x128, so it needs no
   scaling to repeat cleanly). Colour lives here, not in the partial, so every
   instance of pattern-divider.html can be recoloured from one place. */
.pattern-divider {
  box-sizing: border-box;
  width: 100%;
  height: 138px;
  padding: 5px 0;
}

.pattern-divider__svg {
  display: block;
  width: 100%;
  height: 100%;
}

.pattern-divider__fill {
  fill: color-mix(in srgb, var(--a) 12%, transparent);
}

/* .pattern-divider--reveal (#mosagate-gap / #presence-gap / #contacts-gap
   in home.html) is just a plain wrapper now, purely for a stable id — no
   padding, margin or background of its own. .pattern-divider already
   carries its own 5px top padding (see above); giving the wrapper a
   matching padding-top would only double it to 10px. */

.link-arrow:hover .icon { translate: 0.3em 0; }


/* header.css */
/* =========================================================================
   Header — fixed, flat over the content, never hides.

   It has no background plate of its own, so it has to invert as it passes over
   each section. main.js writes data-tone onto the header from whichever section
   is currently under it; everything below keys off that one attribute.

   Two text looks, not a spectrum: white over the photo sections (hero,
   chain, services and its four covers), dark over the cream ones (About Us,
   Presence, Contacts). For About Us and Contacts specifically, main.js holds
   the header at the photo-section look until the photo behind it has fully
   scrolled away (see the "gates" block in main.js) rather than switching the
   instant the section boundary passes, so the text never flips to dark while
   it's still sitting on a dark photo. The room the header needs is opened by
   static padding on the section before each pattern strip (see #chain and
   .services__loop in sections.css), not by the header itself. */

.site-header {
  position: fixed;
  inset: 0 0 auto;
  z-index: 50;
  height: var(--header-h);
  display: flex;
  align-items: center;
  color: var(--on-dark);
  background: transparent;
  transition: color var(--dur) var(--ease), background var(--dur) var(--ease);
}

.site-header__inner {
  width: 100%;
  max-width: var(--measure);
  margin-inline: auto;
  padding-inline: var(--gutter);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 2rem;
}

/* ------------------------------------------------------------------ logo -- */
/* Two files, not one recoloured: the mark keeps its green facets in both, and
   only the wordmark changes. Stacking them avoids a reflow on tone change. */

.brand {
  display: grid;
  flex: none;
  line-height: 0;
}

.brand img {
  grid-area: 1 / 1;
  width: clamp(177.45px, 14.32vw, 210px); /* +10%, then +5% again site-wide */
  height: auto;
  transition: opacity var(--dur) var(--ease);
}

.brand__color { opacity: 0; }

/* Same 0 1px 3px shadow spec both marks share, just inverted per mark: the
   white mark (over photos) gets a dark (Colour A+) shadow to read against a
   light sky/glass; the colour mark (over the cream ground, still a fully
   transparent header — content keeps scrolling underneath it) gets a plain
   white one instead, so it keeps contrast against whatever's directly
   behind it rather than blending in. Drop-shadow, not text-shadow, since
   these are <img>s. */
.brand__white {
  filter: drop-shadow(0 1px 3px color-mix(in srgb, var(--a-plus) 80%, transparent));
}

.brand__color {
  filter: drop-shadow(0 1px 6px white);
}

.site-header[data-tone^="cream"] .brand__white { opacity: 0; }
.site-header[data-tone^="cream"] .brand__color { opacity: 1; }

/* Every cream-tone section shares this look now — About Us, Presence,
   Contacts alike: flat Colour D, fully opaque (no resting dim), hover/active
   moving to Colour B+ as everywhere else. The header stays fully
   transparent in this tone too (see .brand__color above), so the text gets
   the same white shadow as the logo, not none — content keeps scrolling
   underneath it and a solid white glow is what keeps it readable against
   whatever's directly behind it. Scoped selectors below
   match or beat the specificity of every rule they override, rather than
   touching the shared .nav__link/.nav__sublink rules the transparent
   (photo-section) tone still relies on.

   Desktop only (901px+): below that the nav becomes the full-screen dark
   overlay (see the max-width:900px block further down), whose own solid
   background already carries the contrast — its links must stay plain
   white no matter which section's tone opened it. Without this min-width
   guard these selectors' higher specificity was winning inside that overlay
   too, turning its text the dark cream-tone colour against a dark green
   panel. */
@media (min-width: 901px) {
  .site-header[data-tone^="cream"] .nav__link,
  .site-header[data-tone^="cream"] .nav__sublink {
    color: var(--d);
    opacity: 1;
    text-shadow: 0 1px 6px white;
  }

  .site-header[data-tone^="cream"] .nav__group[data-open="true"] .nav__sublink { opacity: 1; }

  .site-header[data-tone^="cream"] .nav__link:hover,
  .site-header[data-tone^="cream"] .nav__link:focus-visible,
  .site-header[data-tone^="cream"] .nav__link[aria-current="true"] { color: var(--b-plus); }

  .site-header[data-tone^="cream"] .nav__group:has(.nav__sublink[aria-current="true"]) .nav__link--parent {
    color: var(--b-plus);
  }

  .site-header[data-tone^="cream"] .nav__group[data-open="true"] .nav__sublink:hover,
  .site-header[data-tone^="cream"] .nav__group[data-open="true"] .nav__sublink:focus-visible,
  .site-header[data-tone^="cream"] .nav__sublink[aria-current="true"] {
    color: var(--b-plus);
  }
}

/* -------------------------------------------------------------------- nav -- */

.nav {
  display: flex;
  align-items: center;
  gap: clamp(1.5rem, 0.4rem + 2.3vw, 3rem);
}

/* Home / Privacy Policy — mobile/tablet dropdown only, matching the
   footer's own link list (see header.html). Desktop's nav is untouched:
   these two are in the DOM at every width but only ever visible under
   901px, where the media block below re-shows them. */
.nav__link--mobile-only { display: none; }

.nav__link {
  position: relative;
  padding-block: 0.5rem;
  font-family: inherit;
  font-size: 14px;
  line-height: var(--t-body-lh);
  font-weight: var(--w-semi);
  letter-spacing: var(--track-nav);
  text-transform: uppercase;
  color: #fff;
  opacity: 0.7;
  white-space: nowrap;
  text-shadow: 0 1px 3px rgba(0, 49, 31, 0.8);
  transition: opacity var(--dur) var(--ease), font-weight var(--dur) var(--ease);
}

/* Group toggles are <button>s standing in for the plain <a> style above —
   reset to blend in, then layer the caret on top. */
.nav__link--parent {
  display: inline-flex;
  align-items: center;
  gap: 0.4em;
  background: none;
  border: 0;
  padding: 0.5rem 0;
  cursor: pointer;
}

/* Arrows removed from the desktop nav — the hover/open state is carried by
   weight and opacity alone now. Still present in the DOM for the mobile
   accordion, where it remains the only expand/collapse affordance. */
.nav__caret {
  width: 0.65em;
  height: 0.65em;
  transition: rotate var(--dur) var(--ease);
}

.nav__group[data-open="true"] .nav__caret { rotate: 180deg; }

/* Removed at every width now — the mobile accordion this used to be the
   only affordance for has been replaced by the same hover/click dropdown
   desktop uses (see .nav__submenu), which needs no arrow either. */
.nav__caret { display: none; }

.nav__group[data-open="true"] .nav__link--parent { opacity: 1; }

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

.nav__link[aria-current="true"] {
  opacity: 1;
  color: #fff;
}

/* -------------------------------------------------------------- dropdowns -- */

.nav__group { position: relative; }

/* A child being current reads onto the parent label too — the visitor is
   somewhere under "Mosagate" even when the toggle itself was never clicked. */
.nav__group:has(.nav__sublink[aria-current="true"]) .nav__link--parent {
  opacity: 1;
  color: #fff;
}

.nav__submenu {
  position: absolute;
  top: 100%;
  /* -0.6rem is this box's own padding below, and -0.75rem on top of that is
     .nav__sublink's own left padding — together they cancel both layers of
     inset so a sublink's TEXT lines up with the parent button's text
     (.nav__link--parent has no left padding of its own), not just this
     box's edge. */
  left: -1.35rem;
  z-index: 1;
  margin-top: -0.4rem;
  min-width: 12rem;
  display: grid;
  gap: 0.05rem;
  padding: 0.6rem;
  opacity: 0;
  visibility: hidden;
  /* A stray trailing ) here previously made this whole declaration invalid
     CSS — the browser dropped it to its default (transition: all 0s), so
     this panel has never actually faded; it only ever snapped, at whatever
     moment data-open flipped. */
  transition: opacity var(--dur-panel) var(--ease), visibility var(--dur-panel) var(--ease);
}

.nav__group[data-open="true"] .nav__submenu {
  opacity: 1;
  visibility: visible;
}

/* Exists only so the mobile accordion below (see header.css's max-width:900px
   block) has something to clip during its grid-row height animation —
   display:contents removes it from the box model everywhere else, so on
   desktop .nav__sublink stays a direct grid item of .nav__submenu, laid out
   exactly as if this wrapper weren't in the markup at all. */
.nav__submenu-track { display: contents; }

.nav__sublink {
  position: relative;
  padding: 0.5rem 0.75rem;
  border-radius: 6px;
  font-size: 14px;
  line-height: 1.0;
  font-weight: var(--w-semi);
  letter-spacing: var(--track-nav);
  text-transform: uppercase;
  color: #fff;
  opacity: 0;
  white-space: nowrap;
  text-shadow: 0 1px 3px rgba(0, 49, 31, 0.8);
  translate: 0 -8px;
  /* Same stray-) bug as .nav__submenu above — this was invalid CSS and fell
     back to transition:all 0s, so each link's own fall-in/out was an instant
     snap at its staggered moment, not an actual fade+rise. */
  transition: opacity var(--dur) var(--ease), font-weight var(--dur) var(--ease), translate var(--dur) var(--ease);
}

.nav__group[data-open="true"] .nav__sublink {
  opacity: 0.7;
  translate: 0 0;
}

/* Scoped to the same [data-open="true"] ancestor as the rule above, not just
   .nav__sublink:hover — that plainer form has LOWER specificity than the
   open-state rule (2 classes vs. 2 classes + an attribute selector), so it
   was silently losing: opacity here never actually reached 1. Weight is
   deliberately left alone — brightness alone is the hover cue. */
.nav__group[data-open="true"] .nav__sublink:hover,
.nav__group[data-open="true"] .nav__sublink:focus-visible {
  opacity: 1;
}

.nav__sublink[aria-current="true"] {
  opacity: 1;
}

/* Cascade animation on submenu open — each link falls in with a staggered delay. */
.nav__group[data-open="true"] .nav__sublink:nth-child(1) { transition-delay: 60ms; }
.nav__group[data-open="true"] .nav__sublink:nth-child(2) { transition-delay: 120ms; }
.nav__group[data-open="true"] .nav__sublink:nth-child(3) { transition-delay: 180ms; }
.nav__group[data-open="true"] .nav__sublink:nth-child(4) { transition-delay: 240ms; }
.nav__group[data-open="true"] .nav__sublink:nth-child(5) { transition-delay: 300ms; }

/* The close is the open run backwards, not just undone — last link leaves
   first, first leaves last, so the cascade reads as retreating the way it
   arrived rather than the whole stack vanishing at once. main.js sets
   data-open="false" explicitly (not just removing the attribute), which is
   what makes it possible to target the closing direction on its own here. */
.nav__group[data-open="false"] .nav__sublink:nth-child(1) { transition-delay: 240ms; }
.nav__group[data-open="false"] .nav__sublink:nth-child(2) { transition-delay: 180ms; }
.nav__group[data-open="false"] .nav__sublink:nth-child(3) { transition-delay: 120ms; }
.nav__group[data-open="false"] .nav__sublink:nth-child(4) { transition-delay: 60ms; }
.nav__group[data-open="false"] .nav__sublink:nth-child(5) { transition-delay: 0ms; }

/* The panel itself must not go invisible until the slowest closing link
   (nth-child(1), delayed 240ms above) has actually finished fading — without
   this it was hiding on its own fixed --dur-panel clock, cutting the reverse
   cascade off partway through instead of letting it finish. Opacity is left
   on the quicker, ungated --dur-panel timing (a plain container fade reads
   fine on its own pace); only visibility — the hard on/off switch — waits
   for the full 240ms delay + the link's own --dur transition. */
.nav__group[data-open="false"] .nav__submenu {
  transition-delay: 0s, calc(240ms + var(--dur));
}

/* Long enough to need it — the only sublink whose label wraps. Its own
   line-height is nudged up slightly from the shared 1.0 — tight enough to
   read as one label, not so tight the two lines crowd each other. */
.nav__sublink[data-nav-link="presence"] {
  white-space: normal;
  max-width: 12ch;
  line-height: 1.15;
}

/* ----------------------------------------------------------- mobile menu -- */

/* Icon only now — the "Menu" word (still in the DOM as .nav-toggle__label,
   see base.css's .visually-hidden) stays as the button's accessible name
   without taking any visual space. Fixed white/80% read (not currentColor +
   button-opacity) so the mark stays flat and legible over any tone. */
.nav-toggle {
  display: none;
  align-items: center;
}

.nav-toggle__bars {
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 6px;
  width: 24px;
  height: 24px;
  translate: -5px 3px;
}

.nav-toggle__bars span {
  width: 100%;
  height: 3px;
  background: rgb(255 255 255 / 0.8);
  transition: translate var(--dur) var(--ease), rotate var(--dur) var(--ease),
              opacity var(--dur-fast) var(--ease);
}

[aria-expanded="true"] .nav-toggle__bars span:nth-child(1) { translate: 0 9px; rotate: 45deg; }
[aria-expanded="true"] .nav-toggle__bars span:nth-child(2) { opacity: 0; }
[aria-expanded="true"] .nav-toggle__bars span:nth-child(3) { translate: 0 -9px; rotate: -45deg; }

/* A full-screen overlay, same idea as the desktop nav's own per-group
   dropdown (.nav__submenu above) — fade + opacity, no slide — just applied
   to the whole menu and filling the viewport instead of a small dropdown
   anchored under the logo. */
@media (max-width: 900px) {
  .nav-toggle { display: inline-flex; }

  /* Own stacking context above the panel, so the logo stays visible and
     the toggle stays clickable (to close again) while the panel is open. */
  .brand,
  .nav-toggle {
    position: relative;
    z-index: 2;
  }

  .nav {
    position: fixed;
    inset: 0;
    width: 100%;
    z-index: 1;
    display: flex;
    flex-direction: column;
    align-items: flex-end;
    gap: 0;
    height: 100%;
    /* clip, not the implicit auto that overflow-y alone would give this
       axis — auto still lets real mobile browsers treat any overflowing
       descendant as reachable-by-scroll and widen their layout viewport
       around it (the exact bug fixed on .service earlier, just triggered
       from inside the nav this time by long Services sublinks). clip
       removes that possibility outright while overflow-y:auto still
       scrolls a tall menu vertically. */
    overflow-x: clip;
    overflow-y: auto;
    padding: calc(var(--header-h) + 50px) var(--gutter) var(--gutter);
    padding-right: calc(var(--gutter) + 5px);
    color: var(--on-dark);
    text-align: right;
    translate: 0 -12px;
    opacity: 0;
    visibility: hidden;
    transition: translate 400ms var(--ease), opacity 400ms var(--ease), visibility 0s linear 400ms;

    background: var(--a-plus);
  }

  .nav[data-open="true"] {
    translate: 0 0;
    opacity: 1;
    visibility: visible;
    transition: translate 400ms var(--ease), opacity 400ms var(--ease), visibility 0s linear;
  }

  /* The same seamless Arabic tile pattern-divider.html tiles edge-to-edge
     elsewhere on the site, masked from its static file instead of inlined
     here so it can be recoloured with a plain background-color. Full-bleed
     over the panel now that it fills the screen. A second, gradient mask
     layer fades the tile from 3% at the top to the base 8% at the bottom
     (0.375 = 3/8, so it scales the 8% background down to 3% up top) —
     the two masks intersect, so the pattern only shows where both are
     opaque. Children get position:relative below so their text paints
     above this, not under it. */
  .nav::before {
    content: "";
    position: absolute;
    inset: 5px;
    pointer-events: none;
    background: color-mix(in srgb, var(--c) 8%, transparent);
    -webkit-mask-image: url("/assets/logos/arabic-pattern.svg"), linear-gradient(to bottom, rgba(0, 0, 0, 0.375) 0%, #000 100%);
    mask-image: url("/assets/logos/arabic-pattern.svg"), linear-gradient(to bottom, rgba(0, 0, 0, 0.375) 0%, #000 100%);
    -webkit-mask-repeat: repeat, no-repeat;
    mask-repeat: repeat, no-repeat;
    -webkit-mask-size: 82px 144.15px, 100% 100%;
    mask-size: 82px 144.15px, 100% 100%;
    -webkit-mask-composite: source-in;
    mask-composite: intersect;
  }

  .nav > * { position: relative; }

  /* A divider between each top-level item (Home, Mosagate, Services,
     Contacts, Privacy), not just the two groups with submenus. Fixed px
     (not em) so the .nav__link items' own 25px font-size doesn't double
     the line width the .nav__group divs render at. 40% visible opacity
     (50% base, dialed down 20%). */
  .nav > * + * {
    border-top: 1.5px solid color-mix(in srgb, currentColor 40%, transparent);
  }

  /* .nav__link (Contacts, Privacy) carries its own opacity:0.7 (see the
     shared .nav__link/.nav__link--parent/.nav__sublink rule below), which
     the .nav__group divs (Mosagate, Services) don't have — the divider
     above would otherwise fade there a second time (0.4 alpha × 0.7
     opacity = 0.28) and look fainter than the .nav__group dividers.
     57.14% here × the element's own 0.7 opacity works back out to the
     same visible 40% as the others. */
  .nav__link + .nav__link,
  .nav__group + .nav__link {
    border-top-color: color-mix(in srgb, currentColor 57.14%, transparent);
  }

  /* Over the open panel the nav text is always on dark, whatever is behind
     it — but the logo is left alone: it keeps following the section's own
     data-tone (colour mark on cream, white mark on dark/photo), exactly as
     it does everywhere else, rather than being forced white just because
     the panel happens to be open. */
  .site-header:has(.nav[data-open="true"]) { color: var(--on-dark); }

  /* Cream tone (About Us, Presence, Contacts): the toggle is otherwise a
     fixed white bar, invisible against a cream page. Solid Colour D, same
     treatment desktop's own cream-tone nav text gets — but only while
     closed, sitting on the actual cream page. Once open this same icon (now
     the X) sits on the panel's own dark green background instead (full-bleed
     on phone, the drawer's own top-right corner on tablet), where Colour D
     is exactly as low-contrast as the white bars are over cream; the
     :not(:has()) guard hands it back to the base white rule for that state. */
  .site-header[data-tone^="cream"]:not(:has(.nav[data-open="true"])) .nav-toggle__bars span {
    background: var(--d);
  }

  /* Same appearance as the desktop .nav__link/.nav__sublink — uppercase,
     tracked — size, weight, line-height, opacity and shadow differ here
     (weight happens to already match --w-semi, restated so the three sit
     together per the brief). .nav__link--parent is included so the
     dropdown-toggle buttons share the exact same treatment as the plain
     links instead of relying on class inheritance for it. No text-shadow
     at this width — the panel's own solid background does the contrast
     work the shadow exists for over a photo. */
  .nav__link,
  .nav__link--parent,
  .nav__sublink {
    font-size: 25px;
    line-height: 1.8;
    font-weight: var(--w-semi);
    opacity: 0.7;
    text-shadow: none;
    /* Tighter than --track-nav's sitewide 0.08em (desktop/footer stay on
       that token, untouched) — the bigger type here reads as loud/spaced
       out at the same tracking desktop uses at 14px. */
    letter-spacing: 0.04em;
  }

  .nav__link { width: 100%; padding-block: 0.4rem; }

  /* Lighter than the top-level links' --w-semi — the submenu items read as
     a secondary tier under their parent, not equal weight to it. */
  .nav__sublink { font-weight: var(--w-medium); }

  /* No override for the open sublink's opacity here — the desktop rule's
     own 0.7 (see the [data-open="true"] rule above) already matches idle
     opacity here, so hover/focus/current is the only thing that reaches
     100%, same idle/active split as every other link in this panel. */

  /* Only label long enough to wrap at these type sizes. The desktop rule
     near the top of this file caps it at 12ch (tuned for that dropdown's
     14px type), which is too narrow here; 17ch gives it the same right-edge
     breathing room as its shorter neighbours. line-height is deliberately
     NOT the shared 1.8 above — that value is the gap BETWEEN items, and on
     this one label's two lines it read as two separate buttons; padding-
     bottom then makes up the difference so the gap to SERVICES below still
     matches every other row. */
  .nav__sublink[data-nav-link="presence"] { max-width: 17ch; line-height: 1.08; padding-bottom: 15px; }

  /* DOM order already puts Home first and Privacy Policy last (see
     header.html) — .nav is flex-direction:column, so no explicit order
     needed. */
  .nav__link--mobile-only { display: block; }

  .nav__group { width: 100%; }

  /* row-reverse puts the caret (last in the DOM, after the label — see
     header.html) on the leading/left edge instead of trailing it, so it
     reads as "before the word" the way the plain links read right to left.
     It also flips which physical side "flex-start"/"flex-end" land on, so
     flex-start here is what actually packs content against the right edge,
     matching every other right-aligned link in this panel. */
  .nav__link--parent { width: 100%; justify-content: flex-start; flex-direction: row-reverse; }

  /* Re-shown here only — desktop dropped the arrow in favour of a hover
     opacity/weight cue (see the unconditional display:none above); the
     full-screen accordion still needs it as the sole expand/collapse
     affordance. Points down at rest, flips via the existing rotate rule
     above when its group is open. width/height explicitly reset to the
     base .icon 1em/1em — the older .nav__caret{width/height:0.65em} rule
     above (dead weight from the desktop accordion this replaced) has the
     same specificity and would otherwise still win for just those two
     properties, shrinking the arrow to look undersized next to the text.

     Stroke (not fill — this is the same stroke-only sprite used everywhere
     else, see icons.svg) is tuned to match the divider lines between menu
     items below (.nav > * + *'s border-top), NOT the other way around:
     those lines sit at a fixed ~1.5px. Sizing this icon box in px
     rather than em maps the sprite's 24-unit viewBox 1:1 onto pixels, so
     --icon-stroke-width IS the rendered pixel width — matched to the line
     without per-size math. */
  .nav__caret {
    display: inline-block;
    width: 24px;
    height: 24px;
    --icon-stroke-width: 1.5;
  }

  /* In-flow, not the floating dropdown desktop uses — desktop's items sit
     side by side, so a float below one doesn't cover the others; this nav
     is a single stacked column, so an absolutely positioned submenu would
     land directly on top of the links below it instead. The fade is the
     same effect as desktop's own dropdown (same nth-child cascade-delay
     rules above, untouched by this breakpoint) — opacity is just paired
     with max-height, not alone, so the closed state is genuinely invisible
     and nothing peeks through before a group opens. */
  .nav__submenu {
    position: static;
    translate: none;
    margin-top: 0;
    padding: 0;
    padding-right: 0.4em;
    border-radius: 0;
    background: none;
    box-shadow: none;
    /* The 0fr → 1fr grid-row trick: a single-row grid whose row track is
       the thing that animates. Works natively everywhere (no feature flag
       needed — unlike animating height:auto, which only Chromium honours
       today; Safari just snaps it instantly, which is the "jumpier than
       before" regression this replaced). .nav__submenu-track is the row's
       one cell — its own overflow:hidden is what clips the sublinks while
       the row is still short of 1fr. */
    display: grid;
    grid-template-rows: 0fr;
    opacity: 0;
    visibility: visible;
    transition: grid-template-rows var(--dur-panel) var(--ease), opacity var(--dur) var(--ease);
  }

  .nav__submenu-track {
    display: grid;
    justify-items: end;
    gap: 0;
    overflow: hidden;
    /* Grid items default to min-height:auto (content's own height), which
       would stop this row from ever actually reaching 0 — this is what
       lets it collapse all the way. */
    min-height: 0;
  }

  .nav__group[data-open="true"] .nav__submenu {
    grid-template-rows: 1fr;
    opacity: 1;
  }

  /* The global .nav__group[data-open="false"] .nav__submenu rule above sets
     transition-delay for the desktop dropdown's own (opacity, visibility)
     pair — at this width the transitioned properties are
     (grid-template-rows, opacity) instead, so that delay list would land on
     the wrong properties. Reset it here: the row waits for the slowest
     closing sublink's own cascade (240ms delay + its --dur fade, same
     figures as desktop's rule) so the accordion doesn't collapse out from
     under a link that's still visibly retreating; opacity can fade right
     away. */
  .nav__group[data-open="false"] .nav__submenu {
    transition-delay: calc(240ms + var(--dur)), 0s;
  }
}

/* Phone-only: the stack starts a little higher than the shared block's
   padding puts it. */
@media (max-width: 700px) {
  .nav {
    padding-top: calc(var(--header-h) + 25px);
  }

  /* Only at this width does the open panel's .nav cover the full viewport
     (inset:0, no left/width limit — see the shared max-width:900px block),
     so the logo sits on the panel's own dark background, not the page
     behind it. Tablet's drawer is narrower (see its own min-width:701px
     block below) and leaves the page — and the logo over it — visible, so
     it keeps following data-tone instead; this override must not reach
     that width. */
  .site-header:has(.nav[data-open="true"]) .brand__white { opacity: 1; }
  .site-header:has(.nav[data-open="true"]) .brand__color { opacity: 0; }
}

/* Tablet-only. Everything phone-width is settled and must not move, so all
   tablet work lives in here rather than in the shared max-width:900px block
   above — editing that block changes phones too. */
@media (min-width: 701px) and (max-width: 900px) {
  /* 4px down from the 33px this used to run at. */
  .nav__link,
  .nav__link--parent,
  .nav__sublink {
    font-size: 29px;
  }

  /* A drawer over the hero photo, not the full-bleed takeover phones get —
     closer to how uniasset's own tablet nav reads. fit-content rather than
     a fixed width so the inset left of the longest label is just this box's
     own padding, matching the gutter on the right instead of leaving dead
     space. left:auto lets the left edge follow from the width; right:0
     (from the shared block's inset:0) is what anchors it. */
  .nav {
    left: auto;
    width: fit-content;
    max-width: min(100%, 382px);
  }

  /* Sublinks wrap at this width — the panel is narrower than the longest
     Services labels, and an unwrapped one would overflow its right edge,
     which widens real mobile browsers' layout viewport and pushes the whole
     header off-screen (the same failure mode as the services-carousel bug).
     Two classes to out-specify the desktop-only presence rule's own 12ch
     cap (tuned for a 14px dropdown, irrelevant here), which would otherwise
     squeeze that one label narrower than its neighbours.

     320px, not 100%: max-width:100% here means 100% of .nav's own
     fit-content width, which is circular — the panel sizes itself to its
     widest content, so a 100%-capped sublink never actually had a reason
     to wrap, it just let the panel grow around it instead. That's why
     "Global Commerce" wasn't wrapping while "International Presence" was:
     only the latter had its own fixed ch cap forcing the issue. 320px is a
     real number, measured directly against this content: it clears the
     widest single WORD here ("International", 285px) so nothing breaks
     mid-word, and stays under "Global Commerce"'s own full unwrapped width
     (353px) so it's still forced onto two lines. Every other label here is
     short enough to still render on one line at this width. */
  .nav__submenu .nav__sublink {
    white-space: normal;
    max-width: 320px;
  }

  /* Line-height and inter-item spacing were the same property before
     (the shared block's line-height:1.8), which is exactly what made two
     different wrapped labels ("International Presence", "Global Commerce")
     render with different-looking internal spacing depending on which one
     happened to get a bespoke override and which didn't. Splitting them
     here — tight line-height only for spacing a label's own wrapped lines,
     padding-block only for spacing one item from the next — means both
     labels (and every other sublink, wrapped or not) share one rule with
     no per-label patch. padding-block's value is measured, not guessed: it
     reproduces the same ~32.7px gap single-line sublinks already had under
     the old line-height:1.8, so nothing single-line visibly moved. */
  .nav__submenu .nav__sublink {
    line-height: 1.15;
    padding-block: 0.49em;
  }

  /* This out-specifies the desktop rule's own max-width/line-height for
     "presence" (see near the top of this file) the same way the rule
     above does — no bespoke padding-bottom patch needed anymore, since
     padding-block above already gives every sublink, presence included,
     the same spacing as its neighbours. */
  .nav__sublink[data-nav-link="presence"] {
    max-width: 320px;
    line-height: 1.15;
    padding-block: 0.49em;
  }
}


/* pin.css */
/* =========================================================================
   The pinning mechanism — shared by the hero and all four service sections.

   A tall track holds a sticky pane. As the track scrolls past, the pane stays
   put and pin.js maps scroll progress to a discrete step. Nothing calls
   preventDefault, so the scrollbar, momentum, keyboard paging, find-in-page and
   a fast flick straight through all behave normally.

   On landing cleanly: the step is an integer, so a panel is always either fully
   open or fully closed no matter where the scroll comes to rest — there is no
   half-open state to land in. CSS scroll-snap was tried on top of this and
   removed: `proximity` snapping on the root also grabs the smooth scroll from a
   nav click, yanking the page a couple of hundred pixels past the section it
   was asked for. The step height below does that job instead — it is sized so
   one comfortable flick of a wheel advances one step.

   Turning pinning off is a CSS decision, not a JS branch: drop the track height
   and the sticky, and pin.js finds nothing pinned to drive. That is how both
   the mobile and reduced-motion fallbacks work.
   ========================================================================= */

.pin-track {
  /* Height comes from the step count, set per section as --steps. */
  height: calc(100vh + (var(--steps, 3) * var(--step-h, 45vh)));
  position: relative;
}

.pin-pane {
  position: sticky;
  top: 0;
  height: 100vh;
  overflow: hidden;
  display: flex;
  flex-direction: column;
}

/* ------------------------------------------------------------- reveals --- */
/* pin.js adds .is-in to any [data-at] element once the step reaches its
   number. Staggering happens in CSS via --i, so a group of four terms costs
   one step rather than four. */

.reveal {
  opacity: 0;
  translate: 0 1.4rem;
  transition:
    opacity var(--dur-panel) var(--ease),
    translate var(--dur-panel) var(--ease);
  transition-delay: calc(var(--i, 0) * 90ms);
}

.reveal.is-in {
  opacity: 1;
  translate: 0 0;
}

/* Scrolling back up should feel like rewinding rather than a fresh entrance,
   so the outgoing direction is quicker and undelayed. */
.reveal:not(.is-in) {
  transition-duration: var(--dur);
  transition-delay: 0ms;
}

/* ====================================================== fallback: no pin === */
/* Phones and reduced motion get the whole thing in normal flow. */

@media (max-width: 900px), (prefers-reduced-motion: reduce) {
  .pin-track { height: auto; }

  .pin-pane {
    position: static;
    height: auto;
    overflow: visible;
  }

  .reveal {
    opacity: 1;
    translate: none;
    transition: none;
  }
}

/* ============================================= hero opts back in: phones === */
/* The hero re-enables pinning below 900px (reduced motion still gets the
   plain-flow fallback above) — this mechanism was never the problem on
   touch. pin.js never calls preventDefault on wheel or touch; the page
   scrolls completely natively the whole time, and only the pane's own
   appearance holds still, via position:sticky, exactly the same passive
   technique site-uniasset's own hero uses. There is nothing here to lose a
   race against native scrolling, unlike auto-glide.js's touch handling.

   --steps/--step-h are shortened for this width — see hero.css, which sets
   them below its own desktop values specifically so it loads after this
   file and actually wins the cascade (equal specificity, source order).
   The scroll-snap block below needs a single ordinary swipe to actually
   reach the next snap point for the "jump" to read as a jump; mandatory
   snap settles at whichever point a gesture ends up CLOSEST to, it doesn't
   teleport a short scroll across a long gap, so desktop's full 210vh track
   would just pull a normal swipe back to the hero's own start instead of
   forward to #chain. */
@media (max-width: 900px) {
  .hero .pin-track {
    height: calc(100vh + (var(--steps, 3) * var(--step-h, 45vh)));
  }

  .hero .pin-pane {
    position: sticky;
    top: 0;
    height: 100vh;
    overflow: hidden;
  }
}

/* ==================================================== phones: the jump === */
/* ==================================================== works too, now  === */
/* Sticky above holds the hero still while you scroll through it — that part
   is solid (position:sticky, no JS scroll interception, works the same as
   site-uniasset's own passive mobile hero). Desktop's "one swipe lands you
   on What We Do" feel is a SEPARATE thing on top of that (auto-glide.js
   grabbing the wheel and animating the scroll position), and three earlier
   attempts at a mobile/touch equivalent of specifically that all failed:
     1. touchmove + preventDefault after a distance threshold — mobile
        browsers commit to native scroll within the first touchmove or two,
        so preventDefault() on a later event in the same gesture did nothing.
     2. touch-action:none set before the gesture, to remove that race
        entirely — still didn't hold on real iOS Safari, which does not
        reliably hand over full page-level scroll control to JS at all.
     3. CSS scroll-snap (mandatory, then proximity) as a JS-free native
        alternative — mandatory measurably broke the precise nav-click
        landing on #chain (off by ~50px), and proximity produced unstable,
        oscillating scroll positions against this page's own scroll-linked
        JS (jumpTo, auto-glide's own listener, header-mask, pin.js) even
        in this session's own testing, before ever reaching a real phone.

   All three tried to INTERCEPT the gesture. auto-glide.js's touch glide
   (see its own "touch glide" comment) does not: it reacts to the native
   'scroll' event after the browser has already moved the page, the same
   idea site-uniasset's own hero auto-glide uses. Phones and tablets get
   the hold from this file, and the jump from that one. */


/* hero.css */
/* =========================================================================
   Hero — the video runs on a loop while the pane is pinned, and the argument
   assembles over it: headline, lead, the four chain terms, then the record.

   Composition is bottom-left, the whole frame given to the footage.
   ========================================================================= */

.hero { --steps: 2; --step-h: 55vh; }

/* Shortened below 900px — phones/tablets get the sticky hold without a
   "jump" past it (see pin.css's own note on why a touch equivalent of
   auto-glide.js's wheel-jump didn't work out). Without a jump, desktop's
   full 210vh hold would mean scrolling through two-plus empty-feeling
   screens of a stuck hero before reaching #chain — a much shorter hold
   keeps the pinned effect without that cost. Placed here, after the
   desktop rule above, so it wins the cascade (pin.css loads before this
   file — equal specificity, source order decides). */
@media (max-width: 900px) {
  .hero { --steps: 1; --step-h: 25vh; }
}

.hero .pin-pane {
  justify-content: flex-end;
  background: var(--a-plus);
}

/* ----------------------------------------------------------------- media -- */

.hero__media {
  position: absolute;
  inset: 0;
  z-index: 0;
}

.hero__media img,
.hero__media video {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* The poster holds the frame until the video can actually play, so the hero is
   never a black rectangle on a slow connection. It is never hidden — it stays
   at opacity:1 underneath for the whole session, as the fallback for an
   autoplay refusal — so this fade always composites the video over it.

   That only works because the two layers agree: hero-poster.webp is frame 0
   of the hero video, and main.js holds the video on frame 0 until this
   transition has finished before letting it play. Both halves matter. Fading
   in over already-moving footage (the old behaviour) superimposed two
   different moments of the same shot and read as a doubled skyline; so would
   a correct sequence with a poster taken from some other moment.

   Keep this duration and main.js's play-delay in step if either changes. */
.hero__video {
  position: absolute;
  inset: 0;
  opacity: 0;
  transition: opacity 150ms var(--ease);
}

.hero__video.is-playing { opacity: 1; }

/* Two fades, both Colour A+, no flat wash — the footage carries its own
   colour now instead of being tinted uniformly.

   The bottom-up fade is the main one: it sits directly on the element's own
   background, normal blending, so the type at the base of the hero always has
   a dark ground under it.

   The top-down fade lives on ::before instead, set to multiply. Multiply only
   darkens — it has no effect at all where the footage is already dark, and
   strengthens exactly where the sky/glass in the shot is brightest, so the
   header stays readable without flattening the image the way a plain alpha
   layer would.

   The first stop (0%, fully opaque) is there because .hero .pin-pane
   releases from its sticky hold over the last full viewport-height of
   .pin-track (see pin.css) — and #chain enters the viewport from the bottom
   over that exact same stretch, so the pane's own footage and #chain's
   content are on screen together while it releases. The other stops are the
   same opacities as a plain resting-view gradient would use (0.3/0.47/0.13),
   just shifted up by ~150px (17% of the pane's own height) so the same
   curve reaches a little further into that overlap before fading out. */
.hero__scrim {
  position: absolute;
  inset: 0;
  background: linear-gradient(0deg, rgba(0, 49, 31, 1) 0%, rgba(0, 49, 31, 0.3) 17%,
              rgba(0, 49, 31, 0.47) 35%, rgba(0, 49, 31, 0.13) 61%, rgba(0, 49, 31, 0) 75%);
}

.hero__scrim::before {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(180deg, rgba(0, 49, 31, 0.6) 0%, rgba(0, 49, 31, 0) 34%);
  mix-blend-mode: multiply;
}

/* --------------------------------------------------------------- content -- */

/* --parallax-travel is read back by parallax.js — how far the copy drifts up
   against the held footage as the hero is scrolled through. `translate` (not
   `transform`) places the block, precisely so the JS can own `transform`
   without the two overwriting each other. */
.hero__content {
  position: relative;
  z-index: 1;
  width: 100%;
  max-width: var(--measure);
  margin-inline: auto;
  padding: 0 var(--gutter) clamp(2.5rem, 1rem + 5vw, 4.5rem);
  display: grid;
  gap: clamp(0.5rem, 0.2rem + 1vw, 1rem);
  /* Two separate motions act on this block, so they are kept on two separate
     properties rather than one: parallax.js owns `transform` (drift against
     the footage), auto-glide.js owns --lead (the lift the copy makes on its
     own before the page follows it down). Written to the same property they
     would each erase the other. */
  translate: 0 calc(15px + var(--lead, 0px));
  --parallax-travel: 80px;
  will-change: transform, translate;
}

.hero__title {
  max-width: 27ch;
  color: #fff;
  /* Two layers, not one bigger blur: 4px/10px alone was already at full
     opacity (rgba alpha 1), so there's no more darkness to add by turning
     that shadow up on its own — a wider, softer layer underneath adds
     reach without needing more alpha than 1 allows (same idea #chain
     .cta__title's own two-layer shadow uses, sections.css). */
  text-shadow:
    0 6px 18px rgba(0, 49, 31, 1),
    0 4px 10px rgba(0, 49, 31, 1);
}

/* Forced breaks between the title's/lead's three lines (see content.json) —
   desktop and tablet keep them. Phone-only drops them so the text wraps
   naturally at whatever width it actually has there instead of following
   breaks tuned for the wide desktop layout. The &nbsp; right after each
   <br> (see home.html) is what keeps the words apart once the break stops
   rendering — a PLAIN space there doesn't work: browsers collapse ordinary
   whitespace touching either side of a <br> to zero width even when that
   <br> is display:none (confirmed via getBoundingClientRect, not just
   visually). A non-breaking space is never collapsible, so it survives
   regardless of the <br>'s display — and reads as an ordinary space once
   the break is hidden. On desktop/tablet it sits invisibly as trailing
   whitespace at the end of the line the real <br> already breaks. */
@media (max-width: 700px) {
  .hero__title-break,
  .hero__lead-break {
    display: none;
  }

  .hero__title {
    max-width: none;
  }
}

/* hero-intro.js's scramble letters. The transition is what keeps a glyph
   swap from reading as a hard strobe: each change dips the letter's opacity
   and eases it back up rather than snapping the new character straight in
   at full brightness, which is what turns a flicker into something that
   reads as smooth. */
.hero-scramble {
  transition: opacity 150ms var(--ease);
}

/* Wraps each word's letters (see hero-intro.js's wrapIntoLines) as one
   atomic box so a line can only break between words, never inside one —
   without this, every letter's own inline-block (needed for the scramble)
   is independently wrappable, which is what let "Presence" split into
   "Pre"/"sence" once the title started wrapping naturally on phone. */
.hero-scramble-word {
  display: inline-block;
}

.hero__lead {
  max-width: 46ch;
  color: #fff;
  font-size: var(--t-subtitle);
  line-height: var(--t-subtitle-lh);
  /* Two layers, not one bigger blur: 4px/10px alone was already at full
     opacity (rgba alpha 1), so there's no more darkness to add by turning
     that shadow up on its own — a wider, softer layer underneath adds
     reach without needing more alpha than 1 allows (same idea #chain
     .cta__title's own two-layer shadow uses, sections.css). */
  text-shadow:
    0 6px 18px rgba(0, 49, 31, 1),
    0 4px 10px rgba(0, 49, 31, 1);
  /* Owned entirely by hero-intro.js now, not the generic [data-at] reveal —
     it has to stay invisible until the title's scramble finishes, which the
     step-based reveal has no notion of. Held at opacity:0/translated down
     until JS adds .is-lit; if that script never runs (or is skipped,
     reduced motion), .hero__lead has no base class here to key off, so it
     stays fully visible with no transition — never silently stuck hidden. */
}

.hero__lead.hero__lead--intro {
  opacity: 0;
  /* Same fade-up shape as .reveal (pin.css) — simple, already the site's
     own convention — rather than the blur this replaced. */
  translate: 0 22px;
  /* Opacity finishes a beat before the rise settles rather than the two
     locked to one timeline: the text is legible well before it's fully in
     place, so the last moments read as a settle, not still-arriving. */
  transition: opacity 650ms var(--ease), translate 900ms var(--ease);
}

.hero__lead--intro.is-lit {
  opacity: 1;
  translate: 0 0;
}


/* -------------------------------------------------------------- the record -- */

/* No top rule — only the three vertical dividers between cells remain, each
   centered in its gap with 50px clear on both sides (the neighbouring
   cells' own padding), not pinned to the page edge. */
/* Content-sized columns, not 1fr — 1fr stretches each column to fill the
   row regardless of its padding, so the dividers sit at fixed 25/50/75%
   marks and the numbers never actually move when padding changes. Sized to
   content and centered, the padding on .hero__kpi is what sets the gap. */
.hero__kpis {
  display: grid;
  grid-template-columns: repeat(4, max-content);
  justify-content: center;
  /* Tuned for sitting right under why-title now (see home.html) rather than
     the About Us body copy this used to trail. Bottom is 15px more than top
     by request — no longer an even match, kept separate rather than one
     shared value so each can move independently. */
  margin-top: 42px;
  padding-bottom: 47px;
}

/* No vertical padding — the divider is a border on this box, so any padding
   here would make it taller than the value+label content it runs beside.
   Same weight and colour as the "why us" accordion's rule below (--rule,
   2px) — both live on Colour C and should read as the same line. */
.hero__kpi {
  padding: 0 40px;
  display: grid;
  gap: 0.25rem;
  justify-items: center;
  text-align: center;
  border-right: 2px solid var(--rule);
}

.hero__kpi:last-child { border-right: 0; }

.hero__kpi-value {
  color: var(--b-plus);
  font-variant-numeric: tabular-nums;
}

.hero__kpi-label {
  color: var(--d);
  letter-spacing: 0.03em;
  line-height: 1.2;
  max-width: 12ch;
}

@media (max-width: 720px) {
  .hero__kpis { grid-template-columns: repeat(2, minmax(0, 1fr)); row-gap: 15px; }
  /* 2-column layout needs the divider only between the two columns, not
     after every cell but the last — border-right by index instead. */
  .hero__kpi { border-right: 0; }
  .hero__kpi:nth-child(odd) { border-right: 2px solid var(--rule); }
}

/* Tablet: still the 4-column layout above, just half the gap between
   cells (the padding is what sets that gap — see the comment on
   .hero__kpi). */
@media (min-width: 721px) and (max-width: 900px) {
  .hero__kpi { padding: 0 20px; }
}

/* ------------------------------------------------------------------- cue -- */

.hero__cue {
  position: absolute;
  right: calc(var(--gutter) + 15px);
  bottom: calc(clamp(2.5rem, 1rem + 5vw, 4.5rem) - 15px);
  z-index: 1;
  display: grid;
  justify-items: center;
  gap: 0.6rem;
  color: var(--on-dark-muted);
  font-size: var(--t-micro);
  letter-spacing: var(--track-eyebrow);
  text-transform: uppercase;
  transition: opacity var(--dur) var(--ease);
}

/* Once the assembly has started the cue has done its job. */
.pin-pane:not([data-step="0"]) .hero__cue { opacity: 0; }

.hero__cue-label {
  font-weight: 700;
}

.hero__mouse {
  width: 22px;
  height: 34px;
  border: 1.5px solid currentColor;
  border-radius: 12px;
  display: flex;
  justify-content: center;
  padding-top: 6px;
}

.hero__mouse-wheel {
  width: 3px;
  height: 7px;
  border-radius: 2px;
  background: currentColor;
  animation: mouse-wheel 2.2s var(--ease) infinite;
}

@keyframes mouse-wheel {
  0%   { transform: translateY(0);    opacity: 0; }
  20%  { opacity: 1; }
  70%  { transform: translateY(11px); opacity: 0; }
  100% { transform: translateY(0);    opacity: 0; }
}

@media (prefers-reduced-motion: reduce) {
  .hero__mouse-wheel { animation: none; opacity: 0.9; transform: none; }
}

@media (max-width: 900px) {
  /* Without the pin the hero is a single tall panel again. */
  .hero .pin-pane { min-height: 100svh; justify-content: flex-end; }
  .hero__content {
    padding-top: calc(var(--header-h) + 2rem);
    /* Same mechanism as the base rule's own translate (--lead stays live
       here too, just always 0px — auto-glide.js doesn't run without the
       pin) — 23px more than its baseline 15px, so the title/lead sit 23px
       lower than they otherwise would at this width. */
    translate: 0 calc(38px + var(--lead, 0px));
  }
}

/* Phone-only: just the mouse icon, no "Scroll" label — the label made the
   cue tall enough to need real clearance from the lead text below (see
   .hero__lead's own padding-bottom next); the icon alone is compact
   enough to sit in the corner without it. */
@media (max-width: 700px) {
  .hero__cue-label { display: none; }
  /* 10px closer to the edge than the shared right offset — with the
     label gone there's no longer a wider line above the icon keeping it
     away from the corner. */
  .hero__cue { right: calc(var(--gutter) + 5px); }
}

/* Phone-only: .hero__cue is still absolutely positioned at the pane's own
   bottom-right, unchanged from desktop. On desktop's wide gutter the lead
   text never reaches that corner; on a narrow phone the lead runs the
   full width and .hero__content sits bottom-flush against the pane
   (justify-content:flex-end above), so without this the lead's last line
   would sit directly under the mouse icon. Tablet doesn't need it — the
   lead is short enough there to already clear the cue (which also still
   has its label at that width). */
@media (max-width: 700px) {
  .hero__lead { padding-bottom: 50px; }
}

/* Tablet-only, scoped to .hero__title alone (not the shared .t-hero token,
   which also sizes the chain/mosagate/services titles and the kpi values —
   those stay untouched). +4px on top of --t-hero's own fluid clamp rather
   than a flat px value, so it stays proportional across the 701-900px
   range instead of locking to one size. line-height needs no separate
   change — .t-hero's line-height is the unitless 1 (see base.css), so it
   already scales with the bigger font-size. */
@media (min-width: 701px) and (max-width: 900px) {
  .hero__title {
    font-size: calc(var(--t-hero) + 4px);
  }

  /* Nudges the visible crop of the hero footage 60px left of centre. Poster
     and video are targeted together deliberately — they are two stacked
     layers showing the same shot, so shifting one without the other would
     misalign them the moment both are visible (see main.js's fade note). */
  .hero__media img,
  .hero__media video {
    object-position: calc(50% - 60px) center;
  }
}

/* Phone-only: same idea as the tablet block above, shifted the other way —
   45px right of centre. Doesn't touch desktop framing. */
@media (max-width: 700px) {
  .hero__media img,
  .hero__media video {
    object-position: calc(50% + 45px) center;
  }
}


/* services.css */
/* =========================================================================
   Services — cover photo, intro, pill-controlled sliding carousel, CTA.

   Not scroll-jacked (no .pin-track here) — every service section is a plain
   static block. The three cards are one active slide at a time, chosen by
   clicking a pill (see js/service-carousel.js), not by scroll position.
   ========================================================================= */

.service {
  position: relative;
  background: var(--a-plus);
  color: var(--ink);
  /* Backstop for the carousel below: .service__carousel already has its
     own overflow:hidden to clip the off-screen slides in the flex track,
     but real Firefox/Chrome (unlike this repo's own Chromium-based preview
     tooling) were still letting that width reach document.documentElement,
     widening the mobile layout viewport and pushing the fixed header's
     nav-toggle off-screen entirely. Scoped to this section (not html/body)
     so it can't interact with the hero/chain's own position:sticky pinning
     elsewhere on the page (see pin.css). */
  overflow-x: clip;
}

/* Invisible spacer, same box the cover photo used to occupy in flow
   (width:100%, 21:9) — its only job is reserving that exact height so the
   intro text below starts at the same position it always has, now that the
   photo itself has moved out of flow to become a backdrop (see
   .service__cover below). */
.service__cover-spacer {
  width: 100%;
  aspect-ratio: 21 / 9;
}

/* Mobile: the cover photo itself is 150% width now (see .service__cover
   img below) — its rendered height grows with it (height:auto), so this
   spacer needs the matching 1.5x height for the text after it to clear
   the taller image instead of sitting under it. 21/1.5 = 14. */
@media (max-width: 700px) {
  .service__cover-spacer {
    aspect-ratio: 14 / 9;
  }
}

/* Backdrop, not a section-opening banner — same idea as #services'
   .services__bg-img: full-bleed, absolutely positioned, sitting behind the
   text rather than above it in flow. Natural aspect ratio, not a fixed
   box + object-fit:cover — these source photos (services-sep-*-fade.jpg,
   already ~21:9 and pre-faded to nothing at their own edges) are sized to
   be shown as-is, not cropped into a taller box. Forcing a taller fixed
   height with object-fit:cover was scaling the image up to cover it,
   which read as "huge" — this renders at the same size the original
   aspect-ratio:21/9 + object-fit:cover box did, just without the crop.

   mask-image here (see service-cover-reveal.js) still targets this element
   directly, unchanged — position:absolute doesn't affect how that script
   reads/writes it. */
.service__cover {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  max-width: none;
  overflow: hidden;
  z-index: 0;
}

.service__cover img {
  width: 100%;
  height: auto;
  display: block;
}

/* Mobile: 50% larger (20%, +20%, +10%) — negative margin re-centers it
   since it's a static, not absolutely positioned, element (unlike
   .services__bg-img's own left-offset centering trick). .service__cover's
   own overflow:hidden above clips the excess. */
@media (max-width: 700px) {
  .service__cover img {
    /* Sitewide img{max-width:100%} reset (base.css) otherwise caps this
       back down to the container width. */
    max-width: none;
    width: 150%;
    margin-left: -25%;
  }
}

/* ------------------------------------------------------------------ intro */

/* No top padding — content sits flush against the cover spacer's bottom
   edge, not offset from it by the sitewide --section-y gap. Raised above
   the backdrop photo via z-index since both now occupy the same stacking
   context (.service is position:relative). */
.service__intro {
  position: relative;
  z-index: 1;
  padding-top: 0;
}

.service .eyebrow { color: var(--c); }

/* .service__title alone couldn't beat the sitewide
   [data-tone="dark-2"] :is(h1,h2,h3){color:#fff} rule — that selector's
   specificity (an attribute plus :is()) is higher than one class, so it
   silently overrode the colour below regardless of source order. Scoping
   through .service raises this rule's own specificity above it instead of
   reaching for an ID or !important. */
.service .service__title {
  margin-top: 1.25rem;
  max-width: 60%;
  color: var(--c);
}

/* Consulting's title breaks across two lines in the copy document; each
   line stays on its own row instead of reflowing into one run (still wraps
   within itself if the column gets narrow). */
.service__title-line { display: block; }

.service__subtitle {
  margin-top: 0.75rem;
  max-width: 60%;
  color: var(--b-plus);
}

.service__lead {
  margin-top: 1rem;
  display: grid;
  gap: 0.9em;
  max-width: 60%;
  color: var(--c);
}

/* Real Estate only — each section's intro column gets tuned individually,
   not as a sitewide rule. This one needs more breathing room than the
   default 60% for its two-line title split to hold. */
#real-estate .service__title,
#real-estate .service__subtitle,
#real-estate .service__lead {
  max-width: 65%;
}

/* Marketing only — same per-section tuning as Real Estate above. */
#marketing .service__title,
#marketing .service__subtitle,
#marketing .service__lead {
  max-width: 75%;
}

/* Global Commerce only — same per-section tuning as Real Estate/Marketing above. */
#global-commerce .service__title,
#global-commerce .service__subtitle,
#global-commerce .service__lead {
  max-width: 70%;
}

/* Tablet: same idea as the mobile override below — the 60/65/70/75%
   desktop tuning left too much blank column to the right at this width.
   85% for Consulting/Marketing/Global Commerce; Real Estate gets more
   (95%) by request. */
@media (min-width: 701px) and (max-width: 900px) {
  .service .service__title,
  .service__subtitle,
  .service__lead,
  #marketing .service__title,
  #marketing .service__subtitle,
  #marketing .service__lead,
  #global-commerce .service__title,
  #global-commerce .service__subtitle,
  #global-commerce .service__lead {
    max-width: 85%;
  }

  #real-estate .service__title,
  #real-estate .service__subtitle,
  #real-estate .service__lead {
    max-width: 95%;
  }

  /* Forces the first title line to break after "Management," instead of
     after "and" (its natural wrap at 95%) or not at all (it fits on one
     line past ~820px). em, not px: --t-hero is still a fluid clamp() across
     this whole 701-900 range (tokens.css), so the line's pixel width moves
     with it — measured 13.13em to fit "Development, Management" and
     15.18em before "and" tips it over, at 701px/768px/900px alike, so 14em
     sits centred in that margin for the full range, not just one width. */
  #real-estate .service__title-line:first-child {
    max-width: 14em;
  }
}

/* Mobile: the 60/65/70/75% column widths above (each tuned for a desktop
   layout with room beside the text) were leaving 25-40% of the screen
   blank on the right, with nothing else sharing the row. Same 90% for
   all four sections instead of each one's own desktop tuning. */
@media (max-width: 700px) {
  .service .service__title,
  .service__subtitle,
  .service__lead,
  #real-estate .service__title,
  #real-estate .service__subtitle,
  #real-estate .service__lead,
  #marketing .service__title,
  #marketing .service__subtitle,
  #marketing .service__lead,
  #global-commerce .service__title,
  #global-commerce .service__subtitle,
  #global-commerce .service__lead {
    max-width: 90%;
  }
}

/* ------------------------------------------------------------------ pills */
/* Tabs, not decoration — role="tablist"/"tab" wiring lives in the markup,
   js/service-carousel.js keeps aria-selected and the active carousel slide
   in sync. Outline only, no fill, so the row reads as a set of choices
   rather than buttons that have already been pressed. */
/* padding-inline matches .service__slide's own 25px x-padding — the pills
   row now lines up with the carousel content below it instead of sitting
   flush with the container edge while the carousel is inset. */
.service__pills {
  margin-top: clamp(2rem, 1.2rem + 3vw, 3rem);
  padding-inline: 25px;
  display: flex;
  flex-wrap: nowrap;
  gap: 0.75rem;
}

.service__pill {
  flex: none;
  border: 1px solid rgba(255, 255, 255, 0.8);
  border-radius: 110px;
  padding: 6px 16px;
  background: none;
  color: rgba(255, 255, 255, 0.8);
  font-size: var(--t-body);
  line-height: var(--t-body-lh);
  font-weight: var(--w-medium);
  white-space: nowrap;
  transition: color var(--dur) var(--ease), border-color var(--dur) var(--ease);
}

/* Tablet: same problem as mobile below (a nowrap row of 3 pills, one of
   them "Project Management and Supervision", is wider than the column),
   same fix — 2-line wrapping pills at a smaller corner radius instead of
   single-line capsules. Also drops the row's own 25px inset (see the pill
   row/carousel note above) so it lines up flush with the intro text's
   edge, same reasoning as the mobile override further down. */
@media (min-width: 701px) and (max-width: 900px) {
  .service__pill {
    font-size: var(--t-disclaim);
    line-height: var(--t-disclaim-lh);
    flex: 1 1 0;
    white-space: normal;
    text-align: center;
    border-radius: 6px;
  }

  .service__pills {
    padding-inline: 0;
  }
}

/* Mobile: the 3 pills in .t-disclaim's smaller scale instead of .t-body,
   and — trying this instead of the nowrap row overflowing off-screen —
   each pill takes an even third of the row and wraps its own label onto
   2 lines rather than staying on one long unbroken line. */
@media (max-width: 700px) {
  .service__pill {
    font-size: var(--t-disclaim);
    line-height: var(--t-disclaim-lh);
    flex: 1 1 0;
    white-space: normal;
    text-align: center;
    /* The 110px pill radius above suited a single-line capsule — now that
       labels wrap to multiple lines, that reads as an odd rounded blob. */
    border-radius: 6px;
  }

  /* "Remove padding in the buttons" meant this row's own left/right inset
     (on top of .container's gutter), not the pills' own inner padding —
     that stays at the base rule's 6px 16px above. */
  .service__pills {
    padding-inline: 0;
  }

  /* "Project Management and Supervision" → "Management and Supervision"
     on this one Real Estate pill (see content.json), mobile-only. */
  .service__pill-mobile-hide {
    display: none;
  }
}

/* No font-weight change on hover/active — weight stays var(--w-medium)
   throughout, only colour marks the state now. */
.service__pill:hover,
.service__pill[aria-selected="true"] {
  color: var(--accent-deep);
  border-color: var(--accent-deep);
}

/* --------------------------------------------------------------- carousel */

.service__carousel {
  /* 5px less than the original clamp(1.5rem, 1rem + 2vw, 2.5rem). */
  margin-top: calc(clamp(1.5rem, 1rem + 2vw, 2.5rem) - 5px);
  overflow: hidden;
}

/* Was 500ms var(--ease), then 650ms with an expo-out curve that jumped
   fast right out of the gate before lazily tailing off — the fast start
   read as a jolt ("violent"), not smooth, whatever the tail did.
   cubic-bezier(0.4, 0, 0.2, 1) (Material's "standard" easing) ramps in
   and out gently on both ends instead of snapping into motion, and 900ms
   is slower throughout, not just at the tail. */
.service__track {
  display: flex;
  transition: transform 900ms cubic-bezier(0.4, 0, 0.2, 1);
}

/* Mobile: a real gap between slides, so panning between them doesn't read
   as one slide's edge touching the next's — service-carousel.js's
   transform reads this value back out and accounts for it. */
@media (max-width: 700px) {
  .service__track {
    gap: 20px;
  }
}

/* The 25px x-padding lives here, on each slide, not on .service__carousel.
   Padding on the overflow:hidden container itself only shifts the visible
   window inward — it doesn't stop the NEXT slide's edge from painting into
   that inset gap, since clipping still happens at the container's outer
   (unpadded) box. Measured directly: with padding on .service__carousel, a
   sliver of the following slide was visible in the last 25px before the
   edge. box-sizing:border-box keeps the padding inside the 100% flex-basis
   instead of adding to it (the sitewide default is content-box — only
   ::after gets border-box, see base.css).

   Opacity/translate below layer a subtle fade-up onto the pill-click
   switch itself (not the one-off scroll-arrival reveal in service-intro.js
   — this fires every time [inert] toggles, i.e. every pill click), same
   gentler curve/timing as the track's own transform above so the two read
   as one smooth motion. Toned down alongside that fix — 16px/0.4 opacity
   was enough drop, combined with the horizontal slide, to itself read as
   part of the jolt; 8px/0.7 is a quieter lift that doesn't compete. */
.service__slide {
  flex: 0 0 100%;
  min-width: 0;
  box-sizing: border-box;
  padding-inline: 25px;
  display: grid;
  grid-template-columns: minmax(0, 5fr) minmax(0, 7fr);
  /* 6px less than the original clamp(1.5rem, 0.5rem + 3vw, 3.5rem). */
  gap: calc(clamp(1.5rem, 0.5rem + 3vw, 3.5rem) - 6px);
  align-items: start;
  opacity: 0.7;
  translate: 0 8px;
  transition: opacity 900ms cubic-bezier(0.4, 0, 0.2, 1), translate 900ms cubic-bezier(0.4, 0, 0.2, 1);
}

.service__slide:not([inert]) {
  opacity: 1;
  translate: 0 0;
}

/* No fixed aspect-ratio here — the source photos aren't all 4:3, and
   .figure's own img rule (width/height:100%, object-fit:cover) would crop
   them to fit a box shape that doesn't match. height:auto lets each image
   keep its native ratio and just scale down to the column's width instead
   of being cropped. */
.service__slide-figure {
  border-radius: 4px;
}

.service__slide-figure img { height: auto; }

.service__slide-copy {
  display: grid;
  gap: 0.9em;
  text-align: left;
  padding-right: 100px;
}

/* Tablet: drops the slide's own 25px inset (see the pill row/carousel note
   above .service__pills) so it lines up flush with the intro text's edge.
   The image column also grows from 5fr to 6fr against the copy's 7fr-to-6fr
   — 6/(6+6) = 50% vs. the base rule's 5/(5+7) = 41.67%, a 20% relative
   increase in the image's share of the row, by request. Cropped to 4:3
   instead of carrying each photo's own natural ratio (the base rule's
   height:auto below) — height:100% re-enables .figure's own
   width/height:100%+object-fit:cover (sections.css) to fill that box. */
@media (min-width: 701px) and (max-width: 900px) {
  .service__slide {
    padding-inline: 0;
    grid-template-columns: minmax(0, 6fr) minmax(0, 6fr);
  }

  /* The base rule's 100px right padding is a desktop-only allowance (see
     the mobile override further down, which drops it for the same reason)
     — at tablet the copy column is already narrow, made narrower still by
     the image column's 20% growth above, so 100px was eating close to a
     third of it as dead space. */
  .service__slide-copy {
    padding-right: 0;
  }

  .service__slide-figure {
    aspect-ratio: 4 / 3;
  }

  .service__slide-figure img {
    height: 100%;
  }
}

/* Mobile: image-over-text instead of the two-column 5fr/7fr split above —
   at mobile widths that copy column (minus its 100px right padding, a
   desktop-only allowance) was left with almost no room, which is what was
   forcing the text to overflow off both edges of the screen. Figure comes
   before copy in the markup, so a single column stacks them in the right
   order with no extra ordering needed. */
@media (max-width: 700px) {
  .service__slide {
    /* Plain block flow instead of the 2-column grid above — grid (even
       explicit grid-template-rows written as minmax(0, auto), even with
       min-height:0 on the figure, even with the figure's own box made via
       height:0 + padding-bottom instead of aspect-ratio so it has no
       content-dependent size at all) kept computing row 1's track taller
       than the figure's own rendered box, by a different amount per
       slide — some CSS Grid auto-track-sizing quirk that survived every
       attempt to make the item's contribution deterministic. Plain block
       flow has no track-sizing indirection to get that wrong: the
       figure's rendered height IS the space it takes up, full stop. */
    display: block;
    /* Both 0 — the image needs to reach the same right edge as the pills
       row/button above it. The gap between slides while panning is
       handled by .service__track's own gap below instead (which
       service-carousel.js's transform accounts for), not by insetting
       each slide's own content. */
    padding-inline: 0;
    /* The 8px vertical settle on top of the track's own horizontal pan
       read as two competing motions at once — horizontal only here. */
    translate: 0;
  }

  .service__slide-copy {
    /* Base rule's 18px (at this width) row-gap — replicated as margin-top
       since block flow (above) has no gap property of its own to carry
       this. */
    margin-top: 25px;
    padding-right: 0;
  }

  /* Full width, height cropped to 80% of the natural ratio (measured off
     Consulting's own first slide: 1920x1213, so 1920/(1213*0.8), i.e.
     height = width / 1.978 = width * 0.5056) via .figure's own
     object-fit:cover — overriding the height:auto above, which let each
     photo's differing natural ratio through uncropped.

     height:0 + padding-bottom:% instead of aspect-ratio: grid items
     otherwise contribute their own min/max-content size as part of the
     row's auto track sizing, and — even with min-height:0 and the row's
     own track written as minmax(0, auto) — taller-natured photos were
     still inflating row 1 past this box's own aspect-ratio height,
     pushing row 2 (the copy) down by the difference and making the
     image-to-text gap inconsistent slide to slide. Zeroing height
     outright and building the box from padding-bottom (a % of the box's
     own WIDTH, per spec, regardless of content) instead severs that
     content dependency completely rather than fighting the grid sizing
     algorithm over it. */
  .service__slide-figure {
    height: 0;
    padding-bottom: 50.56%;
  }

  .service__slide-figure img {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
  }
}

.service__slide-title { color: var(--accent-deep); }

/* Every carousel item's title is forced to two lines (the array carries
   the break, same mechanism as .service__title-line above), even for
   titles that would otherwise fit on one — a deliberate rhythm across the
   three slides, not a wrap fallback. */
.service__slide-title-line { display: block; }

.service__slide-copy p { color: rgba(255, 255, 255, 0.8); }

/* -------------------------------------------------------------------- cta */

.service__cta {
  /* Halved from the original clamp(3rem, 1.8rem + 4.5vw, 5.5rem) /
     clamp(1.5rem, 1rem + 2vw, 2.5rem) — together these set the gap above
     the CTA, between it and the carousel. */
  margin-top: clamp(1.5rem, 0.9rem + 2.25vw, 2.75rem);
  padding-top: clamp(0.75rem, 0.5rem + 1vw, 1.25rem);
  /* 15px less than the sitewide --section-y. */
  padding-bottom: calc(var(--section-y) - 15px);
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 15px;
}

/* Tablet: same row arrangement as desktop (image, title, button inline via
   the base rule's flex/justify-content:center above) — just narrowed to
   90% and centered, matching the intro column/carousel's own tablet
   tuning above rather than running the full container width. */
@media (min-width: 701px) and (max-width: 900px) {
  .service__cta {
    max-width: 90%;
    margin-inline: auto;
  }

  /* The base .service__cta-title rule (further down, unconditional) also
     sets flex:none at equal specificity and comes later in the file, so it
     was winning the cascade here regardless — the title stayed at its
     unwrapped max-content width, which fit fine at the full container
     width but overflowed both edges of the row once max-width:90% above
     narrowed it (image and button each pushed ~58px past the row's own
     box, off both sides at once). The extra .service ancestor raises this
     rule's specificity above the base rule's so it wins on its own merits,
     not source order — same trick as .service .service__title earlier in
     this file. flex:1 1 auto + min-width:0 lets the title shrink into
     whatever space is left over after the image/button (still flex:none,
     so they keep their own size) instead of forcing the row wider than
     its box. */
  .service .service__cta-title {
    flex: 1 1 auto;
    min-width: 0;
    /* 4px down from the .t-subtitle class's own 21px at this width
       (tokens.css's tablet --t-subtitle) — scoped here rather than on the
       shared token, which .service__subtitle above also uses. */
    font-size: 1.0625rem;
  }
}

/* Mobile: stacked instead of a row — image, then centered text, then the
   button centered below it, across all four services' CTAs. */
@media (max-width: 700px) {
  .service__cta {
    flex-direction: column;
  }

  .service__cta-title {
    text-align: center;
  }
}

/* 50% bigger than the original 3.5rem, then another 10% on top. */
.service__cta-image {
  flex: none;
  height: 5.775rem;
  width: auto;
  display: block;
}

/* Slightly tighter than .t-subtitle's own 1.364 default. */
.service__cta-title {
  flex: none;
  line-height: 1.2;
  color: var(--c);
}

.service__cta-title-line { display: block; }

/* Same size/shape as #chain's own "Get in touch" (.cta__button in
   sections.css: 20px radius, 10px/20px padding, .t-body at weight 700) —
   only the colour treatment differs. That one is solid white with dark
   text; this one is an outline on the flat --a-plus ground here, so solid
   white would compete with the section's own white body copy instead of
   reading as a button.

   Border is 1px, matching .service__pill's own outline weight, not 2px —
   a thicker border adds extra height on top of the same padding, which
   made the identical 20px radius read as less rounded than the pill's or
   #chain's own button even though the radius value itself was the same. */
.service__cta-button {
  flex: none;
  border-radius: 20px;
  border: 1px solid rgba(255, 255, 255, 0.8);
  background: none;
  color: rgba(255, 255, 255, 0.8);
  padding: 8px 15px;
  font-size: 15px;
  line-height: var(--t-body-lh);
  font-weight: 600;
  letter-spacing: normal;
}

/* -------------------------------------------------------- shared accordion */
/* Still used by Why Mosagate's accordion (see .accordion--plain in
   home.html) — services no longer use this component, but it isn't
   orphaned, so it stays. */

.accordion {
  border-top: 2px solid var(--rule);
}

.accordion__item {
  border-bottom: 2px solid var(--rule);
  position: relative;
}

.accordion__item::before {
  content: "";
  position: absolute;
  left: 0;
  top: -1px;
  bottom: -1px;
  width: 2px;
  background: var(--accent);
  scale: 1 0;
  transform-origin: top;
  transition: scale var(--dur-panel) var(--ease);
}

.accordion__item[data-open="true"]::before { scale: 1 1; }

.accordion__header {
  width: 100%;
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: center;
  gap: clamp(0.75rem, 0.4rem + 1vw, 1.5rem);
  padding: clamp(1rem, 0.7rem + 0.9vw, 1.4rem) 0
           clamp(1rem, 0.7rem + 0.9vw, 1.4rem) clamp(1rem, 0.5rem + 1.2vw, 1.75rem);
  text-align: left;
  color: var(--ink-muted);
  transition: color var(--dur) var(--ease);
}

.accordion__header:hover { color: var(--ink); }

.accordion__item[data-open="true"] .accordion__header { color: var(--ink); }

/* No index column — title takes the leading track instead of the middle
   one, mark stays flush right. Same component, same colours, just without
   the "01/02/03" numbering. */
.accordion--plain .accordion__header {
  grid-template-columns: minmax(0, 1fr) auto;
}

/* Why Mosagate's accordion only: no open-state side rule, the title itself
   (not the whole row) turns Colour B+ when open instead, tighter rows
   throughout. */
.accordion--plain .accordion__item::before { content: none; }

.accordion--plain .accordion__item[data-open="true"] .accordion__title {
  color: var(--b-plus);
}

.accordion--plain .accordion__header {
  /* "a little" tighter between rows */
  padding-top: clamp(0.8rem, 0.56rem + 0.72vw, 1.1rem);
  /* half the gap between the title and the description below it */
  padding-bottom: clamp(0.5rem, 0.35rem + 0.45vw, 0.7rem);
}

/* Same weight and colour as the hairline dividers around it, not the header
   text colour it used to inherit. */
.accordion--plain .accordion__mark::before,
.accordion--plain .accordion__mark::after {
  height: 2px;
  background: var(--rule);
}

.accordion__title {
  font-size: var(--t-head);
  line-height: var(--t-head-lh);
  font-weight: var(--w-semi);
  color: inherit;
}

/* A plus that becomes a minus. Two rules, one of which rotates away. */
.accordion__mark {
  position: relative;
  width: 14px;
  height: 14px;
  flex: none;
}

.accordion__mark::before,
.accordion__mark::after {
  content: "";
  position: absolute;
  inset: 50% 0 auto 0;
  height: 1px;
  background: currentColor;
}

.accordion__mark::after {
  rotate: 90deg;
  transition: rotate var(--dur-panel) var(--ease), opacity var(--dur) var(--ease);
}

.accordion__item[data-open="true"] .accordion__mark::after {
  rotate: 0deg;
  opacity: 0;
}

/* The height animation. 0fr → 1fr lets the panel ease to its natural height
   without anyone measuring anything in JavaScript. */
.accordion__panel {
  display: grid;
  grid-template-rows: 0fr;
  transition: grid-template-rows var(--dur-panel) var(--ease);
}

.accordion__item[data-open="true"] .accordion__panel {
  grid-template-rows: 1fr;
}

.accordion__panel-inner {
  overflow: hidden;
  min-height: 0;
}

.accordion__body {
  padding: 0 clamp(0.75rem, 0.4rem + 1vw, 1.5rem)
           clamp(1.1rem, 0.8rem + 1vw, 1.6rem) clamp(1rem, 0.5rem + 1.2vw, 1.75rem);
  display: grid;
  gap: 0.9em;
  max-width: 54ch;
  color: var(--ink-muted);
  opacity: 0;
  transition: opacity var(--dur) var(--ease);
}

.accordion__item[data-open="true"] .accordion__body {
  opacity: 1;
  transition-delay: 120ms;
}


/* carousel.css */
/* =========================================================================
   Teaser carousel — the four stages of the chain, each pointing at its service.

   The track is a native overflow-x scroller with x-mandatory snapping, so drag,
   swipe, trackpad and keyboard all work with no script. carousel.js only adds
   the arrows and dots on top of that.
   ========================================================================= */

.carousel {
  position: relative;
  display: grid;
  gap: 0;

  /* Entrance timing for the title only — the cards were tried as an entrance
     (a translate, then a flash-in wave) and both read badly enough that the
     cards are staying as plain static content. Deliberately NOT --dur-panel:
     this needs to be slow enough to be read as a sequence, which is a good
     deal slower than a panel needs to open.

     The title's own timing is not a single number here — it is a running
     total (a fast run of keystrokes, then a beat, then the next word), which
     cannot be expressed as an index times a step, so chain-intro.js computes
     each character's delay and writes it on the element. */
}

.carousel__head {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 1.5rem;
}

/* Groups the eyebrow with the title so both stack as one flex item in
   .carousel__head's row, instead of the eyebrow sitting beside the title
   as a second row item once .carousel__controls (hidden at this width)
   stops occupying the other slot. */
.carousel__title-group { display: flex; flex-direction: column; align-items: center; }

.carousel__title-group .eyebrow { margin-top: 8px; }

/* Tablet-only: 5px lower than the 8px above. Scoped so desktop and phones
   keep the shared spacing (see #chain-title's own tablet block in
   sections.css, which pulls the title onto one line at this width). */
@media (min-width: 701px) and (max-width: 900px) {
  /* 33px = the 13px this ran at + 20px. Pushing the eyebrow down carries
     the h2 with it — they're a flex column — so both drop together. */
  .carousel__title-group .eyebrow { margin-top: 33px; }
}

/* Matches #chain's own padding-top (sections.css) so the title sits centered
   in the free space between the hero above and the cards below, not hugging
   either edge. */
.carousel__head h2 { text-align: center; padding-bottom: clamp(1.25rem, 0.75rem + 1.75vw, 2.5rem); }

/* The track fits all four cards without scrolling at desktop widths (see
   .carousel__track), so the prev/next arrows and dots have nothing to do —
   shown only where the track actually scrolls (mobile, below). */
.carousel__controls { display: none; }

.carousel__track {
  display: grid;
  /* Lifts the row of cards without touching the h2's padding-bottom above —
     that value is deliberately matched to #chain's own padding-top so the
     title stays centered in the space it sits in, and trimming it to move
     the cards would quietly break that. */
  margin-top: -20px;
  /* This box scrolls horizontally, and per spec a browser cannot leave one
     axis of overflow genuinely open once the other is anything but visible
     — overflow-x:auto silently forces overflow-y to auto too, whatever it's
     left at. That clips anything that moves past this box's own bottom
     edge, including the cards' own entrance below: they rise up FROM 38px
     below their resting place, so that bleed was being cut off — visible as
     a hard-edged mask across the bottom of each card while it was still
     mid-rise. padding-bottom reserves that 38px inside the scrollable
     region (the forced clip happens at the padding edge, not the content
     edge, so this genuinely uncrops it) and the matching negative margin
     below cancels the padding back out again for layout, so the space
     above .cta is unchanged. */
  padding-bottom: 44px;
  margin-bottom: -44px;
  /* Cards flow to fit their content (the fixed-width .carousel__figure
     drives the card's width, not the other way round) rather than stretch
     to fill the row — the group of four sits centered in the track, with
     any leftover width on wide viewports becoming margin around the whole
     block. minmax(0, 244px), not a bare max-content: the columns must still
     be able to shrink below that on viewports too narrow to fit all four at
     full size, or the row overflows past the edge instead of adapting (see
     .carousel__figure / .carousel__text for the matching min()-capped
     sizing that lets the image and text shrink with it). Narrower viewports
     still fall back to the scrollable, fixed-width behaviour below 640px,
     where the arrows and dots earn their keep. */
  /* 244px = the 212px image + the card's 15px side padding + its 1px border,
     doubled. Sized off the card's own parts so the image keeps its intended
     width instead of being squeezed by the padding. */
  grid-template-columns: repeat(4, minmax(0, 244px));
  justify-content: center;
  gap: 15px;
  padding-inline: 30px;
  overflow-x: auto;
  overscroll-behavior-x: contain;
  scroll-snap-type: x mandatory;
  scrollbar-width: none;
}

.carousel__track::-webkit-scrollbar { display: none; }

/* A 5th grid item, appended by carousel.js after the 4 real cards, mobile
   only — a plain clone of the first card, not a distinct element of its
   own (see "loop preview" in carousel.js). Hidden here on desktop/tablet,
   where grid-template-columns is an explicit 4-column list — a stray 5th
   item would otherwise flow into an unplanned implicit column and disturb
   the centered layout. Decorative only: aria-hidden and unlinked, real
   navigation to Structure still goes through the real first card, and this
   copy can't intercept a stray tap even without JS re-checking anything. */
.carousel__card[data-carousel-clone] { display: none; pointer-events: none; }

/* Colour A+ at 40% behind the card's own text — the parallax photo runs
   right behind these cards, and a flat border alone left the title/text
   sitting directly on whatever part of the image happened to be there. */
.carousel__card {
  position: relative;
  scroll-snap-align: start;
  border: 1px solid rgba(255, 255, 255, 0.6);
  border-radius: 8px;
  padding: 10px 15px 15px;
  display: flex;
  flex-direction: column;
  background: color-mix(in srgb, var(--a-plus) 40%, transparent);
  transition: background var(--dur) var(--ease);
}

.carousel__card:hover { background: rgba(56, 160, 50, 0.07); }

/* Same shape as .hero__lead--intro (hero.css) — opacity/translate fade-up,
   same easing, same idea of one entrance language across the page — but the
   rise itself is bigger (38px vs. the lead's 22px): a card is a much larger
   visual mass than a line of text, and the lead's own travel read as barely
   there at this size. Owned by chain-intro.js, not a generic reveal: only
   appears once the title's scramble is mostly done (see LEAD_EARLY_MS-
   equivalent overlap there). If that script never runs, .carousel__card has
   no base opacity/translate to key off, so cards stay fully visible with no
   transition — never silently stuck hidden. */
.carousel__card.carousel__card--intro {
  opacity: 0;
  translate: 0 38px;
  transition: background var(--dur) var(--ease), opacity 650ms var(--ease), translate 900ms var(--ease);
}

.carousel__card--intro.is-lit {
  opacity: 1;
  translate: 0;
}

/* Pinned to the bottom-right corner regardless of how tall the text above
   runs — margin-top:auto eats the remaining space in the flex column, and
   grid's default row-stretch already keeps all four cards the same height.
   No ring anymore — just the arrow, coloured with --rule like the card's own
   outline. */
.carousel__go {
  position: absolute;
  /* Tracks the card's own padding, so the arrow stays level with the stage
     label and flush with the text column rather than being left behind. */
  top: 10px;
  right: 15px;
  display: grid;
  place-items: center;
  color: rgba(255, 255, 255, 0.6);
  transition: color var(--dur) var(--ease);
}

/* The icon sprite is stroke-width:1.25 on a 24-unit grid, tuned for the
   sizes used elsewhere (nav, service marks). At 1rem/16px, --icon-stroke-width:3
   scales that up to a clean 2px rendered stroke — the same weight as the
   card's own 2px outline. */
/* Default sprite stroke-width (1.25 on a 24-unit grid) at this render size
   already lands almost exactly on 1px — matches the card's own 1px border. */
.carousel__go .icon { width: 1.2rem; height: 1.2rem; }

.carousel__card:hover .carousel__go { color: var(--accent); }

.carousel__stage {
  color: var(--c);
  font-size: 18px;
  letter-spacing: 0;
  font-family: "IBM Plex Serif", Georgia, serif;
  text-transform: none;
  text-shadow: 0 4px 10px rgba(0, 49, 31, 1);
}

/* Line-height tightened for this spot only — .t-subtitle elsewhere (e.g. the
   services intro) keeps its usual --t-subtitle-lh. */
.carousel__division {
  color: var(--ink);
  margin-top: 10px;
  font-weight: var(--w-semi);
  line-height: 1.2;
  text-shadow: 0 4px 10px rgba(0, 49, 31, 1);
}

/* max-width matches .carousel__figure's own sizing exactly (not a ch unit)
   — the card flows to fit its content, and the widest child sets the
   card's width, so this must never end up wider than the image. min() so
   it shrinks in step with the image on narrower viewports instead of
   forcing an overflow (see .carousel__track's column minmax). */
.carousel__text {
  color: #fff;
  margin-top: calc(0.3rem - 3px);
  max-width: min(212px, 100%);
  line-height: 18px;
  text-shadow: 0 4px 10px rgba(0, 49, 31, 1);
}

/* 212px on wide viewports (5% over the source file's native size), but
   capped to the card's own available width so it shrinks along with the
   grid column below that (see .carousel__track) rather than overflowing.
   Height is exactly the 1.82:1 ratio's own height at 212px wide — a fixed
   height rather than an aspect-ratio so it stays put even as width shrinks;
   object-fit: cover (from .figure img) crops cleanly regardless. */
.carousel__figure {
  margin-top: 7px;
  width: min(212px, 100%);
  height: calc(212px / 1.82);
  background: var(--c);
}

/* Colour A+ at 25% over every image, same trick as .figure--tinted's wash —
   an ::after, not a filter, so it sits cleanly above the img regardless of
   its own contrast/saturation. Card hover (not the figure's own :hover)
   fades it out, so pointing anywhere on the card — not just the image —
   reveals the photo underneath. */
.carousel__figure::after {
  content: "";
  position: absolute;
  inset: 0;
  background: var(--a-plus);
  opacity: 0.25;
  transition: opacity var(--dur) var(--ease);
  pointer-events: none;
}

.carousel__card:hover .carousel__figure::after {
  opacity: 0;
}

/* --------------------------------------------------------------- controls -- */

.carousel__controls {
  align-items: center;
  gap: 0.75rem;
}

.carousel__arrow {
  width: 2.75rem;
  height: 2.75rem;
  display: grid;
  place-items: center;
  border: 2px solid var(--rule-strong);
  color: var(--ink);
  transition: background var(--dur) var(--ease), border-color var(--dur) var(--ease),
              color var(--dur) var(--ease), opacity var(--dur) var(--ease);
}

/* pointer-events:none so a tap anywhere on the button always resolves to
   the <button> element itself, never this nested <svg>/<use>. Safari has
   long-standing bugs where a tap landing on a <use> that references an
   external SVG file (icons.svg#arrow-right, not an inline <symbol>) can
   fail to bubble up to the parent button — Chrome doesn't have this
   problem, which is why arrow taps could work in Chrome and silently do
   nothing in Safari. The icon fills most of the button's visible area, so
   most real taps were landing squarely on it. */
.carousel__arrow .icon { width: 1.1rem; height: 1.1rem; pointer-events: none; }

.carousel__arrow:hover:not(:disabled) {
  background: var(--accent);
  border-color: var(--accent);
  color: #fff;
}

.carousel__arrow:disabled {
  opacity: 0.3;
  cursor: default;
}

.carousel__dots {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin-inline-start: 0.5rem;
}

.carousel__dot {
  width: 1.75rem;
  height: 2px;
  background: var(--rule-strong);
  transition: background var(--dur) var(--ease);
}

.carousel__dot[aria-current="true"] { background: var(--accent); }

/* Tablet-only. Placed down here, not beside the eyebrow override above:
   these properties also exist on the base .carousel__track rule earlier in
   this file at the same specificity, so an override has to come after it to
   win. */
@media (min-width: 701px) and (max-width: 900px) {
  /* Drop the track's own 30px side padding — .container's gutter (32.5px at
     this width) already holds the cards off the edge, so the two stacked to
     62.5px a side. The row isn't scrollable here (all four columns fit), so
     that padding bought nothing and only squeezed the columns: reclaiming
     60px gives each card ~15px more width, which the card text needs.
     padding-inline only — padding-bottom stays, it reserves the space the
     cards' entrance rises through. */
  .carousel__track {
    /* 15px lower than the shared -20px, which had pulled the row up toward
       the title. */
    margin-top: -5px;
    padding-inline: 0;
    /* 5px tighter than the shared 15px — with the columns already capped by
       minmax(0, 244px), the reclaimed 15px goes to the cards themselves. */
    gap: 10px;
  }

  /* Tighter than the shared 10px 15px 15px — buys 10px of content width per
     card on top of the track padding above. */
  .carousel__card { padding: 8px 10px 10px; }

  /* Follows the padding above. This arrow is absolutely positioned to sit
     level with the stage label and flush with the text column (see its base
     rule), so it has to track any padding change or it drifts out of
     alignment with both. */
  .carousel__go { top: 8px; right: 10px; }
}

@media (max-width: 700px) {
  .carousel__track {
    grid-template-columns: none;
    grid-auto-flow: column;
    /* 10px narrower than a plain 82% so the next card peeks in 10px
       further — the current/left card just gives up that 10px, which is
       fine, it's still comfortably the dominant card in view. One value,
       not a per-column list: the 5th grid item (see the loop-preview
       clone below) is exactly the same size as the 4 real cards, so
       there's nothing left to special-case — the last real card gets a
       "next card" peeking beside it exactly like cards 1-3 do, because
       as far as the grid is concerned it's just another card in the row. */
    grid-auto-columns: calc(82% - 10px);
    /* A scrollable grid centred with justify-content clips the same amount
       off the start as it reserves at the end — the first card opened
       already scrolled in rather than flush against the padding. start
       keeps the first card fully in view, with the next one peeking in at
       the edge (grid-auto-columns above leaves room for exactly that). */
    justify-content: start;
    /* #chain-title's own padding-bottom (sections.css) is 50px — this stays
       inside that budget (30px) so the pull-up never eats into the title's
       actual text, only the blank padding below it, leaving a 20px gap to
       the cards (15px more than the -45px this was before). */
    margin-top: -30px;
    order: 2;
    /* Leading edge: padding-inline-start:0 leaves the first card sitting
       exactly at the .container's own gutter — the SAME indent
       #chain-title and the "WHAT WE DO" eyebrow start at directly above
       it. Tried giving this edge a fixed, literal padding instead (a
       plain number, independent of the gutter) to see if a static value
       would be more robust — measured it directly and it changes nothing
       visible: scroll-snap-type:x mandatory (below) auto-scrolls past
       any leading space that isn't itself a valid snap point, landing on
       the first card's own edge regardless of how much padding sits
       before it. The on-screen position is set by where the TRACK's own
       box starts, not by its padding — so that's the only lever that
       actually moves this edge. Trailing edge: padding-inline-end:0 plus
       the matching margin-inline-end below still bleeds the TRACK's own
       box past the gutter, all the way to the true viewport edge —
       that's what lets a peeking NEXT card's right edge run flush to the
       screen while browsing cards 1-3. */
    padding-inline: 0;
    margin-inline-end: calc(-1 * var(--gutter));
  }

  /* Shows the loop-preview clone (see carousel.js) that carousel.js appends
     after the 4 real cards on this breakpoint. Without a 5th item here, the
     last real card has nothing to peek beside it and no room to reach its
     own scroll-snap-align:start position — the track's native scrollable
     range would end exactly at its trailing edge, short of what
     start-aligning it needs, so the browser clamps short: the last card
     pinned to the screen's trailing edge with the previous card clipped
     and peeking on the left. Since the carousel loops back to Structure
     after Grow anyway, the "5th card" is honestly just Structure again —
     showing it as the peek is both true to where Next actually goes AND
     what finally gives the last real card room to land flush-left like
     every other one. */
  .carousel__card[data-carousel-clone] { display: flex; }

  /* Same proportions as the shared 10px 15px 15px, scaled down slightly —
     the card is narrower here than on tablet/desktop, so its padding gets
     the same treatment. */
  .carousel__card { padding: 8px 12px 12px; }

  /* Follows the padding above, same reasoning as the tablet override. */
  .carousel__go { top: 8px; right: 12px; }

  /* The shared rule caps the figure at a fixed 212px — on this narrower
     card that leaves ~25px of empty card past the image's right edge.
     100% fills the card's own content box instead; aspect-ratio (mobile-only,
     10% shorter than the 212:116.48 ratio the fixed height was reproducing:
     116.48 * 0.9 = 104.832) keeps the image's proportions as that width
     changes rather than the fixed height stretching it. */
  .carousel__figure { width: 100%; height: auto; aspect-ratio: 212 / 104.832; margin-top: 7px; }

  /* Bottom side of the same image padding — the shared rule's 10px
     (margin-top on .carousel__division, the gap between the image and the
     title below it) tightened to 7px on mobile, matching the 7px top. */
  .carousel__division { margin-top: 7px; }

  /* The shared rule's max-width: min(212px, 100%) (above) stays pinned to
     the old 212px figure width — since the figure just went full-width
     above, that fixed cap now leaves the text ~65px short of the card's
     actual content width, wrapping it early for no reason. 100% matches
     the figure's own width here, same relationship the shared rule's
     comment describes for its own 212px value. */
  .carousel__text { max-width: 100%; }

  /* .carousel__head becomes a transparent wrapper on mobile so its children
     (title-group, controls) place directly into .carousel's own grid next
     to .carousel__track — that's what lets `order` below move the arrows/
     dots to sit after the cards instead of above them. */
  .carousel__head { display: contents; }
  /* 15px, then another 15px, then 10px back up — sits 20px below where the
     shared rules would otherwise place it. */
  .carousel__title-group { align-items: flex-start; order: 1; margin-top: 20px; }
  .carousel__head h2 { text-align: left; }

  /* 11px tighter than the shared 1.25rem (6px, then another 5px), and
     word-spacing back to normal — the sitewide 0.3em tracking (kept on
     desktop/tablet) reads as oversized gaps between these four short words
     at phone width. */
  #chain-title { margin-top: calc(1.25rem - 11px); word-spacing: normal; }

  /* .carousel__track's padding-bottom/margin-bottom pair (base rule) already
     cancel out, landing the next grid item flush with the cards' own
     visible edge — so this margin-top is the real, full gap beneath them.
     That cancelled-out padding-bottom (44px, base rule) is still part of
     .carousel__track's own box even though it's visually invisible — and
     it overlaps roughly the top 24px of these buttons as a result. Per
     spec, grid items are supposed to paint in `order`-adjusted order (this
     is order:3, .carousel__track is order:2, so this should already paint
     on top) — but that's exactly the part real-world testing found Safari
     doesn't reliably honor here, falling back to raw DOM source order
     instead, where .carousel__track (later in the HTML than this) paints
     OVER the top portion of these buttons instead of under them. The
     practical result: taps landed on the lower ~12px of each circular
     button and worked, taps a few px higher hit the track's invisible
     padding instead and did nothing. position:relative + z-index makes
     the stacking explicit rather than leaning on `order`-affects-paint,
     which removes the ambiguity Safari was getting wrong. */
  .carousel__controls {
    display: flex;
    justify-content: flex-end;
    order: 3;
    margin-top: 20px;
    position: relative;
    z-index: 1;
  }

  /* Smaller than the shared 2.75rem — sized for a thumb-width tap target
     rather than the desktop/tablet row where these buttons have room to
     spare next to the title. Circular, and bordered the same as the cards
     (1px, rgba(255,255,255,0.6)) instead of the shared square/--rule-strong
     look, so the two read as the same visual language. */
  .carousel__arrow {
    width: 2.25rem;
    height: 2.25rem;
    border-radius: 50%;
    border: 1px solid rgba(255, 255, 255, 0.6);
  }
  /* Slightly thicker than the sprite's 1.25 default. */
  .carousel__arrow .icon { width: 0.95rem; height: 0.95rem; --icon-stroke-width: 1.75; }

  /* The dash/dot row is dropped on mobile — arrows alone are enough here. */
  .carousel__dots { display: none; }
}


/* ====================================================== entrance ========== */
/* Entirely owned by chain-intro.js, not by any CSS armed/is-armed class pair
   — that script sets and clears each letter's opacity directly (see the
   note on .carousel__scramble below for why). If that script never runs,
   nothing here was ever hidden and the section reads exactly as it did
   before any of this existed. */

/* The spans are added by the script around the h2's own text, so the heading
   still reads as one sentence to a screen reader. inline-block here keeps a
   word from being broken across lines now that it is a run of separate
   character boxes rather than one word. */
.carousel__word { display: inline-block; }

/* The letter-scramble effect — same mechanism, same tuning, as the hero
   title's (see .hero-scramble in hero.css and the constants at the top of
   chain-intro.js): each character flickers through random glyphs and locks
   into its real one as a sweep passes it, rather than the flat typed
   reveal this replaced. The transition is what keeps a glyph swap from
   reading as a hard strobe — each change dips the letter's opacity and
   eases it back up rather than snapping in at full brightness. Opacity
   itself is set inline by chain-intro.js, not by a CSS armed/is-armed pair
   — that script hides every letter in the same synchronous pass that
   builds it, for the same reason hero-intro.js does (see the note at the
   top of that file): a browser cannot paint mid-script, so there is no
   task in which a letter could exist visible and correct before its turn. */
.carousel__scramble {
  transition: opacity 150ms var(--ease);
}


/* forms.css */
/* =========================================================================
   Contacts — details block and the enquiry form.
   ========================================================================= */

/* --------------------------------------------------------------- details -- */

/* Left column, directly under .split__head (see .split--contacts). */
.contacts__details {
  grid-area: details;
  display: grid;
  gap: clamp(1.5rem, 1rem + 1.4vw, 2.25rem);
  align-self: start;
}

/* Tablet: Address, Email and See Location were 3 stacked rows in a single
   column (the base rule's plain display:grid, no columns set) — a 2-column
   grid instead, Address spanning both rows on the left, Email and See
   Location stacked beside it on the right. nth-child, not a dedicated
   class per block, since the markup doesn't otherwise distinguish them
   (home.html: 3 plain .contacts__block divs in address/email/location
   order). */
@media (min-width: 701px) and (max-width: 900px) {
  .contacts__details {
    grid-template-columns: 1fr 1fr;
    grid-template-areas: "address email" "address location";
  }

  .contacts__block:nth-child(1) { grid-area: address; }
  .contacts__block:nth-child(2) { grid-area: email; }
  .contacts__block:nth-child(3) { grid-area: location; }
}

/* Mobile: pushed down 15px more from the lead paragraph above it — Address,
   Email, See Location and (in flow) the form after it all move with it. */
@media (max-width: 700px) {
  .contacts__details {
    margin-top: 15px;
  }
}

.contacts__block { display: grid; gap: 0.5rem; }

.contacts__label { color: var(--ink-faint); }

.contacts__value { color: var(--ink); font-style: normal; }

a.contacts__value:hover { color: var(--a); }

.contacts__map-link {
  display: inline-flex;
  align-items: center;
  gap: 0.4em;
}

.contacts__map-link .icon {
  width: 1em;
  height: 1em;
  transition: translate var(--dur) var(--ease);
}

.contacts__map-link:hover .icon { translate: 0.25em 0; }

/* ------------------------------------------------------------------ form -- */

/* Right column, spanning both rows of .split--contacts (see sections.css).
   Capped narrower than the column itself — full column width read as too
   wide for a handful of short fields. */
.contacts__form { grid-area: form; max-width: 70%; margin-left: 20px; }

/* Tablet: the 20px-offset above is a desktop-only nudge (see .split's own
   40fr/60fr column split, sections.css) — most of this width range stacks
   .split to a single column (max-width:860px there), where that offset
   just reads as off-centre rather than shifted next to anything. Centred
   instead, keeping the same 70% cap. */
@media (min-width: 701px) and (max-width: 900px) {
  .contacts__form {
    margin-left: auto;
    margin-right: auto;
  }
}

/* Mobile: the 70%/20px-offset above was tuned for the desktop 2-column
   split — in the single-column stack the fields end up both narrower and
   more indented than the rest of the section's content. Full width,
   lined back up with everything else, and pushed down 25px from "See
   Location" above it. */
@media (max-width: 700px) {
  .contacts__form {
    max-width: none;
    margin-left: 0;
    margin-top: 25px;
  }
}

.form { display: grid; gap: clamp(0.7rem, 0.5rem + 0.6vw, 1rem); }

.field { display: grid; gap: 0.5rem; }

.field label { color: var(--ink-muted); }

/* Boxed fields (full border, not underline-only) — same field "box" the
   Uniasset build uses, kept in our own border token/weight, with a slight
   4px radius rather than Uniasset's own rounding or this site's usual
   square corners. */
.field input,
.field textarea {
  width: 100%;
  /* Was 0.7rem 0.9rem — trimmed vertically so the fields don't run so
     tall, horizontal padding untouched. */
  padding: 0.5rem 0.9rem;
  background: transparent;
  border: 1.5px solid var(--rule-strong);
  border-radius: 4px;
  color: var(--ink);
  font-size: var(--t-body);
  line-height: var(--t-body-lh);
  transition: border-color var(--dur) var(--ease);
}

/* Was 6rem, then 4.5rem — halved again by request. */
.field textarea { resize: vertical; min-height: 2.25rem; }

@media (max-width: 700px) {
  .field textarea { min-height: 3.5rem; }
}

.field input:focus,
.field textarea input:focus,
.field textarea:focus {
  outline: none;
  border-color: var(--focus);
}

.field input:focus-visible,
.field textarea:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 4px;
}

.field input::placeholder,
.field textarea::placeholder { color: var(--ink-faint); }

/* The honeypot. Off-screen rather than display:none, which some bots skip. */
.field--trap {
  position: absolute;
  left: -9999px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}

.form__footer {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  margin-top: 0.5rem;
}

/* Rest state: outlined, not filled — same gray border weight/colour as the
   fields above it. Scoped by #id so it wins over both .cta__button's own
   white-fill rule and the generic cream-tone [data-tone="cream"]
   .button:hover rule (sections.css) outright, rather than fighting either
   on source order. */
#contact-form .cta__button {
  background: transparent;
  border: 1.5px solid var(--rule-strong);
  color: var(--ink);
}

#contact-form .cta__button:hover {
  background: var(--b-plus);
  border-color: var(--b-plus);
  color: #fff;
}

/* Turnstile's own widget chrome renders taller than our button — scale it
   down and pull it left so the two sit level, same as the Uniasset build. */
.cf-turnstile {
  transform: scale(0.85);
  transform-origin: left center;
  margin-right: -45px;
}

/* Status messages are rendered from ?sent=1 / ?error=1 after the PHP redirect. */
.form__status {
  padding: 0.9rem 1.1rem;
  border-left: 2px solid var(--a);
  background: rgba(56, 160, 50, 0.1);
  color: var(--ink);
  font-size: var(--t-disclaim);
  line-height: var(--t-disclaim-lh);
}

.form__status[data-state="error"] {
  border-left-color: #c4573f;
  background: rgba(196, 87, 63, 0.12);
}


/* footer.css */
/* =========================================================================
   Footer.
   ========================================================================= */

.site-footer {
  background: var(--surface-dark);
  color: var(--on-dark);
  border-top: 2px solid var(--rule-dark);
  padding-top: clamp(1.75rem, 1rem + 2.1vw, 2.8rem);
  /* 30px less than the original clamp(2.5rem, 1.5rem + 3vw, 4rem). */
  padding-bottom: calc(clamp(2.5rem, 1.5rem + 3vw, 4rem) - 30px);
}

.site-footer__top {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  justify-content: space-between;
  gap: 1.75rem;
  padding-bottom: clamp(1.25rem, 0.8rem + 1.5vw, 2rem);
}

/* -10%, then -15%, then +20% off the original clamp(120px, 11vw, 150px).
   Scoped by .site-footer__brand (two classes) rather than the bare
   .site-footer__logo class alone — the logo is also wrapped in .brand
   (shared with the header), and header.css's ".brand img" (class+type)
   otherwise outranks a single-class selector and wins regardless of
   source order. */
.site-footer__brand .site-footer__logo { width: clamp(110px, 10.1vw, 138px); height: auto; }

.site-footer__brand {
  display: flex;
  align-items: center;
  gap: 1.5rem;
}

/* One column per page, its sub-pages stacked underneath. Columns without
   children (Contacts, Privacy Policy) are just the one link. Fixed gap,
   not fluid — the columns should read the same width apart at any
   viewport, not compress/expand with it. */
.footer-nav {
  display: flex;
  flex-wrap: wrap;
  gap: 55px; /* 40px + 15px */
}

/* Tablet: at this width the base rule's flex-wrap was wrapping .footer-nav
   onto its own row below the logo entirely (brand + all 4 columns together
   didn't fit one line), and even then Contacts/Privacy still didn't fit
   next to Home/Mosagate/Services and wrapped to a 2nd row of its own — a
   4th of the block essentially wasted as air below the logo. A 2-column
   grid (logo | nav) forces them onto one row regardless of content width,
   and folding Home into the same column as Mosagate (stacked above it,
   own row-gap for spacing) drops the nav from 4 page-groups to 3, which is
   what actually makes everything fit beside the now-narrower logo. */
@media (min-width: 701px) and (max-width: 900px) {
  .site-footer__top {
    display: grid;
    grid-template-columns: auto 1fr;
    align-items: start;
    /* Up from 1.5rem — logo and nav read as too close together at that
       gap, by request. */
    gap: 3.25rem;
  }

  /* -10% off the base rule's flat 110px at this width (10.1vw is under the
     110px floor everywhere in 701-900px, so the clamp sits on its floor
     throughout — see the base rule's own comment). */
  .site-footer__brand .site-footer__logo {
    width: 99px;
  }

  /* 3 columns instead of the implicit 4 (Home was its own column, wrapping
     independently) — column 1 carries Home stacked above Mosagate via
     grid-template-areas, columns 2/3 (Services, Contacts) each span both
     rows since neither has a second item stacked under it. */
  .footer-nav {
    display: grid;
    grid-template-columns: repeat(3, auto);
    grid-template-areas:
      "home     services contacts"
      "mosagate services contacts";
    /* Down from the base rule's 55px (then 36px) — 3 narrower columns
       need less air between them than the original 4 did. */
    column-gap: 22px;
    /* Proper spacing between the folded-in Home link and the Mosagate
       group beneath it, not the tight 0.6rem .footer-nav__col's own
       gap uses between a heading and its own children. */
    row-gap: 14px;
  }

  .footer-nav__col:nth-child(1) { grid-area: home; }
  .footer-nav__col:nth-child(2) { grid-area: mosagate; }
  .footer-nav__col:nth-child(3) { grid-area: services; }
  .footer-nav__col:nth-child(4) { grid-area: contacts; }

  /* 1px down from the base rule's 13px (--t-disclaim), on both the column
     headings and the links themselves. The extra .footer-nav ancestor
     raises this rule's specificity above the base .footer-nav__heading /
     .footer-nav__col a rules further down the file (same selector
     otherwise, so without it source order would let those later,
     unconditional rules win regardless of this media query — same trick
     as .service .service__cta-title in services.css). */
  .footer-nav .footer-nav__heading,
  .footer-nav .footer-nav__col a {
    font-size: 0.75rem;
  }

  /* "International Presence" is the one link long enough to noticeably
     widen the whole Mosagate column beyond its 3 siblings (What We Do,
     About Us) — wrapped to 2 lines instead, by request. 140px sits
     between the measured width of "International" alone (99px, so it
     still fits on line 1) and the full "International Presence" (212px,
     so it can't). Last link in the Mosagate column (see home.html/
     content.json — What We Do, About Us, International Presence, in that
     order), so :last-child rather than a dedicated class. */
  .footer-nav__col:nth-child(2) a:last-child {
    max-width: 140px;
  }
}

/* Mobile: logo and nav centred as a block instead of the space-between
   row above, and more breathing room between the 4 page-groups than the
   desktop gap. */
@media (max-width: 700px) {
  .site-footer__top {
    flex-direction: column;
    align-items: center;
    /* Base rule's 1.75rem gap between brand and nav, increased. */
    gap: 2.75rem;
  }

  .footer-nav {
    flex-direction: column;
    align-items: center;
    gap: 24px;
  }

  .footer-nav__col {
    justify-items: center;
  }
}

/* No min-width — a column should hug its own longest word, not pad out
   to a fixed box. Otherwise the flex gap is even box-to-box but the
   visible gap between the actual words reads uneven, since short
   columns (Home, Contacts) would carry blank space the eye still counts
   as part of the gap. */
.footer-nav__col {
  display: grid;
  align-content: start;
  gap: 0.6rem;
}

.footer-nav__heading {
  font-size: var(--t-disclaim);
  line-height: var(--t-disclaim-lh);
  font-weight: var(--w-semi);
  letter-spacing: var(--track-nav);
  text-transform: uppercase;
  color: rgba(255, 255, 255, 0.6);
}

.footer-nav__col a {
  color: rgba(255, 255, 255, 0.6);
  font-size: var(--t-disclaim);
  line-height: var(--t-disclaim-lh);
  font-weight: var(--w-semi);
  letter-spacing: var(--track-nav);
  text-transform: uppercase;
  transition: color var(--dur) var(--ease);
}

.footer-nav__col a:hover { color: #fff; }

.site-footer__bottom {
  padding-top: clamp(1rem, 0.7rem + 1.1vw, 1.5rem);
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  justify-content: space-between;
  gap: 1.25rem;
}

.site-footer__legal {
  color: rgba(255, 255, 255, 0.6);
  font-size: var(--t-micro);
  line-height: var(--t-micro-lh);
  max-width: 62ch;
}

.site-footer__legal a { color: rgba(255, 255, 255, 0.6); text-decoration: underline; text-underline-offset: 0.25em; }
.site-footer__legal a:hover { color: var(--on-dark); }

@media (max-width: 700px) {
  .site-footer__legal-break {
    display: none;
  }
}

/* Right side of the bottom row: social icons, credit line below, both
   right-aligned into the corner. */
.site-footer__aside {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 0.75rem;
}

/* Mobile: bottom row stacked and centred, icons above the disclaimer
   text — display:contents on .site-footer__aside drops its own box out
   of layout so .footer-social and .site-footer__credit become direct
   flex items of .site-footer__bottom (without moving anything in the
   HTML), letting them sit in the same order list as .site-footer__legal
   instead of being stuck together as an inseparable block. */
@media (max-width: 700px) {
  .site-footer__bottom {
    flex-direction: column;
    align-items: center;
  }

  .site-footer__aside {
    display: contents;
  }

  .footer-social {
    order: 1;
    align-self: center;
    translate: 0;
  }

  .site-footer__legal {
    order: 2;
    text-align: center;
  }

  .site-footer__credit {
    order: 3;
    align-self: flex-end;
  }
}

/* 1px under .site-footer__legal's own .t-micro (11px) — a credit line
   reads as a footnote to that text, one notch quieter still. */
.site-footer__credit {
  font-size: 10px;
  line-height: var(--t-micro-lh);
}

.site-footer__credit a { color: rgba(255, 255, 255, 0.6); text-decoration: none; }
.site-footer__credit a:hover { color: #fff; }

/* LinkedIn + Email — same circular-badge treatment and position (right side
   of the bottom row, level with the legal text) as the Uniasset build. */
.footer-social { display: flex; gap: 0.75rem; flex: none; translate: 0 -10px; }

.footer-social__link {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 36px;
  height: 36px;
  border-radius: 50%;
  border: 1.5px solid rgba(255, 255, 255, 0.3);
  color: rgba(255, 255, 255, 0.7);
  transition: border-color var(--dur) var(--ease), color var(--dur) var(--ease);
}

.footer-social__link .icon { width: 16px; height: 16px; }

.footer-social__link:hover { border-color: #fff; color: #fff; }

/* --------------------------------------------------------- legal pages --- */
/* Privacy Policy is the only other page. Same .split layout as #presence
   (eyebrow+title / lead / body-in-the-map-slot) and the same soft
   white-to-cream gradient #presence and #contacts share. It now also gets
   the same pattern-divider-as-first-child treatment as #presence/#contacts
   (see #privacy in sections.css), which overrides this padding-top entirely
   (id beats class) — left here as the fallback for any future .doc page
   that doesn't carry its own divider. */

.doc {
  padding-top: calc(var(--header-h) + var(--section-y));
}

#privacy { background: linear-gradient(to top, color-mix(in srgb, var(--c) 60%, white) 0%, #fff 100%); }

.doc__body {
  grid-area: map;
  max-width: 74ch;
  display: grid;
  gap: 1.1em;
  color: var(--ink-muted);
  align-self: start;
}

/* Sized as .t-body-strong (15px/22, semibold) rather than the page-title
   scale's .t-subtitle — headings this size read as loud in a text-only
   legal column. Grid `gap` (1.1em, shared with every other row) is the
   space below; pulled in 60% with a negative margin-bottom rather than
   touched globally, since that gap also spaces plain paragraph-to-paragraph
   pairs elsewhere in this body that aren't part of the ask. */
.doc__body h2 {
  margin-top: 1.4em;
  margin-bottom: calc(-0.6 * 1.1em);
  font-size: var(--t-body);
  line-height: var(--t-body-lh);
  font-weight: var(--w-semi);
}

.doc__body h2:first-child { margin-top: 0; }

.doc__body ul {
  display: grid;
  gap: 0.5em;
  padding-left: 1.1em;
  list-style: disc;
}

.doc__body a {
  color: var(--a);
  text-decoration: none;
}

