/* =================================================================
   ICON GROUP — HOME PAGE
   ================================================================= */

/* ---------- Tokens ---------- */
:root {
  --color-icon-blue: #00b8f1;
  --color-dark-blue: #14294f;
  --color-icon-deep-blue: #112343;
  --color-black: #000000;
  --color-white: #ffffff;
  --color-white-70: rgba(255, 255, 255, 0.7);
  --color-white-45: rgba(255, 255, 255, 0.45);
  --color-white-40: rgba(255, 255, 255, 0.4);
  --color-white-30: rgba(255, 255, 255, 0.3);
  --color-white-20: rgba(255, 255, 255, 0.2);
  --color-grey-101: #f7f8f9;
  --color-grey-900: #232527;
  --color-body-grey: #6d6e71;

  --font-primary: "Work Sans", "Segoe UI", Arial, sans-serif;

  --page-margin: 48px;
  --content-max: 1920px;
  /* Real height of the fixed/overlay .nav bar, kept in sync by JS
     (initNavHeightVar in js/main.js). This fallback matches desktop
     before that first measurement lands. Anything sitting under the
     nav in a section that must clear it (e.g. .page-intro) should add
     this to its own design spacing rather than guessing a static
     top offset — the nav is an overlay, not a document-flow block, so
     nothing gets a spacer; only content that needs the clearance asks
     for it explicitly. */
  --nav-height: 89px;

  --space-xs: 80px;
  --space-s: 96px;
  --space-m: 128px;
  --space-l: 144px;
  --space-xxs: 64px;

  --radius-s: 8px;
  --radius-xs: 4px;

  --color-grey-600: #8d8f93;
  --color-mid-grey: #aaacb1;
  --color-light-grey: #dcdddf;
}

/* ---------- Reset ---------- */
* { box-sizing: border-box; }
html { scroll-behavior: smooth; }
/* Any in-page anchor target (e.g. Build Your Career's pathway links)
   clears the fixed nav using the same --nav-height convention as
   .page-intro's own top padding, instead of a per-section offset. */
[id] { scroll-margin-top: var(--nav-height); }
body {
  margin: 0;
  font-family: var(--font-primary);
  color: var(--color-white);
  background: var(--color-white);
  overflow-x: hidden;
}
img { max-width: 100%; display: block; }
a { color: inherit; text-decoration: none; }
h1, h2, h3, p { margin: 0; }
ul, ol { margin: 0; padding: 0; list-style: none; }
/* padding/margin: 0 matters as much as the background/border reset —
   without it every plain <button> keeps the UA default inline padding
   (Chrome: ~6px left/right), which shows up as unwanted leading space
   before a button's own text/icon that an equivalent <a> never had
   (e.g. .btn-link's "Apply now" CTAs on Job Detail). Every button
   component on the site already sets its own explicit box model
   (.nav__toggle, .profile-modal__close, .fp-arrow, etc.), so zeroing
   these here is safe — those rules simply override it, same as this
   reset already zeroes ul/ol's own UA margin/padding above. */
button { font-family: inherit; background: none; border: none; margin: 0; padding: 0; cursor: pointer; }

/* ---------- Type utilities ---------- */
.h2 {
  font-size: 48px;
  font-weight: 600;
  line-height: 58.67px;
  letter-spacing: -0.72px;
  color: var(--color-white);
}
.h2--dark { color: var(--color-dark-blue); }
.h2--card { color: var(--color-white); line-height: 58.67px; }
.h5 {
  font-size: 24px;
  font-weight: 600;
  line-height: 28.8px;
  letter-spacing: -0.24px;
  color: var(--color-white);
}
.h5--dark { color: var(--color-dark-blue); }
.h5--blue { color: var(--color-icon-blue); }
/* Figma's "Heading/H6 Regular" token — 18px semibold, distinct from
   .eyebrow's 16px UPPERCASE "H6 Regular - Uppercase" variant. Already
   used ad hoc (identical values, never named) by .leadership-quote__name
   and .project-quote__cite; promoted to a real, reusable class here so
   future instances (like Sustainability's lead-in lines below) reuse it
   by name instead of re-declaring the same five properties again. */
.h6 {
  font-size: 18px;
  font-weight: 600;
  line-height: 27.2px;
  letter-spacing: -0.18px;
  color: var(--color-white);
}
.h6--dark { color: var(--color-dark-blue); }
.body-lg {
  font-size: 18px;
  font-weight: 400;
  line-height: 27.53px;
  letter-spacing: -0.18px;
  color: var(--color-white-70);
}
.body-lg--grey { color: var(--color-body-grey); }
.body-xl {
  font-size: 22px;
  line-height: 33.65px;
  letter-spacing: -0.22px;
  color: var(--color-white);
  max-width: 663px;
}
.text-blue { color: var(--color-icon-blue); }
.text-white { color: var(--color-white); }
.text-white-70 { color: var(--color-white-70); }
.text-white-40 { color: var(--color-white-40); }
.text-black { color: var(--color-black); }

/* Approved Figma arrow (node 21080:13957), recoloured via mask +
   currentColor so one file covers every CTA colour variant instead of
   duplicating files per colour.
     .icon-arrow            -> arrow.svg            (normal text CTAs)
     .icon-arrow-direction  -> arrow-direction.svg   (carousel prev/next) */
.icon-arrow {
  display: inline-block;
  width: 11px;
  height: 11px;
  flex: 0 0 11px;
  background-color: currentColor;

  -webkit-mask-image: url("../assets/icons/arrow.svg");
  mask-image: url("../assets/icons/arrow.svg");

  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;

  -webkit-mask-position: center;
  mask-position: center;

  -webkit-mask-size: contain;
  mask-size: contain;
}
.icon-arrow-direction {
  display: inline-block;
  width: 16px;
  height: 16px;
  flex: 0 0 16px;
  background-color: currentColor;

  -webkit-mask-image: url("../assets/icons/arrow-direction.svg");
  mask-image: url("../assets/icons/arrow-direction.svg");

  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;

  -webkit-mask-position: center;
  mask-position: center;

  -webkit-mask-size: contain;
  mask-size: contain;
}

.eyebrow {
  font-size: 16px;
  font-weight: 600;
  line-height: 27.2px;
  letter-spacing: -0.18px;
  text-transform: uppercase;
  margin: 0;
}
/* The eyebrow element itself is the sticky positioning target (see the
   anchor rules below). Its content sits in a separate inner element,
   .eyebrow__track, which is what GSAP animates — keeping the scroll-
   scrubbed transform off the sticky element avoids the two fighting. */
.eyebrow__track {
  display: inline-flex;
  align-items: center;
  gap: 10px;
}
.eyebrow--blue { color: var(--color-icon-blue); }
.eyebrow--dark { color: var(--color-black); }
.eyebrow--white { color: var(--color-white); }
.eyebrow__dot {
  width: 6px;
  height: 6px;
  background: var(--color-icon-blue);
  transform: rotate(45deg);
  flex-shrink: 0;
}
.eyebrow__dot--dark { background: var(--color-black); }
.eyebrow__dot--white { background: var(--color-white); }

.btn-link {
  display: inline-flex;
  align-items: center;
  gap: 12px;
  font-size: 18px;
  font-weight: 600;
  line-height: 27.53px;
  letter-spacing: -0.18px;
  padding-bottom: 7px;
  position: relative;
  /* Shrink-to-content: the underline must hug the link's own text/arrow
     width, not stretch across a flex-column parent (e.g. Latest News'
     intro column defaults flex children to align-items: stretch). */
  align-self: flex-start;
}
/* Animated underline — unlike the nav/tag/menu rule-fills (which start
   hidden and sweep in on hover), .btn-link's underline is already fully
   visible at rest, so a single scaleX(0->1) fill can't express the
   requested motion. Two stacked 1px fills replace the old static
   border-bottom instead of adding a second one:
     ::after  — the resting line. Anchored at its RIGHT edge so that on
                hover it retracts scaleX(1->0) toward the right and
                disappears there, rather than shrinking back the way the
                nav fills do.
     ::before — the incoming line. Anchored at its LEFT edge, starts at
                scaleX(0) (invisible), and only grows in scaleX(0->1)
                once ::after has fully cleared (transition-delay equal to
                ::after's own duration) — so the two fills never overlap
                mid-animation.
   Both stages reuse the exact 0.45s cubic-bezier(0.4, 0, 0.2, 1) timing
   as .about-menu__rule-fill / .tabbed-content__tab-rule-fill elsewhere on
   the site, so the sweep speed matches even though the shape of the
   motion here is different. Un-hovering mid-sequence is handled for
   free: each fill's own transition (declared once, outside :hover, with
   no delay) just interpolates back to its resting value — ::after back
   to scaleX(1)/right, ::before back to scaleX(0)/left — from wherever it
   currently sits, so the underline always settles back to a full line
   with no jump. Text/arrow/opacity are untouched; only these two fills
   move. */
.btn-link::before,
.btn-link::after {
  content: "";
  position: absolute;
  left: 0;
  bottom: 0;
  width: 100%;
  height: 1px;
  background: currentColor;
  transition: transform 0.45s cubic-bezier(0.4, 0, 0.2, 1);
}
.btn-link::after {
  transform-origin: right center;
  transform: scaleX(1);
}
.btn-link::before {
  transform-origin: left center;
  transform: scaleX(0);
}
.btn-link:hover::after,
.btn-link:focus-visible::after {
  transform: scaleX(0);
}
.btn-link:hover::before,
.btn-link:focus-visible::before {
  transform: scaleX(1);
  transition-delay: 0.45s;
}
.btn-link--blue { color: var(--color-icon-blue); }
.btn-link--white { color: var(--color-white); }
/* --sm — Build Your Career's pathway links use Figma's smaller
   "Button/Button Regular" type (16px/24.47px) instead of .btn-link's
   own 18px default; a size modifier alongside --blue/--white rather
   than a page-specific override. */
.btn-link--sm { font-size: 16px; line-height: 24.47px; letter-spacing: -0.16px; }
/* --xs — Figma's "Button/Button Small" type (14px/16.82px), used by
   Opportunity Results' per-card "View position" CTA where the button
   sits inline with a row of tags instead of on its own line. */
.btn-link--xs { font-size: 14px; line-height: 16.82px; letter-spacing: -0.14px; }

.caption-label {
  font-size: 16px;
  font-weight: 600;
  line-height: 27.2px;
  letter-spacing: -0.18px;
  text-transform: uppercase;
  color: var(--color-white-70);
}
.caption-sub {
  font-size: 12px;
  font-weight: 400;
  line-height: 18.35px;
  letter-spacing: -0.12px;
  color: var(--color-white-70);
}

.tag {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 6px 12px;
  border: 1px solid var(--color-white-70);
  border-radius: 1009px;
  font-size: 14px;
  line-height: 21.41px;
  letter-spacing: -0.14px;
  color: var(--color-white-70);
  white-space: nowrap;
}

/* =================================================================
   SITE FRAME + GRID — shared desktop layout system
   Figma is authored on a 1920px canvas. Section BACKGROUNDS stay
   full-viewport (never constrained), but their CONTENT is centred
   inside a 1920px frame so the composition doesn't stretch apart on
   ultrawide monitors. Pattern:

     full-width section (background)
       > .site-frame (max-width: 1920px, centred)
         > .site-grid (12 columns, 24px gap, 48px gutter) — only where
           a section's top-level children genuinely need explicit
           column placement. Sections whose internal layout is already
           correct relative math (not viewport math) only need
           .site-frame, not a grid conversion — see individual
           component comments.

   Figma's recurring widths map onto this 12-column grid exactly:
     130=1col 284=2col 438=3col 592=4col 746=5col 900=6col 1054=7col
     1208=8col 1362=9col 1516=10col 1824=12col (all cols)

   THIS IS THE ONE SITE-WIDE LAYOUT CONVENTION. Home and About both
   build on these same two primitives — no page gets its own frame or
   grid system. Two application patterns, pick whichever fits:
     1. The content element carries no background of its own (its
        background comes from an ancestor, e.g. .hero__copy, .stats,
        .section--split, .featured-projects__track) → add the
        `site-frame` class straight onto that existing element. No
        wrapper needed, nothing else about it changes.
     2. The content element's background IS the section's own
        full-bleed background (e.g. .section--light, .footer) → do
        NOT cap that element itself (it would clip the background).
        Instead move its horizontal padding onto a new inner
        `site-frame` wrapper around its children, and let the outer
        element keep painting edge-to-edge.
   ================================================================= */
.site-frame {
  width: 100%;
  max-width: 1920px;
  margin-inline: auto;
}
.site-grid {
  display: grid;
  grid-template-columns: repeat(12, minmax(0, 1fr));
  column-gap: 24px;
  padding-inline: var(--page-margin);
}
/* Below the desktop breakpoint every section that uses .site-grid
   collapses back to a plain block so the existing tablet/mobile
   stacking rules (already written per-component) keep working
   unmodified — the grid is a desktop-only placement mechanism. */
@media (max-width: 1199px) {
  .site-grid { display: block; padding-inline: var(--page-margin); }
}

/* =================================================================
   NAVIGATION — dissolves between empty / full states
   ================================================================= */
.nav {
  position: fixed;
  top: 0; left: 0; right: 0;
  z-index: 100;
}
.nav__bar {
  /* Solid dark-blue is the site-wide default (background spans the
     full viewport edge-to-edge, never constrained — the
     .nav__bar-inner .site-frame below is what keeps the actual nav
     content centred inside the 1920px canvas on wider monitors
     instead of hugging the physical screen edges). Only the
     `.nav--transparent` variant below (the three full-image-hero
     templates) overrides this to start transparent. */
  background-color: var(--color-dark-blue);
  transition: background-color 0.35s ease;
}
/* Grid (not flex) so .nav__links can start at the exact same
   grid-column-3 line every .site-grid page-intro/content column starts
   at (see .page-intro__content, grid-column: 3 / span 10) — "About"
   lines up with the h1 above/below it because both are positioned off
   the same 12-column math, not because of a hand-tuned pixel offset.
   Same 12 columns/24px gap/page-margin padding as .site-grid, so the
   available track width is identical to every .site-frame.site-grid
   section (.nav__bar-inner is already a .site-frame, same max-width/
   centering). Only applies >=1200px — <1199px reverts to plain flex
   (see the hamburger-menu breakpoint below), since .nav__links is
   hidden there and grid-column placement has nothing left to align. */
.nav__bar-inner {
  position: relative;
  display: grid;
  grid-template-columns: repeat(12, minmax(0, 1fr));
  column-gap: 24px;
  align-items: center;
  padding: 20px var(--page-margin);
}
/* =================================================================
   NAV — transparent variant (`.nav--transparent`, added in markup)
   Reserved for the three full-image-hero templates (index.html,
   project-galkangu-bendigo-govhub.html,
   article-lorne-street-student-accommodation.html) where the nav
   sits directly over a full-width hero image. Every other page keeps
   the solid `.nav__bar` default above — no divider, no transparency,
   no page-by-page overrides needed.
   ================================================================= */
.nav--transparent .nav__bar {
  background-color: transparent;
}
/* Divider lives on .nav__bar-inner (the .site-frame), not full-bleed
   .nav__bar — left/right: var(--page-margin) inset it to the exact
   same column edge every .site-grid section's content starts at,
   instead of running edge-to-edge across the viewport. Only exists
   for the transparent variant; solid pages get no divider at all. */
.nav--transparent .nav__bar-inner::after {
  content: "";
  position: absolute;
  left: var(--page-margin);
  right: var(--page-margin);
  bottom: 0;
  height: 1px;
  background: var(--color-white-30);
  transition: opacity 0.35s ease;
}
.nav--transparent.nav.is-scrolled .nav__bar,
.nav--transparent.nav.is-open .nav__bar,
.nav--transparent.nav.is-nav-hover .nav__bar {
  background-color: var(--color-dark-blue);
}
.nav--transparent.nav.is-scrolled .nav__bar-inner::after,
.nav--transparent.nav.is-open .nav__bar-inner::after,
.nav--transparent.nav.is-nav-hover .nav__bar-inner::after {
  opacity: 0;
}

/* flex-shrink: 0 on both the link and the image is the actual fix here —
   .nav__logo was the only flex child in .nav__bar without flex-shrink:0,
   so any overflow pressure from the row's other (fixed-width) content
   was being absorbed by squeezing the logo image narrower than its real
   proportions. Explicit width+height (matching the asset's true 175:48
   ratio, also set as HTML attributes on the <img> itself) removes any
   remaining ambiguity in how the browser sizes the SVG. */
.nav__logo { grid-row: 1; grid-column: 1 / span 2; justify-self: start; display: flex; align-items: center; flex-shrink: 0; }
.nav__logo img { display: block; height: 48px; width: 175px; flex-shrink: 0; }
.nav__links {
  /* grid-row: 1 is load-bearing, not decorative: .nav__utility's column
     span (1 / -1) overlaps cells .nav__logo/.nav__links already occupy
     in row 1, and an item with an explicit column but auto row gets
     pushed to the next free row rather than allowed to overlap — which
     silently doubled the nav bar's height (a real "48px 21px" two-row
     grid) until all three were pinned to row 1 explicitly. */
  grid-row: 1;
  grid-column: 3 / span 10;
  /* justify-self: start shrinks this grid item to its own content width
     (its links + gaps) instead of the default stretch, which would fill
     the whole 10-column span all the way to column 12 — the same area
     .nav__utility (grid-column: 1 / -1) needs, so the two boxes would
     silently overlap even though the visible text never does. Shrinking
     .nav__links to just what it needs removes that overlap outright. */
  justify-self: start;
  display: flex;
  align-items: center;
  justify-content: flex-start;
  gap: 48px;
}
.nav__links a {
  font-size: 18px;
  font-weight: 600;
  line-height: 27.53px;
  letter-spacing: -0.18px;
  color: var(--color-white);
}
/* Scoped to .nav__links' own top-level items only (plain links that are
   direct children, plus the About/Careers dropdown triggers) — NOT
   `.nav__links a`, which would also match the mega-menu's nested
   .about-menu__feature/.about-menu__item anchors (they live several
   levels deeper in the DOM but are still descendants of .nav__links)
   and hijack their own separate hover/active treatment. */
.nav__links > a,
.nav__about-trigger,
.nav__careers-trigger {
  position: relative;
  color: var(--color-white);
  transition: color 0.3s ease;
}
/* Animated underline reuses the exact .about-menu__rule-fill technique
   (an absolutely-positioned fill, scaleX(0) -> scaleX(1) from the left,
   transform-transitioned rather than a border-bottom) via ::after so the
   nav markup itself doesn't need a second span per link. Default: no
   line. Hover/focus: fill sweeps left-to-right. Active (aria-current):
   fill stays permanently at scaleX(1) with transition: none, same as
   .about-menu__item[aria-current="page"] — so landing on your own
   current page shows the line immediately instead of re-animating it,
   and it never reverts to scaleX(0) after the pointer leaves. */
.nav__links > a::after,
.nav__about-trigger::after,
.nav__careers-trigger::after {
  content: "";
  position: absolute;
  left: 0;
  bottom: -6px;
  width: 100%;
  height: 1px;
  background: var(--color-icon-blue);
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform 0.45s cubic-bezier(0.4, 0, 0.2, 1);
}
.nav__links > a:hover,
.nav__links > a:focus-visible,
.nav__about-trigger:hover,
.nav__about-trigger:focus-visible,
.nav__careers-trigger:hover,
.nav__careers-trigger:focus-visible { color: var(--color-icon-blue); }
.nav__links > a:hover::after,
.nav__links > a:focus-visible::after,
.nav__about-trigger:hover::after,
.nav__about-trigger:focus-visible::after,
.nav__careers-trigger:hover::after,
.nav__careers-trigger:focus-visible::after { transform: scaleX(1); }
.nav__links > a[aria-current="page"],
.nav__about-trigger[aria-current="page"],
.nav__careers-trigger[aria-current="page"] { color: var(--color-icon-blue); }
.nav__links > a[aria-current="page"]::after,
.nav__about-trigger[aria-current="page"]::after,
.nav__careers-trigger[aria-current="page"]::after { transform: scaleX(1); transition: none; }
.nav__mobile-links a[aria-current="page"] { color: var(--color-icon-blue); }
/* .nav__utility spans the full 12 columns and self-aligns to the end,
   landing flush with the row's right edge (column 12's line) exactly
   like it did as the last child of the old space-between flex row —
   sharing a grid area with .nav__links (3 / span 10) is fine since
   each one's own content hugs its own edge (links: flex-start at
   column 3; utility: justify-self: end at the row's right edge), so
   the two never actually overlap visually. grid-row: 1 is required
   (see .nav__links above) — without it this full-width column span
   collides with cells .nav__logo/.nav__links occupy in row 1, and gets
   auto-placed into a second row instead, doubling the nav bar's height. */
.nav__utility { grid-row: 1; grid-column: 1 / -1; justify-self: end; display: flex; align-items: center; gap: 24px; }
.nav__region {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 18px;
  font-weight: 600;
  letter-spacing: -0.18px;
}
.nav__region .is-active { color: var(--color-white); }
.nav__region span:not(.is-active) { color: var(--color-white-45); }
.nav__divider { width: 1px; height: 18px; background: var(--color-white-30); }
.nav__contact {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 18px;
  font-weight: 600;
  letter-spacing: -0.18px;
  color: var(--color-white);
}

/* Mobile menu trigger + panel — hidden on desktop, enabled in the
   responsive block below a breakpoint. Kept simple: one toggle, one
   stacked link panel, reusing the same nav__region/nav__contact styles. */
.nav__toggle {
  display: none;
  flex-direction: column;
  justify-content: center;
  gap: 5px;
  width: 44px;
  height: 44px;
  flex-shrink: 0;
}
.nav__toggle-bar {
  display: block;
  width: 22px;
  height: 2px;
  margin: 0 auto;
  background: var(--color-white);
  transition: transform 0.25s ease, opacity 0.25s ease;
}
.nav.is-open .nav__toggle-bar:nth-child(1) { transform: translateY(7px) rotate(45deg); }
.nav.is-open .nav__toggle-bar:nth-child(2) { opacity: 0; }
.nav.is-open .nav__toggle-bar:nth-child(3) { transform: translateY(-7px) rotate(-45deg); }

.nav__mobile {
  display: none;
  flex-direction: column;
  gap: 24px;
  padding: 8px var(--page-margin) 32px;
  background: var(--color-dark-blue);
}
.nav.is-open .nav__mobile { display: flex; }
.nav__mobile-links { display: flex; flex-direction: column; }
.nav__mobile-links a {
  padding: 14px 0;
  border-top: 1px solid var(--color-white-20);
  font-size: 18px;
  font-weight: 600;
  letter-spacing: -0.18px;
  color: var(--color-white);
}
.nav__mobile-utility {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding-top: 8px;
}

/* ---------- About mega-menu ----------
   .nav__about stretches to the FULL height of the nav row (not just
   its own link's line-height) so the hoverable area under "About" has
   no gap between the trigger and the row's own top/bottom edge — the
   dropdown panel is a nested descendant positioned flush against the
   bar's bottom edge, so together there is no seam anywhere in this
   region for the pointer to cross. That geometry is what makes the
   open/close state (driven from JS via pointerenter/leave) flicker-
   free; see initAboutMenu in main.js. */
.nav__about {
  position: relative;
  align-self: stretch;
  display: flex;
  align-items: center;
}
.about-menu {
  position: fixed;
  left: 0;
  right: 0;
  top: 0; /* set to the live nav bar height by JS */
  background: var(--color-icon-deep-blue);
  padding: 73px 0;
  opacity: 0;
  visibility: hidden;
  transform: translateY(-8px);
  transition: opacity 0.3s ease, transform 0.3s ease, visibility 0s linear 0.3s;
  z-index: 100;
}
.nav__about.is-open .about-menu {
  opacity: 1;
  visibility: visible;
  transform: translateY(0);
  transition: opacity 0.3s ease, transform 0.3s ease, visibility 0s linear 0s;
}
/* Careers mega-menu is the exact same component (.about-menu panel,
   same markup/behaviour) under its own top-level trigger — see
   initCareersMenu in main.js, a straight copy of initAboutMenu scoped
   to [data-nav-careers] instead of [data-nav-about]. Only the wrapper
   needs its own rule pair here so its open state can be scoped
   independently of the About menu. */
.nav__careers {
  position: relative;
  align-self: stretch;
  display: flex;
  align-items: center;
}
.nav__careers.is-open .about-menu {
  opacity: 1;
  visibility: visible;
  transform: translateY(0);
  transition: opacity 0.3s ease, transform 0.3s ease, visibility 0s linear 0s;
}
.about-menu__inner {
  display: flex;
  align-items: flex-start;
  gap: 64px;
  padding-inline: var(--page-margin);
}
.about-menu__feature {
  display: flex;
  flex-direction: column;
  gap: 32px;
  flex: 0 0 435px;
  max-width: 435px;
}
.about-menu__heading {
  font-size: 42px;
  font-weight: 600;
  line-height: 56px;
  letter-spacing: -0.42px;
  color: var(--color-white);
}
.about-menu__intro {
  font-size: 20px;
  font-weight: 400;
  line-height: 30.59px;
  letter-spacing: -0.2px;
  color: var(--color-white-70);
}
.about-menu__cta { margin-top: 8px; }
.about-menu__divider {
  align-self: stretch;
  width: 1px;
  flex-shrink: 0;
  background: var(--color-white-20);
}
.about-menu__links {
  display: flex;
  flex-wrap: wrap;
  align-content: flex-start;
  gap: 48px;
  flex: 1 1 368px;
}
.about-menu__links li { flex: 0 1 368px; min-width: 280px; }
.about-menu__item {
  display: flex;
  flex-direction: column;
  gap: 20px;
  width: 100%;
}
.about-menu__rule {
  position: relative;
  width: 100%;
  height: 1px;
  background: var(--color-white-20);
}
.about-menu__rule-fill {
  position: absolute;
  top: 0; left: 0;
  height: 1px;
  width: 100%;
  background: var(--color-icon-blue);
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform 0.45s cubic-bezier(0.4, 0, 0.2, 1);
}
.about-menu__item:hover .about-menu__rule-fill,
.about-menu__item:focus-visible .about-menu__rule-fill { transform: scaleX(1); }
.about-menu__item-top {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
}
.about-menu__item-title {
  font-size: 20px;
  font-weight: 600;
  line-height: 30.59px;
  letter-spacing: -0.2px;
  color: var(--color-white);
  transition: color 0.3s ease;
}
.about-menu__item-arrow {
  color: var(--color-icon-blue);
  opacity: 0;
  transition: opacity 0.3s ease;
}
.about-menu__item-desc {
  font-size: 14px;
  font-weight: 400;
  line-height: 21.41px;
  letter-spacing: -0.14px;
  color: var(--color-white-70);
  max-width: 339px;
  transition: color 0.3s ease;
}
.about-menu__item:not([aria-current="page"]):hover .about-menu__item-title,
.about-menu__item:not([aria-current="page"]):focus-visible .about-menu__item-title { color: var(--color-icon-blue); }
.about-menu__item:not([aria-current="page"]):hover .about-menu__item-arrow,
.about-menu__item:not([aria-current="page"]):focus-visible .about-menu__item-arrow { opacity: 1; }
.about-menu__item:not([aria-current="page"]):hover .about-menu__item-desc,
.about-menu__item:not([aria-current="page"]):focus-visible .about-menu__item-desc { color: var(--color-white); }
/* Active (on the corresponding page): permanent blue title, full blue
   rule and white description — same "retain the active colouring"
   treatment as .tabbed-content__tab.is-active /
   .about-gateway__card.is-current, and likewise gated off hover above
   so landing on your own current page never re-triggers the sweep. */
.about-menu__item[aria-current="page"] .about-menu__item-title { color: var(--color-icon-blue); }
.about-menu__item[aria-current="page"] .about-menu__rule-fill { transform: scaleX(1); transition: none; }
.about-menu__item[aria-current="page"] .about-menu__item-desc { color: var(--color-white); }

@media (max-width: 1500px) {
  .about-menu { padding: 56px 0; }
  .about-menu__inner { gap: 40px; }
  .about-menu__feature { flex-basis: 360px; max-width: 360px; }
}

/* Mobile "About" becomes an accordion inside the existing stacked
   mobile link panel — same row styling as its sibling links, just
   toggling a nested sublist instead of navigating directly. */
.nav__mobile-group { display: flex; flex-direction: column; }
.nav__mobile-toggle {
  display: flex;
  align-items: center;
  justify-content: space-between;
  width: 100%;
  padding: 14px 0;
  border-top: 1px solid var(--color-white-20);
  font-size: 18px;
  font-weight: 600;
  letter-spacing: -0.18px;
  color: var(--color-white);
  text-align: left;
}
.nav__mobile-toggle.is-active > span:first-child { color: var(--color-icon-blue); }
.nav__mobile-caret {
  width: 10px;
  height: 10px;
  flex-shrink: 0;
  border-right: 1.5px solid currentColor;
  border-bottom: 1.5px solid currentColor;
  transform: rotate(45deg);
  transition: transform 0.25s ease;
}
.nav__mobile-toggle[aria-expanded="true"] .nav__mobile-caret { transform: rotate(-135deg); }
.nav__mobile-sublinks {
  display: flex;
  flex-direction: column;
  overflow: hidden;
  max-height: 0;
  transition: max-height 0.3s ease;
}
.nav__mobile-group.is-open .nav__mobile-sublinks { max-height: 320px; }
.nav__mobile-sublinks a {
  padding: 10px 0 10px 16px;
  font-size: 15px;
  font-weight: 400;
  letter-spacing: -0.15px;
  color: var(--color-white-70);
}
.nav__mobile-sublinks a[aria-current="page"] { color: var(--color-icon-blue); }

/* =================================================================
   HERO
   ================================================================= */
.hero {
  position: relative;
  min-height: 100vh; /* fallback for browsers without svh support */
  height: 100svh;
  overflow: hidden;
  background: var(--color-black);
  display: flex;
  flex-direction: column;
  justify-content: flex-end; /* content hugs the bottom, whatever the viewport height */
}
.hero__media { position: absolute; inset: 0; }
.hero__bg-image {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: cover;
  opacity: 0.2;
}
.hero__scrim {
  position: absolute; inset: 0;
  background: linear-gradient(to bottom, rgba(102, 102, 102, 0) 44%, rgba(0, 0, 0, 0.7) 100%);
}
.hero__overlay-image {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: cover;
  opacity: 0.7;
}
.hero__copy {
  /* Was absolutely pinned to top: 608px within a fixed 1053px-tall
     hero. Now the hero itself is viewport-height, so the copy block is
     laid out in normal flow and pushed to the bottom by the hero's own
     flex (justify-content: flex-end) with a fixed bottom gap — the
     same ~80px breathing room Figma showed below the content, just no
     longer dependent on a fixed hero height to compute a top offset. */
  position: relative;
  z-index: 1;
  padding: 0 var(--page-margin) 80px;
  width: 100%;
  box-sizing: border-box;
  display: flex;
  flex-direction: column;
  gap: 48px;
}
/* Masks the headline's load-in translateY so it reveals upward cleanly
   instead of sliding in over the content below it (see initHeroIntro
   in js/main.js). Purely a clipping wrapper — no layout effect. */
.hero__heading-mask { overflow: hidden; }
.hero__heading {
  font-size: 100px;
  font-weight: 600;
  line-height: 100px;
  letter-spacing: -2px;
  max-width: 870px;
  color: var(--color-white);
}
.hero__rule { height: 1px; width: 100%; background: var(--color-white-30); }
.hero__foot { display: flex; align-items: flex-end; justify-content: space-between; gap: 40px; }
.hero__actions { display: flex; gap: 40px; flex-shrink: 0; }

/* =================================================================
   STANDARD SECTION PATTERN (eyebrow + heading/content + media)
   Used by: Who We Are, Featured Sectors, Latest News, Careers CTA
   ================================================================= */
.panel--dark { background: var(--color-dark-blue); }

.section {
  position: relative;
  /* Who We Are (.section--split) is the only user of this base rule
     directly — it carries no background of its own (its parent
     .panel--dark paints that), so it gets .site-frame added straight
     onto it in markup and keeps this horizontal padding as its own
     gutter, now relative to that centred frame instead of the raw
     viewport. Featured Sectors / Latest News / Careers CTA all
     override padding below because THEIR padding sits on the same
     element as a real section background, which .site-frame must
     never cap (see the convention comment above .site-frame). */
  padding: var(--space-m) var(--page-margin);
}
.section--light { background: var(--color-grey-101); padding: var(--space-l) 0; }
.section--white { background: var(--color-white); padding: var(--space-l) 0; }
.section--split {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 40px;
}
/* Careers CTA: the section background itself is a full-viewport-width
   white-top/black-bottom split (Figma: 280px / 280px within the 560px
   section) sitting BEHIND the inset dark-blue panel — not inside it.
   Padding only insets the panel's content; the background paints the
   full box, so this single gradient reproduces the split edge-to-edge. */
.section--tight {
  padding: 0;
  background: linear-gradient(to bottom, var(--color-white) 50%, var(--color-black) 50%);
}
/* Shared inner frame for the .section family members whose padding
   can't just move onto .site-frame directly (their background is on
   the same element) — Featured Sectors, Latest News, Careers CTA. */
.section__frame { padding-inline: var(--page-margin); }

/* Shared sticky-eyebrow anchor: an absolutely-positioned, zero-footprint
   column laid over its containing block. It reserves no layout space
   (so full-width content like the sector grid / news grid is
   unaffected) but gives the sticky eyebrow the height of that
   containing block to stick within — and no further. The eyebrow's
   LOCAL ROW is its release boundary, not the whole section:
     - Who We Are: the row IS the whole section (text + image), so the
       anchor sits directly in the section.
     - Featured Sectors / Latest News: the row is just the intro
       heading row above the cards, so the anchor sits inside that row
       and releases at its bottom — never over the cards.
   .eyebrow is the sticky positioning target; .eyebrow__track (see
   above) carries the GSAP-animated transform, so the two never touch
   the same box. */
.section__eyebrow-anchor {
  position: absolute;
  top: 0;
  height: 100%;
  z-index: 2;
  pointer-events: none;
}
.section__eyebrow-anchor .eyebrow {
  position: sticky;
  top: var(--eyebrow-top, 128px);
  pointer-events: auto;
}

/* Direct sticky-eyebrow variant: same convention as the anchor above
   (stick at --eyebrow-top, release at the row's own bottom, drop to
   static below desktop — see the tablet override), but for an eyebrow
   that already sits in its own dedicated grid/flex column rather than
   a flex row it would otherwise have to be pulled out of. The anchor's
   absolute-overlay trick exists purely to avoid reserving flex-row
   space; a grid/flex column already reserves its own, so it can opt in
   with just this one class instead. Used by About's Split Feature,
   Image + List and Icon Group Carousel intro rows. */
.eyebrow--sticky {
  position: sticky;
  top: var(--eyebrow-top, 128px);
}

/* Page breadcrumb — sticky sitewide (any page using .page-intro). Same
   convention as .eyebrow--sticky above: the breadcrumb already owns its
   own grid column (.page-intro__crumb, grid-column: 1 / span 2), so it
   only needs align-self: stretch to give that column the row's real
   height (.page-intro__row uses align-items: flex-start, which would
   otherwise leave the crumb's own cell only as tall as its own text —
   the same bug that under-sized .split-feature__eyebrow before its fix)
   before position: sticky has any real room to work with. Resting
   position is untouched: the <p> itself is a plain block that still
   starts flush at the top of that (now taller) cell. */
.page-intro__crumb { align-self: stretch; }
.page-intro__crumb .breadcrumb {
  position: sticky;
  top: var(--eyebrow-top, 128px);
}

/* Section-scoped anchor (Who We Are, Careers CTA): sits directly in
   the section/panel, which strips out the section's own padding for
   absolutely-positioned children — so it re-creates that inset itself. */
.section--split > .section__eyebrow-anchor {
  left: var(--page-margin);
}
.section--split > .section__eyebrow-anchor .eyebrow {
  margin-top: var(--eyebrow-top, 128px);
}

/* Row-scoped anchor (Featured Sectors, Latest News): nested inside the
   intro row, which is already inset by the section's own padding, so
   no extra offset is needed here. */
.section__intro .section__eyebrow-anchor {
  left: 0;
}
.section__intro .section__eyebrow-anchor .eyebrow {
  margin-top: 0;
}

.section__intro {
  position: relative;
  display: flex;
  align-items: flex-start;
  gap: 24px;
  width: 900px;
  max-width: 100%;
}
.section__body-col {
  /* min(Npx, (N/1920*100)vw): exact at >=1920 (the vw side never wins
     there), scales down proportionally below it. Who We Are's row is
     entirely fixed pixel widths (308 offset + 592 + 592 media) that
     sum to exactly the full 1824 content width, so — same as the
     About SplitNarrative fix — any viewport under ~1920 overflowed
     without this. */
  width: min(592px, 30.83vw);
  display: flex;
  flex-direction: column;
  gap: 48px;
}
/* Who We Are: eyebrow column moved to the anchor above, so the body
   column carries the offset (284px eyebrow width + 24px gap) that used
   to come from sitting beside it in the row. */
.section__body-col--offset { margin-left: min(308px, 16.04vw); }
.section__copy { display: flex; flex-direction: column; gap: 18px; width: min(528px, 27.5vw); max-width: 100%; }
/* Figma: Body Copy Large / White 70% — these paragraphs previously had
   no typography rule at all and fell back to the page's plain white,
   tight-line-height body text. */
.section__copy p {
  margin: 0;
  font-size: 18px;
  font-weight: 400;
  line-height: 27.53px;
  letter-spacing: -0.18px;
  color: var(--color-white-70);
}

.section__media {
  width: min(592px, 30.83vw);
  flex-shrink: 0;
  display: flex;
  flex-direction: column;
  gap: 12px;
}
.media-card { border-radius: var(--radius-s); overflow: hidden; position: relative; height: 750px; }
.media-card img { width: 100%; height: 100%; object-fit: cover; }
.media-card::after {
  content: "";
  position: absolute; inset: 0;
  background: rgba(0, 0, 0, 0.2);
}
.media-caption { display: flex; flex-direction: column; }

/* Featured sectors intro row: eyebrow moved to the anchor (592px wide
   there), so the heading picks up that width + gap as a left offset,
   keeping the heading/description in their original positions. */
.section__intro--three {
  width: 100%;
  align-items: flex-start;
  gap: 24px;
}
.section__intro--three .h2 { width: 592px; margin-left: 616px; }
.section__intro--three .body-lg { width: 592px; max-width: 100%; }

/* Latest news intro row: eyebrow moved to the anchor, so the remaining
   heading/button block simply keeps its original right-aligned slot. */
.section__intro--news {
  width: 100%;
  justify-content: flex-end;
  gap: 24px;
}
.section__intro--news > div:last-child { width: 592px; display: flex; flex-direction: column; gap: 48px; }

/* =================================================================
   WHO WE ARE + STATS
   ================================================================= */
.stats {
  display: flex;
  align-items: flex-start;
  gap: min(178px, 9.27vw);
  padding: 0 var(--page-margin) var(--space-l);
}
.stats__media { width: min(592px, 30.83vw); flex-shrink: 0; display: flex; flex-direction: column; gap: 12px; }
.stats__media .media-card { height: 333px; }
.stats__list { flex: 1; display: flex; flex-direction: column; gap: 64px; }
.stats__row { display: flex; flex-direction: column; gap: 0; }
.stats__rule { height: 1px; background: var(--color-white-20); margin-bottom: 64px; }
.stats__item { display: flex; align-items: center; gap: 24px; }
.stats__number {
  display: flex;
  align-items: flex-end;
  width: 284px;
  flex-shrink: 0;
  font-size: 112px;
  font-weight: 700;
  line-height: 84.07px;
  letter-spacing: -4.5px;
  color: var(--color-icon-blue);
  white-space: nowrap;
}
.stats__text { flex: 1; display: flex; flex-direction: column; gap: 12px; }

/* =================================================================
   FEATURED SECTORS
   ================================================================= */
.sector-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 24px;
  margin-top: var(--space-xxs);
}
.sector-card {
  position: relative;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  height: 770px;
  padding: 48px;
  border-radius: var(--radius-s);
  overflow: hidden;
}
.sector-card img {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: cover;
  transform: scale(1);
  transform-origin: center center;
}
.sector-card__scrim {
  position: absolute; inset: 0;
  background: linear-gradient(to top, rgba(0,0,0,0.6) 0%, rgba(0,0,0,0) 40.26%);
}
.sector-card__content { position: relative; display: flex; flex-direction: column; gap: 18px; }
.sector-card__rule { height: 1px; background: var(--color-white-20); }
.sector-card__tags { display: flex; gap: 10px; flex-wrap: wrap; }

/* =================================================================
   FEATURED PROJECTS
   ================================================================= */
.featured-projects {
  position: relative;
  background: var(--color-grey-101);
}
.featured-projects::after {
  content: "";
  position: absolute;
  left: 0; right: 0; bottom: 0;
  height: 50%;
  background: var(--color-white);
  z-index: 0;
}
/* Scroll runway for the contained -> full-screen expansion (see
   initFeaturedProjectsExpansion in js/main.js). Tall enough to hold a
   controlled expand distance plus a dwell period at full-screen before
   the section releases into Latest News — not a huge empty scroll area,
   just this section's own interaction track. Mobile/reduced-motion keep
   the plain static card, so no runway is needed there. */
/* Deliberately NOT a .site-frame: this track is the full-bleed media's
   own runway. The MEDIA (this stage's image/scrim) is allowed to grow
   past the 1920px frame on ultrawide viewports so it can reach true
   100vw at full expansion; only the CONTENT layer inside it
   (.fp-inner) stays pinned to the shared site frame — see the
   frameLeft/extra-inset math in initFeaturedProjectsExpansion (js/main.js). */
.featured-projects__track { position: relative; height: 220vh; }
.featured-projects__stage {
  position: sticky;
  top: 0; /* actual value set inline by JS to sit just below the fixed nav */
  isolation: isolate;
  z-index: 1;
  margin: 0 var(--page-margin); /* JS-animated: contained (frame-relative) -> 0 (full-bleed) */
  height: clamp(620px, 72svh, 780px);
  border-radius: var(--radius-s);
  overflow: hidden;
  background: var(--color-grey-900);
  padding: var(--space-xs) 48px;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
}
/* Content layer: fills the stage's padded box exactly like the old
   direct fp-controls/fp-content children did, but carries its own
   JS-driven marginLeft/marginRight (data-fp-inner) so it can stay
   aligned to the 1920px site frame even while .featured-projects__stage
   itself is animating past it. At viewports <=1920px this extra inset
   is always 0 (identical to the pre-existing behaviour). */
.fp-inner {
  position: relative;
  z-index: 2;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  flex: 1 1 auto;
  min-height: 0;
}
.fp-slide {
  position: absolute; inset: 0;
  opacity: 0;
  transition: opacity 0.7s ease;
}
.fp-slide.is-active { opacity: 1; }
.fp-slide__image { width: 100%; height: 100%; object-fit: cover; opacity: 0.6; }
.fp-slide__scrim {
  position: absolute; inset: 0;
  background: linear-gradient(to top, rgba(0,0,0,0.6) 0%, rgba(0,0,0,0) 40.26%);
}

.fp-controls {
  position: relative;
  z-index: 2;
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 8px;
}
.fp-arrow {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 50px;
  height: 50px;
  border: 1px solid var(--color-white-70);
  border-radius: 250px;
  color: var(--color-white-70);
  transition: background 0.2s ease, color 0.2s ease;
}
.fp-arrow:hover { background: var(--color-white); color: var(--color-dark-blue); }
.fp-arrow--prev .icon-arrow-direction { transform: rotate(180deg); }
.fp-dots { display: flex; align-items: center; gap: 10px; padding: 16px 24px;
    border: 1px solid  var(--color-white-70);
    border-radius: 100px;
    height: 50px;}
.fp-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--color-white-30); cursor: pointer; transition: background 0.2s ease; }
.fp-dot.is-active { background: var(--color-white); }

.fp-content { position: relative; z-index: 2; display: flex; flex-direction: column; gap: 48px; }
.fp-fade { transition: opacity 0.4s ease; }
.fp-fade.is-fading { opacity: 0; }
.fp-content__top { display: flex; align-items: flex-end; justify-content: space-between; gap: 24px; }
.fp-content__heading { display: flex; flex-direction: column; gap: 24px; }
.fp-title { font-size: 64px; font-weight: 600; line-height: 70.4px; letter-spacing: -1px; color: var(--color-white); }
/* Right-hand rail: 284px wide, flush to the row's right edge, but its
   OWN content (values/labels, and separately the View Project CTA) is
   left-aligned within that rail — not right-aligned against it. This
   is what makes the metadata and the CTA below share the same left
   edge, matching Figma. */
.fp-content__meta { display: flex; flex-direction: column; gap: 20px; width: 284px; flex-shrink: 0; }
.fp-meta-label { font-size: 16px; line-height: 16px; letter-spacing: -0.16px; color: var(--color-white-70); }
.fp-timer { height: 1px; width: 100%; background: var(--color-white-20); position: relative; overflow: hidden; }
.fp-timer__fill { position: absolute; left: 0; top: 0; height: 100%; width: 0%; background: var(--color-icon-blue); }
.fp-content__bottom { display: flex; align-items: center; justify-content: space-between; }
.fp-tags { display: flex; gap: 10px; flex-wrap: wrap; }
.fp-content__cta { width: 284px; flex-shrink: 0; }
.tag--outline { border-color: var(--color-white-70); }
/* --muted — .tag's own white-70 border/text is tuned for dark
   grounds; Opportunity Results' pills sit inside a white card on a
   light-grey section, so this swaps in the light/mid-grey pair
   (Figma: border #dcdddf, text #aaacb1) instead of a one-off tag. */
.tag--muted { border-color: var(--color-light-grey); color: var(--color-mid-grey); }

/* =================================================================
   LATEST NEWS
   ================================================================= */
.news-grid {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 24px;
  align-items: start;
  margin-top: var(--space-xs);
}
.news-card { display: flex; flex-direction: column; gap: 18px; }
.news-card__image { border-radius: var(--radius-xs); overflow: hidden; height: 270px; }
.news-card--tall .news-card__image { height: 500px; }
.news-card--medium .news-card__image { height: 405px; }
.news-card__image img {
  width: 100%; height: 100%;
  object-fit: cover;
  transform: scale(1);
  transform-origin: center center;
}
.news-card__date {
  font-size: 11px;
  font-weight: 600;
  line-height: 16.82px;
  letter-spacing: -0.11px;
  color: var(--color-icon-blue);
}
.news-card__title {
  font-size: 24px;
  font-weight: 600;
  line-height: 28.8px;
  letter-spacing: -0.24px;
  color: var(--color-dark-blue);
}

/* ---------- media-hover: reusable masked image hover zoom ----------
   Site-wide primitive, first established here by Featured Sectors /
   Latest News. Apply .media-hover to whichever element directly frames
   the <img> (already overflow:hidden — the "mask") — the card/link
   ancestor itself is left untouched so a hover zoom never fights a
   scroll-cascade translateY transform already sitting on that ancestor
   (see js/motion.js). Hover-capable pointers only, so touch devices
   never get a stuck hover state; reduced-motion drops the transition
   entirely rather than resizing instantly. */
.media-hover img { transition: transform 650ms cubic-bezier(0.22, 1, 0.36, 1); }
@media (hover: hover) and (pointer: fine) {
  .media-hover:hover img,
  .media-hover:focus-within img {
    transform: scale(1.04);
  }
}
@media (prefers-reduced-motion: reduce) {
  .media-hover img { transition: none; }
}

/* =================================================================
   CAREERS CTA
   ================================================================= */
/* careers-cta.jpg is the complete, finished visual (people + dark-blue
   treatment already baked in) — used directly as the panel's own
   background image. No separate image element, no CSS effects on top. */
.careers-cta {
  position: relative;
  border-radius: var(--radius-s);
  overflow: hidden;
  background-color: var(--color-dark-blue);
  background-image: url("../assets/images/careers/careers-cta.jpg");
  background-size: cover;
  background-position: center;
  background-repeat: no-repeat;
}
/* Stationary content — not part of the shared scroll-cascade system —
   so this is plain stacked block spacing, not a flex/animation group. */
.careers-cta__content {
  position: relative;
  z-index: 1;
  padding: 96px 0 96px 128px;
  width: 720px;
  max-width: 100%;
}
.careers-cta__content .eyebrow { margin-bottom: 24px; }
.careers-cta__content .h2 { width: 592px; max-width: 100%; margin-bottom: 48px; }
.careers-cta__content .body-lg { width: 528px; max-width: 100%; margin-bottom: 32px; }

/* =================================================================
   FOOTER
   ================================================================= */
.footer {
  background: var(--color-black);
  /* Horizontal gutter moved to .footer__frame (a .site-frame) below —
     .footer itself keeps painting full-bleed black. */
  padding: var(--space-m) 0;
}
.footer__frame {
  display: flex;
  flex-direction: column;
  gap: var(--space-xxs);
  padding-inline: var(--page-margin);
}
.footer__top { display: flex; align-items: flex-start; justify-content: space-between; }
.footer__nav { display: flex; gap: 24px; }
/* Shared footer (Home + About): 3 cols (284px each) + gap + businesses
   (593px) are all fixed pixel widths that sum right up against the
   full 1824 content width — min(px, vw) keeps them exact at >=1920
   and shrinks them proportionally below it instead of overflowing
   (same pattern as .section__body-col above). */
.footer__col { display: flex; flex-direction: column; width: min(284px, 14.79vw); }
.footer__col a {
  padding-bottom: 10px;
  margin-bottom: 10px;
  border-top: 1px solid var(--color-dark-blue);
  padding-top: 10px;
  font-size: 16px;
  font-weight: 600;
  line-height: 24.47px;
  letter-spacing: -0.16px;
  color: var(--color-white);
}
.footer__businesses { width: min(593px, 30.89vw); display: flex; flex-direction: column; gap: 24px; }
.footer__businesses-label {
  border-top: 1px solid var(--color-dark-blue);
  padding-top: 12px;
  font-size: 16px;
  font-weight: 600;
  letter-spacing: -0.16px;
}
.footer__business-list { display: flex; gap: 24px; }
.footer__business { display: flex; flex-direction: column; gap: 9px; width: 284px; }
.footer__business-logo { height: 32px; width: auto; object-fit: contain; object-position: left; }
.footer__business-logo-wrap { height: 32px; display: flex; align-items: center; }
.footer__business-logo--kajima { height: 21px; }
.footer__business-link {
  display: flex;
  align-items: center;
  gap: 10px;
  border-top: 1px solid var(--color-dark-blue);
  padding-top: 9px;
  font-size: 16px;
  font-weight: 600;
  letter-spacing: -0.16px;
  color: var(--color-icon-blue);
  width: 100%;
}

.footer__mid { display: flex; align-items: flex-end; justify-content: space-between; }
.footer__brand { display: flex; align-items: flex-end; gap: 24px; }
.footer__logo { width: 284px; height: auto; }
.footer__social { display: flex; gap: 24px; }
.footer__social-link {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 16px;
  font-weight: 600;
  color: var(--color-white);
}
.footer__social-icon { width: 14px; height: 14px; flex: 0 0 14px; }
.footer__social-link .icon-arrow { color: var(--color-icon-blue); margin-left: 2px; }
.footer__legal { width: 593px; display: flex; align-items: flex-end; justify-content: space-between; color: var(--color-white-70); }
.footer__legal-links { display: flex; gap: 34px; font-size: 14px; line-height: 16.82px; letter-spacing: -0.14px; }
.footer__credit { font-size: 14px; }

.footer__rule { height: 1px; background: var(--color-dark-blue); }

.footer__acknowledgement { display: flex; align-items: center; gap: 30px; }
.footer__flags { display: flex; gap: 12px; flex-shrink: 0; }
.footer__flags img { height: 32px; width: auto; }
.footer__acknowledgement p {
  flex: 1;
  font-size: 14px;
  line-height: 21.41px;
  letter-spacing: -0.14px;
  color: var(--color-white);
}

/* =================================================================
   RESPONSIVE
   Base rules above are the approved desktop build and are left as-is.
   Everything below only overrides what needs to change at narrower
   widths — two tiers (tablet, mobile) plus one small-mobile nudge.
   ================================================================= */

/* ---------- TABLET: 768px – 1199px ---------- */
@media (max-width: 1199px) {
  :root { --page-margin: 32px; }

  /* Sticky eyebrows: fine on a tall desktop row, but pinning for an
     extended, awkward stretch on a narrower/shorter viewport. Below
     desktop the eyebrow just scrolls normally with its content.
     (Selectors mirror the base rules' specificity exactly so this
     actually wins instead of losing to the more specific desktop
     margin-top rules.) */
  .section__eyebrow-anchor {
    position: static;
    height: auto;
  }
  .section__eyebrow-anchor .eyebrow,
  .section--split > .section__eyebrow-anchor .eyebrow,
  .section__intro .section__eyebrow-anchor .eyebrow {
    position: static;
    margin-top: 0;
  }
  .eyebrow--sticky {
    position: static;
  }
  .page-intro__crumb .breadcrumb {
    position: static;
  }

  /* Standard section pattern (Who We Are / Featured Sectors / Latest
     News): collapse the eyebrow+content+media row into a single
     stacked column. DOM order already reads eyebrow → content → media,
     so no markup changes are needed here. */
  .section--split { flex-direction: column; gap: 40px; }
  .section__intro { flex-direction: column; width: 100%; gap: 16px; }
  .section__body-col,
  .section__media { width: 100%; max-width: 640px; }
  .section__body-col--offset { margin-left: 0; }
  .section__copy { width: 100%; }
  .media-card { height: auto; aspect-ratio: 4 / 3; }

  .section__intro--three .h2,
  .section__intro--three .body-lg { width: 100%; max-width: 640px; margin-left: 0; }

  .section__intro--news { justify-content: flex-start; }
  .section__intro--news > div:last-child { width: 100%; max-width: 640px; }

  /* Who We Are + Stats */
  .stats { flex-direction: column; gap: 40px; }
  .stats__media { width: 100%; max-width: 640px; }
  .stats__media .media-card { height: auto; aspect-ratio: 16 / 9; }

  /* Featured Sectors: 3 → 2 columns */
  .sector-grid { grid-template-columns: repeat(2, 1fr); }
  .sector-card { height: 560px; }

  /* Latest News: 4 → 2 columns, even image heights read better here */
  .news-grid { grid-template-columns: repeat(2, 1fr); }
  .news-card__image,
  .news-card--tall .news-card__image,
  .news-card--medium .news-card__image { height: 340px; }

  /* Featured Projects: metadata becomes a row instead of a fixed
     284px right rail so it doesn't fight the narrower stage */
  .fp-content__top { flex-direction: column; align-items: flex-start; gap: 24px; }
  .fp-content__meta { flex-direction: row; width: 100%; gap: 40px; }
  .fp-content__bottom { flex-wrap: wrap; gap: 20px; }
  .fp-content__cta { width: auto; }

  /* Careers: fixed 560px panel is too short once text wraps at this
     width, so the panel grows to fit its content instead */
  .careers-cta { height: auto; min-height: 480px; }
  .careers-cta__content { padding: 64px 40px; width: 100%; }
  .careers-cta__content .h2,
  .careers-cta__content .body-lg { width: 100%; max-width: 480px; }

  /* Footer */
  .footer__top { flex-direction: column; gap: 40px; }
  .footer__nav { flex-wrap: wrap; gap: 32px; }
  .footer__businesses { width: 100%; }
  .footer__mid { flex-direction: column; align-items: flex-start; gap: 24px; }
  .footer__legal { width: 100%; flex-direction: column; align-items: flex-start; gap: 12px; }
}

/* ---------- NAV: hamburger menu below desktop ---------- */
@media (max-width: 1199px) {
  /* .nav__links is hidden here, so there's nothing left to grid-align
     to column 3 — revert to the original plain flex row (logo, then
     whatever's left, pinned apart by space-between) rather than risk
     .nav__toggle auto-placing into the same grid cell as .nav__utility. */
  .nav__bar-inner { display: flex; justify-content: space-between; }
  .nav__links,
  .nav__region,
  .nav__divider,
  .nav__utility .nav__contact { display: none; }
  .nav__bar { gap: 16px; }
  .nav__toggle { display: flex; }
}

/* ---------- MOBILE: below 768px ---------- */
@media (max-width: 767px) {
  :root { --page-margin: 20px; }

  .nav__bar { padding-top: 14px; padding-bottom: 14px; }
  .nav__logo img { height: 36px; width: 131px; }

  /* Hero: dominant but comfortable headline, stacked description/CTAs,
     safe bottom breathing room above mobile browser chrome */
  .hero__heading {
    font-size: clamp(36px, 11vw, 56px);
    line-height: 1.05;
    letter-spacing: -1px;
    max-width: 100%;
  }
  .hero__copy {
    gap: 24px;
    padding-bottom: calc(32px + env(safe-area-inset-bottom, 0px));
  }
  .hero__foot { flex-direction: column; align-items: flex-start; gap: 24px; }
  .hero__actions { flex-wrap: wrap; gap: 24px; }
  .body-xl { font-size: 18px; line-height: 1.5; max-width: 100%; }

  /* Section headings scale down; body copy stays comfortably readable */
  .h2 { font-size: clamp(28px, 7vw, 36px); line-height: 1.2; letter-spacing: -0.5px; }
  .section { padding: var(--space-xxs) var(--page-margin); }
  .section--light,
  .section--white { padding: var(--space-xxs) var(--page-margin); }
  .section__body-col { gap: 24px; }
  .media-card { aspect-ratio: 4 / 3; }

  /* Stats: one column, numbers stay large and impactful */
  .stats { padding: 0 var(--page-margin) var(--space-xxs); gap: 40px; }
  .stats__list { gap: 40px; }
  .stats__item { flex-direction: column; align-items: flex-start; gap: 8px; }
  .stats__number { width: auto; font-size: clamp(56px, 18vw, 80px); }
  .stats__rule { margin-bottom: 40px; }

  /* Featured Sectors: one full-width card per row */
  .sector-grid { grid-template-columns: 1fr; gap: 16px; }
  .sector-card { height: 420px; padding: 32px; }
  .sector-card .h2 { font-size: 32px; }

  /* Featured Projects: contained/full-bleed expansion is a desktop-
     scale interaction (see js/main.js, gated by the same breakpoint);
     here the stage is simply a strong, static full-width project
     card. Order is switched (via flex `order`) to match the requested
     mobile hierarchy — image, project info, tags/CTA, progress,
     controls — without touching the shared DOM/JS. */
  .featured-projects__track { height: auto; }
  .featured-projects__stage {
    position: static; /* the contained -> full-screen expansion is desktop/tablet only */
    margin: 0 var(--page-margin);
    height: auto;
    min-height: 640px;
    padding: 32px 24px;
    gap: 32px;
    justify-content: flex-end; /* content+controls stack together at the bottom of the card */
  }
  /* Removed from the box tree so fp-controls/fp-content become direct
     flex children of .featured-projects__stage again — preserves the
     existing order-based mobile stacking untouched. */
  .fp-inner { display: contents; }
  .fp-title { font-size: clamp(32px, 9vw, 44px); line-height: 1.1; }
  .fp-content { gap: 24px; }
  .fp-content__top { order: 1; flex-direction: column; gap: 20px; }
  .fp-content__meta { flex-direction: row; flex-wrap: wrap; width: 100%; gap: 24px; }
  .fp-content__bottom { order: 2; flex-direction: column; align-items: flex-start; gap: 20px; }
  .fp-content__cta { width: 100%; }
  .fp-timer { order: 3; }
  .fp-controls { order: 4; justify-content: flex-start; }
  .fp-arrow { width: 44px; height: 44px; }

  /* Latest News: one card per row, natural aspect ratio */
  .news-grid { grid-template-columns: 1fr; gap: 32px; }
  .news-card__image,
  .news-card--tall .news-card__image,
  .news-card--medium .news-card__image { height: auto; aspect-ratio: 4 / 3; }

  /* Careers: same background image, just repositioned/taller so the
     text and the people both stay legible at this width */
  .careers-cta { min-height: 620px; background-position: 68% center; }
  .careers-cta__content { padding: 40px 20px; }
  .careers-cta__content .h2 {
    font-size: clamp(28px, 8vw, 40px);
    line-height: 1.2;
    margin-bottom: 32px;
  }
  .careers-cta__content .body-lg { margin-bottom: 24px; }

  /* Footer: single stacked column, comfortable tap spacing */
  .footer { padding: var(--space-xxs) var(--page-margin); gap: 32px; }
  .footer__nav { flex-direction: column; gap: 8px; }
  .footer__col { width: 100%; }
  .footer__business-list { flex-direction: column; gap: 24px; }
  .footer__business { width: 100%; }
  .footer__brand { flex-direction: column; align-items: flex-start; gap: 16px; }
  .footer__logo { width: 160px; }
  .footer__social { flex-direction: column; gap: 12px; }
  .footer__legal-links { flex-wrap: wrap; gap: 16px 24px; }
  .footer__acknowledgement { flex-direction: column; align-items: flex-start; gap: 16px; }
}

/* ---------- SMALL MOBILE: below 480px ---------- */
@media (max-width: 479px) {
  .stats__number { font-size: clamp(48px, 20vw, 64px); }
  .sector-card { height: 380px; }
  .footer__logo { width: 140px; }
}

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

/* =================================================================
   ABOUT — SHARED SECTION FAMILIES
   Reusable across every About-family page (About Icon, Our Story,
   How We Build, Sustainability, Reconciliation, ...). Page copy is
   supplied as content; the classes below only describe layout.
   Reuses the same tokens/atoms as the Home build (colours, spacing
   scale, radius, .eyebrow, .tag, .btn-link, .icon-arrow, .h2) rather
   than redeclaring them.
   ================================================================= */

/* ---------- Extra type scale (sizes Home didn't need) ---------- */
.h1 {
  font-size: 64px;
  font-weight: 600;
  line-height: 67.25px;
  letter-spacing: -1px;
  color: var(--color-white);
}
.h3 {
  font-size: 42px;
  font-weight: 600;
  line-height: 56px;
  letter-spacing: -0.42px;
  color: var(--color-dark-blue);
}
.h4 {
  font-size: 32px;
  font-weight: 600;
  line-height: 48px;
  letter-spacing: -0.5px;
  color: var(--color-white);
}
.h4--dark { color: var(--color-dark-blue); }
/* .h1/.h3/.h4 are declared here, after the plain colour utilities
   (.text-blue etc., declared much earlier for Home) — so on their own,
   a combo like `.h4 text-blue` (Job Detail's pull-quotes, Insights'
   article "Thank you to project partners" heading) would lose the
   colour to these heading rules' own default at equal specificity.
   Explicit override, scoped to just the colour, so every future
   `.h1/.h3/.h4 text-blue` instance keeps working without needing this
   worked out again per page. */
.h1.text-blue, .h3.text-blue, .h4.text-blue { color: var(--color-icon-blue); }
.body-xxl {
  font-size: 22px;
  font-weight: 400;
  line-height: 33.65px;
  letter-spacing: -0.22px;
  color: var(--color-white-70);
}
.body-sm {
  font-size: 14px;
  font-weight: 400;
  line-height: 21.41px;
  letter-spacing: -0.14px;
  color: var(--color-white-70);
}

/* ---------- Breadcrumb (PageIntro) ---------- */
.breadcrumb {
  display: flex;
  align-items: center;
  gap: 10px;
  font-size: 18px;
  font-weight: 600;
  line-height: 27.2px;
  letter-spacing: -0.18px;
  color: var(--color-icon-blue);
  white-space: nowrap;
}
.breadcrumb__dim { opacity: 0.6; }

/* =================================================================
   PAGE INTRO — reusable page-header family
   ================================================================= */
.page-intro {
  background: var(--color-dark-blue);
  /* Figma: pt-80 / pb-96 — the bottom gap is intentionally larger than
     the top (breathing room before HeroMedia straddles the boundary).
     Horizontal gutter now comes from .site-grid's padding-inline, not
     from padding here, so the frame can centre on ultrawide screens.
     The nav is a fixed overlay (see .nav), not a document-flow block,
     so this section's own background still starts at y=0 behind it —
     but its CONTENT needs to clear the nav's real height before the
     designed pt-80 breathing room begins, hence the added
     var(--nav-height) here rather than a second margin-top hack. */
  padding: calc(var(--nav-height) + var(--space-xs)) 0 96px;
}
/* .page-intro__row carries .site-frame + .site-grid in markup — real
   column placement (breadcrumb cols 1–2, content cols 3–12) instead
   of a flex row sized by fixed widths relative to the viewport. */
.page-intro__row { align-items: flex-start; }
.page-intro__crumb { grid-column: 1 / span 2; }
/* min-width: 0 overrides the flex/grid-item default of min-width: auto
   (== the content's own min-content size), which otherwise lets a long
   unbroken word in a 64px heading (e.g. Projects' "Construction") force
   this column wider than its own flex/grid track instead of wrapping —
   a latent bug in the shared component, not something About's shorter
   heading happened to trigger. Harmless at desktop (the 1516px column
   is never that tight) and doesn't change any already-shipped page. */
.page-intro__content { grid-column: 3 / span 10; display: flex; flex-direction: column; gap: 48px; max-width: 1516px; min-width: 0; }
.page-intro__heading { max-width: 90%; min-width: 0; }
.page-intro__rule { height: 1px; background: var(--color-white-20); width: 100%; }
.page-intro__copy { max-width: 90%; }
.page-intro__copy p { margin: 0; }

/* =================================================================
   HERO MEDIA — reusable image band that straddles the PageIntro/page
   background transition. The section paints its own top/bottom split
   (same technique as Home's Careers CTA) so the crossover never
   depends on brittle absolute offsets — whatever height the media
   frame is given, half sits on dark blue and half on the page colour
   beneath, because the split is fixed at the section's own midpoint.
   ================================================================= */
.hero-media {
  background: linear-gradient(to bottom, var(--color-dark-blue) 50%, var(--color-white) 50%);
}
.hero-media--onto-black { background: linear-gradient(to bottom, var(--color-dark-blue) 50%, var(--color-black) 50%); }
/* --reverse — white top-half instead of dark-blue (Reconciliation's Full
   Width Media instance sits after a white section and leads into a
   dark-blue one, the opposite order of the component's default). Same
   split mechanic, just the two color-stops swapped. */
.hero-media--reverse { background: linear-gradient(to bottom, var(--color-white) 50%, var(--color-dark-blue) 50%); }
/* .hero-media__inner is .site-frame in markup — the gutter/media
   inset is relative to the centred 1920 frame, not the raw viewport. */
.hero-media__inner { padding-inline: var(--page-margin); }
.hero-media__frame {
  position: relative;
  height: var(--hero-media-height, 900px);
  border-radius: var(--radius-s);
  overflow: hidden;
}
.hero-media__frame img { width: 100%; height: 100%; object-fit: cover; display: block; }
/* Video keeps its native aspect ratio — scaled to the frame's width,
   centred both ways, with the fixed-height frame's overflow:hidden
   above cropping any excess. Never stretched to fill the height. */
.hero-media__frame video {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 100%;
  height: auto;
  transform: translate(-50%, -50%);
  display: block;
}

/* =================================================================
   IMAGE MONTAGE — Life at Icon's four-image band, the ONE new layout
   this page introduces (see CLAUDE.md's Life at Icon build notes;
   every other section on that page reuses an existing component
   verbatim). Full-bleed dual-tone backdrop reuses .hero-media's own
   two-stop-gradient technique (dark-blue over black is a literal Figma
   colour pairing, same mechanic as .hero-media--onto-black), and the
   row composes .site-frame directly (own padding-inline, not
   .site-grid — four images at explicit widths, not a 12-column
   layout). Each item's width/height are per-instance custom properties
   (--montage-w/--montage-h, set inline) — same convention as
   .horizontal-story__panel--media's --panel-w/--panel-h and
   .split-narrative-item's --media-w/--media-h. */
.image-montage { background: linear-gradient(to bottom, var(--color-dark-blue) 50%, var(--color-black) 50%); }
.image-montage__frame { padding-inline: var(--page-margin); }
.image-montage__row { display: flex; align-items: flex-start; gap: 24px; }
/* flex-grow set to the item's own literal Figma width (not a fixed
   width) so the row's fixed 24px gaps are subtracted first and the
   remaining space is then split in the exact same proportions the
   design specifies — at the 1920px frame this resolves to Figma's
   literal 438/282/594/438px, and it scales down proportionally between
   that and the tablet breakpoint below instead of overflowing (same
   "scale via vw/flex, don't hardcode a mid-range breakpoint" approach
   .split-narrative-item's --media-vw already uses). aspect-ratio keeps
   each crop's proportions correct as its flexed width changes.
   --montage-w/--montage-h are set inline as bare UNITLESS numbers
   (e.g. --montage-w:438, not 438px) — both flex-grow and aspect-ratio
   require unitless numbers, and a "438px" value silently makes the
   whole aspect-ratio declaration invalid (the browser drops it, and
   height quietly falls back to the image's own natural ratio instead
   of the crop this component specifies). */
.image-montage__item {
  flex: var(--montage-w) 1 0;
  aspect-ratio: var(--montage-w) / var(--montage-h);
  min-width: 0;
  border-radius: var(--radius-xs);
  overflow: hidden;
}
.image-montage__item img { width: 100%; height: 100%; object-fit: cover; display: block; }

/* =================================================================
   HORIZONTAL STORY — reusable pinned horizontal-scroll section
   (How We Build's Safety + Quality "chapters"). Two independent
   layers: .horizontal-story__eyebrow is a plain sibling of the
   viewport, never touched by the track's transform, so it's a
   structural guarantee (not a timing coincidence) that it stays put
   top-left for the whole pinned sequence — see initHorizontalStory in
   js/motion.js for the scroll-jack itself. Section height is content-
   agnostic (a --horizontal-story-height custom property, same
   convention as .hero-media's own height var) — the ENGINE that
   computes how far the track travels reads the track's real rendered
   width at runtime, never a number tied to Safety's specific content.
   ================================================================= */
.horizontal-story {
  position: relative;
  height: var(--horizontal-story-height, 960px);
  background: var(--color-white);
  overflow: hidden; /* belt-and-braces: nothing in here can ever widen the document */
}
/* Dark theme variant — Quality's own chapter. Same engine, only the
   backdrop and the copy panels' body-copy colour change; the eyebrow
   already reads var(--color-icon-blue) by default (no --dark modifier
   applies to it), and every heading/checklist item is themed per-
   instance in markup (.h3/.h2 default to white already; add .text-blue
   to a span for a highlight, same convention as every other section). */
.horizontal-story--dark { background: var(--color-dark-blue); }
.horizontal-story--dark .horizontal-story__copy p { color: var(--color-white-70); }
/* .h3's own default is dark-blue (it was only ever used on light
   panels before Quality) — scoped override, not a sitewide .h3
   change, so Safety's existing copy panels are untouched. */
.horizontal-story--dark .h3 { color: var(--color-white); }
/* Eyebrow anchor: absolutely-positioned overlay composing the real
   .site-frame + .site-grid classes (markup), exactly the technique
   .scale-group__eyebrow-frame uses (see its long comment for the full
   derivation) — inset: 0 gives left/right both a definite value, which
   is what lets .site-frame's inherited width: 100%/max-width: 1920px/
   margin-inline: auto centering actually engage on an absolutely-
   positioned box instead of hugging the raw viewport edge past 1920px. */
.horizontal-story__frame {
  position: absolute;
  inset: 0;
  z-index: 2;
  pointer-events: none;
}
.horizontal-story__eyebrow {
  grid-column: 1 / span 2;
  align-self: start;
  margin-top: 90px;
  pointer-events: auto;
}
.horizontal-story__viewport {
  position: absolute;
  inset: 0;
  overflow: hidden;
}
.horizontal-story__track {
  display: flex;
  align-items: center;
  height: 100%;
  gap: 176px; /* shared "beat" spacing between story moments — same value Quality's track will read */
  /* Read below for the START inset only (the end-of-sequence resting
     position is resolved in JS instead — see the padding-right comment
     further down). Same centred-container math .site-frame itself uses
     (width: 100%/max-width: var(--content-max)/margin-inline: auto)
     even though this track is deliberately full-bleed — max(0px,
     (100% - var(--content-max)) / 2) is exactly the gap .site-frame
     leaves on either side once the viewport is wider than
     --content-max, and 0 below it (the container fills the viewport,
     same as every other section). Container/grid controls the START
     resting alignment; the full, unconstrained viewport is still what
     the track is free to travel through beyond it. */
  --story-container-inset: max(0px, (100% - var(--content-max)) / 2);
  /* 1 grid column (130px) + column-gap (24px): leaves exactly enough
     room for the eyebrow column above to sit over the track's own
     empty top-left, same 12-column math .site-grid uses everywhere
     else (see the column-width table at .site-frame) — now measured
     from the real centred container edge instead of the raw viewport
     edge, so it still lines up with the eyebrow past 1920px wide. */
  padding-left: calc(var(--story-container-inset) + var(--page-margin) + 130px + 24px);
  /* NOT where the end-of-sequence resting gap is enforced — a flex
     container's trailing (end-side) padding isn't reliably included in
     its own scrollWidth once un-shrinkable flex items overflow it, so
     padding here can't be trusted to stop the scroll short by a fixed
     amount. The real 306px container-relative gap is measured and
     applied in JS instead (see the --horizontal-story-end-inset read
     in initHorizontalStory, js/motion.js, against [data-horizontal-
     frame]'s real rendered right edge). This is just a minimal visual
     buffer so the track's raw content never touches the viewport edge
     even before that JS-computed stop is reached. */
  padding-right: var(--page-margin);
  will-change: transform;
}
/* Grab-to-drag affordance (see initHorizontalStory, js/motion.js) — a
   fine-pointer/hover-capable cursor hint; on touch there's no cursor to
   show anyway. touch-action: none stops a touch drag from also trying
   to natively scroll the page while the section is pinned (only
   relevant here, in the pinned/non-static state — .horizontal-story
   --static below restores default touch scrolling for its own native
   overflow-x strip). --dragging is toggled purely in JS for the actual
   gesture (also disables text selection, which click-dragging across
   panel copy would otherwise trigger). Independent of, and not to be
   confused with, .group-carousel__track's own drag state on the About
   page — a different component with its own separate implementation. */
@media (hover: hover) and (pointer: fine) {
  .horizontal-story__viewport { cursor: grab; }
}
.horizontal-story__viewport { touch-action: none; }
.horizontal-story--dragging .horizontal-story__viewport {
  cursor: grabbing;
  user-select: none;
}
.horizontal-story__panel { flex-shrink: 0; }
/* Text panels: the opening statement (intro) and each chapter's
   heading/body (copy) are the same 592px (4-column) measure. */
.horizontal-story__panel--intro,
.horizontal-story__panel--copy {
  width: 592px;
  display: flex;
  flex-direction: column;
  gap: 20px;
}
.horizontal-story__panel--intro { gap: 48px; }
.horizontal-story__statement { margin: 0; }
.horizontal-story__copy { display: flex; flex-direction: column; gap: 32px; }
.horizontal-story__copy p {
  margin: 0;
  font-size: 16px;
  font-weight: 400;
  line-height: 24.47px;
  letter-spacing: -0.16px;
  color: var(--color-body-grey);
}
.horizontal-story__checklist { display: flex; flex-direction: column; gap: 12px; }
.horizontal-story__check-row { display: flex; align-items: flex-start; gap: 16px; }
.horizontal-story__check-icon { width: 16px; height: 16px; flex-shrink: 0; margin-top: 4px; }
.horizontal-story__check-row p { margin: 0; flex: 1 1 auto; }
/* Media panels: width/height are per-instance (--panel-w/--panel-h,
   set inline — same convention as .split-narrative-item's --media-w/
   --media-h) so one class covers every crop this sequence needs. */
.horizontal-story__panel--media {
  width: var(--panel-w);
  height: var(--panel-h);
  border-radius: var(--radius-s);
  overflow: hidden;
}
.horizontal-story__panel--media img { width: 100%; height: 100%; object-fit: cover; display: block; }
/* Certification-badge treatment (Safety's ISO 45001 mark): a flat
   brand-blue card with the badge artwork inset and letterboxed rather
   than cropped. #165cad is sampled straight from the Figma badge card,
   a one-off for this specific certification graphic, not a site token. */
.horizontal-story__panel--badge {
  width: var(--panel-w);
  height: var(--panel-h);
  border-radius: var(--radius-s);
  /* --panel-bg: per-instance card colour (same custom-property
     convention as --panel-w/--panel-h) — Safety's ISO 45001 mark sits
     on a flat brand-blue card (#165cad, sampled from Figma, a one-off
     for that specific graphic); Quality's ISO 9001 mark sits on a
     plain white card in Figma, set inline via style="--panel-bg: ...". */
  background: var(--panel-bg, #165cad);
  display: flex;
  align-items: center;
  justify-content: center;
}
.horizontal-story__panel--badge img { width: 78%; height: 78%; object-fit: contain; }
/* Two panels that belong together as one visual beat (e.g. a pair of
   photos, or a badge beside its supporting photo) — nested so the
   track's own shared "beat" gap still applies on either side of the
   pair, while the two items inside sit close together. */
.horizontal-story__cluster { display: flex; align-items: center; gap: 24px; flex-shrink: 0; }
/* The sequence's two large "breather" images sit further from their
   neighbours than the standard beat gap — extra margin on top of the
   track's own gap, rather than a second gap value the track can't
   express (CSS `gap` is one uniform value per container). */
.horizontal-story__panel--breather { margin-inline: 90px; }

/* Below the tablet breakpoint (.site-grid's own collapse point) and
   under prefers-reduced-motion, initHorizontalStory() skips the pin/
   scroll-jack and adds this class instead: the section reverts to
   normal document flow and the viewport becomes a plain native
   horizontally-scrollable strip, so every panel stays reachable by a
   direct swipe/drag rather than fighting a pinned vertical scroll. */
.horizontal-story--static { height: auto; }
.horizontal-story--static .horizontal-story__eyebrow { position: static; padding: 48px var(--page-margin) 0; }
.horizontal-story--static .horizontal-story__viewport {
  position: static;
  overflow-x: auto;
  overflow-y: hidden;
  -webkit-overflow-scrolling: touch;
  touch-action: auto; /* native swipe takes over here — the pinned state's touch-action: none doesn't apply */
  cursor: auto;
}
.horizontal-story--static .horizontal-story__track {
  padding-left: var(--page-margin);
  padding-block: 48px;
}

/* =================================================================
   LEADERSHIP QUOTE — reusable eyebrow + portrait beside a pull-quote.
   How We Build's Safety chapter builds the first instance; Quality
   reuses this exact markup/CSS/motion with only its own content
   (eyebrow copy, portrait, quote, attribution) swapped in — nothing
   about the structure is Safety-specific.

   .leadership-quote__frame is a plain flex row (not .site-grid): the
   102px gap between the profile column and the quote doesn't land on
   a clean grid-column boundary at this canvas width, so a fixed
   column width + flex:1 remainder (same technique Split Narrative
   already uses for its own non-grid row) matches Figma exactly
   instead of forcing an approximate grid span. align-items: flex-start
   is what puts the quote's own top flush with the eyebrow's top at
   rest — CSS layout, not a hand-tuned transform offset — and that
   same alignment holds at every point through the scroll-reveal
   below, since the reveal only ever translates both columns by the
   same y, never anything that could pull them out of sync.

   Motion: data-scroll-section/data-scroll-order (js/motion.js) — the
   profile column (order 1: eyebrow + portrait, moving as one block)
   and the quote column (order 2) share a single cascade, same
   sync convention as Split Narrative's media/content pairing. The
   eyebrow here is a plain flow element, never .section__eyebrow-anchor
   / .eyebrow--sticky — it isn't sticky or pinned. */
.leadership-quote { background: var(--color-grey-101); padding: var(--space-m) 0; }
.leadership-quote__frame { display: flex; align-items: flex-start; gap: 102px; padding-inline: var(--page-margin); }
.leadership-quote__profile { width: 284px; flex-shrink: 0; display: flex; flex-direction: column; gap: var(--space-xxs); }
.leadership-quote__portrait { width: 219px; height: 291px; overflow: hidden; }
.leadership-quote__portrait img { width: 100%; height: 100%; object-fit: cover; display: block; }
.leadership-quote__quote { display: flex; align-items: flex-start; gap: 24px; flex: 1 1 auto; min-width: 0; }
.leadership-quote__mark { flex-shrink: 0; display: block; font-size: 128px; font-weight: 400; line-height: 84px; letter-spacing: -2px; color: var(--color-dark-blue); }
.leadership-quote__body { display: flex; flex-direction: column; gap: 48px; max-width: 1306px; }
.leadership-quote__attribution { display: flex; flex-direction: column; gap: 8px; }
.leadership-quote__name { margin: 0; font-size: 18px; font-weight: 600; line-height: 27.2px; letter-spacing: -0.18px; color: var(--color-dark-blue); }
.leadership-quote__role { margin: 0; font-size: 14px; font-weight: 600; line-height: 16.82px; letter-spacing: -0.14px; color: var(--color-body-grey); }

/* Dark theme variant — Quality's own instance. Same structure, only
   the backdrop and the elements that hardcode a dark-blue-on-light
   colour flip to light-on-dark; the quote text itself just drops the
   .h2--dark modifier in markup (bare .h2 already defaults to white)
   and the eyebrow already reads var(--color-icon-blue) via
   .eyebrow--blue, same as Safety's .eyebrow--dark needs no override
   here either. */
.leadership-quote--dark { background: var(--color-icon-deep-blue); }
.leadership-quote--dark .leadership-quote__mark { color: var(--color-white); }
.leadership-quote--dark .leadership-quote__name { color: var(--color-white); }
.leadership-quote--dark .leadership-quote__role { color: var(--color-white-70); }

@media (max-width: 1199px) {
  .leadership-quote { padding: var(--space-xxs) 0; }
  .leadership-quote__frame { flex-direction: column; gap: var(--space-xxs); }
  .leadership-quote__profile { width: 100%; flex-direction: row; align-items: center; gap: 24px; }
  .leadership-quote__quote { gap: 24px; }
  .leadership-quote__mark { font-size: 72px; line-height: 48px; }
  .leadership-quote__text.h2 { font-size: 32px; line-height: 40px; }
}
@media (max-width: 767px) {
  .leadership-quote__quote { flex-direction: column; gap: 16px; }
  .leadership-quote__profile { flex-direction: column; align-items: flex-start; gap: 24px; }
  .leadership-quote__portrait { width: 160px; height: 213px; }
}

/* =================================================================
   SPLIT FEATURE — eyebrow + heading/body beside a media panel
   (photo or illustration). Reused for Our Scale here, and again on
   Sustainability / Reconciliation.
   ================================================================= */
.split-feature { padding: var(--space-m) 0; background: var(--color-white); }
.split-feature--dark { background: var(--color-dark-blue); }
/* --black: Life at Icon's "Building with community" instance sits on
   flat black rather than the --dark modifier's dark-blue — Figma's own
   literal colour there. Applied alongside --dark (not instead of it)
   so the white heading/copy/stats colour handling --dark already
   supplies is reused as-is; this only overrides the background token,
   same minimal convention as --icon-dark-blue on .image-list/
   .split-narrative. */
.split-feature--black { background: var(--color-black); }
/* .split-feature__inner carries .site-frame + .site-grid in markup.
   Eyebrow/content/media are placed directly on the 12-column grid —
   eyebrow col 1, content cols 3–6, media cols 8–12 (mirrored for the
   --media-left variant) — instead of a flex row sized by fixed
   pixel widths. align-items: flex-start is the fix for the eyebrow
   previously being vertically centred against the taller content. */
.split-feature__inner { align-items: flex-start; }
/* align-self: stretch gives this column the row's real (full grid-row)
   height — same fix .page-intro__crumb/.people-section__label already
   needed — so .eyebrow--sticky (used when this section stands alone,
   e.g. Sustainability's Overview) has room to actually stick and
   release instead of sitting static at its own content-only height. */
.split-feature__eyebrow { grid-column: 1 / span 1; align-self: stretch; }
/* Figma's eyebrow label is wider than its nominal 1-column cell — it
   must stay on one line and is allowed to overflow into the (empty)
   gutter/column 2 rather than wrap, same overflow-without-wrapping fix
   already used for .group-carousel__eyebrow/.heritage__eyebrow-col. */
.split-feature__eyebrow .eyebrow__track { white-space: nowrap; }
.split-feature__content { grid-column: 3 / span 4; display: flex; flex-direction: column; gap: 48px; }
.split-feature__media { grid-column: 8 / span 5; }
.split-feature--media-left .split-feature__media { grid-column: 3 / span 5; }
.split-feature--media-left .split-feature__content { grid-column: 9 / span 4; }
/* --media-portrait — a legitimate per-instance layout variant (not a
   refactor of the shared default): Sustainability's Overview uses a
   narrower, taller PORTRAIT media panel flush with the grid's right
   edge (cols 9–12, 592px, matching Figma node 20999:7483 exactly)
   instead of the default wide landscape panel sized for About's Our
   Scale map (cols 8–12, 746px). Column span is still derived from the
   real 12-column grid, not a magic pixel width. */
.split-feature--media-portrait .split-feature__media { grid-column: 9 / span 4; border-radius: var(--radius-s); overflow: hidden; }
.split-feature--media-portrait .split-feature__media img { aspect-ratio: 592 / 750; height: auto; object-fit: cover; }
.split-feature__copy { display: flex; flex-direction: column; gap: 18px; }
/* Build Your Career's "Choose your pathway" instance repeats a
   heading + body + link block per pathway instead of the shared
   checklist-style .split-feature__copy — a genuinely different content
   shape, so it gets its own two small classes rather than overloading
   .split-feature__copy's gap. Pathway blocks are direct children of
   .split-feature__content, so its existing 48px gap already spaces
   "heading to first pathway" and "pathway to pathway" for free. */
.split-feature__pathway { display: flex; flex-direction: column; gap: 20px; }
.split-feature__pathway-cta { display: flex; flex-direction: column; gap: 12px; }
/* :not(.h6) — a lead-in line like "Our approach is focused on three
   outcomes:" uses the real .h6/.h6--dark heading classes (see below),
   not this body-copy default; excluding it here is what lets that
   heading style actually win instead of being overridden by this more
   specific `.split-feature__copy p` selector. */
.split-feature__copy p:not(.h6) { margin: 0; font-size: 18px; font-weight: 400; line-height: 27.53px; letter-spacing: -0.18px; color: var(--color-body-grey); }
.split-feature--dark .split-feature__copy p:not(.h6) { color: var(--color-white-70); }
.split-feature__media img { width: 100%; height: auto; display: block; }
/* --sticky — Careers Pathway's stats column reuses the exact same
   stretch-the-grid-cell technique .split-feature__eyebrow already uses
   (align-self: stretch gives the column the row's full height so a
   sticky child inside it has room to stick and release), just applied
   to the media slot instead of the eyebrow. The homepage's own default
   .split-feature__media (a plain image) is untouched — this only
   applies where the modifier class is present. */
.split-feature__media--sticky { align-self: stretch; }
/* Sticky stats: reuses .stats__list (see WHO WE ARE + STATS) as-is for
   every row/rule/number/text style — only position/top are added here,
   scoped to this instance so the homepage's own Stats section (a plain
   flex:1 column, never sticky) is untouched. Same --eyebrow-top offset
   the sticky eyebrow already sticks at, so both columns release in
   sync relative to the nav. */
.split-feature__media--sticky .stats__list { position: sticky; top: var(--eyebrow-top, 128px); }
/* Suffix following a counted number (Careers Pathway's stats only —
   homepage's own instance has no differently-sized suffix, so
   .stats__number's own font-size still governs unmodified there).
   Two sizes match Figma's own "+" vs "sectors" tokens. */
.stats__suffix { font-weight: 600; margin-left: 8px; }
.stats__suffix--lg { font-size: 64px; line-height: 67.25px; letter-spacing: -1px; }
.stats__suffix--sm { font-size: 48px; line-height: 58.67px; letter-spacing: -0.72px; }

/* =================================================================
   CAREERS PATHWAY — Split Feature's content column, extended with a
   "what you'll experience" checklist + CTA. Three nested gaps here
   mirror Figma's own nesting exactly (48/32/20 instead of one flat
   value) — .split-feature__content's shared 48px gap still governs
   the heading-to-body spacing untouched; these only group what comes
   after it. Checklist layout/icon reuse .horizontal-story__checklist/
   __check-row/__check-icon verbatim (see HORIZONTAL STORY) — only the
   row text's size/colour is added here, since this instance nests the
   list outside that component's own .horizontal-story__copy, which is
   what supplies that styling there.
   ================================================================= */
.pathway__body { display: flex; flex-direction: column; gap: 32px; width: 100%; }
.pathway__cluster { display: flex; flex-direction: column; gap: 48px; width: 100%; }
.pathway__experience { display: flex; flex-direction: column; gap: 20px; width: 100%; }
.pathway__checklist .horizontal-story__check-row p { margin: 0; font-size: 16px; font-weight: 400; line-height: 24.47px; letter-spacing: -0.16px; color: var(--color-white-70); }

/* =================================================================
   CENTERED FEATURE — centred heading/copy with a media/content slot
   beneath. Reused for the Kajima backing statement here; generic
   enough for any centred statement + supporting visual.
   ================================================================= */
.centered-feature { padding: 0 0 var(--space-m); background: var(--color-white); }
/* .centered-feature__frame is .site-frame in markup (max-width:1920,
   centred) — the flex+justify-center that used to live directly on
   .centered-feature now lives here, INSIDE the 1920 cap, so the
   1515-wide statement centres within the frame rather than the raw
   (potentially wider) physical viewport. */
.centered-feature__frame { display: flex; justify-content: center; padding-inline: var(--page-margin); }
.centered-feature__inner { display: flex; flex-direction: column; align-items: center; gap: var(--space-xs); max-width: 1515px; width: 100%; }
/* Heading + copy are their own tighter-spaced group (gap 48); the
   looser gap (--space-xs, 80) only separates that group from the map
   beneath it — the two gaps are NOT the same value in Figma. */
.centered-feature__statement { display: flex; flex-direction: column; align-items: center; gap: 48px; width: 100%; }
.centered-feature__heading { text-align: center; max-width: 1305px; }
.centered-feature__copy { display: flex; flex-direction: column; gap: 18px; text-align: center; max-width: 1231px; }
.centered-feature__copy p { margin: 0; font-size: 18px; font-weight: 400; line-height: 27.53px; letter-spacing: -0.18px; color: var(--color-body-grey); }
/* Figma fixes the map at 1207×580, centred within the 1515 column —
   it does not stretch edge-to-edge like a full-width media panel. */
.centered-feature__media { width: 1920px; max-width: 100%; }

/* =================================================================
   KAJIMA GLOBAL MAP — dedicated child of CenteredFeature so the map
   itself never becomes a one-off "KajimaBackingSection" layout.
   Geographic presence only — no flight paths/routes (see js/about.js,
   KAJIMA_LOCATIONS / initKajimaMap). The base landmass is a static
   dotted-world SVG (assets/images/about/kajima-world-dots.svg,
   plotted at a fixed lng -180..180 / lat 78..-56 equirectangular
   crop); markers/labels are positioned as a % of this same box using
   that identical lat/lng -> %x/%y formula, so they stay geographically
   attached at any rendered size — see initKajimaMap for the maths.
   No overflow:hidden here: a couple of edge labels (e.g. New Zealand)
   are allowed to sit fractionally outside the image's own box while
   staying inside .centered-feature-media/the site frame around it. */
.kajima-map { position: relative; width: 100%; margin-top: 8px; }
.kajima-map__base { display: block; width: 100%; height: auto; border-radius: var(--radius-s);}
.kajima-map__pins { position: absolute; inset: 0; }
.kajima-map__pin {
  position: absolute;
  width: 0;
  height: 0;
  opacity: 0;
  transform: translateY(6px);
}
.kajima-map__pin.is-visible { opacity: 1; transform: translateY(0); transition: opacity 0.5s ease, transform 0.5s ease; }
.kajima-map__dot {
  position: absolute;
  left: 0;
  top: 0;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--color-icon-blue);
  box-shadow: 0 0 0 4px rgba(0, 184, 241, 0.16);
  transform: translate(-50%, -50%);
}
.kajima-map__label {
  position: absolute;
  left: 0;
  top: 0;
  padding: 6px 10px;
  background: var(--color-white);
  border: 1px solid var(--color-grey-200, #ecedef);
  border-radius: var(--radius-xs);
  box-shadow: 0 2px 8px rgba(20, 41, 79, 0.08);
  font-size: 13px;
  font-weight: 600;
  line-height: 1.3;
  letter-spacing: -0.13px;
  color: var(--color-dark-blue);
  white-space: nowrap;
}
/* Label anchor directions — the dot itself always stays exactly on
   the geographic point; only the label is nudged around it to dodge
   its neighbours (allowed per design: "small visual offsets to the
   labels", never to the marker). */
/* Chained translate() — the first call anchors the label to a side of
   the (geographically fixed) dot; the second applies an optional extra
   per-item nudge (--dx/--dy, set inline by initKajimaMap only for the
   few labels dense clusters like SE Asia require) purely to dodge a
   neighbour. Both are cosmetic label adjustments; the dot itself never
   moves. */
.kajima-map__label--top { transform: translate(-50%, calc(-100% - 10px)) translate(var(--dx, 0px), var(--dy, 0px)); }
.kajima-map__label--bottom { transform: translate(-50%, 10px) translate(var(--dx, 0px), var(--dy, 0px)); }
.kajima-map__label--left { transform: translate(calc(-100% - 10px), -50%) translate(var(--dx, 0px), var(--dy, 0px)); }
.kajima-map__label--right { transform: translate(10px, -50%) translate(var(--dx, 0px), var(--dy, 0px)); }
.kajima-map__label--top-left { transform: translate(calc(-100% + 6px), calc(-100% - 8px)) translate(var(--dx, 0px), var(--dy, 0px)); }
.kajima-map__label--top-right { transform: translate(-6px, calc(-100% - 8px)) translate(var(--dx, 0px), var(--dy, 0px)); }
.kajima-map__label--bottom-left { transform: translate(calc(-100% + 6px), 8px) translate(var(--dx, 0px), var(--dy, 0px)); }
.kajima-map__label--bottom-right { transform: translate(-6px, 8px) translate(var(--dx, 0px), var(--dy, 0px)); }
/* Below 1300px the map's percentage-plotted pins land close enough to
   the container's edge at several widths that a label's fixed offset
   can push it past the page's own right edge, causing real horizontal
   document scroll (QA audit finding C1). Rather than reposition/clamp
   per-width, the labels are hidden outright across this whole range —
   the dots/pins stay visible and geographically correct, only the text
   tags drop. Desktop (>=1300px) keeps the original label positions
   exactly as designed. */
@media (max-width: 1299px) {
  .kajima-map__label { display: none; }
}
/* Mobile-only text fallback for the on-map tags — see the <=767px
   override below where it becomes visible and the tags themselves
   are hidden instead. */
.kajima-map__legend { display: none; }

/* =================================================================
   SCALE GROUP — shared sticky boundary wrapping Split Feature (Our
   Scale) + Centered Feature (Kajima Backing), so the "Our scale"
   eyebrow's sticky range covers the whole content group it describes,
   not just the first row. See the HTML comment at .scale-group for why
   this exists. Deliberately carries no padding/max-width of its own —
   both children keep their existing full-bleed backgrounds and internal
   .site-frame/.site-grid layouts completely unchanged; this wrapper
   only supplies the real, combined height the anchor needs.
   ================================================================= */
.scale-group { position: relative; }
/* The anchor composes the real .site-frame + .site-grid classes (see
   markup) rather than hand-copying their declarations — an earlier
   version redeclared the grid recipe (12 cols/24px gap/page-margin)
   without .site-frame's own width: 100%/max-width: 1920px/margin-
   inline: auto, so it never centred past 1920px width and drifted from
   the real grid's horizontal origin. left: 0 + right: 0 here is what
   lets that inherited centering actually engage on an absolutely-
   positioned box: with left/right both set to a definite value, .site-
   frame's width: 100% resolves against that span, max-width clips it at
   1920px, and — because margin-inline: auto is also inherited from
   .site-frame — the remaining space is distributed evenly to left/right
   margins, exactly like every normal-flow .site-frame on the page.
   .scale-group isn't a grid ITEM of a parent .site-grid the way every
   other sticky eyebrow on this page is (.split-feature__eyebrow,
   .group-carousel__eyebrow) — it wraps two independent <section>s, not
   a single grid row — so it has to carry a real .site-grid of its own
   rather than being placed inside one. */
.scale-group__eyebrow-frame {
  left: 0;
  right: 0;
}
.scale-group__eyebrow { grid-column: 1 / span 1; }
/* Section-break gap from Hero Media: pulling the eyebrow out into
   .scale-group's own overlay (see the HTML comment at .scale-group) means
   its resting position is flush with the ANCHOR's top edge, i.e. flush with
   Hero Media's bottom edge — the anchor has no padding/inset of its own, by
   design, so it doesn't push Split Feature's actual content down too. Split
   Feature's row below still gets its own var(--space-m) top padding, so
   without this the eyebrow sits touching the video while its own content
   sits a full --space-m lower — margin-top here just restores that same
   offset to the eyebrow itself, matching it back up with the row it labels
   and giving the video a proper, token-driven section break instead of a
   hand-tuned gap. Doesn't touch .scale-group's own height/release boundary
   (see below) — that's driven by the anchor's top/bottom offsets, not by a
   descendant's margin, since the anchor is out of normal flow. */
.scale-group__eyebrow { margin-top: var(--space-m); }
/* Mobile match: Split Feature's own top padding drops to var(--space-xxs)
   at this breakpoint (see the 767px block), so the eyebrow's offset follows
   it down to keep the two aligned, same as at desktop. */
@media (max-width: 767px) {
  .scale-group__eyebrow { margin-top: var(--space-xxs); }
}
/* Release boundary fix: .scale-group's own height (used by the base
   .section__eyebrow-anchor rule's height: 100%) includes Centered
   Feature's own trailing padding-bottom (var(--space-m)) — the white
   breathing room before the Icon Group carousel — so the sticky range
   used to run all the way through that whitespace and release right up
   against the black section. bottom: var(--space-m) (replacing
   height: 100%) stops the anchor's containing block exactly at the real
   content's bottom edge instead, the same --space-m token that already
   defines that trailing gap, so the whitespace itself is untouched and
   simply sits below/outside the sticky boundary. Deterministic and
   viewport-height independent — not a pixel guess. */
.scale-group > .section__eyebrow-anchor {
  height: auto;
  bottom: var(--space-m);
}

/* =================================================================
   SPLIT NARRATIVE — alternating editorial image/copy pairs. Reused
   across About Icon, Sustainability and Reconciliation.
   ================================================================= */
.split-narrative { background: var(--color-black); padding: var(--space-m) 0; }
/* .split-narrative__frame carries .site-frame + .site-grid in markup
   (the same pair .split-feature__inner / .group-carousel__head use) —
   the whole editorial composition sits inside the centred 1920 frame
   AND its own eyebrow now participates in the real 12-column grid
   instead of a manually-computed pixel offset, so it lands on the
   exact same left column line as every other sticky eyebrow on the
   site (.split-feature__eyebrow, .group-carousel__eyebrow). The
   internal zig-zag math on .split-narrative__items (item offsets) is
   untouched — it's given grid-column: 1 / -1 so it keeps spanning the
   full content width exactly as it did as a plain block child, just
   now as an explicit grid item instead of an implicit one. */
.split-narrative__frame { position: relative; }
/* Sticky eyebrow lane — the SAME zero-footprint anchor convention as
   .section--split (homepage's Who We Are): .section__eyebrow-anchor is
   an absolutely-positioned overlay that reserves no layout space of its
   own, so the eyebrow no longer pushes .split-narrative__items down
   with a block-level margin. Its natural (non-stuck) position is simply
   the anchor's own top — the SAME top edge .split-narrative__items
   starts from — which is what makes image/text top-align with the
   eyebrow's resting line without any animation offset doing the work.
   grid-column: 1 / span 1 (not a manual `left` offset) is what fixes
   the anchor's HORIZONTAL position: giving an absolutely-positioned
   grid item explicit grid-line placement narrows its containing block
   from the whole grid container down to that column's own track, so
   `left: 0` below resolves against the real column-1 grid line — the
   same primitive .split-feature__eyebrow / .group-carousel__eyebrow
   use — rather than the frame's outer padding-box edge. The bottom
   margin only matters once the tablet override (see RESPONSIVE) drops
   the anchor back to position:static, where it's a normal block again
   and needs real spacing above the items below it. */
.split-narrative__frame > .section__eyebrow-anchor {
  grid-column: 1 / span 1;
  grid-row: 1;
  left: 0;
  margin-bottom: var(--space-xxs);
}
/* Same overflow-without-wrapping fix as .split-feature__eyebrow/
   .group-carousel__eyebrow — the label is wider than its nominal
   1-column cell and must stay on one line, overflowing into the
   (empty) gutter/column 2 rather than wrapping. Harmless for About's
   shorter "Who we are"/"Our values" labels, required for
   Sustainability's longer "Environmental responsibility". */
.split-narrative__frame > .section__eyebrow-anchor .eyebrow__track { white-space: nowrap; }
/* Full-width grid item — see the .split-narrative__frame comment above. */
.split-narrative__items { grid-column: 1 / -1; grid-row: 1; }
/* --secondary — an explicit, minimal opt-in for instances that need the
   sticky eyebrow's travel to END at the bottom of the FIRST item instead
   of running the full height of every item (the component's default,
   used by About Icon's Vision/Purpose pair, where both rows are one
   continuous eyebrow-relevant unit). .section__eyebrow-anchor's
   height: 100% resolves against ITS OWN grid area (grid-row: 1) — so
   giving a second `.split-narrative__items` wrapper grid-row: 2 removes
   it from that area entirely, without touching the anchor rule or the
   default (still grid-row: 1) that every other split-narrative keeps.
   Used by Sustainability's Environmental Targets row, which is a
   structurally distinct sub-section the "Environmental responsibility"
   eyebrow shouldn't keep describing once it's scrolled past. */
.split-narrative__items--secondary { grid-row: 2; margin-top: var(--space-xxs); }
/* Paired with --secondary above: for an ABSOLUTELY POSITIONED grid item
   (the anchor), grid-row: 1 (= grid-row-start: 1; grid-row-end: auto)
   does NOT mean "span row 1 only" — an indefinite end line resolves to
   the grid's LAST line instead, so the anchor would still stretch
   across both row tracks despite starting at row 1, silently
   defeating the whole point of splitting the items into two rows.
   This explicit end line (1 / 2) is what actually confines it. Only
   needed where a --secondary row exists; the single-row default
   (About's Vision/Purpose) is unaffected by this rule since it never
   carries this class. */
.split-narrative__frame > .split-narrative__eyebrow-anchor--pin-first { grid-row: 1 / 2; }
/* Every fixed pixel value below is paired with
   min(Npx, calc((100vw - 96px) * N/1824)) — exact at >=1920 (the
   calc() side only ever wins below 1920) and scales down with the
   viewport's ACTUAL usable width between 1199 and 1920, not raw
   100vw. (An earlier version used plain `vw`, i.e. a fraction of
   100vw — but --page-margin is a fixed 96px total in this tier, not
   a proportional one, so a pure-vw fraction still overflowed by a
   growing amount as the viewport shrank, e.g. ~24px at 1440. Basing
   the fraction on `100vw - 96px` — the frame's real content width —
   fixes that properly instead of just papering over it.) Below 1199
   the tablet breakpoint takes over and stacks everything anyway. */
.split-narrative__items {
  display: flex;
  flex-direction: column;
  gap: var(--space-xxs);
  /* margin-left AND width both need to fit inside the frame's content
     box together — max-width: 100% alone doesn't account for the
     margin eating into that space, so it has to be subtracted
     explicitly (100% - the same margin value, kept in one variable
     so the two can never drift apart). */
  --items-inset: min(308px, calc((100vw - 96px) * 0.16886));
  margin-left: var(--items-inset);
  width: 1516px;
  max-width: calc(100% - var(--items-inset));
}
/* Each row is its own intrinsic width (image + gap + copy), not a
   50/50 split of the 1516 column — Figma deliberately leaves a 154px
   editorial margin on one side. Item 1 (image left) hugs the RIGHT
   edge of the column, so it gets the 154px inset on its left; item 2
   (image right) already lands flush left because its wider 747px
   image naturally leaves that same 154px gap on the right. */
/* align-items: flex-start (not center) — same top-alignment convention
   already established by .section--split (homepage's Who We Are): text
   content aligns to its media's top edge rather than floating centred
   against it, especially visible on the shorter Purpose media/copy
   pairing where centering left the text sitting well below the image's
   own top. */
.split-narrative__item { display: flex; align-items: flex-start; gap: min(178px, calc((100vw - 96px) * 0.09759)); width: max-content; max-width: 100%; margin-left: min(154px, calc((100vw - 96px) * 0.08443)); }
.split-narrative__item--media-right { flex-direction: row-reverse; margin-left: 0; }
.split-narrative__media {
  width: min(var(--media-w, 592px), var(--media-vw, calc((100vw - 96px) * 0.32456)));
  flex-shrink: 0;
  border-radius: var(--radius-s);
  overflow: hidden;
}
.split-narrative__media img { width: 100%; height: min(var(--media-h, 750px), var(--media-h-vw, 39.06vw)); object-fit: cover; display: block; }
.split-narrative__content { width: min(var(--content-w, 592px), var(--content-vw, calc((100vw - 96px) * 0.32456))); flex-shrink: 0; display: flex; flex-direction: column; gap: var(--space-xxs); max-width: 100%; }
.split-narrative__content .h2 { color: var(--color-white); }
/* Multi-paragraph copy blocks need real spacing between paragraphs —
   margin: 0 on the <p>s below otherwise runs them straight together.
   18px matches the sitewide body-copy-large paragraph gap already used
   identically by .split-feature__copy/.centered-feature__copy/
   .project-copy. Invisible on every existing single-paragraph
   split-narrative row (About's Vision/Purpose) since a gap between one
   item is a no-op; only Sustainability's 3-paragraph row is affected. */
.split-narrative__copy { display: flex; flex-direction: column; gap: 18px; }
/* :not(.h5) — the Environmental Targets lead-in line ("Our
   environmental ambition is focused on...") uses the real .h5/.h5--dark
   heading classes (see below), not this body-copy default/528px column
   width; excluding it here lets that heading style actually win, and
   lets it span the full 900px content width instead of being capped to
   the narrower single-column width every other split-narrative copy
   block uses. */
.split-narrative__copy p:not(.h5) { margin: 0; font-size: 18px; font-weight: 400; line-height: 27.53px; letter-spacing: -0.18px; color: var(--color-white-70); max-width: 528px; }
/* Quote attribution (Reconciliation's Artwork Story instance only) —
   the "lower content treatment" Figma calls a configurable variant of
   this same shared component: .split-narrative__content's second row
   holds a quote + name/role instead of a heading, reusing
   .leadership-quote__attribution/__name/__role verbatim rather than
   inventing new type. Those default to the light-panel colours
   (dark-blue/body-grey); this scoped override recolors them for
   split-narrative's own black ground, same convention as
   .split-narrative--light recolouring .h2/.split-narrative__copy
   below for ITS ground. About's Vision/Purpose rows never use these
   classes, so they're untouched. */
.split-narrative__content .leadership-quote__name { color: var(--color-white); }
.split-narrative__content .leadership-quote__role { color: var(--color-white-70); }
/* --light variant — Sustainability's "Environmental responsibility"
   row uses a light-grey ground with dark-blue headings/body-grey copy
   instead of the component's default black/white ground. Nothing about
   the shared structure, eyebrow/sticky mechanism or per-item scroll
   reveal changes — only background + text-colour tokens. */
.split-narrative--light { background: var(--color-grey-101); }
.split-narrative--light .split-narrative__content .h2 { color: var(--color-dark-blue); }
.split-narrative--light .split-narrative__copy p:not(.h5) { color: var(--color-body-grey); }
/* Figma specifies 48px between each heading and what follows it in this
   instance (the component's generic default reuses the section-rhythm
   --space-xxs token, 64px) — scoped to --light rather than changed
   globally so About's own row spacing is untouched. */
.split-narrative--light .split-narrative__content { gap: 48px; }
/* --icon-dark-blue: Life at Icon's "Women in construction" instance
   sits on the brand dark-blue ground (#14294f) instead of the
   component's default flat black — Figma's own literal colour for
   this instance, same minimal token-only modifier convention as
   --light above (and .image-list--icon-dark-blue). White text/copy
   already read correctly against dark-blue exactly as they do against
   black, so no text-colour overrides are needed here. */
.split-narrative--icon-dark-blue { background: var(--color-dark-blue); }

/* Environmental Targets — the 3-column stat layout inside the second
   row's content block (900px wide, see the item's --content-w inline
   style). A genuinely new, minimal shape scoped to this component:
   nothing existing already gives a 3-up heading+paragraph grid, so it's
   built here rather than shoehorned into .split-feature__copy or
   .image-list__rows. Column width/gap are Figma's own literal values
   (284px / 24px), not derived from the unrelated 12-column site grid. */
.split-narrative__targets { display: flex; gap: 24px; }
.split-narrative__targets-item { width: 284px; flex-shrink: 0; display: flex; flex-direction: column; gap: 24px; }
.split-narrative__targets-heading { margin: 0; }
.split-narrative__targets-body { margin: 0; font-size: 18px; font-weight: 400; line-height: 27.53px; letter-spacing: -0.18px; color: var(--color-body-grey); }

/* =================================================================
   IMAGE + LIST — image beside a data-driven list of rows/dividers.
   ================================================================= */
.image-list { background: var(--color-black); padding: 0 0 var(--space-m); }
/* .image-list__frame is .site-frame in markup. The existing flex
   math (592px image/eyebrow col + 178 gap + flex:1 remainder) already
   lands exactly on the 4-col/7-col Figma split — it just needs to be
   relative to the centred frame, not the raw viewport. */
/* z-index: 0 (not just position: relative) is required here — it's
   what makes .image-list__frame establish its OWN stacking context, so
   the anchor's negative z-index (below) is scoped to sit behind THIS
   frame's own content only. Without it, the negative z-index falls
   through to the nearest real stacking context, which is all the way
   up at .image-list's own solid black section background — hiding the
   eyebrow completely, everywhere, not just behind the image. */
.image-list__frame { padding-inline: var(--page-margin); position: relative; z-index: 0; }
/* Anchor is now nested inside .image-list__intro (see the HTML comment
   there) rather than a sibling of it, so its containing block is the
   intro row itself — left: 0 lands at the intro's own start edge, which
   is already inset by the frame's padding, same convention as
   .section__intro .section__eyebrow-anchor. */
.image-list__intro > .section__eyebrow-anchor { left: 0; padding-bottom: 96px;}
.image-list__intro { display: flex; position: relative; gap: 178px; align-items: flex-start; padding-bottom: 96px; }
/* Empty spacer replacing the old .image-list__eyebrow-col: keeps the
   heading's original left offset (592px + the row's 178px gap) now that
   the actual eyebrow is no longer a real flex child reserving that
   space itself. */
.image-list__eyebrow-spacer { width: 592px; flex-shrink: 0; }
.image-list__heading { flex: 1; color: var(--color-white); padding-top: 8px; }
.image-list__body { display: flex; gap: 178px; align-items: flex-start; }
.image-list__media { width: 592px; flex-shrink: 0; border-radius: var(--radius-s); overflow: hidden; height: 366px; }
.image-list__media img { width: 100%; height: 100%; object-fit: cover; display: block; }
/* Optional media column (Reconciliation's Innovate RAP instance only —
   About's Our Values instance has no CTA, so .image-list__media stays a
   direct flex child of .image-list__body there, untouched). Wraps the
   image so a trailing CTA can sit in the SAME column, pushed to the
   bottom of the row via margin-top: auto rather than a fixed offset —
   align-self: stretch lets this column match .image-list__rows' taller
   height without changing .image-list__body's own shared align-items. */
.image-list__media-col { width: 592px; flex-shrink: 0; display: flex; flex-direction: column; gap: 24px; align-self: stretch; }
/* Figma closes the list with a trailing rule after the last row too,
   not just dividers between rows. */
.image-list__rows { flex: 1; display: flex; flex-direction: column; border-bottom: 1px solid var(--color-white-20); }
.image-list__row { display: flex; gap: 40px; padding: 40px 0; }
.image-list__row + .image-list__row { border-top: 1px solid var(--color-white-20); }
.image-list__row-title { width: 283px; flex-shrink: 0; color: var(--color-icon-blue); font-size: 24px; font-weight: 600; line-height: 28.8px; letter-spacing: -0.24px; }
.image-list__row-body { flex: 1; margin: 0; font-size: 18px; font-weight: 400; line-height: 27.53px; letter-spacing: -0.18px; color: var(--color-white-70); }
/* --light variant — Reconciliation's "Innovate RAP" instance uses a
   white ground with dark-blue headings/body-grey copy instead of the
   component's default black/white ground, same convention as
   .split-narrative--light. Nothing about the shared structure,
   eyebrow/sticky mechanism or scroll-reveal cascade changes here —
   only background + text-colour tokens. */
.image-list--light { background: var(--color-white); }
.image-list--light .image-list__heading { color: var(--color-dark-blue); }
.image-list--light .image-list__row-title { color: var(--color-dark-blue); }
.image-list--light .image-list__row-body { color: var(--color-body-grey); }
.image-list--light .image-list__rows { border-bottom-color: var(--color-light-grey); }
.image-list--light .image-list__row + .image-list__row { border-top-color: var(--color-light-grey); }
/* --icon-dark-blue: Life at Icon's "Industry recognition" instance sits
   on the brand dark-blue ground (#14294f) instead of the component's
   default flat black — Figma's own literal colour for this instance.
   Same minimal token-only modifier convention as --light above; nothing
   about the structure/eyebrow/scroll-reveal changes. */
.image-list--icon-dark-blue { background: var(--color-dark-blue); }
/* Optional intro-copy wrapper (Reconciliation's Innovate RAP instance
   adds a supporting paragraph under the heading; About's Our Values
   instance has no copy and never uses this class, so its own
   heading-as-direct-flex-child layout is untouched). Reuses
   .split-feature__copy for the paragraph styling itself instead of
   duplicating it. */
.image-list__intro-col { flex: 1; display: flex; flex-direction: column; gap: 48px; }
/* Optional list-heading row (Reconciliation's "Our commitment includes
   72 actions..." lead-in, sitting above the rule-separated rows) —
   reuses .h4/.h4--dark for its type rather than a new size. Margin
   matches the 40px rhythm .image-list__row's own padding already
   establishes between rows. */
.image-list__list-heading { margin: 0 0 40px; }
/* Optional CTA (Reconciliation only) — reuses .btn-link as-is. Lives in
   .image-list__media-col, bottom-aligned via margin-top: auto so it
   sits at the same baseline as .image-list__rows' own bottom edge
   without a fixed offset. */
.image-list__cta { margin-top: auto; }

/* =================================================================
   HERITAGE TIMELINE — Our Story page only (not a shared section; no
   other About-family page uses this layout). White section tracing
   Icon's history as a rail of dated milestones. The rail's fill height
   and each dot's active state are driven by scroll position (see
   initHeritageTimeline, js/about.js), so the line always shows exactly
   how far through the story the reader has scrolled — never a static
   decoration. Milestone images reuse .image-list__media's exact sizing
   (592x366, radius-s) rather than redeclaring it.
   ================================================================= */
.heritage { background: var(--color-white); padding: var(--space-m) 0 var(--space-xxs); }
.heritage__frame { padding-inline: var(--page-margin); }
.heritage__intro { display: flex; gap: 178px; align-items: flex-start; margin-bottom: var(--space-xs); }
.heritage__eyebrow-col { width: 130px; flex-shrink: 0; padding-top: 4px; }
/* The eyebrow column is only 130px (kept for the shared spacer/gap
   math below), narrower than "OUR HERITAGE" at this weight/size — same
   overflow-without-wrapping fix already used for .group-carousel__eyebrow. */
.heritage__eyebrow-col .eyebrow__track { white-space: nowrap; }
.heritage__content { flex: 1; display: flex; flex-direction: column; gap: 48px; max-width: 1210px; }
.heritage__heading { max-width: 100%; }
.heritage__copy { max-width: 900px; }

/* Timeline: an empty spacer column keeps the rail/items aligned under
   .heritage__content above (same 130px + 178px gap math as the intro
   row), rather than under the eyebrow. */
.heritage__timeline { display: flex; gap: 178px; }
.heritage__timeline-spacer { width: 130px; flex-shrink: 0; }
.heritage__rail-wrap { flex: 1; position: relative; }
/* Pinned storytelling mode (desktop/tablet only — applied by
   initHeritageTimeline, js/about.js, and never on mobile). Height is
   set inline by JS since it depends on the live --nav-height and the
   intro's own (variable) height; this class carries the parts that
   are always true once it's added: clip the moving rail-wrap to that
   height instead of letting it stretch/overflow, and fade its top and
   bottom edges so milestones dissolve in and out rather than being
   hard-cut by the clip. */
.heritage__timeline--pinned {
  overflow: hidden;
  align-items: flex-start;
  /* Exposed as a variable (rather than baked into the gradient below)
     so initHeritageTimeline (js/about.js) can read the same number to
     hold the first milestone clear of the fade at rest, instead of a
     second hand-tuned JS constant drifting out of sync with this one. */
  --heritage-fade: 24px;
  -webkit-mask-image: linear-gradient(to bottom, transparent 0, #000 var(--heritage-fade), #000 calc(100% - var(--heritage-fade)), transparent 100%);
  mask-image: linear-gradient(to bottom, transparent 0, #000 var(--heritage-fade), #000 calc(100% - var(--heritage-fade)), transparent 100%);
}
.heritage__rail { position: absolute; top: 0; bottom: 0; left: 0; width: 4px; background: var(--color-grey-200, #dcdddf); border-radius: 2px; }
.heritage__rail-fill { position: absolute; top: 0; left: 0; width: 100%; height: 0%; background: var(--color-icon-blue); border-radius: 2px; }
.heritage__dot {
  position: absolute;
  left: -3px;
  top: 0;
  width: 10px;
  height: 10px;
  border-radius: 50%;
  background: var(--color-grey-200, #dcdddf);
  transform: translateY(-50%);
  transition: background-color 0.3s ease;
}
.heritage__dot.is-active { background: var(--color-icon-blue); }

.heritage__list { display: flex; flex-direction: column; gap: 114px; padding-left: 48px; }
.heritage__item { display: flex; gap: 178px; align-items: flex-start; }
.heritage__item-text { display: flex; gap: 24px; width: 100%; max-width: 699px; }
.heritage__year { width: 237px; flex-shrink: 0; }
.heritage__copy-wrap { flex: 1; min-width: 0; padding-top: 14px; display: flex; flex-direction: column; gap: 13px; }
.heritage__title { margin: 0; }
.heritage__desc { margin: 0; }
/* Two milestones (1856, 1861) use Figma's smaller Body/Body Copy
   Regular size — every other milestone uses the larger .body-lg--grey
   (already defined above) as-is, so only the smaller size needs its
   own rule here. */
.heritage__desc--sm { font-size: 16px; font-weight: 400; line-height: 24.47px; letter-spacing: -0.16px; color: var(--color-body-grey); }
.heritage__media { flex-shrink: 0; }
/* Figma reuses the 1856 photo (at 40% opacity) for the 2017–19
   milestone rather than supplying a distinct asset — reproduced as
   designed via this modifier, not a placeholder added during build. */
.heritage__media--faded img { opacity: 0.4; }

/* =================================================================
   ABOUT GATEWAY — shared closing nav block for every About page.
   ================================================================= */
.about-gateway {
  position: relative;
  /* The dark treatment stays the section's own background — it's the
     base layer beneath everything, always what's visible until a
     card's preview media fades in above it, and what shows again once
     it fades back out. */
  background: var(--color-black);
  overflow: hidden;
  /* Figma fixes this section at a 700px panel (96 top / 80 bottom),
     with the eyebrow pinned to the top and the cards to the bottom —
     not a fixed gap between them. min-height + space-between
     reproduces that at the 1920 desktop size this pass targets. */
  min-height: 700px;
  padding: 96px 0 80px;
  display: flex;
  flex-direction: column;
}
/* Hover-preview media stack — see initAboutGatewayPreview (js/about.js).
   Absolutely positioned/full-bleed and z-index'd below the content, so
   it never participates in the section's own layout/height (that's
   still governed entirely by .about-gateway__frame below). */
.about-gateway__bg { position: absolute; inset: 0; z-index: 0; }
.about-gateway__media {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  opacity: 0;
  transition: opacity 0.5s ease-in-out;
}
.about-gateway__media.is-active { opacity: 1; }
/* Sits between the media and the content — keeps card/heading text
   legible over a photo or video the same way it already reads over
   the plain black default, without needing the photography itself to
   be dimmed or desaturated. */
.about-gateway__scrim {
  position: absolute;
  inset: 0;
  z-index: 1;
  background: rgba(0, 0, 0, 0.68);
}
/* .about-gateway__frame is .site-frame in markup — keeps the cards
   from spreading wider than the 1920 canvas on ultrawide monitors. */
.about-gateway__frame { position: relative; z-index: 2; flex: 1; display: flex; flex-direction: column; justify-content: space-between; padding-inline: var(--page-margin); }
.about-gateway__cards { display: flex; flex-wrap: wrap; gap: 48px; justify-content: center; }
.about-gateway__card {
  width: 326px;
  flex: 1 1 220px;
  max-width: 326px;
  display: flex;
  flex-direction: column;
  color: var(--color-white-70);
}
/* Rule + fill reuses the exact .about-menu__rule/__rule-fill /
   .tabbed-content__tab-rule(-fill) pattern — a light static base line
   with an Icon-blue fill scrubbed over it via scaleX, not a
   border-top colour swap (a border can't be animated this way). Same
   0.45s cubic-bezier(0.4, 0, 0.2, 1) timing as both of those, so the
   sweep feels identical across all three components. Base colour
   stays the card's existing white-40% (Figma's "Default" hairline),
   not the menu's white-20%/tab's white-70% — that shade was already
   correct here, only the fake border-based animation was wrong. */
.about-gateway__card-rule {
  position: relative;
  width: 100%;
  height: 1px;
  background: var(--color-white-40);
}
.about-gateway__card-rule-fill {
  position: absolute;
  top: 0; left: 0;
  height: 1px;
  width: 100%;
  background: var(--color-icon-blue);
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform 0.45s cubic-bezier(0.4, 0, 0.2, 1);
}
/* padding-top now lives on -top (not the card itself) so the rule
   element can sit in normal flow as the first flex child — preserves
   the Figma 24px rule->title / 10px title->desc rhythm without a
   single shared `gap` mis-applying itself across the new 3rd child.
   align-items: flex-start (not the About menu's `center`) — this
   28px/38px-line-height title is tall enough that centring pins the
   arrow to the row's middle instead of its own top edge; flush + a
   small nudge reads as "top-right of the content" instead. */
.about-gateway__card-top { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; padding-top: 24px; }
.about-gateway__card-title {
  margin: 0;
  /* Default (Figma "Default"): subdued grey — Current/Hover promote
     it to Icon blue, same token/timing as the About menu + tabs. */
  color: var(--color-white-70);
  transition: color 0.3s ease;
      font-size: 28px;
    font-weight: 600;
    line-height: 38px;
}
.about-gateway__card-desc { margin: 0; margin-top: 10px; }
.about-gateway__card-arrow {
  width: 18px; height: 18px; flex: 0 0 18px;
  margin-top: 8px;
  color: var(--color-icon-blue);
  opacity: 0;
  transform: translateX(-4px);
  transition: opacity 0.3s ease, transform 0.3s ease;
}
/* Active (.is-current) is a permanent state: blue title, full blue
   rule, no arrow, no hover animation — `:not(.is-current)` gates every
   hover/focus rule below so hovering the current page's own card
   never replays the sweep or reveals the arrow (previously it did,
   since :hover applied unconditionally). */
.about-gateway__card.is-current .about-gateway__card-title {
  color: var(--color-icon-blue);
}
.about-gateway__card.is-current .about-gateway__card-rule-fill {
  transform: scaleX(1);
  transition: none;
}
.about-gateway__card.is-current .about-gateway__card-desc {
  color: var(--color-white);
}
.about-gateway__card:not(.is-current):hover .about-gateway__card-title,
.about-gateway__card:not(.is-current):focus-visible .about-gateway__card-title {
  color: var(--color-icon-blue);
}
.about-gateway__card:not(.is-current):hover .about-gateway__card-rule-fill,
.about-gateway__card:not(.is-current):focus-visible .about-gateway__card-rule-fill {
  transform: scaleX(1);
}
.about-gateway__card:not(.is-current):hover .about-gateway__card-arrow,
.about-gateway__card:not(.is-current):focus-visible .about-gateway__card-arrow { opacity: 1; transform: translateX(0); }

/* =================================================================
   GROUP CAROUSEL — horizontally scrolling cards. Reuses Featured
   Projects' own control treatment (.fp-arrow / .fp-dots / .fp-dot /
   .icon-arrow-direction) rather than a second control component.
   ================================================================= */
.group-carousel { background: var(--color-black); padding: var(--space-l) 0; overflow: hidden; }
/* .group-carousel__head carries .site-frame + .site-grid in markup.
   Eyebrow sits at column 1; heading and controls share the same
   cols 3–12 area (heading left-aligned via its own max-width,
   controls right-aligned via justify-self: end) so controls track
   the grid's right edge, not the physical viewport edge. */
.group-carousel__head { align-items: center; margin-bottom: 92px; }
/* All three share row 1 explicitly — heading and controls deliberately
   occupy the SAME column range (see below) so they can sit side by
   side within it; without an explicit grid-row, auto-placement would
   push the second same-column item onto its own row instead. */
.group-carousel__eyebrow { grid-column: 1 / span 1; grid-row: 1; }
/* Figma's eyebrow label is wider than its nominal 1-column cell —
   it must stay on one line and is allowed to overflow into the
   (empty) gutter/column 2 rather than wrap or force the column wider. */
.group-carousel__eyebrow .eyebrow__track { white-space: nowrap; }
.group-carousel__heading { grid-column: 3 / span 10; grid-row: 1; max-width: 898px; }
.group-carousel__controls { grid-column: 3 / span 10; grid-row: 1; justify-self: end; }
.group-carousel__track {
  display: flex;
  gap: 24px;
  overflow-x: auto;
  scroll-snap-type: x proximity;
  scroll-behavior: smooth;
  /* Figma's track padding is deliberately asymmetric: the first card
     aligns under the heading's left edge (130 eyebrow + 178 gap = 308,
     plus the 48 page margin ≈ 355), not under the plain page gutter —
     and the right padding is trimmed to 202 so the next card peeks in. */
  padding: 0 202px 0 355px;
  -ms-overflow-style: none;
  scrollbar-width: none;
}
.group-carousel__track::-webkit-scrollbar { display: none; }
.group-carousel__track > .group-card:first-child { scroll-snap-align: none; }
/* Grab-to-drag affordance (see initGroupCarousel, js/about.js) — a
   fine-pointer/hover-capable cursor hint only; touch has no cursor and
   keeps its own native swipe untouched. --dragging is toggled purely
   in JS for the actual gesture (also disables text selection, which
   click-dragging across card copy would otherwise trigger). */
@media (hover: hover) and (pointer: fine) {
  .group-carousel__track { cursor: grab; }
}
.group-carousel__track--dragging {
  cursor: grabbing;
  scroll-behavior: auto; /* dragging writes scrollLeft every pointermove — no smoothing fighting the pointer's own position */
  user-select: none;
}
.group-card {
  scroll-snap-align: start;
  flex: 0 0 auto;
  /* Card 1 alone is exempt (see :first-child below) — it rests at
     the frame-relative 355px inset (scrollLeft 0), which is NOT its
     own scroll-snap-align:start position (that would be 355). Giving
     it a snap point there means the browser continually re-pulls
     scrollLeft to 355 the instant snapping is active — on load, and
     again every time we return to it — eating the designed inset. */
  width: min(1516px, 82vw);
  min-height: 590px;
  background: var(--color-white);
  border: 1px solid var(--color-grey-200, #ecedef);
  border-radius: var(--number-scale-m, 12px);
  padding: 48px;
  display: flex;
  align-items: stretch;
  justify-content: space-between;
  gap: 48px;
}
.group-card__content { display: flex; flex-direction: column; justify-content: space-between; width: 543px; max-width: 100%; }
.group-card__logo { height: 64px; width: auto; object-fit: contain; object-position: left; margin-bottom: 32px; }
/* Barpa's brand mark is exported white-on-transparent (built for the
   black footer/nav); inverted here so it reads on the carousel's
   white card without needing a second colour asset. */
.group-card__logo--on-light { filter: invert(1); }
.group-card__body { display: flex; flex-direction: column; gap: 32px; }
.group-card__title { margin: 0 0 16px; }
.group-card__desc { margin: 0; font-size: 18px; font-weight: 400; line-height: 27.53px; letter-spacing: -0.18px; color: var(--color-body-grey); }
.group-card__cta { color: var(--color-icon-blue); font-size: 16px; font-weight: 600; letter-spacing: -0.16px; display: inline-flex; align-items: center; gap: 8px; align-self: flex-start; }
.group-card__foot { display: flex; flex-direction: column; gap: 32px; }
.group-card__rule { height: 1px; background: var(--color-grey-200, #dcdddf); }
.group-card__tags { display: flex; gap: 10px; flex-wrap: wrap; }
.group-card__tag {
  display: inline-flex; align-items: center; justify-content: center;
  padding: 6px 12px; border: 1px solid var(--color-grey-200, #dcdddf);
  border-radius: 1009px; font-size: 14px; line-height: 21.41px; letter-spacing: -0.14px;
  color: var(--neutral-mid-grey, #aaacb1); white-space: nowrap;
}
.group-card__media { width: 697px; max-width: 100%; flex-shrink: 0; border-radius: var(--radius-s); overflow: hidden; }
.group-card__media img { width: 100%; height: 100%; object-fit: cover; display: block; }

/* =================================================================
   ABOUT — RESPONSIVE
   ================================================================= */
@media (max-width: 1199px) {
  /* .site-grid becomes display:block at this breakpoint (see the site
     grid rule itself); .page-intro__row needs flex back to stack the
     crumb/content with a gap between them. */
  .page-intro__row { display: flex; flex-direction: column; gap: 24px; }
  .page-intro__crumb { width: auto; }

  .hero-media__frame { height: clamp(320px, 55vw, 640px); }

  /* .site-grid is display:block here, so the 2 remaining grid items
     (eyebrow moved out to .scale-group__eyebrow-frame — see below) just
     stack in DOM order as full-width blocks — reset their column
     placement (meaningless outside grid) and give them a gap via
     margin since block layout has no `gap`. */
  .split-feature__content,
  .split-feature__media,
  .split-feature--media-left .split-feature__media,
  .split-feature--media-left .split-feature__content,
  .split-feature--media-portrait .split-feature__media { grid-column: auto; width: 100%; margin-bottom: 32px; }
  .split-feature__media:last-child { margin-bottom: 0; }
  /* --media-portrait's fixed aspect-ratio is a desktop-grid artifact
     (matches the 592×750 column exactly at 1920) — once the media
     panel goes full-width in this stacked layout it should crop by the
     component's normal default (natural image ratio) instead. */
  .split-feature--media-portrait .split-feature__media img { aspect-ratio: auto; }
  /* Same reasoning as the sticky eyebrow's own reset above — Careers
     Pathway's stats column stacks under the content below desktop, so
     pinning it would trap it awkwardly rather than let it scroll with
     its row. Placed after the base rule (see SPLIT FEATURE) so it
     actually wins at equal specificity. */
  .split-feature__media--sticky {
    align-self: auto;
  }
  .split-feature__media--sticky .stats__list {
    position: static;
  }

  /* Scale group: sticky pinning only makes sense across a tall desktop
     range — below desktop the eyebrow just scrolls normally with its
     content, same convention as every other sticky eyebrow's tablet
     reset. Collapses the custom 12-col recipe back to a simple padded
     block sitting above Split Feature, in the same DOM position (and
     therefore the same visual order) the eyebrow always occupied. */
  .scale-group__eyebrow-frame {
    position: static;
    display: block;
    height: auto;
    max-width: none;
    margin-bottom: 32px;
  }
  .scale-group__eyebrow-frame .eyebrow { position: static; }

  .centered-feature__heading,
  .centered-feature__copy { max-width: 100%; }

  .kajima-map__label { padding: 5px 8px; font-size: 12px; }

  .split-narrative__items { margin-left: 0; width: 100%; }
  .split-narrative__item,
  .split-narrative__item--media-right { flex-direction: column; gap: 24px; width: 100%; margin-left: 0; }
  .split-narrative__media,
  .split-narrative__content { width: 100%; }

  /* Image montage: same "stack full-width, let the natural image ratio
     take over" reset .split-feature--media-portrait's own tablet rule
     already uses above — the fixed desktop widths/heights are a
     12-column-grid artifact. */
  .image-montage__row { flex-direction: column; }
  .image-montage__item { flex: none; width: 100%; aspect-ratio: auto; height: auto; }
  .split-narrative__media img { height: auto; aspect-ratio: 4 / 3; }
  .split-narrative__copy p { max-width: 100%; }
  .split-narrative__items--secondary { grid-row: auto; margin-top: 0; }
  .split-narrative__targets { flex-direction: column; gap: 32px; }
  .split-narrative__targets-item { width: 100%; }

  .image-list__intro,
  .image-list__body { flex-direction: column; gap: 24px; }
  .image-list__media { width: 100%; }
  .image-list__media { height: auto; aspect-ratio: 4 / 3; }
  .image-list__media-col { width: 100%; }
  .image-list__row { flex-direction: column; gap: 12px; }
  .image-list__row-title { width: auto; }
  /* Eyebrow spacer served only to hold the heading's flex offset next
     to the (now-overlaid) eyebrow on desktop — meaningless once the
     row stacks. Anchor drops to normal flow, sitting above the intro
     in the same DOM position it always occupied. */
  .image-list__eyebrow-spacer { display: none; }
  .image-list__eyebrow-anchor { position: static; height: auto; margin-bottom: 24px; }
  .image-list__eyebrow-anchor .eyebrow { position: static; }

  /* Heritage timeline: drop the empty spacer column (nothing left to
     align it against once .heritage__intro stacks) and give the rail
     a simple fixed indent instead. */
  .heritage__intro { flex-direction: column; gap: 24px; margin-bottom: var(--space-xxs); }
  .heritage__eyebrow-col { width: auto; padding-top: 0; }
  .heritage__timeline { gap: 0; }
  .heritage__timeline-spacer { display: none; width: 0; }
  .heritage__list { padding-left: 32px; gap: 80px; }
  .heritage__item { gap: 40px; }
  .heritage__item-text { gap: 20px; max-width: 100%; }

  /* .about-gateway__frame now holds the eyebrow+cards flex column
     (see desktop rule); below desktop it falls back to a simple gap
     instead of space-between, since there's no more fixed 700px
     panel height for space-between to distribute. */
  .about-gateway { min-height: 0; }
  .about-gateway__frame { justify-content: flex-start; gap: 64px; }

  /* .site-grid is display:block here — restore a wrapping flex row
     so eyebrow/heading/controls degrade gracefully instead of relying
     on the desktop-only grid-column/justify-self placement. */
  .group-carousel__head { display: flex; flex-wrap: wrap; gap: 24px; }
  .group-carousel__eyebrow { grid-column: auto; width: auto; }
  .group-carousel__heading,
  .group-carousel__controls { grid-column: auto; }
  .group-carousel__controls { margin-left: auto; justify-self: auto; }
  .group-carousel__track { padding: 0 var(--page-margin); }
  .group-card { width: 88vw; flex-direction: column-reverse; padding: var(--space-xxs); gap: 32px; min-height: 0; }
  .group-card__content,
  .group-card__media { width: 100%; max-width: 100%; }
  .group-card__media { aspect-ratio: 16 / 10; }
}

@media (max-width: 767px) {
  /* Every About section's horizontal gutter now comes from its
     .site-frame child's padding-inline (always var(--page-margin),
     which itself already changes to 20px at this breakpoint via the
     :root override above) — so none of these need padding-left/right
     added directly on the section itself any more. */
  .page-intro { padding-top: calc(var(--nav-height) + var(--space-xxs)); padding-bottom: var(--space-xxs); }
  .page-intro__content { gap: 32px; }
  /* .h1 had no mobile override at all before Projects — About's own H1
     happened never to contain a single word wide enough at a flat 64px
     to overflow a narrow column, but that was luck of the copy, not a
     property of the shared type utility (Projects' "Construction" is
     the word that finally exposed it: a flex/grid item's box can't
     shrink narrower than its own longest unbreakable word). Same
     clamp treatment .h2 already gets above. */
  .page-intro__heading { font-size: clamp(32px, 9vw, 48px); line-height: 1.15; }

  .hero-media__frame { height: 60vw; min-height: 220px; }

  .split-feature { padding-top: var(--space-xxs); padding-bottom: var(--space-xxs); }
  .centered-feature { padding-bottom: var(--space-xxs); }
  .kajima-map__dot { width: 6px; height: 6px; box-shadow: 0 0 0 3px rgba(0, 184, 241, 0.16); }
  /* The dense South/East Asia cluster has no room left at this width
     to scatter 7 separate tags without them overlapping (their real
     geographic anchors are only a few % apart). Rather than force
     illegible overlap or an oddly tall map, the dots stay correctly
     plotted on the map (still geographically real) but the on-map
     TAGS are swapped for a plain wrapped legend list underneath — see
     .kajima-map__legend below, populated from the same data array. */
  .kajima-map__label { display: none; }
  .kajima-map__legend {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
    margin: 16px 0 0;
    padding: 0;
    list-style: none;
  }
  .kajima-map__legend li {
    padding: 5px 10px;
    background: var(--color-white);
    border: 1px solid var(--color-grey-200, #ecedef);
    border-radius: var(--radius-xs);
    font-size: 12px;
    font-weight: 600;
    color: var(--color-dark-blue);
  }
  .split-narrative { padding-top: var(--space-xxs); padding-bottom: var(--space-xxs); }
  .split-narrative__items { gap: 40px; }
  .image-list { padding-top: var(--space-xxs); padding-bottom: var(--space-xxs); }
  .image-list__intro { padding-bottom: var(--space-xxs); }

  .heritage { padding-top: var(--space-xxs); padding-bottom: var(--space-xxs); }
  .heritage__list { padding-left: 24px; gap: 56px; }
  .heritage__item { flex-direction: column; gap: 20px; }
  .heritage__item-text { flex-direction: column; gap: 12px; }
  .heritage__year { width: auto; font-size: 40px; line-height: 1.1; }
  .heritage__copy-wrap { padding-top: 0; }

  .about-gateway__cards { flex-direction: column; align-items: stretch; }
  .about-gateway__card { width: 100%; max-width: 100%; }

  .group-carousel { padding: var(--space-xxs) 0; }
  .group-card { width: 92vw; padding: 24px; }
}

/* =================================================================
   OUR PEOPLE — Managing Directors + Group & Divisional Team
   Both sections share one row shape: a sticky left label beside a
   card collection, reusing the site's existing primitives rather than
   inventing new ones —
     - .people-section__row is .site-frame + .site-grid in markup, same
       pair every other section on the site uses for column placement.
     - the label reuses .eyebrow/.eyebrow--dark/.eyebrow--sticky as-is
       (it already owns its own grid column here, the exact shape
       .eyebrow--sticky was built for — see .split-feature__eyebrow /
       .group-carousel__eyebrow) — nothing about the sticky mechanism
       itself is touched.
     - person-card is one data-driven primitive with two size variants
       (--lg for the two Managing Directors, --sm for the Group &
       Divisional Team grid) rather than a bespoke card per section,
       per the Figma annotation on both source frames.
   ================================================================= */
.people-directors { background: var(--color-white); padding: var(--space-s) 0; }
.people-team { background: var(--color-grey-101); padding: var(--space-s) 0 var(--space-m); }
.people-section__row { align-items: flex-start; }
/* align-self: stretch gives the label column the row's real (full card-
   grid) height — same fix .page-intro__crumb/.split-feature__eyebrow
   already needed — so .eyebrow--sticky has room to actually stick and
   release instead of sitting static at its own (content-only) height. */
.people-section__label { grid-column: 1 / span 3; align-self: stretch; }
.people-section__content { grid-column: 4 / span 9; }

.people-directors__cards { display: flex; gap: 24px; }
.people-team__grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 96px 24px; }

/* Whole card is the click target (a real <button> in markup — see
   js/people.js), opening the shared profile modal. .media-hover
   already supplies the established site-wide hover-zoom on the framed
   image (see the primitive's own comment above); this adds a subtle,
   site-consistent focus affordance for keyboard users on top of that,
   using the same icon-blue accent every other interactive element on
   the page already uses. */
.person-card {
  display: flex;
  flex-direction: column;
  gap: 32px;
  width: 100%;
  text-align: left;
  cursor: pointer;
}
.person-card:focus-visible {
  outline: 2px solid var(--color-icon-blue);
  outline-offset: 6px;
  border-radius: var(--radius-s);
}
.person-card__media {
  position: relative;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  border-radius: var(--radius-s);
  overflow: hidden;
  padding: 48px 32px;
}
.person-card__media img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  z-index: 0;
}
.person-card__scrim {
  position: absolute;
  inset: 0;
  background: linear-gradient(to top, rgba(0, 0, 0, 0.6) 0%, rgba(0, 0, 0, 0) 40.26%);
}
.person-card__body { position: relative; z-index: 1; display: flex; flex-direction: column; gap: 2px; }
.person-card__role {
  margin: 0;
  font-size: 16px;
  font-weight: 600;
  line-height: 24.47px;
  letter-spacing: -0.16px;
  color: var(--color-grey-600);
}

/* --lg: Managing Directors (2-up, Figma 669x650) */
.person-card--lg { flex: 1 1 0; min-width: 0; }
.person-card--lg .person-card__media { aspect-ratio: 669 / 650; }
.person-card__name--lg {
  margin: 0;
  font-size: 32px;
  font-weight: 600;
  line-height: 48px;
  letter-spacing: -0.5px;
  color: var(--color-dark-blue);
}

/* --sm: Group & Divisional Team grid (3-up, Figma 438x584 = 3:4) */
.person-card--sm .person-card__media { aspect-ratio: 3 / 4; }
/* .person-card__name reuses .h5.h5--dark directly in markup (identical
   Figma type spec) — no size override needed here. */

/* ---------- OUR PEOPLE — RESPONSIVE ---------- */
@media (max-width: 1199px) {
  /* .site-grid is display:block at this tier — label/content just
     stack in DOM order (label above cards), same convention as every
     other sticky-eyebrow row's tablet reset. */
  .people-section__label,
  .people-section__content { grid-column: auto; }
  .people-section__label { margin-bottom: 24px; }
  .people-team__grid { grid-template-columns: repeat(2, 1fr); gap: 48px 24px; }
}
@media (max-width: 767px) {
  .people-directors,
  .people-team { padding-top: var(--space-xxs); }
  .people-team { padding-bottom: var(--space-xxs); }
  .people-directors__cards { flex-direction: column; gap: 32px; }
  .people-team__grid { grid-template-columns: 1fr; gap: 32px; }
}

/* =================================================================
   PROFILE MODAL — reusable full-screen person profile (Our People).
   Figma: node 20915:3148. One dialog in the DOM, populated at
   open-time by js/people.js from whichever card was clicked — never
   duplicated per person. The "View <next person>" next-profile control
   shown in Figma is INTENTIONALLY not implemented: the frame's own
   annotation says that interaction is not yet specified, so nothing
   beyond "Back to team" / close / Escape exists yet (see js/people.js
   header comment).

   Sizing: position:fixed + inset:0 is the actual full-viewport cover
   (robust even where dvh support is patchy); explicit height:100vh
   with a height:100dvh enhancement underneath is layered on top so
   percentage-height children (.profile-modal__dialog /
   .profile-modal__panel / .profile-modal__scroll) have a reliable
   height ancestor on browsers with dynamic mobile toolbars, per the
   brief's request for dvh-aware sizing rather than vh alone. The
   MODAL ITSELF never scrolls (overflow: hidden) — only
   .profile-modal__scroll (the right column's lower content) does,
   via flex: 1 1 auto + min-height: 0 + overflow-y: auto, exactly the
   "left stays put, right scrolls independently" split the brief asks
   for. The portrait needs no special scroll-prevention of its own; it
   simply never sits inside the one scrolling element. */
.profile-modal {
  position: fixed;
  inset: 0;
  z-index: 200;
  background: var(--color-icon-deep-blue);
  height: 100vh;
  height: 100dvh;
  overflow: hidden;
}
.profile-modal[hidden] { display: none; }
/* Dialog carries .site-frame + .site-grid in markup (the same pair
   every other 12-column layout on the site uses) rather than a
   hand-rolled 50/50 flex split — portrait/panel below are placed by
   real grid-column lines, so they land on the exact same column grid
   as everything else instead of an approximated width. .site-grid
   already collapses to display:block below 1199px on its own (see its
   base rule), which is what drops this back to the stacked mobile
   layout further down. */
.profile-modal__dialog {
  position: relative;
  height: 100%;
  padding: 85px var(--page-margin) 48px;
  box-sizing: border-box;
  /* Restrained entrance/exit — no bespoke animation system, just the
     same translateY + opacity language the rest of the site already
     avoids for CONTENT reveals (those never fade) but is appropriate
     here since a modal is a discrete open/close state, not a scroll
     reveal. Toggled by .profile-modal.is-open (js/people.js); skipped
     entirely under prefers-reduced-motion via IconMotion.reduceMotionMQ. */
  opacity: 0;
  transform: translateY(12px);
  transition: opacity 0.25s ease, transform 0.25s ease;
}
.profile-modal.is-open .profile-modal__dialog {
  opacity: 1;
  transform: translateY(0);
}
@media (prefers-reduced-motion: reduce) {
  .profile-modal__dialog { transition: none; }
}

/* grid-column: 2 / span 4 — Figma's portrait sits on columns 2-5,
   column 1 and column 6 both deliberately empty (column 1 as the
   frame's own left gutter, column 6 as the gap before the right
   content). align-self: start (not the grid default of stretch) is
   what lets aspect-ratio actually derive the portrait's height from
   its grid-column-derived width, instead of being stretched to the
   full row height first and leaving aspect-ratio nothing to resolve. */
.profile-modal__portrait {
  position: relative;
  grid-column: 2 / span 4;
  align-self: start;
  aspect-ratio: 592 / 790;
  max-height: 100%;
  border-radius: var(--radius-s);
  overflow: hidden;
  background: var(--color-grey-900);
}
.profile-modal__portrait-img { width: 100%; height: 100%; object-fit: cover; display: block; }
.profile-modal__portrait-scrim {
  position: absolute;
  inset: 0;
  background: linear-gradient(to top, rgba(0, 0, 0, 0.6) 0%, rgba(0, 0, 0, 0) 40.26%);
}

/* grid-column: 7 / span 5 — Figma's right content sits on columns
   7-11 (column 12 stays clear for the close control's own alignment).
   5 columns already lands at the same ~746px the old max-width hard-
   coded, but now as a real consequence of the grid rather than a
   number that had to be kept in sync with it by hand. */
.profile-modal__panel {
  display: flex;
  flex-direction: column;
  gap: 48px;
  grid-column: 7 / span 5;
  min-width: 0;
  height: 100%;
}
.profile-modal__top { flex: 0 0 auto; display: flex; align-items: center; }
.profile-modal__back {
  display: flex;
  align-items: center;
  gap: 21px;
  color: var(--color-icon-blue);
  font-size: 18px;
  font-weight: 600;
  line-height: 27.2px;
  letter-spacing: -0.18px;
}
.profile-modal__back-icon { transform: rotate(180deg); }
.profile-modal__back-icon.icon-arrow-direction { width: 28px; height: 28px; flex: 0 0 28px; }
.profile-modal__rule { flex: 0 0 auto; height: 1px; background: var(--color-white-20); }

/* The ONLY scrolling region on desktop — see the component comment
   above. flex: 1 1 auto + min-height: 0 is what lets a flex child
   actually shrink below its content's natural height so overflow-y
   can engage instead of the column just growing taller than its
   fixed-height ancestor. */
.profile-modal__scroll {
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  flex-direction: column;
  gap: 40px;
  overflow-y: auto;
  /* Unobtrusive scrollbar, only where it can actually appear. */
  scrollbar-width: thin;
  scrollbar-color: var(--color-white-30) transparent;
}
.profile-modal__scroll::-webkit-scrollbar { width: 6px; }
.profile-modal__scroll::-webkit-scrollbar-track { background: transparent; }
.profile-modal__scroll::-webkit-scrollbar-thumb { background: var(--color-white-30); border-radius: 3px; }

.profile-modal__heading { display: flex; flex-direction: column; gap: 8px; }
.profile-modal__name { margin: 0; }
.profile-modal__role {
  margin: 0;
  font-size: 24px;
  font-weight: 600;
  line-height: 28.8px;
  letter-spacing: -0.24px;
  color: var(--color-icon-blue);
}
.profile-modal__bio p {
  margin: 0 0 14px;
  font-size: 16px;
  font-weight: 400;
  line-height: 24.47px;
  letter-spacing: -0.16px;
  color: var(--color-white-70);
}
.profile-modal__bio p:last-child { margin-bottom: 0; }

.profile-modal__icons { display: flex; gap: 12px; }
/* .profile-modal__icons/__icon are hidden/shown per-profile via the
   plain `hidden` attribute (js/people.js) rather than a class, but an
   unconditional `display: flex` below is an AUTHOR rule — author
   styles always beat the UA stylesheet's default `[hidden] {
   display: none }` at equal specificity, so without this override
   `hidden` was doing nothing and empty icon circles rendered even with
   no data. `!important`-free: an attribute-selector variant of the
   same rule instead, applied first so the unconditional declaration
   below still wins whenever `hidden` isn't present. */
.profile-modal__icons[hidden],
.profile-modal__icon[hidden] { display: none; }
/* Shared 48px icon-blue circle — used by the close button and each
   social/contact link (LinkedIn, email), matching Figma's identical
   treatment for both. */
.profile-modal__icon,
.profile-modal__close {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 48px;
  height: 48px;
  border: 1px solid var(--color-icon-blue);
  border-radius: 50px;
  color: var(--color-icon-blue);
  transition: background 0.2s ease, color 0.2s ease;
}
.profile-modal__icon:hover,
.profile-modal__close:hover {
  background: var(--color-icon-blue);
  color: var(--color-icon-deep-blue);
}
.profile-modal__icon-glyph {
  width: 20px;
  height: 20px;
  background-color: currentColor;
  mask-repeat: no-repeat;
  -webkit-mask-repeat: no-repeat;
  mask-position: center;
  -webkit-mask-position: center;
  mask-size: contain;
  -webkit-mask-size: contain;
}
.profile-modal__icon-glyph--linkedin {
  mask-image: url("../assets/icons/icon-linkedin-glyph.svg");
  -webkit-mask-image: url("../assets/icons/icon-linkedin-glyph.svg");
}
.profile-modal__icon-glyph--email {
  mask-image: url("../assets/icons/icon-email.svg");
  -webkit-mask-image: url("../assets/icons/icon-email.svg");
}

/* Positioning only — the circle/hover look is the shared rule above
   (.profile-modal__icon, .profile-modal__close). The dialog is an
   absolutely-positioned child's containing block via its PADDING box,
   whose outer edge sits at the dialog's own border edge — not inset by
   the dialog's padding — so `right: 0` actually lands flush with the
   dialog's (viewport-wide) outer edge, ignoring var(--page-margin)
   entirely. right: var(--page-margin) is what actually lands it on the
   page-margin line every column-12 edge on the site aligns to (the
   mobile override below already does this correctly). */
.profile-modal__close {
  position: absolute;
  top: 40px;
  right: var(--page-margin);
  z-index: 2;
}
.profile-modal__close-icon { position: relative; width: 48px; height: 48px; display: block; }
.profile-modal__close-icon::before,
.profile-modal__close-icon::after {
  content: "";
  position: absolute;
  top: 50%; left: 50%;
  width: 18px; height: 1.5px;
  background: currentColor;
  transform-origin: center;
}
.profile-modal__close-icon::before { transform: translate(-50%, -50%) rotate(45deg); }
.profile-modal__close-icon::after { transform: translate(-50%, -50%) rotate(-45deg); }

/* ---------- PROFILE MODAL — RESPONSIVE ----------
   Tablet/mobile: no split-screen composition to preserve — the brief
   asks for a clean stacked adaptation instead, using the site's
   existing two-tier breakpoints. The modal becomes a single scrolling
   column (portrait, then name/role/bio/icons); nothing needs its own
   overflow behaviour any more since the whole dialog scrolls together. */
@media (max-width: 1199px) {
  .profile-modal { overflow-y: auto; height: auto; min-height: 100vh; min-height: 100dvh; }
  /* .site-grid already collapses to display: block below this
     breakpoint (see its own base rule) — a plain block container has
     no `gap`, so spacing between the now-stacked portrait/panel comes
     from margin-bottom on the portrait instead, same convention every
     other .site-grid user on the site falls back to at this
     breakpoint (e.g. .split-feature__eyebrow/__content/__media). */
  .profile-modal__dialog {
    height: auto;
    min-height: 100%;
    padding: 96px var(--page-margin) 48px;
  }
  .profile-modal__portrait {
    width: 100%;
    max-width: 480px;
    aspect-ratio: 4 / 3;
    max-height: none;
    margin-bottom: 32px;
  }
  .profile-modal__panel { max-width: 100%; height: auto; }
  .profile-modal__scroll { overflow-y: visible; flex: 0 0 auto; }
  .profile-modal__close { top: 24px; right: var(--page-margin); }
}
@media (max-width: 767px) {
  .profile-modal__dialog { padding: 88px var(--page-margin) 40px; }
  .profile-modal__portrait { aspect-ratio: 4 / 3; margin-bottom: 24px; }
  .profile-modal__panel { gap: 32px; }
}

/* =================================================================
   CAREER STORY MODAL — Build Your Career (Figma node 21210:21339)
   A SCOPED VARIANT of the Profile Modal above, not an extension of it:
   every .profile-modal* rule stays exactly as-is, and this component
   owns its own .career-modal* classes end to end. It deliberately
   copies the same structural techniques (fixed full-viewport overlay,
   site-frame + site-grid dialog, portrait pinned to columns 2-5 while
   only .career-modal__scroll on the right scrolls, the same open/close
   opacity+translateY transition, the same body-scroll-lock contract)
   because the brief's source of truth for modal behaviour is the
   Profile Modal — see js/career-story.js, which mirrors
   js/people.js's lock/unlock/focus-trap functions rather than
   inventing new ones. Content differs (a long-form career story
   instead of a bio), so the right column grows its own section/quote/
   image-figure pieces the Profile Modal never needed.

   Per this frame's own dev annotation, the "View Liam Chen" next-story
   control Figma shows top-right of the panel isn't a working link —
   that interaction isn't specified yet (identical to the Profile
   Modal's own unbuilt "View <next person>", see that component's
   header comment) — so it's left out of the markup entirely. */
.career-modal {
  position: fixed;
  inset: 0;
  z-index: 200;
  background: var(--color-icon-deep-blue);
  height: 100vh;
  height: 100dvh;
  overflow: hidden;
}
.career-modal[hidden] { display: none; }
.career-modal__dialog {
  position: relative;
  height: 100%;
  /* minmax(0, 1fr) — not the implicit `auto` .site-grid normally gets
     — is what actually lets .career-modal__scroll engage overflow
     below. With an auto row, this single-row grid's track sizes to the
     TALLEST item's own full content height (a long career story is
     taller than the dialog itself), and align-content's stretch-by-
     default then only ever grows the row, never clips it, so
     .career-modal__panel's height:100% resolves to that oversized
     content height too and nothing overflows. minmax(0, 1fr) caps the
     row at the dialog's own definite height instead, so the panel (and
     therefore its flex:1 1 auto/min-height:0 scroll child) is genuinely
     clipped to the viewport and can scroll — the fix this component
     needs since its content is longer than the viewport; left off
     .profile-modal__dialog intentionally, since that's the existing,
     unmodified component. */
  grid-template-rows: minmax(0, 1fr);
  padding: 85px var(--page-margin) 48px;
  box-sizing: border-box;
  opacity: 0;
  transform: translateY(12px);
  transition: opacity 0.25s ease, transform 0.25s ease;
}
.career-modal.is-open .career-modal__dialog {
  opacity: 1;
  transform: translateY(0);
}
@media (prefers-reduced-motion: reduce) {
  .career-modal__dialog { transition: none; }
}

/* Same grid-column placement as .profile-modal__portrait — the brief
   asks for the left portrait to "remain positioned as per the existing
   modal" while the story content on the right keeps scrolling. */
.career-modal__portrait {
  position: relative;
  grid-column: 2 / span 4;
  align-self: start;
  aspect-ratio: 592 / 790;
  max-height: 100%;
  border-radius: var(--radius-s);
  overflow: hidden;
  background: var(--color-grey-900);
}
.career-modal__portrait-img { width: 100%; height: 100%; object-fit: cover; display: block; }
.career-modal__portrait-scrim {
  position: absolute;
  inset: 0;
  background: linear-gradient(to top, rgba(0, 0, 0, 0.6) 0%, rgba(0, 0, 0, 0) 40.26%);
}

.career-modal__panel {
  display: flex;
  flex-direction: column;
  gap: 48px;
  grid-column: 7 / span 5;
  min-width: 0;
  height: 100%;
}
.career-modal__top { flex: 0 0 auto; display: flex; align-items: center; }
.career-modal__back { display: flex; align-items: center; gap: 21px; }
.career-modal__back-icon { transform: rotate(180deg); }
.career-modal__rule { flex: 0 0 auto; height: 1px; background: var(--color-white-20); }

/* The ONLY scrolling region on desktop, exactly like
   .profile-modal__scroll — left portrait never sits inside it. Figma's
   own section rhythm for this component is 48px (not the Profile
   Modal's 40px), kept as its own value rather than forced to match. */
.career-modal__scroll {
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  flex-direction: column;
  gap: 48px;
  overflow-y: auto;
  /* Breathing room between the scrollbar track and the content itself
     — without it the thumb sits flush against the text/images. */
  padding-right: 16px;
  scrollbar-width: thin;
  scrollbar-color: var(--color-white-30) transparent;
}
.career-modal__scroll::-webkit-scrollbar { width: 6px; }
.career-modal__scroll::-webkit-scrollbar-track { background: transparent; }
.career-modal__scroll::-webkit-scrollbar-thumb { background: var(--color-white-30); border-radius: 3px; }

/* ---------- Intro: eyebrow + name/role + lead paragraph + quote ----------
   Two nested gaps, not one uniform one: 28px between the eyebrow and
   everything below it, then 40px between the name/role block and the
   lead paragraph+quote — .career-modal__intro-body isolates the second
   gap so it doesn't collapse into the first. */
.career-modal__intro { display: flex; flex-direction: column; }
.career-modal__intro-eyebrow { margin: 0 0 28px; }
.career-modal__intro-body { display: flex; flex-direction: column; gap: 40px; }
.career-modal__heading { display: flex; flex-direction: column; gap: 8px; }
.career-modal__name { margin: 0; }
.career-modal__role { margin: 0; }
.career-modal__lead { display: flex; flex-direction: column; gap: 24px; }
.career-modal__lead p { margin: 0; }

/* ---------- Story sections (Starting out / Growing through project
   experience / Taking the next step) — heading + one shared body-copy
   rule reused by every paragraph instead of redeclaring it per section. ---------- */
.career-modal__section { display: flex; flex-direction: column; gap: 20px; }
.career-modal__section-heading { margin: 0; }
.career-modal__body { margin: 0; font-size: 16px; font-weight: 400; line-height: 24.47px; letter-spacing: -0.16px; color: var(--color-white-70); }

/* ---------- Wraps a .career-modal__section together with the
   .career-modal__journey block directly under it ("Growing through
   project experience" is the one section with both), matching Figma's
   own 32px gap between the two. ---------- */
.career-modal__section-group { display: flex; flex-direction: column; gap: 32px; }

/* ---------- Career journey (Graduate → Site Engineer → ...) ---------- */
.career-modal__journey { display: flex; flex-direction: column; gap: 12px; }
.career-modal__journey-detail { margin: 0; font-size: 16px; font-weight: 400; line-height: 24.47px; letter-spacing: -0.16px; color: var(--color-white); }

/* ---------- Landscape story images — 746 / 419.625 in Figma, i.e. 16:9 ---------- */
/* flex-shrink: 0 is the actual fix here, not the width rule above (kept
   anyway, it's harmless/correct). .career-modal__scroll's children all
   carry the flex column's default flex-shrink: 1; text blocks resist
   shrinking below their own content height so they never visibly
   shrink, but an aspect-ratio box has no content-driven minimum size —
   it happily shrinks to 0 to absorb the deficit once total content
   exceeds the visible area, which is exactly the long-story case this
   modal is built for. Excluding just the images from shrinking is what
   lets the excess correctly overflow .career-modal__scroll (and get
   picked up by its overflow-y: auto) instead of collapsing the images
   to nothing. */
/* margin: 0 overrides the browser's UA default <figure> margin
   (commonly 1em 40px) — without it the image reads as inset from the
   text column on both sides instead of matching its full width; same
   fix .article-image already applies for the same element. */
.career-modal__image { width: 100%; margin: 0; flex-shrink: 0; border-radius: var(--radius-s); overflow: hidden; aspect-ratio: 16 / 9; }
.career-modal__image img { width: 100%; height: 100%; object-fit: cover; display: block; }

/* ---------- Close — identical circle treatment to .profile-modal__close/__icon ---------- */
.career-modal__close {
  position: absolute;
  top: 40px;
  right: var(--page-margin);
  z-index: 2;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 48px;
  height: 48px;
  border: 1px solid var(--color-icon-blue);
  border-radius: 50px;
  color: var(--color-icon-blue);
  transition: background 0.2s ease, color 0.2s ease;
}
.career-modal__close:hover { background: var(--color-icon-blue); color: var(--color-icon-deep-blue); }
.career-modal__close-icon { position: relative; width: 48px; height: 48px; display: block; }
.career-modal__close-icon::before,
.career-modal__close-icon::after {
  content: "";
  position: absolute;
  top: 50%; left: 50%;
  width: 18px; height: 1.5px;
  background: currentColor;
  transform-origin: center;
}
.career-modal__close-icon::before { transform: translate(-50%, -50%) rotate(45deg); }
.career-modal__close-icon::after { transform: translate(-50%, -50%) rotate(-45deg); }

/* ---------- CAREER STORY MODAL — RESPONSIVE ----------
   Same stacked-column fallback as the Profile Modal's own responsive
   block: no split-screen to preserve below 1199px, so the whole dialog
   scrolls together instead of only .career-modal__scroll. */
@media (max-width: 1199px) {
  .career-modal { overflow-y: auto; height: auto; min-height: 100vh; min-height: 100dvh; }
  .career-modal__dialog {
    height: auto;
    min-height: 100%;
    padding: 96px var(--page-margin) 48px;
  }
  .career-modal__portrait {
    width: 100%;
    max-width: 480px;
    aspect-ratio: 4 / 3;
    max-height: none;
    margin-bottom: 32px;
  }
  .career-modal__panel { max-width: 100%; height: auto; }
  .career-modal__scroll { overflow-y: visible; flex: 0 0 auto; }
  .career-modal__close { top: 24px; right: var(--page-margin); }
}
@media (max-width: 767px) {
  .career-modal__dialog { padding: 88px var(--page-margin) 40px; }
  .career-modal__portrait { aspect-ratio: 4 / 3; margin-bottom: 24px; }
  .career-modal__panel { gap: 32px; }
  .career-modal__top { flex-wrap: wrap; gap: 16px; }
}

/* =================================================================
   PROJECTS — INDEX PAGE
   Reuses .page-intro (the "Projects" label is a normal breadcrumb-slot
   child of it now — see below), the shared filter-control primitive
   (.project-filter, built generically so Careers can reuse it per the
   Figma annotation on that frame), and one Project Card component with
   a --wide span modifier instead of one-off per-project markup.
   ================================================================= */

/* ---------- "Projects" breadcrumb ----------
   REFACTOR: this was previously built as a sticky eyebrow hoisted into
   a wrapper spanning Page Intro + Filters + Grid (.projects-group +
   .section__eyebrow-anchor + --eyebrow-top). That was the wrong
   primitive — Figma shows a single static label sitting in the Page
   Intro's own breadcrumb column, not a sticky one. It is now a plain,
   non-positioned child of .page-intro__row/.page-intro__crumb (the
   exact same grid slot About's breadcrumb occupies — grid-column: 1 /
   span 2, already defined on the shared .page-intro__crumb rule, no
   new CSS needed for placement). It is NOT given the .breadcrumb class
   (that class is what triggers About's sticky-under-nav behaviour via
   .page-intro__crumb .breadcrumb { position: sticky } — deliberately
   avoided here), so it scrolls away with the dark Page Intro exactly
   like the heading beside it, and the only sticky thing left on the
   page is the global nav. Visual treatment (blue dot + blue uppercase
   text) is the existing .eyebrow/.eyebrow--blue primitive, which
   carries no position rule of its own — reusing it here does not make
   this sticky, it only reuses the look. */
.page-intro__eyebrow { margin: 0; }

/* .page-intro--projects: same shared .page-intro/.page-intro__row/
   .page-intro__content classes About uses — only the bottom padding
   differs (Figma: pb-128 here vs. pb-96 on About). */
.page-intro--projects { padding-bottom: var(--space-m); }

/* ---------- Filter bar ----------
   Reusable primitive (Figma: "Projects and Careers use the same
   filter-bar and filter-control pattern... configure labels, options
   and query state per page") — .project-filter is generic (a labelled
   value + chevron + underline, opening a listbox of options), so a
   future Careers build reuses these same classes with different
   labels/options rather than a new dropdown implementation.

   Stacking context fix (REFACTOR): dropdowns were previously rendering
   under the project cards. Root cause was never z-index magnitude —
   .project-filter__menu already had z-index: 20, which should out-
   ration any z-index: auto content. The real problem is that
   .project-filters__row itself carries [data-reveal] (IconMotion
   translates it via a CSS transform), and every .project-card below
   ALSO carries [data-reveal] — a CSS transform promotes its element to
   its own stacking context, so once both are "promoted", they become
   siblings competing on DOM order (filters first, so it paints BELOW
   the later grid's promoted cards) rather than on the menu's z-index,
   which only wins comparisons *inside* whichever stacking context
   contains it. Fixing this at the .project-filter__menu level (a bigger
   z-index) can't work — the menu is trapped inside its transformed
   ancestor's context either way. The correct fix is to give the whole
   .project-filters SECTION (a plain, never-transformed element) its
   own explicit stacking context via position + a modest z-index, so
   the entire filter bar — menus included — is compared against
   .project-grid at the section level, where an explicit z-index always
   wins over auto, regardless of DOM order or what transforms happen to
   individual cards inside the grid. This is also exactly the structure
   that lets .project-filters become position: sticky later (one line,
   top: var(--nav-height) or similar) without any rework — the section
   already owns its own box/stacking/layout independent of its
   children's motion transforms. */
.project-filters {
  position: relative;
  z-index: 5;
  background: var(--color-white);
  padding: var(--space-xxs) 0;
}
/* --grey — Current Opportunities only: this page's filter bar sits on
   grey-101 instead of the component's shared white default (Projects/
   Insights keep --color-white). A background-only modifier, same
   convention as .image-list--light/--icon-dark-blue — nothing else
   about the shared filter primitive changes. */
.project-filters--grey { background: var(--color-grey-101); }
.project-filters__frame { overflow: visible; }
.project-filters__row {
  display: flex;
  /* baseline, not center: .project-filters__text/.project-filters__aside are
     themselves flex containers whose rendered height differs from a plain
     text row (dropdown control + underline vs. a single text line) — see
     .project-filters__text below for the full explanation. Aligning this
     outer row on baseline too keeps the sentence and the Keywords/Clear
     filters cluster reading as one line at wide viewports where they share
     a row. */
  align-items: baseline;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 16px 24px;
}
.project-filters__text {
  display: flex;
  /* REFACTOR: was align-items: center. Each .project-filter is a flex
     COLUMN (control text + its own underline below), so its rendered
     cross-size is taller than a plain .project-filters__phrase text node —
     centering the two against each other put the dropdown value's text a
     few px above the surrounding sentence text instead of sharing its
     baseline (Figma has the whole sentence, including selected filter
     values, reading as one clean line). baseline alignment fixes this
     correctly: a flex container's baseline is resolved recursively to its
     first in-flow child's baseline, so this lines up .project-filter's
     control text with .project-filters__phrase's own text baseline,
     regardless of the underline sitting below — no per-word margin/
     transform nudging needed. */
  align-items: baseline;
  flex-wrap: wrap;
  gap: 12px 16px;
}
/* Sentence text — stepped down from Figma's literal H5 (24px) token to
   the site's existing H6 scale (18px/600/27.2/-0.18, same values as the
   .h6 utility) at feedback that 24px read oversized in the browser
   despite matching the design file's own component token. Reuses an
   existing type scale rather than inventing a one-off size. */
.project-filters__phrase {
  font-size: 18px;
  font-weight: 600;
  line-height: 27.2px;
  letter-spacing: -0.18px;
  color: var(--color-dark-blue);
  white-space: nowrap;
}
.project-filter { position: relative; display: inline-flex; flex-direction: column; gap: 6px; }
/* Real hit-area enlargement (not just a bigger click target on paper):
   padding grows the button's own border-box (used for hover/focus
   background + hit-testing) while an equal negative margin cancels the
   extra space back out of the surrounding flex row's layout, so the
   sentence doesn't visually shift or gain gaps — the value+chevron
   still sits exactly where Figma has it, it's just easier to hit. */
.project-filter__control {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: 10px 8px;
  margin: -10px -8px;
  font-family: inherit;
  border-radius: var(--radius-xs);
  cursor: pointer;
  transition: background 0.15s ease;
}
.project-filter__control:hover { background: var(--color-grey-101); }
.project-filter__control:hover .project-filter__value { color: var(--color-dark-blue); }
.project-filter__control:focus-visible {
  outline: 2px solid var(--color-icon-blue);
  outline-offset: 2px;
  background: var(--color-grey-101);
}
.project-filter__value {
  font-size: 18px;
  font-weight: 600;
  line-height: 27.2px;
  letter-spacing: -0.18px;
  color: var(--color-grey-600);
  white-space: nowrap;
  transition: color 0.2s ease;
}
.project-filter.is-active .project-filter__value { color: var(--color-dark-blue); }
.project-filter__chevron {
  width: 10px;
  height: 10px;
  flex-shrink: 0;
  border-right: 2px solid var(--color-icon-blue);
  border-bottom: 2px solid var(--color-icon-blue);
  transform: rotate(45deg) translate(-1px, -1px);
  transition: transform 0.2s ease;
}
.project-filter.is-open .project-filter__chevron { transform: rotate(-135deg) translate(-1px, -1px); }
/* Underline sits below the (visually unchanged) control at its real
   rendered width — the negative-margin trick above only affects the
   control's own hit-box, never the flex column's allocated size, so
   this still lines up under the value/chevron exactly as in Figma. */
.project-filter__underline { height: 1px; width: 100%; background: var(--color-icon-blue); }
.project-filter__menu {
  position: absolute;
  top: calc(100% + 12px);
  left: -8px;
  width: max-content;
  min-width: 180px;
  max-width: 280px;
  max-height: 320px;
  overflow-y: auto;
  background: var(--color-white);
  border: 1px solid rgba(20, 41, 79, 0.08);
  border-radius: var(--radius-xs);
  box-shadow: 0 20px 40px rgba(20, 41, 79, 0.16);
  padding: 8px;
  display: flex;
  flex-direction: column;
  gap: 2px;
  z-index: 2;
}
.project-filter__menu[hidden] { display: none; }
/* ~44-48px clickable row height (16px option text + generous padding),
   per spec — comfortable on both mouse and touch. */
.project-filter__option {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  width: 100%;
  text-align: left;
  padding: 13px 14px;
  border-radius: var(--radius-xs);
  font-size: 16px;
  font-weight: 600;
  color: var(--color-dark-blue);
  white-space: nowrap;
  transition: background 0.15s ease;
}
.project-filter__option:hover,
.project-filter__option:focus-visible { background: var(--color-grey-101); }
/* Selected option is identifiable by colour + weight + a check mark —
   not colour alone. */
.project-filter__option[aria-selected="true"] { color: var(--color-icon-blue); background: var(--color-grey-101); }
.project-filter__option-check {
  width: 10px;
  height: 10px;
  flex-shrink: 0;
  opacity: 0;
  border-right: 2px solid var(--color-icon-blue);
  border-bottom: 2px solid var(--color-icon-blue);
  transform: rotate(45deg) translateY(-2px);
}
.project-filter__option[aria-selected="true"] .project-filter__option-check { opacity: 1; }
.project-filters__clear {
  display: inline-flex;
  flex-direction: column;
  gap: 6px;
  padding: 10px 8px;
  margin: -10px -8px;
  border-radius: var(--radius-xs);
  transition: background 0.15s ease;
}
.project-filters__clear:hover:not([aria-disabled="true"]) { background: var(--color-grey-101); }
.project-filters__clear:focus-visible {
  outline: 2px solid var(--color-icon-blue);
  outline-offset: 2px;
}
.project-filters__clear-row { display: inline-flex; align-items: center; gap: 7px; }
.project-filters__clear-label {
  font-size: 18px;
  font-weight: 600;
  line-height: 27.2px;
  letter-spacing: -0.18px;
  color: var(--color-grey-600);
}
.project-filters__clear-icon {
  display: inline-block;
  width: 13px;
  height: 13px;
  flex-shrink: 0;
  background-color: var(--color-icon-blue);
  -webkit-mask-image: url("../assets/icons/icon-reset.svg");
  mask-image: url("../assets/icons/icon-reset.svg");
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
  -webkit-mask-position: center;
  mask-position: center;
  -webkit-mask-size: contain;
  mask-size: contain;
}
.project-filters__clear-underline { height: 1px; width: 100%; background: var(--color-icon-blue); }
.project-filters__clear:disabled,
.project-filters__clear[aria-disabled="true"] { opacity: 0.4; pointer-events: none; }

/* ---------- Mobile filter trigger + panel ----------
   Below 768px the sentence composition gets replaced entirely by a
   single "Filters" control + count badge that opens a simple full-
   screen stack (no sentence, no per-field dropdowns) — see the
   responsive block for the breakpoint swap. Desktop/tablet markup
   (.project-filters__text/.project-filters__clear) and this mobile
   markup (.project-filters__mobile/.project-filters__panel) both exist
   in the DOM at all times; only one is ever visible, and both drive the
   SAME shared filter state in js/projects.js so choosing a value in
   either place stays in sync with the other. */
.project-filters__mobile { display: none; }
.project-filters__mobile-toggle {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  padding: 12px 18px;
  border: 1px solid var(--color-grey-600);
  border-radius: 1009px;
  font-size: 16px;
  font-weight: 600;
  color: var(--color-dark-blue);
  cursor: pointer;
}
.project-filters__mobile-toggle:focus-visible { outline: 2px solid var(--color-icon-blue); outline-offset: 2px; }
.project-filters__mobile-icon {
  width: 16px;
  height: 16px;
  flex-shrink: 0;
  background-color: var(--color-dark-blue);
  -webkit-mask-image: url("../assets/icons/icon-filter.svg");
  mask-image: url("../assets/icons/icon-filter.svg");
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
  -webkit-mask-position: center;
  mask-position: center;
  -webkit-mask-size: contain;
  mask-size: contain;
}
.project-filters__mobile-count {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 20px;
  height: 20px;
  padding: 0 6px;
  border-radius: 999px;
  background: var(--color-icon-blue);
  color: var(--color-white);
  font-size: 12px;
  font-weight: 600;
}
.project-filters__mobile-count[hidden] { display: none; }

/* Simple full-screen stack — no animation system, just hidden/visible
   (matches the brief: "do not introduce a complex animation system").
   Sits above the global nav (z-index: 100) while open, same convention
   the profile modal (z-index: 200) already uses for a full-screen
   overlay on this site. */
.project-filters__panel {
  position: fixed;
  inset: 0;
  z-index: 150;
  display: flex;
  flex-direction: column;
  background: var(--color-white);
  height: 100vh;
  height: 100dvh;
}
.project-filters__panel[hidden] { display: none; }
.project-filters__panel-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 20px var(--page-margin);
  border-bottom: 1px solid var(--color-grey-101);
  flex: 0 0 auto;
}
.project-filters__panel-title { font-size: 20px; font-weight: 600; color: var(--color-dark-blue); }
.project-filters__panel-close {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 40px;
  height: 40px;
  border-radius: 50%;
  color: var(--color-dark-blue);
  font-size: 24px;
  line-height: 1;
  cursor: pointer;
}
.project-filters__panel-close:hover,
.project-filters__panel-close:focus-visible { background: var(--color-grey-101); }
.project-filters__panel-body {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  padding: 8px var(--page-margin) 32px;
}
.project-filters__panel-group + .project-filters__panel-group { margin-top: 32px; }
.project-filters__panel-label {
  font-size: 14px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--color-grey-600);
  margin: 24px 0 12px;
}
.project-filters__panel-options { display: flex; flex-wrap: wrap; gap: 10px; }
.project-filters__panel-option {
  padding: 12px 18px;
  border: 1px solid var(--color-grey-101);
  background: var(--color-grey-101);
  border-radius: 1009px;
  font-size: 15px;
  font-weight: 600;
  color: var(--color-dark-blue);
  cursor: pointer;
}
.project-filters__panel-option[aria-pressed="true"] {
  background: var(--color-dark-blue);
  border-color: var(--color-dark-blue);
  color: var(--color-white);
}
.project-filters__panel-option:focus-visible { outline: 2px solid var(--color-icon-blue); outline-offset: 2px; }
.project-filters__panel-foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  padding: 16px var(--page-margin);
  border-top: 1px solid var(--color-grey-101);
  flex: 0 0 auto;
  padding-bottom: calc(16px + env(safe-area-inset-bottom, 0px));
}
.project-filters__panel-clear {
  font-size: 15px;
  font-weight: 600;
  color: var(--color-grey-600);
  text-decoration: underline;
  cursor: pointer;
}
.project-filters__panel-clear[aria-disabled="true"] { opacity: 0.4; pointer-events: none; }
.project-filters__panel-apply {
  flex: 1 1 auto;
  padding: 16px 24px;
  border-radius: 1009px;
  background: var(--color-icon-blue);
  color: var(--color-white);
  font-size: 16px;
  font-weight: 600;
  text-align: center;
  cursor: pointer;
}

/* ---------- Project grid + card ----------
   One shared card component (.project-card) with a --wide span
   modifier for the 2-column feature cards — never bespoke markup per
   project. Row order in the DOM drives the grid's auto-placement
   (three 1-col cards fill a row; a --wide card takes the remaining 2
   cols alongside a preceding 1-col card, or leads its own row), so the
   layout matches Figma without any manual grid-column bookkeeping. */
.project-grid { background: var(--color-white); padding: 0 0 var(--space-m); }
.project-grid__list {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 128px 24px;
}
/* While any filter is active, wide cards normalise to a standard
   single-column span so the remaining (possibly non-contiguous) set of
   visible cards reflows without holes or empty cells — restored to the
   designed editorial wide/standard mix the moment filters clear. No
   grid-auto-flow: dense (that would visually reorder cards out of DOM/
   reading order) — plain auto-placement in DOM order is enough once
   every visible card is a uniform span. */
.project-grid__list.is-filtered .project-card--wide { grid-column: span 1; }
.project-card { display: flex; flex-direction: column; gap: 32px; }
.project-card--wide { grid-column: span 2; }
.project-card__media {
  position: relative;
  height: 650px;
  border-radius: var(--radius-s);
  overflow: hidden;
  background: var(--color-grey-900);
}
.project-card__media img { width: 100%; height: 100%; object-fit: cover; display: block; }
.project-card__media::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(to top, rgba(0, 0, 0, 0.6) 0%, rgba(0, 0, 0, 0) 40.26%);
  pointer-events: none;
}
.project-card__content { display: flex; flex-direction: column; gap: 8px; }
.project-card__title { max-width: 100%; }
.project-card__meta {
  margin: 0;
  font-size: 16px;
  font-weight: 600;
  line-height: 24.47px;
  letter-spacing: -0.16px;
  color: var(--color-grey-600);
}
/* Hidden by the filter bar (js/projects.js toggles this, never
   display:none directly, so IconMotion.refresh() sees a clean
   before/after layout pass either way). */
.project-card[hidden] { display: none; }

.project-grid__more { display: flex; justify-content: center; margin-top: var(--space-xxs); }
.project-grid__empty {
  display: none;
  padding: var(--space-xxs) 0;
  text-align: center;
  color: var(--color-body-grey);
  font-size: 18px;
}
.project-grid__empty.is-visible { display: block; }

@media (max-width: 1199px) {
  .project-filters__row { gap: 20px; }
  .project-filters__clear { align-self: flex-start; }

  .project-grid__list { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 64px 24px; }
  .project-card__media { height: 460px; }
}

/* Sentence-style filter bar is comfortable down to 768px (tablet's
   2-column grid tier); below that the sentence wraps into an awkward,
   cramped stack, so the compact "Filters" trigger + full-screen panel
   takes over instead — see .project-filters__mobile/__panel above. */
@media (max-width: 767px) {
  .project-filters__text,
  .project-filters__clear { display: none; }
  .project-filters__mobile { display: block; }
  .project-filters__row { justify-content: flex-start; }

  .project-grid { padding-bottom: var(--space-xxs); }
  .project-grid__list { grid-template-columns: 1fr; gap: 40px; }
  .project-card--wide,
  .project-grid__list.is-filtered .project-card--wide { grid-column: span 1; }
  .project-card__media { height: 320px; }
  .project-card { gap: 24px; }
}

/* =================================================================
   INSIGHTS — ARTICLES CATEGORY PAGE
   Reuses .page-intro (dark-blue variant, same as every other page),
   the Projects/Careers .project-filter sentence-dropdown primitive
   (same classes/CSS/JS pattern, three fields instead of Projects'
   three), .project-filters__clear as-is, and the homepage Latest
   News .news-card component for every article tile. Only two things
   are genuinely new to this page: the rounded Keyword search field
   (Figma has no existing equivalent anywhere else on the site) and
   the scoped equal-height override below so all 12 image frames
   match — the homepage's own --tall/--medium variants are simply
   never used here, so cards fall back to the shared aspect-ratio
   instead of a fixed homepage height.
   ================================================================= */

/* ---------- Keyword search ----------
   New primitive (Figma: rounded pill, 1px Icon Blue border, blue
   .icon-arrow submit icon) — sits inline with the existing
   .project-filters__clear on the row's right side via
   .project-filters__aside, matching the shared "Text" flex/gap
   convention .project-filters__text already uses on the left. */
/* baseline, not center: .project-filters__clear is a flex COLUMN (label
   text + its own underline below, same shape as .project-filter), so its
   text needs the same baseline fix as .project-filters__text above to
   read on the same line as the Keywords label beside it. */
.project-filters__aside { display: inline-flex; align-items: baseline; gap: 24px; flex-wrap: wrap; }
.article-keywords { display: inline-flex; align-items: center; gap: 8px; }
.article-keywords__label {
  font-size: 18px;
  font-weight: 600;
  line-height: 27.2px;
  letter-spacing: -0.18px;
  color: var(--color-dark-blue);
  white-space: nowrap;
}
.article-keywords__field {
  display: inline-flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  width: 264px;
  border: 1px solid var(--color-icon-blue);
  border-radius: 66px;
  padding: 6px 24px;
}
.article-keywords__input {
  flex: 1 1 auto;
  min-width: 0;
  border: 0;
  background: none;
  font-family: inherit;
  font-size: 18px;
  font-weight: 600;
  line-height: 27.2px;
  letter-spacing: -0.18px;
  color: var(--color-dark-blue);
}
.article-keywords__input::placeholder { color: var(--color-grey-600); }
.article-keywords__input:focus { outline: none; }
.article-keywords__submit {
  display: inline-flex;
  flex: 0 0 auto;
  color: var(--color-icon-blue);
  cursor: pointer;
}
/* Mobile filter panel reuses the same field, full width, as one more
   .project-filters__panel-group row alongside Sector/etc. */
.project-filters__panel-group .article-keywords__field { width: 100%; }

/* ---------- Equal-size article image frames ----------
   The one deliberate visual difference from the homepage Latest News
   component (see CLAUDE.md): every image frame on this page must be
   the identical size, whereas the homepage varies --tall/--medium
   per card. Scoped to this page's grid only — .news-card__image's
   own height/--tall/--medium rules (used by index.html) are
   untouched. Figma's card is 437x296 (~1.4764:1); aspect-ratio keeps
   every frame identical at any column width instead of a fixed px
   height that would only match the desktop column count. */
.news-grid--insights .news-card__image { height: auto; aspect-ratio: 437 / 296; }

/* ---------- Row spacing ----------
   Scoped row-gap increase between the 3 article rows — column-gap (24px,
   inherited from .news-grid's own shorthand) and every other .news-grid
   rule (homepage included) are untouched. var(--space-xs) already equals
   80px, so this reuses the existing spacing scale rather than a one-off
   number. Responsive tiers fall back to the same values the shared
   .news-grid rules already use at each breakpoint (see the tablet/mobile
   overrides below), not the 80px desktop figure. */
.news-grid--insights { row-gap: var(--space-xs); }

/* ---------- Gap after the filter bar ----------
   .news-grid's shared margin-top (var(--space-xs), 80px) is tuned for
   the homepage where it follows a heading/intro block. Here it stacks
   on top of .project-filters' own vertical padding (var(--space-xxs),
   64px) below the filter row, doubling up into an oversized 144px gap
   before the first card row. Zero it out here so .project-filters' own
   padding is the only gap — tighten just this instance rather than
   touching the shared rule homepage still uses. */
.news-grid--insights { margin-top: 0; }

@media (max-width: 1199px) {
  .news-grid--insights { row-gap: var(--space-xxs); }
}

@media (max-width: 767px) {
  .news-grid--insights { row-gap: 32px; }
  .news-grid--insights .news-card__image { aspect-ratio: 4 / 3; }
}

@media (max-width: 767px) {
  .project-filters__aside { display: none; }
}

/* =================================================================
   PROJECT DETAIL — Galkangu / Bendigo GovHub
   Built entirely from existing primitives (.hero/.hero__media/
   .hero__scrim load-in convention, .fp-content__top/__meta/__bottom/
   .fp-title/.fp-meta-label carousel-slide typography, .tag--outline,
   .btn-link, .eyebrow, .h2/.h3/.h4, .site-frame/.site-grid,
   .media-hover, .hero-media__frame video chrome). Only the handful of
   layout shapes below have no existing equivalent anywhere else on the
   site (the label/value info list, the two-column statement grid, the
   asymmetric gallery composition, the 50/50 prev/next panels).
   ================================================================= */

/* ---------- Hero ----------
   Reuses .hero/.hero__media/.hero__scrim/.hero__copy/.hero__rule (see
   HERO above) for the full-bleed image + load-in convention, and the
   homepage carousel's .fp-content__top/__heading/__meta/__bottom/
   .fp-title/.fp-meta-label for the sector eyebrow, heading (white /
   70%-white second line), value/completion columns and tag+CTA row —
   this is the exact same "image hero with project facts" shape the
   Featured Projects slide already established, just stationary instead
   of auto-cycling. Nothing new needed except letting the eyebrow here
   render without its usual dot (Figma omits it for this instance). */
.project-hero__eyebrow { gap: 0; }
/* .hero__bg-image/.hero__overlay-image/.hero__scrim's default values
   are tuned for the homepage's own hero (a dim blurred-placeholder base
   image under a separate overlay photo, faded only at the very bottom).
   A project hero has no such placeholder — .hero__bg-image is just a
   near-blank loading swatch — so left at the shared defaults the single
   real photo (.hero__overlay-image) renders lighter/flatter than the
   same photo does as a Featured Projects slide. Override to match
   .fp-slide__image/.fp-slide__scrim exactly: hide the placeholder layer,
   dim the photo itself to the same 0.6 opacity, and reuse the same
   bottom-heavy scrim gradient. */
.project-hero .hero__bg-image { opacity: 0; }
.project-hero .hero__overlay-image { opacity: 0.6; }
/* The shared markup order is bg-image -> scrim -> overlay-image, so by
   default the scrim paints UNDER the overlay photo and its gradient is
   never actually visible. Give it a z-index so it stacks on top of the
   photo instead, the way a scrim needs to. */
.project-hero .hero__scrim { z-index: 1; background: linear-gradient(to top, rgba(0, 0, 0, 0.6) 0%, rgba(0, 0, 0, 0) 40.26%); }

/* News/Insights article template's own hero instance: .fp-content__heading
   is otherwise an unconstrained flex child (see .fp-content__top,
   justify-content: space-between), so it grows to fill whatever space
   .fp-content__meta's fixed 284px rail leaves — ~10 columns, wider than
   Figma's article heading, which wraps at 8 columns (1208px). Scoped to
   this modifier (not the shared .fp-content__heading rule) so Project
   Detail's own hero instance is untouched. min(px, vw) matches the
   col-width mapping convention used everywhere else (1208/1920 = 62.92vw). */
.hero--article .fp-content__heading { max-width: min(1208px, 62.92vw); }

/* ---------- Overview ---------- */
.project-overview { padding: var(--space-s) 0 var(--space-m); background: var(--color-white); }
.project-overview__frame { align-items: start; }
.project-overview__data { grid-column: 1 / span 5; display: flex; flex-direction: column; gap: var(--space-xxs); }
.project-overview__statement { grid-column: 8 / span 5; display: flex; flex-direction: column; gap: 48px; justify-content: center; }

/* Label/value fact list — shared shape for Overview's project data AND
   Project Recognition's rows below (same 284px uppercase label column
   feeding a dark-blue value), so it's one component, not two. */
.project-info { display: flex; flex-direction: column; gap: 24px; width: 100%; }
.info-row { display: flex; gap: 24px; align-items: baseline; flex-wrap: wrap; }
.info-row__label {
  width: 284px;
  flex-shrink: 0;
  margin: 0;
  font-size: 16px;
  font-weight: 600;
  line-height: 27.2px;
  letter-spacing: -0.18px;
  text-transform: uppercase;
  color: var(--color-mid-grey);
}
.info-row__value {
  margin: 0;
  font-size: 18px;
  font-weight: 400;
  line-height: 27.53px;
  letter-spacing: -0.18px;
  color: var(--color-dark-blue);
}

/* Shared 32px-gap copy block for the two "Section statement" moments
   (Overview's right column, Split Content + Media's left column) —
   distinct from the sitewide 18px-gap `.section__copy`/`.split-feature
   __copy` pattern because Figma consistently authors this specific
   component at 32px, not 18px. */
.project-copy { display: flex; flex-direction: column; gap: 32px; width: 100%; max-width: 630px; }
.project-copy p { margin: 0; font-size: 18px; font-weight: 400; line-height: 27.53px; letter-spacing: -0.18px; color: var(--color-body-grey); }

/* ---------- Feature Media ----------
   Structurally a video slot (Figma's frame is an empty placeholder with
   a hidden play-glyph group) — reuses the existing large-media chrome
   (.hero-media__inner/.hero-media__frame + video-centring rule) already
   established on About, minus that component's own dark/white split
   background, which doesn't apply here. */
.project-feature-media { background: var(--color-white); }

/* ---------- Split Content + Media ---------- */
.project-split { padding: 128px 0 var(--space-s); background: var(--color-white); }
.project-split__frame { align-items: center; }
.project-split__content { grid-column: 1 / span 4; display: flex; flex-direction: column; gap: 32px; }
.project-split__media { grid-column: 7 / span 6; border-radius: var(--radius-s); overflow: hidden; }
.project-split__media img { width: 100%; height: 100%; object-fit: cover; display: block; }

/* ---------- Gallery + Story ---------- */
.project-gallery { background: var(--color-white); padding-bottom: var(--space-l); }
.project-gallery__row { display: flex; flex-direction: column; gap: var(--space-s); }
.project-gallery__inset { border-radius: var(--radius-s); overflow: hidden; height: 768px; }
.project-gallery__inset img { width: 100%; height: 100%; object-fit: cover; display: block; }
.project-gallery__pair { display: flex; gap: 24px; }
.project-gallery__pair .project-gallery__inset { flex: 1 1 0; height: 503px; }
.project-gallery__full { height: 768px; }
.project-gallery__full img { width: 100%; height: 100%; object-fit: cover; display: block; }

/* Centred client quote — its own intentional single reveal block, not
   part of the surrounding image cascade. */
.project-quote { padding: var(--space-m) var(--page-margin); }
.project-quote__inner { display: flex; flex-direction: column; align-items: center; gap: 52px; max-width: 1365px; margin-inline: auto; text-align: center; }
.project-quote__text { margin: 0; }
.project-quote__cite { margin: 0; font-style: normal; font-size: 18px; font-weight: 600; line-height: 27.2px; letter-spacing: -0.18px; color: var(--color-mid-grey); }

/* Asymmetric composition: two stacked images on the left, one tall
   image spanning their combined height on the right. The stack's own
   two aspect-ratio'd images are what should set the row's height; the
   tall image is meant to simply crop to match, not dictate its own
   height. Left as `img { height: 100% }` inside an auto-height parent,
   a percentage height resolves to auto — so the browser fell back to
   the tall image's OWN (portrait, much taller-than-wide) natural aspect
   ratio to size it, which then stretched the whole row past the
   stack's real content height, leaving empty space under the stack's
   two images. Taking the image out of flow (absolute, inset: 0) means
   it no longer contributes any intrinsic height of its own: the row's
   height comes purely from the stack, and the tall image just fills
   whatever that resolves to via object-fit: cover. */
.project-gallery__asym { display: flex; gap: 24px; }
.project-gallery__asym-stack { display: flex; flex-direction: column; gap: 24px; flex: 1 1 0; min-width: 0; }
.project-gallery__asym-stack .project-gallery__inset { height: auto; aspect-ratio: 900 / 515; }
.project-gallery__asym-tall { position: relative; flex: 1 1 0; min-width: 0; border-radius: var(--radius-s); overflow: hidden; }
.project-gallery__asym-tall img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; display: block; }

/* ---------- Project Recognition ----------
   Same label/value fact list as Overview, but each row also carries a
   second (narrow, date-shaped) value and a trailing CTA — so unlike
   Overview's single fixed-width value, the primary value here flexes to
   fill whatever room is left after the label, the short date column and
   the right-aligned CTA claim theirs. Fixed 284px columns (Overview's
   shape) don't leave enough room for all four pieces below ~1650px, so
   this row earns its own sizing rather than inheriting Overview's. */
.project-recognition { display: flex; gap: min(332px, 8vw); padding: 32px var(--page-margin); }
.project-recognition__eyebrow { flex-shrink: 0; }
.project-recognition__list { flex: 1 1 0; min-width: 0; display: flex; flex-direction: column; gap: 24px; }
.project-recognition .info-row { justify-content: flex-start; flex-wrap: nowrap; }
.project-recognition .info-row__label { width: min(200px, 14vw); }
.project-recognition .info-row__value--primary { flex: 1 1 auto; min-width: 0; }
.project-recognition .info-row__value--secondary { flex: 0 0 auto; }
.project-recognition .info-row + .project-recognition__rule { margin: 0; }
.project-recognition__rule { height: 1px; background: var(--color-light-grey); border: 0; margin: 0; width: 100%; }
.project-recognition .info-row__cta { margin-left: auto; flex-shrink: 0; }

/* ---------- Previous / Next Project ---------- */
.project-pagination { display: flex; }
.project-pagination__panel {
  position: relative;
  flex: 1 1 50%;
  min-width: 0;
  min-height: 700px;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  padding: 72px var(--page-margin);
  overflow: hidden;
  background: var(--color-grey-900);
}
.project-pagination__media { position: absolute; inset: 0; overflow: hidden; }
.project-pagination__media img { width: 100%; height: 100%; object-fit: cover; opacity: 0.4; }
.project-pagination__scrim { position: absolute; inset: 0; background: linear-gradient(to top, rgba(0, 0, 0, 0.6) 0%, rgba(0, 0, 0, 0) 40.26%); }
.project-pagination__content { position: relative; z-index: 1; display: flex; flex-direction: column; gap: 24px; }
.project-pagination__heading { display: flex; flex-direction: column; gap: 10px; }
.project-pagination__rule { height: 1px; border: 0; margin: 0; width: 100%; background: rgba(255, 255, 255, 0.2); }
.project-pagination__bottom { display: flex; align-items: flex-end; justify-content: space-between; gap: 24px; }
.project-pagination__bottom .fp-tags { flex: 1 1 auto; min-width: 0; }
.project-pagination__bottom .btn-link { flex: 0 0 auto; align-self: flex-end; }
.project-pagination__cta .icon-arrow-direction { transform: rotate(180deg); }

@media (max-width: 1199px) {
  .project-overview__data,
  .project-overview__statement,
  .project-split__content,
  .project-split__media { grid-column: auto; width: 100%; }
  .project-overview__frame { display: flex; flex-direction: column; gap: var(--space-xxs); }
  .project-split__frame { display: flex; flex-direction: column; gap: 40px; }
  .info-row__label { width: 200px; }
  .project-recognition { gap: 64px; flex-direction: column; }
  .project-gallery__row { gap: var(--space-xxs); }
  .project-gallery__inset,
  .project-gallery__full { height: 55vw; }
  .project-quote__text.h3 { font-size: 32px; line-height: 44px; }
}

@media (max-width: 767px) {
  .project-hero .fp-title { font-size: clamp(32px, 9vw, 44px); line-height: 1.1; }
  .project-hero .fp-content__top { flex-direction: column; align-items: flex-start; gap: 24px; }
  .project-hero .fp-content__meta { flex-direction: row; width: 100%; gap: 40px; }
  .project-hero .fp-content__bottom { flex-wrap: wrap; gap: 20px; align-items: flex-start; }
  .project-hero .fp-content__cta { width: auto; }

  .project-overview,
  .project-split { padding-top: var(--space-xxs); padding-bottom: var(--space-xxs); }
  .info-row { flex-direction: column; gap: 8px; }
  .info-row__label { width: auto; }

  .project-gallery { padding-bottom: var(--space-xxs); }
  .project-gallery__row { gap: 24px; }
  .project-gallery__pair,
  .project-gallery__asym { flex-direction: column; }
  .project-gallery__pair .project-gallery__inset,
  .project-gallery__inset,
  .project-gallery__full { height: 60vw; }
  .project-gallery__asym-stack .project-gallery__inset { aspect-ratio: 4 / 3; }
  .project-gallery__asym-tall { aspect-ratio: 4 / 3; }

  .project-quote { padding: var(--space-xxs) var(--page-margin); }
  .project-quote__inner { gap: 32px; }

  .project-recognition { padding: 24px var(--page-margin); gap: 40px; }
  .project-recognition .info-row { flex-direction: row; flex-wrap: wrap; }

  .project-pagination { flex-direction: column; }
  .project-pagination__panel { min-height: 420px; }
}

/* =================================================================
   TABBED CONTENT — genuinely new shared component. Figma explicitly
   annotates this "REUSABLE SECTION / TABBED CONTENT — use one shared
   tabbed-content section for this family... do not build separate tab
   systems for Sustainability and Reconciliation", so it's built once
   here for Reconciliation to reuse verbatim later. Typography reuses
   existing utilities throughout (.eyebrow, .h2, .body-lg); only the
   tab list / panel layout below is new. Interaction (single active
   tab, no layout shift) lives in js/about.js (initTabbedContent),
   guarded by [data-tabbed-content].
   ================================================================= */
.tabbed-content { background: var(--color-black); padding: var(--space-l) 0 var(--space-m); }
/* --dark-blue: Build Your Career's "Graduate & cadet program" instance
   sits on Icon dark blue rather than the component's flat-black
   default — same minimal single-property override convention as
   .split-feature--black. */
.tabbed-content--dark-blue { background: var(--color-dark-blue); }
/* .tabbed-content__frame carries .site-frame in markup (centres/caps
   the whole section at 1920px) and hosts the sticky-eyebrow anchor —
   same "whole frame is the anchor's containing block" convention as
   .image-list__frame: height: 100% now spans __head AND __body
   together, so the eyebrow sticks through the heading/copy, the tabs
   and the panels, releasing only at the section's own end rather than
   just the (short) head row. */
.tabbed-content__frame { position: relative; }
.tabbed-content__frame > .section__eyebrow-anchor { left: var(--page-margin); }
.tabbed-content__head { align-items: flex-start; margin-bottom: 96px; }
.tabbed-content__intro { grid-column: 3 / span 10; display: flex; flex-direction: column; gap: 48px; max-width: 1208px; }
/* .tabbed-content__head/__body carry .site-grid in markup (the outer
   .tabbed-content__frame already supplies .site-frame centring) —
   tabs and panels both sit in cols 3–12, same content column every
   other section on the site places its body copy in. */
.tabbed-content__tabs {
  grid-column: 3 / -1;
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 48px;
}
/* Each tab owns its OWN top rule (not one continuous border on the
   .tabbed-content__tabs container) — a shared container border would
   draw straight through the 48px gaps between tabs as one unbroken
   line; Figma shows four separate segments, one per tab's own width.
   display: flex + align-items/justify-content: flex-start is a
   deliberate reset of the browser default button-content centering
   (buttons keep native internal alignment even once this grid's
   align-items: stretch makes them taller than their own text) — text
   must stay pinned to the top, under the rule, never vertically
   centred in the stretched cell. */
.tabbed-content__tab {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  justify-content: flex-start;
  text-align: left;
  font-size: 28px;
  font-weight: 600;
  line-height: 38px;
  letter-spacing: -0.5px;
  color: var(--color-white-70);
  transition: color 0.3s ease;
}
/* Rule + fill reuses the exact .about-menu__rule/__rule-fill pattern
   (see above) so the blue left->right sweep is identical, timing and
   all — a light static base line the fill animates over, not a
   border-top swap (a border can't be scrubbed/animated this way).
   Duration/easing (0.45s cubic-bezier(0.4, 0, 0.2, 1)) and the heading
   colour transition (0.3s ease) are copied verbatim from
   .about-menu__rule-fill / .about-menu__item-title — do not give the
   tabs their own faster timing, even though these tabs are wider than
   an About-menu column, or the sweep reads as snappier than the menu
   at the same duration. */
.tabbed-content__tab-rule {
  position: relative;
  width: 100%;
  height: 1px;
  background: var(--color-white-20);
}
.tabbed-content__tab-rule-fill {
  position: absolute;
  top: 0; left: 0;
  height: 1px;
  width: 100%;
  background: var(--color-icon-blue);
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform 0.45s cubic-bezier(0.4, 0, 0.2, 1);
}
/* align-items: flex-start (not the About menu's `center`) — these
   headings are 32px/48px line-height, tall enough that centring the
   arrow against the row reads as vertically-centred on the tab rather
   than pinned to the heading's own top edge like the About menu's
   20px title does. Flush to the top instead, with a small nudge so it
   optically lines up with the cap-height of the first line of text. */
.tabbed-content__tab-top {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 12px;
  width: 100%;
  padding-top: 24px;
}
.tabbed-content__tab-arrow {
  /* Scaled up from the About menu's 11px default to stay proportionate
     to this component's much larger (32px vs 20px) heading. */
  width: 18px;
  height: 18px;
  flex: 0 0 18px;
  margin-top: 6px;
  color: var(--color-icon-blue);
  opacity: 0;
  transition: opacity 0.3s ease;
}
.tabbed-content__tab:not(.is-active):hover,
.tabbed-content__tab:not(.is-active):focus-visible { color: var(--color-icon-blue); }
.tabbed-content__tab:not(.is-active):hover .tabbed-content__tab-rule-fill,
.tabbed-content__tab:not(.is-active):focus-visible .tabbed-content__tab-rule-fill { transform: scaleX(1); }
.tabbed-content__tab:not(.is-active):hover .tabbed-content__tab-arrow,
.tabbed-content__tab:not(.is-active):focus-visible .tabbed-content__tab-arrow { opacity: 1; }
.tabbed-content__tab.is-active { color: var(--color-icon-blue); }
.tabbed-content__tab.is-active .tabbed-content__tab-rule-fill { transform: scaleX(1); transition: none; }
.tabbed-content__panels { grid-column: 5 / -1; margin-top: 96px; }
/* display: none/flex here (not reliance on the [hidden] attribute alone)
   because author-stylesheet specificity always beats the UA
   stylesheet's [hidden] rule — an explicit .is-active toggle is what
   actually keeps only one panel in flow at a time. */
.tabbed-content__panel { display: none; gap: min(178px, calc((100vw - 96px) * 0.09759)); align-items: flex-start; }
.tabbed-content__panel.is-active { display: flex; }
.tabbed-content__panel-copy { flex: 1 1 0; min-width: 0; display: flex; flex-direction: column; gap: 24px; }
.tabbed-content__panel-heading { margin: 0; font-size: 24px; font-weight: 600; line-height: 28.8px; letter-spacing: -0.24px; color: var(--color-icon-blue); }
.tabbed-content__panel-body { display: flex; flex-direction: column; gap: 18px; }
.tabbed-content__panel-body p { margin: 0; font-size: 18px; font-weight: 400; line-height: 27.53px; letter-spacing: -0.18px; color: var(--color-white); }
.tabbed-content__panel-media { width: min(592px, calc((100vw - 96px) * 0.32456)); flex-shrink: 0; border-radius: var(--radius-s); overflow: hidden; }
.tabbed-content__panel-media img { width: 100%; height: min(366px, 24.8vw); object-fit: cover; display: block; }

/* --five-tabs variant (Reconciliation's "Creating opportunity" instance
   only — Sustainability's Social Impact instance keeps the base 4-tab
   layout entirely unchanged; nothing below touches an unqualified
   .tabbed-content__* selector). Two structural changes, both scoped to
   this modifier:

   1. Sticky eyebrow constrained to the header. The base component's
      anchor is a child of .tabbed-content__frame, so its height: 100%
      (see .section__eyebrow-anchor) spans __head AND __body together —
      correct for the 4-tab layout, where the eyebrow should track the
      tabs/panels too. Here the anchor instead nests INSIDE
      .tabbed-content__head (see the HTML) — .tabbed-content__frame's
      own position: relative no longer matters to it, since __head
      becomes ITS containing block instead, so height: 100% now
      resolves against the header's own height only. It sticks through
      the header and releases at the header's own bottom edge, never
      trailing down beside the tab row — a structural fix (which
      element the anchor's height comes from), not an offset or JS
      scroll calculation.
   2. Full-width 5-column tab row. The base .tabbed-content__tabs sits
      at grid-column: 3 / -1 with 4 columns, matching Figma's 4-tab
      instance. This variant instead spans every column (1 / -1, the
      full site-grid width Figma's 5-tab instance uses) with 5 equal
      columns — same 48px gap as the base rule (already the exact value
      Figma's own 326px-wide tab columns resolve to across 1824px/5
      columns, so it's left inherited rather than redeclared). Content
      panels below are NOT part of this override — .tabbed-content__panels
      keeps the base component's normal grid-column: 5/-1 placement, so
      the active tab's content stays the existing indented layout, not
      full width. */
.tabbed-content--five-tabs .tabbed-content__head { position: relative; }
.tabbed-content--five-tabs .tabbed-content__head > .section__eyebrow-anchor { left: var(--page-margin); }
.tabbed-content--five-tabs .tabbed-content__tabs { grid-column: 1 / -1; grid-template-columns: repeat(5, 1fr); }

@media (max-width: 1199px) {
  .tabbed-content__head { display: flex; flex-direction: column; gap: 24px; margin-bottom: 64px; }
  .tabbed-content__intro { grid-column: auto; max-width: none; }
  .tabbed-content__tabs { grid-template-columns: repeat(2, 1fr); gap: 24px 40px; }
  .tabbed-content__panel { flex-direction: column; gap: 32px; }
  .tabbed-content__panel-media { width: 100%; }
}
@media (max-width: 767px) {
  .tabbed-content { padding: var(--space-xs) 0 var(--space-xxs); }
  .tabbed-content__tabs { grid-template-columns: 1fr; gap: 24px; }
  .tabbed-content__tab { font-size: 24px; line-height: 32px; }
  .tabbed-content__panel-media img { height: 240px; }
}

/* =================================================================
   ARTICLE CONTENT LAYOUT — reusable News/Insights article body
   Sits directly below the article hero (see .hero.project-hero,
   reused verbatim from Project Detail, on the article template).

   .article-layout__frame is .site-frame.site-grid with the sidebar and
   content placed on EXPLICIT grid lines rather than a hand-tuned gap:
   .article-sidebar takes cols 1-3 (3 x 130px cols + 2 x 24px gaps =
   438px) and .article-content takes cols 5-12 (8 cols = 1208px),
   leaving col 4 empty on purpose — that empty track (130px) plus the
   24px gutter on either side of it reproduces the designed 178px gap
   exactly, same explicit-grid-column technique the rest of the site
   uses instead of faking a gap with margins (see the col-width mapping
   table above .site-frame).
   ================================================================= */
.article-layout { background: var(--color-white); padding: var(--space-m) 0 var(--space-l); }
.article-layout__frame { align-items: flex-start; }

/* ---------- Sidebar: contents/share panel, sticky as ONE unit ---------- */
/* Reuses .eyebrow--blue, .h6/.h6--dark and the .project-recognition__rule
   light-grey-divider convention outright rather than declaring new type.
   position: sticky (no anchor overlay needed, unlike the homepage's
   sticky eyebrows) because the sidebar already owns its own real grid
   column and reserves its own space — same technique as
   .eyebrow--sticky. Its containing block is the implicit grid row
   shared with .article-content, so it releases naturally at the row's
   bottom edge (the end of the article) without any extra markup. */
.article-sidebar {
  grid-column: 1 / span 3;
  display: flex;
  flex-direction: column;
  gap: 32px;
  position: sticky;
  top: var(--eyebrow-top, 128px);
}
.article-sidebar__rule,
.article-content__rule {
  height: 1px;
  border: 0;
  margin: 0;
  width: 100%;
  background: var(--color-light-grey);
}
.article-sidebar__title { margin: 0; }

.article-toc { display: flex; flex-direction: column; gap: 16px; }
.article-toc__link {
  display: flex;
  align-items: flex-start;
  gap: 16px;
  font-size: 16px;
  font-weight: 400;
  line-height: 24.5px;
  color: var(--color-body-grey);
  transition: color 0.2s ease;
}
.article-toc__link:hover { color: var(--color-dark-blue); }
.article-toc__num { flex: 0 0 20px; }
.article-toc__label { flex: 1 1 auto; }

.article-share { display: flex; align-items: center; gap: 24px; }
.article-share__label {
  margin: 0;
  font-size: 18px;
  font-weight: 600;
  line-height: 27.2px;
  letter-spacing: -0.18px;
  color: var(--color-icon-blue);
}
.article-share__list { display: flex; align-items: center; gap: 12px; }
.article-share__link {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 24px;
  height: 24px;
  flex-shrink: 0;
  border-radius: 50%;
  background: var(--color-icon-blue);
  transition: opacity 0.2s ease;
}
.article-share__link:hover { opacity: 0.75; }
/* Recolours the existing social glyphs via mask (same technique as
   .icon-arrow) instead of new icon files with a hardcoded fill — the
   mask reads only the source SVG's alpha shape, so the button's own
   background-color: white applies regardless of that file's own fill.
   icon-facebook.svg is the one glyph this set didn't already have
   (footer only ever needed Instagram + LinkedIn); added in the same
   14x14 single-path style as its siblings, no new icon library. */
.article-share__icon {
  display: block;
  width: 12px;
  height: 12px;
  background-color: var(--color-white);
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
  -webkit-mask-position: center;
  mask-position: center;
  -webkit-mask-size: contain;
  mask-size: contain;
}
.article-share__icon--linkedin {
  -webkit-mask-image: url("../assets/icons/icon-linkedin.svg");
  mask-image: url("../assets/icons/icon-linkedin.svg");
}
.article-share__icon--facebook {
  -webkit-mask-image: url("../assets/icons/icon-facebook.svg");
  mask-image: url("../assets/icons/icon-facebook.svg");
}
.article-share__icon--instagram {
  -webkit-mask-image: url("../assets/icons/icon-instagram.svg");
  mask-image: url("../assets/icons/icon-instagram.svg");
}

/* ---------- Content: editorial flow ---------- */
.article-content { grid-column: 5 / span 8; min-width: 0; display: flex; flex-direction: column; gap: 48px; }

.article-lead {
  margin: 0;
  font-size: 28px;
  font-weight: 600;
  line-height: 44px;
  letter-spacing: -0.5px;
  color: var(--color-dark-blue);
}

/* Same explicit height + object-fit: cover crop as .project-gallery__full
   (Project Detail's own full-width article image), just re-scoped here
   so a future article's image count isn't tied to that component. */
.article-image { margin: 0; border-radius: var(--radius-s); overflow: hidden; }
.article-image img { width: 100%; height: 680px; object-fit: cover; display: block; }

/* Video embed slot (first article-image position, currently a Vimeo
   iframe): same 8px-radius/full-width treatment as .article-image, but
   sized with aspect-ratio rather than a fixed height so the iframe (no
   intrinsic size of its own) fills the container edge-to-edge at every
   width instead of needing a separate breakpoint override — 1208/680
   is the exact same ratio .article-image's own fixed 680px desktop
   height resolves to at the 1208px content width, so the two sit at
   identical sizes on desktop. */
.article-video { margin: 0; border-radius: var(--radius-s); overflow: hidden; position: relative; aspect-ratio: 1208 / 680; }
.article-video iframe { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; display: block; }

.article-heading-group { display: flex; flex-direction: column; gap: 24px; }
/* TOC anchor targets only (the 4 headings the sidebar contents list
   links to) — the site-wide `[id] { scroll-margin-top: var(--nav-height); }`
   rule (see the reset section near the top of this file) clears the
   fixed nav for every id on the site, but landing a heading flush
   against that exact offset reads as jammed under the nav for a
   deliberate contents-list jump. The extra 48px added on top of the
   nav clearance is .article-content's own real block-to-block gap (see
   `.article-content { gap: 48px }`) — not an arbitrary pad — so a TOC
   jump lands with the previous block's bottom edge at the nav's own
   bottom edge and the heading keeps its normal 48px rhythm below that,
   reading as a natural continuation of the article's vertical spacing
   rather than a new offset invented just for anchors. Scoped to these
   4 anchors only; every other `[id]` on the site keeps the shared rule. */
.article-toc__target { scroll-margin-top: calc(var(--nav-height) + 48px); }

/* Headings reuse .h4/.h4--dark (32px/600/48px/-0.5px) verbatim — the
   "Thank you to project partners" heading swaps in .text-blue instead
   of a new colour modifier. Body copy reuses .body-lg/.body-lg--grey
   (18px/400/27.53px/-0.18px) verbatim; --bold is the one new modifier
   needed, for the bold partners paragraph. */
.body-lg--bold { font-weight: 700; }

.article-quote__text { font-style: italic; }

.article-back {
  display: inline-flex;
  align-items: center;
  gap: 12px;
  align-self: flex-start;
  font-size: 18px;
  font-weight: 600;
  line-height: 27.53px;
  letter-spacing: -0.18px;
  color: var(--color-mid-grey);
  transition: opacity 0.2s ease;
}
.article-back:hover { opacity: 0.75; }
.article-back__icon { transform: rotate(180deg); }

@media (max-width: 1199px) {
  /* .site-grid itself collapses to display: block below this
     breakpoint (see the shared rule above .site-frame), so the
     sidebar/content gap has to come from a margin here, not the
     grid's own column-gap. DOM order already reads sidebar → content,
     so the sidebar lands above the article body with no markup
     change. */
  .article-sidebar {
    grid-column: auto;
    position: static;
    top: auto;
    margin-bottom: 48px;
  }
  .article-content { grid-column: auto; }
  .article-image img { height: auto; aspect-ratio: 1208 / 680; }
}
@media (max-width: 767px) {
  .article-layout { padding: var(--space-xxs) 0; }
  .article-sidebar { gap: 24px; margin-bottom: 40px; }
  .article-content { gap: 32px; }
  .article-lead { font-size: 22px; line-height: 33.65px; letter-spacing: -0.22px; }
  .article-heading-group .h4 { font-size: 24px; line-height: 32px; }
}

/* =================================================================
   RELATED INSIGHTS — homepage Latest News heading row + .news-card,
   reused verbatim on the article template (see the HTML comment
   above this section for why it can't just reuse .news-grid--insights
   wholesale). The only new rule this section needs is the equal-
   height image treatment; heading row, eyebrow, CTA and card styling
   all come from the shared .section__intro--news/.news-grid/.news-card
   rules above.
   ================================================================= */
.related-insights .news-card__image { height: auto; aspect-ratio: 437 / 296; }
@media (max-width: 767px) {
  .related-insights .news-card__image { aspect-ratio: 4 / 3; }
}

/* =================================================================
   OPPORTUNITY RESULTS — Current Opportunities (Figma node 20997:7268)
   Light-grey section (.section--light) below the filter bar. Same
   two-column shell math as Image + List (592px media / 178px gutter
   = 1 grid column + 2 gaps) but built directly on .site-grid with
   explicit grid-column placement instead of flex, since the gutter
   IS a real grid column here, not a flex gap value borrowed from it.

   One reusable .opportunity-card component (head row: blue .h5 title
   + .body-lg description, then a divider and a tags/CTA row) repeated
   per role — never seven bespoke card layouts, per the Figma
   annotation on this frame. Tags reuse .tag with a new --muted
   variant (light-grey border/mid-grey text, for a white card on a
   light-grey ground — .tag's own --outline/default pairs are tuned
   for dark grounds). The per-card CTA reuses .btn-link with the new
   --xs size modifier (Figma's "Button Small"); the section's own
   closing "View more insights" CTA reuses .btn-link at its own
   default (Figma's "Button Large") — no modifier needed, identical to
   Insights' own instance.

   Motion: each card/media is tall enough, and the list long enough,
   that a single [data-scroll-section] cascade sized off this
   wrapper's own top would resolve the last few cards before they're
   ever on screen (see js/motion.js's scoping rule). Content-reveal
   scopes every [data-reveal] to its OWN top edge instead, exactly
   like a tall standalone list should. */
/* No extra top padding: .section--light's own var(--space-l) top
   would stack on top of .project-filters' own bottom padding
   (var(--space-xxs)) above it, doubling up the gap — same convention
   as .project-grid (padding: 0 0 var(--space-m)) sitting directly
   under the same filter bar on Projects. */
.opportunity-results { padding-top: 0; }
.opportunity-results__frame { align-items: start; }
.opportunity-results__media {
  grid-column: 1 / span 4;
  height: 366px;
  border-radius: var(--radius-s);
  overflow: hidden;
  /* Stays in view alongside the (much taller) results list as the
     page scrolls, same --eyebrow-top offset every other sticky
     element on the site clears the fixed nav by. Reset to static
     once the columns stack below 1199px. */
  position: sticky;
  top: var(--eyebrow-top, 128px);
}
.opportunity-results__media img { width: 100%; height: 100%; object-fit: cover; display: block; }
.opportunity-results__list { grid-column: 6 / span 7; display: flex; flex-direction: column; gap: 12px; }

.opportunity-card {
  display: flex;
  flex-direction: column;
  gap: 40px;
  width: 100%;
  padding: 32px;
  background: var(--color-white);
  border-radius: var(--radius-s);
}
.opportunity-card__head { display: flex; gap: 24px; }
.opportunity-card__title { width: 283px; flex-shrink: 0; }
.opportunity-card__desc { flex: 1; min-width: 0; }
.opportunity-card__foot { display: flex; flex-direction: column; gap: 32px; }
.opportunity-card__divider { height: 1px; width: 100%; background: var(--color-light-grey); }
.opportunity-card__row { display: flex; align-items: flex-start; justify-content: space-between; gap: 24px; }
.opportunity-card__tags { display: flex; flex-wrap: wrap; gap: 10px; }
/* Tags wrap onto a second line first (flex-wrap above) rather than
   the CTA itself breaking across lines when the row gets tight below
   1920px — nowrap + flex-shrink: 0 keeps "View position" a single,
   never-squeezed line at every desktop width. */
.opportunity-card__cta { flex-shrink: 0; white-space: nowrap; }

.opportunity-results__more { display: flex; justify-content: center; padding: var(--space-s) 0; }

@media (max-width: 1199px) {
  .opportunity-results__media { grid-column: auto; width: 100%; margin-bottom: 32px; position: static; }
  .opportunity-results__list { grid-column: auto; }
}
@media (max-width: 767px) {
  .opportunity-results__media { height: auto; aspect-ratio: 4 / 3; }
  .opportunity-card { padding: 24px; gap: 24px; }
  .opportunity-card__head { flex-direction: column; gap: 12px; }
  .opportunity-card__title { width: auto; }
  .opportunity-card__row { flex-direction: column; gap: 20px; }
}
@media (max-width: 479px) {
  .opportunity-card { padding: 20px; }
  .opportunity-card__tags { gap: 8px; }
}

/* =================================================================
   JOB DETAIL — reusable job/role detail template (Figma node 21242:21760)
   New page family for a single opportunity (e.g. job-contract-
   administrator.html), linked from an .opportunity-card on Current
   Opportunities. Reuses existing primitives throughout — .page-intro
   (dark-blue variant, same as every other page), .breadcrumb, .h1/.h2/
   .h4/.h6, .tag, .btn-link, .eyebrow/.eyebrow--dark/.eyebrow--sticky,
   .horizontal-story__checklist/__check-row/__check-icon (see
   HORIZONTAL STORY) and .article-back/.article-quote__text (see
   ARTICLE CONTENT LAYOUT) — only the two-column job-intro row and the
   single eyebrow+long-content grid below are new, since no existing
   component already shapes either.
   ================================================================= */

/* ---------- Page Intro: tags + highlights + Apply CTA row ----------
   Sits inside the shared .page-intro__content column (grid-column: 3 /
   span 10, 1516px). Figma splits that same 1516px into an 1208px job-
   summary column + a 284px Apply CTA side by side (8 cols + 2 cols +
   the 24px gap between them) — expressed here as a flex row rather
   than a second nested grid, since the two pieces are just distributed
   across the already-reserved width, not placed on new column lines. */
.job-hero__row { display: flex; align-items: flex-start; justify-content: space-between; gap: 24px; width: 100%; }
.job-hero__main { display: flex; flex-direction: column; gap: 48px; flex: 1 1 auto; min-width: 0; max-width: 1208px; }
.job-hero__apply { flex-shrink: 0; }
.job-hero__tags { display: flex; flex-wrap: wrap; gap: 10px; }
/* Job highlights reuse .horizontal-story__checklist/__check-row/
   __check-icon verbatim; only the row text's size/colour is added
   here (identical convention to .pathway__checklist), matching
   Figma's "Body Copy XX-Large" token (22px/33.65px/-0.22px) at 70%
   white for this dark ground. The icon's top margin is nudged from
   the shared component's 4px default to keep it centred against this
   taller line-height instead of the 16px copy it was tuned for. */
.job-hero__highlights .horizontal-story__check-row { gap: 16px; }
.job-hero__highlights .horizontal-story__check-icon { margin-top: 9px; }
.job-hero__highlights .horizontal-story__check-row p { margin: 0; font-size: 22px; font-weight: 400; line-height: 33.65px; letter-spacing: -0.22px; color: var(--color-white-70); }

/* ---------- Body: single eyebrow + long-form content column ----------
   Same explicit-grid-column technique as every other eyebrow+content
   row on the site (see .split-feature__eyebrow/__content) — eyebrow
   cols 1-2, content cols 3-10 (1208px), leaving cols 11-12 empty on
   purpose (Figma's own pr-308 gap). No media column: this template is
   one long content column, not a split layout. */
.job-detail { padding: var(--space-m) 0; background: var(--color-white); }
.job-detail__frame { align-items: flex-start; }
.job-detail__eyebrow { grid-column: 1 / span 2; align-self: stretch; }
.job-detail__eyebrow .eyebrow__track { white-space: nowrap; }
.job-detail__content { grid-column: 3 / span 8; min-width: 0; display: flex; flex-direction: column; gap: 48px; }
.job-detail__intro { display: flex; flex-direction: column; gap: 32px; }
.job-detail__copy { display: flex; flex-direction: column; gap: 18px; }
.job-detail__block { display: flex; flex-direction: column; gap: 24px; }
.job-detail__rule { height: 1px; width: 100%; background: var(--color-light-grey); }
/* Responsibilities/Requirements/Benefits lists reuse the same shared
   checklist component as the hero highlights above, at Figma's
   smaller "Body Copy Regular" token (16px/24.47px/-0.16px) in body-
   grey for this light ground — the light-ground counterpart to
   .pathway__checklist's dark-ground white-70 variant. */
.job-detail__list .horizontal-story__check-row p { margin: 0; font-size: 16px; font-weight: 400; line-height: 24.47px; letter-spacing: -0.16px; color: var(--color-body-grey); }

@media (max-width: 1199px) {
  .job-hero__row { flex-direction: column; }
  .job-hero__main { max-width: 100%; }
  .job-detail__eyebrow { grid-column: auto; width: 100%; margin-bottom: 32px; }
  .job-detail__content { grid-column: auto; }
}
@media (max-width: 767px) {
  .job-detail { padding-top: var(--space-xxs); padding-bottom: var(--space-xxs); }
  .job-hero__highlights .horizontal-story__check-row p { font-size: 18px; line-height: 1.5; }
  .job-detail__block .h4 { font-size: 24px; line-height: 32px; }
}

/* =================================================================
   APPLY MODAL — job application overlay (Figma "Careers / Modal",
   node 21242:22585). Opened by the "Apply now" CTAs on a job detail
   page (job-hero__apply, .job-detail__block's own Apply link); see
   js/main.js's initApplyModal for open/close/scroll-lock, which
   mirrors js/people.js's lockPageScroll/unlockPageScroll technique
   rather than inventing a new one.

   NOT a Profile/Career Modal variant: those are two-column (fixed
   portrait + independently-scrolling panel). This one is a single
   centred column with no portrait, so it gets its own .apply-modal*
   classes rather than forcing a portrait-less case onto that grid.
   Per the Figma frame's own dev annotation, opening/closing transition
   and any next/previous behaviour are unspecified — only the layout
   below, plus the same restrained opacity+translateY the other modals
   already use for their open/close state, is implemented.

   The whole dialog is this component's one scroll region (no split
   left/right to preserve), so unlike Profile/Career Modal the close
   button is `position: fixed` rather than `absolute` — it needs to
   stay put in the viewport while .apply-modal__dialog scrolls under
   it, rather than riding along with scrolled content. */
.apply-modal {
  position: fixed;
  inset: 0;
  z-index: 200;
  background: var(--color-dark-blue);
  height: 100vh;
  height: 100dvh;
  overflow: hidden;
}
.apply-modal[hidden] { display: none; }

.apply-modal__close {
  position: fixed;
  top: 40px;
  right: var(--page-margin);
  z-index: 2;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 48px;
  height: 48px;
  border: 1px solid var(--color-icon-blue);
  border-radius: 50px;
  color: var(--color-icon-blue);
  transition: background 0.2s ease, color 0.2s ease;
}
.apply-modal__close:hover { background: var(--color-icon-blue); color: var(--color-dark-blue); }
.apply-modal__close-icon { position: relative; width: 48px; height: 48px; display: block; }
.apply-modal__close-icon::before,
.apply-modal__close-icon::after {
  content: "";
  position: absolute;
  top: 50%; left: 50%;
  width: 18px; height: 1.5px;
  background: currentColor;
  transform-origin: center;
}
.apply-modal__close-icon::before { transform: translate(-50%, -50%) rotate(45deg); }
.apply-modal__close-icon::after { transform: translate(-50%, -50%) rotate(-45deg); }

/* The dialog is the modal's only scrolling region — .apply-modal
   itself stays overflow: hidden above so the fixed close button never
   needs to compensate for a scrollbar. Same restrained entrance/exit
   as Profile/Career Modal: opacity + translateY, skipped under
   prefers-reduced-motion, toggled by .apply-modal.is-open. */
.apply-modal__dialog {
  height: 100%;
  overflow-y: auto;
  opacity: 0;
  transform: translateY(12px);
  transition: opacity 0.25s ease, transform 0.25s ease;
}
.apply-modal.is-open .apply-modal__dialog {
  opacity: 1;
  transform: translateY(0);
}
@media (prefers-reduced-motion: reduce) {
  .apply-modal__dialog { transition: none; }
}

/* Figma's content column is a fixed 746px, centred in the frame —
   expressed as max-width + auto margins rather than a grid-column
   placement, since there's no second column (portrait/panel) to place
   it against; this column is the whole layout. */
.apply-modal__frame {
  max-width: 746px;
  margin: 0 auto;
  padding: 90px var(--page-margin) var(--space-s);
  display: flex;
  flex-direction: column;
  gap: var(--space-xs);
}
.apply-modal__top { display: flex; align-items: center; }
.apply-modal__back {
  display: flex;
  align-items: center;
  gap: 21px;
  color: var(--color-icon-blue);
  font-size: 18px;
  font-weight: 600;
  line-height: 27.2px;
  letter-spacing: -0.18px;
  text-align: left;
}
.apply-modal__back-icon { transform: rotate(180deg); }
.apply-modal__rule { flex: 0 0 auto; height: 1px; background: var(--color-white-20); }

/* White application card — logo strip + form image, Figma's own
   64px gap between the two, 80px of breathing room under the image
   before the card's own bottom edge. */
.apply-modal__card {
  width: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-xxs);
  background: var(--color-white);
  border-radius: var(--radius-s);
  overflow: hidden;
  padding-bottom: var(--space-xs);
}
.apply-modal__logos {
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  flex-wrap: wrap;
  gap: var(--space-xs);
  padding: 40px var(--page-margin);
  background: var(--color-grey-101);
}
.apply-modal__logo { display: block; height: 40px; width: auto; object-fit: contain; }
/* logo-barpa.svg's shapes are authored white-on-transparent (built for
   the dark footer/nav grounds it's used on elsewhere) — inverted here
   for this component's light card background, same fix as
   .group-card__logo--on-light on About. */
.apply-modal__logo--on-light { filter: invert(1); }

/* The form image IS the form for now (see component header comment) —
   sized from its own intrinsic 506×728 rather than a hard-coded
   aspect-ratio, so swapping the file later (the real JobAdder-
   integrated form) doesn't require a matching CSS edit. object-fit
   isn't needed since the element is never cropped to a foreign box. */
.apply-modal__form-img {
  display: block;
  width: 100%;
  max-width: 506px;
  height: auto;
  margin: 0 auto;
}

@media (max-width: 1199px) {
  .apply-modal__frame { padding-top: 96px; }
}
@media (max-width: 767px) {
  .apply-modal__close { top: 24px; right: var(--page-margin); }
  .apply-modal__frame { padding: 88px var(--page-margin) var(--space-xxs); gap: var(--space-xxs); }
  .apply-modal__logos { gap: 32px; padding: 32px var(--page-margin); }
  .apply-modal__card { gap: 40px; padding-bottom: var(--space-xxs); }
}

