  :root {
    /* Official Bensimon Byrne palette — see BRAND.md. These eleven values
       are the only colours in the site; everything else is one of them at an
       opacity. The -rgb triplets exist so rgba() can draw from the same
       source rather than restating the channels. */
    --bb-white: #FFFFFF;
    --bb-off-white: #F4ECE1;
    --bb-black: #000000;
    --bb-orange: #D76742;
    --bb-red: #782715;
    --bb-burgundy: #3E0E0C;
    --bb-pink: #F0A8B9;
    --bb-sky: #ADD4F1;
    --bb-navy: #1B1D3C;
    --bb-forest: #163612;
    --bb-cream: #E9D8C1;

    --bb-white-rgb: 255, 255, 255;
    --bb-off-white-rgb: 244, 236, 225;
    --bb-black-rgb: 0, 0, 0;
    --bb-orange-rgb: 215, 103, 66;
    --bb-red-rgb: 120, 39, 21;
    --bb-burgundy-rgb: 62, 14, 12;
    --bb-pink-rgb: 240, 168, 185;
    --bb-sky-rgb: 173, 212, 241;
    --bb-navy-rgb: 27, 29, 60;
    --bb-forest-rgb: 22, 54, 18;
    --bb-cream-rgb: 233, 216, 193;

    /* Column-wipe transition pattern (see .transition__panel below) —
       brand navy / forest green. */
    --transition-color-a: var(--bb-navy);
    --transition-color-b: var(--bb-forest);

    /* Page grid system — the shared column/gap/margin values behind every
       .cs-* template grid (and reused by .pillars__inner/.portfolio-grid
       elsewhere). New pages and sections should build their grids off
       these instead of hardcoding 12/8/6, 16px or 20px/16px directly, so
       a sitewide adjustment only has to happen once, here.

       --grid-cols/--page-margin are the LIVE values for the current
       breakpoint (redefined below at 1200px/900px — the .cs-* template's
       established tablet/mobile grid breakpoints) — reference these in a
       grid whose own column count/margin is meant to track the standard
       responsive steps automatically. --grid-cols-desktop stays fixed at
       12 regardless of viewport — reference it instead in a grid that
       deliberately keeps a constant 12 tracks through tablet and changes
       layout some other way (full-bleed spans, switching to flex) rather
       than reducing its column count, which several .cs-* blocks do on
       purpose. --page-margin-desktop is the equivalent fixed constant for
       margins. --grid-gap does not change per breakpoint in this system. */
    --grid-cols-desktop: 12;
    --grid-cols-tablet: 8;
    --grid-cols-mobile: 6;
    --grid-cols: var(--grid-cols-desktop);

    --grid-gap: 16px;

    --page-margin-desktop: 20px;
    --page-margin-mobile: 16px;
    --page-margin: var(--page-margin-desktop);
  }

  @media (max-width: 1200px) {
    :root { --grid-cols: var(--grid-cols-tablet); }
  }

  @media (max-width: 900px) {
    :root {
      --grid-cols: var(--grid-cols-mobile);
      --page-margin: var(--page-margin-mobile);
    }
  }

  /* WOFF2 first: the same fonts, compressed (Haas 177K -> 59K, the STIX
     italic 447K -> 173K). Apache doesn't gzip the .ttf files, so they
     were going out at full size. The .ttf stays as a fallback. The
     Haas file is also preloaded from each page's <head>. */
  @font-face {
    font-family: "Haas Recast VF";
    src: url("../fonts/HaasRecastVF_TrakWght.woff2") format("woff2"),
         url("../fonts/HaasRecastVF_TrakWght.ttf") format("truetype-variations");
    font-weight: 100 900;
    font-style: normal;
    font-display: swap;
  }
  @font-face {
    font-family: "STIX Two Text";
    src: url("../fonts/STIX_Two_Text/STIXTwoText-VariableFont_wght.woff2") format("woff2"),
         url("../fonts/STIX_Two_Text/STIXTwoText-VariableFont_wght.ttf") format("truetype-variations");
    font-weight: 400 700;
    font-style: normal;
    font-display: swap;
  }
  @font-face {
    font-family: "STIX Two Text";
    src: url("../fonts/STIX_Two_Text/STIXTwoText-Italic-VariableFont_wght.woff2") format("woff2"),
         url("../fonts/STIX_Two_Text/STIXTwoText-Italic-VariableFont_wght.ttf") format("truetype-variations");
    font-weight: 400 700;
    font-style: italic;
    font-display: swap;
  }

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

  /* ------------------------------------------------------------------
     Vertical metric trim, site-wide.

     Every text block's box is trimmed to its own cap height (top) and
     baseline (bottom), so the leading no longer leaks out of the top and
     bottom of the block. What's left is an optical box: the space you
     set between a heading and the copy under it is the space you get.

     Why this is safe for our two faces — both carry a real sCapHeight in
     OS/2, so browsers trim to true metrics rather than guessing:

       Haas Recast VF   upem 1000   asc 993  desc -273  cap 720
       STIX Two Text    upem 1000   asc 762  desc -238  cap 657

     Haas Recast is the happy case: ascender - cap = 273, which is
     exactly the descender depth. trim-both therefore removes an
     identical slice off the top and the bottom, so nothing shifts
     optically off-centre — blocks just get shorter, symmetrically. How
     much comes off each side, at the line-heights used in this file:

       line-height 0.92-0.97  (display/headings)  0.100-0.125em per side
       line-height 1.19-1.30  (body/subcopy)      0.235-0.290em per side

     So a 120px hero heading loses ~15px top and bottom; 18px body copy
     loses ~5px. Anywhere that reclaimed space was doing layout work, the
     compensation lives in the @supports block further down, next to the
     rule it's correcting — not here.

     Applied via * deliberately. GSAP SplitText re-wraps headings into
     per-line <div>s at runtime, and text-box-trim only acts on a block
     that owns an inline formatting context — so a rule scoped to h1-h6/p
     would silently stop trimming the moment SplitText ran. Inheriting it
     onto the generated line wrappers keeps trimmed and untrimmed
     headings identical. .line-mask below re-opens the room those
     wrappers need for ascenders and descenders once trimmed.

     Chrome/Edge 133+, Safari 18.2+, Firefox 154+. Everywhere else the
     declaration is dropped and the page renders exactly as it did
     before — which is why every compensating tweak is @supports-gated
     rather than unconditional. */
  * {
    text-box: trim-both cap alphabetic;
  }

  /* Opt-out. Put this on anything where the glyph box genuinely needs
     its full leading — e.g. a character that has to stay vertically
     centred inside a fixed-size box it doesn't control. */
  .no-text-trim,
  .no-text-trim * {
    text-box: normal;
  }

  /* Avoid single-word widows/orphans on headings and body copy site-wide. */
  h1, h2, h3, h4, h5, h6, p, li {
    text-wrap: pretty;
  }

  /* Paragraph break, site-wide. Trimmed, a <p> ends at its own baseline,
     so this margin is the entire gap between paragraphs rather than a
     top-up on the leading that used to hang below the last line.

     For this to survive SplitText, a prose block's <p>s have to be the
     split roots themselves. Splitting their *container* makes SplitText
     re-wrap every rendered line in its own clone of the source <p>, at
     which point no two <p>s are siblings any more, p + p matches nothing
     and paragraph breaks silently vanish — which is what was happening
     on the case-study pages. The markup now carries data-split="heading"
     on each <p> (see .cs-hero__copy / .cs-text-block__body and the
     footer address block); keep it there when adding prose. */
  p + p {
    margin-top: 14px;
  }

  /* Headings that were <p>s until they became real headings (2026-09-25)
     and still take their type from their container, as the <p>s did:
     without this the browser's own h2 size and bold would win. The
     Tadiem card's description keeps the 14px it had as a p + p. */
  .team__intro-lead,
  .careers__intro-lead {
    font-size: inherit;
    font-weight: inherit;
  }

  .tadiem-card__heading + .tadiem-card__desc {
    margin-top: 14px;
  }

  /* ...except where the parent is already a flex column spacing its own
     children. These are layout stacks, not running prose — a value above
     its label, a quote above its attribution — and a flex item's margin
     adds to the container's gap rather than replacing it. */
  .clients__list > p + p,
  .cs-quote__inner > p + p,
  .cs-stats__card > p + p,
  .mobile-menu__contact > p + p,
  .capabilities__panel-copy > p + p,
  .combine__text > p + p,
  .team__meta > p + p,
  .team__intro > p + p,
  .careers__intro > p + p {
    margin-top: 0;
  }

  /* Reachable by assistive tech, invisible on screen — live regions, skip links. */
  .visually-hidden {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }

  /* Baseline focus ring for everything focusable. currentColor rather than a
     fixed hue because the site runs light and dark sections against each
     other — the ring then inherits whatever the text already contrasts
     against, on cream, burgundy, navy and forest alike, and .pillars__item-link
     set that precedent before this rule existed.

     :where() keeps it at zero specificity, so any block wanting a different
     treatment overrides it by simply existing rather than by out-specifying
     it. Nothing opts out: removing the ring without replacing it is the one
     thing this is here to prevent.

     tabindex="-1" is excluded because those are programmatic focus targets,
     not controls anyone tabs to. <main id="main" tabindex="-1"> is the skip
     link's destination, and without the exclusion Chrome matched it on
     :focus-visible and drew a 2px ring around a 1024x7667 box — the whole
     page. The slider's off-screen slides carry -1 for the same reason. */
  :where(a, button, input, select, textarea, summary,
         [tabindex]:not([tabindex="-1"])):focus-visible {
    outline: 2px solid currentColor;
    outline-offset: 3px;
  }

  /* Excluding tabindex="-1" above only stops OUR ring; the browser still
     draws its own, so <main> was left ringed in Chrome's orange across the
     full page. This is the one place outline: none is right: a -1 element
     is not keyboard reachable and is not a control, so there is no focus
     anyone needs to see here — the skip link that sent focus in is the
     visible affordance, and it has its own ring. */
  [tabindex="-1"]:focus {
    outline: none;
  }

  /* An outline is painted outside the border box, so any ancestor clipping its
     overflow swallows the ring of everything inside it. That hit the reveal
     masks hardest — they wrap tight around a CTA, so the ring had nowhere to
     go — and it was already breaking the bespoke focus styles that predate the
     rule above: .pillars__item-link draws at 6px offset and the video controls
     at 4px, both straight into a clip.

     Releasing the clip only while something inside has focus: none of these
     need to be clipping at that moment, and the reveal they exist for has
     long since finished by the time anything is tabbed to.

     :focus-within rather than :has(:focus-visible) — it is the older, more
     widely supported selector, and releasing the clip on a mouse click too
     costs nothing, since the ring itself is still :focus-visible only.

     Written out rather than wrapped in :where(): each block sets its own
     overflow further down the file, and at :where()'s zero specificity the
     later rule simply won. */
  .reveal-mask:focus-within,
  .line-mask:focus-within,
  .pillars:focus-within,
  .capabilities:focus-within {
    overflow: visible;
  }

  /* The exception. .vimeo-bg cannot give up its clip — the iframe inside is
     deliberately oversized to cover the stage, and letting it out would spill
     video across the page. Its controls are inset: 0 against it, so the ring
     goes inward instead. */
  .vimeo-bg :where(a, button):focus-visible {
    outline-offset: -4px;
  }

  /* First thing in the tab order on every page: the navbar is fixed and ~30
     links deep, so without this a keyboard user walks the whole of it before
     reaching content. Off-screen until focused, then parked over the navbar
     (which is z-index 100). */
  .skip-link {
    position: fixed;
    top: 8px;
    left: 8px;
    z-index: 200;
    padding: 12px 16px;
    background: var(--bb-white);
    color: var(--bb-black);
    /* The chip's own edge, separate from the focus ring outside it: white on
       off-white is 1.17:1, so without a border the shape has no definition of
       its own. 1px against the ring's 2px so the two read as edge and ring
       rather than a double ring. */
    border: 1px solid var(--bb-black);
    font-family: "Haas Recast VF", "Helvetica Neue", Arial, sans-serif;
    font-weight: 500;
    font-variation-settings: 'TRAK' 0;
    font-size: 16px;
    line-height: 1;
    text-decoration: none;
    transform: translateY(calc(-100% - 16px));
  }

  .skip-link:focus {
    transform: translateY(0);
  }

  /* No focus-ring override here on purpose: the baseline rule's currentColor
     resolves to this chip's own black, which is what makes it readable. An
     off-white ring was tried and is invisible — the chip is white, the page
     behind it is off-white, and the two are a shade apart. */


  html {
    overflow-x: hidden;
  }

  a {
    color: inherit;
  }

  /* overflow-x is only set on <html> (above), not repeated here: a mismatched
     overflow-x/overflow-y pair on the *same* element (hidden + the initial
     visible) makes that element compute overflow-y as auto too, per the CSS
     Overflow spec — which would make body a scroll container of its own.
     It isn't one (html is document.scrollingElement, body's scrollTop never
     moves), so that phantom container just breaks position:sticky for any
     descendant, matching to body's static box instead of the real viewport.
     html's own overflow-x:hidden already clips horizontal overflow for the
     whole page, so body doesn't need to repeat it. */
  body {
    background: var(--bb-off-white);
  }

  /* Barba page transition: column wipe overlay, driven by main.js. Above
     .navbar and .mobile-menu (both z-index <= 100) so it covers the
     originating page's nav/menu instead of stacking underneath them. */
  .transition {
    z-index: 1000;
    pointer-events: none;
    position: fixed;
    inset: 0;
    overflow: clip;
  }

  .transition__panels {
    width: 100%;
    height: 100%;
    display: flex;
    position: absolute;
    top: 0;
    left: 0;
  }

  .transition__panel {
    /* Brick tile from Figma (node 1:1921, "Brick - HalfMargin_
       QuarterAsymetricRow"): a 20x18 unit, 2 equal-width columns x 2 rows
       split 2fr/1fr (12px/6px), colors swapped between columns — top-
       left/bottom-right one color, top-right/bottom-left the other.
       background-color is color B (the base); ::before paints color A
       through a shape-only SVG mask, so both colors stay plain CSS
       variables instead of being baked into the encoded SVG. background-
       size: 100% auto scales the tile to each panel's own width, height
       following automatically off the mask image's intrinsic 20:18
       ratio. */
    background-color: var(--transition-color-b);
    width: 100%;
    height: 100%;
    position: relative;
    top: -100%;
    overflow: hidden;
  }

  .transition__panel::before {
    content: '';
    position: absolute;
    inset: 0;
    background-color: var(--transition-color-a);
    -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='18'%3E%3Crect y='12' width='10' height='6' fill='white'/%3E%3Crect x='10' width='10' height='12' fill='white'/%3E%3C/svg%3E");
    mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='18'%3E%3Crect y='12' width='10' height='6' fill='white'/%3E%3Crect x='10' width='10' height='12' fill='white'/%3E%3C/svg%3E");
    -webkit-mask-size: 100% auto;
    mask-size: 100% auto;
    -webkit-mask-repeat: repeat;
    mask-repeat: repeat;
  }

  /* Masked scroll reveal: hide [data-split="heading"] up front so there's no
     flash of unsplit text before SplitText/ScrollTrigger run (main.js swaps
     this for visibility:visible right as the split completes). [data-mask-reveal]
     marks an element as its own reveal mask for a child image/media element. */
  [data-split="heading"] {
    visibility: hidden;
  }

  /* Both of these are clip wrappers, not type — exempt from the sitewide
     trim for the same reason the odometer is.

     A reveal mask is a block box whose only job is to hide its child
     while it translates. Trimmed, its own line box collapses to the cap
     height of its inherited font — around 10.6px at the card's 16px —
     and anything inline inside it that is taller than that gets sliced
     off by the overflow:hidden. That is what was cutting the padding off
     .tag: the pill is 27.58px of inline-flex, the generated wrapper
     trimmed itself down to 10.59px, and 17px of pill disappeared the
     moment the reveal initialised and the wrapper appeared. First paint
     looked right because the wrapper does not exist yet.

     Untrimmed, the wrapper's line box sizes to the inline content it
     holds, which is what a mask has to do. Nothing here sets type, so
     nothing is lost by opting out: the element being revealed keeps its
     own trim. Applies to both the hand-placed [data-mask-reveal] (today
     only ever on media — gallery items, card images, the footer
     wordmark) and the wrapper main.js generates. */
  [data-mask-reveal],
  .reveal-mask {
    text-box: normal;
  }

  [data-mask-reveal] {
    position: relative;
    overflow: hidden;
  }

  /* Auto-generated by main.js around any [data-reveal-group] slot that has
     no [data-split]/[data-mask-reveal] of its own.

     It clips at the edge of what it wraps, and text on this site is
     text-box-trimmed to cap height and baseline, so without room to spare
     the mask cuts off anything past either edge: descenders (g, y, p),
     accents and ascenders above a capital (É). The padding buys that room
     and the equal negative margin cancels it, so nothing moves. This is
     the same idea as .line-mask's 0.2em, for the masks SplitText makes.

     The negative margin is only layout-neutral because every generated
     mask on the site sits in a flex container, where margins never
     collapse (checked across all pages). In ordinary block flow it would
     collapse with a neighbour's margin and shift spacing. */
  .reveal-mask {
    overflow: hidden;
    padding: 0.3em 0;
    margin: -0.3em 0;
  }

  /* SplitText's own per-line mask (mask:"lines") clips at its padding
     edge, which otherwise flat-cuts descenders (g, y, p, q, j) on the
     line below. The bottom padding gives them room inside the mask; the
     matching negative margin cancels the padding's added height so line
     spacing/layout stays exactly as before. */
  .line-mask {
    padding-bottom: 0.2em;
    margin-bottom: -0.2em;
  }

  /* A split heading is ONE paragraph pulled apart, not a stack of separate
     blocks — so it has to trim like one. The sitewide * rule would trim
     every .line independently, which zeroes the leading BETWEEN lines and
     welds a two-line hero shut; a split heading would stop matching the
     unsplit markup it was built from. Scoping it to the first and last
     line restores the intent: leading comes off the outside of the
     heading only, and every interior line keeps its full line box.

     :first-child:last-child is not redundant — a one-line heading is both
     ends at once, and without it the :last-child rule would win outright
     and leave the top untrimmed. */
  @supports (text-box: trim-both cap alphabetic) {
    /* text-box-edge takes BOTH edges or neither — "trim-start cap" and
       "trim-end alphabetic" parse as invalid and are dropped on the floor,
       silently leaving the line untrimmed. The edge pair always reads
       "cap alphabetic"; it's the trim keyword that picks which side of it
       actually gets cut. */
    /* Each rule has to reach the mask's whole subtree, not just .line.
       SplitText keeps the source markup inside the wrapper, so a split
       <p> comes out as .line-mask > .line > p — and it's that inner <p>
       that owns the inline formatting context and therefore the trim.
       Resetting only .line leaves the <p> still trimmed by the * rule
       and every interior line collapses to cap height anyway. Matching
       the descendants too covers both shapes: bare text in .line, or a
       block nested inside it. */
    .line-mask,
    .line-mask * { text-box: normal; }

    .line-mask:first-child,
    .line-mask:first-child * { text-box: trim-start cap alphabetic; }

    .line-mask:last-child,
    .line-mask:last-child * { text-box: trim-end cap alphabetic; }

    .line-mask:first-child:last-child,
    .line-mask:first-child:last-child * { text-box: trim-both cap alphabetic; }

    /* Trimmed, the outer lines' boxes stop at cap height and the baseline,
       so the mask now clips at the ink on both ends rather than out in the
       leading. Measured glyph extremes, both faces at upem 1000:

                      cap     tallest ink      deepest ink
         Haas Recast  0.720   À 0.910          g -0.184
         STIX italic  0.657   Î 0.836          f -0.227

       Haas is drawn for this: b/d/h/k/l/i all top out at exactly 0.720,
       flush with cap, so ordinary headline text needs no top room at all
       — only accents and $ (0.800) reach past it. STIX italic is the one
       that does, its ascenders running 0.048em above its cap. 0.2em/0.25em
       clears the worst case in both faces including accented caps.

       Only the first and last mask are padded, so no two negative margins
       ever adjoin: CSS collapses adjoining margins to the single most
       negative value instead of summing them, which would otherwise leave
       every line an unpaid 0.2em taller than its line-height. Interior
       masks keep the base rule's bottom padding, which is correct for
       them precisely because they are not trimmed.

       hiddenOffset() in main.js measures the real mask/line ratio at
       runtime, so the reveal animation re-derives its travel from these
       values on its own. */
    .line-mask:first-child { padding-top: 0.2em; margin-top: -0.2em; }
    .line-mask:last-child { padding-bottom: 0.25em; margin-bottom: -0.25em; }
  }

  /* ------------------------------------------------------------------
     .bb-mark — the BB logo as a Lottie, wherever it appears.

     A square box (.bb-mark) with the animation's own artboard laid over
     it (.bb-mark__art) and grown inside it. Two elements rather than
     one so the artboard size fitLottieToMark() writes sits on a box of
     its own, leaving .bb-mark's transform free for whatever is
     positioning the mark.

     The scale is needed because the mark occupies only the middle of
     its 1080x1080 artboard — at the box's own size it would come out at
     well under half of it. main.js measures the resting frame and
     scales by the shortfall, so the rendered mark is exactly the box:
     measured rather than hardcoded, so a re-export with different
     padding still lands.

     Colour: the export paints the mark with an After Effects "Fill"
     effect (type 21), which Lottie renders as an feColorMatrix no
     stylesheet can reach — leaving the mark stuck at whatever colour
     the .aep was rendered in. main.js strips those effects at load,
     which uncovers each layer's own artwork (ordinary filled paths),
     and the two rules below repaint that artwork and the centre line's
     stroke in currentColor. So the mark inherits --nav-text in the
     navbar, recolouring with the section underneath exactly like the
     static <svg> it replaces.

     Scoped to `svg > g` — Lottie's live render tree — on purpose. The
     same export's masks and clip paths live in <defs> as black-filled
     shapes used for luminance, so repainting those would silently
     change which parts of the mark get revealed.
     ------------------------------------------------------------------ */

  .bb-mark {
    position: relative;
    display: block;
    pointer-events: none;
  }

  .bb-mark__art {
    position: absolute;
    inset: 0;
    transform-origin: 50% 50%;
  }

  .bb-mark__art svg {
    display: block;
    width: 100%;
    height: 100%;
  }

  .bb-mark__art svg > g path {
    fill: currentColor;
  }

  .bb-mark__art svg > g path[stroke]:not([stroke="none"]) {
    stroke: currentColor;
  }

  .navbar {
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    height: 139px;
    background-color: var(--nav-bg, var(--bb-off-white));
    color: var(--nav-text, var(--bb-black));
    /* Three columns, the outer two equal, so the logo sits on the page's
       exact centre. As a centred flex row it was centred on the whole
       group instead, and the left links run 21px wider than the right,
       which pushed it ~11px off. Each link group hugs the logo from its
       own side, so the spacing between them is unchanged. */
    display: grid;
    grid-template-columns: 1fr auto 1fr;
    align-items: center;
    column-gap: 5px;
    z-index: 100;
    transform: translateY(0);
    transition: transform 0.35s ease, height 0.35s ease, background-color 0.3s ease, color 0.3s ease;
  }

  .navbar--hidden {
    transform: translateY(-100%);
  }

  /* A hidden bar still holds focusable links, so keyboard focus landing
     in it (shift-tabbing up from the page, or from the footer, where the
     bar stays hidden) brings it back rather than focusing a link that is
     off screen.

     :has(:focus-visible), not :focus-within: a mouse click on a nav link
     also leaves focus on it, and Chrome and Firefox keep it there. Clicking
     Careers on Who we are scrolls the page without leaving it, so with
     :focus-within the bar stayed pinned on screen, hidden class and all,
     until something else took focus. :focus-visible matches keyboard
     focus and not a click, which is exactly the case this rule is for.
     Browsers without :has() keep :focus-within. */
  .navbar--hidden:has(:focus-visible) {
    transform: translateY(0);
  }

  @supports not selector(:has(*)) {
    .navbar--hidden:focus-within {
      transform: translateY(0);
    }
  }

  .navbar__links {
    display: flex;
    align-items: center;
    gap: 24px;
  }

  /* Placed explicitly, so the logo keeps the middle column on phones,
     where both link groups are display:none and auto-placement would
     otherwise drop it into the first. */
  .navbar__links--left { grid-column: 1; justify-self: end; }
  .navbar__logo { grid-column: 2; }
  .navbar__links--right { grid-column: 3; justify-self: start; }

  .navbar__link {
    font-family: "STIX Two Text", Georgia, "Times New Roman", serif;
    font-weight: 400;
    font-style: italic;
    font-size: 16px;
    line-height: 1;
    color: inherit;
    text-decoration: none;
    white-space: nowrap;
    opacity: 0.5;
    transition: opacity 0.3s ease;
    /* Hit area: the trimmed text box is only ~11px tall, under WCAG
       2.5.8's 24px, and on Who we are the nav sits over the reel's
       full-frame pause control, so spacing alone can't rescue it. The
       padding makes each link a 25px-tall target; the equal negative
       margin keeps the bar's layout exactly as it was. */
    display: inline-block;
    padding: 7px 0;
    margin: -7px 0;
  }

  /* Current page's link stays at full opacity. So does every link on a page
     that isn't one of the four nav items — the homepage, News, Privacy —
     since there no link can carry aria-current and dimming all four reads
     as though the whole nav were inactive. Such pages need no marker class
     of their own; the absence of a current link is the signal.

     The second selector tests that condition directly rather than naming
     those pages, so a new non-nav-item page is covered the day it's added.
     It lands at the same specificity as the hover rule below and comes
     first, so hovering still spotlights one link on those pages. */
  .navbar__link[aria-current="page"],
  .navbar:not(:has(.navbar__link[aria-current="page"])) .navbar__link {
    opacity: 1;
  }

  /* Hovering any nav link spotlights it: that link goes to full opacity and
     every other one drops to 0.5, including whichever link is current. The
     0.3s ease above carries them all back on leave.

     :has() keys the dimming to a link actually being hovered — .navbar:hover
     would fire for the logo, the burger and the bar's empty space too. Both
     selectors carry the :has() prefix so they out-specify the full-opacity
     rule above; without it on the second, the dimming would win over the
     hovered link itself. Hover-gated so a tap can't latch it on. */
  @media (hover: hover) and (pointer: fine) {
    .navbar:has(.navbar__link:hover) .navbar__link {
      opacity: 0.5;
    }

    .navbar:has(.navbar__link:hover) .navbar__link:hover {
      opacity: 1;
    }
  }

  .navbar__logo {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 139px;
    height: 139px;
    padding: 36px;
    flex-shrink: 0;
    color: inherit;
    position: relative;
    transition: height 0.35s ease, padding 0.35s ease;
  }

  /* Direct child only. These predate the Lottie and were written when the
     mark was the sole <svg> in here — unscoped they also reach inside the
     Lottie, including the mask and clip paths it keeps in <defs>, which
     are black-filled shapes used for luminance. Repainting those corrupts
     the draw-on mattes and the mark renders with a ghost of itself. */
  .navbar__logo > svg {
    width: 67px;
    height: 67px;
  }

  .navbar__logo > svg path {
    fill: currentColor;
  }

  /* Where a .bb-mark sits when it is the navbar's logo:
     initNavLogoLottie() injects one on first load.

     The inset matches .navbar__logo's own padding, which puts this box
     exactly where the static <svg> renders — and that holds at both
     breakpoints, though not for the reason it looks like. Above 640px
     the logo's content box is 67px and the <svg> fills it. Below, the
     content box is 52px and the <svg>, still asking for 67, is
     flex-shrunk to 52 wide; preserveAspectRatio then draws the mark at
     52. Either way the mark ends up the size of the content box, so
     tracking the padding tracks the mark. */
  .navbar__logo-lottie {
    position: absolute;
    inset: 36px;
    transition: inset 0.35s ease;
  }

  /* Compact bar, once the page has scrolled past the full-height one
     (initPeekabooNav() adds .navbar--compact): 70px tall, the mark at
     34px, about half. The logo slot keeps its full width and only loses
     height, so the links either side stay exactly where they were; the
     horizontal padding is what shrinks the mark, (139 - 34) / 2 each
     side. The Lottie's inset mirrors the padding, as above.

     Phones get a 64px bar with a 32px mark in their 96px slot. The burger
     menu's panel is pinned to the full 96px bar (.mobile-menu's top), so
     opening the menu drops the compact class first (see the burger init
     in main.js) and the panel always meets the bar. */
  @media (min-width: 641px) {
    .navbar--compact { height: 70px; }
    .navbar--compact .navbar__logo { height: 70px; padding: 18px 52.5px; }
    .navbar--compact .navbar__logo-lottie { inset: 18px 52.5px; }
  }

  @media (max-width: 640px) {
    /* .navbar.navbar--compact: the phone .navbar height rule further down
       has the same weight as a lone class and would win on order. */
    .navbar.navbar--compact { height: 64px; }
    .navbar--compact .navbar__logo { height: 64px; padding: 16px 32px; }
    .navbar--compact .navbar__logo-lottie { inset: 16px 32px; }
  }

  @media (prefers-reduced-motion: reduce) {
    .navbar { transition: transform 0.35s ease, background-color 0.3s ease, color 0.3s ease; }
    .navbar__logo,
    .navbar__logo-lottie { transition: none; }
  }

  /* The static mark is hidden, not removed, for as long as the Lottie is
     standing in for it — which is the rest of the visit, since the
     animation holds on its last frame rather than handing back. Keeping
     it in the DOM keeps the navbar's layout identical either way, and
     leaves it ready to uncover if the Lottie ever fails to load. */
  .navbar__logo--lottie > svg {
    opacity: 0;
  }

  .navbar__burger {
    display: none;
    position: relative;
    width: 30px;
    height: 14px;
    flex-shrink: 0;
    padding: 0;
    background: none;
    border: none;
    color: inherit;
    cursor: pointer;
  }

  .navbar__burger-bar {
    position: absolute;
    left: 0;
    width: 100%;
    height: 2px;
  }

  .navbar__burger-bar--top {
    top: 0;
    transition: transform 0.25s ease 0.25s;
  }

  .navbar__burger-bar--top span,
  .navbar__burger-bar--bottom span {
    display: block;
    width: 100%;
    height: 100%;
    background: currentColor;
    transition: transform 0.25s ease 0s;
  }

  .navbar__burger-bar--mid {
    top: 6px;
    background: currentColor;
    transition: opacity 0.2s ease 0.2s;
  }

  .navbar__burger-bar--bottom {
    top: 12px;
    transition: transform 0.25s ease 0.25s;
  }

  /* Opening: bars converge to the middle first, then the outer two rotate into an X */
  .navbar__burger--open .navbar__burger-bar--top {
    transform: translateY(6px);
    transition-delay: 0s;
  }

  .navbar__burger--open .navbar__burger-bar--top span {
    transform: rotate(45deg);
    transition-delay: 0.25s;
  }

  .navbar__burger--open .navbar__burger-bar--bottom {
    transform: translateY(-6px);
    transition-delay: 0s;
  }

  .navbar__burger--open .navbar__burger-bar--bottom span {
    transform: rotate(-45deg);
    transition-delay: 0.25s;
  }

  .navbar__burger--open .navbar__burger-bar--mid {
    opacity: 0;
    transition-delay: 0.15s;
  }

  /* Phones only: tablet keeps the desktop bar. */
  @media (max-width: 640px) {
    .navbar { height: 96px; gap: 12px; }
    .navbar__links { gap: 14px; }
    .navbar__link { font-size: 13px; }
    .navbar__logo { width: 96px; height: 96px; padding: 22px; }
    .navbar__logo-lottie { inset: 22px; }
  }

  @media (max-width: 640px) {
    .navbar__links { display: none; }
    .navbar__burger {
      display: block;
      position: absolute;
      right: 16px;
      top: 50%;
      transform: translateY(-50%);
    }
  }

  /* Mobile burger menu: the bar itself is the persistent .navbar (it just
     recolors via .navbar--menu-open below); only the panel underneath it
     slides down from behind the bar's bottom edge. */

  .navbar--menu-open {
    --nav-bg: var(--bb-burgundy);
    --nav-text: var(--bb-off-white);
  }

  .mobile-menu {
    position: fixed;
    top: 96px;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 90;
    display: flex;
    flex-direction: column;
    background: var(--bb-burgundy);
    opacity: 0;
    visibility: hidden;
    transition: opacity 0.5s cubic-bezier(0.4, 0, 0.2, 1), visibility 0s linear 0.5s;
  }

  .mobile-menu--open {
    opacity: 1;
    visibility: visible;
    transition: opacity 0.5s cubic-bezier(0.4, 0, 0.2, 1);
  }

  .mobile-menu__body {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 52px;
    flex-shrink: 0;
    padding: 40px var(--page-margin) 32px;
  }

  .mobile-menu__links {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 20px;
  }

  .mobile-menu__link {
    font-family: "STIX Two Text", Georgia, "Times New Roman", serif;
    font-weight: 400;
    font-style: italic;
    font-size: 28px;
    line-height: 1.3;
    letter-spacing: 0.28px;
    color: var(--bb-off-white);
    text-decoration: none;
  }

  .mobile-menu__contact {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 8px;
    text-align: center;
  }

  .mobile-menu__contact-label,
  .mobile-menu__contact-email {
    font-family: "Haas Recast VF", "Helvetica Neue", Arial, sans-serif;
    font-weight: 400;
    font-variation-settings: 'TRAK' 0;
    font-size: 15px;
    line-height: 1.3;
  }

  .mobile-menu__contact-label { color: var(--bb-pink); }
  .mobile-menu__contact-email { color: var(--bb-white); }

  .mobile-menu__pattern {
    flex: 1;
    min-height: 0;
    background-repeat: repeat;
  }

  .mobile-menu__pattern--pink-orange {
    background-image: url("../images/menu/menu-tile-pink-orange.png");
    background-size: 49px 121.5px;
    animation: menuPatternScroll-pinkOrange 12.5s linear infinite;
  }

  .mobile-menu__pattern--sky-navy {
    background-image: url("../images/menu/menu-tile-sky-navy.png");
    background-size: 33.5px 111.5px;
    animation: menuPatternScroll-skyNavy 12.5s linear infinite;
  }

  .mobile-menu__pattern--cream-red {
    background-image: url("../images/menu/menu-tile-cream-red.png");
    background-size: 56px 111.5px;
    animation: menuPatternScroll-creamRed 12.5s linear infinite;
  }

  .mobile-menu__pattern--green-navy {
    background-image: url("../images/menu/menu-tile-green-navy.png");
    background-size: 48px 96px;
    animation: menuPatternScroll-greenNavy 12.5s linear infinite;
  }

  /* Each keyframe scrolls by exactly one tile width so the loop is seamless */
  @keyframes menuPatternScroll-pinkOrange {
    from { background-position: 0 0; }
    to { background-position: 49px 0; }
  }

  @keyframes menuPatternScroll-skyNavy {
    from { background-position: 0 0; }
    to { background-position: 33.5px 0; }
  }

  @keyframes menuPatternScroll-creamRed {
    from { background-position: 0 0; }
    to { background-position: 56px 0; }
  }

  @keyframes menuPatternScroll-greenNavy {
    from { background-position: 0 0; }
    to { background-position: 48px 0; }
  }

  @media (prefers-reduced-motion: reduce) {
    .mobile-menu__pattern { animation: none; }
  }

  .hero {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: flex-end;
    padding-left: var(--page-margin);
    padding-right: var(--page-margin);
    width: 100%;
    /* A floor, not a fixed height, with the nav's 139px as top padding:
       in a short window (a 1278x640 laptop viewport put the homepage
       headline under the nav) the hero grows rather than letting its
       bottom-aligned content climb behind the navbar. */
    min-height: 80vh;
    padding-top: 139px;
  }

  /* The comp's 52px gaps at 900px tall and up, tightening with the
     window's height (5.8vh is 52px at 900) to no less than 24px, the
     phone gap. */
  .hero__content {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: clamp(24px, 5.8vh, 52px);
    width: 100%;
    padding-bottom: clamp(24px, 5.8vh, 52px);
  }

  .hero__heading {
    font-family: "Haas Recast VF", "Helvetica Neue", Arial, sans-serif;
    font-weight: 400;
    font-variation-settings: 'TRAK' -35;
    font-style: normal;
    /* The comp's 102px at 1440x900, scaled down by whichever of the
       window's width or height runs out first, but never below 72px: in
       a shorter window than that allows, the hero grows past its 80vh
       floor instead (see .hero). 7.1vw keeps the nowrap line inside the
       page margins. Tracking is the comp's -2.04px as a proportion, so
       it scales with the size. */
    font-size: clamp(72px, min(7.1vw, 11.4vh), 102px);
    line-height: 0.93;
    letter-spacing: -0.02em;
    color: var(--bb-black);
    text-align: center;
    white-space: nowrap;
    text-wrap: balance;
  }

  .hero__subcopy {
    font-family: "Haas Recast VF", "Helvetica Neue", Arial, sans-serif;
    font-weight: 400;
    font-variation-settings: 'TRAK' 0;
    font-style: normal;
    font-size: 26px;
    line-height: 1.28;
    letter-spacing: -0.36px;
    color: var(--bb-black);
    text-align: center;
    max-width: 560px;
  }

  /* Phones only (see the 640px block below): the homepage subcopy's
     last phrase takes its own line, so it breaks before "to find out
     why" rather than wherever the 310px measure falls. A block span,
     not a <br> hidden on desktop: SplitText turns every <br> into a
     line break whatever its display, so a hidden one still broke the
     sentence on desktop. */
  /* Temporary placeholder page header — swap out once real page designs land. */
  .placeholder-hero {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 100%;
    min-height: 80vh;
    margin: 0 auto;
    padding: 139px var(--page-margin) 0;
    text-align: center;
  }

  .placeholder-hero__heading {
    font-family: "Haas Recast VF", "Helvetica Neue", Arial, sans-serif;
    font-weight: 400;
    font-variation-settings: 'TRAK' -35;
    font-style: normal;
    font-size: 102px;
    line-height: 0.93;
    letter-spacing: -2.04px;
    color: var(--bb-black);
  }

  @media (max-width: 768px) {
    .placeholder-hero__heading { font-size: 56px; }
  }

  .cta {
    display: flex;
    align-items: center;
    gap: 6px;
    text-decoration: none;
    cursor: pointer;
  }

  .cta__label {
    font-family: "STIX Two Text", Georgia, "Times New Roman", serif;
    font-weight: 400;
    font-style: italic;
    font-size: 18px;
    line-height: 0.97;
    letter-spacing: -0.36px;
    color: inherit;
  }

  .cta__icon {
    position: relative;
    width: 32px;
    height: 32px;
    flex-shrink: 0;
    overflow: hidden;
  }

  .cta__icon-box {
    position: absolute;
    top: 50%;
    left: 50%;
    width: 22px;
    height: 22px;
    background: var(--bb-burgundy);
    transform: translate(-50%, -50%) rotate(0deg);
    transition: transform 0.4s ease, background-color 0.3s ease;
  }

  .cta__icon-glyph {
    position: absolute;
    top: 50%;
    left: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 20px;
    height: 20px;
    transform: translate(-50%, -50%);
  }

  .cta__icon-glyph svg {
    width: 13px;
    height: 10.83px;
  }

  .cta__icon-glyph svg path {
    stroke: var(--bb-sky);
  }

  /* Only the square turns on hover; the arrow holds still over it. */
  .cta:hover .cta__icon-box {
    transform: translate(-50%, -50%) rotate(180deg);
  }

  /* On dark backgrounds the icon swaps to a light-blue box with a burgundy glyph */
  .cta--on-dark .cta__icon-box { background: var(--bb-sky); }
  .cta--on-dark .cta__icon-glyph svg path { stroke: var(--bb-burgundy); }

  /* Download: the same CTA with its arrow pointing down. */
  .cta--download .cta__icon-glyph {
    transform: translate(-50%, -50%) rotate(90deg);
  }

  /* External link (Figma 2384:1671, Button_ExternalIcon): the same CTA
     with its arrow turned 45 degrees to point up and right, for any link
     that leaves the site. Only the angle: each keeps the box and glyph
     colours of where it sits (pink and burgundy on the team's LinkedIn
     links, see .team__linkedin; sky on The Combine and Tadiem). */
  .cta--external .cta__icon-glyph {
    transform: translate(-50%, -50%) rotate(-45deg);
  }

  /* Download + Share row closing the survey article (Figma 2331:4025).
     Both icons are the design's 48px here rather than the site CTA's 32,
     so the Download icon is scaled up to sit level with Share's. The
     container stops clipping so the square's hover turn isn't cut. */
  .cs-actions {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 32px 72px;
  }

  .cs-actions .cta { gap: 23px; }
  .cs-actions .cta__icon { width: 48px; height: 48px; overflow: visible; }
  .cs-actions .cta__icon-box { width: 48px; height: 48px; }
  .cs-actions .cta__icon-glyph { width: 29px; height: 29px; }
  .cs-actions .cta__icon-glyph svg { width: 20px; height: 16.7px; }

  /* Share: a sky 48px box with the share glyph. Pressed, the box turns
     burgundy with a sky close glyph and a sky tray opens to its right
     holding LinkedIn and Copy link, the whole strip 151px as drawn. The
     tray is visibility: hidden while shut so its two controls leave the
     tab order, and waits for the width to finish before hiding. Closed
     styling applies only once initShare() has run (.share--ready);
     without script the tray simply stays open. */
  .share {
    display: flex;
    align-items: center;
  }

  .share__toggle {
    display: flex;
    align-items: center;
    gap: 32px;
    padding: 0;
    border: 0;
    background: none;
    color: inherit;
    cursor: pointer;
  }

  /* The square is the ::before, not the box itself, so it can turn on
     hover the way every CTA's does while the glyph above it holds still. */
  .share__box {
    position: relative;
    width: 48px;
    height: 48px;
  }

  .share__box::before {
    content: "";
    position: absolute;
    inset: 0;
    background: var(--bb-sky);
    transition: transform 0.4s ease, background-color 0.3s ease;
  }

  /* Turned on hover, and held turned while the tray is open even once
     the pointer has left. Closing turns it back at once, pointer or not:
     .share--settling (initShare) holds off the hover turn until the
     pointer next leaves the button. */
  .share__toggle:hover .share__box::before,
  .share--open .share__box::before { transform: rotate(180deg); }
  .share--settling:not(.share--open) .share__toggle:hover .share__box::before { transform: none; }
  /* On close the turn back waits for the tray to finish folding away
     (its 0.3s width transition), so the two don't happen at once. */
  .share--closing .share__box::before {
    transition: transform 0.4s ease 0.3s, background-color 0.3s ease;
  }

  .share__glyph {
    position: absolute;
    inset: 0;
    width: 48px;
    height: 48px;
  }

  .share__glyph--open path { fill: var(--bb-burgundy); }
  .share__glyph--close path { fill: var(--bb-sky); }
  .share__glyph--close { visibility: hidden; }

  .share__toggle[aria-expanded="true"] .share__box::before { background: var(--bb-burgundy); }
  .share__toggle[aria-expanded="true"] .share__glyph--open { visibility: hidden; }
  .share__toggle[aria-expanded="true"] .share__glyph--close { visibility: visible; }

  .share__tray {
    display: flex;
    align-items: center;
    height: 48px;
    padding: 0 7px 0 10px;
    background: var(--bb-sky);
    color: var(--bb-burgundy);
    overflow: hidden;
    white-space: nowrap;
  }

  .share--ready .share__tray {
    width: 0;
    padding: 0;
    visibility: hidden;
    transition: width 0.3s ease, padding 0.3s ease, visibility 0s linear 0.3s;
  }

  .share--open .share__tray {
    width: 103px;
    padding: 0 7px 0 10px;
    visibility: visible;
    transition: width 0.3s ease, padding 0.3s ease, visibility 0s;
  }

  /* 43 x 48 each, so the icons land where the design has them and both
     targets clear 24px. The ring is drawn inside, since the tray clips. */
  .share__option {
    display: flex;
    flex-shrink: 0;
    align-items: center;
    justify-content: center;
    width: 43px;
    height: 48px;
    padding: 0;
    border: 0;
    background: none;
    color: inherit;
    cursor: pointer;
  }

  .share__option:focus-visible { outline-offset: -4px; }

  /* Copy link's confirmation: the tick replaces the link glyph, and the
     note sits under the strip, right-aligned to it, for two seconds
     (initShare). Black at 60% on Off-white, 5.5:1. It is status text,
     not hover content, so 1.4.13 does not apply; it is aria-hidden
     because the role="status" line already announces it. */
  .share { position: relative; }
  .share__copied-glyph { display: none; }
  .share__option--copied .share__copy-glyph { display: none; }
  .share__option--copied .share__copied-glyph { display: block; }

  .share__note {
    position: absolute;
    top: calc(100% + 8px);
    right: 0;
    font-family: "Haas Recast VF", "Helvetica Neue", Arial, sans-serif;
    font-variation-settings: 'TRAK' 0;
    font-size: 14px;
    line-height: 1.2;
    white-space: nowrap;
    color: rgba(var(--bb-black-rgb), 0.6);
    opacity: 0;
    transition: opacity 0.2s ease;
    pointer-events: none;
  }

  .share--noted .share__note { opacity: 1; }
  .share__option svg path { fill: currentColor; }
  .share__option[data-share-copy] svg path { fill: none; stroke: currentColor; }

  @media (prefers-reduced-motion: reduce) {
    .share--ready .share__tray,
    .share--open .share__tray,
    .share__box::before,
    .share--closing .share__box::before,
    .share__note { transition: none; }
  }

  @media (max-width: 1200px) {
    .hero__heading { font-size: 64px; white-space: normal; }
  }

  @media (max-width: 640px) {
    .hero__heading { font-size: 36px; letter-spacing: -0.72px; }
    .hero__subcopy { font-size: 18px; max-width: 310px; }
    .hero__subcopy-tail { display: block; }
    .hero__content { gap: 24px; }
  }

  /* "Unlock growth." never splits; it used an inline style until the
     phone rule below needed to reach it. */
  .hero__heading-tail { white-space: nowrap; }

  /* Homepage, phones: the same three lines of headline and three of
     subcopy on every phone. Each line is marked in the markup (the <br>s
     and the -tail spans, which go block here) and none can wrap, so the
     browser never chooses a break; Safari sets this type a touch wider
     than Chrome and was pushing the subcopy onto a fourth line on an
     iPhone 12. The size follows the screen width instead: the comp's 36px
     and 18px at 390 wide (9.23vw, 4.6vw), clamped so a very small or
     large phone stays readable. The longest line is about 62% of the
     screen at every width, which leaves Safari plenty of room. */
  @media (max-width: 640px) {
    .hero--home .hero__heading {
      font-size: clamp(28px, 9.23vw, 40px);
      letter-spacing: -0.02em;
      white-space: nowrap;
    }
    .hero--home .hero__heading-tail { display: block; }
    .hero--home .hero__subcopy {
      font-size: clamp(15px, 4.6vw, 20px);
      max-width: none;
      white-space: nowrap;
    }
  }

  .featured {
    --featured-bg: var(--bb-off-white);
    position: relative;
    width: 100%;
    height: 120vh;
    overflow: hidden;
    background: var(--featured-bg);
    transition: background-color 0.6s ease;
    /* Grid purely to centre the copy on the section's own midline. The
       background image and the step wipe are both absolutely positioned,
       so .featured__content is the only item this lays out. Centring it
       here is what makes "halfway through the section" and "copy across
       the middle of the screen" the same moment — which is the anchor
       the copy's parallax is measured from (see initFeaturedParallax). */
    display: grid;
    align-content: center;
  }

  .featured__bg {
    position: absolute;
    top: -8%;
    left: 0;
    width: 100%;
    height: 116%;
    object-fit: cover;
    object-position: center top;
  }

  /* Portrait screens get the portrait cut (see the <picture> in
     index.html), which on a phone is still wider than the ~1:3 box.
     Centred, the crop took ~130px of image off each side and cut into
     the Audi's grille; weighted left, the front of the car stays whole
     and the trim comes off the door side instead. Same media query as
     the <picture>, so the two can't disagree. Tuned to that image:
     retune if the featured case changes. */
  @media (orientation: portrait) {
    .featured__bg { object-position: 25% top; }
  }

  /* Outer layout div: full width, the site's standard side gutter. */
  .featured__content {
    position: relative;
    z-index: 1;
    width: 100%;
    padding-left: var(--page-margin);
    padding-right: var(--page-margin);
    color: var(--bb-white);
  }

  /* Inner div: capped width to keep the copy a narrow, centered column
     within the padded layout div above. */
  .featured__inner {
    position: relative;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 32px;
    width: 100%;
    max-width: 700px;
    margin: 0 auto;
  }

  /* Soft black halo behind the copy so the white type holds contrast over
     the case-study image at every breakpoint. */
  .featured__inner::before {
    content: '';
    position: absolute;
    inset: 0;
    z-index: -1;
    background: var(--bb-black);
    opacity: 0.5;
    filter: blur(100px);
    pointer-events: none;
  }

  .featured__heading {
    font-family: "Haas Recast VF", "Helvetica Neue", Arial, sans-serif;
    font-weight: 400;
    font-variation-settings: 'TRAK' -35;
    font-style: normal;
    font-size: 52px;
    line-height: 0.93;
    letter-spacing: -0.52px;
    color: var(--bb-white);
    text-align: center;
    /* Even lines rather than a long first line and a short tail. */
    text-wrap: balance;
  }

  .featured__subcopy {
    font-family: "Haas Recast VF", "Helvetica Neue", Arial, sans-serif;
    font-weight: 400;
    font-variation-settings: 'TRAK' 0;
    font-style: normal;
    font-size: 18px;
    line-height: 1.22;
    color: var(--bb-white);
    text-align: center;
    text-wrap: balance;
  }

  .featured__steps {
    position: absolute;
    inset: 0;
    z-index: 2;
    pointer-events: none;
  }

  .featured__steps svg {
    display: block;
    width: 100%;
    height: 100%;
  }

  /* The Lottie source draws its bars in white; recolor them to match the
     section's (dynamic) background so they read as the section covering
     itself rather than a white overlay. !important beats the fill lottie-web
     writes inline on each shape. */
  .featured__steps svg path,
  .featured__steps svg rect {
    fill: var(--featured-bg) !important;
    transition: fill 0.6s ease, stroke 0.6s ease;
    /* The bars are stretched to the section (preserveAspectRatio none), so
       their shared edges land on fractional pixels and anti-alias to a
       hairline gap that shows the photo through: vertical lines up the
       wipe, clearest on iPhone. A 1px stroke in the same colour, kept 1px
       by non-scaling-stroke whatever the stretch, overlaps each seam. */
    stroke: var(--featured-bg) !important;
    stroke-width: 1px;
    vector-effect: non-scaling-stroke;
  }

  @media (max-width: 1200px) {
    .featured__inner { max-width: 600px; }
    .featured__heading { font-size: 40px; letter-spacing: -0.4px; }
  }

  @media (max-width: 640px) {
    .featured__inner { gap: 24px; align-items: flex-start; }
    .featured__heading { font-size: 32px; letter-spacing: -0.32px; text-align: left; }
    .featured__subcopy { font-size: 16px; text-align: left; }
  }

  /* Cascade transition: a standalone section between clients and the
     portfolio grid. The section's own background is the fixed
     burgundy/light-blue stripe image — it never moves. On top sit 9 rows,
     each a 900vw-wide strip of three panels: navy (matching the clients
     section, 100vw), transparent (a 700vw window onto the fixed image
     below), and cream (matching the portfolio grid, 100vw). At rest each
     row shows its navy panel; scrolling slides every row right-to-left
     (eased, scrubbed to scroll), staggered so the bottom row moves first
     and each row above follows — sweeping navy -> reveal -> cream in a
     cascading stair pattern, driven by GSAP + ScrollTrigger. */
  .cascade-transition {
    position: relative;
    width: 100%;
    height: 401px;
    overflow: hidden;
    background-image: url("../images/burgundy-lightblue-pattern-tile.png");
    background-repeat: repeat;
    background-size: 120px 18px;
  }

  .cascade-transition__row {
    position: absolute;
    left: 0;
    width: 900vw;
    display: flex;
  }

  .cascade-transition__rect {
    height: 100%;
    flex-shrink: 0;
  }

  .cascade-transition__rect--prev {
    width: 100vw;
    background: var(--bb-navy);
  }

  .cascade-transition__rect--transparent {
    width: 700vw;
    background: transparent;
  }

  .cascade-transition__rect--next {
    width: 100vw;
    background: var(--bb-off-white);
  }

  @media (max-width: 1200px) {
    .cascade-transition { height: 300px; }
  }

  @media (max-width: 640px) {
    .cascade-transition { height: 180px; }
  }

  /* Four pillars: sits between the featured section and the client list. Cream
     by default; once 40% of the section has scrolled into view it settles
     into a burgundy resting state (--pillars-* vars driven by JS via the
     .is-<pillar> class on .pillars). Hovering/focusing a word re-themes
     the whole section to that pillar's colour and cross-fades its image.
     Layout is a 12-column grid: "Find your" (2 cols) | the four words +
     CTA (5 cols) | image (4 cols) | one blank col on the right — fixed
     across every state. Only colour and the "Find your" node's vertical
     position change: it stays in its own column and GSAP tweens its Y
     offset to line up with whichever word is active (initPillars() in
     main.js measures the target row's position each time). Below 900px
     the grid collapses to a single stacked column and "Find your" is
     simply moved inline before the active word instead. The chosen
     colour never reverts on mouseleave, and is handed down to the client
     list section (and the peekaboo nav) directly below it. */
  .pillars {
    --pillars-bg: var(--bb-off-white);
    /* One token for the word/prefix type size so the hover arrow (sized in
       multiples of it, see .pillars__arrow) stays in proportion at every
       breakpoint. */
    --pillars-type-size: 52px;
    --pillars-fg: var(--bb-black);
    --pillars-accent: var(--bb-burgundy);
    position: relative;
    width: 100%;
    background: var(--pillars-bg);
    color: var(--pillars-fg);
    transition: background-color 0.6s ease, color 0.6s ease;
    padding: 140px var(--page-margin);
    /* Pulls up 1px to overlap the featured section's bottom edge — Lenis's
       transform-driven scroll can leave a sub-pixel seam between adjacent
       sections where the featured section's own background image shows through
       for an instant; this section's opaque background covers it. */
    margin-top: -1px;
    overflow: hidden;
  }

  /* Each pillar pairs its background with a contrasting brand colour for
     the type (orange on burgundy, pink on green, sky blue on red, off
     white on navy). --pillars-accent stays tied to the background: it
     paints the CTA arrow, which sits on a sky blue box and so reads as a
     knockout of the section colour. */
  .pillars.is-opportunity { --pillars-bg: var(--bb-burgundy); --pillars-fg: var(--bb-orange); --pillars-accent: var(--bb-burgundy); }
  .pillars.is-comp-advantage { --pillars-bg: var(--bb-forest); --pillars-fg: var(--bb-pink); --pillars-accent: var(--bb-forest); }
  .pillars.is-expression { --pillars-bg: var(--bb-red); --pillars-fg: var(--bb-sky); --pillars-accent: var(--bb-red); }
  .pillars.is-presence { --pillars-bg: var(--bb-navy); --pillars-fg: var(--bb-off-white); --pillars-accent: var(--bb-navy); }

  /* Grid matches the rest of the site's (see .portfolio-row): 12 columns,
     16px gap, no max-width — full-bleed within the section's own 20px
     side gutter above. */
  .pillars__inner {
    position: relative;
    display: grid;
    grid-template-columns: repeat(var(--grid-cols-desktop), 1fr);
    gap: var(--grid-gap);
    align-items: start;
    width: 100%;
  }

  .pillars__prefix-col {
    grid-column: 1 / span 2;
    position: relative;
  }

  /* Position rules are scoped to whichever parent currently holds the
     node (see positionPrefix() in main.js): pinned top-right of its own
     grid column on desktop (so it reads right up against the active
     word), plain inline flow with a trailing gap when it's DOM-moved in
     front of the active word on mobile. */
  .pillars__prefix-col .pillars__prefix {
    position: absolute;
    top: 0;
    right: 0;
    margin-right: -6px;
    text-align: right;
    will-change: transform;
  }

  .pillars__item-link .pillars__prefix {
    margin-right: 0.25em;
  }

  .pillars__prefix {
    white-space: nowrap;
  }

  .pillars__main {
    grid-column: 3 / span 5;
  }

  /* align-items, so each row shrinks to its own word rather than
     stretching to the width of the longest one ("competitive advantage").
     The items are links now (see index.html) — a stretched row would make
     the dead space to the right of a short word like "presence" navigate,
     and would draw the focus ring around that whole empty band. */
  .pillars__list {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 15px;
    list-style: none;
  }

  .pillars__item { opacity: 0.3; transition: opacity 0.5s ease; }
  .pillars__item--opportunity { opacity: 0.4; }
  .pillars__item.is-active { opacity: 1; }

  /* An <a>, not a <button> (see index.html) — so it also needs the
     underline reset that the button never did. */
  .pillars__item-link {
    display: flex;
    align-items: baseline;
    background: none;
    border: 0;
    margin: 0;
    padding: 0;
    cursor: pointer;
    color: inherit;
    font: inherit;
    text-decoration: none;
  }

  /* A pillar that isn't the active one sits at opacity 0.3, and focusing
     one only lifts that while the section has settled (see initPillars) —
     so keyboard focus needs a ring of its own to be findable at all. The
     offset keeps it clear of the type's descenders. */
  .pillars__item-link:focus-visible {
    outline: 2px solid currentColor;
    outline-offset: 6px;
  }

  .pillars__prefix,
  .pillars__word {
    font-family: "Haas Recast VF", "Helvetica Neue", Arial, sans-serif;
    font-weight: 400;
    font-variation-settings: 'TRAK' -35;
    font-style: normal;
    font-size: var(--pillars-type-size);
    line-height: 0.97;
    letter-spacing: -0.26px;
    white-space: nowrap;
    color: inherit;
  }

  /* Trailing arrow on the hovered word (Figma "Capability pillars" hover
     states). Desktop shows it only while the pointer is actually on the
     word — unlike the section colour and the "Find your" tracker, which
     stay on the last-hovered pillar, this reverts on mouseleave. Touch
     has no hover, so there it rides the .is-active class initPillars()
     sets on tap instead; the width query mirrors the section's own
     tablet/mobile breakpoint and the (hover: none) query catches touch
     devices wide enough to miss it. It's always in the DOM at opacity 0
     so revealing it never reflows the word list (which "Find your"
     measures its offset against). Sized off --pillars-type-size rather
     than em because the button inherits the body font size, not the
     word's. The button is baseline-aligned and this box has no text of
     its own, so its bottom edge lands on the word's baseline. */
  .pillars__arrow {
    /* Figma draws the arrow as a 36x30 box at 52px type, its top on the
       word's ascender — hence the two ratios below, plus an 8px gap off
       the final glyph. --pillars-arrow-lift raises it off the baseline
       the flex row aligns it to, by the difference between the ascender
       (0.719em for Haas Recast) and the box (0.577em). */
    --pillars-arrow-lift: calc(var(--pillars-type-size) * -0.142);
    --pillars-arrow-slide: calc(var(--pillars-type-size) * -0.12);
    display: block;
    flex: none;
    width: calc(var(--pillars-type-size) * 0.692);
    margin-left: 8px;
    color: inherit;
    opacity: 0;
    transform: translate(var(--pillars-arrow-slide), var(--pillars-arrow-lift));
    transition: opacity 0.4s ease, transform 0.4s ease;
  }

  .pillars__arrow svg {
    display: block;
    width: 100%;
    height: auto;
  }

  /* Desktop: a true hover affordance, keyboard users get it on focus. */
  @media (min-width: 901px) {
    .pillars__item-link:hover .pillars__arrow,
    .pillars__item-link:focus-visible .pillars__arrow {
      opacity: 1;
      --pillars-arrow-slide: 0px;
    }
  }

  /* Tablet/mobile, and any touch device: the tapped word carries it. */
  @media (max-width: 900px) {
    .pillars__item.is-active .pillars__arrow { opacity: 1; --pillars-arrow-slide: 0px; }
  }

  @media (hover: none) {
    .pillars__item.is-active .pillars__arrow { opacity: 1; --pillars-arrow-slide: 0px; }
  }

  @media (prefers-reduced-motion: reduce) {
    .pillars__arrow { transition: opacity 0.4s ease; --pillars-arrow-slide: 0px; }
  }

  .pillars__cta { color: inherit; }

  /* White on every pillar colour, rather than taking the section's
     re-themed type colour on hover. Only once a pillar class is on: the
     cream resting state before the section scrolls in keeps black, since
     white on off-white would all but vanish. White clears AA on all four
     backgrounds (red, the lowest, at 10:1). */
  .pillars.is-opportunity .pillars__cta,
  .pillars.is-comp-advantage .pillars__cta,
  .pillars.is-expression .pillars__cta,
  .pillars.is-presence .pillars__cta {
    color: var(--bb-white);
  }
  .pillars__cta .cta__icon-box { background: var(--bb-sky); }
  .pillars__cta .cta__icon-glyph svg path { stroke: var(--pillars-accent); transition: stroke 0.5s ease; }

  .pillars__media {
    grid-column: 8 / span 4;
    position: relative;
  }

  .pillars__media-inner {
    position: relative;
    width: 100%;
    aspect-ratio: 1 / 1;
    overflow: hidden;
  }

  .pillars__image {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
    opacity: 0;
    transition: opacity 0.8s ease;
  }

  .pillars__image.is-active { opacity: 1; }

  .pillars__logo {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    width: 55%;
    height: 55%;
    color: inherit;
    transition: color 0.5s ease;
    pointer-events: none;
  }

  /* Desktop only (Figma "Capability pillars"): the word list is centred
     against the square image on the right and the CTA hangs off the
     image's bottom edge. Both are placed in the same single cell of
     .pillars__main so the list centres on the FULL column height — i.e.
     on the image — rather than on whatever is left above the CTA. The
     row height comes from the image (align-items: stretch lets
     .pillars__main fill it), and the list is far shorter than the image
     at every desktop width, so the two never collide. Below 901px
     .pillars__main becomes display:contents and this is all moot. */
  @media (min-width: 901px) {
    .pillars__inner { align-items: stretch; }

    .pillars__main {
      display: grid;
      grid-template-columns: minmax(0, 1fr);
      grid-template-rows: minmax(0, 1fr);
    }

    .pillars__list { grid-area: 1 / 1; align-self: center; justify-self: start; }
    .pillars__cta { grid-area: 1 / 1; align-self: end; justify-self: start; }
  }

  @media (max-width: 1200px) {
    .pillars { padding: 100px var(--page-margin); }
    .pillars { --pillars-type-size: 40px; }
    .pillars__prefix, .pillars__word { letter-spacing: -0.2px; }
  }

  /* Tablet AND mobile (Figma "Capability Pillars - Tablet" / "Mobile /
     Capability pillars"): this section is a deliberate exception to the
     site's normal mobile grid — it keeps tablet's 8-column layout all
     the way down rather than collapsing further. "Find your" keeps its
     own right-aligned, Y-tweened track (span 2, same mechanism as
     desktop; see positionPrefix() in main.js) and the word list takes
     the remaining 6. The image and CTA no longer fit beside that in row
     1, so grid auto-placement drops each into its own full-width row
     below, centred. .pillars__main goes display:contents so its two
     children (the list and the CTA, still nested under it in the markup)
     can be placed independently this way. */
  @media (max-width: 900px) {
    .pillars__inner { grid-template-columns: repeat(var(--grid-cols-tablet), 1fr); }
    .pillars__main { display: contents; }
    /* Explicit order: grid auto-placement otherwise walks DOM order, and
       the CTA precedes .pillars__media in the markup (nested inside
       .pillars__main, ahead of its .pillars__media sibling) — without
       this it would land above the image instead of below it. */
    .pillars__list { grid-column: 3 / span 6; order: 0; }
    .pillars__media { grid-column: 1 / -1; order: 1; justify-self: center; width: 524px; max-width: 100%; margin-top: 60px; }
    .pillars__cta { grid-column: 1 / -1; order: 2; justify-self: center; margin-top: 60px; }
    .pillars__prefix-col .pillars__prefix { margin-right: -7px; }
  }

  /* Mobile only: same 8-column layout as tablet, just smaller type, and
     the image fills the full width instead of sitting at a fixed centred
     size. */
  @media (max-width: 640px) {
    .pillars { padding: 64px var(--page-margin); }
    .pillars__media { width: 100%; }
    .pillars { --pillars-type-size: 24px; }
    .pillars__list { gap: 7px; }
    .pillars__prefix, .pillars__word { letter-spacing: -0.12px; }
    .pillars__prefix-col .pillars__prefix { margin-right: -12px; }
  }

  .clients {
    position: relative;
    width: 100%;
    height: 510px;
    background: var(--clients-bg, var(--bb-navy));
    color: var(--clients-fg, var(--bb-sky));
    transition: background-color 0.6s ease, color 0.6s ease;
    overflow: hidden;
  }

  /* Colour handed down from .pillars (see initPillars() in main.js) once
     the visitor scrolls past it — same state classes, same hex values.
     Keep these in step with the .pillars.is-* rules above and with THEMES
     in main.js, which feeds the same pair to the peekaboo nav. */
  .clients.is-opportunity { --clients-bg: var(--bb-burgundy); --clients-fg: var(--bb-orange); }
  .clients.is-comp-advantage { --clients-bg: var(--bb-forest); --clients-fg: var(--bb-pink); }
  .clients.is-expression { --clients-bg: var(--bb-red); --clients-fg: var(--bb-sky); }
  .clients.is-presence { --clients-bg: var(--bb-navy); --clients-fg: var(--bb-off-white); }

  .clients__inner {
    position: absolute;
    top: 0;
    left: 20px;
    right: 20px;
    bottom: 0;
    display: flex;
    gap: 16px;
    justify-content: center;
  }

  .clients__label-col {
    display: flex;
    align-items: center;
    height: 100%;
    flex-shrink: 0;
    width: 220px;
  }

  .clients__label {
    font-family: "Haas Recast VF", "Helvetica Neue", Arial, sans-serif;
    font-weight: 600;
    font-variation-settings: 'TRAK' 0;
    font-style: normal;
    font-size: 20px;
    line-height: 0.97;
    letter-spacing: 0.1px;
    color: var(--clients-fg, var(--bb-sky));
    transition: color 0.6s ease;
  }

  .clients__viewport {
    position: relative;
    flex: 1;
    height: 100%;
    overflow: hidden;
  }

  .clients__list {
    position: absolute;
    top: 54px;
    left: 0;
    width: 100%;
    display: flex;
    flex-direction: column;
    gap: 16px;
    padding-top: 25vh;
    padding-bottom: 25vh;
    will-change: transform;
  }

  .clients__name {
    font-family: "Haas Recast VF", "Helvetica Neue", Arial, sans-serif;
    font-weight: 400;
    font-variation-settings: 'TRAK' -35;
    font-style: normal;
    font-size: 90px;
    line-height: 0.97;
    letter-spacing: -1.8px;
    color: var(--clients-fg, var(--bb-sky));
    white-space: nowrap;
    transition: color 0.6s ease;
  }

  @media (max-width: 1200px) {
    .clients { height: 380px; }
    .clients__inner { top: 0; bottom: 0; }
    .clients__name { font-size: 60px; letter-spacing: -1.2px; }
  }

  @media (max-width: 640px) {
    .clients { height: 170px; }
    .clients__inner { flex-direction: column; top: 30px; bottom: 25vh; gap: 24px; }
    .clients__label-col { width: 100%; height: auto; align-items: flex-start; justify-content: center; }
    .clients__label { text-align: center; }
    /* The top fade that used to live here is now part of the shared
       top-and-bottom fade in cascade.css, which applies at every
       breakpoint rather than mobile alone. */
    .clients__list { align-items: center; padding-top: 10vh; padding-bottom: 10vh; }
    .clients__name { font-size: 34px; letter-spacing: -0.68px; }
    .clients__inner { left: 16px; right: 16px; }
  }

  /* Portfolio grid */

  /* Portfolio filters (Osmo Supply "Multi Filter Setup (Multi Match)":
     multiple pillar buttons can be active together with OR logic — see
     initPortfolioFilters() in main.js). Adapted only in how a filtered-
     out project leaves the layout: the source resource takes a
     not-active item out of a flex-wrap list with position:absolute, but
     here every project sits directly in one flat CSS Grid
     (.portfolio-grid, sized by grid-column:span per card) —
     position:absolute would escape that grid context instead of just
     freeing its cell, so not-active items go display:none once fully
     faded and let the grid's remaining projects reflow via ordinary
     auto-placement. */

  .portfolio-filters {
    display: flex;
    flex-direction: column;
    gap: 24px;
  }

  .portfolio-filters__list {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
  }

  /* The filter bar's pills are .tag .tag--filter — see the .tag block
     further down for the shape and the modifier that makes them a
     button. Nothing pill-shaped defines its own metrics any more. */

  .portfolio-filters__rule {
    display: block;
    width: 100%;
    height: 1px;
    background: rgba(var(--bb-black-rgb), 0.15);
  }

  /* The fade itself (a plain, simultaneous opacity tween — no
     scale/slide, no stagger) is driven directly in JS by
     initPortfolioFilters(), not by a CSS transition on this attribute —
     see paint() in main.js. */
  .p-card[data-filter-status="not-active"] {
    display: none;
  }

  /* .portfolio is just the section shell (background/padding) plus a
     tight flex stack of the filters and the grid — never a grid itself,
     so the filter bar is never a grid item needing its own span/column
     overrides.

     .portfolio-grid is ONE flat 12-column grid (auto-placement, no row
     wrappers) that every project sits in directly, always — not a
     curated flex layout that switches to a different uniform grid while
     filtering. Saintly (.p-span-8) and Planet Fitness (.p-span-10-right, an
     explicit offset start) are the only two with unique large/offset
     treatment; every other project — including the "first row" partners
     HEC Montréal and Casey House — is a plain .p-span-4 "regular"
     card, so the grid naturally packs three per row wherever they fall.
     A filtered-out project just leaves the flow via
     [data-filter-status="not-active"] (display:none) — the same grid,
     the same auto-placement, hidden or shown, is what makes filtering
     safe here: no structural mode-switch, so nothing needs to be
     rebuilt or re-triggered. See .portfolio-group/.portfolio-row below
     for the older row-wrapped structure — those are what the homepage's
     .portfolio still uses and stay in place for it. */
  .portfolio {
    background: var(--bb-off-white);
    padding: 120px var(--page-margin);
    display: flex;
    flex-direction: column;
    gap: 24px;
  }

  /* Any .portfolio with a filter bar (What We Do) pulls its top in to
     the same 24px News uses above its own filters, so the pills sit
     close under the section above rather than at the full section
     padding. .portfolio--home has no filter bar and keeps the 120px.
     Higher specificity than the plain .portfolio rules, including the
     breakpoint one below, so the 24px holds at every width — again
     matching News, whose top padding doesn't change either. */
  .portfolio:not(.portfolio--home) {
    padding-top: 24px;
  }

  /* Homepage rows: 80px between rows, the same row rhythm the grids on
     What We Do and News use. It lives on both the section and
     .portfolio-group because the first four rows are wrapped in a group
     (they share reveal triggers) and the rest are direct children — the
     two gaps together are what the eye reads as one even column. */
  .portfolio--home {
    gap: 80px;
  }

  /* Column gap is the grid's own 16px; rows get the taller 80px the
     layout uses between rows everywhere (homepage, What We Do, News).
     Row gap is a property of the grid, not of what's in it, so it holds
     unchanged through filtering — .is-filtering only resizes spans. */
  .portfolio-grid {
    display: grid;
    grid-template-columns: repeat(var(--grid-cols-desktop), 1fr);
    align-items: start;
    column-gap: var(--grid-gap);
    row-gap: 80px;
    width: 100%;
  }

  .portfolio-group {
    display: flex;
    flex-direction: column;
    gap: 80px;
    width: 100%;
  }

  .portfolio-row {
    display: grid;
    grid-template-columns: repeat(var(--grid-cols-desktop), 1fr);
    align-items: start;
    gap: var(--grid-gap);
    width: 100%;
  }

  .p-card {
    display: flex;
    flex-direction: column;
    color: inherit;
    text-decoration: none;
  }

  .p-card__img {
    position: relative;
    width: 100%;
    overflow: hidden;
  }

  .p-card__img img {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
  }

  .p-card__img-overlay {
    position: absolute;
    inset: 0;
    background: rgba(var(--bb-black-rgb), 0.2);
    /* On/off, not a fade -- as are the arrow and the marquee. */
    opacity: 0;
    pointer-events: none;
  }

  .p-card__img-arrow {
    position: absolute;
    top: 50%;
    left: 50%;
    display: block;
    /* 90px = the old 72px arrow, 25% larger, on every tile size.
       --arrow-scale is a multiplier left at 1 (the large tiles used to
       set it to 2). */
    width: calc(90px * var(--arrow-scale, 1));
    transform: translate(-50%, -50%);
    /* Pointer offset from the image centre, set exactly by
       initTileCursorFollow() in main.js. The separate `translate`
       property, not part of `transform`, so it composes with the
       centring instead of replacing it. Zero when
       the script is not running (touch, reduced motion). */
    translate: var(--follow-x, 0px) var(--follow-y, 0px);
    /* Shows and hides instantly, with no fade or scale pop: the only
       transition is the translate, which is the follow's trail -- the
       script sets the offset exactly and this eases the graphic onto it.
       See --follow-duration. */
    opacity: 0;
    transition: translate var(--follow-duration) ease-out;
    pointer-events: none;
  }

  .p-card__img-arrow svg {
    display: block;
    width: 100%;
    height: auto;
  }

  /* Divides the markup's stroke-width="9" by the same factor the arrow
     grows by, so size alone never changes the line weight on screen
     (about 11px at weight 1). --arrow-weight then sets the weight
     independently: 1.5 draws a line 50% heavier. */
  .p-card__img-arrow path {
    stroke-width: calc(9 * var(--arrow-weight, 1) / var(--arrow-scale, 1));
  }

  /* The hover belongs to the image, not the whole card: the tint and the
     arrow show only while the pointer is over the picture, and the text
     below it reads as plain text. The card is still one link, so a click
     anywhere on it still goes through. */
  .p-card__img.is-revealed:hover .p-card__img-overlay {
    opacity: 1;
  }

  .p-card__img.is-revealed:hover .p-card__img-arrow {
    opacity: 1;
  }

  /* On a project tile the arrow stands in for the cursor: the system one
     hides over the image and comes back over the text. Project tiles only
     (an image carrying the arrow) — article tiles keep the regular cursor.
     And only while initTileCursorFollow() is running (it sets
     .is-tile-follow on <html>, for fine pointers without reduced motion);
     anywhere else the arrow stays centred, and hiding the cursor would
     leave no pointer at all. */
  .is-tile-follow .p-card__img:has(> .p-card__img-arrow):hover {
    cursor: none;
  }

  .p-card__body {
    display: flex;
    flex-direction: column;
    /* One 8px step between all three children (title, desc, tags).
       Pre-trim this was 3px here plus 13px of padding on .p-card__tags,
       the two together making up for the leading each block carried;
       trimmed, the gap is the whole of the spacing and the tags need no
       padding of their own. */
    gap: 8px;
    padding-top: 16px;
    padding-bottom: 16px;
  }

  .p-card__title {
    font-family: "Haas Recast VF", "Helvetica Neue", Arial, sans-serif;
    font-weight: 600;
    font-variation-settings: 'TRAK' 0;
    font-size: 20px;
    line-height: 1;
    letter-spacing: -0.4px;
    color: var(--bb-black);
  }

  /* Dense, against the Open of the title above it — the pairing the cards
     and the leadership grid both use. See the note on .team__role. */
  .p-card__desc {
    font-family: "Haas Recast VF", "Helvetica Neue", Arial, sans-serif;
    font-weight: 450;
    font-variation-settings: 'TRAK' -35;
    font-size: 20px;
    line-height: 1;
    letter-spacing: 0;
    /* 0.6, not the 0.47 this used to carry: on --bb-off-white that composites
       to #625E5A for 5.49:1, where 0.47 gave #817D77 and 3.49:1. At 20px
       regular this is body text, so AA wants 4.5:1 — 0.47 failed it. */
    color: rgba(var(--bb-black-rgb), 0.6);
    max-width: 100%;
  }

  /* The tags sit a step further down than the two text lines do: the
     body's 8px gap plus this, so 16px below the description against 8px
     between title and description. */
  .p-card__tags {
    display: flex;
    gap: 8px;
    flex-wrap: wrap;
    margin-top: 8px;
  }

  /* ====================================================================
     Tag — the one pill in the system.

     Every pill on the site is this class. Anything that needs different
     colour or behaviour adds a modifier beside it and changes nothing
     about the shape, so retuning the pill here reaches all of them:

       .tag              default — Cream fill on a light ground
       .tag--filter      the What We Do / News filter bar (a <button>)
       .tag--hero        the case-study hero, on Red

     The filter bar used to be a separate .portfolio-filter-btn carrying
     its own copy of the same seventeen declarations, and the two had
     already drifted: it rendered 2px taller than .tag and, because a
     <button> is a block box and .tag is inline-flex, the trim applied to
     one and not the other. Both now come from here and match exactly.

     Exempt from the sitewide trim, deliberately. A pill is a drawn box,
     not a text block: its padding is meant to be measured from the box
     edge to the type's own line box, the way it was drawn, and not
     recomputed against cap height. Setting it here rather than relying
     on inline-flex to dodge the trim by accident also means the pill
     keeps its height if it is ever switched to another display type —
     which is exactly how the filter button came to differ.

     Metrics, at the 14px/0.97 type: line box 13.58px, cap 10.08px. With
     a 1px border and 6px of padding the visible space above the cap is
     8.75px against 8px at the sides, and the pill stands 27.58px. The
     transparent border is load-bearing — .tag--filter colours it in on
     the active state, and without it here that state would make the
     pill 2px taller than its neighbours. */
  .tag {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    background: var(--bb-cream);
    color: var(--bb-forest);
    font-family: "Haas Recast VF", "Helvetica Neue", Arial, sans-serif;
    font-weight: 500;
    font-variation-settings: 'TRAK' 0;
    font-size: 14px;
    line-height: 0.97;
    letter-spacing: 0.28px;
    padding: 6px 8px;
    border: 1px solid transparent;
    border-radius: 4px;
    white-space: nowrap;
    text-box: normal;
  }

  /* Filter bar: same pill, made interactive. State lives on
     data-filter-status, which is what initPortfolioFilters() in main.js
     reads and writes — the class is presentation only. */
  .tag--filter {
    appearance: none;
    -webkit-appearance: none;
    cursor: pointer;
    transition: background-color 0.4s ease, border-color 0.4s ease;
  }

  .tag--filter[data-filter-status="active"] {
    background: transparent;
    border-color: var(--bb-black);
  }

  /* span helpers */
  .p-span-4 { grid-column: span 4; }
  .p-span-8 { grid-column: span 8; }
  .p-span-10-right { grid-column: 3 / span 10; }

  /* When any specific pillar filter is active (not "All"), every
     project — including Saintly/Planet Fitness/Casey House — becomes the same
     regular size, at every breakpoint, so a filtered subset never
     leaves oddly-sized tiles: see initPortfolioFilters() in main.js,
     which toggles this class on .portfolio-grid. Higher specificity
     than the plain span helpers (and their own breakpoint version
     below) lets it win without !important. */
  .portfolio-grid.is-filtering .p-span-4,
  .portfolio-grid.is-filtering .p-span-8,
  .portfolio-grid.is-filtering .p-span-10-right,
  .portfolio-grid.is-filtering .p-tablet-full {
    grid-column: span 4;
  }

  .portfolio-grid.is-filtering .p-card__img {
    aspect-ratio: 456 / 257;
  }

  /* Natural-proportion tiles keep their own shape while filtering too,
     so a tile does not change shape as filters come and go. */
  .portfolio-grid.is-filtering .p-card--natural .p-card__img:has(> img) {
    aspect-ratio: auto;
  }

  @media (max-width: 1100px) {
    .portfolio-grid.is-filtering .p-span-4,
    .portfolio-grid.is-filtering .p-span-8,
    .portfolio-grid.is-filtering .p-span-10-right,
    .portfolio-grid.is-filtering .p-tablet-full {
      grid-column: span 1;
    }
  }

  /* Row 1: Saintly (big) + award/blurb column */
  .p-card--saintly .p-card__img { aspect-ratio: 928 / 619; }

  .portfolio-col {
    display: flex;
    flex-direction: column;
    color: inherit;
    text-decoration: none;
  }

  .portfolio-award {
    position: relative;
    width: 100%;
    aspect-ratio: 456 / 365;
    overflow: hidden;
  }

  .portfolio-award img {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
  }

  /* Natural proportions, as .p-card--natural: once the slot holds a
     photo rather than a pattern, it takes the photo's own shape (the
     <img> carries width/height) instead of the 456:365 box. */
  .portfolio-award--natural:has(> img) {
    aspect-ratio: auto;
  }

  .portfolio-award--natural > img {
    position: static;
    display: block;
    height: auto;
  }

  /* Rests on the card's vertical middle; initTileCursorFollow() moves it
     up and down from there. The 16px top and bottom padding is how close
     to either edge it may follow — the script reads it back rather than
     restating it. */
  .marquee-overlay {
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
    padding: 16px 0;
    overflow: hidden;
    opacity: 0;
    pointer-events: none;
  }

  .marquee-overlay--burgundy { background: var(--bb-burgundy); }
  .marquee-overlay--burgundy .marquee__advanced__p { color: var(--bb-sky); }

  .marquee-overlay--blue { background: var(--bb-sky); }
  .marquee-overlay--blue .marquee__advanced__p { color: var(--bb-burgundy); }

  /* Reduced motion: initMarqueeScrollDirection returns before it clones
     or animates anything, leaving the one "Read more" in the markup
     standing still. Give it a left inset to match the 16px edge gap -- the
     scrolling version needs none, since its copy is always travelling
     past that edge rather than resting against it. The trailing margin on
     the <p> is the gap between repeats, so it goes with them. */
  @media (prefers-reduced-motion: reduce) {
    .marquee-overlay { padding-left: 16px; }
    .marquee__advanced__p { margin-right: 0; }
  }

  /* Any card carrying a marquee overlay gets it on hover -- the article
     tile on the homepage, and every tile on the news page. Cards without
     one fall back to the arrow (.p-card__img-arrow above). Unlike the
     arrow, the whole card triggers it, by design.

     Why the loop needs no pause button (WCAG 2.2.2): it is never moving
     content the visitor didn't ask for. It shows only while the pointer
     is on the card and disappears the moment it leaves, so moving the
     pointer away is the stop control. Hence the fine-pointer gate: on a
     touch screen a tap can leave :hover stuck after the article opens
     in a new tab, and the loop would then run with nothing to stop it.
     Keyboard focus deliberately does not show it, for the same reason.
     Reduced motion stills it entirely (see above). Speed alone would
     not exempt it: 2.2.2 has no slow-enough threshold. */
  @media (hover: hover) and (pointer: fine) {
    .portfolio-col:hover .portfolio-award.is-revealed .marquee-overlay,
    .p-card:hover .p-card__img.is-revealed .marquee-overlay {
      opacity: 1;
    }
  }

  /* Marquee w/ scroll direction (adapted from Osmo Supply's resource) */
  .marquee-advanced {
    width: 100%;
    position: relative;
    overflow: hidden;
    /* Vertical only: how far up or down from its resting line, the
       card's middle, the marquee has followed the pointer
       (initTileCursorFollow() in main.js). The sideways motion belongs to
       the loop, on the children. */
    translate: 0 var(--follow-y, 0px);
    transition: translate var(--follow-duration) ease-out;
  }

  /* How far the tile hover graphics (the arrow, the "Read more" marquee)
     trail the pointer. The one knob: higher is lazier, 0s is exact. Set
     to 0s so the graphic sits right on the pointer; 0.15s was the old
     trail, if one is wanted back.

     .is-follow-snap switches the trail off for the single placement on
     pointerenter, so a graphic appears under the pointer instead of
     sliding in from wherever it was last shown — initTileCursorFollow()
     adds it, places the graphic, forces a style flush and removes it. */
  :root { --follow-duration: 0s; }

  .p-card__img-arrow.is-follow-snap,
  .marquee-advanced.is-follow-snap { transition: none; }

  .marquee-advanced__scroll {
    will-change: transform;
    width: 100%;
    display: flex;
    position: relative;
  }

  .marquee-advanced__collection {
    will-change: transform;
    display: flex;
    position: relative;
  }

  .marquee-advanced__item {
    align-items: center;
    display: flex;
  }

  .marquee__advanced__p {
    white-space: nowrap;
    /* Trim the top only. The global text-box trim (see the * rule up top)
       cuts the box to cap-height/baseline, and .marquee-advanced clips at
       overflow:hidden -- so the baseline overshoot on the round letters of
       "Read more" (e, a, o) was being shaved flat. Keeping trim-start
       preserves the cap-height alignment against .marquee-overlay's 16px
       padding-top; dropping trim-end hands the descender space back. */
    text-box: trim-start cap alphabetic;
    margin: 0 40px 0 0;
    font-family: "Haas Recast VF", "Helvetica Neue", Arial, sans-serif;
    font-weight: 400;
    font-variation-settings: 'TRAK' -35;
    font-size: 72px;
    line-height: 0.92;
  }

  /* Row 2: Planet Fitness, offset right */
  .p-card--planet-fitness .p-card__img { aspect-ratio: 1164 / 619; }

  /* Row 3: Brand Touch (red) + Casey House */
  .brand-touch {
    position: relative;
    overflow: hidden;
    display: flex;
    flex-direction: column;
  }

  .brand-touch--red {
    background: var(--bb-red);
    color: var(--bb-off-white);
    aspect-ratio: 1 / 1;
    padding: 16px;
    justify-content: space-between;
  }

  .brand-touch__heading {
    font-family: "Haas Recast VF", "Helvetica Neue", Arial, sans-serif;
    font-weight: 400;
    font-variation-settings: 'TRAK' -35;
    font-size: 72px;
    line-height: 0.92;
    color: var(--bb-pink);
  }

  .brand-touch__stripe {
    position: absolute;
    top: 0;
    right: 0;
    width: 42px;
    height: 100%;
    background: repeating-linear-gradient(
      to bottom,
      var(--bb-forest) 0px,
      var(--bb-forest) 12px,
      var(--bb-cream) 12px,
      var(--bb-cream) 15px
    );
  }

  .p-card--casey-house .p-card__img { aspect-ratio: 928 / 619; }

  /* Row 4: article photo + Brand Touch (navy) + HEC Montréal */
  .p-card--article .p-card__img { aspect-ratio: 456 / 365; }

  .brand-touch--navy {
    background: var(--bb-navy);
    aspect-ratio: 456 / 570;
    justify-content: space-between;
  }

  .brand-touch--navy .brand-touch__heading {
    color: var(--bb-cream);
    padding: 16px;
  }

  .brand-touch__stripe--bottom {
    position: static;
    width: 100%;
    height: 40px;
    background: repeating-linear-gradient(
      to right,
      var(--bb-burgundy) 0px,
      var(--bb-burgundy) 24px,
      var(--bb-sky) 24px,
      var(--bb-sky) 48px
    );
    border-radius: 0;
  }

  .p-card--hec-montreal .p-card__img { aspect-ratio: 456 / 257; }

  /* Standard 3-up rows */
  .p-card--wide .p-card__img { aspect-ratio: 456 / 257; }

  .p-tile {
    width: 100%;
    aspect-ratio: 456 / 257;
    overflow: hidden;
    position: relative;
  }

  .p-tile img {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
  }

  @media (max-width: 1100px) {
    /* Both portfolios collapse to the same 2-column tablet layout:
       .portfolio-grid on What We Do, and .portfolio--home on the
       homepage, where .portfolio-group/.portfolio-row go display:
       contents so their cards join one continuous grid instead. In both,
       .p-span-4/8/10-right fold to span 1 and Saintly (.p-tablet-full)
       runs the full 2 columns as the one larger project. On What We Do
       .portfolio stays the plain flex stack of the filters and the grid
       and only takes the section padding here; the homepage has no
       filter bar, so .portfolio--home becomes the grid itself. */
    .portfolio { padding: 80px var(--page-margin); }
    .portfolio-grid,
    .portfolio--home {
      display: grid;
      grid-template-columns: repeat(2, 1fr);
      align-items: start;
      column-gap: var(--grid-gap);
      row-gap: 32px;
    }
    .portfolio-group,
    .portfolio-row {
      display: contents;
    }
    .p-span-4, .p-span-8, .p-span-10-right {
      grid-column: span 1;
    }
    .p-tablet-full { grid-column: span 2; }
    .brand-touch__heading { font-size: 72px; }

    .p-tile { display: none; }
    .brand-touch--navy { display: none; }
  }

  @media (max-width: 640px) {
    /* Below tablet, back to one project per row — same grids, just a
       single column, so .p-tablet-full (Saintly) doesn't span into an
       implicit second track. */
    .portfolio-grid,
    .portfolio--home {
      grid-template-columns: 1fr;
      row-gap: 52px;
    }
    .p-tablet-full { grid-column: 1; }
  }

  /* News: the article index. Deliberately thin — it is the same
     component set as the What We Do portfolio (.portfolio-filters,
     .portfolio-grid, .p-card, .tag) driven by the same
     initPortfolioFilters() group in main.js, so filtering, the
     masked image reveal and the staggered body reveal all come for
     free and cannot drift from that page. Only three things are
     actually new here: the white ground, the fixed 456/257 tile ratio,
     and the brand-pattern tile that stands in for an article image
     until real ones land. */

  .news {
    background: var(--bb-white);
    /* Tight 24px top at every breakpoint — the filter bar sits close
       under the hero rather than at the section padding .portfolio
       uses. The bottom keeps that rhythm (120px, 80px below 1100px). */
    padding: 24px var(--page-margin) 120px;
    display: flex;
    flex-direction: column;
    gap: 24px;
  }

  /* The articles grid is .portfolio-grid, unchanged and unmodified —
     every card is a plain .p-span-4, so it packs three per row, and the
     80px row gap / 16px column gap come from that grid itself. Only the
     card is news-specific, via .p-card--news below. */

  .p-card--news .p-card__img { aspect-ratio: 456 / 257; }

  /* Natural proportions, for the project and article grids on the
     homepage, What we do and News: an image sets its own height instead
     of filling its card's fixed box, so a row mixes shapes and its tiles
     stagger (rows and grids align to the top). The <img> carries
     width/height attributes, so the box is reserved before the file
     loads and the reveal and ScrollTrigger positions are measured
     against the right height.

     Only where the box actually holds an <img> (:has(> img)). An article
     card still standing a .news-pattern in for its photo has no shape of
     its own, so it keeps its modifier's fixed box until the photo lands
     — then it takes the photo's shape with no CSS change. After the
     .p-card--news rule above, which it has to beat. */
  .p-card--natural .p-card__img:has(> img),
  .p-tile--natural {
    aspect-ratio: auto;
  }

  .p-card--natural .p-card__img > img,
  .p-tile--natural img {
    position: static;
    display: block;
    height: auto;
  }

  /* Placeholder article tile. Each card gets one of the four brand
     pattern tiles instead of a photograph; swap the whole
     .news-pattern div for an <img> once real article images exist —
     [data-mask-reveal] animates whichever element is first inside
     .p-card__img, so nothing else has to change.

     Sized and positioned exactly like .p-card__img img so the two are
     interchangeable. Tile dimensions match .mobile-menu__pattern —
     same four PNGs, same intrinsic sizes. */
  .news-pattern {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    background-repeat: repeat;
    background-position: 0 0;
  }

  .news-pattern--pink-orange { background-image: url("../images/menu/menu-tile-pink-orange.png"); background-size: 49px 121.5px; }
  .news-pattern--sky-navy    { background-image: url("../images/menu/menu-tile-sky-navy.png");    background-size: 33.5px 111.5px; }
  .news-pattern--cream-red   { background-image: url("../images/menu/menu-tile-cream-red.png");   background-size: 56px 111.5px; }
  .news-pattern--green-navy  { background-image: url("../images/menu/menu-tile-green-navy.png");  background-size: 48px 96px; }

  @media (max-width: 1100px) {
    .news { padding-bottom: 80px; }
  }

  /* Capabilities: four "what we do" pillars. Desktop hovers/focuses a
     word on the left to swap the copy + numbered list on the right
     (crossfaded via GSAP — see initCapabilitiesPanel() in main.js).
     Below 1024px that layout gets too cramped for the hover interaction
     anyway, so it's replaced by a swipeable GSAP Draggable carousel of
     cards instead (initCapabilitiesSlider() in main.js), one pillar per
     card, current card in full colour and the rest dimmed. */

  .capabilities {
    background: var(--bb-navy);
    color: var(--bb-off-white);
  }

  /* Repeating burgundy/sky column pattern, same repeating-linear-gradient
     technique as .brand-touch__stripe--bottom, sized to the Figma swatch
     (24px burgundy + 6px sky, 48px tall). */
  .capabilities__pattern {
    width: 100%;
    height: 48px;
    background: repeating-linear-gradient(
      to right,
      var(--bb-burgundy) 0px,
      var(--bb-burgundy) 24px,
      var(--bb-sky) 24px,
      var(--bb-sky) 30px
    );
  }

  .capabilities__inner {
    width: 100%;
    padding: 140px var(--page-margin);
  }

  .capabilities__rule {
    display: block;
    width: 100%;
    height: 1px;
    background: rgba(var(--bb-off-white-rgb), 0.2);
    flex-shrink: 0;
  }

  /* Desktop hover grid */

  .capabilities__grid {
    display: grid;
    grid-template-columns: 3fr 2fr;
    gap: 15px;
    align-items: stretch;
    max-width: 1164px;
    margin: 0 auto;
  }

  .capabilities__options {
    display: flex;
    flex-direction: column;
    gap: 18px;
  }

  .capabilities__option {
    display: flex;
    align-items: center;
    gap: 12px;
    background: none;
    border: none;
    padding: 0;
    margin: 0;
    font: inherit;
    text-align: left;
    /* The regular arrow, not the pointer hand: on desktop a pillar
       switches the panel on hover, so there is nothing to click for and
       the hand promised an action that isn't there. Still a <button>
       for keyboard and screen-reader use (see initCapabilitiesPanel). */
    cursor: default;
    opacity: 0.3;
    transition: opacity 0.4s ease;
  }

  .capabilities__option.is-active,
  .capabilities__option:hover,
  .capabilities__option:focus-visible {
    opacity: 1;
  }

  .capabilities__option-label {
    font-family: "Haas Recast VF", "Helvetica Neue", Arial, sans-serif;
    font-weight: 400;
    font-variation-settings: 'TRAK' -35;
    font-size: clamp(32px, 3.2vw, 45px);
    line-height: 0.97;
    letter-spacing: -0.9px;
    color: var(--bb-off-white);
    white-space: nowrap;
  }

  .capabilities__option-arrow {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 20px;
    height: 17px;
    flex-shrink: 0;
    opacity: 0;
    transform: translateX(-6px);
    transition: opacity 0.3s ease, transform 0.3s ease;
  }

  .capabilities__option-arrow svg {
    width: 100%;
    height: 100%;
  }

  .capabilities__option-arrow svg path {
    stroke: var(--bb-off-white);
  }

  .capabilities__option:not(.is-active):hover .capabilities__option-arrow {
    opacity: 1;
    transform: translateX(0);
  }

  /* No fixed gap: the panel is a stretched grid item, so space-between
     alone pushes the two blocks to the top and bottom edges of the row
     and the panel reads as the same height as the options column on the
     left, whatever the copy inside it is. (Figma's gap:auto.) */
  .capabilities__panel {
    display: flex;
    flex-direction: column;
    justify-content: space-between;
  }

  .capabilities__panel-block {
    display: flex;
    flex-direction: column;
    gap: 24px;
  }

  /* Exists only so the scroll reveal has something to animate that
     isn't the block itself: [data-reveal-group-nested] on the block
     makes its children the reveal slots, and each slot gets its own
     generated overflow:hidden mask (see buildGenericRecipe in main.js).
     Without this wrapper every .capabilities__panel-body would get a
     mask of its own — four flex children where there is meant to be
     one visible box, and three phantom 24px gaps from the hidden ones.
     The rule above it opts out via data-ignore="true" so the block's
     own structure stays put while the copy slides in. */
  /* The generated mask around this used to need its own 8px of
     descender room; the .reveal-mask rule now gives every mask that. */
  .capabilities__panel-copy {
    min-width: 0;
  }

  .capabilities__panel-body,
  .capabilities__panel-list {
    display: none;
  }

  .capabilities__panel-body.is-active { display: block; }
  .capabilities__panel-list.is-active { display: flex; }

  .capabilities__panel-body {
    font-family: "Haas Recast VF", "Helvetica Neue", Arial, sans-serif;
    font-weight: 400;
    font-variation-settings: 'TRAK' 0;
    font-size: 26px;
    line-height: 1.19;
    letter-spacing: -0.52px;
    max-width: 480px;
  }

  .capabilities__panel-list {
    flex-direction: column;
    gap: 6px;
  }

  .capabilities__panel-list li {
    display: grid;
    grid-template-columns: 9px 1fr;
    gap: 12px;
    font-family: "Haas Recast VF", "Helvetica Neue", Arial, sans-serif;
    font-weight: 500;
    font-variation-settings: 'TRAK' 0;
    font-size: 14px;
    line-height: 1.19;
  }

  .capabilities__panel-list-index { opacity: 0.6; }

  /* Mobile card carousel */

  .capabilities-slider {
    --slider-status: on;
    display: none;
  }

  .capabilities-slider__collection {
    width: 100%;
    overflow: hidden;
  }

  .capabilities-slider__list {
    display: flex;
    -webkit-user-select: none;
    user-select: none;
    will-change: transform;
    touch-action: pan-y;
    /* Lead-in gutter on the track itself, not a margin on the first
       item: either works for the leading edge, but see the trailing
       spacer below for why the trailing edge needs different handling. */
    padding-left: 36px;
  }

  .capabilities-slider__item {
    /* A flex row so the card inside fills the item's stretched height:
       the track's default align-items:stretch sizes every item to the
       tallest one, and flex:1 on the card passes that height through.
       Cards therefore stay level with each other however the copy
       rewraps at a given width. */
    display: flex;
    flex: none;
    /* vw, not %: a percentage would resolve against the track's own
       (now padded) content box and shrink below the intended 85% of
       the actual viewport. */
    width: 80vw;
    margin-right: 12px;
  }

  /* :nth-last-child(2), not :last-child — the trailing spacer below is
     the true last child once it's in the DOM. */
  .capabilities-slider__item:nth-last-child(2) { margin-right: 0; }

  /* A real element, not padding/margin: a scroll container's
     scrollWidth excludes trailing padding/margin on its last child (a
     long-standing cross-browser quirk), so the drag bounds computed
     from it would never reach a trailing margin-right/padding-right —
     the card would stop short and that gutter would never appear. An
     actual box's right edge is always counted, so this is what makes
     the lead-out gutter after the last card reachable when dragged. It
     carries no data-gsap-slider-item attribute, so the slider script
     doesn't treat it as a slide. */
  .capabilities-slider__spacer {
    flex: none;
    width: 36px;
  }

  [data-gsap-slider-list-status="grab"] { cursor: grab; }
  [data-gsap-slider-list-status="grabbing"] { cursor: grabbing; }

  .capability-card {
    background: var(--bb-navy);
    border-radius: 4px;
    flex: 1;
    display: flex;
    flex-direction: column;
    /* Heading pinned to the top, body + list pinned to the bottom; the
       80px is the minimum between them, and any extra height a taller
       sibling card forces opens up in that gap rather than trailing off
       below the list. */
    justify-content: space-between;
    gap: 80px;
    /* Tablet-width default — the carousel only ever shows below 1024px
       (see .capabilities-slider), so this is effectively the tablet
       value; @media (max-width: 640px) below scales it back down for
       phone-width cards. */
    padding: 30px var(--page-margin-desktop);
  }

  .capability-card__heading {
    font-family: "Haas Recast VF", "Helvetica Neue", Arial, sans-serif;
    font-weight: 400;
    font-variation-settings: 'TRAK' -35;
    font-size: 32px;
    line-height: 0.97;
    color: rgba(var(--bb-off-white-rgb), 0.52);
    transition: color 0.4s ease;
  }

  .capability-card__content {
    display: flex;
    flex-direction: column;
    gap: 24px;
  }

  .capability-card__body {
    font-family: "Haas Recast VF", "Helvetica Neue", Arial, sans-serif;
    font-weight: 400;
    font-variation-settings: 'TRAK' 0;
    font-size: 18px;
    line-height: 1.28;
    letter-spacing: -0.36px;
    color: rgba(var(--bb-off-white-rgb), 0.52);
    transition: color 0.4s ease;
  }

  .capability-card__rule {
    display: block;
    width: 100%;
    height: 1px;
    background: rgba(var(--bb-off-white-rgb), 0.2);
  }

  .capability-card__list {
    display: flex;
    flex-direction: column;
    gap: 4px;
  }

  .capability-card__list li {
    display: grid;
    grid-template-columns: 12px 1fr;
    gap: 6px;
    font-family: "Haas Recast VF", "Helvetica Neue", Arial, sans-serif;
    font-size: 12px;
    line-height: 1.19;
    letter-spacing: -0.24px;
    color: rgba(var(--bb-off-white-rgb), 0.52);
  }

  [data-gsap-slider-item-status="active"] .capability-card__heading,
  [data-gsap-slider-item-status="active"] .capability-card__body {
    color: var(--bb-off-white);
  }

  @media (max-width: 1024px) {
    .capabilities {
      overflow: hidden;
      background: linear-gradient(rgba(var(--bb-white-rgb), 0.05), rgba(var(--bb-white-rgb), 0.05)), var(--bb-navy);
    }
    /* Zero horizontal padding is deliberate, not an oversight: below
       1024px the grid gives way to .capabilities-slider, and the card
       track is meant to bleed off both screen edges. The page margin
       would box it in. The track's own lead-in stays at its drawn 36px
       (see .capabilities-slider__list) rather than the page margin, so
       the first card sits slightly proud of the sections above it. */
    .capabilities__inner { padding: 100px 0; }
    .capabilities__grid { display: none; }
    .capabilities-slider {
      display: block;
    }
  }

  /* Tablet: widen slides to ~2-up so two cards sit fully in view (and
     read as "active") with a peek of the third, instead of the phone
     layout's single active card. */
  @media (min-width: 641px) and (max-width: 1024px) {
    .capabilities-slider {
      --slider-active-count: 2;
    }

    .capabilities-slider__item {
      width: 42vw;
    }
  }

  @media (max-width: 640px) {
    /* Still full-bleed for the slider — see the 1024px block above. */
    .capabilities__inner { padding: 80px 0; }
    .capability-card { padding: 16px 10px; }
  }

  /* --------------------------------------------------------------------
     Leadership grid — Figma 1944:14440 (desktop), 2129:3602 (mobile).

     The page-grid inset is the case-study one, reused rather than
     restated: .team__inner carries .cs-grid in the markup for the shared
     12 tracks and gap, and only the column span lives here. Content sits
     in 2 / span 10, which leaves the first and last column empty and puts
     the group's edge exactly where the comp's 138px does — page margin
     plus one column plus one gap — without that number appearing
     anywhere.

     The comp's body grey (#4C4A46) is not in the palette and BRAND.md
     allows no hex outside :root, so it is expressed as black at 69% over
     the section's own off-white, which resolves to #4C4A46 to the byte.
     That also matches how the rest of the file writes muted text. */
  .team {
    /* Above the Combine video when careers is shorter than a screen and
       the tucked-in stage reaches up here (see .combine-scope). */
    position: relative;
    z-index: 1;
    background: var(--bb-off-white);
    padding: 220px var(--page-margin);
  }

  .team__content {
    grid-column: 2 / span 10;
    display: flex;
    flex-direction: column;
    gap: 80px;
  }

  .team__intro {
    display: flex;
    flex-direction: column;
    gap: 7px;
    font-family: "Haas Recast VF", "Helvetica Neue", Arial, sans-serif;
    font-weight: 400;
    font-variation-settings: 'TRAK' 0;
    font-size: 30px;
    line-height: 1.1;
    letter-spacing: -0.6px;
  }

  .team__intro-lead { color: var(--bb-black); }
  .team__intro-sub  { color: rgba(var(--bb-black-rgb), 0.69); }

  /* Three across inside the ten-column inset, with the page grid's own
     gap between them horizontally and 24px between rows. The comp's 48px
     was halved once each card gained its LinkedIn link, which already
     adds 36px below the role. */
  .team__grid {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    column-gap: var(--grid-gap);
    row-gap: 24px;
    margin: 0;
    padding: 0;
    list-style: none;
  }

  .team__member {
    display: flex;
    flex-direction: column;
    gap: 4px;
  }

  /* One 8:9 box at every breakpoint. The eight source photos are all
     different shapes; object-fit crops each into the same box, so every
     card in a row is the same height and their bottom edges line up. */
  .team__photo {
    aspect-ratio: 8 / 9;
    overflow: hidden;
  }

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

  /* The same block as .p-card__body on the project tiles, to the same
     spec: 16px above and below, an 8px step between the lines, an Open
     semibold name and a Dense medium role in lightened black. Keep the
     two in step — a change to one is meant for both. */
  .team__meta {
    display: flex;
    flex-direction: column;
    gap: 8px;
    padding-top: 16px;
    padding-bottom: 16px;
  }

  .team__name {
    font-family: "Haas Recast VF", "Helvetica Neue", Arial, sans-serif;
    font-weight: 600;
    font-variation-settings: 'TRAK' 0;
    font-size: 20px;
    line-height: 1;
    letter-spacing: -0.4px;
    color: var(--bb-black);
  }

  /* Dense (TRAK -35) at 20px, paired with the Open title above it. This
     is a deliberate exception to CLAUDE.md's "body/UI type uses 0" — the
     designer specified Dense for card and member body copy, and the same
     pairing runs on every project tile. */
  .team__role {
    font-family: "Haas Recast VF", "Helvetica Neue", Arial, sans-serif;
    font-weight: 450;
    font-variation-settings: 'TRAK' -35;
    font-size: 20px;
    line-height: 1;
    letter-spacing: 0;
    /* Matches .p-card__desc — see the contrast note there. */
    color: rgba(var(--bb-black-rgb), 0.6);
  }

  /* Each member's LinkedIn link, under their role. It sits in the flow
     at all times, so revealing it never moves the grid. On a mouse it
     stays transparent until the pointer is over the card, and appears
     as soon as it takes keyboard focus. Touch screens have no hover,
     so there it is always shown. It fades in, instead of appearing
     instantly, unless reduced motion is on. */
  /* fit-content, not align-self: the scroll reveal wraps the link in a
     block .reveal-mask, so the link is not the flex item and would
     otherwise stretch to the card's full width. */
  .team__linkedin {
    width: fit-content;
    margin-top: 4px;
    color: var(--bb-black);
  }

  /* The Figma button's colours: burgundy on pink is 8.7:1. */
  .team__linkedin .cta__icon-box { background: var(--bb-pink); }
  .team__linkedin .cta__icon-glyph svg path { stroke: var(--bb-burgundy); }

  @media (hover: hover) and (pointer: fine) {
    .team__linkedin {
      opacity: 0;
      transition: opacity 0.3s ease;
    }

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

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

  /* The one size step for both blocks. It has to sit here rather than up
     with the other 1100px rules: .team__name and .team__role are declared
     below that block, and at equal specificity the later rule wins. Both
     selectors are in one rule so the tiles and the grid cannot drift. */
  @media (max-width: 1100px) {
    .p-card__title,
    .team__name { font-size: 18px; }

    .p-card__desc,
    .team__role { font-size: 17px; }
  }

  /* Tablet: the inset is dropped and the group runs the full width, the
     same move every other section makes at this size. The three columns
     stay — it is the empty first and last page columns that go, not a
     card per row. */
  @media (max-width: 1200px) {
    .team {
      padding: 140px var(--page-margin);
    }

    .team__content {
      grid-column: 1 / -1;
      gap: 64px;
    }

  }

  /* Mobile: two across. The photo keeps its 8:9 box from above. */
  @media (max-width: 640px) {
    .team {
      padding: 96px var(--page-margin);
    }

    .team__content { gap: 48px; }

    .team__intro {
      gap: 12px;
      font-size: 24px;
      line-height: 1.04;
      letter-spacing: -0.48px;
    }

    .team__grid {
      grid-template-columns: repeat(2, 1fr);
      row-gap: 16px;
    }

    .team__member { gap: 2px; }
  }

  /* --------------------------------------------------------------------
     Open roles — Figma 1944:14485 (desktop), 2129:3209 (mobile),
     1441:19963 (row hover).

     The intro sits on the page grid (.cs-grid, columns 2-11, the same
     inset as the leadership grid above). The LIST does not, and that is
     the whole reason this section is built in two halves: the hover tint
     runs the full width of the frame while the row's text stays on the
     grid, so the row has to reach the viewport edges and inset its own
     contents instead.

     --careers-inset is that inset, and it is the grid's own arithmetic
     rather than the comp's 138px: page margin, plus one of twelve
     columns, plus one gap. Writing it out keeps the rows aligned to the
     intro above them at every width, which a hardcoded 138 would only
     manage at 1440. The 100% resolves against the row's containing
     block, which is the full-width list — not 100vw, which would include
     the scrollbar and push the rows wider than the page.

     scroll-margin-top clears the fixed 139px navbar for the unscripted
     path: a native hash jump, arriving from another page or with JS off,
     would otherwise land the section under it. The scripted scroll does
     not need it, since the peekaboo nav has hidden itself by then. */
  .careers {
    --careers-inset: calc(
      var(--page-margin)
      + (100% - 2 * var(--page-margin) - 11 * var(--grid-gap)) / 12
      + var(--grid-gap)
    );
    background: var(--bb-off-white);
    padding: 80px 0 160px;
    scroll-margin-top: 139px;
    /* Above the Combine video, which is tucked behind the end of this
       section (see .combine-scope). */
    position: relative;
    z-index: 1;
  }

  .careers__head {
    padding: 0 var(--page-margin);
    margin-bottom: 72px;
  }

  .careers__intro {
    grid-column: 2 / span 10;
    display: flex;
    flex-direction: column;
    gap: 7px;
    font-family: "Haas Recast VF", "Helvetica Neue", Arial, sans-serif;
    font-weight: 400;
    font-variation-settings: 'TRAK' 0;
    font-size: 30px;
    line-height: 1.1;
    letter-spacing: -0.6px;
  }

  .careers__intro-lead { color: var(--bb-black); }
  .careers__intro-sub  { color: rgba(var(--bb-black-rgb), 0.69); }

  .careers__list {
    margin: 0;
    padding: 0;
    list-style: none;
  }

  /* One rule above the list, not one per row — the desktop comp draws a
     single line under the intro and leaves the rows open. It is a
     ::before rather than a border on the list because the list now runs
     edge to edge and the rule does not: it keeps the rows' own inset. */
  .careers__list::before {
    content: '';
    display: block;
    height: 0.5px;
    margin: 0 var(--careers-inset) 32px;
    background: rgba(var(--bb-black-rgb), 0.5);
  }

  /* The row is full width and pads its own contents in. overflow:hidden
     keeps the tint to the row's own box.

     Even padding top and bottom. The comp had 10px over 16px, which was
     quietly correcting for the text sitting low in the row — see
     .careers__apply, which used to set the row's height. With that fixed
     the band is the text itself, so this is 16px of visible space above
     and below it, not padding plus a hidden band. (Was 24px; tightened
     by 8 on each side to match the design.)

     Deliberately not the 8px the mobile rule below uses: there the row
     is a two-line grid that sets its own height, so the same number would
     not buy the same space. */
  /* isolation gives the row its own stacking context, so the tint can
     sit under the text (z-index -1) without dropping behind the page. */
  .careers__row {
    position: relative;
    isolation: isolate;
    display: block;
    overflow: hidden;
    padding: 16px var(--careers-inset);
    color: var(--bb-black);
  }

  /* The row clips the tint, which would also clip a link's focus ring
     where it reaches past the text into the row's padding. */
  .careers__row:focus-within { overflow: visible; }

  /* The hover tint from 1441:19963 — 5% black, full width of the row.
     Plain on/off with no transition, like the project tiles' hover. It
     sits under the text rather than over it, which is why the fields
     carry a z-index at all. */
  .careers__tile {
    position: absolute;
    inset: 0;
    z-index: -1;
    background: rgba(var(--bb-black-rgb), 0.05);
    opacity: 0;
  }

  .careers__fields {
    display: flex;
    align-items: flex-end;
    gap: 16px;
  }

  .careers__dept,
  .careers__type {
    font-family: "Haas Recast VF", "Helvetica Neue", Arial, sans-serif;
    font-weight: 400;
    font-variation-settings: 'TRAK' 0;
    font-size: 18px;
    line-height: 1.28;
    letter-spacing: -0.36px;
  }

  /* Fixed widths from the comp: they set the column rhythm down the list,
     so every row's title starts and every "Full-time" lands on the same
     vertical line without a grid. */
  .careers__dept {
    flex: none;
    width: 180px;
  }

  /* The job description link. Its ::after stretches over the whole row,
     so the row is still one big target for the description, as it was
     when the row itself was the link; Apply sits above it (z-index 2)
     as the one part of the row that goes somewhere else. The ring on
     keyboard focus stays on the title's own text. */
  .careers__title {
    flex: none;
    width: 732px;
    color: inherit;
    text-decoration: none;
    font-family: "Haas Recast VF", "Helvetica Neue", Arial, sans-serif;
    font-weight: 400;
    font-variation-settings: 'TRAK' 0;
    font-size: 26px;
    line-height: 1.19;
    letter-spacing: -0.52px;
  }

  .careers__type { white-space: nowrap; }

  .careers__title::after {
    content: '';
    position: absolute;
    inset: 0;
    z-index: 1;
  }

  /* Pushed to the edge by the auto margin rather than space-between,
     which would also spread the three fields and break their rhythm.
     A link of its own (the application email), on top of the title's
     stretched link. Transparent at rest on a mouse and revealed with the
     tint. Opacity only, not visibility: it has to stay in the tab order
     and the accessibility tree, and it shows as soon as it (or the
     title) takes keyboard focus.

     Nudged down so the label sits on the same baseline as the row's text.
     .cta centres its label and icon on a common centre line, and the 32px
     icon box makes this block taller than the text beside it — so with the
     fields bottom-aligned the word "Apply" floated 10.09px above the
     baseline the department, title and "Full-time" share. Offsetting the
     whole block keeps the label and icon in their usual relationship,
     which aligning inside the CTA would break. It has to be an offset
     rather than align-items: baseline on .careers__fields: that resolves
     the other way, holding this block still and lifting the three text
     fields instead, which moves every row at rest and not just on hover.

     The negative margins are what stop this block sizing the row. It is
     32px tall because of the icon box, against an 18.72px title, and it
     is invisible until hover — so left alone it padded every row with
     13.28px of empty band above the text and made even padding read as
     top-heavy. Trimming 7px off each end takes its layout height to 18px,
     just under the title, so the text sets the row and the icon simply
     overhangs into the padding. It stays a flex item, so margin-left:auto
     still parks it at the edge and it still reserves its width — going
     position: absolute instead would hand that space to the title and
     move "Full-time" with it.

     position/top for the baseline nudge rather than more margin, which
     would drag the row's height along with it. Between them the block
     overhangs the text band by 10px below and 7px above, which the row's
     padding absorbs, so nothing meets its overflow: hidden. */
  .careers__apply {
    margin-left: auto;
    margin-top: -7px;
    margin-bottom: -7px;
    flex: none;
    position: relative;
    top: 3px;
    z-index: 2;
    color: inherit;
    text-decoration: none;
    opacity: 0;
  }

  .careers__apply .cta__icon-box { background: var(--bb-sky); }
  .careers__apply .cta__icon-glyph svg path { stroke: var(--bb-black); }

  /* Keyboard focus anywhere in the row shows Apply and the tint, on any
     device, so a keyboard user sees where they are. */
  .careers__row:has(:focus-visible) .careers__apply,
  .careers__row:has(:focus-visible) .careers__tile {
    opacity: 1;
  }

  /* Hover is a fine-pointer affordance only. mouseenter-style hover can
     stick after a tap on some touch browsers, which this media query
     rules out. */
  @media (hover: hover) and (pointer: fine) {
    .careers__row:hover .careers__apply,
    .careers__row:hover .careers__tile {
      opacity: 1;
    }
  }

  /* No hover to reveal it on a touch screen, so Apply is always there;
     the phone layout below also drops its label to the icon. */
  @media (hover: none), (pointer: coarse) {
    .careers__apply { opacity: 1; }
  }

  @media (max-width: 1200px) {
    .careers {
      padding: 64px 0 120px;
    }

    .careers__head { margin-bottom: 56px; }

    /* The comp's fixed widths only hold at 1440; below it the title takes
       the slack so the row still fills the line. */
    .careers__title { flex: 1 1 auto; width: auto; }
    .careers__dept  { width: 140px; }
  }

  /* Mobile: title over a meta row, icon alone at the right, no hover.
     Grid placement rather than a wrapper, so the markup stays one copy
     shared with desktop. min-width on the title sets the first two
     columns' combined width — the meta row beneath is narrower than the
     comp's 194px title box, and without it the title would wrap to
     whatever "Client Services Full-time" happens to measure. */
  @media (max-width: 640px) {
    .careers {
      padding: 88px 0;
      --careers-inset: var(--page-margin);
    }

    .careers__head { margin-bottom: 48px; }

    .careers__intro {
      grid-column: 1 / -1;
      gap: 12px;
      font-size: 24px;
      line-height: 1.04;
      letter-spacing: -0.48px;
    }

    .careers__list::before { display: none; }

    .careers__row {
      padding: 8px var(--careers-inset);
      border-top: 0.5px solid rgba(var(--bb-black-rgb), 0.5);
    }

    /* No hover here, so the tint never shows. */
    .careers__tile { display: none; }

    .careers__fields {
      display: grid;
      grid-template-columns: max-content max-content 1fr;
      align-items: center;
      column-gap: 12px;
      row-gap: 8px;
    }

    .careers__title {
      grid-area: 1 / 1 / 2 / 3;
      min-width: 194px;
      width: auto;
      font-size: 18px;
      line-height: 1.35;
      letter-spacing: -0.36px;
    }

    .careers__dept,
    .careers__type {
      font-size: 14px;
      line-height: 1.28;
      letter-spacing: -0.28px;
      color: rgba(var(--bb-black-rgb), 0.69);
    }

    .careers__dept { grid-area: 2 / 1 / 3 / 2; width: auto; }
    .careers__type { grid-area: 2 / 2 / 3 / 3; }

    /* Always visible: there is no hover to reveal it, so the icon is the
       affordance. The word is dropped, as the comp has it. */
    .careers__apply {
      grid-area: 1 / 3 / 3 / 4;
      justify-self: end;
      margin-left: 0;
      /* The label is hidden here, so there is no baseline to meet, and the
         row is a grid whose height this does not set — the desktop nudge
         and trim would only push the icon off the row's centre. */
      top: 0;
      margin-top: 0;
      margin-bottom: 0;
      opacity: 1;
    }

    /* The icon keeps its full 32px box, so the tap target clears 24px
       (§ 2.5.8) even though only the 22px square shows. The negative
       margin keeps that square flush with the row's inset. */
    .careers__apply .cta__label { display: none; }
    .careers__apply { margin-right: -5px; }
  }

  /* ------------------------------------------------------------------
     The Combine  (who-we-are only; Figma 2175:10679)

     A second Vimeo background, pinned the way the reel is (see
     .reel-scope): the stage is the first child of .combine-scope, which
     also holds the intro, the Tadiem banner and the footer. A sticky box
     is confined to its parent, so with all four in one wrapper the video
     stays fixed until the footer has covered it — whatever height the
     banner and footer come out at. The earlier version kept the stage
     inside .combine and pulled the banner up into its scroll range with
     arithmetic, which only met the footer at some window heights; on a
     phone the video scrolled away ~300px before the footer arrived.

     --combine-intro-hold is how much scroll the intro gets before the
     banner starts climbing, in vh so it scales with the screen.
     ------------------------------------------------------------------ */
  /* On :root, not on .combine, because .tadiem--combine reads
     --combine-tail too and it is .combine's sibling. */
  :root {
    --combine-intro-hold: 120vh;
    /* Video still showing between the banner and the footer (the comp has
       ~179px of it). */
    --combine-tail: 180px;
  }

  .combine {
    /* Page margin + one of twelve columns + one gap — the grid's own
       arithmetic for the comp's 138px inset, the same calc .careers
       uses, so the type lines up with the sections above at every width
       rather than only at 1440. */
    --combine-inset: calc(
      var(--page-margin)
      + (100% - 2 * var(--page-margin) - 11 * var(--grid-gap)) / 12
      + var(--grid-gap)
    );
    /* Starts where careers ends: the stage before it is the screen tucked
       behind careers (see .combine-scope), so the intro scrolls across the
       already-pinned video. Stacked above the stage but transparent to the
       pointer, so a click anywhere on the video reaches the stretched
       play/pause control under it; the CTA opts back in. */
    position: relative;
    z-index: 1;
    pointer-events: none;
  }

  .combine a { pointer-events: auto; }

  /* The wrapper the video pins to — see the block comment above.

     Pulled up one screen behind the sections before it (the team and
     careers, stacked above it — see .team and .careers), so the video is
     already pinned when it comes into view and is uncovered as careers
     scrolls off, the way the reel sits behind the hero. Without the pull
     the wrapper started below careers and the video slid up into place
     before it pinned. */
  /* lvh, not svh, for the stage and the pull that cancels it: on a phone
     the browser's address bar slides away as you scroll down, the screen
     gets taller by its height, and a 100svh stage (sized with the bar
     showing) left a blank strip of that height under the video until you
     scrolled back up. 100lvh is the screen with the bar hidden, so the
     video always covers it; with the bar showing, the extra sits behind
     the bar. The 100vh line is the fallback for browsers without lvh,
     where it means the same thing on Android Chrome. */
  .combine-scope {
    position: relative;
    z-index: 0;
    margin-top: -100vh;
    margin-top: -100lvh;
  }

  .combine__stage {
    position: sticky;
    top: 0;
    height: 100vh;
    height: 100lvh;
    /* The Vimeo player is absolutely positioned against this box. */
    overflow: hidden;
    background: var(--bb-black);
  }

  /* A 15% black wash over the Combine film, so the white type and the
     play pill carry over bright frames. Inside .vimeo-bg, after the
     poster and before the controls (see the markup), so it dims both the
     video and its poster but never the control. */
  .combine__tint {
    position: absolute;
    inset: 0;
    background: rgba(var(--bb-black-rgb), 0.15);
    pointer-events: none;
  }

  .combine__intro {
    position: relative;
    height: var(--combine-intro-hold);
    display: flex;
    align-items: center;
    padding-left: var(--combine-inset);
  }

  /* The comp's blurred black block (1944:14418): 808x210 at 100px blur
     and half opacity. It is what keeps the type legible over a frame the
     video picks, so it tracks the text rather than the section. */
  .combine__scrim {
    position: absolute;
    left: 0;
    top: 50%;
    transform: translateY(-50%);
    width: 808px;
    height: 210px;
    background: var(--bb-black);
    /* Heavier than the comp's 50%: spread over 100px of blur it has to
       carry white and cream type over whatever the background happens to
       be behind it, and the interior still is bright where the first line
       sits. This is the dial if it reads too dark. */
    opacity: 0.7;
    filter: blur(100px);
    pointer-events: none;
  }

  /* Two levels, as the comp draws them (1944:14419): the pair of lines is
     its own group on an 8px gap, and the 32px is between that group and
     the CTA — not between all three. */
  .combine__content {
    position: relative;
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 32px;
  }

  .combine__text {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 8px;
  }

  .combine__lead,
  .combine__sub {
    font-family: "Haas Recast VF", "Helvetica Neue", Arial, sans-serif;
    font-weight: 400;
    font-variation-settings: 'TRAK' 0;
    font-size: 30px;
    line-height: 1.1;
    letter-spacing: -0.6px;
  }

  .combine__lead {
    color: var(--bb-white);
    /* The comp's 611px text box. max-width rather than width because the
       copy is shorter than the box at every size this renders at, so the
       two are identical here and this one survives a narrow viewport. */
    max-width: 611px;
  }

  /* nowrap is the comp's: the second line is meant to run unbroken under
     the first. Dropped below 900px, where it would overflow the screen. */
  .combine__sub {
    color: var(--bb-cream);
    white-space: nowrap;
  }

  /* Sky icon box here, against the pink the Tadiem card below uses — the
     two CTAs sit in the same scroll and the comp separates them by it. */
  /* White label, as the comp has it: .cta--on-dark recolours the icon box
     and glyph but leaves the word to inherit, which over the background
     is black. */
  .combine__cta .cta__label { color: var(--bb-white); }
  .combine__cta .cta__icon-box { background: var(--bb-sky); }
  .combine__cta .cta__icon-glyph svg path { stroke: var(--bb-black); }

  @media (max-width: 900px) {
    :root {
      --combine-intro-hold: 100vh;
    }
    .combine__lead,
    .combine__sub { font-size: 24px; letter-spacing: -0.48px; }
    .combine__sub { white-space: normal; }
    .combine__scrim { width: 100%; }
  }

  @media (max-width: 640px) {
    .combine__intro { padding-left: var(--page-margin); padding-right: var(--page-margin); }
    .combine__lead,
    .combine__sub { font-size: 20px; letter-spacing: -0.4px; }
  }

  /* Tadiem collective banner */

  .tadiem {
    background: var(--bb-off-white);
    display: flex;
    flex-direction: column;
    align-items: center;
    padding: 60px var(--page-margin) 20px;
  }

  /* Deliberate exception to the universal page margin. The banner is a
     drawn object rather than a page section: the 86/106/107 inset here
     and the card's own 48px below are the proportions the composition
     was built at, and the card is capped at 1188px instead of riding the
     12-column grid. The margin tokens are used on the responsive
     overrides further down, where this collapses into a normal padded
     section — it's only the desktop composition that sits off-grid. */
  /* Capped at 1328px — its width at the 1440 comp — so on a wider window
     the module holds its size and centres (the section is a centred flex
     column) while whatever is behind the section keeps stretching: the
     off-white here, the Combine video on Who we are. */
  .tadiem-bg {
    position: relative;
    width: 100%;
    max-width: 1328px;
    height: 548px;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 86px 106px 107px;
    overflow: hidden;
  }

  .tadiem-bg__pattern {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
    z-index: 0;
  }

  .tadiem-card {
    position: relative;
    z-index: 1;
    background: var(--bb-burgundy);
    color: var(--bb-cream);
    width: 100%;
    max-width: 1188px;
    height: 357px;
    display: flex;
    align-items: center;
    /* Off-grid by design — see the note on .tadiem-bg above. */
    padding: 48px 0 48px 48px;
    gap: 40px;
  }

  .tadiem-card__col {
    display: flex;
    flex-direction: column;
    gap: 24px;
    flex-shrink: 0;
    width: 368px;
  }

  .tadiem-card__heading {
    font-family: "Haas Recast VF", "Helvetica Neue", Arial, sans-serif;
    font-weight: 400;
    font-variation-settings: 'TRAK' -35;
    font-size: 40px;
    line-height: 0.97;
    letter-spacing: -0.2px;
    color: var(--bb-cream);
  }

  .tadiem-card__desc {
    font-family: "Haas Recast VF", "Helvetica Neue", Arial, sans-serif;
    font-weight: 400;
    font-variation-settings: 'TRAK' 0;
    font-size: 16px;
    line-height: 1.24;
    color: var(--bb-cream);
    max-width: 414px;
  }

  .tadiem-card__logos {
    display: flex;
    flex: 1;
    flex-wrap: wrap;
    row-gap: 24px;
    column-gap: 24px;
    align-items: center;
    justify-content: center;
    min-width: 0;
  }

  .tadiem-card__logo-group {
    display: flex;
    align-items: center;
    gap: 24px;
    flex-shrink: 0;
  }

  .tadiem-card__logos svg { flex-shrink: 0; width: 72px; height: 72px; }
  .tadiem-card__logos svg.bb-mark { width: 58px; height: 57px; }
  .tadiem-card__logos svg path { fill: currentColor; }

  .tadiem-card__logo-box {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 72px;
    height: 72px;
    flex-shrink: 0;
  }

  @media (max-width: 1100px) {
    .tadiem-bg { height: auto; padding: 40px var(--page-margin); }
    .tadiem-card {
      flex-direction: column;
      height: auto;
      padding: 32px;
      gap: 32px;
      align-items: flex-start;
    }
    .tadiem-card__col { width: 100%; }
    .tadiem-card__logos {
      width: 100%;
      justify-content: flex-start;
    }
  }

  @media (max-width: 640px) {
    /* Inset from the screen edge on every side rather than full bleed,
       so the pattern reads as a framed object here as it does above. */
    .tadiem { padding: var(--page-margin); }
    .tadiem-bg { padding: 80px var(--page-margin); }
    .tadiem-card { padding: 40px var(--page-margin); gap: 48px; }
    .tadiem-card__heading { font-size: 30px; letter-spacing: -0.15px; }
    .tadiem-card__logos {
      width: 100%;
      justify-content: center;
      row-gap: 16px;
      column-gap: 16px;
    }
    .tadiem-card__logo-group { gap: 16px; }
    .tadiem-card__logos svg { width: 57.6px; height: 57.6px; }
    .tadiem-card__logos svg.bb-mark { width: 46.4px; height: 45.6px; }
    .tadiem-card__logo-box { width: 57.6px; height: 57.6px; }
  }

  /* Footer */

  .footer {
    background: var(--bb-burgundy);
    color: var(--bb-cream);
    /* 1vh taller than the screen, with the extra 1vh as top padding, so
       at the bottom of the page the footer overfills the viewport and no
       sliver of the section above shows along its top edge (phones
       resizing the viewport as the address bar moves were the usual
       case). */
    min-height: 101vh;
    display: flex;
    flex-direction: column;
    justify-content: space-between;
    align-items: flex-end;
    padding: calc(var(--page-margin) + 1vh) var(--page-margin) var(--page-margin);
    gap: 60px;
  }

  /* Four columns on the page grid, two tracks each (Figma 39:7324) —
     so at 12 tracks they occupy the left 8 and leave 4 empty, and at the
     8-track tablet grid they fill the row. */
  .footer__links {
    display: grid;
    grid-template-columns: repeat(var(--grid-cols), 1fr);
    gap: var(--grid-gap);
    width: 100%;
    font-family: "Haas Recast VF", "Helvetica Neue", Arial, sans-serif;
    font-weight: 400;
    font-variation-settings: 'TRAK' 0;
    color: inherit;
  }

  /* The footer is a list of lines, not running prose, so it opts out of
     the sitewide 14px paragraph break and sets its own rhythm instead:
     every line advances by the column's own 1.24 line-height, whether
     it is a separate <p> or a wrapped second line inside one. Change the
     1.24 here and in .footer__col together to retune.

     Where the 0.92em comes from: trimmed, a line's box is cap height
     (0.72em), and its reveal mask adds the 0.2em/0.25em of clip room
     that lets ascenders and descenders through. What survives margin
     collapsing is 0.92em of occupied space, so this tops it up to a
     full 1.24em step rather than setting the gap outright.

     A margin reset used to live here, and zeroing it was what left the
     columns stacked cap-to-baseline once the leading was trimmed out. */
  .footer__links p + p {
    margin-top: calc(1.24em - 0.92em);
  }

  /* Two links stacked in one column (Work with us / Join our team,
     LinkedIn / Instagram) sit 25px apart, centre to centre, not the
     17px text rhythm above. Each link is only 16px tall, so WCAG 2.5.8
     wants a 24px circle on one to clear the next; at 17px they
     overlapped. The address is one link over two lines and doesn't need
     it, but takes the same pitch so all three columns read alike. */
  .footer__links p:has(> a) + p:has(> a),
  .footer__col > a > p + p {
    margin-top: calc(25px - 0.92em);
  }


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

  .footer__col a:hover {
    text-decoration: underline;
  }

  .footer__col {
    grid-column: span 2;
    font-size: 14px;
    line-height: 1.24;
    white-space: nowrap;
  }

  .footer__col--legal {
    /* Matches .footer__col rather than keeping its own 1.5: below 640px
       .footer__col drops white-space:nowrap and this column is the one
       long enough to wrap, which at 1.5 would put its wrapped line off
       the rhythm the other three columns are on. */
    line-height: 1.24;
    color: rgba(var(--bb-cream-rgb), 0.75);
  }

  .footer__wordmark {
    width: 100%;
  }

  /* Inline SVG rather than an <img>, so the mark takes the footer's own
     colour. fill comes from currentColor; .footer sets the colour. */
  .footer__wordmark-svg {
    display: block;
    width: 100%;
    height: auto;
    fill: currentColor;
  }

  @media (max-width: 900px) {
    .footer { padding: calc(var(--page-margin) + 1vh) var(--page-margin) var(--page-margin); gap: 80px; }
    /* --grid-cols drops to 6 here, which cannot seat four columns at two
       tracks each. Hold the 8-track tablet grid until the footer stacks
       into a single column at 640px. */
    .footer__links { grid-template-columns: repeat(var(--grid-cols-tablet), 1fr); }
  }

  @media (max-width: 640px) {
    .footer { padding: 1vh 0 0; }
    .footer__links {
      display: flex;
      flex-direction: column;
      align-items: flex-start;
      /* Stacked, the four columns read as groups by the space between
         them: 40px, well clear of the 25px line pitch inside a group,
         which is already the least WCAG 2.5.8 allows for stacked
         single-line links (see .footer__links p + p above). */
      gap: 40px;
      width: 100%;
      /* .footer drops its own padding at this width and the links block
         carries the inset instead, so the top matches the page margin
         the way it does on desktop. The 20px bottom is the gap down to
         .footer__wordmark, not a margin, so it stays as drawn. */
      padding: var(--page-margin) var(--page-margin) 20px;
      color: var(--bb-off-white);
    }
    .footer__col { font-size: 14px; white-space: normal; }
    .footer__wordmark { padding: var(--page-margin); }
  }

  /* ------------------------------------------------------------------
     Who We Are only  (Figma 1944:14424 / 1944:14438)

     The page-scoped recolour. Both blocks are shared — the banner also
     runs on the homepage and our-network, the footer on nine pages — so
     the changes hang off a modifier the markup adds on this page alone,
     and nothing else moves. Geometry is untouched in both: this is
     colour, plus the banner giving up its background so the video behind
     it shows through.
     ------------------------------------------------------------------ */

  /* Transparent, not off-white: the .combine video runs behind this
     whole section, and the z-index puts the banner over the stage rather
     than under it. It follows the intro in .combine-scope, so it climbs
     across the still-pinned video as the comp has it. Like .combine it
     lets the pointer through to the video except on the card itself. */
  .tadiem--combine {
    position: relative;
    z-index: 1;
    margin-bottom: var(--combine-tail);
    background: transparent;
    pointer-events: none;
  }

  .tadiem--combine .tadiem-card { pointer-events: auto; }

  .tadiem--combine .tadiem-card {
    background: var(--bb-navy);
  }

  /* Pink box against the navy card, where the shared rule uses sky. */
  .tadiem--combine .tadiem-card .cta__icon-box { background: var(--bb-pink); }
  .tadiem--combine .tadiem-card .cta__label { color: var(--bb-off-white); }

  /* Case study footers take their header's colour (see .cs-hero--navy
     and friends); burgundy is the default above. Background only: the
     cream text holds AA on all of them, the legal column's 75% tint
     included (red 4.7, forest 6.1, navy 7.1). Each footer's data-nav-bg
     carries the same hex. */
  .footer--red    { background: var(--bb-red); }
  .footer--navy   { background: var(--bb-navy); }
  .footer--forest { background: var(--bb-forest); }

  /* Main pages: each footer is a background, a wordmark colour and a
     text colour. The text stays cream everywhere (the legal column at
     the default 75%); the background comes from the modifiers above and
     the wordmark from these. Case study footers keep a cream wordmark.

       Homepage     burgundy  · orange wordmark (4.6:1)
       What we do   red       · sky wordmark    (6.4:1)
       Who we are   forest    · pink wordmark   (7.0:1)

     Our network and Careers are still placeholders; their colours are
     recorded in CLAUDE.md for when the full pages come back. */
  .footer--mark-orange .footer__wordmark { color: var(--bb-orange); }
  .footer--mark-sky    .footer__wordmark { color: var(--bb-sky); }
  .footer--mark-pink   .footer__wordmark { color: var(--bb-pink); }

  /* Positioned for the same reason as the banner above it: .combine is a
     stacking context, so a static footer would paint under its pinned
     video rather than closing it off. */
  .footer--combine {
    position: relative;
    z-index: 1;
  }

  /* Carousel controls — off-screen until focused (Figma has no place for
     them, so they are not drawn at rest).

     They exist because the track is a GSAP Draggable and dragging cannot be
     the only way to move it. The slider resource already wires anything
     carrying [data-gsap-slider-control]: click, disabled at the ends and
     aria-disabled, all for free — the markup simply never included any.

     This closes the keyboard gap (2.1.1): the controls are real buttons in
     the tab order and show themselves when tabbed to. It does NOT close
     2.5.7 Dragging Movements, which asks for a single-POINTER alternative —
     a mouse or touch user still has only the drag, because these are
     invisible until focused. Shipping that would mean revealing them on
     hover too, which is a design decision rather than a code one. */
  .cs-slider__controls {
    display: flex;
    gap: 8px;
  }

  .cs-slider__control {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
    background: none;
    color: inherit;
    font: inherit;
    cursor: pointer;
  }

  .cs-slider__control:focus-visible {
    position: static;
    width: auto;
    height: auto;
    margin: 0;
    overflow: visible;
    clip-path: none;
    padding: 8px 12px;
    background: var(--bb-black);
    color: var(--bb-off-white);
    font-family: "Haas Recast VF", "Helvetica Neue", Arial, sans-serif;
    font-size: 14px;
    line-height: 1;
    border-radius: 4px;
  }

  .cs-slider__control[disabled] {
    opacity: 0.4;
    cursor: default;
  }

  /* A slider whose slides already fit has nothing to drag, and the resource
     marks it not-active rather than initialising it. Its controls would do
     nothing, so they come out of the tab order too — there is no drag here
     for them to be the alternative to. */
  [data-gsap-slider-status="not-active"] .cs-slider__controls {
    display: none;
  }

  /* ==========================================================================
     Number odometer — Osmo Supply's rolling-digit scroll animation (see
     assets/javascript/number-odometer.js, kept unmodified). Unscoped, on
     the [data-odometer-*] attributes the script targets, not under any
     .cs- namespace — this is shared, sitewide infrastructure, first used
     on case-study stats (.cs-stats__number below) but meant to be reused
     as-is for any other large-number callout (e.g. who-we-are, careers).
     A consuming element supplies its own font/color/size; this block only
     provides the structural rules the roller mechanism depends on. */
  /* Exempt from the sitewide trim (see the * rule at the top of the file).
     buildRollerDOM() stacks the digits 0-9 as newline-separated lines in a
     white-space:pre roller and parks digit n at exactly n x step em, with
     the mask clipping to a one-step window. That arithmetic only holds
     while every line box is a full step tall — trim-both would shorten
     the strip's first and last line and slide every digit out of its
     window. Nothing here is optical spacing, so there's nothing to gain
     by trimming it: the consuming element (e.g. .cs-stats__number) is
     what gets trimmed, and it still does. */
  [data-odometer-element],
  [data-odometer-element] * {
    text-box: normal;
  }

  [data-odometer-element] {
    /* ---- Tracking knob -------------------------------------------------
       Tighten or loosen the rolled characters here and nowhere else. The
       value is in em, i.e. a share of whatever font-size the consuming
       element sets: -0.04em is 4% of the type size pulled out from
       between every pair of characters, and it stays proportional when
       .cs-stats__number drops from 54px to 44px to 28px across the
       breakpoints. A single consumer can override it on itself.

       It has to be a margin rather than letter-spacing: the odometer
       gives every character its own inline-block, and this element is an
       inline-flex container, so letter-spacing has nothing to act on
       between them. Nor can the tracking come from the font — the
       variable TRAK axis runs -35 to 0 and .cs-stats__number already
       sits at -35, the tight end.

       What it is correcting: tabular-nums below is not optional, since
       every digit cell has to be the same width or the numbers would
       jitter as they roll. Tabular figures are set on the widest digit's
       advance, which on Haas Recast runs about 0.045em per character
       wider than the proportional figures the rest of the site uses. At
       -0.04em the odometer sets at roughly the width it would have as
       plain text. */
    --odometer-tracking: -0.04em;

    display: inline-flex;
    /* Baseline, not centre: the static parts (",", "%", "+") carry the
       deeper bottom padding below, so a taller box centred with the digit
       masks sat about 0.05em higher — the digits read low against their
       "$", "M" and "%". Baseline alignment puts every character on one
       line whatever its box. */
    align-items: baseline;
    font-variant-numeric: tabular-nums;
  }

  [data-odometer-part="mask"],
  [data-odometer-part="static"] {
    display: inline-block;
    overflow: clip;
    padding: 0.05em;
    margin: -0.05em;
  }

  /* Between characters only — never leading the first or trailing the
     last, so the element's own box still measures its ink. buildRoller's
     width transition reads that box (oldWidthEm/newWidthEm in
     number-odometer.js), so a trailing pull would quietly shorten it. */
  [data-odometer-element] > * + * {
    margin-inline-start: calc(-0.05em + var(--odometer-tracking));
  }

  [data-odometer-part="roller"] {
    display: block;
    white-space: pre;
    text-align: center;
    will-change: transform;
  }

  [data-odometer-part="static"] {
    display: inline-block;
  }

  /* Room for a comma's tail. The script gives each static character
     (",", "%", "+") an inline height of 0.92em and overflow: hidden for its
     width reveal, which clips at 0.05em below the baseline. A comma drops
     about 0.14em, so "6,000" read as "6.000". Height back to auto (the
     same 0.92em line box, set by the script's line-height), and a deeper
     bottom padding cancelled by the same negative margin, so the clip
     edge moves down and nothing moves on screen. !important only because
     the height it overrides is an inline style. */
  [data-odometer-part="static"] {
    height: auto !important;
    padding-bottom: 0.25em;
    margin-bottom: -0.25em;
  }

  /* ==========================================================================
     Case study page template (case-study-template.html). Three sections are
     shared across every case study — hero, background-image stats, and
     related projects — and live under the .cs- namespace below. The body
     content each project builds between the stats and related-projects
     sections is project-specific and reuses the site's existing section
     patterns instead (see what-we-do.html / index.html), not these classes.

     .cs-grid is the ONE definition of the grid every case-study section
     builds on (hero, stats, text-block, gallery, quote, slider/related —
     and any new section layout added later). Every section's grid
     container carries this class in the markup alongside its own BEM
     class; the BEM class then only adds what's genuinely different about
     that section (padding, sticky positioning, which columns its content
     occupies) and never restates display/gap/width — so there is exactly
     one place to fix or change the grid itself, and a missing-gap bug
     like this one can't happen again.

     grid-template-columns defaults to the desktop constant (a section
     that keeps 12 tracks all the way through tablet — repositioning
     content rather than reducing column count — needs no override here).
     .cs-text-block__row/.cs-quote are the two sections whose tablet
     layout genuinely drops to 8 tracks; they override this one property
     with the responsive --grid-cols var, see their own rules below. */
  .cs-grid {
    display: grid;
    grid-template-columns: repeat(var(--grid-cols-desktop), 1fr);
    gap: var(--grid-gap);
    width: 100%;
  }

  /* min-height rather than height, and top padding that clears the
     139px nav, so a short window grows the hero instead of pushing the
     headline up behind the navbar. The bottom padding tightens with the
     window's height like the homepage hero's gaps. */
  .cs-hero {
    background: var(--bb-red);
    min-height: 90vh;
    display: flex;
    align-items: flex-end;
    padding: 160px var(--page-margin) clamp(24px, 5.8vh, 52px);
  }

  /* Both columns top-align, and the summary drops by the eyebrow's height
     plus the intro's gap, so its body copy starts level with the headline,
     not the eyebrow. The pair still sits at the bottom of the hero
     (.cs-hero is align-items: flex-end), so the taller column meets the
     bottom edge.

     The offset is derived, not measured: the eyebrow is 20px, trimmed to
     its cap height (0.72em on Haas Recast), and the intro gap is 32px —
     46.4px, the headline's measured distance from the column top. Below
     901px the columns stack and it no longer applies. */
  .cs-hero {
    --cs-hero-headline-offset: calc(20px * 0.72 + 32px);
  }

  .cs-hero__intro {
    grid-column: 2 / span 5;
    align-self: start;
    display: flex;
    flex-direction: column;
    gap: 32px;
  }

  .cs-hero__eyebrow {
    font-family: "Haas Recast VF", "Helvetica Neue", Arial, sans-serif;
    font-weight: 600;
    font-variation-settings: 'TRAK' 0;
    font-size: 20px;
    line-height: 1;
    color: var(--bb-off-white);
  }

  .cs-hero__heading {
    font-family: "Haas Recast VF", "Helvetica Neue", Arial, sans-serif;
    font-weight: 400;
    font-variation-settings: 'TRAK' -35;
    font-size: 67px;
    line-height: 0.93;
    letter-spacing: -1.34px;
    color: var(--bb-off-white);
    text-wrap: balance;
  }

  /* Forest ground, for articles (Figma 2145:8465). Everything else is the
     case-study hero: Off-white type on Forest is 11.4:1 and Cream 9.6:1. */
  /* Per-case-study header colours. Each case study picks the one that
     sits apart from its first image (no red behind Vanguard's red floor).
     Background only: the off-white text holds AA on all four (burgundy
     14.1, navy 13.9, forest 11.4, red 8.5). Orange is left out on
     purpose — off-white on it is 3.0, which fails for the body copy.
     Each hero's data-nav-bg carries the same hex so the nav matches. */
  .cs-hero--burgundy { background: var(--bb-burgundy); }
  .cs-hero--navy     { background: var(--bb-navy); }
  .cs-hero--forest   { background: var(--bb-forest); }

  .cs-hero__summary {
    grid-column: 8 / span 4;
    align-self: start;
    display: flex;
    flex-direction: column;
    justify-content: space-between;
    min-height: 234px;
    gap: 24px;
    padding-top: var(--cs-hero-headline-offset);
  }

  .cs-hero__copy {
    font-family: "Haas Recast VF", "Helvetica Neue", Arial, sans-serif;
    font-weight: 400;
    font-variation-settings: 'TRAK' 0;
    font-size: 18px;
    line-height: 1.28;
    letter-spacing: -0.36px;
    color: var(--bb-cream);
  }

  .cs-hero__tags {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
  }

  /* Case-study hero: the same pill on Red. Named for where it is used
     rather than what colour it was — it has not been maroon since the
     recolour to black-20%/Cream-80% (see BRAND.md). */
  .tag--hero {
    background: rgba(var(--bb-black-rgb), 0.2);
    color: rgba(var(--bb-cream-rgb), 0.8);
  }

  @media (max-width: 1200px) {
    .cs-hero__intro { grid-column: 1 / span 6; }
    .cs-hero__summary { grid-column: 7 / span 6; }
    .cs-hero__heading { font-size: 52px; letter-spacing: -1px; }
  }

  @media (max-width: 900px) {
    /* min-height (not height) so a project with more hero copy/tags than
       Saintly's can grow past 90vh instead of clipping. */
    .cs-hero { height: auto; min-height: 90vh; padding: 160px var(--page-margin) 20px; }
    .cs-hero__inner { display: flex; flex-direction: column; gap: 40px; width: 100%; }
    .cs-hero__intro { grid-column: auto; gap: 24px; width: 100%; }
    .cs-hero__intro > * { width: 100%; }
    .cs-hero__summary { grid-column: auto; min-height: 0; gap: 32px; }
    .cs-hero__eyebrow { font-size: 18px; letter-spacing: -0.36px; }
    .cs-hero__heading { font-size: 48px; letter-spacing: -0.96px; line-height: 0.96; }
    .cs-hero__copy { font-size: 16px; letter-spacing: -0.32px; line-height: 1.13; }
  }

  /* Video header — the alternative hero for a case study with a featured
     film (Figma 2215:10965 desktop, 2137:850 mobile). It REPLACES both
     .cs-hero and .cs-stats: the stats move up into the summary column as
     small cards, and the film sits under the copy on the same Red
     ground. Everything else — intro, copy, tags — is the ordinary hero's
     markup and rules, unchanged.

     The film sets the section's height; only the space above the copy
     is tied to the window, below. */
  /* Like the plain hero: the copy sits at the foot of the first 90vh,
     so the headline and summary are on the first screen, and the film
     follows below. The comp's fixed 400px above the copy left a short
     window with a screen of empty navy under the nav and the headline
     cut off at the fold. The top padding is the 160px every case-study
     hero keeps under the 139px nav, and __inner's min-height is the rest
     of that 90vh: when the copy is taller than that, it simply starts at
     160px and the section grows. */
  .cs-hero--video {
    height: auto;
    flex-direction: column;
    align-items: stretch;
    gap: 40px;
    padding: 160px var(--page-margin) 80px;
  }

  .cs-hero--video .cs-hero__inner {
    min-height: calc(90vh - 160px);
    align-content: end;
  }

  /* The comp drops the column to the top of its copy with 46px of air
     and stacks it at a plain 32px — no space-between, since the film
     below now fixes where this column ends, not the viewport. The drop to
     the headline is the shared --cs-hero-headline-offset (see
     .cs-hero__intro); the comp's "46px of air" is that same distance. */
  .cs-hero--video .cs-hero__summary {
    justify-content: flex-start;
    min-height: 0;
    gap: 32px;
  }

  .cs-hero__stats {
    display: flex;
    gap: 10px;
  }

  /* The nested reveal group wraps each card in a .reveal-mask, so the
     flex sizing goes on whatever the direct child is. */
  .cs-hero__stats > * {
    flex: 1 1 0;
    min-width: 0;
  }

  .cs-hero__stat {
    height: 145px;
    padding: 12px 10px;
    border-radius: 4px;
    background: var(--bb-off-white);
    color: var(--bb-red);
    display: flex;
    flex-direction: column;
    justify-content: space-between;
    /* A size container, so the number below can cap itself to the
       card's own width. The cards are equal-width flex items, so the
       width never depends on their content and every number in a row
       resolves to the same size. */
    container-type: inline-size;
  }

  .cs-hero__stat > p + p { margin-top: 0; }

  .cs-hero__stat-number {
    font-family: "Haas Recast VF", "Helvetica Neue", Arial, sans-serif;
    font-weight: 400;
    font-variation-settings: 'TRAK' -35;
    /* The comp's 47px, capped at 38% of the card's inner width. Four
       characters like "$5M+" set about 2.53px wide per px of type, so
       38cqi is the most that still fits, and it reaches 47px at the
       comp's 1440 width, where the cap does nothing. Narrower cards
       (901–960px, 1201–1370px and phones) used to push the number past
       the card's edge. */
    font-size: min(47px, 38cqi);
    line-height: 0.92;
  }

  .cs-hero__stat-label {
    font-family: "Haas Recast VF", "Helvetica Neue", Arial, sans-serif;
    font-weight: 400;
    font-variation-settings: 'TRAK' 0;
    font-size: 17px;
    line-height: 1.13;
    letter-spacing: -0.34px;
  }

  /* Columns 2–11, the same 10-column span as .cs-gallery. The comp's
     1164x655 is that span at 1440px; aspect-ratio keeps the shape. */
  .cs-hero__video {
    grid-column: 2 / span 10;
    position: relative;
    overflow: hidden;
    aspect-ratio: 1164 / 655;
  }

  @media (max-width: 1200px) {
    .cs-hero__video { grid-column: 1 / -1; }
  }

  /* Mobile: the comp's 288px above the copy, 20px + 32px to the film,
     32px under it. On phones (next block) the film goes to a fixed
     358x288 crop rather than 16:9, which the player's cover mode fills. */
  @media (max-width: 900px) {
    /* The same on phones, where __inner is a flex column and the nav is
       96px tall, so the copy clears it by 32px. */
    .cs-hero--video {
      min-height: 0;
      gap: 52px;
      padding: 128px var(--page-margin) 32px;
    }
    .cs-hero--video .cs-hero__inner {
      min-height: calc(90vh - 128px);
      justify-content: flex-end;
    }
    .cs-hero__summary { padding-top: 0; }
    .cs-hero__stats { gap: 8px; }
    .cs-hero__stat { height: 146px; padding: 8px; }
    .cs-hero__stat-number { font-size: min(38px, 38cqi); }
    .cs-hero__stat-label { font-size: 14px; letter-spacing: -0.28px; }
  }

  /* Phone widths only: at 800px this crop would stand 620px tall, so the
     641–900 band keeps the desktop 16:9. */
  @media (max-width: 640px) {
    .cs-hero__video { aspect-ratio: 358 / 288; }
  }

  /* Fixed-height image/pattern band. The background pattern is the
     template's placeholder (same burgundy/light-blue tile as the homepage
     cascade transition, at its native 240x36 2x size) — a project page
     that has real photography adds a .cs-stats__bg <img> on top of it
     instead (see saintly-wine-rebels-deserve-halos.html). */
  .cs-stats {
    position: relative;
    width: 100%;
    height: 85vh;
    background-color: var(--bb-red);
    background-image: url("../images/burgundy-lightblue-pattern-tile.png");
    background-repeat: repeat;
    background-size: 240px 36px;
  }

  /* Matches the hero above it (see .cs-hero--burgundy and friends), with
     data-nav-bg to match, so the nav keeps the hero's colour as the
     photo scrolls under it instead of flipping to red. Only the colour
     behind the image changes — the photo still covers it. */
  .cs-stats--burgundy { background-color: var(--bb-burgundy); }
  .cs-stats--navy     { background-color: var(--bb-navy); }
  .cs-stats--forest   { background-color: var(--bb-forest); }

  .cs-stats__bg {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
  }

  /* Sticks to the bottom of the viewport while .cs-stats scrolls by, then
     releases naturally once the section's own bottom edge passes. Uses a
     `top` offset rather than `bottom` — bottom-sticky only pins once an
     element's static position would fall below the pin line as you scroll,
     which never happens for a row sitting at the top of a tall container;
     `top: calc(100vh - row's own total height)` gets the same "hugs the
     bottom" look off the more reliable top-sticky behavior. The row's own
     30px top/bottom padding both supplies the visual gap off the screen
     edge and counts toward that height (234px card + 30px + 30px = 294px)
     — bump both together if the card height ever changes. Grid-aligned to
     the site's 12-column convention: column 1 stays empty, each card spans
     2 of the remaining columns. */
  .cs-stats__row {
    position: sticky;
    top: calc(100vh - 294px);
    /* Horizontal is the universal page margin; the 30px vertical is not
       decorative — it's half of the 294px the sticky top calc above is
       built from, so the two move together. */
    padding: 30px var(--page-margin);
  }

  .cs-stats__card:nth-child(1) { grid-column: 2 / span 2; }
  .cs-stats__card:nth-child(2) { grid-column: 4 / span 2; }
  .cs-stats__card:nth-child(3) { grid-column: 6 / span 2; }

  .cs-stats__card {
    background: var(--bb-off-white);
    color: var(--bb-red);
    height: 234px;
    padding: 12px;
    border-radius: 4px;
    display: flex;
    flex-direction: column;
    justify-content: space-between;
  }

  /* Covers both the plain <p> elements and the .reveal-mask wrapper
     main.js inserts around each one for the scroll-in animation (see the
     data-reveal-group comment on .cs-stats__row markup) — either way, the
     card's content should fill its width, not shrink to its text. */
  .cs-stats__card > * {
    width: 100%;
  }

  .cs-stats__number {
    font-family: "Haas Recast VF", "Helvetica Neue", Arial, sans-serif;
    font-weight: 400;
    font-variation-settings: 'TRAK' -35;
    font-size: 54px;
    line-height: 0.92;
  }

  .cs-stats__label {
    font-family: "Haas Recast VF", "Helvetica Neue", Arial, sans-serif;
    font-weight: 400;
    font-variation-settings: 'TRAK' 0;
    font-size: 17px;
    line-height: 1.13;
    letter-spacing: -0.34px;
  }

  /* Tablet uses the same row as mobile below (not a scaled-down version
     of desktop's layout) — just capping each card at 200px so they don't
     stretch to fill a much wider tablet row. Centered within its column
     via margin:auto, since capping width alone would otherwise leave it
     flush to one side of its 1fr track. The 200px cap is a no-op on true
     mobile, where a card is already narrower than that. */
  @media (max-width: 1200px) {
    /* Figma keeps the row and the sticky-bottom behavior on mobile too, but
       this row deliberately breaks from the site's 12-column grid — flex,
       not grid, left-aligned, each card growing to fill available space up
       to a 200px cap. On a tablet-width row that's wider than 3×200px+gaps,
       the cards just sit left with empty space after them, rather than
       being spread/centered across the full row. */
    .cs-stats__row {
      top: calc(100vh - 178px);
      display: flex;
      justify-content: flex-start;
      gap: var(--grid-gap);
      /* As on desktop: universal margin across, fixed 16px down — it's
         half of this breakpoint's own 178px sticky calc (146 + 16 + 16). */
      padding: 16px var(--page-margin);
    }
    .cs-stats__card {
      flex: 1 1 0;
      max-width: 200px;
      height: 146px;
      padding: 8px;
    }
    .cs-stats__number { font-size: 44px; }
    .cs-stats__label { font-size: 14px; letter-spacing: -0.28px; }
  }

  @media (max-width: 900px) {
    /* Deliberate exception to the site's 16px --grid-gap: on mobile the
       three stat cards sit 8px apart so all three still fit the row at
       their narrowest. This row is already off-grid (flex, see the 1200px
       block above), so the exception stays local to it — don't touch
       --grid-gap, and don't copy this 8px into other mobile sections. */
    .cs-stats__row { gap: 8px; }
    .cs-stats__number { font-size: 28px; }
    .cs-stats__label { font-size: 12px; }
  }

  /* ==========================================================================
     Case study body — the project-specific content between the stats
     section and related projects. .cs-body is the cream section shell;
     .cs-text-block is the most common content block within it: a fixed-
     width label, an optional heading, and either flowing paragraphs
     (.cs-text-block__body) or a plain list (.cs-text-block__list — roles,
     deliverables, awards, anything enumerable). Stack as many
     .cs-text-block elements as a project needs; each draws its own top
     rule, so consecutive blocks read as a continuous, evenly-spaced list.
     ========================================================================== */
  .cs-body {
    background: var(--bb-off-white);
  }

  .cs-text-block__inner {
    width: 100%;
    padding: 40px var(--page-margin);
  }

  /* The last block on a page, sitting on the footer: 80px more below it,
     on top of the inner's own padding, so the closing CTA is not pressed
     against the footer. The survey article's Download block. */
  .cs-text-block--closing {
    padding-bottom: 80px;
  }

  .cs-body > .cs-text-block:first-child .cs-text-block__inner {
    padding-top: 80px;
  }

  /* Same 12-column convention as .cs-hero__inner/.cs-stats__row: columns 1
     and 12 stay empty, label and content each take 5 of the 10 center
     columns (2–6 and 7–11). */
  .cs-text-block__row {
    grid-template-columns: repeat(var(--grid-cols), 1fr);
    row-gap: 32px;
  }

  /* The rule reads as the text's own underline, so it stops where the
     text stops: label starts at column 2, content ends at column 11, and
     the line spans exactly that. A border-top on .cs-text-block__row
     itself would run the full 12 columns and overhang the copy at both
     ends.

     It's a grid item rather than an absolutely-positioned bar so it
     inherits the track sizing — no restating of column widths or gap
     arithmetic. Placed on its own implicit row, with the label and
     content auto-placing into the row beneath it, and row-gap supplying
     the space the old padding-top did. Both narrower breakpoints run the
     copy edge to edge, so there the line goes back to being a plain
     border on the row (see the media queries below). */
  .cs-text-block__row::before {
    content: "";
    grid-column: 2 / span 10;
    border-top: 0.5px solid rgba(var(--bb-black-rgb), 0.5);
  }

  .cs-text-block__label {
    grid-column: 2 / span 5;
    font-family: "STIX Two Text", Georgia, "Times New Roman", serif;
    font-weight: 400;
    font-style: italic;
    font-size: 16px;
    line-height: 0.97;
    letter-spacing: 0.08px;
    /* 0.6, the tile descriptions' and team job titles' grey: about 5.5:1
       on Off-white. The 0.54 this replaced measured 4.4:1, under AA's 4.5
       for text this size. */
    color: rgba(var(--bb-black-rgb), 0.6);
  }

  .cs-text-block__content {
    grid-column: 7 / span 5;
    display: flex;
    flex-direction: column;
    gap: 32px;
  }

  /* Shared with .cs-split — the two blocks set the same heading and body
     spec, so it is stated once here rather than copied. */
  .cs-text-block__heading,
  .cs-split__heading {
    font-family: "Haas Recast VF", "Helvetica Neue", Arial, sans-serif;
    font-weight: 400;
    font-variation-settings: 'TRAK' 0;
    font-size: 30px;
    line-height: 1.1;
    letter-spacing: -0.6px;
    color: var(--bb-black);
  }

  .cs-text-block__body,
  .cs-split__body {
    font-family: "Haas Recast VF", "Helvetica Neue", Arial, sans-serif;
    font-weight: 400;
    font-variation-settings: 'TRAK' 0;
    font-size: 18px;
    line-height: 1.28;
    letter-spacing: -0.36px;
    /* 0.6, the tile descriptions' and team job titles' grey: about 5.5:1
       on Off-white. The 0.54 this replaced measured 4.4:1, under AA's 4.5
       for text this size. */
    color: rgba(var(--bb-black-rgb), 0.6);
  }

  .cs-text-block__list {
    list-style: none;
    font-family: "Haas Recast VF", "Helvetica Neue", Arial, sans-serif;
    font-weight: 400;
    font-variation-settings: 'TRAK' 0;
    font-size: 19px;
    line-height: 1.28;
    letter-spacing: -0.38px;
    color: var(--bb-black);
  }

  /* Footnotes/sources under a block's body — small print, not prose. At
     0.69 black (about 7.6:1), a step darker than the body's 0.6, so the
     smallest type on the page is not also the faintest. */
  .cs-text-block__note {
    font-family: "Haas Recast VF", "Helvetica Neue", Arial, sans-serif;
    font-weight: 400;
    font-variation-settings: 'TRAK' 0;
    font-size: 13px;
    line-height: 1.35;
    color: rgba(var(--bb-black-rgb), 0.69);
  }

  /* Tablet: --grid-cols is already 8 here (see :root), so .cs-text-block__row
     picks up an 8-column grid automatically — this just repositions
     label/content within it: 2/6 instead of desktop's 5/5-of-12-with-a-
     margin-column — no empty margin columns, and label gets
     proportionally less width than content. */
  @media (max-width: 1200px) {
    .cs-text-block__label { grid-column: 1 / span 2; }
    .cs-text-block__content { grid-column: 3 / span 6; }
    /* Copy runs the full 8 tracks here, so the line has nothing to inset
       from and goes back to being the row's own border. */
    .cs-text-block__row {
      border-top: 0.5px solid rgba(var(--bb-black-rgb), 0.5);
      padding-top: 32px;
    }
    .cs-text-block__row::before { display: none; }
  }

  @media (max-width: 900px) {
    .cs-body > .cs-text-block:first-child .cs-text-block__inner { padding-top: 40px; }
    .cs-text-block__row { grid-template-columns: 1fr; row-gap: 32px; padding-top: 16px; }
    .cs-text-block__label, .cs-text-block__content { grid-column: 1; }
    .cs-text-block__content { gap: 32px; }
    .cs-text-block__label, .cs-text-block__body, .cs-split__body { color: rgba(var(--bb-black-rgb), 0.69); }
    .cs-text-block__heading, .cs-split__heading { font-size: 26px; letter-spacing: -0.52px; line-height: 1.19; }
    .cs-text-block__body, .cs-split__body { font-size: 16px; letter-spacing: -0.32px; line-height: 1.35; }
    .cs-text-block__list { font-size: 18px; letter-spacing: -0.36px; }
  }

  /* Body themes (the survey article, Figma 2334:3381 / 2334:3387). A
     .cs-body carrying data-body-themes changes colour as a whole when a
     block marked data-body-theme scrolls in, rather than that block
     having its own background: like the Who we are hand-over from the
     cards to the team, the colour fades in and out across everything on
     screen, so no hard edge ever crosses the viewport. initBodyThemes()
     in main.js sets data-body-theme on the .cs-body itself; the
     transitions here make the change a timed fade either way.

     Navy: sky text throughout, 10.5:1. Sky: navy headings, and body copy
     keeps its black at 60% (4.9:1 on sky; 0.69 on phones, higher still).
     The Figma body grey (#706D68) is 3.4:1 on sky and fails AA, so it
     isn't used. */
  [data-body-themes],
  [data-body-themes] :is(.cs-text-block__label, .cs-text-block__heading,
    .cs-text-block__body, .cs-text-block__list, .cs-text-block__note,
    .cs-split__heading, .cs-split__body, .cs-quote__text,
    .cs-quote__attribution, .cs-caption) {
    transition: background-color 0.5s ease-in-out, color 0.5s ease-in-out;
  }

  [data-body-themes] .cs-text-block__row::before {
    transition: border-color 0.5s ease-in-out;
  }

  .cs-body[data-body-theme="navy"] {
    background-color: var(--bb-navy);
    color: var(--bb-sky);
  }

  .cs-body[data-body-theme="navy"] :is(.cs-text-block__label,
    .cs-text-block__heading, .cs-text-block__body, .cs-text-block__list,
    .cs-text-block__note, .cs-split__heading, .cs-split__body,
    .cs-quote__text, .cs-quote__attribution, .cs-caption) {
    color: var(--bb-sky);
  }

  .cs-body[data-body-theme="navy"] .cs-text-block__row::before {
    border-top-color: rgba(var(--bb-sky-rgb), 0.5);
  }

  .cs-body[data-body-theme="sky"] {
    background-color: var(--bb-sky);
  }

  .cs-body[data-body-theme="sky"] :is(.cs-text-block__heading,
    .cs-split__heading, .cs-quote__text) {
    color: var(--bb-navy);
  }

  /* Image-only gallery row — one flexible component for 1–4+ images (the
     four Figma variants — single/double/triple/quad — are just this same
     row with a different number of .cs-gallery__item children, each
     splitting the row evenly). Sits inside .cs-body alongside
     .cs-text-block, sharing its cream background and nav theming.

     No shared aspect-ratio is set here on purpose — like .p-card__img
     elsewhere in this file, each project's real photos rarely share one
     ratio. Set aspect-ratio inline per .cs-gallery__item to match the
     actual source image (the multi-image Figma rows consistently use
     574/718; a single full-width image is usually closer to its own
     natural ratio — measure the real asset rather than assuming either). */
  .cs-gallery {
    padding: 40px var(--page-margin);
  }

  .cs-gallery__row {
    grid-column: 2 / span 10;
    display: flex;
    gap: var(--grid-gap);
  }

  .cs-gallery__item {
    position: relative;
    flex: 1 1 0;
    min-width: 0;
    overflow: hidden;
  }

  .cs-gallery__item img {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
  }

  /* Before/after split slider (Osmo; assets/javascript/before-after-split-
     slider.js). The resource's rules, resized to fill a gallery frame
     (the frame sets the 16:9, so the wrapper drops the demo's own ratio,
     width and rounded corners) and coloured from the tokens. The frame's
     `img` rule above already sizes both images to cover. */
  .splitter-wrapper {
    position: relative;
    width: 100%;
    height: 100%;
    overflow: hidden;
    /* The left-right arrows: a press anywhere drags the split and a
       click anywhere moves it (see the JS). Draggable sets the same
       cursor inline on this element, so this is what shows before the
       script runs, and what the rule says rather than a pointer the
       inline style silently overrode. */
    cursor: ew-resize;
  }

  .splitter-content {
    z-index: 0;
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
  }

  /* Both slider images carry a thin white frame in the file itself
     (6px left, 4px right, 3px top, 2px bottom of 1920x1080). Each is set
     a few pixels past the frame's edges, as percentages of it, so the
     wrapper's overflow: hidden trims the white off without re-exporting
     the images. The frame is 16:9 like the files, so cover barely crops. */
  .cs-gallery__item .splitter-content__img {
    top: -0.46%;
    left: -0.42%;
    width: 100.73%;
    height: 100.83%;
    /* A press on either picture starts the split's drag (see the trigger
       note in before-after-split-slider.js), so the browser must not
       start dragging the image itself instead. draggable="false" on each
       <img> covers Firefox. */
    -webkit-user-drag: none;
    user-select: none;
  }

  .splitter-content.is--after {
    -webkit-clip-path: inset(0 0 0 50%);
    clip-path: inset(0 0 0 50%);
  }

  /* A tag-shaped knob (4px radius, like .tag and the Before/After
     labels): sky with burgundy arrows at rest, the CTA icon's pairing
     reversed, and on hover or keyboard focus it swaps to burgundy with
     sky arrows and widens a little, as if opening to be dragged; nothing
     rotates. Both pairings are 10.6:1. The divider is a slim off-white
     bar. */
  .splitter-handle {
    z-index: 2;
    cursor: ew-resize;
    background-color: var(--bb-off-white);
    justify-content: center;
    align-items: center;
    width: 2px;
    margin-left: -1px; /* centres the bar on the reveal edge */
    height: 100%;
    display: flex;
    position: absolute;
    top: 0;
    bottom: 0;
    left: 50%;
  }

  .splitter-handle__center {
    background-color: var(--bb-sky);
    color: var(--bb-burgundy);
    border-radius: 4px;
    flex: none;
    justify-content: center;
    align-items: center;
    width: 56px;
    height: 40px;
    display: flex;
    position: relative;
    transition: width 0.3s ease, background-color 0.3s ease, color 0.3s ease;
  }

  .splitter-handle:hover .splitter-handle__center,
  .splitter-handle:focus-visible .splitter-handle__center {
    width: 72px;
    background-color: var(--bb-burgundy);
    color: var(--bb-sky);
  }

  .splitter-handle__icon {
    display: flex;
    width: 22px;
  }

  /* The resource's arrow is a filled outline about 1px thick at this
     size; a stroke in the same colour thickens it evenly. */
  .splitter-handle__icon path {
    stroke: currentColor;
    stroke-width: 1;
    stroke-linejoin: round;
  }

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

  .splitter-content__img::selection { background: none; }

  /* "Before" / "After" in the site's tag style, each inside its own
     layer, so the after label is clipped along with the after image and
     only the side on show is labelled. aria-hidden: each image's alt
     already says which state it is. */
  .splitter-label {
    position: absolute;
    top: 16px;
    z-index: 1;
    pointer-events: none;
  }

  .splitter-label--before { left: 16px; }
  .splitter-label--after { right: 16px; }

  /* Keyboard focus rings the knob, not the full-height bar: the frame
     clips the bar's top and bottom, and a single-colour ring on a photo
     can vanish. Off-white inside burgundy is 14:1, so one of the pair
     holds 3:1 against anything behind it. */
  .splitter-handle:focus-visible { outline: none; }
  .splitter-handle:focus-visible .splitter-handle__center {
    outline: 2px solid var(--bb-off-white);
    outline-offset: 3px;
    box-shadow: 0 0 0 7px var(--bb-burgundy);
  }

  /* Stand-in for an image that has not been made yet: the brand pattern
     the stats band already uses as its placeholder, so a missing image
     reads as deliberate rather than broken. Decorative, so aria-hidden in
     the markup, and the first child of its frame so the mask reveal still
     has something to slide in. */
  .cs-image-placeholder {
    position: absolute;
    inset: 0;
    background-color: var(--bb-red);
    background-image: url("../images/burgundy-lightblue-pattern-tile.png");
    background-repeat: repeat;
    background-size: 240px 36px;
  }

  /* Art-directed single image: a wide web cut, and a square cut for phones,
     swapped by a <picture> at the same 640px step where galleries stack.
     The frame's shape changes with it, so it lives here rather than as an
     inline aspect-ratio.

     The <picture> is made the positioned box because the mask reveal
     animates the item's first child: a transform on a static <picture>
     would become the containing block for the absolute <img> inside it,
     with no height of its own, and the image would collapse mid-reveal. */
  /* The box follows the image supplied for each size. 16:9 and square
     (or 4:5) are the defaults; an image of any other shape sets its own
     with --ratio-wide / --ratio-phone inline, e.g. Coppola's 1.45:1 card.
     Set both to the two files' own width / height and nothing is cropped. */
  .cs-gallery__item--wide-square,
  .cs-gallery__item--wide-portrait {
    aspect-ratio: var(--ratio-wide, 16 / 9);
  }

  .cs-gallery__item--wide-square picture,
  .cs-gallery__item--wide-portrait picture {
    position: absolute;
    inset: 0;
    display: block;
  }

  /* The phone crop's shape follows the image supplied for it: square, or
     4:5 portrait where the art is a tall layout (Scene+'s route map) that
     a square crop would cut through. */
  @media (max-width: 640px) {
    .cs-gallery__item--wide-square {
      aspect-ratio: var(--ratio-phone, 1 / 1);
    }

    .cs-gallery__item--wide-portrait {
      aspect-ratio: var(--ratio-phone, 4 / 5);
    }
  }

  /* Tablet: images fill the full width (no empty margin columns), each
     still sized by its own aspect-ratio/flex-basis within that width. */
  @media (max-width: 1200px) {
    .cs-gallery__row { grid-column: 1 / -1; }
  }

  @media (max-width: 900px) {
    .cs-gallery { padding: 24px var(--page-margin); }
    .cs-gallery__row { flex-wrap: wrap; gap: 12px; }
    .cs-gallery__item { flex: 1 1 calc(50% - 6px); }
  }

  @media (max-width: 640px) {
    .cs-gallery__row { flex-direction: column; }
    /* Explicit width, not just flex:1 1 auto — in a column-direction flex
       row, width is resolved via cross-axis stretch, and aspect-ratio
       doesn't reliably derive a height off a stretched (rather than
       definite) width, collapsing the item to 0 height. */
    .cs-gallery__item { flex: 1 1 auto; width: 100%; }
  }

  /* Pull quote — centered, 6 of 12 columns (3–8), matching the site's
     grid convention used throughout the template. */
  .cs-quote {
    grid-template-columns: repeat(var(--grid-cols), 1fr);
    padding: 160px var(--page-margin);
  }

  .cs-quote__inner {
    grid-column: 4 / span 6;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 32px;
    text-align: center;
  }

  .cs-quote__text {
    font-family: "STIX Two Text", Georgia, "Times New Roman", serif;
    font-weight: 400;
    font-style: italic;
    font-size: 45px;
    line-height: 0.99;
    letter-spacing: -1.35px;
    color: var(--bb-black);
    text-wrap: balance;
  }

  .cs-quote__attribution {
    font-family: "Haas Recast VF", "Helvetica Neue", Arial, sans-serif;
    font-weight: 400;
    font-variation-settings: 'TRAK' 0;
    font-size: 15px;
    line-height: 1.3;
    letter-spacing: -0.3px;
    color: var(--bb-black);
  }

  /* The role runs on after the comma on desktop (one comma-separated
     line) and takes its own line at the mobile breakpoint below, where
     Figma hard-breaks onto two lines instead. A span that goes block
     there, not a <br> hidden on desktop: SplitText turns every <br> into
     a line break whatever its display, so the hidden one still broke
     the attribution onto two lines on desktop. */
  .cs-quote__attribution-role {
    display: inline;
  }

  /* Tablet: --grid-cols is already 8 here (see :root), so .cs-quote picks
     up an 8-column grid automatically — quote capped to 6 of the 8, the
     one place on this page that keeps a narrower, centered column at this
     breakpoint rather than expanding to fill the row. */
  @media (max-width: 1200px) {
    .cs-quote__inner { grid-column: 2 / span 6; }
  }

  @media (max-width: 900px) {
    .cs-quote { padding: 80px var(--page-margin); }
    .cs-quote__inner { grid-column: 1 / -1; }
    .cs-quote__text { font-size: 32px; letter-spacing: -0.96px; }
    /* Figma's mobile attribution hard-breaks onto two lines, dropping the
       comma before the source; desktop keeps it as one comma-separated
       line — see the .cs-quote__attribution-sep/-role markup. */
    .cs-quote__attribution-sep { display: none; }
    .cs-quote__attribution-role { display: block; }
  }

  /* Image + text split — one image beside a heading and body copy (Figma
     2145:8092 image-left, 2215:10843 image-right). Sits inside .cs-body
     alongside .cs-text-block and .cs-gallery, sharing their cream
     background and nav theming; the heading and body take the
     .cs-text-block spec (see the shared rules above).

     Image-left is the default. .cs-split--media-right swaps the two
     column ranges; the markup order swaps with it, so the reading order
     always matches what is on screen and the stacked mobile layout comes
     out in the order Figma draws it.

     On the page grid: the image is 4 columns (2–5, or 8–11 mirrored) and
     the copy 5 (7–11, or 2–6), each leaving one empty track between
     them. The image's 456x570 in the comp is exactly 4 columns at
     1440px, so aspect-ratio carries that shape to every other width
     rather than a fixed height.

     Unlike .cs-text-block, whose rule is inset to the copy, the comp runs
     this rule the full width of the grid — so it is a plain border on
     the row, not a ::before grid item. */
  .cs-split {
    padding: 40px var(--page-margin);
  }

  .cs-split__row {
    grid-template-columns: repeat(var(--grid-cols), 1fr);
    align-items: start;
    padding-top: 32px;
    border-top: 0.5px solid rgba(var(--bb-black-rgb), 0.5);
  }

  .cs-split__media {
    grid-column: 2 / span 4;
    position: relative;
    overflow: hidden;
    aspect-ratio: 456 / 570;
  }

  .cs-split__media img {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
  }

  .cs-split__content {
    grid-column: 7 / span 5;
    display: flex;
    flex-direction: column;
    gap: 32px;
  }

  .cs-split--media-right .cs-split__content { grid-column: 2 / span 5; }
  .cs-split--media-right .cs-split__media { grid-column: 8 / span 4; }

  /* The last block on a page when it is a split, sitting on the footer:
     the same 80px more below it that .cs-text-block--closing adds. */
  .cs-split--closing {
    padding-bottom: 120px;
  }

  /* No divider rule: for a run of splits that continue one section, as in
     the article's numbered sections, where the comp draws no line. */
  .cs-split--plain .cs-split__row {
    border-top: 0;
    padding-top: 0;
  }

  /* A navy ground behind a transparent image, for the Gladwell quote on
     The End of Everything: light blue on transparent, sitting in the
     navy body theme. The site's own navy, so it is invisible while the
     section is navy; once the page fades on to sky as the quote scrolls
     away, it keeps the light-blue dots from disappearing into the sky. */
  .cs-split__media--navy { background-color: var(--bb-navy); }

  /* Tablet: --grid-cols is 8 here, so the row picks that up and the two
     halves split it evenly, edge to edge — the same no-margin-columns
     treatment .cs-text-block and .cs-gallery get at this width. */
  @media (max-width: 1200px) {
    .cs-split__media,
    .cs-split--media-right .cs-split__content { grid-column: 1 / span 4; }
    .cs-split__content,
    .cs-split--media-right .cs-split__media { grid-column: 5 / span 4; }
  }

  /* Large phones / small tablets: --grid-cols is 6, split 3 and 3. Still
     side by side, as .cs-gallery is at this width — stacked, the 4:5
     image would run close to 1000px tall on an 800px screen. */
  @media (max-width: 900px) {
    .cs-split__row { padding-top: 16px; }
    .cs-split__media,
    .cs-split--media-right .cs-split__content { grid-column: 1 / span 3; }
    .cs-split__content,
    .cs-split--media-right .cs-split__media { grid-column: 4 / span 3; }
  }

  /* Mobile: one column, image full width, in markup order — the same
     width .cs-gallery stacks at. Same 32px stack gap as .cs-text-block. */
  @media (max-width: 640px) {
    .cs-split__row { grid-template-columns: 1fr; row-gap: 32px; }
    .cs-split__media,
    .cs-split__content,
    .cs-split--media-right .cs-split__media,
    .cs-split--media-right .cs-split__content { grid-column: 1; }
  }

  /* Image below the copy, full width: for a landscape image in a run of
     splits, which beside the copy would shrink to a third of the page.
     The copy keeps the column a media-right split gives it; the image
     takes its own row under it at .cs-gallery's width (columns 2-11,
     the full grid from 1200px down), so it lines up with the full-width
     charts around it. Declared after the breakpoint rules above so it
     wins at every width. */
  .cs-split--media-below .cs-split__row { row-gap: 32px; }
  .cs-split--media-below .cs-split__content { grid-column: 2 / span 5; }
  .cs-split--media-below .cs-split__media { grid-column: 2 / span 10; grid-row: 2; }

  @media (max-width: 1200px) {
    .cs-split--media-below .cs-split__content { grid-column: 1 / span 4; }
    .cs-split--media-below .cs-split__media { grid-column: 1 / -1; }
  }

  @media (max-width: 900px) {
    .cs-split--media-below .cs-split__content { grid-column: 1 / -1; }
  }

  @media (max-width: 640px) {
    .cs-split--media-below .cs-split__content,
    .cs-split--media-below .cs-split__media { grid-column: 1; }
  }

  /* Chart captions: UNUSED for now. They were taken off every page for
     launch (2026-09-28) to be revisited, and the charts' full alt text
     went back in their place. Kept so bringing them back is markup only:
     a <p class="cs-caption" data-split="heading"> straight after the
     image's .cs-split__media or .cs-gallery__row, and the chart's alt
     cut back to a short description.

     The charts and maps carry their figures only inside
     the image, where search engines and answer engines can't quote
     them, so each gets its data as a small caption under it (the
     image's alt then stays a short description, rather than reading
     the same figures twice). Black at 60% on Off-white, 5.5:1; the body
     themes recolour it (see "Body themes").

     In a split the caption shares the image's column, one row down,
     and the copy spans both rows beside them: the second row takes the
     leftover height (1fr) so the caption stays tucked under the image
     however long the copy runs. Stacked on phones, in markup order. */
  /* Italic at 10pt (13.33px), in STIX Two Text: the site's italic face,
     since Haas Recast has no italic of its own. */
  .cs-caption {
    font-family: "STIX Two Text", Georgia, "Times New Roman", serif;
    font-weight: 400;
    font-style: italic;
    font-size: 10pt;
    line-height: 1.35;
    color: rgba(var(--bb-black-rgb), 0.6);
  }

  /* 12px under the image: the grid's own row gap, less the difference. */
  .cs-gallery__inner > .cs-caption {
    grid-column: 2 / span 10;
    margin-top: calc(12px - var(--grid-gap));
    max-width: 720px;
  }

  .cs-split__row > .cs-caption {
    grid-column: 2 / span 4;
    align-self: start;
    margin-top: calc(12px - var(--grid-gap));
  }

  .cs-split--media-right .cs-split__row > .cs-caption { grid-column: 8 / span 4; }

  .cs-split__row:has(> .cs-caption) { grid-template-rows: auto 1fr; }
  .cs-split__row:has(> .cs-caption) > .cs-split__content { grid-row: 1 / span 2; }

  @media (max-width: 1200px) {
    .cs-gallery__inner > .cs-caption { grid-column: 1 / -1; }
    .cs-split__row > .cs-caption { grid-column: 1 / span 4; }
    .cs-split--media-right .cs-split__row > .cs-caption { grid-column: 5 / span 4; }
  }

  @media (max-width: 900px) {
    .cs-split__row > .cs-caption { grid-column: 1 / span 3; }
    .cs-split--media-right .cs-split__row > .cs-caption { grid-column: 4 / span 3; }
  }

  @media (max-width: 640px) {
    .cs-split__row > .cs-caption,
    .cs-split--media-right .cs-split__row > .cs-caption { grid-column: 1; margin-top: -20px; }
    .cs-split__row:has(> .cs-caption) { grid-template-rows: none; }
    .cs-split__row:has(> .cs-caption) > .cs-split__content { grid-row: auto; }
  }

  /* Shared slider base — Osmo Supply's Basic GSAP Slider (see
     assets/javascript/gsap-slider.js, kept unmodified) restyled to this
     template's visual language. .cs-slider__* is the ONE definition of
     the slider's structure/mechanics/breakpoints, used by both the image
     carousel (.cs-carousel) and the related-projects carousel
     (.cs-related): change a layout/padding/margin rule here once and
     both pick it up, rather than maintaining two parallel copies. Only
     what's genuinely different per use — plain full-bleed images here vs.
     .p-card project cards there — lives outside this block, scoped by
     context (see ".cs-carousel .cs-slider__item" etc. below).

     Two-tier width on purpose, matching the original resource's own
     .gsap-slider (full width, clips) / .gsap-slider__collection (narrower
     content reference) split: .cs-slider is the actual clipping boundary
     and spans the full page, so a dragged slide can travel all the way
     to the true viewport edge instead of hard-cutting at an inner line;
     .cs-slider__content is what's aligned to the site's 12-column grid
     (columns 2–11, matching .cs-gallery/.cs-hero) and is what the
     slider math (spv/gap) actually sizes slides against. */
  .cs-slider {
    width: 100%;
    overflow: hidden;
  }

  /* The pages wrap their sliders in .cs-carousel and .cs-related rather
     than .cs-slider, so the boundary above never applied, and html's
     overflow-x: hidden was the only clip. Phones don't treat that as
     one: they widen the layout viewport to fit the off-screen slides
     (966px on Saintly, 1500px on Casey House), which pushed the fixed
     navbar's logo and burger out of view. Both wrappers are already full
     width, so clipping here is the viewport edge the note above wants.
     clip, not hidden, so neither becomes a scroll container and the
     vertical axis stays open for focus rings. */
  .cs-carousel,
  .cs-related {
    overflow-x: clip;
  }

  .cs-slider__inner {
    padding: 40px var(--page-margin);
    --slider-status: on;
    --slider-spv: 3;
    --slider-gap: 16px;
  }

  .cs-slider__content {
    grid-column: 2 / span 10;
    display: flex;
    flex-direction: column;
    gap: var(--grid-gap);
  }

  .cs-slider__collection {
    width: 100%;
  }

  .cs-slider__list {
    display: flex;
    -webkit-user-select: none;
    user-select: none;
    will-change: transform;
    touch-action: pan-y;
    backface-visibility: hidden;
  }

  .cs-slider__item {
    position: relative;
    flex: none;
    overflow: hidden;
    width: calc(((100% - 1px) - (var(--slider-spv) - 1) * var(--slider-gap)) / var(--slider-spv));
    margin-right: var(--slider-gap);
  }

  .cs-slider__item:last-child {
    margin-right: 0;
  }

  /* Drag is the pointer affordance; the prev/next buttons above
     (.cs-slider__control) are the keyboard one and stay off-screen until
     focused, so nothing is drawn here at rest.

     grab/grabbing only apply while the script's own
     data-gsap-slider-list-status attribute is set, which it only sets on
     an active slider (more items than fit at once — see sliderEnabled in
     assets/javascript/gsap-slider.js), so a slider with nothing to drag
     through never shows it. */
  [data-gsap-slider-list-status="grab"] { cursor: grab; }
  [data-gsap-slider-list-status="grabbing"] { cursor: grabbing; }

  /* Tablet: the slider fills the full width (no empty margin columns),
     but — unlike desktop's deliberate right-bleed — stays inset from the
     true edges here, matching the ~20px margin every other block gets at
     this breakpoint. Slides themselves are still sized by
     --slider-spv/width below. */
  @media (max-width: 1200px) {
    .cs-slider__inner { padding: 40px var(--page-margin); }
    .cs-slider__content { grid-column: 1 / -1; }
  }

  @media (max-width: 900px) {
    /* Tablet: 2 full tiles per view, with a slice of the 3rd peeking in
       to signal there's more to drag through — width is set directly
       here (not via the --slider-spv formula above) so it's an exact,
       predictable proportion rather than however the formula's gap math
       happens to net out. */
    .cs-slider__inner { --slider-spv: 2; padding: 24px var(--page-margin); }
    .cs-slider__item { width: 45%; }
  }

  @media (max-width: 640px) {
    /* Mobile: one tile at 90% width, same peekaboo idea. */
    .cs-slider__inner { --slider-spv: 1; --slider-gap: 12px; padding: 24px var(--page-margin); }
    .cs-slider__item { width: 90%; }
  }

  /* Image carousel — the only thing specific to it is that a slide is a
     plain full-bleed photo, not a .p-card. */
  .cs-carousel .cs-slider__item {
    aspect-ratio: 574 / 718;
  }

  .cs-carousel .cs-slider__item img {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
  }

  /* Landscape variant, for a project whose campaign imagery is all wide
     (Casey House). 16:9 slides, and two per view rather than three, so a
     wide frame is not shrunk to thumbnail size. --slider-spv is restated
     at the phone step too: this rule outranks the plain .cs-slider__inner
     one there, and gsap-slider.js reads the value to decide whether the
     slider runs, so it has to match the one slide actually shown. */
  .cs-carousel--landscape .cs-slider__item {
    aspect-ratio: 16 / 9;
  }

  .cs-carousel--landscape .cs-slider__inner {
    --slider-spv: 2;
  }

  @media (max-width: 640px) {
    .cs-carousel--landscape .cs-slider__inner {
      --slider-spv: 1;
    }
  }

  /* Pair variant: portrait slides, but two per view on desktop, for a
     carousel with only two photos (Saintly's Sip 'n' Suds pair) that
     would otherwise leave the third slot empty. --slider-spv is
     restated at the phone step for the same reason as the landscape
     variant above. */
  .cs-carousel--pair .cs-slider__inner {
    --slider-spv: 2;
  }

  @media (max-width: 640px) {
    .cs-carousel--pair .cs-slider__inner {
      --slider-spv: 1;
    }
  }

  /* Related-projects carousel — same .cs-slider structure, .p-card
     project cards as slides instead of plain images. Unlike the image
     carousel (nested inside .cs-body, which supplies its own vertical
     rhythm between blocks), this is a standalone section immediately
     before the footer, so it needs its own extra top/bottom breathing
     room on top of — not instead of — .cs-slider__inner's shared 40px
     vertical padding: 40px here + 40px shared = 80px top; 120px here +
     40px shared = 160px bottom. */
  .cs-related {
    background: var(--bb-white);
    padding: 40px 0 120px;
  }

  .cs-related__label {
    font-family: "STIX Two Text", Georgia, "Times New Roman", serif;
    font-weight: 400;
    font-style: italic;
    font-size: 20px;
    letter-spacing: -0.4px;
    color: var(--bb-black);
  }

  /* Reuses the site's existing .p-card component (see .portfolio-grid in
     what-we-do.html) — only the image aspect ratio is specific here. */
  .p-card--related .p-card__img { aspect-ratio: 456 / 257; }

  /* ==========================================================================
     WHO WE ARE  (who-we-are.html)

     Three sections, top to bottom: the hero, the reel (a full-bleed HLS
     background video that sits *behind* the hero and is uncovered as the
     hero scrolls off), and "What we're made of" (a burgundy title panel
     that pins while three sky cards arrive over it one at a time).

     Figma: "Desktop / Who we are" — 1928:9745 (hero), 1928:10307 (reel),
     1928:9735 (title panel) and 1928:9738 (cards).

     The whole scroll choreography here is plain CSS `position: sticky`,
     which is what the Figma frames are built on too — no GSAP pinning.
     That matters with Lenis in play: Lenis drives real scrollTop rather
     than transforming a wrapper, so sticky behaves natively and there is
     no pin-spacer to keep in sync. */
  :root {
    /* Declared here rather than beside .hero--who because .reel has to
       cancel exactly this height to sit behind the hero, and the two
       drifting apart is the one way this layout breaks. 76vh is Figma's
       681/900 — the hero occupies the top three-quarters of the first
       screen and the reel shows below it as a peek. The 380px floor is
       the nav plus the headline at its 72px minimum and the subcopy, so
       a very short window grows the hero instead of putting the
       headline under the nav; the reel's pull-up reads the same value. */
    --who-hero-height: max(76vh, 380px);
  }

  /* --------------------------------------------------------------------
     Hero. The homepage's .hero component, unchanged, with three
     page-specific overrides: a shorter box (to leave the reel's peek),
     a heading that wraps at its own <br> instead of running nowrap, and
     a full-measure subcopy — this page's is a 2-line paragraph at 928px
     where the homepage's is a short single line at 560px.

     The subcopy's line-height/letter-spacing are Figma's "Headline 2"
     token (26/1.19/-2%), which is a hair tighter than the homepage
     hero's 1.28/-0.36px. Overridden rather than changed at source: the
     homepage hero is not this design. */
  /* min-height: 0 undoes the shared hero's 80vh floor: this box has to
     be exactly --who-hero-height for the reel's pull-up to cancel. */
  .hero--who {
    position: relative;
    z-index: 1;
    min-height: 0;
    height: var(--who-hero-height);
    background: var(--bb-off-white);
  }

  .hero--who .hero__heading { white-space: normal; }

  .hero--who .hero__subcopy {
    max-width: 928px;
    line-height: 1.19;
    letter-spacing: -0.52px;
  }

  /* Above 900px the subcopy breaks after "insight," into two even
     lines; left to wrap, the 928px measure only pushed "drive growth."
     onto the second line. Smaller screens wrap on their own, which
     already reads well. A block span rather than a <br>: SplitText makes
     every <br> a line break whatever its display (see .hero__subcopy-tail). */
  @media (min-width: 901px) {
    .hero__subcopy-line { display: block; }
  }

  @media (max-width: 900px) {
    .hero--who { height: auto; min-height: 60svh; padding-top: 139px; }
    .hero--who .hero__subcopy { max-width: 46ch; }
  }

  @media (max-width: 640px) {
    .hero--who .hero__subcopy { max-width: none; }
  }

  /* --------------------------------------------------------------------
     Reel: the HLS background video.

     Pulled up by exactly the hero's height so the video plane starts at
     the top of the page, with the hero's opaque off-white sitting over
     its top three-quarters — which is how Figma draws it (image 691 is a
     full 1440x900 frame at y=0; the hero frame is only 681 tall). The
     stage is sticky from scroll 0, so the video reads as fixed behind
     the page rather than as a panel sliding up into place, and the hero
     scrolling away is what uncovers it.

     The stage pins to .reel-scope, which also holds "What we're made
     of", so the video stays fixed while that section slides up over it
     rather than scrolling away first. A sticky box is confined to its
     parent: with the stage inside .reel it unpinned at the end of the
     reel's scroll and moved off before the burgundy panel arrived.

     --reel-hold is how much scroll the video gets before the panel
     starts climbing over it. .reel is that stretch and nothing else —
     it overlaps the stage (the -100lvh) and carries the nav theme for
     it; the made-of section after it sits above the stage. */
  .reel-scope {
    --reel-hold: 150vh; /* was 250vh: a full screen less of scrolling over the video */
    position: relative;
    z-index: 0;
    margin-top: calc(-1 * var(--who-hero-height));
  }

  .reel {
    margin-top: -100vh;
    margin-top: -100lvh;
    height: var(--reel-hold);
  }

  /* Below 900px the hero is height:auto — it has to be, or this page's
     long subcopy is clipped on a phone — so there is no longer a fixed
     height for the pull-up to cancel and --who-hero-height would drag
     the reel above the top of the document. The overlap is dropped
     instead of guessed at: the reel simply follows the hero, and because
     the hero is only ~60svh the video still shows under the fold on
     first paint. The one thing lost is the video being *behind* the hero
     rather than after it, which is not visible on a phone anyway. */
  @media (max-width: 900px) {
    .reel-scope { margin-top: 0; }
  }

  /* lvh for the same reason as .combine__stage: a 100svh stage left a
     blank strip the height of the phone's address bar under the video
     once the bar slid away. The overlap in .reel above uses the same
     unit, so the two still cancel. */
  .reel__stage {
    position: sticky;
    top: 0;
    height: 100vh;
    height: 100lvh;
    /* The Vimeo player is absolutely positioned against this box. */
    overflow: hidden;
    background: var(--bb-black);
  }

  /* --------------------------------------------------------------------
     Vimeo background player — Osmo Supply's resource (see
     assets/javascript/vimeo-background.js, kept unmodified).

     The *script* is the part kept verbatim; its styling is ours, the same
     split the number-odometer block above documents. So these are the
     resource's rules with the structural mechanism (the attribute-driven
     show/hide of the placeholder and the two glyphs) intact, and every
     colour moved onto --bb-* — the original's #efeeec, #131313, #000,
     #64646433 and #ffffff1a are all off-palette, and BRAND.md allows no
     hex in this file outside the :root block. The demo section chrome
     from the original is dropped: the section shell here is .reel above.

     The player sizes itself by wrapper geometry rather than object-fit:
     .vimeo-bg__before holds the film's aspect ratio open as padding-top
     and the script widens .vimeo-bg__iframe-wrapper past 100% whenever
     the stage is proportionally taller than the film. That is the
     resource's own cover mechanism and the reason an iframe — which
     object-fit cannot crop — fills the stage without letterboxing.

     Three presentational changes on top of that, listed here rather than
     as a later override block so there is one place to read what this
     component looks like:

       - the disc is Sky with a Burgundy glyph, which is the site's
         existing on-dark icon pairing (see .cta--on-dark), not the
         original's blurred grey;
       - the play control hides by `display` rather than the original's
         `opacity: 0`. On a fine pointer both controls are stretched over
         the whole stage (see the .reel block below), and a transparent
         but still-present play button would sit over the pause button
         and swallow every click meant for it;
       - on a fine pointer the hit area is the whole stage and the disc
         rides the cursor, which is what makes "the cursor is the
         play/pause button". Touch and coarse pointers keep the original's
         fixed bottom-right button.

     Unlike the Bunny player this replaces, Vimeo surfaces no buffering
     state to style, so the loading spinner has no equivalent here and is
     gone. The placeholder covers the same gap: it holds until the first
     play event and then fades. */
  .vimeo-bg {
    pointer-events: none;
    isolation: isolate;
    background-color: var(--bb-black);
    justify-content: center;
    align-items: center;
    width: 100%;
    height: 100%;
    display: flex;
    position: absolute;
    top: 0;
    left: 0;
    overflow: hidden;
    transform: translateX(0);
  }

  .vimeo-bg__iframe-wrapper {
    justify-content: center;
    align-items: center;
    width: 100%;
    display: flex;
    position: absolute;
  }

  /* Replaced by the script with the real film's ratio once Vimeo reports
     its dimensions; 62.5% is the resource's own 16:10 starting guess. */
  .vimeo-bg__before {
    padding-top: 62.5%;
  }

  .vimeo-bg__iframe {
    pointer-events: none;
    border: 0;
    width: 100%;
    height: 100%;
    position: absolute;
  }

  /* Placeholder */
  .vimeo-bg__placeholder {
    object-fit: cover;
    width: 100%;
    height: 100%;
    display: block;
    position: absolute;
    transition: opacity 0.3s linear, visibility 0.3s linear;
  }

  .vimeo-bg[data-vimeo-activated="true"][data-vimeo-loaded="true"] .vimeo-bg__placeholder {
    opacity: 0;
    visibility: hidden;
    transition-delay: 0.2s;
  }

  /* Play/Pause */
  /* <button>s, not the original's <div>s: they are the page's only
     control for the video, so they have to be focusable and operable
     from the keyboard. Osmo's handlers bind to whatever carries
     [data-vimeo-control], which a real button satisfies natively on
     Enter/Space — nothing in the script changed to allow this. UA button
     styling is reset here because of it. */
  .vimeo-bg__play,
  .vimeo-bg__pause {
    pointer-events: auto;
    cursor: pointer;
    justify-content: center;
    align-items: center;
    display: flex;
    position: absolute;
    bottom: 4vw;
    right: 4vw;
    appearance: none;
    padding: 0;
    border: 0;
    background: none;
    font: inherit;
    color: inherit;
  }

  .vimeo-bg__play:focus-visible,
  .vimeo-bg__pause:focus-visible {
    outline: 2px solid var(--bb-sky);
    outline-offset: 4px;
  }

  /* Exactly one of the two is in the tree at a time. */
  .vimeo-bg[data-vimeo-playing="true"] .vimeo-bg__play,
  .vimeo-bg[data-vimeo-playing="false"] .vimeo-bg__pause {
    display: none;
  }

  /* The cursor pill — Figma 2069:3343 (pause) and 2069:3350 (play). One
     rule for both: they differ only in glyph and word.

     Sized in px rather than em because the comp's numbers are absolute
     and this rides the pointer at a fixed size — it is chrome over the
     video, not type in the page flow, so it should not track a
     surrounding font-size. Values straight from the comp: 6px/8px
     padding, 10px gap, a 2px radius that reads as a soft corner rather
     than a pill, and a 20px blur over 30% black. */
  .vimeo-bg__btn {
    background-color: rgba(var(--bb-black-rgb), 0.3);
    -webkit-backdrop-filter: blur(20px);
    backdrop-filter: blur(20px);
    color: var(--bb-white);
    border: 0;
    border-radius: 2px;
    align-items: center;
    gap: 10px;
    padding: 6px 8px;
    display: flex;
    position: relative;
  }

  /* Both glyphs occupy the same 9px box so the word sits on the same
     vertical line whichever control is showing — otherwise the label
     shifts by a pixel on every toggle. */
  .vimeo-bg__glyph {
    flex: none;
    justify-content: center;
    align-items: center;
    width: 9px;
    height: 10.6667px;
    display: flex;
  }

  .vimeo-bg__glyph--bars {
    gap: 2px;
  }

  .vimeo-bg__bar {
    background-color: currentColor;
    width: 2px;
    height: 10px;
  }

  .vimeo-bg__glyph-play {
    display: block;
    width: 8px;
    height: 10.6667px;
  }

  /* 0.97 line-height and the trimmed box in the comp mean the word is
     measured cap-height to baseline; line-height:1 plus the flex
     centring above lands in the same place without relying on
     text-box-trim, which Safari does not have yet. */
  .vimeo-bg__label {
    font-family: "Haas Recast VF", "Helvetica Neue", Arial, sans-serif;
    /* TRAK 0, not the -35 display type carries: this is a 14px UI caption,
       and every other -35 in this file sits on 32px and up. The comp's own
       0.28px letter-spacing still applies on top. */
    font-variation-settings: 'TRAK' 0;
    font-weight: 500;
    font-size: 14px;
    line-height: 1;
    letter-spacing: 0.28px;
    white-space: nowrap;
  }

  /* The cursor IS the play/pause button. On a fine pointer whichever
     control is showing stretches to cover the whole stage — so a click
     anywhere in the video toggles it — the native cursor is hidden
     inside it, and the disc is parked at the stage's top-left for
     initReelCursor() in main.js to translate onto the pointer. It starts
     hidden and fades in on the first pointermove, so it never flashes at
     0,0 before the pointer has been anywhere.

     Hung on [data-video-cursor], which the markup puts on the players
     that want it, rather than on .vimeo-bg globally or on a section
     class. Who We Are runs two of these — the reel and The Combine — and
     they behave identically, so the hook travels with the player; a
     player added anywhere without the attribute still gets the
     resource's ordinary corner button. */
  @media (hover: hover) and (pointer: fine) {
    [data-video-cursor] .vimeo-bg__play,
    [data-video-cursor] .vimeo-bg__pause {
      inset: 0;
      bottom: auto;
      right: auto;
      width: 100%;
      height: 100%;
      display: block;
      cursor: none;
    }

    /* Re-stated inside the query: the rule above sets `display: block` on
       both controls, which would otherwise override the hide above it. */
    [data-video-cursor][data-vimeo-playing="true"] .vimeo-bg__play,
    [data-video-cursor][data-vimeo-playing="false"] .vimeo-bg__pause {
      display: none;
    }

    /* No width/height here on purpose: the pill is sized by its own
       padding and label, and the word changes length between the two
       controls. Pinning it to a box would either clip "Pause" or leave
       "Play" swimming in space. */
    [data-video-cursor] .vimeo-bg__btn {
      position: absolute;
      top: 0;
      left: 0;
      opacity: 0;
      /* -50%,-50% centres the pill on the pointer; initReelCursor()
         writes the pointer position into these two properties and never
         touches transform, so the two can't fight. */
      transform: translate(calc(var(--reel-cursor-x, 0px) - 50%),
                           calc(var(--reel-cursor-y, 0px) - 50%));
      transition: opacity 0.25s ease;
    }

    /* On the stage, not on a control: the control carrying it would be
       display:none'd on the next toggle and take the pill with it. */
    [data-video-cursor].is-tracking .vimeo-bg__btn { opacity: 1; }

    /* Keyboard focus has no pointer to ride, so the pill comes back to
       the corner the touch fallback uses and shows itself there. */
    [data-video-cursor] .vimeo-bg__play:focus-visible .vimeo-bg__btn,
    [data-video-cursor] .vimeo-bg__pause:focus-visible .vimeo-bg__btn {
      opacity: 1;
      top: auto;
      left: auto;
      bottom: 4vw;
      right: 4vw;
      transform: none;
    }
  }

  @media (prefers-reduced-motion: reduce) {
    [data-video-cursor] .vimeo-bg__btn { transition: none; }
  }

  @media screen and (max-width: 991px) {
    .vimeo-bg__play,
    .vimeo-bg__pause {
      bottom: 1em;
      right: 1em;
    }
  }

  /* Touch target. The pill is 61x23 at the comp's size, which is well
     under the 44px minimum a finger needs, and on a coarse pointer it is
     the ONLY control for the video — there is no cursor-sized hit area
     stretched over the stage to fall back on, the way there is on a fine
     pointer.

     So the button grows and the pill does not: 11px of padding takes the
     pressable box to 45px tall and 83px wide while the pill inside stays
     exactly as drawn. The inset drops from 16px to 5px by the same 11px,
     which leaves the pill's visible edge where the comp puts it — the
     extra area is padding hanging off it, not a shifted control.

     Keyed off the pointer rather than a width: a narrow window on a
     laptop still has a mouse and keeps the cursor behaviour, while a
     large tablet has a finger and needs this. */
  @media (hover: none), (pointer: coarse) {
    .vimeo-bg__play,
    .vimeo-bg__pause {
      padding: 11px;
      bottom: 5px;
      right: 5px;
    }
  }

  @media (hover: none) and (max-width: 991px), (pointer: coarse) and (max-width: 991px) {
    .vimeo-bg__play,
    .vimeo-bg__pause {
      bottom: calc(1em - 11px);
      right: calc(1em - 11px);
    }
  }

  /* --------------------------------------------------------------------
     Vimeo player — Osmo Supply's Custom Vimeo Player (Advanced), see
     assets/javascript/vimeo-player.js. The click-to-play, with-sound
     sibling of the background player above; used by the case-study video
     header (.cs-hero__video).

     Same split as .vimeo-bg: the script is kept, the styling is ours. The
     resource's attribute-driven state rules are intact; every colour is
     moved onto --bb-* (the original's #efeeec, #131313, #646464 and
     #FF4C24 are off-palette). What changed on top, and why:

       - Controls are <button>s, the timeline an <input> it already was,
         each with a name — the original's <div>s were unreachable by
         keyboard. So the UA button styling is reset below.
       - The play control hides by `display` when playing, not the
         original's `opacity: 0`, which left an invisible Play button in
         the tab order over the pause one. initVideoControlFocus() in
         main.js hands focus across when the swap takes it away.
       - The control bar is hidden (visibility, so nothing in it can be
         tabbed to) until the film has been started: the comp shows only
         "Play" on the poster, where the original showed the bar. Once
         playing, it also shows on :focus-within, since the original only
         brought it back on mouse hover.
       - Play/Pause are the comp's word, not the original's glyph disc:
         STIX italic on desktop, the reel's blurred pill on mobile.
       - Square corners and a 20% black wash on the poster, per the comp.
       - Keyboard focus rings throughout; the original removed the
         range's outline and replaced it with nothing. */
  .vimeo-player {
    pointer-events: auto;
    color: var(--bb-off-white);
    isolation: isolate;
    background-color: var(--bb-black);
    justify-content: center;
    align-items: center;
    width: 100%;
    display: flex;
    position: relative;
    overflow: hidden;
    transform: translateX(0);

    --timeline-rounded-corners: 1.5em;
    --timeline-dot-height: 0.75em;
    --timeline-dot-color: var(--bb-off-white);
    --progress-bg: rgba(var(--bb-off-white-rgb), 0.2);
    --progress-fill-bg: var(--bb-off-white);
    --progress-height: 0.2em;
  }

  /* Cover: fills its parent, which sets the shape (.cs-hero__video). */
  .vimeo-player[data-vimeo-update-size="cover"] {
    width: 100%;
    height: 100%;
    position: absolute;
    inset: 0;
  }

  .vimeo-player__iframe {
    pointer-events: none;
    border: 0;
    width: 100%;
    height: 100%;
    position: absolute;
  }

  .vimeo-player__before {
    padding-top: 62.5%;
  }

  .vimeo-player__placeholder {
    object-fit: cover;
    width: 100%;
    height: 100%;
    transition: opacity 0.3s linear, visibility 0.3s linear;
    display: block;
    position: absolute;
  }

  .vimeo-player[data-vimeo-activated="true"][data-vimeo-loaded="true"] .vimeo-player__placeholder {
    opacity: 0;
    visibility: hidden;
  }

  /* Dark overlay. The resource's states, except the poster (not yet
     activated), which carries the comp's 20% wash instead of none. */
  .vimeo-player__dark {
    opacity: 0.5;
    pointer-events: none;
    background-color: var(--bb-black);
    width: 100%;
    height: 100%;
    transition: opacity 0.3s linear;
    position: absolute;
  }

  .vimeo-player[data-vimeo-playing="false"] .vimeo-player__dark { opacity: 0.33; }
  .vimeo-player[data-vimeo-activated="false"][data-vimeo-playing="false"] .vimeo-player__dark { opacity: 0.2; }
  .vimeo-player[data-vimeo-activated="true"][data-vimeo-loaded="true"] .vimeo-player__dark { opacity: 0; }

  @media (hover: hover) and (pointer: fine) {
    .vimeo-player[data-vimeo-hover="true"]:hover .vimeo-player__dark {
      opacity: 0.33 !important;
    }
  }

  /* Play/Pause: stretched over the whole film, so a click anywhere
     toggles — the resource's own geometry. */
  .vimeo-player__play,
  .vimeo-player__pause {
    cursor: pointer;
    justify-content: center;
    align-items: center;
    width: 100%;
    height: 100%;
    display: flex;
    position: absolute;
    inset: 0;
    appearance: none;
    padding: 0;
    border: 0;
    background: none;
    font: inherit;
    color: inherit;
  }

  /* Inset ring, as on .vimeo-bg: the player clips, so an outset ring on a
     full-cover button would be cut off. */
  .vimeo-player :where(button, input):focus-visible {
    outline: 2px solid var(--bb-sky);
    outline-offset: -4px;
  }

  /* Desktop: the word alone, no chip (comp 2215:10963). */
  .vimeo-player__btn {
    display: flex;
    align-items: center;
    gap: 10px;
    position: relative;
    color: var(--bb-white);
    transition: opacity 0.3s linear;
  }

  .vimeo-player__glyph {
    display: none;
  }

  .vimeo-player__label {
    font-family: "STIX Two Text", Georgia, "Times New Roman", serif;
    font-weight: 500;
    font-style: italic;
    font-size: 32px;
    line-height: 0.97;
    letter-spacing: 0.16px;
    white-space: nowrap;
  }

  /* Exactly one of the two is in the tree at a time. */
  .vimeo-player[data-vimeo-playing="true"] .vimeo-player__play,
  .vimeo-player[data-vimeo-playing="false"] .vimeo-player__pause {
    display: none;
  }

  /* While playing, the pause word only surfaces on hover or focus. */
  .vimeo-player .vimeo-player__pause .vimeo-player__btn { opacity: 0; }

  .vimeo-player[data-vimeo-activated="true"][data-vimeo-hover="true"]:hover .vimeo-player__pause .vimeo-player__btn,
  .vimeo-player .vimeo-player__pause:focus-visible .vimeo-player__btn {
    opacity: 1;
  }

  @media (hover: none) and (pointer: coarse) {
    .vimeo-player[data-vimeo-activated="true"][data-vimeo-playing="true"] .vimeo-player__pause .vimeo-player__btn {
      opacity: 0 !important;
    }
  }

  /* Mobile: the reel's blurred pill with a glyph (comp 2137:836) — the
     same values as .vimeo-bg__btn. */
  @media (max-width: 900px) {
    .vimeo-player__btn {
      background-color: rgba(var(--bb-black-rgb), 0.3);
      -webkit-backdrop-filter: blur(20px);
      backdrop-filter: blur(20px);
      border-radius: 2px;
      padding: 6px 8px;
    }

    .vimeo-player__glyph {
      flex: none;
      justify-content: center;
      align-items: center;
      width: 9px;
      height: 10.6667px;
      display: flex;
    }

    .vimeo-player__glyph--bars { gap: 2px; }

    .vimeo-player__bar {
      background-color: currentColor;
      width: 2px;
      height: 10px;
    }

    .vimeo-player__glyph-play {
      display: block;
      width: 8px;
      height: 10.6667px;
    }

    .vimeo-player__label {
      font-family: "Haas Recast VF", "Helvetica Neue", Arial, sans-serif;
      font-variation-settings: 'TRAK' 0;
      font-style: normal;
      font-weight: 500;
      font-size: 14px;
      line-height: 1;
      letter-spacing: 0.28px;
    }
  }

  /* Loading. Pinned to the frame with an explicit inset rather than left
     to its static position: the player is a flex container, and an
     absolute child with no offsets lands wherever the browser puts its
     static position — Chrome centres it, Safari can drop it below the
     aspect spacer, and it moved as the player resized itself for the film
     (data-vimeo-update-size="cover"). z-index keeps it over the resized
     iframe; the fixed size keeps it from scaling with the text. */
  .vimeo-player__loading {
    pointer-events: none;
    color: var(--bb-off-white);
    justify-content: center;
    align-items: center;
    inset: 0;
    z-index: 3;
    transition: opacity 0.3s linear;
    display: flex;
    position: absolute;
    opacity: 0;
  }

  /* Spun here rather than by the SMIL <animateTransform> the resource
     shipped, which sat on the outer <svg> and so rotated the element
     about a point 50px in from its corner rather than its centre: in
     Chrome the arc orbited the frame instead of turning in place. A CSS
     rotation with its origin at the centre can't drift. */
  .vimeo-player__loading-svg {
    flex: none;
    width: 96px;
    height: 96px;
    transform-origin: 50% 50%;
    animation: vimeo-player-spin 1s linear infinite;
  }

  @keyframes vimeo-player-spin {
    to { transform: rotate(360deg); }
  }

  .vimeo-player[data-vimeo-playing="true"] .vimeo-player__loading { opacity: 1; }
  .vimeo-player[data-vimeo-playing="true"][data-vimeo-loaded="true"] .vimeo-player__loading { opacity: 0; }

  /* Under reduced motion the spinner goes altogether rather than slowing:
     the poster holding is signal enough that the film is coming. */
  @media (prefers-reduced-motion: reduce) {
    .vimeo-player__loading { display: none; }
  }

  /* Interface */
  .vimeo-player__interface {
    pointer-events: none;
    flex-flow: column;
    justify-content: flex-end;
    align-items: stretch;
    width: 100%;
    height: 100%;
    padding: min(2em, 4vw);
    transition: opacity 0.3s linear, visibility 0.3s linear;
    display: flex;
    position: absolute;
    opacity: 0;
  }

  .vimeo-player .vimeo-player__interface * {
    pointer-events: all;
  }

  .vimeo-player[data-vimeo-activated="false"] .vimeo-player__interface {
    visibility: hidden;
  }

  .vimeo-player[data-vimeo-activated="true"][data-vimeo-playing="false"] .vimeo-player__interface,
  .vimeo-player[data-vimeo-activated="true"][data-vimeo-hover="true"]:hover .vimeo-player__interface,
  .vimeo-player[data-vimeo-activated="true"] .vimeo-player__interface:focus-within {
    opacity: 1;
  }

  @media (hover: none) and (pointer: coarse) {
    .vimeo-player[data-vimeo-activated="true"][data-vimeo-playing="true"] .vimeo-player__interface:not(:focus-within) {
      opacity: 0 !important;
    }
  }

  .vimeo-player__interface-bottom {
    gap: 1em;
    justify-content: flex-start;
    align-items: center;
    display: flex;
  }

  .vimeo-player__duration {
    flex-shrink: 0;
    width: 2.25em;
    font-family: "Haas Recast VF", "Helvetica Neue", Arial, sans-serif;
    font-variation-settings: 'TRAK' 0;
    font-size: 14px;
  }

  .vimeo-player__duration-span {
    text-align: center;
    white-space: nowrap;
    user-select: none;
    width: 100%;
    display: block;
  }

  .vimeo-player__timeline {
    flex-grow: 1;
    justify-content: center;
    align-items: center;
    height: 1.5em;
    display: flex;
    position: relative;
  }

  .vimeo-player__timeline-input {
    pointer-events: auto;
    cursor: pointer;
    -webkit-appearance: none;
    appearance: none;
    background-color: transparent;
    width: 100%;
    height: 100%;
    margin: 0;
    display: block;
    position: relative;
  }

  .vimeo-player__timeline-progress {
    vertical-align: top;
    -webkit-appearance: none;
    appearance: none;
    height: var(--progress-height);
    border-radius: var(--timeline-rounded-corners);
    color: var(--progress-fill-bg);
    background-color: transparent;
    border: none;
    width: 100%;
    margin: 0;
    padding: 0;
    position: absolute;
    left: 0;
    overflow: hidden;
  }

  .vimeo-player progress::-webkit-progress-bar {
    border-radius: var(--timeline-rounded-corners);
    background-color: var(--progress-bg);
  }

  .vimeo-player progress::-webkit-progress-value {
    background: var(--progress-fill-bg);
  }

  .vimeo-player progress::-moz-progress-bar {
    border-radius: var(--timeline-rounded-corners);
    background: var(--progress-fill-bg);
  }

  .vimeo-player [type="range"]::-webkit-slider-runnable-track {
    width: 100%;
    height: var(--progress-height);
    cursor: pointer;
    border-radius: var(--timeline-rounded-corners);
    background-color: transparent;
    border-color: transparent;
    color: transparent;
  }

  .vimeo-player [type="range"]::-webkit-slider-thumb {
    -webkit-appearance: none;
    height: var(--timeline-dot-height);
    width: var(--timeline-dot-height);
    border-radius: var(--timeline-rounded-corners);
    background-color: var(--timeline-dot-color);
    cursor: pointer;
    margin-top: calc((var(--progress-height) / 2) - (var(--timeline-dot-height) / 2));
  }

  .vimeo-player [type="range"]::-moz-range-track {
    width: 100%;
    height: var(--progress-height);
    cursor: pointer;
    background: var(--progress-bg);
    border: 0;
    border-radius: var(--timeline-rounded-corners);
  }

  .vimeo-player [type="range"]::-moz-range-thumb {
    border: 0;
    height: var(--timeline-dot-height);
    width: var(--timeline-dot-height);
    border-radius: var(--timeline-rounded-corners);
    background: var(--timeline-dot-color);
    cursor: pointer;
  }

  /* Volume slider — ours, not the resource's. Styled as a short twin of
     the timeline: same track, dot and colours. The fill is a gradient
     stop at --volume-fill, which the script writes, since a plain range
     has no fill of its own outside Firefox. 24px tall for the 2.5.8
     target minimum; the input is the hit area, the track draws inside. */
  .vimeo-player__volume {
    flex-shrink: 0;
    width: 80px;
    height: 24px;
    display: flex;
    align-items: center;
  }

  .vimeo-player__volume-input {
    --volume-fill: 50%;
    cursor: pointer;
    -webkit-appearance: none;
    appearance: none;
    background-color: transparent;
    width: 100%;
    height: 100%;
    margin: 0;
    display: block;
  }

  .vimeo-player .vimeo-player__volume-input::-webkit-slider-runnable-track {
    height: var(--progress-height);
    border-radius: var(--timeline-rounded-corners);
    background: linear-gradient(to right,
      var(--progress-fill-bg) var(--volume-fill),
      var(--progress-bg) var(--volume-fill));
  }

  .vimeo-player .vimeo-player__volume-input::-moz-range-track {
    height: var(--progress-height);
    border-radius: var(--timeline-rounded-corners);
    background: linear-gradient(to right,
      var(--progress-fill-bg) var(--volume-fill),
      var(--progress-bg) var(--volume-fill));
  }

  /* Touch devices get mute only. iOS ignores volume set from a page — only
     the hardware buttons change it — so there the slider would move and
     nothing would happen. Android does honour it, but a finger-sized
     80px slider in a bar that hides itself mid-play is poor there too,
     and the hardware buttons are right to hand. */
  @media (hover: none) and (pointer: coarse) {
    .vimeo-player__volume { display: none; }
  }

  /* Mute + fullscreen: 24px square, the 2.5.8 minimum exactly. */
  .vimeo-player__mute,
  .vimeo-player__fullscreen {
    cursor: pointer;
    flex-shrink: 0;
    justify-content: center;
    align-items: center;
    width: 24px;
    height: 24px;
    display: flex;
    position: relative;
    appearance: none;
    padding: 0;
    border: 0;
    background: none;
    color: inherit;
  }

  .vimeo-player__volume-up-svg,
  .vimeo-player__volume-mute-svg,
  .vimeo-player__fullscreen-scale-svg,
  .vimeo-player__fullscreen-shrink-svg {
    width: 100%;
    position: absolute;
  }

  .vimeo-player .vimeo-player__mute svg:nth-child(2),
  .vimeo-player[data-vimeo-muted="true"] .vimeo-player__mute svg:nth-child(1),
  .vimeo-player .vimeo-player__fullscreen svg:nth-child(2),
  .vimeo-player[data-vimeo-fullscreen="true"] .vimeo-player__fullscreen svg:nth-child(1) {
    display: none;
  }

  .vimeo-player .vimeo-player__mute svg:nth-child(1),
  .vimeo-player[data-vimeo-muted="true"] .vimeo-player__mute svg:nth-child(2),
  .vimeo-player .vimeo-player__fullscreen svg:nth-child(1),
  .vimeo-player[data-vimeo-fullscreen="true"] .vimeo-player__fullscreen svg:nth-child(2) {
    display: block;
  }

  /* --------------------------------------------------------------------
     "What we're made of": the burgundy title panel (Figma 1928:9735) and
     the three sky cards that arrive over it (Figma 1928:9738).

     Base rules below are the small-screen layout — the panel pins and a
     single column of cards scrolls up over it. The three-column
     choreography is in the min-width: 901px block further down, so the
     two layouts don't have to undo each other. */
  .made-of {
    position: relative;
    /* Above the pinned reel it slides over — see .reel-scope. */
    z-index: 1;
    background: var(--bb-burgundy);
  }

  .made-of__panel {
    position: sticky;
    top: 0;
    z-index: 0;
    height: 100svh;
    /* Breathing room at the top and bottom of the panel. The composition
       is centred in what is left, so this is real clear space around the
       words rather than a crop. */
    padding: clamp(80px, 9svh, 120px) var(--page-margin);
    /* flex, not grid: the stage is taller than this box on most screens
       and has to overflow EQUALLY at both ends — Figma hangs the 1000px
       panel 50px above a 900px viewport and lets the other 50 fall below
       it. A centred flex item does that; a grid item does not, because
       align-items centres it inside its own row and the row is the one
       that overflows, all of it off the bottom. */
    display: flex;
    align-items: center;
    justify-content: center;
    overflow: hidden;
  }

  /* The word composition, locked to Figma's 1440x1000 frame.

     Everything inside is sized in cqw — including the vertical offsets —
     so the whole arrangement scales as one piece and the diagonal never
     shears. That only works because the stage's aspect ratio is fixed:
     with width and height locked to each other, a width-relative unit is
     a height-relative unit too.

     It fits to WIDTH and lets the height overflow, rather than
     contain-fitting. The composition carries 112px of empty frame above
     the first cap and below the last baseline (11.2% each side), so a
     viewport shorter than width/1.44 crops into that margin and not into
     the type — while a contain fit would shrink the 285.836px display
     type on every laptop screen to avoid cropping space that is empty
     anyway. At 1440x900, the design's own size, this renders the type at
     exactly 285.836px. */
  /* Everything inside the stage is expressed in cqw — the type size and
     both axes of every word's position — so sizing the STAGE is what
     scales the composition. Clamping this one width therefore scales the
     type and the diagonal by the same factor, and the arrangement cannot
     shear. Putting the clamp on font-size instead would have let the two
     drift apart at the clamp's flat ends.

     86vw rather than 100%: the stage no longer runs the full width of the
     panel, which is what pulls the type down and opens the clear space
     above and below it. The bounds stop it collapsing on a narrow window
     and stop it running away on a very wide one.

     86 is measured, not guessed. The composition is not vertically
     symmetric — the gap above the first cap and the gap below the last
     baseline differ by about 14px — so the two only land inside the
     80-120px target together in a narrow band of widths. At 1512x900,
     86vw gives 100px above and 86px below with the type at 258px; 90vw
     left the bottom at 69px, short of the target. */
  .made-of__stage {
    position: relative;
    width: min(100%, clamp(600px, 86vw, 1400px));
    aspect-ratio: 1440 / 1000;
    container-type: inline-size;
  }

  .made-of__word {
    position: absolute;
    /* Figma places each word by its centre and its baseline. The sitewide
       text-box trim already makes the block's bottom edge the baseline,
       so -100% on Y lands it exactly. */
    transform: translate(-50%, -100%);
    white-space: nowrap;
    font-family: "Haas Recast VF", "Helvetica Neue", Arial, sans-serif;
    font-weight: 300;
    font-variation-settings: 'TRAK' -35;
    font-size: 19.8497cqw;
    line-height: 0.93;
    letter-spacing: -0.02em;
    color: var(--bb-orange);
  }

  /* x = Figma's offset from the frame's centre line, y = its baseline,
     both over the 1440 frame width. */
  .made-of__word--what { left: calc(50% - 11.8750cqw); top: 22.0833cqw; }
  .made-of__word--were { left: calc(50% + 22.1528cqw); top: 36.4583cqw; }
  .made-of__word--made { left: calc(50% - 18.1597cqw); top: 50.7639cqw; }
  .made-of__word--of   { left: calc(50% + 16.1111cqw); top: 61.6667cqw; }

  .made-of__cards {
    position: relative;
    z-index: 1;
    padding: 0 var(--page-margin) 80px;
  }

  .made-of__cards-inner {
    display: grid;
    gap: var(--grid-gap);
  }

  .made-of__card {
    display: flex;
    flex-direction: column;
    justify-content: space-between;
    gap: 40px;
    width: 100%;
    padding: 20px;
    border-radius: 4px;
    background: var(--bb-sky);
    color: var(--bb-forest);
  }

  .made-of__card-heading {
    font-family: "STIX Two Text", Georgia, "Times New Roman", serif;
    font-weight: 400;
    font-style: italic;
    font-size: 48px;
    line-height: 0.94;
    letter-spacing: -0.02em;
    color: inherit;
  }

  .made-of__card-body {
    font-family: "Haas Recast VF", "Helvetica Neue", Arial, sans-serif;
    font-weight: 400;
    font-variation-settings: 'TRAK' 0;
    font-size: 18px;
    line-height: 1.28;
    letter-spacing: -0.36px;
    color: inherit;
  }

  @media (max-width: 640px) {
    /* min(): 36px wherever the longest heading fits on one line, scaling
       down with the viewport below about 340px so it still does. */
    .made-of__card-heading { font-size: min(36px, 10.5vw); }
    .made-of__card { gap: 28px; }
  }

  /* Mobile: one wrapped heading, and the cards stack into a deck.
     Figma 2129:3580.

     The panel drops the diagonal entirely. Below 900px the four words
     become one centred paragraph that simply wraps — the <p>s go inline
     and the whitespace between them in the markup is the word space, so
     the same four elements serve both layouts and neither needs its own
     copy. The fixed 1440x1000 stage goes with it: there is no diagonal
     left to protect, so the composition no longer needs an aspect ratio
     to scale against.

     35.06vw is the comp's 136.76px over its 390px frame, clamped so it
     stays readable on a very narrow phone and does not overshoot on a
     large tablet. TRAK 0 here, not the -35 the desktop words carry —
     the comp sets it that way, and at this size the tighter tracking
     closes the counters up too far. */
  @media (max-width: 900px) {
    .made-of__panel {
      padding: clamp(48px, 8svh, 80px) var(--page-margin);
    }

    .made-of__stage {
      width: 100%;
      aspect-ratio: auto;
      container-type: normal;
      text-align: center;
    }

    .made-of__word {
      position: static;
      display: inline;
      transform: none;
      white-space: normal;
      font-size: clamp(64px, 35.06vw, 160px);
      font-variation-settings: 'TRAK' 0;
      line-height: 1.11;
    }

    /* The deck. The cards pile up as the group scrolls, each sitting one
       strip lower than the one before, so every card behind the front one
       keeps its heading showing.

       --made-of-strip is that visible strip: 64px from one card's top edge
       to the next, which is the card's 20px top padding, the one-line
       heading and a little air, and none of the body copy. (It was the
       comp's 100px, which showed the top of the body under each heading.)
       It is the single number to change if the heading size moves.

       The finished pile — one card plus two strips — is centred in the
       viewport. That needs the card's height, which depends on the copy
       and the width, so initMadeOfDeck() in main.js measures it into
       --made-of-card-h. Without the script the pile pins at the
       fallback below instead; max() keeps a pile taller than a short
       screen from pinning above it.

       All three pin at the SAME top and are the same height — the grid's
       1fr rows equalise them — and the stagger is a transform instead.
       That is what makes the pile release as one: a sticky box unpins
       when its parent's bottom reaches its own, so boxes with matching
       tops and heights unpin on the same pixel. Staggering the sticky
       top instead unpinned them one by one, and with unequal heights the
       second card rode up over the first on the way out.

       The columns are display: contents so the three sticky wrappers are
       grid items of one shared parent. Left as boxes, each column was
       exactly its card's height, and a sticky box cannot travel outside
       its parent — nothing stuck at all.

       The empty ::after row is the hold: the finished pile stays put for
       that much scroll before it leaves. It has to be a real box, not
       padding — a sticky box is confined to its parent's content box, so
       bottom padding buys no travel.

       The pile releases resting on the group's bottom edge, so the two
       strips the transforms push the last cards down by hang below it.
       The section's bottom padding carries those strips, or the third
       card would overlap the team section. */
    .made-of {
      --made-of-strip: 64px;
      --made-of-stack-top: max(24px, calc(
        (100svh - var(--made-of-card-h, 260px) - 2 * var(--made-of-strip)) / 2
      ));
    }

    /* The page's own side margin rather than the comp's 50px, so each
       heading ("Natural curiosity." is the longest, 247px at 36px) fits on
       one line: that is what the 64px strip above assumes. */
    .made-of__cards {
      padding: 0 var(--page-margin) calc(80px + 2 * var(--made-of-strip));
    }

    .made-of__cards-inner {
      grid-template-rows: repeat(3, 1fr);
      row-gap: 24px;
    }

    .made-of__cards-inner::after {
      content: "";
      height: 30svh;
    }

    .made-of__col {
      display: contents;
    }

    .made-of__sticky {
      position: sticky;
      top: var(--made-of-stack-top);
    }

    .made-of__col:nth-child(2) .made-of__sticky {
      transform: translateY(var(--made-of-strip));
    }

    .made-of__col:nth-child(3) .made-of__sticky {
      transform: translateY(calc(2 * var(--made-of-strip)));
    }

    /* The comp's soft shadow along each card's top edge, which is what
       separates one sky strip from the next in the pile. */
    .made-of__card {
      height: 100%;
      box-shadow: 0 -8px 24px rgba(var(--bb-black-rgb), 0.12);
    }
  }

  /* Desktop: the three-column sticky choreography.

     Read the scroll as a timeline measured from the section's top. The
     panel pins at 0. The cards region is pulled back up over it, waits
     out --made-of-lead, then column 1 pins; each next column is offset by
     one --made-of-stagger, so the three cards arrive in turn and line up
     in a row. --made-of-tail is how long the completed row holds before
     everything releases.

     The panel and column 1 unpin on the same pixel, and that is
     arithmetic rather than a coincidence to maintain by hand: the section
     is (lead + column 1) tall, a sticky box unpins at its parent's bottom
     minus its own height, and both boxes are 100svh tall inside parents
     that end together. Change any of the three variables and they stay in
     step.

     Figma has the panel trailing ~390px past the last card; that is
     dropped on purpose, since it only buys an empty burgundy screen
     before the next section. */
  @media (min-width: 901px) {
    .made-of {
      --made-of-lead: 62svh;
      --made-of-stagger: 46svh;
      --made-of-tail: 52svh;
    }

    .made-of__cards {
      margin-top: -100svh;
      padding: var(--made-of-lead) var(--page-margin) 0;
    }

    /* Three cards of three grid columns each, with the grid's own gap
       between them, centred in the page — which is nine of the twelve
       columns and puts the group's edges where Figma has them (177px in
       at 1440). Written off --grid-cols-desktop/--grid-gap rather than as
       a max-width in px so it tracks the page grid. */
    .made-of__cards-inner {
      width: 100%;
      max-width: calc(
        (100% - (var(--grid-cols-desktop) - 1) * var(--grid-gap))
        / var(--grid-cols-desktop) * 9
        + 8 * var(--grid-gap)
      );
      margin: 0 auto;
      grid-template-columns: repeat(3, 1fr);
      align-items: stretch;
    }

    /* Column 1 is the only one with a height: it sets the grid row, and
       the other two stretch into what is left after their offset — which
       is what bottom-aligns all three and makes them release together. */
    .made-of__col:nth-child(1) {
      height: calc(100svh + 2 * var(--made-of-stagger) + var(--made-of-tail));
    }
    .made-of__col:nth-child(2) { margin-top: var(--made-of-stagger); }
    .made-of__col:nth-child(3) { margin-top: calc(2 * var(--made-of-stagger)); }

    /* 33.5svh is Figma's 302px over its 900px frame: the gap from the top
       of the viewport down to the top of a pinned card. Kept proportional
       rather than fixed so the row sits in the same place on the screen
       at any window height. */
    .made-of__sticky {
      position: sticky;
      top: 0;
      height: 100svh;
      padding-top: 33.5svh;
    }

    .made-of__card { height: 423px; }
  }
