/* Enduro HOMEPAGE styles — externalized verbatim from index.html's inline <style>
   (2026-06-25). Cascade position preserved: linked exactly where the <style> was,
   i.e. BEFORE nav.css / footer.css / contact-widget.css. No rules changed. */

      /* ─── RESET & TOKENS ─────────────────────────────────────── */
      *,
      *::before,
      *::after {
        box-sizing: border-box;
        margin: 0;
        padding: 0;
      }
      :root {
        /* Brand red aligned with the master logo SVG (2026-05-19).
           Was #E43338; now matches the #E43338 fill in the locked logo
           so the Process Step 04 stripe / red CTA tiles / eyebrows all
           read the same red as the logo. */
        --red: #E43338;
        --yellow: #e3ff17;
        --black: #191A1C;
        --surface: #141414;
        --off-white: #F4ECE2;
        --white: #ffffff;
        --w70: rgba(255, 255, 255, 0.7);   /* used by contact-widget.css */
        --max: 1440px;
        --gutter: 80px;
        /* Section rhythm tokens (2026-05-21 — Option B sweep).
           Shared by content sections (#projects, #reviews, #team). Hero
           and Process are 100vh containers and ignore these; #services
           uses the curtain-derived margin-top math; footer has its own
           rhythm. Changing these here re-paces every content section. */
        --section-pad-top: 160px;
        --section-pad-bot: 120px;
        /* IOE curtain reveal (2026-05-20 — sandbox v2 port).
           Pin y = viewport y where the TOP of .services-top sits during
           Phase 1 (the curtain reveal).
           Lift amount = how many px IOE travels upward during Phase 2
           (after "WHAT WE DO" is revealed, IOE is dragged upward by the
           photo's continued lift). Set to 0 to disable Phase 2.
           #services margin-top is derived from these two values.
           rev3: pinned higher (360) + Phase-2 lift sped up (200 → 500).
           rev4: lift backed off to 300 — at 500 the IOE flew off-screen by
           scrub-end AND cards' doc-y landed above the collapsed photo's
           bottom on MBP-16 (vh=996), causing visible clutter. 300 keeps
           naturalAtScrubEnd at +60 (top of viewport) and pushes cards
           safely below the photo on every viewport.
           rev5: IOE bumped down 50 px (360 → 410) per user — too high.
           rev6: another 40 px down (410 → 450) per user.
           rev7: another 30 px down (450 → 480) per user. */
        --ioe-pin-y: 480px;
        --ioe-lift-amount: 300px;
      }
      html {
        scroll-behavior: smooth;
      }
      body {
        font-family: "Inter", sans-serif;
        background: var(--white); /* white backdrop behind the transparent grid-reveal folds — the grid canvas already paints white on top (so no visual change), and the white body is also the nav-ink sampler's true base layer over those folds (2026-07-13; sampler v4 2026-07-16) */
        color: var(--black);
        font-size: 16px;
        line-height: 1.6;
        /* clip, NOT hidden (2026-06-11): overflow-x:hidden can
           make body a scroll container, silently killing every
           position:sticky descendant (the ledger cover-dock). clip
           clips identically without that side effect. */
        overflow-x: clip;
      }
      /* .page-content sits at z-index:2 ABOVE the fixed hero scrub-frame (z<2) so
         the in-flow sections render over it; transparent so the (now white) body
         backdrop shows through any transparent sections. */
      .page-content {
        position: relative;
        z-index: 2;
        overflow-x: clip;     /* clip the ~22px horizontal bleed from the scale(1.03) hero + team photo zooms */
        overflow-y: visible;  /* keep Y visible → no scroll container, scroll engine + team photo's vertical overflow untouched (2026-06-22) */
        background: transparent;
        /* PERF-BONUS contain:paint was removed — it caused a white fill to
           appear behind the hero photo during the curtain animation. The hero
           frame is fixed/behind page-content; contain:paint clipped the
           background flood and broke the wipe reveal. Not safe here. */
      }
      /* (Persistent-nav view-transition-name REMOVED 2026-06-12:
         giving #main-nav its own transition group isolates it into a
         separate paint layer, which kills the backdrop-filter glass.
         Glass wins — the nav rides with the page wipe for now. The two
         are mutually exclusive in the browser.) */
      img {
        display: block;
        width: 100%;
        height: 100%;
        object-fit: cover;
      }
      a {
        text-decoration: none;
        color: inherit;
      }

      /* ─── CONTAINER ──────────────────────────────────────────── */
      .container {
        max-width: var(--max);
        margin: 0 auto;
        padding: 0 var(--gutter);
      }
      /* Homepage: while the red closing band is under the nav, drop the #nav-glass frost — its backdrop-filter
         darkens + lags the animating red on iOS (the "darker-red doubling"). Nav ink is already white over the
         red there, so links stay legible. Body class set by c11Text in home.js. (2026-07-10) */
      #nav-glass { transition: opacity 0.2s ease; }
      body.c11-red-under-nav #nav-glass { opacity: 0; }

      /* ─── BUTTONS ────────────────────────────────────────────── */
      .btn {
        display: inline-flex;
        align-items: center;
        gap: 10px;
        font-family: "Inter", sans-serif;
        font-size: 12px;
        font-weight: 600;
        letter-spacing: 0.09em;
        text-transform: uppercase;
        padding: 0 28px;
        height: 52px;
        border-radius:0;
        cursor: pointer;
        border: none;
        transition:
          transform 0.32s cubic-bezier(0.22, 1, 0.36, 1) 0.06s,
          background 0.22s ease,
          color 0.22s ease,
          box-shadow 0.18s ease;
        white-space: nowrap;
        position: relative;
      }
      /* Higher-specificity hover so it isn't overridden by .reveal.in
         (which also sets transform on the element). Same animation across
         every filled/outline button on the site. */
      @media (hover: hover) and (pointer: fine) {
      .btn:hover,
      .btn.reveal:hover,
      .btn.reveal.in:hover {
        transform: translateX(4px);
      }
      .btn-ghost:hover {
        background: var(--yellow);
        color: var(--black);
      }
      /* Hero "Portfolio" (ghost) drops the translateX slide on hover;
         the red "Contact Us" keeps it (2026-06-12). */
      .hero-actions .btn-ghost:hover { transform: none; }
      }
      .btn-red {
        background: var(--red);
        color: var(--white);
      }
      .btn-ghost {
        background: rgba(255, 255, 255, 0.12);
        color: var(--white);
      }

      /* ─── SECTION 1 · HERO (fixed-overlay scrub) ──────────────── */
      /* Section is one viewport tall; photo + text are position:fixed
         so they overlay the viewport while JS scrubs their transform
         against window.scrollY. Once scrolled past, they hide and the
         next section flows in naturally — no blank-page residue. */
      #hero {
        position: relative;
        /* Restored to 100vh (2026-05-19). The earlier 80vh shortcut
           was breaking the scroll position of the In Our Element section.
           Keeping the canonical full-viewport hero. */
        height: 100vh;
        height: 100svh;   /* svh = the SMALL (URL-bar-shown) viewport → the fixed hero == the visible screen on mobile and is never clipped behind the browser bar. 100vh fallback for old engines; pairs with the JS heroVH lock. (2026-07-10) */
        background: var(--white);
      }
      .hero-sticky {
        position: relative;
        height: 100%;
      }
      .hero-photo-frame {
        position: fixed;
        top: 0;
        left: 0;
        width: 100vw;
        height: 100vh;
        height: 100svh;   /* visible-screen height on mobile — never clipped behind the URL bar; 100vh fallback (2026-07-10) */
        overflow: hidden;
        z-index: 1;
        pointer-events: none;
        will-change: clip-path, transform, opacity;
      }
      .hero-bg {
        position: absolute;
        inset: 0;
        transform-origin: center center;   /* the scroll-driven 1.03→1.00 depth-zoom rides here (JS) */
        will-change: transform;
      }
      /* Inner layer carries the photo + a SEPARATE, very subtle 2% zoom-OUT on initial land (rides WITH the
         text wipe). Free of the scroll JS (which transforms .hero-bg) so the two compose. (2026-06-16) */
      .hero-bg-zoom {
        position: absolute; inset: 0;
        background-image: url("/assets/img/ph_hero_seedstone_retail.webp");   /* Seed & Stone retail hero — 2.png */
        background-size: cover; background-position: center center;
        transform-origin: center center; transform: scale(1.02);
        transition: transform 1.1s cubic-bezier(0.22, 1, 0.36, 1);
      }
      /* on-demand will-change: only during the one-shot land-zoom (before .hero-wiped); dropped after (perf — was permanent). */
      #hero:not(.hero-wiped) .hero-bg-zoom { will-change: transform; }
      #hero.hero-wiped .hero-bg-zoom { transform: scale(1); }
      @media (prefers-reduced-motion: reduce) { .hero-bg-zoom { transform: none !important; transition: none !important; } }
      .hero-overlay {
        position: absolute;
        inset: 0;
        background: linear-gradient(
          to top,
          rgba(0, 0, 0, 0.88) 0%,
          rgba(0, 0, 0, 0.45) 45%,
          rgba(0, 0, 0, 0.15) 100%
        );
      }
      /* Full-viewport fixed layer — JS clips it to the photo frame each frame
         so the headline + buttons get cropped by the shrinking container.
         Both Recent Projects and Discuss Your Space disappear together as the
         container shrinks above them. */
      .hero-content {
        position: fixed;
        top: 0;
        left: 0;
        width: 100vw;
        height: 100vh;
        height: 100svh;   /* visible-screen height so the buttons sit inside the viewport, not below the URL bar (2026-07-10) */
        z-index: 3;
        pointer-events: none;
        display: flex;
        align-items: flex-end;
        justify-content: center;
        will-change: opacity, clip-path;
      }
      .hero-content .hero-inner {
        width: 100%;
        max-width: var(--max);
        /* Bottom padding bumped 96 → 196 (2026-05-20). Pushes the
           whole hero stack (headline + subtext + buttons) up by ~100 px so
           the bottom of "STAND BEHIND" lands on the iPhone-667 ruler
           instead of the iPad-768 ruler. Subtext + buttons follow because
           they're stacked below the headline in normal flow inside this
           same container; the spacing between the three elements is
           untouched. */
        /* bottom padding trimmed 196 → 92 (2026-06-13): the big pad sat
           the hero stack too high with a long empty hold before the lift —
           lowers "A GC You Can Stand Behind" and removes that bottom-pause. */
        /* 92 → 142 (2026-06-23): lift the whole hero stack (headline +
           subtext + buttons) up 50px. */
        padding: 0 var(--gutter) 142px;
        box-sizing: border-box;
        pointer-events: auto;
      }
      .hero-inner {
        max-width: var(--max);
        margin: 0 auto;
        padding: 0 var(--gutter);
      }
      /* Tightened line-height (0.83 → 0.78) and zero descender padding so the
         visual bottom of "STAND BEHIND" optically matches the green-square's
         left padding. Headline size -10% (2026-05-19): clamp values
         all scaled by 0.9. */
      /* hero CTAs sharp (2026-06-11) */
      .hero-actions .btn { border-radius: 0; width: 100%; justify-content: center; }   /* fill the equal grid column + keep the label centred now the ghost button is wider (2026-07-10) */

      .hero-headline {
        font-family: "Inter", sans-serif;
        font-size: clamp(44px, 5.2vw, 80px);   /* min 38→44 to match the "In Our Element" .section-heading (clamp 44/5vw/72) on mobile+tablet, where both pin to 44px; laptop unchanged, hero still rides 5.2vw (2026-07-10) */
        font-weight: 900;
        line-height: 0.815;   /* home only: ~75% back toward the original tighter 0.78 (likes it tighter here) — 2026-07-08 */
        letter-spacing: -0.066em;   /* home only: ~75% back toward the original tight -0.075em */
        text-transform: uppercase;
        color: var(--white);
        /* Subtext (.hero-sub) sits between this and the buttons now; let
           that element own its own top/bottom margins so the headline
           only needs a small breathing room below it. */
        margin-bottom: 0;
        max-width: 14ch;
        padding-bottom: 0;
      }
      /* Hero content elements are scroll-driven via inline opacity/transform —
         override the .reveal transition + per-element delays so they don't
         desync (subtext was lagging buttons by 80ms due to .d1 vs .d2). */
      .hero-headline,
      .hero-sub,
      .hero-actions {
        transition: none !important;
      }
      /* HERO WIPE (2026-06-16) — "A GC You Can Stand Behind" + "Commercial Construction Experts" wipe L→R
         into view on initial page LAND (same effect as the portfolio's "Every Build. One Standard."). On an INNER
         span so it's free of the parent's transition:none + the scroll-driven inline opacity/transform. Fires once
         on land (#hero.hero-wiped), so it never re-triggers on scroll. */
      .hero-wipe { display: inline-block; vertical-align: top; clip-path: inset(0 100% 0 0);
        transition: clip-path 0.9s cubic-bezier(0.76, 0, 0.24, 1); will-change: clip-path;
        padding-right: 0.14em; margin-right: -0.14em; }   /* per-line span; the padding keeps the clip box clear of
        the last glyph's overhang (negative letter-spacing pulled inset(0) into the "D"); the equal negative margin
        keeps layout width unchanged (2026-06-16) */
      #hero.hero-wiped .hero-wipe { clip-path: inset(0 0 0 0); }
      /* each headline LINE fires a split-second after the previous (sequential), then the subtext */
      #hero.hero-wiped .hero-headline .hero-wipe:nth-of-type(1) { transition-delay: 0s; }
      #hero.hero-wiped .hero-headline .hero-wipe:nth-of-type(2) { transition-delay: 0.08s; }
      #hero.hero-wiped .hero-headline .hero-wipe:nth-of-type(3) { transition-delay: 0.16s; }
      #hero.hero-wiped .hero-sub .hero-wipe { transition-delay: 0.24s; }
      /* CONTACT US + PORTFOLIO buttons wipe in next, continuing the sequence (2026-06-16). On a wrapper span so
         each button keeps its own hover transition; the -6px right inset on the open state gives room for the red
         button's translateX(4px) hover slide so it isn't clipped. */
      .hero-btn-wipe { display: inline-block; clip-path: inset(0 100% 0 0); transition: clip-path 0.9s cubic-bezier(0.76, 0, 0.24, 1); will-change: clip-path; }
      #hero.hero-wiped .hero-btn-wipe { clip-path: inset(0 -6px 0 0); }
      #hero.hero-wiped .hero-btn-wipe:nth-of-type(1) { transition-delay: 0.32s; }
      #hero.hero-wiped .hero-btn-wipe:nth-of-type(2) { transition-delay: 0.40s; }
      @media (prefers-reduced-motion: reduce) { .hero-wipe, .hero-btn-wipe { clip-path: none !important; transition: none !important; } }
      .hero-sub {
        /* "Commercial Construction Experts" — matched to the In-Our-Element
           card titles (.service-title): Inter 800, line-height 0.92,
           letter-spacing -0.04em, uppercase, white. Size tops out at 30px
           (the card size at the 1440 canvas) and scales down on smaller
           screens so it never overflows; nowrap dropped so it wraps if the
           viewport is too narrow rather than clipping. (2026-06-23) */
        font-size: clamp(15px, 1.3vw, 19px);   /* matched to the other pages' hero sub size + weight (2026-07-08) */
        font-weight: 300;
        line-height: 1.6;
        letter-spacing: 0;
        text-transform: none;
        color: var(--white);
        margin-top: 22px;
        margin-bottom: 36px;
      }
      .hero-actions {
        /* Both hero buttons the SAME width on all screens: inline-grid, two 1fr columns. In a
           shrink-to-fit grid, 1fr resolves BOTH columns to the WIDER button's width, so "Portfolio"
           grows rightward to match "Contact Us" — no magic min-width. (2026-07-10) */
        display: inline-grid;
        grid-auto-flow: column;
        grid-auto-columns: 1fr;
        align-items: center;
        gap: 10px;   /* was 16px — the two buttons moved closer (2026-07-10) */
      }

      /* ─── PAGE REVEAL ANIMATION ──────────────────────────────── */
      /*
  Sections that are white/light slide over the hero as a panel.
  Dark sections simply reveal their content in place.
  The "panel" creates the feeling of a page being built on top.
*/
      /* Reveal for individual elements inside sections */
      .reveal {
        opacity: 0;
        transform: translateY(48px) scale(0.985);
        transition:
          opacity 0.85s cubic-bezier(0.16, 0.84, 0.34, 1),
          transform 0.95s cubic-bezier(0.16, 0.84, 0.34, 1);
      }
      .reveal.in {
        opacity: 1;
        transform: translateY(0) scale(1);
      }
      /* Stagger delays — varied for a more organic, dynamic feel */
      .reveal.d1 { transition-delay: 0.12s; }
      .reveal.d2 { transition-delay: 0.26s; }   /* .d3/.d4 removed — never applied (2026-06-25 dead-code sweep) */
      /* PERF-BONUS: Fast exit when .in is removed (scrolling back up).
         Prevents simultaneous long entry+exit transitions running on 12+
         cards at once. Entry delays stay intact; exit snaps in 0.15s. */
      .reveal:not(.in) {
        transition-duration: 0.15s;
        transition-delay: 0s;
        /* on-demand will-change: promote ONLY while the element still waits to
           reveal (dozens of .reveal nodes); dropped once .in so we don't hold a
           permanent GPU layer on every revealed element (perf — was on base .reveal). */
        will-change: transform, opacity;
      }
      /* CONTINUITY (2026-06-22): EVERY fold title + eyebrow handle on .reveal-early
         (Featured Projects, Our Process, …) exits with the SAME smooth timing as its entry —
         never the 0.15s perf-snap above. One rule = all fold titles animate out identically. */
      .reveal-early:not(.in) {
        transition-duration: 0.85s, 0.95s;
        transition-delay: 0s;
      }
      /* (2026-05-29) — the 0.15s perf-snap above is meant for the 12
         grid cards. It also caught the "Featured Projects" eyebrow + heading,
         making them flick out on scroll-up while every other section eases
         out slowly — eye-catching. Restore a smooth exit for just the FP
         title block (matches its own entry: opacity 0.85s, transform 0.95s).
         Grid cards keep the fast perf exit (they're handled by #6 separately). */
      .projects-top .reveal:not(.in) {
        transition-duration: 0.85s, 0.95s;
      }
      /* SERVICE CARDS (desktop) — start hidden, offset down. Revealed by
         .services-grid.trigger (added by the JS when the grid enters the viewport)
         gated on body.curtain-done — not the generic .reveal IO. Slowed ~15% so the
         entry feels less rushed. (≤960 overrides this — see the IOE-block rules there.) */
      .service-card {
        opacity: 0;
        transform: translateY(140px);
        transition:
          opacity 1.04s cubic-bezier(0.18, 0.89, 0.32, 1.05),
          transform 1.21s cubic-bezier(0.18, 0.89, 0.32, 1.05);
      }
      .service-card:nth-child(2) {
        transform: translateY(160px);
        transition-delay: 0.14s;
      }
      .service-card:nth-child(3) {
        transform: translateY(180px);
        transition-delay: 0.28s;
      }
      /* v2 rev4: visible state requires BOTH .trigger (set when grid is
         in viewport) AND body.curtain-done (set when Phase 2 starts) so
         cards never reveal during the curtain phase. */
      body.curtain-done .services-grid.trigger .service-card {
        opacity: 1;
        transform: translateY(0);
      }
      /* Direction-aware exit: when user scrolls DOWN past services, cards
         slide UP and disappear under the nav. When scrolling UP back, cards
         re-enter from below (default initial state already handles this). */
      .services-grid.trigger-exit-up .service-card {
        opacity: 0;
        transform: translateY(-140px);
      }

      /* ─── SECTION 2 · IN OUR ELEMENT (Services) ─────────────── */
      #services {
        background: var(--white);

        /* Bottom padding 176 → 100 (2026-05-20 rev8.6) — tightens
           the gap between IOE cards and the "Featured Projects" title. */
        padding: 16px 0 100px;
        /* IOE curtain port (2026-05-20 — sandbox v2, rev2).
           Derived so that at scrub-end the natural-flow viewport y of
           .services-top equals (--ioe-pin-y − --ioe-lift-amount). That's
           where Phase 2 ends, so the JS transform reaches 0 there → no
           jump. Subtract 16 for #services padding-top.
           Math: 100vh + margin_top + 16 = 0.65vh + pinY − liftAmount
                 ⇒ margin_top = (pinY − liftAmount − 16) − 35vh. */
        margin-top: calc(var(--ioe-pin-y, 440px) - var(--ioe-lift-amount, 200px) - 16px - 35vh);
      }
      .section-tag {
        display: block;
        font-size: 10px;
        font-weight: 600;
        letter-spacing: 0.18em;
        text-transform: uppercase;
        color: var(--red);
        margin-bottom: 16px;
      }
      .section-heading {
        font-family: "Inter", sans-serif;
        font-size: clamp(44px, 5vw, 72px);
        font-weight: 900;
        line-height: 0.88;
        letter-spacing: -0.048em;
        text-transform: uppercase;
        color: var(--black);
      }
      /* services-top: wrapper for the section title block.
         (2026-05-20 rev7 — reverted z-index/pin experiment that
         made things worse. The clean fix landed at the section level
         via #services { margin-top: -X } instead.)
         (2026-07-22) Was a two-child flex row — title left, the "Two core
         services, one standard…" tagline bottom-aligned on the right gutter.
         The tagline was removed with the "Core services" rename, so
         justify-content / align-items / gap had nothing left to position and
         were retired. Kept as flex (one item) so the title block keeps
         shrink-wrapping to its own width exactly as before. */
      .services-top {
        /* STACKING GUARD (2026-07-22, corrected). #services is pulled UP into the hero's box
           by its negative margin-top and the hero's fixed layers carry z-index 3, so the hero
           painted over the section heading and sliced it — the defect both principals
           photographed (105px overlap at 1366x768 before today's parity change).
           Guard the CONTENT only, never #services itself. NOTE the earlier note here claimed
           #services has "a solid white background" — it does NOT: index.html forces
           `#hero, #services, … { background-color: transparent !important }`, the white comes
           from <body>. Promoting the whole section still covered the photo, but via stacking
           order, not a background fill.
           (2026-07-22, later) The guard is now SCOPED TO `body.curtain-done` — see the rule
           below. It is only needed while the block is revealed and rising past the hero; the
           moment the class drops, z-index falls back to auto and the hero's fixed layers
           reclaim the top, so the block's fade-out can only ever play BEHIND the photo. That
           is what lets the fade exist at all: an unscoped z-index:5 meant a fade-out running
           longer than the scroll left the eyebrow painting on the hero photo. Tying paint
           order to the same state that controls visibility removes the whole failure class —
           the two can no longer disagree. */
        position: relative;
        z-index: auto;
        display: flex;
        /* +20px above the section title (2026-06-12). Lives INSIDE
           the curtain-transformed box, so the box top — which the
           curtain math lands at (pinY − liftAmount) — is unchanged;
           only the title content shifts down. No curtain jump. */
        padding-top: 20px;

        margin-bottom: 72px;
        /* No clip-path here — clip lives on #servicesTitleBlock instead (v2 rev3). */
        /* (2026-05-21 rev21) — promote to a GPU layer so the
           JS-applied translateY transform doesn't re-rasterise the
           title text every scroll frame. Without this, sub-pixel
           transform values during the curtain reveal (Phase 1, scroll
           y ≈ 0 – curtainEnd) produced a subtle text jitter on
           "What We Do" + the heading. The service cards below were
           unaffected because they aren't transformed in Phase 1. */
        will-change: transform;
      }
      /* The stacking guard itself — deliberately keyed to the SAME class that reveals the
         block, so paint order and visibility can never disagree. z-index is discrete: it
         flips with the class, it does not transition, which is exactly what makes the
         fade-out safe no matter how long it runs. (2026-07-22) */
      body.curtain-done .services-top { z-index: 5; }
      /* (2026-05-29) — the IOE title block was revealed by a clip-wipe
         tied to the hero scrub (played during scroll 0–300, easy to miss and
         unlike the other section titles). Swapped to a fade/rise that plays as
         the curtain finishes (body.curtain-done) so the eyebrow + heading come
         in like the other red titles.
         (2026-07-22) THE FADE IS SAFE AGAIN — and it is the STACKING GUARD, not the
         timer, that makes it safe. History, because this was got wrong twice:
         the fade was originally the only time-based property in an otherwise
         scroll-scrubbed hero. `body.curtain-done` flips on SCROLL POSITION while the
         fade ran on a CLOCK, so a fast scroll-up outran it and left the eyebrow
         painting on the hero photo (measured 173ms fast / 199ms slow). Deleting the
         timer fixed that but made the block hard-cut in and out — correct, and ugly.
         The real fix is to scope `.services-top`'s z-index to the SAME class that
         reveals the block (see the rule above): the instant `curtain-done` drops, the
         block falls behind the hero's fixed layers, so however long the fade-out
         takes it can only ever play BEHIND the photo — never on top of it. Paint
         order, not timing, is what guards the photo. With that in place the fade is
         free to be as slow as it looks good.
         Enter/exit are ASYMMETRIC on purpose (motion standard: exits snap): 0.2275s
         in — the hand-tuned value from earlier today — and ~30% quicker out, so
         scrolling back up feels responsive rather than draggy.
         A pure bottom-up wipe (dropping the opacity gate entirely and letting the
         photo edge uncover the block) was built and REJECTED: it uncovers the
         heading before its eyebrow and slices mid-glyph for ~30px of scroll —
         breaking hard rule 4 (eyebrow → title → content) and reproducing the very
         defect the owners photographed.
         ≤960 keeps its own fade below (native hero, no overlap to guard). */
      #servicesTitleBlock {
        opacity: 0;
        transition: opacity 0.16s ease-out;      /* EXIT — snappier than the entrance */
      }
      body.curtain-done #servicesTitleBlock {
        opacity: 1;
        transition: opacity 0.2275s ease-out;    /* ENTER — the tuned value, restored */
      }
      /* (2026-07-22) `.services-desc` + `.services-desc-more` RETIRED — the
         "Two core services, one standard…" tagline was removed from the markup
         with the "Core services" rename, so its entrance animation (desc-in,
         gated on body.curtain-done, toggled from applyTrigger) and its
         right-gutter typography had nothing left to style. The matching
         `desc-in` toggle was removed from home.js at the same time. */
      /* Cards constrained to container — tall, centered.
         Magnetic Expand (2026-05-19): the row redistributes on hover
         — hovered card grows 1fr → 1.2fr, the other two shrink to 0.9fr.
         The magnet is DISABLED while the page is actively scrolling
         (body.is-scrolling), so the cards don't flash bigger as the
         cursor passes over them mid-scroll. They only react to genuine
         hover after the user has been still for ~1.5s. */
      .services-grid {
        display: grid;
        grid-template-columns: 1fr 1fr 1fr;
        gap: 3px;
        /* transform transition smooths the JS-driven scroll lift between
           discrete scroll events — fixes the jumpy feel at varied scroll
           speeds. (2026-05-20 v2 rev13.) */
        transition:
          grid-template-columns 0.55s cubic-bezier(0.22, 1, 0.36, 1),
          transform 0.18s cubic-bezier(0.22, 1, 0.36, 1);
        /* will-change:transform dropped — the grid itself is no longer transformed
           (the JS scroll-lift was removed rev15; the entrance transform lives on
           .service-card children), so the hint held a permanent layer for nothing. */
      }
      @media (hover: hover) and (pointer: fine) {
      body:not(.is-scrolling) .services-grid:has(.service-card:nth-child(1):hover) { grid-template-columns: 1.2fr 0.9fr 0.9fr; }
      body:not(.is-scrolling) .services-grid:has(.service-card:nth-child(2):hover) { grid-template-columns: 0.9fr 1.2fr 0.9fr; }
      body:not(.is-scrolling) .services-grid:has(.service-card:nth-child(3):hover) { grid-template-columns: 0.9fr 0.9fr 1.2fr; }
      }
      .service-card {
        position: relative;
        overflow: hidden;
        /* -10% (2026-05-19): 680 → 612. Combined with the smaller
           title-to-cards gap, the full row fits comfortably on a 16" MBP.
           -10% again (2026-05-29): 612 → 551, more breathing room on
           the services fold. */
        height: 551px;
        background: var(--surface);
        cursor: pointer;
      }
      .service-card-img {
        position: absolute;
        inset: 0;
        transition: transform 0.65s ease;
      }
      @media (hover: hover) and (pointer: fine) {
      body:not(.is-scrolling) .service-card:hover .service-card-img {
        transform: scale(1.06);
      }
      }
      /* Per-photo crop offset for the AI service-card photo. */
      img[src="assets/img/photos_ai/image.webp"] {
        object-position: 55.7% 50.4%;
      }
      .service-card-overlay {
        position: absolute;
        inset: 0;
        background: linear-gradient(
          to top,
          rgba(0,0,0, 1) 0%,
          rgba(0,0,0, 0.438) 50%,
          rgba(0,0,0, 0.15) 100%
        );
        transition: background 0.4s;
      }
      @media (hover: hover) and (pointer: fine) {
      .service-card:hover .service-card-overlay {
        background: linear-gradient(
          to top,
          rgba(0,0,0, 1) 0%,
          rgba(0,0,0, 0.525) 50%,
          rgba(0,0,0, 0.225) 100%
        );
      }
      }
      .service-card-content {
        position: absolute;
        bottom: 0;
        left: 0;
        right: 0;
        padding: 44px 40px;
      }
      .service-title {
        font-family: "Inter", sans-serif;
        font-size: 30px;
        font-weight: 800;
        line-height: 0.92;
        letter-spacing: -0.04em;
        text-transform: uppercase;
        color: var(--white);
        margin-bottom: 16px;
      }
      .service-body {
        font-size: 14px;
        line-height: 1.5;
        color: rgba(255, 255, 255, 0.7);
        white-space: normal;
        /* TWO-line wrap, LOCKED (2026-06-25): a FIXED body width so the
           subtext wraps the SAME no matter how wide the card gets — the hover
           column-resize (1.2fr / 0.9fr) was shifting the line breaks. 300px fits
           inside even the narrowest hovered card (~302px) and keeps both GC + CM
           subtexts on two lines; max-width:100% lets it shrink on mobile. */
        width: 300px;
        max-width: 100%;
      }
      /* ── tablet-only (641–960) wrap tweaks (2026-07-10) ── */
      .hero-sub-br { display: none; }   /* the <br> after "research labs," in the hero sub shows on TABLET only */
      @media (min-width: 641px) and (max-width: 960px) {
        .hero-sub-br { display: inline; }                     /* hero sub breaks after "labs," on tablet */
        .service-body { width: auto; white-space: nowrap; }   /* GC/CM card subtext runs across one line (unlocks the 300px 2-line lock) on tablet */
        /* IOE ("What We Do") reveals a tad sooner on TABLET — the +30px gap (≤960) read as too much scroll to
           clear the hero here (cutting "Stand Behind" before IOE showed); trimmed to 12px on tablet only. Mobile
           keeps 30px, which reads beautifully. (2026-07-10) */
        #services { margin-top: 12px; }
      }

      /* ─── CTA card variant ─────────────────────────────────────
         The 3rd card slot is no longer "Coming Soon" — it's a solid red
         CTA that calls "Let's Talk." with an animated underline on
         hover. Shares .service-card sizing + reveal animations so the
         existing slide-in/out IO logic keeps working untouched. */
      .service-card.service-card--cta {
        background: var(--red);
        cursor: pointer;
        display: flex;
        flex-direction: column;
        justify-content: flex-end;
        /* bottom padding 44 → 21 (2026-06-23): drops the whole CTA
           block so the BOTTOM of "Let's Talk." lands on the GC/CM one-line
           subtext baseline (title bottom = 21 + underline 1 + its 22 margin
           = 44px from the card bottom = the subtext line). Underline rides
           just below, near the card floor. */
        padding: 44px 40px 21px;
      }
      .service-card--cta .cta-bar, .project-card--cta .cta-bar {   /* little stroke accent — WHITE on the red CTA cards (mirrors the "Cleared to build?" tile's .f04-cap-bar, which is black on yellow) (2026-07-16) */
        width: 30px;
        height: 3px;
        background: var(--white, #fff);
        margin-bottom: 16px;
      }
      .service-card--cta .cta-tag {
        font-size: 11px;
        font-weight: 700;
        letter-spacing: 0.22em;
        text-transform: uppercase;
        color: rgba(255, 255, 255, 0.75);
        margin-bottom: 14px;
      }
      .service-card--cta .cta-title {
        font-family: "Inter", sans-serif;
        font-size: 44px;
        font-weight: 900;
        line-height: 0.92;
        letter-spacing: -0.04em;
        text-transform: uppercase;
        color: var(--white);
      }
      .service-card--cta .cta-underline {
        display: block;
        width: 0;
        height: 1px;
        background: var(--white);
        margin-top: 22px;
        transition: width 0.55s ease;
      }
      @media (hover: hover) and (pointer: fine) { .service-card--cta:hover .cta-underline { width: 65%; } }

      /* ─── SECTION 3 · PROJECTS ───────────────────────────────── */
      #projects {
        position: relative;
        background: var(--white);
        /* Padding normalised to the section rhythm tokens (
           2026-05-21 Option B). Was 121/220.
           2026-06-11: +25/+25 over the tokens — the 12-card grid is
           the densest fold on the page and read as cramped at the
           standard rhythm (spacing audit).
           2026-07-09: top +25 dropped (→ token) to tighten the
           services→projects seam; bottom keeps +25. */
        padding: var(--section-pad-top) 0
          calc(var(--section-pad-bot) + 25px);
      }
      .projects-top {
        display: flex;
        justify-content: space-between;
        align-items: flex-end;
        margin-bottom: 72px;
        gap: 32px;
      }
      /* Title row stays inside .container for alignment with other sections */
      #projects .container {
        max-width: var(--max);
      }
      /* Grid wrapper — gutter-aligned with the rest of the site (
         2026-05-19). Was max-width 1800 / padding 96; now matches the
         standard 1440 / 80 container so the photo grid sits on the same
         left + right rulers as every other section. Photos and content
         inside scale via 1fr units. */
      .projects-grid-wrap {
        max-width: var(--max);
        margin: 0 auto;
        padding: 0 var(--gutter);
        box-sizing: border-box;
      }
      /* 6×4 bento grid with explicit placement — no row/column fr transitions
         (those caused the lag). Hover uses transform: scale on cards so the
         interaction stays GPU-light. */
      .projects-grid {
        display: grid;
        grid-template-columns: repeat(6, 1fr);
        grid-template-rows: repeat(4, 1fr);
        gap: 14px;
        /* -23% (2026-05-29): 1100 → 850. Trims the Projects bento from
           ~1.7 folds to ~1.4 so the section scrolls in less. Cards scale with
           the grid (rows ~202px); object-fit:cover just shows a shorter slice. */
        height: 850px;
      }
      .project-card .pc-link { position: absolute; inset: 0; z-index: 6; }
      .project-card .pc-link:focus-visible { outline: 2px solid var(--red); outline-offset: -3px; }
      .project-card {
        position: relative;
        overflow: hidden;
        background: var(--surface);
        cursor: pointer;
        min-width: 0;
        min-height: 0;
        transition:
          transform 0.55s cubic-bezier(0.22, 1, 0.36, 1),
          opacity 0.4s ease;
        will-change: transform;
        transform-origin: center center;
      }
      /* Bento placement — the ORIGINAL 12-card layout (2026-05-19):
         Maison Margiela = bottom-left BIG block (3 cols × 2 rows); card #4 =
         narrow tall slot (col 5, rows 1-2), a project card. The Portfolio CTA
         is card #12 — the wide bottom-right tile (see .project-card--cta below). */
      .project-card:nth-child(1)  { grid-column: 1 / span 2; grid-row: 1 / span 2; }
      .project-card:nth-child(2)  { grid-column: 3 / span 1; grid-row: 1 / span 1; }
      .project-card:nth-child(3)  { grid-column: 4 / span 1; grid-row: 1 / span 1; }
      .project-card:nth-child(4)  { grid-column: 5 / span 1; grid-row: 1 / span 2; }
      .project-card:nth-child(5)  { grid-column: 6 / span 1; grid-row: 1 / span 1; }
      .project-card:nth-child(6)  { grid-column: 3 / span 2; grid-row: 2 / span 1; }
      .project-card:nth-child(7)  { grid-column: 6 / span 1; grid-row: 2 / span 1; }
      .project-card:nth-child(8)  { grid-column: 1 / span 3; grid-row: 3 / span 2; } /* Maison Margiela */
      .project-card:nth-child(9)  { grid-column: 4 / span 1; grid-row: 3 / span 1; }
      .project-card:nth-child(10) { grid-column: 5 / span 2; grid-row: 3 / span 1; }
      .project-card:nth-child(11) { grid-column: 4 / span 1; grid-row: 4 / span 1; }
      .project-card:nth-child(12) { grid-column: 5 / span 2; grid-row: 4 / span 1; } /* Portfolio CTA */

      /* (2026-05-29) — z-index by row so a card descending from above
         on fast scroll-up never gets painted OVER by a lower row. Earlier rows
         sit on top: row 1 (cards 1-5) > row 2 (6,7) > row 3 (8-10) > row 4
         (11,12). Fixes "row 1 ends up under row 2" during the staggered
         re-entry. Cards are position:relative already, so z-index applies. */
      .project-card:nth-child(1),
      .project-card:nth-child(2),
      .project-card:nth-child(3),
      .project-card:nth-child(4),
      .project-card:nth-child(5)  { z-index: 4; }
      .project-card:nth-child(6),
      .project-card:nth-child(7)  { z-index: 3; }
      .project-card:nth-child(8),
      .project-card:nth-child(9),
      .project-card:nth-child(10) { z-index: 2; }
      .project-card:nth-child(11),
      .project-card:nth-child(12) { z-index: 1; }

      /* Portfolio CTA — wide bottom-right tile (2 cols × 1 row, where
         Henning Lab used to be). Just "Portfolio", sized to match the
         In Our Element "Let's Talk." CTA. */
      .project-card--cta {
        background: var(--red);
        color: var(--white);
        padding: 28px 32px;
        display: flex;
        flex-direction: column;
        justify-content: flex-end;
        cursor: pointer;
      }
      .portfolio-cta-tag {   /* eyebrow — mirrors the Let's Talk CTA's .cta-tag (2026-07-10) */
        font-size: 11px;
        font-weight: 700;
        letter-spacing: 0.22em;
        text-transform: uppercase;
        color: rgba(255, 255, 255, 0.75);
        margin-bottom: 14px;
      }
      .portfolio-cta-title {
        font-family: "Inter", sans-serif;
        font-weight: 900;
        font-size: 44px;          /* matches the Let's Talk CTA */
        line-height: 0.92;
        letter-spacing: -0.04em;
        text-transform: uppercase;
        color: var(--white);
      }
      .portfolio-cta-underline {
        display: block;
        width: 0;
        height: 1px;
        background: var(--white);
        margin-top: 14px;
        transition: width 0.55s ease;
      }
      @media (hover: hover) and (pointer: fine) { .project-card--cta:hover .portfolio-cta-underline { width: 50%; } }
      /* v2 rev7: dimming on non-active cards REMOVED per user — they
         want only the saturation to change. Rest stay exactly as they were.
         No more scale-down on grid hover or sibling-of-auto-active. */
      /* Vertical-climb entrance (rev17.6) — matches the three service
         cards' style: 140 px translateY + bouncy ease so cards rise
         visibly into place. Per-card stagger (6 pairs × 0.15 s, below)
         carries the sequencing. The setupProjectsReveal IO fires
         ~50 px after the title's IO so there's a natural slight delay
         between "Featured Projects" coming in and the grid climbing. */
      .project-card.reveal {
        opacity: 0;
        transform: translateY(140px);
        transition:
          opacity 1.04s cubic-bezier(0.18, 0.89, 0.32, 1.05),
          transform 1.21s cubic-bezier(0.18, 0.89, 0.32, 1.05);
      }
      /* v2 rev5: 12 cards grouped into 6 pairs that fire 0.15s apart so
         the entrance has a more visible stacking effect (was per-card
         stagger of 0.06s, total ~0.66s; now 0s → 0.75s in 6 steps). */
      .project-card.reveal:nth-child(1),
      .project-card.reveal:nth-child(2)  { transition-delay: 0s; }
      .project-card.reveal:nth-child(3),
      .project-card.reveal:nth-child(4)  { transition-delay: 0.15s; }
      .project-card.reveal:nth-child(5),
      .project-card.reveal:nth-child(6)  { transition-delay: 0.30s; }
      .project-card.reveal:nth-child(7),
      .project-card.reveal:nth-child(8)  { transition-delay: 0.45s; }
      .project-card.reveal:nth-child(9),
      .project-card.reveal:nth-child(10) { transition-delay: 0.60s; }
      .project-card.reveal:nth-child(11),
      .project-card.reveal:nth-child(12) { transition-delay: 0.75s; }
      /* Entering from BELOW (scroll-up re-entry, 2026-06-11):
         bottom rows are visible first, so they must also MOVE first —
         the same pair stagger, reversed. With the order matching the
         direction of travel, cards stay clear of each other and the
         mid-flight overlap can't happen. */
      .projects-grid.enter-up .project-card.reveal:nth-child(11),
      .projects-grid.enter-up .project-card.reveal:nth-child(12) { transition-delay: 0s; }
      .projects-grid.enter-up .project-card.reveal:nth-child(9),
      .projects-grid.enter-up .project-card.reveal:nth-child(10) { transition-delay: 0.15s; }
      .projects-grid.enter-up .project-card.reveal:nth-child(7),
      .projects-grid.enter-up .project-card.reveal:nth-child(8)  { transition-delay: 0.30s; }
      .projects-grid.enter-up .project-card.reveal:nth-child(5),
      .projects-grid.enter-up .project-card.reveal:nth-child(6)  { transition-delay: 0.45s; }
      .projects-grid.enter-up .project-card.reveal:nth-child(3),
      .projects-grid.enter-up .project-card.reveal:nth-child(4)  { transition-delay: 0.60s; }
      .projects-grid.enter-up .project-card.reveal:nth-child(1),
      .projects-grid.enter-up .project-card.reveal:nth-child(2)  { transition-delay: 0.75s; }
      /* rev20: body.scroll-up reverse-stagger CSS REMOVED per user.
         Both scroll-down exit and scroll-up entry now use the same
         forward stagger (cards 1/2 first). The natural CSS transition
         on .in toggle handles the reverse direction smoothly. */
      .project-card.reveal.in {
        opacity: 1;
        transform: translateY(0);
      }
      /* rev21 (2026-05-21) — direction-aware exit, mirroring the
         service-cards pattern. When the user scrolls DOWN past the
         grid, JS adds .exit-up to .projects-grid and cards slide UP
         out of view (matches IOE cards). When the user later scrolls
         back UP into the grid, .in is re-added and cards transition
         from translateY(-140) → 0 → coming DOWN into place from above
         (matches IOE cards re-entering on scroll-up). On the FIRST
         downward approach (before .exit-up has ever been set), cards
         sit at the default translateY(140) and rise from below — same
         as before. */
      .projects-grid.exit-up .project-card.reveal {
        opacity: 0;
        transform: translateY(-140px);
      }
      /* Reduced-motion: cards sit visible by default (no 140px climb / no exit
         slide), no hover/auto-cycle zoom — the auto-cycle JS also bails. (2026-06-23) */
      @media (prefers-reduced-motion: reduce) {
        .project-card.reveal,
        .projects-grid.exit-up .project-card.reveal { opacity: 1 !important; transform: none !important; }
        .project-card-img,
        .project-card:hover .project-card-img,
        .project-card.auto-active .project-card-img { transform: none !important; }
      }
      .project-card-img {
        position: absolute;
        inset: 0;
        /* Rev20: dropped further (0.8 → 0.65) for visibly stronger
           "desaturated rest, pop on active" contrast. Filter transition
           slowed to 0.8s so the swap is perceptible rather than instant.
           Transform on this DIV (not the img) — card has overflow:hidden
           so the scale grows the photo INSIDE the card bounds without
           pushing the card itself into neighbors. */
        filter: saturate(0.65);
        transition: transform 0.8s ease, filter 0.8s ease;
      }
      /* Auto-active runs regardless of scroll state — it's the idle
         rotation, not a hover. The hovered-card variant is gated on
         body:not(.is-scrolling) so a fast-scroll past the grid can't
         flicker hover styling on the card under the cursor. */
      .project-card.auto-active .project-card-img {
        filter: saturate(1);
        transform: scale(1.04);
      }
      @media (hover: hover) and (pointer: fine) {
      body:not(.is-scrolling) .project-card:hover .project-card-img {
        filter: saturate(1);
        transform: scale(1.04);
      }
      }
      .project-card-overlay {
        position: absolute;
        inset: 0;
        background: linear-gradient(
          to top,
          rgba(0,0,0, 1) 0%,
          transparent 55%
        );
      }
      .project-card-content {
        position: absolute;
        bottom: 0;
        left: 0;
        right: 0;
        padding: 24px 28px;
      }
      .project-cat {
        font-size: 10px;
        font-weight: 700;
        letter-spacing: 0.18em;
        text-transform: uppercase;
        color: var(--yellow);
        margin: 8px 0 0 0;
        text-shadow: 0 1px 4px rgba(0, 0, 0, 0.65);
      }
      .project-title {
        font-family: "Inter", sans-serif;
        font-size: 18px;
        font-weight: 800;
        line-height: 1.05;
        letter-spacing: -0.02em;
        text-transform: uppercase;
        color: var(--white);
        text-shadow: 0 2px 10px rgba(0, 0, 0, 0.4);
      }

      /* ─── SECTION 3.5 · REVIEWS + LOGOS ────────────────────────
         Imported from the "Combined" sandbox concept. Three Google
         review cards over a continuous client-logo marquee. Sits
         between Featured Projects (off-white) and Process (off-white)
         on a matching off-white background so the section transitions
         feel continuous. */
      #reviews {
        position: relative;
        background: var(--white);
        /* Fold normalized to one full screen with its content vertically centered
           (2026-06-25) — matches #p1-process so the two folds share a rhythm.
           Padding is now just a min safety gap; justify-content distributes the rest.
           Capped at 900px (the 1440×900 design height) so it fills a normal screen but
           stops spraying empty whitespace on tall monitors (2026-07-09). */
        min-height: min(100vh, 900px);
        display: flex;
        flex-direction: column;
        justify-content: center;
        padding: 48px 0;
      }
      .reviews-top {
        /* +25 % (56 → 70) between heading and the three review cards
           (2026-05-20). */
        margin-bottom: 72px;
      }
      /* Tiny, low-contrast prev/next arrows. Float top-right of the cards
         row so they don't compete with the heading. Manual click pauses
         the auto-cycle for 5 s; idle window past that resumes auto-rotate
         (2026-05-20). */
      /* Minimal carousel control (2026-06-13): a tiny, faint "next"
         chevron sitting just at the right edge of the rightmost card, plus 4
         super-small ghost dots (one per 3-card section) centred below. Both
         deliberately low-contrast so they stay quiet and never pull focus. */
      .reviews-carousel { position: relative; }
      .reviews-dots {
        display: flex;
        justify-content: center;
        align-items: center;
        gap: 4px;
        margin-top: 20px;
        margin-bottom: 10px;
        /* reveal WITH the section like the Google review cards (2026-06-14). */
        opacity: 0;
        transform: translateY(18px);
        transition:
          opacity 0.8s cubic-bezier(0.18, 0.89, 0.32, 1.05) 0.10s,
          transform 0.9s cubic-bezier(0.18, 0.89, 0.32, 1.05) 0.10s;
      }
      #reviews.in-view .reviews-dots {
        opacity: 1;
        transform: translateY(0);
        transition-delay: 1.0s, 1.0s;
      }
      /* Clickable slide controls (2026-06-23): a touch bigger + clearly
         interactive (pointer, hover feedback, a pill for the active set) so it
         reads as "click to switch sets" — but kept quiet / low-contrast. */
      .reviews-dot {
        appearance: none;
        -webkit-appearance: none;
        border: 0;
        background: transparent;
        padding: 9px 6px;            /* generous tap/click target around the small visual dot */
        margin: 0;
        cursor: pointer;
        display: inline-flex;
        align-items: center;
        justify-content: center;
        line-height: 0;
      }
      .reviews-dot::before {
        content: "";
        display: block;
        width: 7px;
        height: 7px;
        border-radius:0;
        background: rgba(16,24,32, 0.10);
        /* ease-OUT = the morph STARTS instantly then settles (responsive + still liquid) */
        transition: width 0.5s cubic-bezier(0.22, 1, 0.36, 1), background 0.42s cubic-bezier(0.22, 1, 0.36, 1), transform 0.3s cubic-bezier(0.22, 1, 0.36, 1);
      }
      @media (hover: hover) and (pointer: fine) { .reviews-dot:hover::before { background: rgba(16,24,32, 0.22); transform: scale(1.1); } }
      .reviews-dot:focus-visible { outline: none; }
      .reviews-dot:focus-visible::before { background: rgba(16,24,32, 0.22); box-shadow: 0 0 0 3px rgba(16,24,32,0.10); }
      .reviews-dot.is-active::before {
        width: 12px;                 /* subtle pill — kept close to the dots so the four read continuous/uniform */
        border-radius:0;
        background: rgba(16,24,32, 0.30);
        transform: none;
      }

      /* ════ HONEYCOMB CLIENTS MODAL (2026-06-23) — trigger + modal follow
         the SITE-STANDARD pop-up shell (= portfolio sector/gallery modals, 2026-07-13):
         rgba(0,0,0,0.32) scrim, white panel fade + single-axis rise, rotate-on-hover ✕,
         fixed header + scrolling body, ESC + scrim close, pin scroll-lock, nav hidden while open. ════ */
      .hc-trigger-wrap {
        display: flex; justify-content: center; margin-top: 65px;   /* centered + dropped ~35px (2026-06-25) */
        /* enters/exits WITH the logo marquee — identical opacity+translateY reveal
           and 1.21s entry delay, anchored to #reviews.in-view so the button and the
           logos appear (and leave) together. (2026-06-23) */
        opacity: 0;
        transform: translateY(36px);
        transition: opacity 0.9s ease 0s, transform 0.9s cubic-bezier(0.22, 1, 0.36, 1) 0s;
      }
      #reviews.in-view .hc-trigger-wrap { opacity: 1; transform: translateY(0); transition-delay: 1.21s, 1.21s; }
      .hc-trigger { display: inline-flex; align-items: center; gap: 9px; background: none; border: 0; padding: 0; cursor: pointer; font-family: inherit; font-size: 10px; font-weight: 700; letter-spacing: 0.16em; text-transform: uppercase; color: rgba(16,24,32,0.4); transition: color 0.25s ease; }   /* ghost grey 0.4 — matches the "See how we run a project" link (2026-06-25) */
      /* the hex ALWAYS spins — one animation, every device (2026-07-10): the old freeze-when-static
         (body.is-scrolling / :hover play-state) left it frozen on touch, where there's no hover; now it just
         runs, synced across desktop/tablet/phone. Reduced-motion still stops it (below). */
      .hc-trigger .hc-tri-hex { width: 12px; height: 14px; background: currentColor; clip-path: polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%); animation: hc-hex-spin 1.4s linear infinite; }
      @keyframes hc-hex-spin { to { transform: rotate(360deg); } }
      @media (hover: hover) and (pointer: fine) { .hc-trigger:hover { color: var(--red); } }   /* match About Us — clients trigger turns site red on hover (2026-06-25) */
      .hc-trigger:focus-visible { outline: 2px solid var(--red); outline-offset: 3px; }

      .hc-modal { position: fixed; inset: 0; z-index: 600; display: grid; place-items: center; padding: 24px; opacity: 0; pointer-events: none; transition: opacity 0.22s ease; }   /* quick, clean fade — site-standard pop-up timing, trimmed 0.4→0.22s (2026-07-15) */
      .hc-modal.open { opacity: 1; pointer-events: auto; }
      body.hc-open #main-nav, body.hc-open #nav-glass { opacity: 0; pointer-events: none; }
      .hc-scrim { position: absolute; inset: 0; background: rgba(0,0,0, 0.32); }   /* subtle dim, fades in with the panel — the SITE-STANDARD pop-up scrim (= portfolio sector galleries), no black wall (2026-07-13) */
      /* panel = the SITE-STANDARD pop-up shell (portfolio sector/gallery modals, 2026-07-13):
         clean fade + single-axis RISE (no clip-wipe), fixed header, scrolling body, bottom breathing
         on the panel so the scrollbar ends at the last row, not the panel edge */
      .hc-panel { position: relative; z-index: 2; width: min(1120px, 94vw); max-height: 88vh; display: flex; flex-direction: column; overflow: hidden; background: #fff; color: var(--black); padding: 0 0 clamp(40px, 4.4vw, 54px); transform: translateY(16px); transition: transform 0.3s cubic-bezier(0.22, 1, 0.36, 1); }   /* trimmed 0.55→0.3s for a quicker, simpler pop-up — site-standard (2026-07-15) */
      .hc-modal.open .hc-panel { transform: none; }
      .hc-close { position: absolute; top: 14px; right: 14px; width: 40px; height: 40px; border: 0; background: transparent; cursor: pointer; font-size: 18px; line-height: 1; color: var(--black); z-index: 4; transition: background 0.2s, color 0.2s, transform 0.3s cubic-bezier(0.22,1,0.36,1); }
      @media (hover: hover) and (pointer: fine) { .hc-close:hover { transform: rotate(90deg); } }
      /* FIXED header (kicker + title); the sub + logo grid scroll in .hc-scroll beneath it (unified pop-up behavior) */
      .hc-view { display: flex; flex-direction: column; min-height: 0; flex: 1 1 auto; overflow: hidden; animation: hcFade 0.28s ease; }   /* quicker inner fade — trimmed 0.38→0.28s to match the quick pop-up shell (2026-07-15) */
      .hc-scroll { flex: 1 1 auto; min-height: 0; overflow-y: auto; -webkit-overflow-scrolling: touch; padding: 2px 48px 8px; }
      .hc-scroll::-webkit-scrollbar { width: 7px; }
      .hc-scroll::-webkit-scrollbar-track { background: transparent; }
      .hc-scroll::-webkit-scrollbar-thumb { background: rgba(16,24,32,0.15); border-radius:0; border-right: 4px solid transparent; background-clip: padding-box; }
      @keyframes hcFade { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }
      .hc-head { flex: 0 0 auto; margin: 0; padding: 44px 48px 20px; }   /* LEFT-justified, matching the sectors modal */
      .hc-kicker { display: block; font-size: 11px; font-weight: 700; letter-spacing: 0.18em; text-transform: uppercase; color: var(--red); }
      .hc-title { margin: 6px 0 0; font-weight: 900; font-size: clamp(24px, 3vw, 40px); letter-spacing: -0.03em; text-transform: uppercase; line-height: 0.95; }
      .hc-sub { margin: 0 0 22px; font-size: 14px; color: #666; max-width: 560px; line-height: 1.5; }

      /* CLEAN UNIFORM GRID — one cell per logo (no placeholders), NO fill, NO
         stroke: every logo just sits on white. Logos normalised to a consistent
         height (max 40px) so none read too large or too small. (2026-06-23) */
      .hc-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 7.5px 20px; }   /* +25% inter-logo spacing (gutter unchanged) */
      @media (max-width: 820px) { .hc-grid { grid-template-columns: repeat(3, 1fr); } }
      @media (max-width: 520px) { .hc-grid { grid-template-columns: repeat(2, 1fr); } }
      .hc-cell { display: grid; place-items: center; min-height: 125px; padding: 14px 18px; }   /* +25% cell height = more vertical breathing */
      .hc-cell img { max-height: 40px; max-width: 88%; width: auto; height: auto; object-fit: contain; filter: grayscale(1) opacity(0.55); transition: filter 0.3s ease, transform 0.3s cubic-bezier(0.22,1,0.36,1); }
      @media (hover: hover) and (pointer: fine) { .hc-cell:hover img { filter: grayscale(0) opacity(1); transform: scale(1.05); } }
      @media (prefers-reduced-motion: reduce) { .hc-panel, .hc-cell img, .hc-tri-hex { transition: none !important; } .hc-view, .hc-tri-hex { animation: none !important; } }
      /* (2026-05-21 rev21.3) — "Reviews" eyebrow now rides
         in/out with the heading instead of sitting statically on
         the page. Same opacity + translateY pattern as the heading,
         and shares the same .in-view trigger so they enter together
         and reverse-stagger out together when scrolling up. Default
         (exit) state holds the same 0.60 s delay as the heading so
         they leave in lockstep on the way out. */
      .reviews-top .section-tag {
        position: relative;
        margin-bottom: 16px;   /* standardized to the base eyebrow gap (was 12px + a -20px nudge) — 2026-07-09 */
        opacity: 0;
        transform: translateY(48px);

        transition:
          opacity 0.85s cubic-bezier(0.16, 0.84, 0.34, 1) 0s,
          transform 0.95s cubic-bezier(0.16, 0.84, 0.34, 1) 0s;
      }
      #reviews.in-view .reviews-top .section-tag {
        opacity: 1;
        transform: translateY(0);
        /* (2026-05-29) — entry delay 0s → 0.35s so it holds off the 86%
           bottom edge and fades in in view, in lockstep with the heading (both
           0.35s). Was doing its opacity shift at the bottom, unseen. */
        transition-delay: 0.35s, 0.35s;
      }
      /* Carousel frame — clips the cards row so cards can slide off-
         screen during the right→left transition (2026-05-20 rev7). */
      .reviews-cards-frame {
        overflow: hidden;
        margin-top: 10px;                    /* 10px above the review cards (2026-06-14) */
      }
      .reviews-cards {
        display: grid;
        grid-template-columns: repeat(3, 1fr);
        gap: 18px;
        align-items: start;   /* so one card can expand on hover without stretching its neighbours */
        /* Default transform = 0; JS animates translateX off-screen and
           re-renders content during the swap. */
        transform: translateX(0);
      }
      .reviews-cards .rc {
        /* Subtle white-on-white card (2026-05-20 rev): 1% hairline
           border + 1% drop-shadow. Previous 5% shadow was reading too
           strong against the white page — reduced so the cards are
           barely there but still lift a millimetre off the page. */
        border: 1px solid rgba(16,24,32, 0.01);
        box-shadow: 0 6px 18px rgba(0, 0, 0, 0.01);
        border-radius: 0;            /* square corners — Enduro UI is right-angle (2026-06-24) */
        padding: 26px 24px;
        display: flex;
        flex-direction: column;
        gap: 14px;
        background: var(--white);
      }
      /* Hover-expand = an OVERLAY (Netflix-style hover card; researched 2026-06-24). The open card
         grows downward OVER the marquee on its own stacking context — z-index is inert without
         position, which is why the old `z-index:5` alone never layered. The cards row is height-
         locked in JS so the open card OVERFLOWS its track instead of stretching it, and the whole
         frame is lifted above the marquee while open (the cards row makes its own stacking context
         via transform, so the frame — not just the card — must out-rank the marquee). Net: layout
         never changes, so expand/collapse can't jump the page in either scroll direction. */
      .reviews-cards .rc.rc-open { position: relative; z-index: 5; box-shadow: 0 18px 44px rgba(16,24,32, 0.085); }
      .reviews-cards .rc.rc-open .rc-body { -webkit-line-clamp: unset; }
      .reviews-cards-frame:has(.rc-open) { overflow: visible; position: relative; z-index: 5; }

      /* ─── Reviews title — standard reveal (2026-05-20 rev8.2) ─
         Was a clip-wipe from above (translateY -110 % inside overflow:
         hidden). Replaced with the same opacity + translateY reveal
         used by "In Our Element" and "Featured Projects" so the heading
         enters consistently with the rest of the site. */
      .reviews-heading-clip {
        display: block;
        overflow: visible;
      }
      .reviews-heading {
        font-family: "Inter", sans-serif;
        font-size: clamp(44px, 5vw, 72px);
        font-weight: 900;
        line-height: 0.94;
        letter-spacing: -0.048em;
        text-transform: uppercase;
        color: var(--black);
        opacity: 0;
        transform: translateY(48px);

        transition:
          opacity 0.85s cubic-bezier(0.16, 0.84, 0.34, 1) 0s,
          transform 0.95s cubic-bezier(0.16, 0.84, 0.34, 1) 0s;
      }
      #reviews.in-view .reviews-heading {
        opacity: 1;
        transform: translateY(0);
        /* (2026-05-29) — entry delay 0s → 0.35s (0.6s felt like too long
           a wait). Holds the heading off the 86% bottom edge so it animates in
           view, in lockstep with the eyebrow (both 0.35s); both still lead the
           cards (0.82s). Tunable — raise if it still fires too low, lower if the
           wait feels long; the alternative is shifting the .in-view trigger. */
        transition-delay: 0.35s, 0.35s;
      }

      /* ─── Reviews card stack-stagger + logo marquee timing ──
         Entry stagger (when .in-view is added): heading → cards
         1/2/3 (1.02 / 1.10 / 1.18 s) → marquee (1.515 s).
         Exit stagger (when .in-view is removed, i.e. scroll-up out
         of the section, 2026-05-21 rev21): REVERSED. Marquee
         exits FIRST (0 s), then card 3, 2, 1 (0.15 / 0.30 / 0.45 s),
         then heading last (0.60 s). Subtle and gentle, mirrors the
         entry in reverse so the section "unbuilds" the way it built.
         The .in-view rule below pins the entry delays; the default
         (initial) state holds the exit delays — so each direction
         picks up the appropriate timing. */
      .reviews-cards .rc {
        opacity: 0;
        transform: translateY(80px);
        /* EXIT delay (default state): card 1 last out — 0.45 s */
        transition:
          opacity 0.85s cubic-bezier(0.18, 0.89, 0.32, 1.05) 0.45s,
          transform 0.95s cubic-bezier(0.18, 0.89, 0.32, 1.05) 0.45s;
      }
      .reviews-cards .rc:nth-child(2) {
        transform: translateY(100px);
        /* EXIT delay: card 2 middle — 0.30 s */
        transition-delay: 0.30s, 0.30s;
      }
      .reviews-cards .rc:nth-child(3) {
        transform: translateY(120px);
        /* EXIT delay: card 3 first out — 0.15 s */
        transition-delay: 0.15s, 0.15s;
      }
      /* ENTRY delays (apply when .in-view is added). Reverses the exit.
         (2026-05-21 rev21.2) — cards 20 % sooner relative to the
         heading: 1.02 → 0.82, 1.10 → 0.88, 1.18 → 0.94. */
      #reviews.in-view .reviews-cards .rc {
        opacity: 1;
        transform: translateY(0);
        transition-delay: 0.82s, 0.82s;
      }
      #reviews.in-view .reviews-cards .rc:nth-child(2) {
        transition-delay: 0.88s, 0.88s;
      }
      #reviews.in-view .reviews-cards .rc:nth-child(3) {
        transition-delay: 0.94s, 0.94s;
      }
      .reviews-marquee-frame {
        opacity: 0;
        transform: translateY(36px);
        /* EXIT delay: marquee FIRST out — 0 s. */
        transition:
          opacity 0.9s ease 0s,
          transform 0.9s cubic-bezier(0.22, 1, 0.36, 1) 0s;
      }
      #reviews.in-view .reviews-marquee-frame {
        opacity: 1;
        transform: translateY(0);
        /* ENTRY delay: marquee LAST in. (2026-05-21 rev21.2) —
           1.515 → 1.21 s (additional 20 % sooner relative to heading). */
        transition-delay: 1.21s, 1.21s;
      }
      /* Card-swap fade (2026-05-20 rev8): all three .rc elements
         fade together when the rotation fires. The logo marquee already
         carries the left-scroll motion in the section — the cards
         dropping to opacity 0 and rising to 1 again is the cleaner
         readable transition for the cards themselves. */
      .reviews-cards .rc.rc-fading .rc-head,
      .reviews-cards .rc.rc-fading .rc-stars,
      .reviews-cards .rc.rc-fading .rc-body {
        opacity: 0;
        transition: opacity 0.5s cubic-bezier(0.65, 0, 0.35, 1);   /* slower, eased-both-ways crossfade (2026-06-23) */
      }
      .reviews-cards .rc .rc-head,
      .reviews-cards .rc .rc-stars,
      .reviews-cards .rc .rc-body {
        transition: opacity 0.55s cubic-bezier(0.65, 0, 0.35, 1);
      }
      .reviews-cards .rc-head {
        display: flex;
        align-items: center;
        gap: 12px;
      }
      .reviews-cards .rc-avatar {
        width: 36px; height: 36px;
        border-radius:0;
        background: var(--off-white);
        display: flex; align-items: center; justify-content: center;
        font-weight: 800; font-size: 14px;
        color: rgba(16,24,32, 0.55);
      }
      .reviews-cards .rc-meta { display: flex; flex-direction: column; line-height: 1.2; }
      .reviews-cards .rc-name { font-size: 13px; font-weight: 700; letter-spacing: 0.01em; }
      .reviews-cards .rc-date { font-size: 11px; color: rgba(16,24,32, 0.55); margin-top: 2px; }
      .reviews-cards .rc-stars { color: #f5b704; font-size: 14px; }
      .reviews-cards .rc-body {
        font-size: 14px;
        line-height: 1.55;
        color: rgba(16,24,32, 0.85);
        margin: 0;
        /* LOCK to 4 lines so the cards stay the SAME height as the 3 swap — no jump
           (2026-06-14). Reviews are trimmed to ≤4 lines; the clamp is a safety. */
        display: -webkit-box;
        -webkit-box-orient: vertical;
        -webkit-line-clamp: 4;
        overflow: hidden;
        min-height: calc(1.55em * 4);
      }

      /* Marquee — gutter-bound (2026-05-19). Sits inside .container
         so it shares the same left/right gutters as the cards above. Logos
         enter from the right gutter edge and exit at the left gutter edge.
         A short soft-fade at each end keeps the in/out feel smooth without
         eating into the visible run. Logo containers are locked to a uniform
         140 × 36 box so every brand reads at the same visual weight.
         Flat-black silhouette filter; hover restores full colour. */
      .reviews-marquee-frame {
        /* +25 % (64 → 80) between bottom of review cards and the logo
           marquee (2026-05-20). */
        margin-top: 80px;
        overflow: hidden;
        mask-image: linear-gradient(90deg, transparent 0%, #000 3%, #000 97%, transparent 100%);
        -webkit-mask-image: linear-gradient(90deg, transparent 0%, #000 3%, #000 97%, transparent 100%);
        padding: 18px 0;
      }
      .reviews-marquee-track {
        display: flex;
        width: max-content;
        animation: reviews-marquee 120s linear infinite;   /* 60s → 120s: logo count doubled 8→16, so double the duration to keep the same scroll SPEED (2026-06-23) */
      }
      /* SEAMLESS LOOP FIX (2026-06-23): the inter-logo spacing was a
         flex `gap: 140px`, but `gap` is only applied BETWEEN items — so the
         seam between the two duplicated sets was a half-gap short of the
         internal rhythm, and the -50% loop snapped back ~70px on reset (the
         "jump"). Spacing now lives on each .reviews-logo-box as margin-right,
         so every copy is an identical tiling unit and -50% lands perfectly.
         Hover-to-pause REMOVED — the marquee never stops; only the per-logo
         intensify-on-hover (below) remains. */
      .reviews-logo-box {
        /* Box dimensions: 130 × 48. (2026-05-20 rev8.2)
           overflow: hidden removed — was clipping scaled logos at the
           bottom (City of North Vancouver "VANCOUVER" line was getting
           cut). The WPM mask trick that required it is gone. */
        width: 130px; height: 48px;
        flex-shrink: 0;
        display: flex; align-items: center; justify-content: center;
        overflow: visible;
        margin-right: 140px;   /* inter-logo spacing — was the track's `gap`; on the box so the two copies tile exactly for a seamless loop */
      }
      @media (max-width: 960px) { .reviews-logo-box { margin-right: 117px; } }   /* mobile/tablet: logos pulled tighter — 140 → 117 (−10px then −10% again) so they don't read stretched at these widths (2026-07-10) */
      .reviews-logo-box img {
        width: 100%; height: 100%;
        object-fit: contain;
        /* Uniform color treatment — grayscale + opacity gives a muted-
           silhouette read; mix-blend-mode: multiply hides any white PNG
           background. Per-logo opacity overrides below even out logos
           that come in too dark (Saputo, Cutler) or too thin (Maison
           Margiela's serif). */
        filter: grayscale(1) opacity(0.55);
        mix-blend-mode: multiply;
        transition: filter 0.25s ease, transform 0.25s ease;
      }
      @media (hover: hover) and (pointer: fine) {
      .reviews-logo-box:hover img {
        filter: grayscale(0) opacity(1);
      }
      }

      /* ─── Per-logo tuning (2026-05-20 rev6) ──────────────
         Reference: Strong Pilates (scale 1.0, opacity baseline 0.55).
         All other text logos targeted to match its optical height +
         font weight. Tune by eye. */

      /* Strong Pilates — reference. No overrides needed. */

      /* Cutler — was rendering too tall and too bold. Reduced. */
      .reviews-logo-box img[src*="Cutler" i] {
        transform: scale(0.77);   /* -10% from 0.85 (2026-06-23) */
        filter: grayscale(1) opacity(0.45);
      }

      /* Motif — bold + tall, lighten + shrink. */
      .reviews-logo-box img[src*="Motif" i] {
        transform: scale(0.82);   /* -3% from 0.85 (2026-06-23) */
        filter: grayscale(1) opacity(0.50);
      }

      /* Saputo — bold + tall, same treatment as Motif/Cutler. */
      .reviews-logo-box img[src*="Saputo" i] {
        transform: scale(0.88);   /* +3% from 0.85 (2026-06-23) */
        filter: grayscale(1) opacity(0.50);
      }

      /* Maison Margiela — thin serif wordmark. Bump scale a touch + a
         touch more opacity so the thin glyphs read at the same density
         as Strong Pilates. */
      .reviews-logo-box img[src*="Maison_Margiela" i] {
        transform: scale(1.21);   /* +5% from 1.15 (2026-06-23) */
        filter: grayscale(1) opacity(0.70);
      }

      /* City of Delta — was bottom-justified due to top whitespace in
         the source PNG. Lift up more aggressively via translateY -30 %
         (was -12 %) to visually center the wordmark in the box.
         (2026-05-20 rev8.2) */
      .reviews-logo-box img[src*="City_of_Delta" i] {
        transform: scale(1.06) translateY(-30%);   /* -10% then -8% from 1.28 (2026-06-23) */
        filter: grayscale(1) opacity(0.55);
      }

      /* City of North Vancouver — scale dropped 1.14 → 1.00. The
         scaled-up image was extending past the box's bottom edge so
         "VANCOUVER" got clipped. Natural-size + object-fit: contain
         keeps the whole wordmark inside. */
      .reviews-logo-box img[src*="City_of_North_Vancouver" i] {
        transform: scale(1.00);
      }

      /* Warrington PCI Management — hidden until a clean WPM-only PNG
         is available. The CSS mask + scale approach was rendering a
         blank logo box in some cases. Display: none collapses the box
         entirely so the marquee tracks contiguously without a gap
         (2026-05-20 rev7). */
      /* Warrington PCI Management — un-hidden + scaled +10% (2026-06-23).
         (Was display:none under an old CSS-mask approach that rendered blank;
         that mask is gone, so it shows normally now via object-fit:contain.) */
      .reviews-logo-box img[src*="Warrington" i] { transform: scale(1.16); }   /* +5% from 1.1 (2026-06-23) */
      /* New clients 2026-06-23 — starting sizes by classification (wide/thin marks boosted, square ones ~1.0); fine-tune by eye, they respond now. */
      .reviews-logo-box img[src*="logo_ubc" i] { transform: scale(1.15); }
      .reviews-logo-box img[src*="logo_bc_liquor" i] { transform: scale(1.25); }
      .reviews-logo-box img[src*="logo_ufv" i] { transform: scale(1.0); }
      .reviews-logo-box img[src*="logo_city_of_surrey" i] { transform: scale(1.0); }
      .reviews-logo-box img[src*="logo_vancouver_coastal_health" i] { transform: scale(1.0); }
      .reviews-logo-box img[src*="logo_city_of_burnaby" i] { transform: scale(0.95); }
      .reviews-logo-box img[src*="logo_abdera" i] { transform: scale(1.05); }
      .reviews-logo-box img[src*="logo_physioeffect" i] { transform: scale(1.2); }
      @keyframes reviews-marquee {
        from { transform: translateX(0); }
        to   { transform: translateX(-50%); }
      }
      @media (max-width: 980px) {
        .reviews-cards { grid-template-columns: 1fr; }
      }



      /* ─── SECTION 4b · ONE LINE, HELD — red band (lab c11, 2026-06-11) ──
         Spliced after the Build Ledger on 's spec: a static red
         band that rides welded under the ledger, de-couples at the
         dock and wipes over the pinned fold 1:1 with scroll. The line
         fires at the ~52% gate while the band is moving, white on red,
         dark emphasis beat. This fold is the white→dark bridge into Team. */
      #c11{
        position:relative;
        /* transparent (2026-06-11): during the 50vh station the
           welded ledger shows above the red edge; the red itself is
           the flood inside the sticky stage. */
        background:transparent;
        color:var(--black,#191A1C);
      }
      #c11 *{box-sizing:border-box;}

      /* Inert wrapper (2026-06-12 sweep): the sticky-scrub track is
         retired — zero slack, the band rides 1:1 with scroll. Kept
         as a positioned ancestor for the flood. */
      #c11 .c11-track{
        position:relative;
        height:auto;
      }

      #c11 .c11-stage{
        /* BANNER (2026-06-11): a content-sized red band — an
           extension of the 01-04 fold, not a full screen. STICKY
           RETIRED (2026-06-12 sweep): with zero track slack the old
           50vh station could never engage; the band simply rides
           with scroll and de-couples from the fold at the dock.
           position:relative keeps the flood's containing block. */
        position:relative;
        /* content-sized band (2026-06-11): red above + below the
           text, not a fixed vh. Band grown +20px total — 198/198 (
           2026-06-12). Cover height tracks stage.offsetHeight, stays aligned. */
        height:auto;
        display:flex;
        align-items:center;
        padding-top:198px;
        padding-bottom:198px;
        width:100%;
        overflow:visible;
      }
      #c11 .c11-container{
        width:100%;
        max-width:var(--max,1440px);
        margin:-75px auto 0;
        padding:0 var(--gutter,80px);
        position:relative;
        z-index:2;
      }
      #c11 .c11-flood{
        position:absolute;
        inset:0;
        background:var(--red,#E43338);
        /* STATIC (2026-06-11): no trigger, no surge — the fold
           is simply red. (Colour-gradient idea reverted 2026-06-12 —
           the dark instead SPEED-RAMPS up over the red, see .team-cover.) */
        z-index:0;
        pointer-events:none;
      }

      #c11 .c11-inner{
        width:100%;
        max-width:1080px;
      }
      #c11 .c11-eyebrow{
        /* house tag→heading spacing (2026-06-11) */
        margin:0 0 16px 0;
        font-size:11px;
        font-weight:600;
        line-height:1;
        letter-spacing:0.22em;
        text-transform:uppercase;
      }

      #c11 .c11-line{
        margin:0;
        font-weight:900;
        text-transform:uppercase;
        /* match every other section title (2026-06-12): the
           canonical .section-heading clamp, down from 44/5.5vw/80.
           Line + letter spacing tightened 5% for the 3-line stack. */
        font-size:clamp(44px,5vw,72px);
        line-height:0.91;
        letter-spacing:-0.0315em;
        text-wrap:normal;   /* NORMAL — not balance/pretty. The c11Text engine (home.js) measures .c11-container every scroll frame to drive the red-banner rise + the team photo; ANY text-wrap reflow here produced a flashing white seam on the banner AND made the team photo cut short. Reverted to the original wrapping. Overrides the global p{pretty}. (2026-07-10) */
      }

      /* Each word holds its FINAL position from load (inline-block), so only the ink
         inside animates — no reflow as words reveal. Outer span clips the rise. */
      #c11 .c11-w{
        display:inline-block;
        overflow:hidden;
        vertical-align:top;
        /* NO extra word gap (2026-06-12): the inline-block words are
           already separated by the natural whitespace in the markup (= one
           real space). A margin-right STACKED on top of that, doubling the
           gap vs every other fold title. Dropped to 0 so the spacing equals
           a single normal space and matches the other headings exactly. */
        margin-right:0;
        padding:0.04em 0;
      }

      #c11 .c11-ink{
        display:inline-block;
        position:relative;
        /* entrance styling lives in the live-skin .is-on block below
           (2026-06-12 sweep — the .is-shown scrub styles that sat
           here were dead: nothing toggles .is-shown anymore). */
      }

      /* Optional red underscore: punctuation, draws in once WRONG lands. */
      #c11 .c11-rule{
        position:absolute;
        left:0;
        bottom:-0.12em;
        width:100%;
        height:0.1em;
        overflow:visible;
      }
      #c11 .c11-rule-line{
        stroke-width:6;
        stroke-linecap:round;
        vector-effect:non-scaling-stroke;
        /* draw state + transition live in the .is-on block below;
           the dead .is-shown release and its reduced-motion mirror
           were removed in the 2026-06-12 sweep. */
      }

      /* ---- Mobile: un-pin, stack clean, larger type rhythm ---- */
      @media (max-width:960px){
        #c11 .c11-track{ height:auto; }
        #c11 .c11-stage{
          position:static;
          height:auto;
          min-height:72vh;
          padding:96px var(--gutter,80px);
        }
        #c11 .c11-inner{ max-width:none; }
        #c11 .c11-line{ font-size:clamp(38px,9vw,64px); }
      }
      @media (max-width:560px){
        #c11 .c11-stage{ padding:80px 24px; }
      }


      /* red-world inversion (2026-06-11) */
      #c11 .c11-eyebrow{ color:var(--white,#ffffff); }
      #c11 .c11-line{ color:var(--white,#ffffff); }
      #c11 .c11-ink{ color:var(--white,#ffffff); }
      #c11 .c11-red{ color:var(--black,#191A1C); font-style:italic; }
      #c11 .c11-rule-line{ stroke:var(--black,#191A1C); }
      /* Red fold: STATIC background (no surge/trigger). The held
         line keeps its original word-by-word scrub effect (
         2026-06-11) — white words on red, black emphasis beat. */
      #c11{ z-index:1; }
      /* Text entrance (2026-06-11): begins when the fold's top
         touches the bottom third of the screen (#c11.is-on, single
         gate per the fold law — open from below too, closes only
         fully above). Cascade: eyebrow, then the line. */
      #c11 .c11-eyebrow,
      #c11 .c11-line {
        opacity: 0;
        transform: translateY(36px);
        transition:
          opacity 0.85s cubic-bezier(0.16, 0.84, 0.34, 1),
          transform 0.95s cubic-bezier(0.16, 0.84, 0.34, 1);
        will-change: transform, opacity;
      }
      #c11.is-on .c11-eyebrow,
      #c11.is-on .c11-line {
        opacity: 1;
        transform: translateY(0);
      }
      #c11.is-on .c11-line { transition-delay: 0.14s; }
      /* SCROLL-UP REVERSE = the forward reveal played backward (2026-06-14): the
         line + eyebrow fade out LAST (after the words cascade out), mirroring eyebrow-in-
         first. Was: everything snapped out together. */
      #c11:not(.is-on) .c11-eyebrow { transition-delay: 0.30s; }
      #c11:not(.is-on) .c11-line    { transition-delay: 0.30s; }
      /* word cascade fires AT the station (2026-06-11) — no
         scroll scrub; each word follows the last, underline beat last. */
      #c11 .c11-w .c11-ink{
        display:inline-block;
        opacity:0;
        transform:translateY(0.5em);
        /* the lab base hides words with a clip mask (L~2009) — it must
           be declared AND released here, or the words stay invisible
           forever (the un-clip used to live in the removed scrub's
           .is-shown class). Found via cascade audit 2026-06-11. */
        clip-path:inset(0 0 100% 0);
        transition:
          opacity 0.45s cubic-bezier(0.16,0.84,0.34,1),
          transform 0.5s cubic-bezier(0.16,0.84,0.34,1),
          clip-path 0.5s cubic-bezier(0.16,0.84,0.34,1);
      }
      #c11.is-on .c11-w .c11-ink{
        opacity:1;
        transform:none;
        clip-path:inset(0 0 0 0);
      }
      /* delays keyed to data-i (not nth-child) so the forced line-break
         <br>s for "over." on its own line don't shift the cascade. */
      /* cascade tightened (2026-06-12) — total ~0.34s so a fast
         scroll barely outruns it; was 0.20→0.68. */
      #c11.is-on .c11-w[data-i="0"] .c11-ink{ transition-delay:0.06s; }
      #c11.is-on .c11-w[data-i="1"] .c11-ink{ transition-delay:0.10s; }
      #c11.is-on .c11-w[data-i="2"] .c11-ink{ transition-delay:0.13s; }
      #c11.is-on .c11-w[data-i="3"] .c11-ink{ transition-delay:0.17s; }
      #c11.is-on .c11-w[data-i="4"] .c11-ink{ transition-delay:0.20s; }
      #c11.is-on .c11-w[data-i="5"] .c11-ink{ transition-delay:0.24s; }
      #c11.is-on .c11-w[data-i="6"] .c11-ink{ transition-delay:0.27s; }
      #c11.is-on .c11-w[data-i="7"] .c11-ink{ transition-delay:0.31s; }
      #c11.is-on .c11-w[data-i="8"] .c11-ink{ transition-delay:0.34s; }
      /* words exit RIGHT→LEFT on scroll-up — the entrance cascade reversed (2026-06-14) */
      #c11:not(.is-on) .c11-w[data-i="8"] .c11-ink{ transition-delay:0s; }
      #c11:not(.is-on) .c11-w[data-i="7"] .c11-ink{ transition-delay:0.035s; }
      #c11:not(.is-on) .c11-w[data-i="6"] .c11-ink{ transition-delay:0.07s; }
      #c11:not(.is-on) .c11-w[data-i="5"] .c11-ink{ transition-delay:0.105s; }
      #c11:not(.is-on) .c11-w[data-i="4"] .c11-ink{ transition-delay:0.14s; }
      #c11:not(.is-on) .c11-w[data-i="3"] .c11-ink{ transition-delay:0.175s; }
      #c11:not(.is-on) .c11-w[data-i="2"] .c11-ink{ transition-delay:0.21s; }
      #c11:not(.is-on) .c11-w[data-i="1"] .c11-ink{ transition-delay:0.245s; }
      #c11:not(.is-on) .c11-w[data-i="0"] .c11-ink{ transition-delay:0.28s; }
      #c11 .c11-rule-line{
        stroke-dasharray:200;
        stroke-dashoffset:200;
        transition:stroke-dashoffset 0.55s cubic-bezier(0.16,0.84,0.34,1) 0s;
      }
      #c11.is-on .c11-w--last .c11-rule-line{
        stroke-dashoffset:0;
        transition-delay:0.85s;
      }
      @media (prefers-reduced-motion: reduce) {
        #c11 .c11-eyebrow, #c11 .c11-line {
          transition: none; opacity: 1; transform: none;
        }
        #c11 .c11-w .c11-ink{ transition:none; opacity:1; transform:none; }
        #c11 .c11-rule-line{ transition:none; stroke-dashoffset:0; }
      }

      /* ─── SECTION 5 · TEAM — Dark Canvas (Concept 08) ─────────
         Replaces the old white team section with a full-bleed dark band
         (2026-05-20). Background = var(--black) so it matches both
         the Process section's final stage AND the footer — the page now
         transitions Process → Team → Footer as one continuous dark block
         with no off-white seam in between. Big white headline on black,
         supporting paragraph, then a single wide site photo. */
      #team {
        position: relative;
        /* must stack ABOVE the docked sticky ledger (z 0) and the red
           fold (z 1) — everything after the dock needs explicit
           positioning or the pinned fold paints through it. */
        z-index: 2;
        background: transparent;
        color: var(--white);
        /* Top padding trimmed ~130px from the section rhythm token so "Meet the
           Team" sits higher (2026-06-12). padding-bottom is 0 — the team
           fold is in-flow and self-sizes; the old reverse-hero runway/sticky
           approach is gone. */
        padding: calc(var(--section-pad-top) - 100px) 0 0;
      }


      /* ═══════════════════════════════════════════════════════════
         RESPONSIVE OVERHAUL (rev20)
         Master was designed desktop-first (1280–1920). These rules
         add tablet + mobile behavior.
         Breakpoints:
           ≤ 1180  · laptop trim (4-col FP, tighter gutters)
           ≤ 960   · tablet      (nav hamburger, IOE stack 2-col, FP 2-col,
                                  footer 2×2, hero padding scale)
           ≤ 640   · mobile      (full stack, IOE 1-col, FP 1-col,
                                  footer stacked, hero compact)
         ═══════════════════════════════════════════════════════════ */

      /* ─── Touch-device removals ───────────────────────────────── */
      @media (hover: none) {
        /* IOE magnetic-expand + card zoom are already gated to (hover:hover) at their source (≈L540/L561), so they never fire on touch — no touch reset needed. The old reset here forced `.services-grid:has(.service-card:hover){grid-template-columns:1fr 1fr 1fr !important}`, which (via :has() specificity) OVERRODE the ≤960 1-col stack — so tapping/scrolling a card un-stacked the row to 3 columns on phones. Removed (2026-07-10). */
        /* Don't shrink FP photos with no hover ability. */
        .project-card:hover .project-card-img { transform: none; }
      }

      /* ─── ≤ 1180  laptop trim ─────────────────────────────────── */
      @media (max-width: 1180px) {
        :root { --gutter: 48px; }
        .projects-grid {
          grid-template-columns: repeat(4, 1fr);
          grid-template-rows: repeat(6, 1fr);
          height: 1200px;
        }
        .project-card:nth-child(1)  { grid-column: 1 / span 2; grid-row: 1 / span 2; }
        .project-card:nth-child(2)  { grid-column: 3 / span 1; grid-row: 1 / span 1; }
        .project-card:nth-child(3)  { grid-column: 4 / span 1; grid-row: 1 / span 1; }
        .project-card:nth-child(4)  { grid-column: 3 / span 2; grid-row: 2 / span 1; } /* CTA */
        .project-card:nth-child(5)  { grid-column: 1 / span 1; grid-row: 3 / span 1; }
        .project-card:nth-child(6)  { grid-column: 2 / span 1; grid-row: 3 / span 1; }
        .project-card:nth-child(7)  { grid-column: 3 / span 1; grid-row: 3 / span 1; }
        .project-card:nth-child(8)  { grid-column: 4 / span 1; grid-row: 3 / span 1; }
        .project-card:nth-child(9)  { grid-column: 1 / span 2; grid-row: 4 / span 2; }
        .project-card:nth-child(10) { grid-column: 3 / span 2; grid-row: 4 / span 2; }
        .project-card:nth-child(11) { grid-column: 1 / span 2; grid-row: 6 / span 1; }
        .project-card:nth-child(12) { grid-column: 3 / span 2; grid-row: 6 / span 1; }
      }

      /* ─── ≤ 960  tablet ───────────────────────────────────────── */
      @media (max-width: 960px) {
        :root { --gutter: 32px; }

        /* Hero content sits in the lower third — same feel as desktop. ONE offset (the inner's bottom
           padding), svh-based so it's consistent across every phone/tablet height (desktop keeps its fixed
           142px). Replaces a doubled padding-top(18vh)+padding-bottom(14vh) that stacked on the inner's 142px
           and sat the whole stack too high. (2026-07-10) */
        .hero-content .hero-inner { padding-bottom: calc(16svh - 75px); }   /* dropped 75px lower on mobile/tablet (2026-07-10) */
        /* NATIVE MOBILE HERO (2026-07-10) — the fixed-overlay JS scrub is bypassed on ≤960 (home.js
           `mobileHero`). The photo + content stop being position:fixed and become in-flow ABSOLUTE layers that
           fill #hero and simply scroll away WITH the page. Result: no per-frame transform on a fixed layer (the
           jitter root cause is gone) and the hero tracks the finger 1:1 on the compositor (the ~2× parallax that
           made a slow drag feel "too fast" is gone). Desktop (>960) keeps the fixed-overlay scrub untouched.
           #hero is a full-screen 100svh block (was 90svh — a fixed-photo-era "clear sooner" tuning that has no
           meaning once the hero scrolls natively). */
        #hero { height: 100vh; height: 100svh; }
        .hero-photo-frame { position: absolute; top: 0; left: 0; width: 100%; height: 100%; will-change: auto; }
        /* Content stays FIXED (pinned) so it can be CLIPPED from the bottom to the native photo's rising edge
           (the "wipe" — the H1 never translates, only gets clipped) + its subtext/buttons FADE — both driven in
           home.js's mobileHero branch, reusing the laptop applyHeroPhotoState formulas. The PHOTO above is native
           (absolute, scrolls with the page) so the jitter/glitch stays gone. (2026-07-10) */
        .hero-content { position: fixed; top: 0; left: 0; width: 100%; height: 100svh; will-change: clip-path; }
        /* IOE flows normally right below the native hero — drop the curtain's negative pull-up margin (it only
           made sense while the hero was fixed and #services slid up underneath it; the JS no longer transforms
           .services-top/.services-grid on mobile, so there's nothing to counter). */
        #services { margin-top: 30px; position: relative; z-index: 5; }   /* breathing room above "What We Do" (~46px gap incl. the 16px section pad). z-index:5 keeps IOE ABOVE the now-fixed hero content (z-index:3) as it scrolls up, so a wiping H1 can never flash over the IOE title. (2026-07-10) */
        /* Hero buttons stay SIDE BY SIDE + equal width on tablet/mobile too — the base .hero-actions grid
           (two 1fr columns) already equalizes them, so no stacking/column override here. (2026-07-10) */

        /* IOE service cards → stack 1-col.
           (2026-07-22) flex-direction/align-items/gap kept: harmless with the single
           remaining title block, and they are what stacks it if a second child ever
           returns. The `.services-desc` tablet override that sat here died with the
           tagline. */
        .services-top {
          flex-direction: column;
          align-items: flex-start;
          gap: 24px;
        }
        .services-grid {
          grid-template-columns: 1fr !important;
          gap: 14px;
        }
        /* IOE cards on mobile/tablet: height −25% (base 551 → 413; the old min-height:360 was a no-op —
           height:551 always won). The whole IOE block moves as ONE here: the cards ride the SAME curtain
           transform as the title+subtext (applied to .services-grid in home.js), so nothing translates on
           its own — reveals are opacity-only, no per-element translateY, no stagger — so it appears together
           and the subtext never drifts off the cards. (2026-07-10) */
        .service-card { height: 413px; transform: translateY(40px); transition: opacity 0.9s ease, transform 0.9s ease; }
        .service-card:nth-child(1), .service-card:nth-child(2), .service-card:nth-child(3) { transform: translateY(40px); transition-delay: 0s; }
        #servicesTitleBlock { transform: translateY(40px); transition: opacity 0.9s ease, transform 0.9s ease; }
        /* ENTRY + EXIT both translate now (2026-07-10): the opacity-only entry override was removed so the
           title / subtext / cards translate up (40px → 0) AND fade IN on scroll-down — matching the laptop reveal
           — not just 0→100 opacity. The base transition above (opacity 0.9s + transform 0.9s) drives both directions. */
        /* "Let's Talk" CTA: SAME height as the GC / CM cards — EXPLICIT 413px + aspect-ratio:auto (changed
           his mind 2026-07-10 from the short/wide 16:9 treatment). Title stays its 2-line "Let's / Talk." */
        .service-card--cta { aspect-ratio: auto; height: 413px; }

        /* FP grid → 2-col bento with aspect-ratio sizing */
        #projects { padding: 80px 0 120px; }
        .projects-grid {
          grid-template-columns: repeat(2, 1fr) !important;
          grid-template-rows: none !important;
          grid-auto-rows: 240px;
          grid-auto-flow: row dense;   /* backfill the col-2 gap a wide (span-2) card leaves when it can't fit beside a lone 1×1 (right of CEME / FSIAP) — a later 1×1 fills it. Keeps every card's span incl. Maison's big feature; only nudges one small card up visually. (2026-07-10) */
          height: auto;
          gap: 12px;
        }
        .project-card:nth-child(n) {
          grid-column: span 1 !important;
          grid-row: span 1 !important;
        }
        .project-card:nth-child(1) { grid-column: span 2 !important; grid-row: span 2 !important; }
        .project-card:nth-child(4) { grid-column: span 2 !important; }  /* CTA wide */
        .project-card:nth-child(8) { grid-column: span 2 !important; }
        .portfolio-cta-title { font-size: 36px; }

        /* Reviews → 1-col cards already covered at 980 */
        #reviews { padding: 100px 0 60px; }
        /* "See all clients" centering: the logo marquee's max-content track inflates its parent .container
           WIDER than the phone screen (it's an auto-margin flex item, so it shrink-wraps to content instead
           of stretching), so the justify-content:center trigger lands at that wide container's centre = off
           to the right. Cap ONLY the container that holds the trigger to the viewport so it centres on
           screen; the review-cards .container is left alone. (2026-07-10) */
        #reviews .container:has(.hc-trigger-wrap) { width: 100%; }

        /* Team — mobile padding */
        #team { padding: 80px 0; }
      }

      /* Reduced-motion (≤960): the JS wipe/fade branch is skipped for these users, so the now-FIXED content would
         never get clipped/hidden → it'd stay pinned over the whole page. Revert it to an in-flow absolute layer
         that just scrolls away with the hero (no animation — appropriate for reduced-motion). Must sit AFTER the
         ≤960 block so it wins the cascade. (2026-07-10) */
      @media (max-width: 960px) and (prefers-reduced-motion: reduce) {
        .hero-content { position: absolute; height: 100%; will-change: auto; }
      }
      /* Desktop (>960) + reduced-motion: same stranding — the scrub (which clips/hides the fixed hero) is skipped,
         so BOTH the fixed photo AND content would pin over the page. Revert both to absolute so the hero just
         scrolls away. (Pre-existing since the reduced-motion scrub-skip; surfaced by the hero review 2026-07-10.) */
      @media (min-width: 961px) and (prefers-reduced-motion: reduce) {
        .hero-photo-frame, .hero-content { position: absolute; }
      }

      /* ─── ≤ 640  mobile ───────────────────────────────────────── */
      @media (max-width: 640px) {
        :root { --gutter: 20px; }

        /* Hero — extra compact, photo locked into smaller box */
        .hero-content { padding-top: 120px; padding-bottom: 60px; }
        /* No phone hero-title size override: the base .hero-headline clamp (min 44px) already renders 44px at
           every phone width, matching the "In Our Element" .section-heading. The old clamp(36,9vw,56) here
           undercut that and broke the match on phones (2026-07-10). */

        /* IOE intro compacted on phones (2026-07-10): the title block pushed closer to the cards
           (gap 24→16); the 3 cards pulled up under the intro (72→40 — a step down the site's 72px
           intro→content grid, not a random value). Whole IOE fold nudged UP 35px (−35 margin-top on
           the container); the curtain re-measures .services-top's position so the hero→IOE handoff
           stays synced.
           (2026-07-22) The half of this note about the subtext showing only "Two core services, one
           standard" with the rest hidden went with the tagline itself — there is no subtext on this
           fold any more, and the block it referred to is now eyebrow "Core services" + heading
           "What We Do". The `gap` below now spaces the title block against nothing; kept because it
           is what would space a second child if one returns. */
        .services-top { gap: 16px; margin-bottom: 40px; }
        #services .container { margin-top: 0; }   /* was −35 (an earlier "nudge IOE up" that now fought the new #services top gap → left the title almost touching the hero on mobile). Cleared so the ~46px gap holds. (2026-07-10) */
        #servicesTitleBlock, .service-card { transform: translateY(46px); }   /* mobile: IOE exit drifts 15% lower than tablet's 40px (2026-07-10). `.services-desc` dropped from this list 2026-07-22 with the tagline. */

        /* FP → 1-col stacked with consistent 4:3 cards (no bento). */
        .projects-grid {
          grid-template-columns: 1fr !important;
          grid-auto-rows: auto !important;
          gap: 12px;
        }
        .project-card:nth-child(n) {
          grid-column: span 1 !important;
          grid-row: span 1 !important;
          aspect-ratio: 4/3;
          min-height: 0 !important;
        }
        .project-card--cta { aspect-ratio: 16/9; }
        .portfolio-cta-title { font-size: 32px; }
      }

      /* ─── ≥ 1920  ultra-wide nicety ───────────────────────────── */
      @media (min-width: 1920px) {
        /* Let the logo marquee breathe a bit on ultra-wide. Spacing now lives
           on the box's margin-right (the track no longer uses `gap`), so bump
           that — uniformly, so the seamless-loop tiling stays exact. */
        .reviews-logo-box { margin-right: 180px; }
        /* MONITOR-ONLY (≥1920 — every laptop width stays untouched): team-photo fold 5%
           shorter (2026-07-13). Same amount off BOTH the section and the photo box,
           so the photo's +250px overflow / clip choreography (250→50 in home.js) is unchanged. */
        #team.tg-sec { height: calc((100vh - 165px) * 0.95); }
        #team.tg-sec .tg-photo { height: calc((100vh - 165px) * 0.95 + 250px); }
      }
    
