
  /* Aspekta — self-hosted variable sans (SIL OFL 1.1, see
     /hero-v2/assets/fonts/aspekta/LICENSE.txt), replacing DM Sans and
     Archivo Expanded as the site's one typeface. One variable file
     covers the whole 100-900 weight range we use (regular body copy
     through the statement/services display weights), so no separate
     @font-face per weight the way the two Google Fonts it replaces
     needed. DM Sans stays listed as the fallback in every font-family
     below rather than being removed, so a failed/blocked font load
     degrades to the previous look instead of the browser default. */
  @font-face {
    font-family: 'Aspekta Variable';
    font-weight: 100 900;
    font-display: swap;
    font-style: normal;
    src: url('https://cdn.prod.website-files.com/6aaab8aa5da02fa0b7406b4a/6aad41da541e9e7bb7608099_AspektaVF.woff2') format('woff2');
  }
  :root{
    /* PALETTE — the whole site's six colors, each with exactly one
       job. Every other color in this file (the CSS below, the JS
       configs, and the canvas/shader draw calls) reads from these
       var()s or from window.PALETTE (the matching JS mirror, defined
       at the top of the first script block) rather than a hard-coded
       hex, so the whole palette is a one-line edit in either place. */
    --color-near-black: #0B0B0F; /* dark section backgrounds, text on light */
    --color-cream: #E9E6DF;      /* light section backgrounds, text on dark */
    --color-orange: #FF4F00;     /* brand accent + anything interactive */
    --color-teal: #00E5C3;       /* highlights / "alive" content */
    --color-violet: #7A5CFF;     /* footer; situation card hover fill */
    --color-yellow: #E6FF00;     /* motion only — cursor trail + doodle sparks. Never static text/backgrounds/UI. */

    --bg: var(--color-cream);
    --ink: var(--color-near-black);
    --logo: var(--color-orange);
    --blob-core: var(--color-teal);
    --blob-mid: color-mix(in srgb, var(--color-teal) 70%, var(--color-cream) 30%);
    --blob-edge: color-mix(in srgb, var(--color-teal) 40%, var(--color-cream) 60%);
  }
  *{ box-sizing: border-box; }
  html, body{
    margin: 0;
    background: var(--bg);
    color: var(--ink);
    overflow-x: hidden;
    font-family: 'Aspekta Variable', 'DM Sans', -apple-system, BlinkMacSystemFont, sans-serif;
  }

  /* ============================================================
     HERO SHELL
  ============================================================ */
  .hero{
    position: relative;
    width: 100%;
    height: 100svh;
    min-height: 620px;
    overflow: hidden;
    background: var(--hero-bg, var(--bg));
  }
  /* Every full-hero-cover child below (blob, headline layers, pixel
     trail, logo/nav/tagline/scroll-cue) is deliberately
     position:absolute against THIS box, not position:fixed against
     the viewport. Since the hero is exactly one viewport tall, both
     look identical while the hero is the only thing on screen — the
     difference only shows once there's a section below it to scroll
     into: fixed would stay pinned over that section forever, while
     absolute correctly scrolls away with the hero. The flame cursor
     is the one deliberate exception (still position:fixed) — it's
     meant to keep tracking the mouse across the hero AND the
     statement section below, not just inside this box. */

  /* ============================================================
     DOT GRID — fine white dots, confined to the left/upper-mid
     region only (matches report: x 0-47%, y 19-77%).
  ============================================================ */
  .dot-grid{
    position: absolute;
    left: 0;
    top: 19%;
    width: 47%;
    height: 58%;
    background-image: radial-gradient(circle, color-mix(in srgb, var(--color-near-black) 10%, transparent) 1px, transparent 1.6px);
    background-size: 10px 10px;
    z-index: 0;
    pointer-events: none;
  }

  /* ============================================================
     BLOB — static (per report: fully static in the reference,
     no idle drift, no cursor-follow). Plain CSS radial-gradient
     + blur, cropped by the bottom-right corner.

     Own fix, per explicit report: the hero already clips this via
     .hero's own overflow:hidden, so it can't literally bleed past
     the section box — the actual complaint was that it stayed at
     full static teal intensity while SEAM_CONFIG (below, near the
     end of the file) darkens the hero's own --hero-bg right at the
     boundary, so right at the seam it read as a hard bright shape
     against the next (already-dark) section instead of fading out
     with its own backdrop. --hero-fade-out is the same eased 0→1
     crossfade progress SEAM_CONFIG already computes for the hero
     boundary (see its own comment there) — reusing it here instead
     of a second scroll listener keeps this in lockstep with the bg
     transition by construction. Falls back to fully opaque (the
     resting state, and the prefers-reduced-motion path where that
     script never runs and the var is never set).
  ============================================================ */
  .blob{
    position: absolute;
    right: -8%;
    bottom: -16%;
    width: 52vw;
    height: 52vw;
    max-width: 800px;
    max-height: 800px;
    border-radius: 50%;
    background: radial-gradient(circle at 58% 38%,
                var(--blob-core) 0%, var(--blob-mid) 30%, var(--blob-edge) 48%, transparent 68%);
    filter: blur(42px);
    opacity: calc(1 - var(--hero-fade-out, 0));
    z-index: 1;
    pointer-events: none;
  }

  /* ============================================================
     SMALL UI — logo mark, top line, tagline, nav, scroll cue
  ============================================================ */
  .logo-mark{
    position: absolute;
    left: 2.8%;
    top: 5.5%;
    width: 36px;
    height: 36px;
    background: var(--logo);
    border-radius: 30%;
    z-index: 20;
  }
  .top-line{
    position: absolute;
    left: 50%;
    top: 5.5%;
    width: 47.2%;
    border-top: 1px solid var(--ink);
    z-index: 20;
  }
  .tagline{
    position: absolute;
    left: 66%;
    top: 8.2%;
    width: max-content;
    max-width: 230px;
    font-size: 17px;
    line-height: 25px;
    font-weight: 400;
    color: var(--hero-text, var(--ink));
    opacity: var(--hero-text-opacity, 1);
    z-index: 20;
    margin: 0;
  }
  .nav{
    position: absolute;
    left: 89.6%;
    top: 8.5%;
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 14px;
    z-index: 20;
  }
  .nav a{
    color: var(--hero-text, var(--ink));
    opacity: var(--hero-text-opacity, 1);
    text-decoration: none;
    font-size: 20px;
    font-weight: 500;
    line-height: 24px;
  }
  .scroll-cue{
    position: absolute;
    left: 4.2%;
    bottom: 3%;
    font-size: 17px;
    line-height: 24px;
    font-weight: 400;
    color: var(--hero-text, var(--ink));
    opacity: var(--hero-text-opacity, 1);
    z-index: 20;
    margin: 0;
  }

  /* ============================================================
     HEADLINE — three independently-positioned lines, matching
     the report's exact per-line offsets. All three (ECHO, BOOMER,
     DIGITAL) warp identically now. Each line's real text stays in
     the DOM — present for SEO/screen readers — but is visually
     hidden (opacity:0, still in the a11y tree) — with a single
     shared WebGL <canvas> laid on top rendering the warped version
     of all three. See the WARP RENDERER section below for the
     canvas side of this.
  ============================================================ */
  .headline-wrap{
    position: absolute;
    inset: 0;
    z-index: 3;
    pointer-events: none;
  }
  .headline-inner{
    position: absolute;
    inset: 0;
    transform-origin: left center;
    transform: scale(var(--fit, 1));
  }
  h1.headline{
    margin: 0;
    font-family: 'Aspekta Variable', 'DM Sans', sans-serif;
    font-weight: 650;
    color: var(--ink);
  }
  /* Font-size is capped by BOTH vw and vh (not just vw) so the
     three-line stack always fits within one screen, on any
     aspect ratio — the reference itself lets its bottom line run
     past the fold (hence its "scroll down" prompt); we don't. */
  .line-slot{
    position: absolute;
    white-space: nowrap;
    font-size: clamp(120px, min(21.7vw, 30vh), 420px);
  }
  .line-slot--1{ left: 3.8%; top: 10%; line-height: 0.69; }
  .line-slot--2{ left: 34.0%; top: 33%; line-height: 0.69; }
  .line-slot--3{ left: 3.8%; top: 56%; line-height: 0.96; }

  .line-text{
    position: relative;
    display: block;
    /* real text, present for SEO/screen readers — visually hidden
       via opacity (not display:none/visibility:hidden) so it stays
       in the accessibility tree. All three lines render through the
       single shared headline canvas below (never painted directly —
       this opacity:0 is what keeps only the canvas copy visible). */
    opacity: 0;
  }
  /* reduced-motion / no-hover-capable fallback: no canvas at all,
     just show the real static text */
  .reduce-motion .line-text{
    opacity: 1;
  }
  /* Own fix, per explicit request: was one canvas covering the whole
     3-line block (inset:0); now one per line, sized/positioned by
     JS to that line's own box plus padding — see the padFraction
     comment in HeadlineFX below for why. top/left/width/height are
     set inline per instance, not here. */
  .headline-canvas{
    position: absolute;
    z-index: 4;
    display: block;
    pointer-events: none;
  }
  .reduce-motion .headline-canvas{ display: none; }

  /* ============================================================
     PIXEL TRAIL — a halftone-style trail of small marks following
     recent cursor motion. Rebuilt as a faithful, own-code
     re-implementation of lamalama.com's exact measured pipeline —
     see /research/lamalama/PIPELINE.md for the full deconstruction
     (every formula/constant CONFIRMED from their real source) and
     /research/lamalama/harness/ for the numeric verification that
     our buffer values match theirs. Own placeholder mark, own
     colors — no assets or code from that site.
     PAGE-WIDE now, not hero-scoped: the canvas is a fixed,
     full-viewport layer that's a direct child of <body> (not
     nested in any section), so it isn't clipped by a section's own
     overflow:hidden and keeps covering the screen as the page
     scrolls. z-index 50 sits above every section's own content
     (highest other z-index anywhere on the page is 20) and BELOW
     the flame cursor (z-index 60), matching the hero's original
     "above content, below the flame hand" layering. See
     TRAIL_CONFIG + the PIXEL TRAIL section in the script for the
     actual mechanism, and data-trail="off" below for the per-area
     opt-out.
  ============================================================ */
  .pixel-trail-canvas{
    position: fixed;
    inset: 0;
    z-index: 50;
    display: block;
    pointer-events: none;
  }
  .reduce-motion .pixel-trail-canvas{ display: none; }

  /* ============================================================
     FLAME CURSOR — replaces the system cursor inside the hero on
     fine-pointer devices (mouse/trackpad only; nothing shows on
     touch). Two-layer structure:
       .flame-cursor        outer wrapper — position ONLY, moved
                             with a raw JS `transform: translate()`
                             on every pointermove with NO CSS
                             transition, so it tracks the mouse
                             with zero added lag.
       .flame-cursor-inner  inner wrapper — rotate()/scale() only,
                             CSS-transitioned so the direction-tilt
                             settles back smoothly and the nav-hover
                             scale-up feels smooth. transform-origin
                             is set from JS to the match-flame tip
                             (the hotspot) so both rotation and
                             scale pivot around that exact point.
     Color is fixed — CURSOR_COLOR (set once in the script) is
     applied to `color` (the SVG uses fill="currentColor") a single
     time at init. No brightness sampling, no per-frame switching.
  ============================================================ */
  html.has-flame-cursor #hero,
  html.has-flame-cursor #hero *,
  html.has-flame-cursor #statement,
  html.has-flame-cursor #statement *,
  html.has-flame-cursor #works,
  html.has-flame-cursor #works *,
  html.has-flame-cursor #showcase,
  html.has-flame-cursor #showcase *{
    cursor: none !important;
  }
  .flame-cursor{
    position: fixed;
    left: 0;
    top: 0;
    z-index: 60;
    pointer-events: none;
    opacity: 0;
    transition: opacity 0.15s ease;
    will-change: transform;
  }
  .flame-cursor.is-active{ opacity: 1; }
  .flame-cursor-inner{
    width: 40px;
    transition: transform 0.12s ease-out;
  }
  .flame-cursor-inner svg{
    display: block;
    width: 40px;
    height: auto;
  }

  /* ============================================================
     STATEMENT SECTION — below the hero. Fluid type-size scale still
     matched to ashleybrookecs.com's own statement section (see
     /research/ashleybrooke/REPORT.md for the measured clamp()
     formula). The reveal itself is our own letter-by-letter
     illumination (ILLUMINATE_CONFIG in the script below); the
     background is a faint technical grid (GRID_BG_CONFIG), replacing
     an earlier particle field. Modeled on digitalflagship.com's hero
     background (read live: a background-image SVG of straight 1px
     hairlines, white at 0.3 opacity, ~66px apart in a 1920px-wide
     SVG, no rotation). Their own accent there is a cluster of
     concentric circles; the diagonal lines here are our own addition
     instead, not theirs — see GRID_BG_CONFIG's own comment.
  ============================================================ */
  .statement{
    position: relative;
    overflow: hidden;
    /* Own fix: was a flat var(--color-near-black) with no crossfade
       hook — now the same --X-bg indirection every other seam-crossed
       section already uses, so the SEAM_CONFIG script (below, near
       the end of the file) can blend this into works' new cream
       right at the boundary instead of a hard cut. Fallback stays
       near-black, this section's own resting color, for the
       prefers-reduced-motion path where that script never runs. */
    background: var(--statement-bg, var(--color-near-black));
    padding: 6vh 5vw; /* vertical padding on vh, not vw — vw padding at a wide/short viewport was adding hundreds of px of extra height, which the JS fit-check (measuring the text alone) didn't know about, and the section grew taller than one viewport as a result */
    min-height: 100vh;
    display: flex;
    align-items: center;
    justify-content: center;
  }
  /* Grid background — pure CSS, no canvas/SVG to size or redraw: two
     repeating-linear-gradient layers draw the straight grid, a third
     draws the diagonal accent lines, and a radial-gradient mask fades
     the whole thing toward the section's edges so it reads strongest
     behind the text in the middle. Values come from GRID_BG_CONFIG in
     the script below, applied as CSS custom properties so the config
     stays the one place to tune this. */
  .grid-bg{
    position: absolute;
    inset: 0;
    pointer-events: none;
    background-image:
      repeating-linear-gradient(to right, var(--grid-line-color) 0, var(--grid-line-color) 1px, transparent 1px, transparent var(--grid-spacing)),
      repeating-linear-gradient(to bottom, var(--grid-line-color) 0, var(--grid-line-color) 1px, transparent 1px, transparent var(--grid-spacing)),
      repeating-linear-gradient(var(--grid-diagonal-angle), var(--grid-diagonal-color) 0, var(--grid-diagonal-color) 1px, transparent 1px, transparent var(--grid-diagonal-spacing));
    -webkit-mask-image: radial-gradient(ellipse 68% 62% at 50% 50%, #000 0%, transparent 100%);
    mask-image: radial-gradient(ellipse 68% 62% at 50% 50%, #000 0%, transparent 100%);
  }
  .statement-inner{
    position: relative;
    width: 90vw;
    max-width: 2200px;
    margin: 0 auto;
  }
  /* Fluid type size — same SHAPE as ashleybrookecs.com's own clamp()
     (see REPORT.md): fixed at/below a small reference viewport,
     fixed (larger) at/above a large one, linear vw between — set as
     --statement-fs by applyStatementFontSize() in the script, plus a
     fit check that shrinks it further if the rendered block would
     ever exceed the viewport height. Archivo Expanded/Black is this
     build's pick for an extra-wide, heavy display font in the
     reference's feel — our headline font (DM Sans) doesn't come in a
     wide/black cut, so it wasn't a fit here the way it is for the
     hero's own headline. */
  .statement-text{
    position: relative;
    z-index: 3; /* above .statement-cards (z-index 2), in the SAME stacking context as it (.statement-inner has no z-index of its own, so it doesn't isolate) — mix-blend-mode below only reaches across to .statement-cards because this element carries both the z-index and the blend mode together, exactly like the old .statement-text--fill layer did */
    margin: 0;
    font-family: 'Aspekta Variable', 'DM Sans', sans-serif;
    font-weight: 800;
    text-transform: uppercase;
    text-align: center;
    line-height: 0.9;
    letter-spacing: 0;
    font-size: var(--statement-fs, 80px);
    mix-blend-mode: difference; /* restored from the pre-illumination .statement-text--fill layer: against our near-black section background it's close to a no-op, but wherever a lit letter crosses one of the floating cards below it reads as a striking color-inverted flash. A first attempt put this on a descendant span instead (.statement-text__lit) — since THIS element already establishes its own stacking context (position+z-index above), a blend mode set one level down never escaped it to reach .statement-cards at all. A second attempt replaced the blend with per-letter opacity, which worked but changed the look (no inversion, and every letter that merely touched a card edge got dimmed everywhere, even over the plain background) — reverted; the inverted look is the intended one. */
  }
  .reduce-motion .statement-text{ mix-blend-mode: normal; } /* reduced motion: flat final colors, no card-crossing invert flash — matches the old .reduce-motion .statement-text--fill{ display:none } simplification */
  /* ---- letter-by-letter illumination ----
     Built by buildStatementText() in the script: every word is its
     own inline-block span (so a line only ever breaks between words,
     never mid-word), and — in the default "letter"
     ILLUMINATE_CONFIG.mode — every letter inside it is its own span
     too, colored/lifted directly by updateIllumination() on each
     scroll tick. No CSS transition here on purpose: the per-frame JS
     already interpolates color/position every frame, and a competing
     CSS transition would just fight it and add lag. The one
     genuinely highlighted run of words ("Passionate oddballs") is
     still wrapped in a single .hl span exactly as before, since
     DOODLES_CONFIG's sparkler doodle anchors to it by that selector.
     .sr-only carries the plain, real sentence for screen readers; the
     decorative word/letter spans sit in a sibling marked
     aria-hidden — it contributes no pixels of its own, so blending
     the whole .statement-text paragraph (above) is visually
     identical to blending just the visible span. ---- */
  .statement-text .word{
    display: inline-block;
    white-space: nowrap;
  }
  .statement-text .ltr{
    display: inline-block;
    color: var(--color-near-black);
  }
  .statement-text .sr-only{
    position: absolute;
    width: 1px; height: 1px;
    padding: 0; margin: -1px;
    overflow: hidden;
    clip: rect(0,0,0,0);
    white-space: nowrap;
    border: 0;
  }

  /* ---- floating placeholder cards ----
     Sized/positioned/moved to match the reference's own measured
     values (see REPORT.md): each card capped to ~20vw wide and a
     fixed max-height, arranged down the section alternating left/
     right (not scattered or bleeding off the viewport edge like the
     earlier digitalflagship-inspired layout), and — importantly —
     ALL FIVE MOVE TOGETHER AS ONE GROUP at a single parallax speed
     (the reference moves its whole photo column as one unit, not at
     individually different per-card speeds — confirmed in
     REPORT.md), driven by one transform on .statement-cards itself. */
  .statement-cards{
    position: absolute;
    inset: 0;
    z-index: 2; /* above the base text, below the fill/wipe text layer — letters crossing a card is the point */
    pointer-events: none;
    will-change: transform;
  }
  .floating-card{
    position: absolute;
    display: flex;
    align-items: flex-end;
    overflow: hidden;
    width: 20vw;
    max-width: 320px;
    min-width: 140px;
    height: 15.875rem;
    max-height: 22vh;
    border-radius: 18px;
    background: var(--color-near-black); /* shows briefly while the image loads */
  }
  .floating-card__img{
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
    z-index: 0;
  }
  /* bottom scrim so the label stays legible over any real photo,
     regardless of how light/dark that photo is at the bottom edge —
     the one addition real photography needed that flat color blocks
     didn't. */
  .floating-card::after{
    content: "";
    position: absolute;
    inset: 0;
    z-index: 1;
    background: linear-gradient(to top, color-mix(in srgb, var(--color-near-black) 78%, transparent), transparent 60%);
    pointer-events: none;
  }
  .floating-card__label{
    position: relative;
    z-index: 2;
    display: block;
    padding: 14px 16px;
    font-family: 'Aspekta Variable', 'DM Sans', sans-serif;
    font-weight: 650;
    font-size: 12px;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--color-cream);
  }

  /* ---- per-card placement ----
     Real client photography, one per card — see
     /hero-v2/assets/clients (originals in ./original, these are the
     optimized WebP versions used here). Position/size values below
     are untouched from the placeholder-block version; only the
     background-color -> background-image swap (now an <img>, for
     object-fit: cover + lazy loading) and the id/label per card
     changed. Alternating left/right down the section, matching the
     reference's own vertical column of images rather than scattering
     cards at the viewport edges. */
  #cardPoppi{
    top: 6%; left: 4%;
  }

  #cardHyatt{
    top: 4%; right: 6%;
    height: 18vw;
    max-height: 26vh;
  }

  #cardChilis{
    top: 38%; left: 7%;
  }

  #cardWinterPark{
    top: 40%; right: 4%;
  }

  /* Own fix, per explicit request: was centered (top:74%; left:50%;
     transform:translateX(-50%)) — moved to the left column, in line
     with the section's own established left/right alternation
     (Poppi/Chilis on the left, Hyatt/WinterPark on the right), so the
     middle of the section stays clear for the statement text. A new
     sixth card sits opposite it on the right — see #cardBkf below. */
  #cardWildeChips{
    top: 74%; left: 7%;
  }

  #cardBkf{
    top: 76%; right: 6%;
  }

  /* below the reference's own breakpoint (1024px, confirmed in the
     digitalflagship report — carried forward here since ashleybrooke
     couldn't be confirmed either way on mobile, see REPORT.md):
     smaller text, fewer cards, no scroll-driven motion. The JS also
     checks this same width once at load (see script). */
  @media (max-width: 1024px){
    .statement{ padding: 8vh 6vw; }
    .floating-card--hide-mobile{ display: none; }
    .statement-cards{ transform: none !important; }
    .floating-card{ width: 32vw; height: 22vw; max-height: none; }
  }

  /* ============================================================
     SELECTED WORKS — own build, grid/proportions/scroll-in matched
     from julienpianetti.com's own "Selected works" section (see
     /research/pianetti/REPORT.md for the full deconstruction —
     everything below marked CONFIRMED there is taken from their
     real compiled CSS/JS, not guessed). Own code throughout: no
     assets, fonts, or copied source from that site.

     GRID — CONFIRMED 3-per-row × 2 rows (their 12-col grid, each
     item spanning 4, is the same shape as our simpler 3-column
     grid), CONFIRMED 4:5 portrait media boxes. Column/row gap here
     are our own values in the same spirit as their fluid gutter
     tokens (tight column gap, much taller row gap) — not a literal
     px match, they use responsive custom properties we don't have.
     Breakpoints (3 → 2 → 1 column) are our own equivalent of their
     12 → 6 → 4 column steps, since our items span "1 of 3" rather
     than "4 of 12".

     CAPTION — CONFIRMED title + one line of type/category directly
     under the media, left-aligned. Their 6 boxes carry no year or
     project number at all (a genuine finding, not an oversight —
     see the report); ours adds both anyway, per this task's own
     brief, as an easy-to-edit "service · year" line.

     HOVER — their box-level hover is "video plays from frame 0,
     pauses+resets on leave" (see report); we're using static client
     photography here, so that specific mechanic has no
     video-loop-to-swap-in, and the box itself just gets a light
     scale-up on hover as its only own reaction (own minimal
     addition, not a reference transplant — see .works-item__img
     below). Their REAL describable "hover mechanic" — a small dot
     that continuously tracks the raw cursor position across the
     whole page with a soft ~500ms smoothing catch-up, shown via
     body:hover, mix-blend-mode:exclusion, default label "Play" —
     is what .works-cursor below reproduces, with our cherry bomb
     silhouette standing in for their plain circle (the one place
     the reference "uses its own shape that follows the mouse" —
     see WORKS_CONFIG + the cursor-follower script for the mechanism
     and REPORT.md for the citation).

     SCROLL-IN — their own items are visibility:hidden until
     Webflow's Interactions 3 engine boots, then something reveals
     them; the exact reveal timeline lives in Webflow's compiled
     runtime and wasn't recoverable from source (flagged GUESS in
     the report). The fade+rise-with-stagger below is our own
     reasonable default for that same "boots in on load/scroll"
     character, not copied precision.
  ============================================================ */
  /* Own fix, per explicit request: was the same near-
     black as the statement section directly above it, so the two ran
     together with no visible seam. Switched to cream — this section's
     own choice, after trying violet first: cream reads as a clean,
     confident break from statement's near-black (this file's other
     two accents, orange and teal/yellow, are reserved for interactive
     signaling and motion respectively, not resting backgrounds — see
     their own :root comments). Cream also makes this the SAME color
     as showcase directly below, so — unlike every other section
     boundary in this file — there's genuinely nothing to crossfade at
     the works/showcase seam; SEAM_CONFIG below no longer carries a
     "works" entry (removed, not just left inert) for exactly that
     reason, matching how statement/works used to have no boundary
     between them back when THEY shared a color. The statement/works
     seam (near-black -> cream, the biggest jump in the whole page)
     keeps its own crossfade via the "statement" entry there. This
     section now carries data-theme="light" (see the markup), the same
     mechanism as every other cream section in this file (showcase,
     situations, crafted, team-slider) — required here, not cosmetic:
     doodleUrl() and the pixel-trail's per-area color swap both key off
     that attribute, and without it the paper-airplane doodle would
     render in its cream-ink variant (invisible on this now-cream bg)
     and the cursor trail would keep its low-contrast-on-light default
     color while over this section. Card labels/meta sit on their own
     per-card photo + dark scrim (see .works-item__scrim below), not
     directly on this background, so they're unaffected either way;
     the heading text below reads near-black off the plain fallback,
     same static pairing every other light section in this file uses,
     no dynamic contrast-picking needed once there's no crossfade. ---- */
  .works{
    position: relative;
    background: var(--color-cream);
    color: var(--color-near-black);
    padding: 12vh 5vw 14vh;
  }
  .works-heading{
    font-family: 'Aspekta Variable', 'DM Sans', sans-serif;
    font-weight: 800;
    text-transform: uppercase;
    line-height: 0.95;
    font-size: inherit; /* set on .works-heading-row below, which now wraps this heading + its doodle, so the doodle's own em-based size (see #doodleWorksPaperAirplane) always tracks the same value as the heading text itself, sibling or not */
    margin: 0; /* margin moved to .works-heading-row below */
  }
  .works-grid{
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    column-gap: 1.5vw;
    row-gap: 6vh;
  }
  @media (max-width: 991px){
    .works-grid{ grid-template-columns: repeat(2, 1fr); }
  }
  @media (max-width: 640px){
    .works-grid{ grid-template-columns: 1fr; }
  }
  .works-item{
    display: block;
    color: inherit;
    text-decoration: none;
    /* scroll-in reveal, own timing (see block comment above) —
       toggled by .is-in, added via IntersectionObserver in script. */
    opacity: 0;
    transform: translateY(28px);
    transition: opacity 0.8s cubic-bezier(.16,1,.3,1), transform 0.8s cubic-bezier(.16,1,.3,1);
  }
  .works-item.is-in{
    opacity: 1;
    transform: translateY(0);
  }
  .works-item__media{
    position: relative;
    aspect-ratio: 4 / 5; /* CONFIRMED, see block comment above */
    width: 100%;
    border-radius: 18px; /* matches the statement section's own floating-card radius */
    overflow: hidden;
    background: color-mix(in srgb, var(--color-cream) 88%, var(--color-near-black)); /* shows briefly while the image loads — own fix: a soft warm grey off the section's own new cream bg (was flat near-black) so a not-yet-loaded card reads as part of the section rather than flashing a contrasting black box, while staying visibly distinct from the bg so the card's own shape still shows */
  }
  .works-item__img{
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
    border-radius: 18px; /* redundant with .works-item__media's own radius+clip, but kept for safety */
    transform: scale(1);
    transition: transform 0.6s cubic-bezier(.16,1,.3,1);
  }
  .works-item:hover .works-item__img{ transform: scale(1.045); }

  /* ---- title/meta overlay — moved on top of the image (bottom-left)
     per this task's brief, standing in the same visual spot
     tbwachiatdayla.com/work uses. A dark scrim keeps white text
     legible over any photo; both sit inside .works-item__media so the
     section's existing border-radius clips them the same as the
     photo. ---- */
  .works-item__scrim{
    position: absolute;
    left: 0; right: 0; bottom: 0;
    height: 58%;
    background: linear-gradient(to top, color-mix(in srgb, var(--color-near-black) 60%, transparent), transparent 100%);
    pointer-events: none;
  }
  .works-item__text{
    position: absolute;
    left: 0; right: 0; bottom: 0;
    padding: 22px 22px 20px;
  }
  .works-item__caption{ margin-top: 16px; } /* still used by the placeholder card, which has no photo to overlay */
  .works-item__title{
    font-family: 'Aspekta Variable', 'DM Sans', sans-serif;
    font-weight: 650;
    font-size: 17px;
    margin: 0 0 4px;
    color: var(--color-cream);
  }
  .works-item__meta{
    font-family: 'Aspekta Variable', 'DM Sans', sans-serif;
    font-weight: 500;
    font-size: 13px;
    letter-spacing: 0.03em;
    text-transform: uppercase;
    color: color-mix(in srgb, var(--color-cream) 50%, transparent);
    margin: 0;
  }
  .works-item__text .works-item__title{
    font-size: 24px;
    color: var(--color-cream);
  }
  .works-item__text .works-item__meta{
    color: color-mix(in srgb, var(--color-cream) 85%, transparent);
  }
  @media (max-width: 640px){
    .works-item__text .works-item__title{ font-size: 20px; }
    .works-item__text{ padding: 18px 18px 16px; }
  }

  /* sixth box — no client yet, kept as an editable placeholder
     rather than substituting a real client into an unused slot. */
  .works-item--placeholder .works-item__media{
    background: transparent;
    border: 1px dashed color-mix(in srgb, var(--color-cream) 28%, transparent);
    display: flex;
    align-items: center;
    justify-content: center;
  }
  .works-item--placeholder .works-item__placeholder-mark{
    font-family: 'Aspekta Variable', 'DM Sans', sans-serif;
    font-size: 13px;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: color-mix(in srgb, var(--color-cream) 35%, transparent);
  }

  /* ---- cursor-follower — cherry bomb outline standing in for the
     reference's plain circular dot (see block comment above).
     Fixed + pointer-events:none like the pixel trail; z-index 55
     sits above the trail (50) and below the flame cursor (60). Size/
     easing/timing all live in WORKS_CONFIG in the script. ---- */
  .works-cursor{
    position: fixed;
    top: 0;
    left: 0;
    width: 56px;
    height: 56px;
    pointer-events: none;
    z-index: 55;
    opacity: 0;
    color: var(--color-teal);
    mix-blend-mode: exclusion; /* CONFIRMED on the reference's own dot — see report */
    transition: opacity 0.2s ease; /* CONFIRMED ~200ms on the reference's pointer fade */
    will-change: transform, opacity;
  }
  .works-cursor svg{ width: 100%; height: 100%; display: block; }
  /* touch / no fine pointer, and prefers-reduced-motion: never shown.
     Matches the reference's own touch fallback (their dot has no
     separate touch handling either; it just never gets the
     body:hover it depends on) and this task's explicit "no
     mouse-following motion" rule under reduced motion. Enforced in
     script — the follower is only ever initialized when canHover
     && !reduceMotion, so under either condition it's simply never
     shown (stays at its default opacity:0, no listeners attached). */
  .reduce-motion .works-cursor{ display: none; }

  /* ============================================================
     INTERACTIVE SHOWCASE v2 — own build, rebuilt against
     zermatt.qodeinteractive.com's actual measured mechanics (see
     /research/zermatt-showcase/REPORT.md, including the ADDENDUM —
     everything marked CONFIRMED there is taken from their real
     compiled CSS/JS/live-DOM measurements, not guessed). Own code
     throughout: no assets, fonts, Swiper, or copied source from
     that site — the horizontal loop below is our own hand-rolled
     equivalent of their Swiper-driven one, not the library itself.

     ROWS — CONFIRMED: each row is display:inline-flex,
     white-space:nowrap, its content repeated 3x for a seamless
     loop, sized by its own content (not the 100%-wide row
     container), so it naturally overflows past both screen edges —
     nothing clips it horizontally except the physical viewport.
     Row item: align-items:flex-start (this — NOT a font-size-plus-
     vertical-align trick — is what makes the subtitle read as a
     superscript: title and subtitle are flex siblings pinned to the
     same top edge, and the subtitle is just much smaller, so it
     visually clusters near the top of the tall title). Subtitle is
     ~20% of the title's font-size, CONFIRMED ratio (30px/150px on
     their desktop). Motion (JS, own design — the reference site
     doesn't scroll vertically so its wheel/drag-driven row advance
     doesn't apply here): row position is a continuous function of
     window.scrollY, lerp-smoothed per frame, alternating direction
     per row (odd rows left, even rows right on scroll down, both
     reversing on scroll up), looping infinitely via a repeat-and-
     wrap technique (N repeated copies per row, wrapped every exact
     multiple of one copy's width so the seam is always pixel-
     identical content). Only animates while the section intersects
     the viewport; static under prefers-reduced-motion. This section
     used to be the last thing on the page — briefly needed a filler
     spacer after it just to give scrolling enough room for this
     motion to run through the section's own exit — but the services
     section below now provides that same room as real content
     instead, so the spacer's gone.

     HOVER — CONFIRMED: text color only (#e6e6e6-equivalent ->
     #0B0B0F), .2s ease-out, no motion on the text itself. Only the
     exact hovered element gets it — a repeated copy of the same
     brand elsewhere stays untouched (own fix: the reference's own
     demo never repeats a name across multiple simultaneously-visible
     rows the way an all-40-clients layout can).

     IMAGE REVEAL — CONFIRMED fixed position (not cursor-following —
     verified live, their own image sits at a constant screen
     position regardless of which row/name is hovered), 450ms
     delayed-hide so the outgoing image overlaps the incoming one,
     400ms fade-in (cubic-bezier(.215,.61,.355,1)), the image itself
     300ms opacity + 400ms scaleY(1.1)->1/skewY(5deg)->0 "un-skew"
     (cubic-bezier(.22,.61,.36,1)). CONFIRMED zero canvas elements
     exist on their page — no shader/WebGL anywhere — so the
     "irregular blob" look is their illustrated artwork's own organic
     outline, not a mask. We don't have illustrated assets, so we
     get the same "irregular blob, not a rectangle" quality with our
     own clip-path (an SVG blob shape, defined once, reused for every
     image) — same effect, necessarily different (our own)
     construction, on top of the confirmed skew/scale/timing above.
     Own fix: the image layer sits in its own stacking context
     (.showcase has isolation:isolate) with a higher z-index than the
     rows, so a revealed image always paints on top of the row text
     rather than behind it; pointer-events:none on the layer keeps it
     from blocking hover on the rows underneath. The hovered name
     itself then rises one level above THAT (a promoted copy in
     .showcase-active-name, since a z-index on the real item can't
     escape the stacking context its own row-track's `transform`
     creates — see that block comment below), with a second,
     white, clip-path-masked copy of the same name stacked exactly on
     top of it and clipped every frame to the image's own live
     position, so the hovered name reads black on the plain
     background and white wherever it overlaps the photo, split
     exactly at the image's edge.

     VIEW CURSOR — CONFIRMED: the site's own general custom cursor,
     8px dot, scales 10x on hover via a 0.3s cubic-ish ease-out,
     "View" label fades in over the same 0.3s, and — CONFIRMED, the
     one part of this whole component that IS truly cursor-tracking
     — its position updates with ZERO lag (duration 0), not eased at
     all. Ours: same base size/scale/timing, colored flame orange
     per this task's brief (reference is a red-orange #d83c27, close
     but not identical — an intentional own-brand substitution), and
     layered below our flame-hand cursor (z-index) so the flame hand
     stays the visible "primary" cursor, matching this task's
     explicit "flame hand cursor stays visible" instruction.
  ============================================================ */
  .showcase{
    position: relative;
    background: var(--showcase-bg, var(--color-cream));
    color: var(--showcase-text, var(--color-near-black));
    min-height: 100vh;
    display: flex;
    flex-direction: column;
    justify-content: center;
    padding: 4vh 0;
    overflow: hidden;
    isolation: isolate; /* own stacking context so rows/image-reveal/View-circle z-index order can't be disturbed by anything outside the section */
  }
  .showcase-rows{
    position: relative;
    z-index: 10;
    display: flex;
    flex-direction: column;
  }
  .showcase-row{
    width: 100%;
    overflow: visible;
    margin: 4px 0;
  }
  .showcase-row-track{
    display: inline-flex;
    white-space: nowrap;
    will-change: transform;
  }
  .showcase-item{
    display: inline-flex;
    align-items: flex-start; /* CONFIRMED — see block comment: this is the actual superscript mechanism */
    padding: 0 12px 3px;
    color: color-mix(in srgb, var(--color-near-black) 10%, transparent);
    text-decoration: none;
    transition: color .2s ease-out; /* CONFIRMED */
  }
  html.has-flame-cursor .showcase-item{ cursor: none; } /* the View cursor + flame hand take over — see block comment */
  .showcase-item__title{
    font-family: 'Aspekta Variable', 'DM Sans', sans-serif;
    font-weight: 800;
    text-transform: uppercase;
    line-height: 1.19; /* CONFIRMED ratio */
    font-size: 64px;
  }
  .showcase-item__subtitle{
    font-family: 'Aspekta Variable', 'DM Sans', sans-serif;
    font-weight: 500;
    text-transform: uppercase;
    letter-spacing: 0.02em;
    font-size: 13px; /* CONFIRMED ~20% of the title size */
    line-height: 3.1em; /* CONFIRMED */
    margin-left: 6px;
  }
  .showcase-item.is-active,
  .showcase-item:hover{
    color: var(--color-near-black); /* CONFIRMED */
  }
  /* own fix: the real hovered element stays in place (so it keeps
     receiving the mouseenter/mouseleave that drive all of this) but
     renders invisible — opacity, not visibility, so it stays
     hit-testable — while its promoted .showcase-active-name copy
     stands in for it visually, above the image. */
  .showcase-item.is-active{ opacity: 0; }
  @media (max-width: 1600px){ .showcase-item__title{ font-size: 44px; } }
  @media (max-width: 1366px){ .showcase-item__title{ font-size: 32px; } }
  @media (max-width: 680px){
    .showcase-item__title{ font-size: 20px; }
    .showcase-item__subtitle{ font-size: 8px; }
  }

  /* ---- reveal images — fixed position (CONFIRMED, not cursor-following), blob clip-path (own technique) + CONFIRMED skew/scale/timing ---- */
  .showcase-images{
    position: absolute;
    inset: 0;
    z-index: 20; /* above .showcase-rows (10) — image reveal must sit on top of the row text, never behind it */
    overflow: hidden;
    pointer-events: none; /* image layer never blocks hovering the rows underneath it */
  }
  .showcase-image{
    position: absolute;
    top: 50%;
    right: 10%;
    width: clamp(280px, 30vw, 480px);
    height: clamp(280px, 30vw, 480px);
    transform: translateY(-50%);
    opacity: 0;
    visibility: hidden;
    /* CONFIRMED: 0-duration transition with a 450ms DELAY. */
    transition: opacity 0s var(--showcase-hide-delay, .45s), visibility 0s var(--showcase-hide-delay, .45s);
  }
  .showcase-image.is-active{
    opacity: 1;
    visibility: visible;
    z-index: 20;
    transition: opacity var(--showcase-fade-in, .4s) cubic-bezier(.215,.61,.355,1), visibility 0s;
  }
  .showcase-image__img{
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
    clip-path: url(#showcaseBlob); /* own technique — see block comment */
    opacity: 0;
    transform: scaleY(var(--showcase-scale-y-rest, 1.1)) skewY(var(--showcase-skew-rest, 5deg));
    will-change: transform;
    transition: opacity var(--showcase-img-fade, .3s) ease-out, transform var(--showcase-img-transform, .4s) cubic-bezier(.22,.61,.36,1);
  }
  .showcase-image.is-active .showcase-image__img{
    opacity: 1;
    transform: scaleY(1) skewY(0deg);
  }
  /* prefers-reduced-motion: images appear with no motion — plain
     instant swap, no delayed-hide overlap, no skew/scale settle. */
  .reduce-motion .showcase-image,
  .reduce-motion .showcase-image__img{
    transition: none !important;
    transform: none !important;
  }

  /* ---- active-name overlay — own fix: promoted copy of the hovered
     name, positioned (JS, every frame) to sit exactly over the real
     (now invisible) item underneath. Lives as a direct child of the
     isolated .showcase — NOT inside any .showcase-row-track, whose
     own `transform` would otherwise cap it at the track's stacking
     level no matter what z-index it's given, trapping it under the
     image regardless. position:fixed + z-index here is resolved
     against .showcase's own isolation:isolate context, same as
     .showcase-cursor below. Two full-text copies stacked exactly on
     top of each other: __base (black, the default "hovered" color,
     covers the whole name) and __clip (white, clip-path recomputed
     every frame from the live image rect in this element's own local
     coordinates — see JS — so only the part actually overlapping the
     image shows white, with the split landing exactly on the image's
     edge, mid-letter included). Not mix-blend-mode: difference —
     that reads as noise over a colorful photo; an exact geometric
     clip keeps both halves flat, intentional colors. pointer-events:
     none throughout so this layer, sitting above every row, never
     blocks hovering a different name underneath it. */
  .showcase-active-name{
    position: fixed;
    top: 0;
    left: 0;
    z-index: 30; /* above .showcase-images (20), below .showcase-cursor (58) */
    pointer-events: none;
  }
  .showcase-active-name .showcase-item{
    position: absolute;
    top: 0;
    left: 0;
    /* own fix: unlike a real row item, this clone isn't a descendant
       of .showcase-row-track, so it doesn't inherit that element's
       white-space:nowrap — without this, its shrink-to-fit width
       (computed against .showcase-active-name's own zero-width fixed
       box, since its only children are taken out of flow by their
       own position:absolute) comes out too narrow and the name wraps
       to a second line instead of staying on one. */
    white-space: nowrap;
  }
  .showcase-active-name__base.showcase-item{
    color: var(--color-near-black);
    opacity: 0;
    transition: opacity .15s ease-out;
  }
  .showcase-active-name.is-active .showcase-active-name__base.showcase-item{ opacity: 1; }
  .showcase-active-name__clip.showcase-item{
    color: var(--color-cream);
    opacity: 0;
    /* mirrors .showcase-image's own delayed-hide / fade-in exactly,
       so the white split only appears once (and only for as long as)
       the image itself is actually visible */
    transition: opacity 0s var(--showcase-hide-delay, .45s);
  }
  .showcase-active-name.is-active .showcase-active-name__clip.showcase-item{
    opacity: 1;
    transition: opacity var(--showcase-fade-in, .4s) cubic-bezier(.215,.61,.355,1);
  }
  .reduce-motion .showcase-active-name__base,
  .reduce-motion .showcase-active-name__clip{
    transition: none !important;
  }

  /* ---- View cursor — CONFIRMED size/scale/timing, own flame-orange color ---- */
  .showcase-cursor{
    position: fixed;
    top: 0;
    left: 0;
    width: 0;
    height: 0;
    pointer-events: none;
    z-index: 58; /* below the flame cursor (60) — flame hand stays the primary visible cursor */
    opacity: 0;
    transition: opacity .3s ease-out;
  }
  .showcase-cursor.is-active{ opacity: 1; }
  .showcase-cursor__dot{
    position: absolute;
    width: 8px; /* CONFIRMED base size */
    height: 8px;
    margin: -4px 0 0 -4px; /* centers on the tracked point, CONFIRMED offset convention */
    border-radius: 50%;
    background: var(--color-orange);
    scale: 1;
    transition: scale .3s cubic-bezier(.215,.61,.355,1); /* CONFIRMED duration/curve family */
  }
  .showcase-cursor.is-active .showcase-cursor__dot{ scale: 10; } /* CONFIRMED */
  .showcase-cursor__text{
    position: absolute;
    left: 0;
    top: 0;
    transform: translate(-50%, -50%);
    color: var(--color-cream);
    font-family: 'Aspekta Variable', 'DM Sans', sans-serif;
    font-weight: 650;
    font-size: 11px;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    white-space: nowrap;
  }
  /* touch / no fine pointer, and prefers-reduced-motion: never
     shown — matches this task's explicit "no mouse-following
     motion" rule, same convention as .works-cursor above. */
  .reduce-motion .showcase-cursor{ display: none; }

  /* ============================================================
     DOODLES — hand-drawn GIF accents, own build, own pattern
     (see /research/mad/DOODLES.md for how mad-creative-agency.com
     uses theirs; not their assets or code, just the documented
     pattern). Placement/sizing driven entirely by DOODLES_CONFIG in
     the script below; these rules are just the shared mechanics:
     never interactive (pointer-events:none, decorative alt=""), and
     never part of layout flow at their mount point (position:
     absolute against a positioned ancestor) so they can sit next to
     content without pushing it around or being pushed themselves.
  ============================================================ */
  .doodle{
    position: absolute;
    display: block;
    height: auto;
    aspect-ratio: 1 / 1; /* every doodle source is a 600x600 square — fixes rendered height from CSS width alone, so position math never has to wait on image-load to know it */
    pointer-events: none;
    user-select: none;
  }
  /* works section: the heading + its doodle share one flex row
     instead of the doodle being measured/positioned by JS — simpler
     and correct at any heading width/wrap without extra code. */
  .works-heading-row{
    display: flex;
    align-items: center;
    gap: 0.35em;
    font-size: clamp(40px, 6.4vw, 104px); /* same value .works-heading used to carry directly — now shared with the row so the doodle's em-based width (a sibling, not a descendant, of the heading) tracks it too */
  }
  .works-heading-row .works-heading{ margin: 0 0 7vh; } /* moved from .works-heading itself so the row, not the heading alone, carries it */
  .works-heading-row .doodle{
    position: static; /* inline with the heading here, not absolutely placed */
    flex: none;
  }
  /* ---- per-placement size (position for the anchored/corner ones is
     computed in JS — see DOODLES_CONFIG — but size is CSS so each one
     scales with the text it sits next to, per this task's own rule) --- */
  #doodleStatementSparkler{
    width: clamp(26px, calc(var(--statement-fs, 80px) * 0.4), 60px); /* tracks the statement's own fluid font-size custom property */
    z-index: 2; /* above the grid background, below the statement text's z-index:3 so it never sits on top of the highlighted phrase itself */
  }
  #doodleWorksPaperAirplane{
    width: 0.5em; /* em here inherits .works-heading's own clamp()'d font-size, so it scales with the heading at any breakpoint */
  }
  #doodleShowcaseStickyNote{
    width: 56px;
    top: 14px;
    left: 14px;
    z-index: 15; /* above .showcase-rows (10); sits in the section's own top-left clear margin above the row stack, never over row text or the image-reveal area */
  }
  @media (max-width: 680px){
    #doodleShowcaseStickyNote{ width: 40px; top: 8px; left: 8px; }
  }

  /* ============================================================
     SERVICES — "What we build with you", own build based on the
     row layout documented from highfulminds.com (see
     /research/highfulminds/REPORT.md — own description of their
     pattern, not their copy/fonts/illustrations). CONFIRMED there:
     plain un-pinned rows in normal document flow, each one a
     12-column grid (number / title+subs / paragraph / illustration),
     separated by a thin top-border divider, with a lot of vertical
     breathing room — no sticky, no pin, no slide, no stacking. The
     only motion is a light once-only fade-up per row on scroll
     (CONFIRMED: 0.6s power2.out, opacity 0->1 + 10% Y offset ->0,
     triggered once at 80% viewport, never reversed) — reproduced
     below with a plain IntersectionObserver, no animation library
     needed for something this simple.

     COLOR — own choice, a near-black background so this section
     reads distinct from the light showcase section directly above it.
     data-theme="dark" (not "light") so the cursor trail keeps its own
     default bright color here instead of switching to the dark color
     reserved for light-background sections — same mechanism as every
     other data-theme switch in this file, just the opposite branch of
     it. Every color lives in SERVICES_CONFIG.colors in the script and
     is applied here as CSS custom properties — swap the whole palette
     in one place, not scattered across these rules. Doodles: this
     section being dark flips the light/dark line variant each one
     auto-selects too (see doodleUrl in the script) — off-white lines
     read correctly here without any extra code.
  ============================================================ */
  .services{
    position: relative;
    background: var(--services-bg, var(--color-near-black)); /* own colors, set from SERVICES_CONFIG.colors in the script — see there to swap the whole palette in one place */
    color: var(--services-text, var(--color-cream));
    padding: 8vh 6vw;
  }
  .services-head{
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 3vw;
    flex-wrap: wrap;
    padding-bottom: 6vh;
  }
  .services-heading{
    font-family: 'Aspekta Variable', 'DM Sans', sans-serif;
    font-weight: 800; /* CONFIRMED reference uses 400 (regular) — kept at 900 for consistency with every other display heading already established in this file */
    text-transform: uppercase;
    line-height: 0.95;
    font-size: clamp(32px, 5vw, 76px); /* CONFIRMED scale (76px) */
    margin: 0;
    opacity: var(--services-text-opacity, 1);
  }
  .services-cta{
    display: inline-flex;
    align-items: center;
    gap: 0.5em;
    font-family: 'Aspekta Variable', 'DM Sans', sans-serif;
    font-weight: 650;
    font-size: clamp(13px, 1vw, 16px); /* CONFIRMED scale (16px) */
    text-transform: uppercase;
    letter-spacing: 0.02em;
    color: var(--services-text, var(--color-cream));
    opacity: var(--services-text-opacity, 1);
    text-decoration: none;
    white-space: nowrap;
    transition: color .2s ease-out; /* own choice: a color hover per this task's own style spec, not the reference's padding-nudge */
  }
  .services-cta:hover{ color: var(--services-cta-hover, var(--color-orange)); } /* only recolors the text span below — the arrow has its own fixed color regardless of hover */
  .services-cta__arrow{ display: inline-block; color: var(--services-cta-arrow, var(--color-orange)); }

  .services-stage{ position: relative; }

  /* ---- one row — CONFIRMED: 12-col grid, number/title+subs/
     paragraph/illustration, 1px divider top border, 32px/128px
     top/bottom padding (own choice: scaled down a little from the
     reference's exact px so it reads generously without ballooning
     the section's total scroll length across 6 rows instead of
     their 4). fade-up class is added once by IntersectionObserver
     (see the script) and removed entirely under reduced-motion. ---- */
  .service-row{
    position: relative; /* positioning context for the full-bleed ::before hover backdrop below */
    display: grid;
    grid-template-columns: repeat(12, 1fr);
    column-gap: 16px;
    align-items: start;
    padding: 28px 0 96px;
    border-top: 1px solid var(--services-divider, color-mix(in srgb, var(--color-cream) 15%, transparent));
    color: inherit; /* own reset — the row is a link now (whole-row click target), not just the text inside it */
    text-decoration: none;
    cursor: pointer;
    opacity: 0;
    transform: translateY(24px);
    transition: opacity .6s cubic-bezier(.25,.1,.25,1), transform .6s cubic-bezier(.25,.1,.25,1);
  }
  .service-row.is-visible{ opacity: 1; transform: translateY(0); }
  .service-row:first-child{ border-top: none; } /* CONFIRMED-adjacent: the section itself sits right below the heading's own divider (padding-bottom above), no need for a second line directly under it */
  /* ---- full-bleed hover backdrop — the row's own grid stays inside
     the section's 6vw side padding (so text/doodle positions don't
     move), but the hover/focus tint should read as a full-width band
     edge-to-edge, ignoring that padding. Classic full-bleed-within-a-
     padded-container trick: a ::before stretched to 100vw and
     re-centered on the viewport with the 50vw/-50vw offset pair, sat
     behind the row's own content (z-index -1) since .service-row is
     itself the positioning context (position: relative above). ---- */
  .service-row::before{
    content: "";
    position: absolute;
    top: 0;
    bottom: 0;
    left: 50%;
    width: 100vw;
    margin-left: -50vw;
    z-index: -1;
    background: transparent;
    transition: background-color .3s ease;
    pointer-events: none;
  }
  .service-row:hover::before,
  .service-row:focus-visible::before{
    background: color-mix(in srgb, var(--color-cream) 6%, transparent); /* whole row is clickable now — a faint lightening against the near-black section background reads as hover/focus feedback without needing a separate button */
  }
  .service-row:focus-visible{
    outline: 2px solid var(--color-orange);
    outline-offset: -2px;
  }

  .service-row__number{
    grid-column: 1 / 2;
    grid-row: 1;
    font-family: 'Aspekta Variable', 'DM Sans', sans-serif;
    font-weight: 650;
    font-size: clamp(14px, 1vw, 17px); /* CONFIRMED scale (16px) — hierarchy here is color, not size, matching the reference */
    color: var(--services-number, var(--color-orange)); /* own choice: a small orange accent, breaking from the reference's own same-color-as-title number */
  }
  .service-row__titlecol{
    grid-column: 2 / 4; /* CONFIRMED ~1.6/12 columns */
    grid-row: 1;
  }
  .service-row__title{
    font-family: 'Aspekta Variable', 'DM Sans', sans-serif;
    font-weight: 800; /* CONFIRMED reference: 400, same size as the number — kept bold/uppercase for legibility at this size in our own type system */
    text-transform: uppercase;
    font-size: clamp(14px, 1vw, 17px); /* CONFIRMED scale (16px), same as the number */
    line-height: 1.3;
    margin: 0 0 0.6em;
  }
  .service-row__subs{
    list-style: none;
    margin: 0;
    padding: 0;
    font-family: 'Aspekta Variable', 'DM Sans', sans-serif;
    font-weight: 500;
    font-size: clamp(14px, 1vw, 17px); /* CONFIRMED: same 16px as the title — hierarchy is color only */
    line-height: 1.6; /* CONFIRMED: stacked one per line */
    color: var(--services-subs, color-mix(in srgb, var(--color-cream) 55%, transparent)); /* CONFIRMED: this is the only rule touching .service-row__subs — no other selector overrides it (checked) */
  }
  .service-row__para{
    grid-column: 5 / 10; /* CONFIRMED ~col5-10.9, roughly half the row */
    grid-row: 1;
    font-family: 'Aspekta Variable', 'DM Sans', sans-serif;
    font-weight: 400;
    font-size: clamp(20px, 2vw, 34px); /* CONFIRMED scale (34px) */
    line-height: 1.18; /* CONFIRMED (40px/34px) */
    margin: 0;
  }
  /* ---- animated text fill — each word starts dim and lights up to
     full color as the row's own scroll-fill progress reaches it (see
     the FILL block in the script below); color itself lives on the
     word, not the paragraph, since the fill state is per-word. ---- */
  .service-row__word{
    color: color-mix(in srgb, currentColor 45%, transparent); /* dim state — currentColor still resolves through .service-row__para to var(--services-text) */
    transition: color .35s ease; /* own choice: a soft crossfade rather than a hard cut, so the fill reads as continuous even though it's driven by discrete per-word class toggles */
  }
  .service-row__word.is-filled{ color: currentColor; }
  .service-row__media{
    grid-column: 10 / 13; /* right side, clear of the paragraph's own 5/10 track — CSS grid never lets the two overlap */
    grid-row: 1;
    margin-top: -60px; /* nudges the doodle up further, matching the reference's alignment — crosses above the row's own 28px padding-top/divider line, but that's empty space (the previous row's own 96px bottom padding), so nothing collides */
    height: 422px; /* ~1.6x the original 264px, so the doodle reads clearly at a glance */
    display: flex;
    align-items: flex-start; /* top-align the doodle with the paragraph's top instead of centering it in the row */
    justify-content: center;
  }
  .service-row__media .doodle{
    position: static; /* own fix: overrides the shared .doodle rule's position:absolute — without this it positions against some further-up ancestor instead of centering in this flex panel */
    width: 100%;
    max-height: 100%;
    object-fit: contain;
    pointer-events: none;
  }

  @media (max-width: 991px){
    /* CONFIRMED: the reference's whole grid (this row included) drops
       from 12 to 4 columns at this breakpoint — own choice: let every
       part of the row go full-width and stack (number, title+subs,
       paragraph, illustration, in that document order), since the
       exact reference stacking order wasn't pixel-confirmed (GUESS
       in REPORT.md §4) and this is the most natural reading order. */
    .services-heading{ font-size: clamp(28px, 8vw, 37px); } /* CONFIRMED scale (37px) */
    .service-row{
      grid-template-columns: 1fr;
      row-gap: 2.5vh;
      padding: 24px 0 48px; /* CONFIRMED scale (64px desktop-equivalent, tightened a touch for 6 rows) */
    }
    .service-row__number, .service-row__titlecol, .service-row__para, .service-row__media{
      grid-column: 1 / -1;
      grid-row: auto;
    }
    .service-row__media{ height: 40vw; max-height: 220px; margin-top: -16px; justify-content: flex-start; align-items: center; } /* own override, smaller than desktop's -60px — mobile stacks the doodle directly below the paragraph in normal flow, so too big a pull here would overlap the paragraph text */
    .service-row__media .doodle{ width: auto; height: 100%; }
  }
  @media (max-width: 479px){
    .services-cta{ display: none; } /* CONFIRMED: hidden entirely at this breakpoint */
    .service-row__para{ font-size: clamp(20px, 8vw, 26px); } /* CONFIRMED-scale fluid clamp */
  }

  /* ---- prefers-reduced-motion: rows are simply visible, no fade/
     translate at all — matches this task's own explicit rule. ---- */
  @media (prefers-reduced-motion: reduce){
    .service-row{ opacity: 1; transform: none; transition: none; }
  }

  /* ---- row View cursor — same follow-the-mouse pattern as the
     showcase section's own .showcase-cursor (own duplicate rather
     than a shared rule, matching this file's self-contained-per-
     section convention): instant position tracking (JS), eased
     scale/opacity on enter/exit (CSS). Desktop/fine-pointer only,
     off entirely under reduced motion — see the script below. ---- */
  .services-row-cursor{
    position: fixed;
    top: 0;
    left: 0;
    width: 0;
    height: 0;
    pointer-events: none;
    z-index: 58;
    opacity: 0;
    transition: opacity .3s ease-out;
  }
  .services-row-cursor.is-active{ opacity: 1; }
  .services-row-cursor__dot{
    position: absolute;
    width: 8px;
    height: 8px;
    margin: -4px 0 0 -4px;
    border-radius: 50%;
    background: var(--color-orange);
    scale: 1;
    transition: scale .3s cubic-bezier(.215,.61,.355,1);
  }
  .services-row-cursor.is-active .services-row-cursor__dot{ scale: 10; }
  .services-row-cursor__text{
    position: absolute;
    left: 0;
    top: 0;
    transform: translate(-50%, -50%);
    color: var(--color-cream);
    font-family: 'Aspekta Variable', 'DM Sans', sans-serif;
    font-weight: 650;
    font-size: 11px;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    white-space: nowrap;
  }
  .services.has-row-cursor .service-row:hover{ cursor: none; } /* the View More bubble takes over as the visible cursor, same as the flame-hand/showcase-cursor handoff elsewhere */
  .reduce-motion .services-row-cursor{ display: none; }

  /* ============================================================
     EVERY SITUATION — own build, several passes deep. Card sizing
     (~3-4 visible, full-height portrait columns) CONFIRMED off
     shift5.io/compliance/'s feature-card row; the current hover — a
     background swap behind the icon plus a rounded pill sliding up
     from the bottom (name, desc, circular arrow) — is modeled on
     luminaireauthentik.com's product-card hover instead (both read
     live via DOM/CSSOM, not their code/fonts/images). Neither
     reference's card row is scroll-linked — the scroll-driven
     horizontal advance below is this task's own build. The headline/
     subhead layout (huge light-weight wide-tracked headline, small
     narrow subhead pinned to the far right, no eyebrow label) is
     this task's own explicit spec, not copied from any reference.

     One live headline (see .situations-headline). own explicit
     request: the wk.com/about/-style type effect (see the HEADLINE
     comment further down in the script) is reserved for the default
     headline only now — card-to-card swaps via hover/scroll go back
     to this section's own original per-letter dim-to-lit ease (see
     .situations-headline__text .ltr below and setHeadlineLetters() in
     the script). Card copy lives in SITUATIONS_CONFIG in the script
     below.

     data-theme="light": cream background, near-black text, matching
     this task's own explicit spec — same mechanism as every other
     data-theme switch in this file.
  ============================================================ */
  .situations{
    position: relative;
    height: 100vh; /* JS inflates this to 100vh + extra scroll room once .is-pinned is added — see the SCROLL block in the script. Plain 100vh is also the whole story under reduced motion / narrower than MOTION_MIN_WIDTH, where that class never gets added. */
    color: var(--color-near-black);
  }
  .situations-sticky{
    position: relative;
    height: 100vh; /* NOT 100% — .situations is the tall scroll-spacer once pinned (see its own comment above), so a percentage here would resolve against that huge height instead of the viewport */
    display: flex;
    flex-direction: column;
    background: var(--color-cream);
    padding: 8vh 6vw;
    /* ---- INTRO — own explicit request: the section now opens on a
       full-screen moment (huge headline + the seal badge, cards
       hidden) before shrinking down into today's compact layout as
       the user keeps scrolling. 1 = fully shrunk/compact (today's
       look, and the default here so anything that never gets driven
       by the SCROLL script below — reduced motion, narrower than
       MOTION_MIN_WIDTH — just renders the compact layout it always
       has). The SCROLL script sets this every frame during the new
       intro phase (see measure()/applyScroll() below), counting DOWN
       from 1 to 0 as the user scrolls further INTO the intro range —
       0 is the full-screen open. .situations-headline's font-size and
       .situations-copy's margin-top below read this directly;
       .situations-seal has its own matching scale(). --situations-
       cards-opacity is a separate property (not just 1 - progress)
       because the cards fade in over only the LAST portion of the
       intro range (see setIntroProgress()) — the headline settles
       into place first, then the cards row appears, rather than
       everything happening at once. ---- */
    --situations-intro-progress: 1;
    --situations-cards-opacity: 1;
  }
  /* ---- pin — while motion is enabled (fine pointer, >=MOTION_MIN_
     WIDTH, no reduced-motion), the script toggles this inner
     element's position directly (fixed while scrolling through the
     extra height .situations gets, then absolute once that range is
     spent) rather than using CSS position: sticky — this file sets
     `overflow-x: hidden` on html/body (see near the top of this
     stylesheet, kept for other sections' full-bleed tricks), and per
     spec that silently promotes overflow-y to auto too, which makes
     sticky lock onto body's own (non-scrolling) box instead of the
     real viewport. fixed/absolute aren't affected by an ancestor's
     overflow, only by transform/filter/contain creating a new
     containing block — none of which exist up this chain — so this
     is the robust option here. Still no scroll-jacking either way:
     native scroll is never prevented, this only repositions an
     element in response to it. ---- */
  /* ---- typewriter headline — own explicit request: replaces the
     crossfade-stack-of-prebuilt-variants system this used to be with
     the same type/delete effect wk.com/about/ uses for its own
     rotating headline, reverse-engineered directly from their
     bundled TypeIt.js config (read from their actual JS, not
     guessed — see the matching constants in the script below). One
     live headline element now, not one per card — the script types
     and deletes its own text content instead of crossfading between
     separate DOM nodes, so there's no longer a multi-child stack to
     grid-area on top of itself. grid-template-columns still reserves
     the same second-column width this always has, for the same
     reason: keeps the headline at its own narrower wrap instead of
     running the full section width, leaving the corner free for
     .situations-seal to float over without affecting layout height —
     see .situations-seal's own comment. ---- */
  .situations-copy{
    position: relative;
    display: grid;
    grid-template-columns: 1fr minmax(180px, 260px);
    align-items: start;
    gap: 40px;
    margin-bottom: 56px;
    flex-shrink: 0;
    /* INTRO — own explicit request: pushes the whole copy block
       (just the headline — .situations-seal is a separate sibling
       with its own scale()) further down the sticky's flex column as
       --situations-intro-progress counts down toward 0, so a huge
       headline reads as roughly screen-centered rather than pinned
       to the top edge under the 8vh padding. Plain margin (not
       transform) on purpose — .situations-cards-viewport below is
       flex:1 with min-height:0, so it just absorbs the shrinking
       remainder rather than needing anything explicit here to make
       room. */
    margin-top: calc(14vh * (1 - var(--situations-intro-progress, 1)));
  }
  .situations-headline{
    grid-column: 1;
    font-family: 'Aspekta Variable', 'DM Sans', sans-serif;
    font-weight: 400; /* CONFIRMED: light/regular, not this file's usual bold display weight */
    line-height: 0.95;
    letter-spacing: 0.02em; /* CONFIRMED: wide tracking */
    /* INTRO — own explicit request: grows toward a full-screen size
       as --situations-intro-progress counts down toward 0 (see
       .situations-sticky's own comment for what drives it). A real
       font-size change (not transform: scale()) on purpose — at 0
       this text needs to REWRAP into fewer, bigger lines within
       .situations-copy's own grid-column-1 width, not visually
       overflow past the viewport edge the way scaling an
       already-wrapped block would. Each clamp() argument is its own
       calc() blending today's compact value (at progress 1) up to a
       bigger one (at progress 0) — clamp()'s min/preferred/max can
       each be an independent expression like this. */
    font-size: clamp(
      calc(36px + 20px * (1 - var(--situations-intro-progress, 1))),
      calc(5.5vw + 3.5vw * (1 - var(--situations-intro-progress, 1))),
      calc(84px + 56px * (1 - var(--situations-intro-progress, 1)))
    );
    max-width: none;
    margin: 0;
    color: var(--color-near-black);
    /* own fix, per explicit report: reserve room for 2 lines ALWAYS
       (not just when the current headline happens to wrap to 2) —
       otherwise a 1-line headline sits shorter than a 2-line one and
       everything below (the cards viewport) shifts up/down as the
       typewriter cycles between short and long headlines. em-based so
       it tracks the clamp()'d font-size at every breakpoint. */
    min-height: calc(0.95em * 2);
  }
  /* ---- cursor — own choice, matching wk.com's own TypeIt defaults
     read from their bundle: a "|" character blinking on a 1000ms
     cycle (their own cursorSpeed). own fix, per explicit request:
     card-to-card swaps no longer type at all (see the .ltr ease
     below), so the cursor now only shows while .situations-headline
     carries .is-typewriter — the default headline only, toggled in
     showVariant() in the script. Gated to the same motion query the
     rest of this file uses for anything purely decorative — under
     reduced motion the cursor just sits static (no blink), matching
     setHeadlineText() below also skipping the type animation entirely
     there and swapping text instantly. ---- */
  .situations-headline__cursor{
    display: none;
    margin-left: 0.03em;
  }
  .situations-headline.is-typewriter .situations-headline__cursor{
    display: inline-block;
  }
  @media (prefers-reduced-motion: no-preference){
    .situations-headline__cursor{ animation: situationsCursorBlink 1s step-end infinite; }
  }
  @keyframes situationsCursorBlink{ 50%{ opacity: 0; } }
  /* ---- letter ease — own explicit request: reverts card-to-card
     headline swaps to this section's original per-letter dim-to-lit
     ease (the typewriter above is reserved for the default headline
     only now). One span per word (so a line only ever breaks between
     words, same convention the statement section's own
     buildStatementText() uses) and, inside each word, one span per
     letter — .ltr starts dim, .is-lit is added after a forced reflow
     in setHeadlineLetters() below so the same class can be removed
     and re-added to replay the ease on every call rather than only
     the first. transition-delay is set once per letter, from its own
     position among ALL the letters in the headline (spaces don't
     consume a position — see buildHeadlineLetters() in the script),
     so later letters ease in later with no per-frame JS driving it. */
  .situations-headline__text .word{
    display: inline-block;
  }
  .situations-headline__text .ltr{
    display: inline-block;
    color: color-mix(in srgb, var(--color-near-black) 18%, transparent);
  }
  @media (prefers-reduced-motion: no-preference){
    .situations-headline__text .ltr{
      transition: color 0.45s ease-out;
    }
  }
  .situations-headline__text .ltr.is-lit{
    color: var(--color-near-black);
  }
  @media (max-width: 900px){
    .situations-copy{ grid-template-columns: 1fr; gap: 16px; }
    .situations-headline{ font-size: clamp(28px, 9vw, 48px); }
  }

  /* ============================================================
     SEAL — circular rotating badge, own explicit request: "A
     SOLUTION FOR EVERY SITUATION" repeated around a ring with dot
     separators, our own flame-hand cursor artwork reused (unmoving)
     at the center. Sits top-right, next to the headline, in the
     same corner the subhead used to occupy before it was removed
     (see .situations-copy's own comment) — sized up significantly
     (was 84px) per explicit follow-up now that it's the only thing
     in that corner.

     Own fix, per explicit correction: this was briefly a normal flex
     item stacked above .situations-copy, which meant growing it grew
     the whole flex column and shrank the card row below — exactly
     the "cards got shorter, top section got taller" regression
     reported. position:absolute takes it out of .situations-sticky's
     flex flow entirely (anchored to that flex container's own
     padding box, which is the same "relative" it already was, just
     no longer flex-participating) so its size can never affect the
     copy block's or the card row's own height — it only ever
     overlaps the corner space the grid's second column already
     reserves, visually reading as sitting right next to the
     headline, same as the subhead once did. ---- */
  .situations-seal{
    position: absolute;
    /* own fix, per explicit correction: an absolutely positioned
       child's inset is measured from its containing block's padding
       EDGE, not its content edge — so top:0;right:0 ignored
       .situations-sticky's own 8vh 6vw padding entirely and sat
       flush with the true section edge instead of lining up with
       everything else in it (the headline, the card row's own right
       border). right: 6vw still matches that padding for the same
       reason; top is now 5vh (own choice, per explicit follow-up,
       down from matching the padding at 8vh) to sit a little higher,
       closer to the section's own top edge than the headline row
       below it starts. */
    top: 5vh;
    right: 6vw;
    width: 200px;
    height: 200px;
    /* INTRO — own explicit request: grows from its own top-right
       corner (transform-origin below matches the top:/right: anchor
       above, so it grows further INTO that corner rather than
       drifting) as --situations-intro-progress counts down toward 0.
       transform, not a width/height change, since this is already
       taken out of flow entirely (see the comment above) — nothing
       else needs to reflow around it either way. */
    transform-origin: top right;
    transform: scale(calc(1 + 0.5 * (1 - var(--situations-intro-progress, 1))));
  }
  .situations-seal__ring{
    display: block;
    width: 100%;
    height: 100%;
  }
  /* own fix, per explicit report ("make this our font"): swapped off
     the generic monospace to this file's own type system — 'Aspekta
     Variable' ('DM Sans' as its own fallback — see the @font-face
     block near the top of this stylesheet) at weight 650, the same
     small-uppercase-label weight this file already uses elsewhere
     (e.g. .situation-card__footer-label). Letter-spacing halved from
     the mono value (0.12em -> 0.06em) for the same reason that swap
     halved its own: a monospace font's uniform character widths
     carry heavy tracking gracefully, but the same value on a
     proportional font like Aspekta reads as loose/uneven. */
  .situations-seal__ring text{
    font-family: 'Aspekta Variable', 'DM Sans', sans-serif;
    font-weight: 650;
    font-size: 8.6px;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    fill: color-mix(in srgb, var(--color-near-black) 65%, transparent);
  }
  .situations-seal__icon{
    position: absolute;
    left: 50%;
    top: 50%;
    transform: translate(-50%, -50%);
    width: 84px;
    height: 84px;
    color: var(--color-near-black);
    pointer-events: none;
  }
  /* overrides the flame cursor's own fixed 40px sizing (see the
     FLAME CURSOR block near the top of this stylesheet) for this
     one static, much smaller reuse — same specificity trick the
     rest of this file uses (an extra ancestor class), no !important
     needed. */
  .situations-seal__icon .flame-cursor-inner,
  .situations-seal__icon .flame-cursor-inner svg{
    width: 84px;
    height: 84px;
  }
  @media (prefers-reduced-motion: no-preference){
    .situations-seal__ring{ animation: situationsSealSpin 18s linear infinite; }
  }
  @keyframes situationsSealSpin{ to{ transform: rotate(360deg); } }
  /* own fix, per explicit follow-up: the "01 / 08" counter that used
     to sit here (a small wayfinder label above the card row) has
     been removed entirely. */

  /* ---- card viewport — clips the track below to exactly the space
     left after the label/headline block above, full section width.
     The track inside is wider than this (all 8 cards laid out in a
     row) and gets translated horizontally by the script; this is
     just the window that width gets cropped to. ---- */
  .situations-cards-viewport{
    position: relative;
    flex: 1 1 auto;
    min-height: 0; /* lets it shrink below content size on very short viewports instead of forcing .situations past 100vh */
    overflow: hidden;
    border-top: 1px solid color-mix(in srgb, var(--color-near-black) 15%, transparent);
  }
  /* INTRO — own explicit request: the cards stay hidden through the
     full-screen open, fading in only once the headline/seal are
     mostly done shrinking (see setIntroProgress() in the SCROLL
     script, which is also what toggles pointer-events off directly
     on this element so a stray hover can't fire a card's headline
     swap while it's still invisible). Scoped under .is-pinned only —
     the mobile/reduced-motion fallback below (native horizontal
     scroll, no pin) never gets this custom property updated, so it
     must never affect that layout. */
  .situations.is-pinned .situations-cards-viewport{
    opacity: var(--situations-cards-opacity, 1);
  }
  .situations-cards{
    display: flex;
    height: 100%;
    will-change: transform;
  }
  /* ---- one card — full-height portrait column, big icon filling the
     upper area (own choice on icon size, direction CONFIRMED off
     shift5.io: theirs fills most of the card's upper area too).
     flex-basis is vw-based (not %) so it sizes against the viewport
     rather than the track's own auto width, which would be circular;
     the numbers land on ~3-4 visible cards across the whole motion-
     enabled range (see MOTION_MIN_WIDTH in the script).

     Hover modeled on luminaireauthentik.com's product-card hover
     (read live via DOM/CSSOM, not their code): the card's own
     background swaps to a contrasting color behind the icon, and a
     rounded pill — name, description, circular arrow button — slides
     up from below the card's bottom edge. overflow:hidden on the
     card is what clips the pill to invisible at its resting
     translateY(150%); own choice of exact percentage/timing, not
     copied 1:1 off the reference. Gated as ONE block behind real
     hover + fine pointer + no-reduced-motion (not just the
     transitions) so touch and reduced-motion users get neither the
     fill nor the pill at all, per this task's own explicit rule —
     stricter than this section's very first hover pass, which only
     gated the transition and still let reduced-motion users see an
     instant color jump. ---- */
  .situation-card{
    position: relative;
    overflow: hidden; /* clips the pill's off-screen resting position */
    /* Own fix, per explicit request: 4 cards visible per row again,
       not 5. .situations-sticky's own 6vw side padding leaves 88vw of
       viewport width for the card row, so 22vw per card (88/22=4)
       keeps exactly ~4 in view at any width the fluid middle value is
       actually driving — the old 26vw undershot that (88/26≈3.4) at
       typical widths, and its 400px cap didn't scale with the
       viewport at all, so wide/large screens kept fitting a 5th card
       in as the available width grew past it. Cap raised in step,
       widening the range this stays viewport-proportional across
       before an ultra-wide monitor could tip it to 5 again. ---- */
    flex: 0 0 clamp(260px, 22vw, 460px);
    display: flex;
    flex-direction: column;
    align-items: stretch; /* icon-zone needs to bleed full card width for its hover fill to read as "the card's background behind the icon" */
    text-align: left;
    padding: 0; /* moved to .situation-card__icon-zone / __footer individually, below */
    background: var(--color-cream);
    border: none;
    border-right: 1px solid color-mix(in srgb, var(--color-near-black) 15%, transparent);
    font: inherit;
    color: var(--color-near-black); /* icon/text color — stays constant; it already contrasts fine on both cream and the icon-zone's orange, so it never needs to flip */
    cursor: pointer;
    -webkit-appearance: none;
    appearance: none;
  }
  .situation-card:focus-visible{ outline: 2px solid var(--color-near-black); outline-offset: -2px; } /* a plain focus ring isn't "animation" or "fill" — kept even under reduced motion, unlike the block below */
  /* ---- icon zone — own choice: a full-width flex region wrapping
     just the icon, separate from the footer below, so the hover fill
     ("Instead of filling the whole card... behind the icon") only
     colors this upper area and never touches the footer/pill's own
     ground. ---- */
  .situation-card__icon-zone{
    flex: 1 1 auto;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 40px 28px 24px;
  }
  .situation-card__icon{
    width: clamp(128px, 18vw, 256px);
    height: clamp(128px, 18vw, 256px);
    object-fit: contain;
    display: block;
  }
  /* ---- footer — always-visible name + short description (own
     choice: kept from this section's original spec so touch and
     reduced-motion users, who never get the hover-only pill below,
     still have something to read). Fades out on hover as the pill
     slides up to occupy roughly the same area. ---- */
  .situation-card__footer{
    display: flex;
    flex-direction: column;
    gap: 4px;
    padding: 0 28px 32px;
  }
  /* own fix, per explicit report ("generic monospace that looks
     default/templated"): swapped to this file's own type system —
     'Aspekta Variable' (the site's one loaded display/body typeface,
     'DM Sans' as its own fallback — see the @font-face block near
     the top of this stylesheet) at weight 650, the same small-
     uppercase-label weight already established elsewhere in this
     file (e.g. .showcase-cursor, .services-cta). Letter-spacing
     halved from the mono values (0.06em/0.04em -> 0.03em/0.02em):
     a monospace font's uniform character widths carry heavy tracking
     gracefully, but the same values on a proportional font like
     Aspekta read as loose/uneven rather than crisp. */
  .situation-card__footer-label{
    font-family: 'Aspekta Variable', 'DM Sans', sans-serif;
    font-weight: 650;
    font-size: 13px;
    text-transform: uppercase;
    letter-spacing: 0.03em;
  }
  .situation-card__footer-desc{
    font-family: 'Aspekta Variable', 'DM Sans', sans-serif;
    font-weight: 500;
    font-size: 10px;
    text-transform: uppercase;
    letter-spacing: 0.02em;
    opacity: 0.6;
  }
  /* ---- pill — hover-only, richer echo of the footer above (name +
     desc again, plus the circular arrow) modeled on
     luminaireauthentik.com's product-card hover (read live via DOM/
     CSSOM, not their code): a rounded pill slides up from below the
     card's bottom edge. overflow:hidden on the card is what clips it
     to invisible at its resting translateY(150%); own choice of
     exact percentage/timing, not copied 1:1 off the reference. ---- */
  .situation-card__pill{
    position: absolute;
    left: 20px;
    right: 20px;
    bottom: 20px;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    padding: 10px 10px 10px 20px;
    border-radius: 999px;
    background: var(--color-cream); /* CONFIRMED: the pill itself sits on a light ground regardless of the icon-zone's own hover background */
    color: var(--color-near-black);
    transform: translateY(150%);
    opacity: 0;
    pointer-events: none;
  }
  .situation-card__pill-text{
    display: flex;
    flex-direction: column;
    gap: 4px;
    min-width: 0;
  }
  .situation-card__label{
    font-family: 'Aspekta Variable', 'DM Sans', sans-serif;
    font-weight: 650;
    font-size: 13px;
    text-transform: uppercase;
    letter-spacing: 0.03em;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }
  .situation-card__desc{
    font-family: 'Aspekta Variable', 'DM Sans', sans-serif;
    font-weight: 500;
    font-size: 10px;
    text-transform: uppercase;
    letter-spacing: 0.02em;
    opacity: 0.6;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }
  .situation-card__arrow{
    flex-shrink: 0;
    width: 36px;
    height: 36px;
    border-radius: 50%;
    border: 1px solid color-mix(in srgb, var(--color-near-black) 25%, transparent);
    display: flex;
    align-items: center;
    justify-content: center;
  }
  .situation-card__arrow svg{ width: 16px; height: 16px; }
  /* ---- gated as ONE block behind real hover + fine pointer + no-
     reduced-motion (not just the transitions) so touch and reduced-
     motion users get neither the fill nor the pill at all, per this
     task's own explicit rule. ---- */
  @media (hover: hover) and (pointer: fine) and (prefers-reduced-motion: no-preference){
    .situation-card{ transition: color .3s ease-in-out; }
    .situation-card__icon-zone{ transition: background-color .3s ease-in-out; }
    .situation-card__footer{ transition: opacity .25s ease; }
    .situation-card__pill{ transition: transform .4s cubic-bezier(.25,.1,.25,1), opacity .3s ease; }
    /* violet instead of the brand orange — orange behind the icon read
       as a same-color clash against the icons' own orange accent
       strokes, and this section already leans on orange elsewhere.
       Card text (the fading footer) switches to cream so it stays
       legible on violet. own fix, per explicit follow-up (new style-C
       icon set): the icon itself no longer gets a recolor filter here
       — that used to flatten the old single-tone black linework to
       cream via brightness(0)/invert(1)/brightness(0.915), but this
       set's own hover GIF (see /hero-v2/assets/icons and
       setActiveCard() in the SCROLL script) already bakes its cream
       linework in directly, alongside an orange fill that filter
       would have flattened to white too. No CSS recolor needed: the
       PNG/GIF swap alone carries both the line-color change and the
       kept-orange fill. The pill is untouched either way — it sets
       its own color directly, not inherited from .situation-card.
       Own fix, per explicit request: the fill used to land only on
       .situation-card__icon-zone (the upper flex region), which
       stopped short of the footer area the pill sits in — so the
       pill's own bottom portion sat against the card's plain resting
       cream, the same color as the pill itself, and effectively
       vanished there (read as "cut off"/"invisible", not actually
       clipped). Moved to the card as a whole: icon-zone and footer
       have no background of their own, so violet now shows through
       both and the pill reads as a cream pill on a violet card top to
       bottom, with violet visible below and around it the whole time
       it's in view.
       Own fix, per explicit follow-up: the fill/icon-invert/footer-
       fade/pill-reveal used to be painted directly by :hover and
       :focus-visible here — a SEPARATE visual authority from the
       .is-current class below (driven by setActiveCard(), scroll AND
       mouse alike). Two independent sources meant two cards really
       could end up painted "active" at once — a real hover sustained
       on one card by a motionless pointer, plus .is-current landing
       on a different card from the scroll-driven advance — a bug a
       screenshot confirmed. Removed entirely: :hover/:focus-visible
       paint nothing of their own now, and the mouseenter/mouseleave/
       focus/blur listeners below call setActiveCard() the same as
       scroll does, so .is-current is the ONLY thing that ever paints
       this fill — one card, always, never two. ---- */
  }

  /* ---- .is-current — own fix, per explicit follow-up: the ONLY
     source of the card-active fill/footer-fade/pill-reveal (see the
     removal note above) — every interaction that can make a card
     "active," scroll (see setActiveCard() in the SCROLL script below)
     and real mouse/keyboard hover alike, goes through the same
     setActiveCard() call, which always resets every OTHER card's
     class in the same pass. That single shared gate is what
     guarantees at most one card is ever current, regardless of which
     interaction triggered it last. NOT gated to the transition
     block's hover-capable media query above: motionEnabled (width +
     reduced-motion only, see the script) is what gates whether this
     class ever gets toggled at all, so a wide touch-only device still
     gets the instant (untransitioned) end state instead of nothing.
     own fix, per explicit follow-up (new style-C icon set): no longer
     also swaps a .situation-card__icon filter here — see the media
     query above for why that's gone. ---- */
  .situation-card.is-current{ background: var(--color-violet); color: var(--color-cream); }
  .situation-card.is-current .situation-card__footer{ opacity: 0; }
  .situation-card.is-current .situation-card__pill{
    transform: translateY(0);
    opacity: 1;
    pointer-events: auto;
  }

  /* ---- fallback — reduced motion, or narrower than
     MOTION_MIN_WIDTH: the script never adds .is-pinned or the extra
     scroll height, so .situations stays a plain 100vh block and this
     becomes a native horizontally-scrollable, snap-to-card strip
     instead of a scroll-pinned one — every card stays reachable
     (swipe/trackpad/arrow-scroll) with no JS driving it and no
     vertical scroll-hijacking. ---- */
  .situations:not(.is-pinned) .situations-cards-viewport{
    overflow-x: auto;
    scroll-snap-type: x proximity;
    -webkit-overflow-scrolling: touch;
  }
  .situations:not(.is-pinned) .situation-card{ scroll-snap-align: start; }

  @media (max-width: 680px){
    .situation-card{ flex-basis: 82vw; }
    .situation-card__icon-zone{ padding: 32px 24px 20px; }
    .situation-card__footer{ padding: 0 24px 28px; }
  }

  /* ============================================================
     CRAFTED / "PASSIONATE ODDBALLS" — v2 rebuild, own build
     recreating redondo.design's "CRAFTED DESIGN" text-echo effect
     (see /research/redondo/REPORT.md + source/ — values read
     directly off their live SVG assets/CSS/GSAP timeline, not their
     code/fonts/images). Every tunable lives in ECHO_CONFIG below.

     ONE line ("PASSIONATE ODDBALLS"), matching the reference's own
     single-line "CRAFTED DESIGN" structure. Copies 2-6 crop the
     letters from the TOP, keeping the baseline intact (CONFIRMED via
     zoomed screenshot + the exact per-copy SVG heights — REPORT.md
     §1a): each copy is a fixed-height, overflow:hidden box with its
     full-height text bottom-aligned inside it, so only the bottom
     slice shows. A small fixed gap (CONFIRMED 4px/~2.2% of copy 1's
     own height, from .crafted-text's own margin-bottom — REPORT.md
     §1b) sits between every row.

     FONT — DM Sans at weight 500 (Medium): the reference reads as a
     regular/medium-weight grotesque, not the heavy/expanded face this
     file uses for other headlines (Archivo Expanded 900 would be far
     too bold/wide a match here) — DM Sans is this file's own
     grotesque body/UI face and 500 is already loaded.

     COLOR — data-theme="light" so the cursor trail switches to its
     dark color here, same mechanism as every other data-theme switch
     in this file — and the same switch is what makes the corner
     doodle below auto-load its -dark line variant. Colors live in
     ECHO_CONFIG.colors and are applied as CSS custom properties.
  ============================================================ */
  .crafted{
    position: relative;
    background: var(--crafted-bg, var(--color-orange));
    color: var(--crafted-text, var(--color-near-black));
    padding: 18vh 0; /* CONFIRMED reference: zero side padding/margin, full-bleed edge-to-edge (REPORT.md §5) — vertical padding is our own choice, a deliberate bold pause between sections */
    overflow: hidden; /* clips each echo copy's off-screen translateY starting position */
  }
  .crafted-stack{
    display: flex;
    flex-direction: column;
    width: 100%;
  }
  .crafted-copy{
    margin: 0;
    font-family: 'Aspekta Variable', 'DM Sans', sans-serif;
    font-weight: 500;
    text-transform: uppercase;
    line-height: 1;
    white-space: nowrap;
    /* fluid fallback before the script measures+fits the real width
       (see fitText) — script overwrites --crafted-fs once layout/fonts settle. */
    font-size: var(--crafted-fs, clamp(32px, 13vw, 220px));
  }
  /* echo copies 2-6: each is a shorter box (script sets
     --crafted-copy-h from copy 1's own measured height x
     ECHO_CONFIG.cropFractions), overflow:hidden + flex align-items:
     flex-end so the full-height text inside sits bottom-aligned —
     only its bottom slice is visible, top cropped away, baseline
     intact (CONFIRMED crop direction — REPORT.md §1a). A small
     margin-bottom (ECHO_CONFIG.gapFraction of copy 1's height)
     matches the reference's own confirmed 4px/~2.2% row gap
     (REPORT.md §1b). Each starts translated up (own %, from
     ECHO_CONFIG.startY — CONFIRMED reference values) and slides down
     to translateY(0) once the section first scrolls into view;
     duration/delay/easing are set inline by the script from
     ECHO_CONFIG so that object stays the single source of truth. */
  .crafted-copy:not(.crafted-copy--1){
    overflow: hidden;
    height: var(--crafted-copy-h, auto);
    margin-bottom: var(--crafted-gap, 0);
    display: flex;
    align-items: flex-end;
    transform: translateY(var(--crafted-start-y, -100%));
    transition-property: transform;
  }
  .crafted-copy__inner{ display: inline; } /* default inline — so getBoundingClientRect() on it measures the text's own glyph width, not the parent h2's full box width */
  .crafted.is-played .crafted-copy:not(.crafted-copy--1){ transform: translateY(0); }

  /* ---- per-letter hover flip — own build modeled on letude.group's
     giant wordmark (read live: not a true 3D rotateY, just
     transform:scaleX(-1) on each letter around its own center,
     0.75s cubic-bezier(.76,0,.24,1) — the smooth pass through zero
     width during the transition is what reads as a "flip"). Applied
     to every copy, cropped ones included: since each crafted-copy
     clones the SAME letter-span markup (built once below, before
     the clone loop runs) and copies 2-6 are just a shorter
     overflow:hidden window onto that same full-height text, a
     letter flipping in place is naturally clipped to whatever slice
     of it that copy already shows — no special-casing needed. Gated
     to real hover + fine pointer + no reduced-motion in one media
     query rather than a separate reduced-motion override rule, so
     touch and reduced-motion users never get a transition to
     disable in the first place. ---- */
  .crafted-letter{
    display: inline-block;
    transform-origin: center;
  }
  /* Own fix: this used to be a pure CSS :hover transition, which let a
     mouse leaving mid-flip simply reverse from whatever scaleX it
     happened to be at — the reported stuck, half-turned, washed-out
     letter. Now .is-flipping (added/removed by the LETTER FLIP script
     below, never by :hover) plays this keyframe animation once,
     start to finish, with no way to interrupt it mid-transform: flip
     out over the first 30%, hold flipped through the middle 40% (~1s
     of the 2.5s total), flip back over the last 30%. Same easing as
     the old transition, just now a single uninterruptible sequence
     instead of a value CSS can reverse out from under itself. */
  @media (hover: hover) and (pointer: fine) and (prefers-reduced-motion: no-preference){
    .crafted-letter.is-flipping{
      animation: craftedLetterFlip 2.5s cubic-bezier(.76,0,.24,1);
    }
    @keyframes craftedLetterFlip{
      0%   { transform: scaleX(1); }
      30%  { transform: scaleX(-1); }
      70%  { transform: scaleX(-1); }
      100% { transform: scaleX(1); }
    }
  }

  #doodleCraftedBananaPeel{
    width: 64px;
    top: 24px;
    right: 24px;
    z-index: 2; /* above the crafted-stack's own copies */
  }
  @media (max-width: 680px){
    #doodleCraftedBananaPeel{ width: 44px; top: 14px; right: 14px; }
  }

  /* ---- prefers-reduced-motion: final resting state immediately, no
     slide/transition at all (REPORT.md §6: the reference itself has
     no reduced-motion handling — this is our own addition). ---- */
  @media (prefers-reduced-motion: reduce){
    .crafted-copy:not(.crafted-copy--1){ transform: translateY(0); transition: none; }
  }

  /* ============================================================
     TEAM SLIDER — own build recreating the drifting, full-bleed
     photo row from mad-creative-agency.com/about/'s bottom slideshow
     (see /research/mad-slider/REPORT.md + source/ — own description
     of their pattern plus values read directly off their own loaded
     bundle; not their code or photos). Photos are our own team/office
     shots (see /hero-v2/assets/team). Every tunable lives in
     SLIDER_CONFIG in the script below.

     BACKGROUND — light #E9E6DF (overnight revision — was near-black;
     see OVERNIGHT-NOTES.md Part 1), data-theme="light" on the section
     in the HTML so the pixel-trail cursor switches to its dark color
     here, same mechanism every other data-theme switch in this file
     uses. The direction cursor stays flame-orange regardless (its own
     fixed color, not tied to data-theme).

     LAYOUT — CONFIRMED from the reference (REPORT.md §1): full-bleed
     (no side padding), photos edge-to-edge with zero gap, 10px corner
     radius, portrait-ish ~0.88 width/height ratio.

     No flame-hand cursor here — this section is simply not one of
     the zones the script's own flameCursorZones list already covers
     (see that script above), so the system cursor takes over
     automatically the moment the mouse leaves #showcase and the
     custom direction-cursor below takes over automatically the
     moment it enters this section. data-trail="off" (on the section
     in the HTML) suppresses the pixel trail here the same way every
     other opt-out area in this file already works.
  ============================================================ */
  .team-slider{
    position: relative;
    background: var(--team-slider-bg, var(--color-cream));
    padding: 8vh 0;
    overflow: hidden;
  }
  .team-slider:focus-visible{ outline: 2px solid var(--color-orange); outline-offset: -2px; }
  .team-slider-viewport{
    position: relative;
    overflow: hidden;
    width: 100%;
    touch-action: pan-y; /* lets vertical page-scroll pass through on touch while horizontal drag is handled by our own pointer code */
  }
  .team-slider-track{
    display: flex;
    width: max-content;
    gap: var(--team-slide-gap, 12px); /* own choice, a small breathing gap between photos — the reference itself is edge-to-edge (REPORT.md §1) */
    will-change: transform;
  }
  .team-slide{
    flex: none;
    height: var(--team-slide-h, 46vh);
    max-height: 480px;
    min-height: 240px;
    aspect-ratio: var(--team-slide-aspect, 0.877);
  }
  .team-slide img{
    display: block;
    width: 100%;
    height: 100%;
    object-fit: cover;
    border-radius: 10px;
    pointer-events: none; /* dragging/clicking is handled by the track/viewport, not individual images */
    user-select: none;
    -webkit-user-drag: none;
  }

  /* ---- direction cursor: a flame-orange circular badge with a left
     or right arrow, following the pointer with zero added lag (same
     position: fixed + raw-transform convention as .flame-cursor and
     .showcase-cursor above), swapped between left/right arrow
     depending which half of the slider it's currently over. Hidden
     entirely on touch/no-fine-pointer (canHover gates its init in
     the script), and the system/flame cursor stays hidden over the
     slider via plain CSS cursor:none while it's active. ---- */
  .team-slider-cursor{
    position: fixed;
    top: 0;
    left: 0;
    z-index: 59; /* below the flame cursor's own 60 (moot here since flame cursor is inactive in this section, but keeps the stacking order consistent with the rest of the file) */
    width: var(--tsc-size, 68px);
    height: var(--tsc-size, 68px);
    margin: calc(var(--tsc-size, 68px) / -2) 0 0 calc(var(--tsc-size, 68px) / -2);
    border-radius: 50%;
    background: var(--color-orange);
    display: flex;
    align-items: center;
    justify-content: center;
    pointer-events: none;
    opacity: 0;
    transform: scale(0.6);
    transition: opacity .2s ease, transform .2s cubic-bezier(.215,.61,.355,1);
  }
  .team-slider-cursor.is-active{ opacity: 1; transform: scale(1); }
  .team-slider-cursor svg{ width: 38%; height: 38%; display: block; }
  .team-slider.has-cursor, .team-slider.has-cursor .team-slide{ cursor: none; }

  .team-slider-a11y-btn{
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0,0,0,0);
    white-space: nowrap;
    border: 0;
  }

  @media (max-width: 680px){
    .team-slide{ height: 34vh; min-height: 180px; }
  }

  /* ============================================================
     FOOTER v2 — own build, structure modeled on the Neue Montreal
     product-page footer at pangrampangram.com/products/neue-montreal
     (confirmed live: a small logo mark top-left beside a grid of
     titled link columns, each with a thin rule under its title and a
     thin rule down its left edge, a full-width divider, then a
     dominant display-text line lower in the footer, and a slim
     credits row at the very bottom). Own colors, own copy, own "To
     The Moon" line, own columns/content throughout — matched
     structure only, nothing copied. Replaces the earlier "EBD"
     wordmark footer (see git history). Violet background + white
     text/lines (data-theme="dark" so the cursor trail keeps its
     light/bright default color here, same mechanism every other
     data-theme switch in this file uses — this background reads
     dark enough to want that). Columns, moon line, and bottom row
     all built by script from FOOTER_CONFIG, same convention as every
     other generated section.
  ============================================================ */
  .site-footer{
    position: relative;
    background: var(--footer-bg, var(--color-violet));
    padding: 8vh 4vw 5vh;
    overflow: hidden; /* clips the moon line's pre-reveal translateY offset */
  }
  /* subtle paper-grain overlay — a tiny tiled SVG feTurbulence noise
     tile, painted behind every other footer child (z-index) at low
     opacity so it never touches text sharpness (it's a background
     layer, not a filter/blur on the content itself). */
  .site-footer-grain{
    position: absolute;
    inset: 0;
    z-index: 0;
    pointer-events: none;
    opacity: .05;
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix type='matrix' values='0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.7 0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
    background-repeat: repeat;
    background-size: 120px 120px;
  }

  .site-footer-top{
    position: relative;
    z-index: 1;
    display: flex;
    align-items: flex-start;
    gap: 4vw;
  }
  /* logo-mark slot — the doodle + its "Made in Boulder" caption,
     stacked and centered as one unit so the caption always stays
     aligned under the drawing at any breakpoint (flex-column on the
     wrapper, not on the doodle itself) — same static-position
     override every other inline doodle placement in this file uses;
     sized bigger than a typical small logo mark so the skier itself
     stays readable, per this task's own instruction. */
  .site-footer-logo-slot{
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 8px;
    flex: none;
  }
  /* ~2x the original size so the skier + lava read clearly, per
     explicit feedback; shrinks a step on tablet so it doesn't crowd
     the 4 columns, and drops to a comfortable in-between size on
     mobile where it stacks above the columns instead of beside them
     (see .site-footer-top's own max-width:560px rule). Own fix, per
     explicit follow-up ("still not very legible"): another ~30% on
     top of that at every step (192->250, 140->182, 112->146). */
  #doodleFooterFlatironsSki{ position: static; width: 250px; }
  @media (max-width: 900px){
    #doodleFooterFlatironsSki{ width: 182px; }
  }
  @media (max-width: 560px){
    #doodleFooterFlatironsSki{ width: 146px; }
  }
  /* matches the column titles exactly (font/size/weight/tracking/
     uppercase/white) per this task's own spec — no rule line under
     it, that's specific to the column titles. */
  .site-footer-logo-caption{
    font-family: 'Aspekta Variable', 'DM Sans', sans-serif;
    font-weight: 650;
    font-size: 13px;
    text-transform: uppercase;
    letter-spacing: .06em;
    color: var(--color-cream);
  }
  .site-footer-columns{
    flex: 1;
    display: grid;
    grid-template-columns: repeat(4, 1fr);
    gap: 24px;
  }
  .site-footer-col{
    border-left: 1px solid var(--color-cream);
    padding-left: 20px;
  }
  .site-footer-col-title{
    font-family: 'Aspekta Variable', 'DM Sans', sans-serif;
    font-weight: 650;
    font-size: 13px;
    text-transform: uppercase;
    letter-spacing: .06em;
    color: var(--color-cream);
    margin: 0 0 16px;
    padding-bottom: 12px;
    border-bottom: 1px solid var(--color-cream);
  }
  .site-footer-col-list{
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: 10px;
  }
  .site-footer-col-list li{ display: flex; align-items: center; gap: 6px; }
  .site-footer-col-list a{
    display: inline-block;
    color: var(--color-cream);
    text-decoration: none;
    font-family: 'Aspekta Variable', 'DM Sans', sans-serif;
    font-weight: 500;
    font-size: 15px;
    transition: transform .15s ease-out;
  }
  /* subtle hover — text nudges right + underlines, per this task's
     own spec, in place of the old footer's color-swap hover. */
  .site-footer-col-list a:hover{
    transform: translateX(4px);
    text-decoration: underline;
    text-underline-offset: 3px;
  }
  /* banana-peel moved here (was the old footer's Contact-nav
     accent) — sits small next to "Contact us" in the Echo Boomer
     column, the one link it fits next to cleanly. */
  #doodleFooterBananaPeel{ position: static; width: 16px; flex: none; }

  .site-footer-divider{
    position: relative;
    z-index: 1;
    width: 100%;
    border-top: 1px solid var(--color-cream);
    margin: 5vh 0;
  }

  /* "To The Moon" — same treatment as the hero headline: real text
     (this wrap's child span) stays in the DOM for SEO/a11y, visually
     hidden via opacity:0, while a WebGL <canvas> on top renders the
     warped/hole-cut version through the shared window.HeadlineFX
     factory (see that block comment for why one factory, not a
     duplicate implementation). The wrap itself only carries layout +
     the reveal transform; typography lives on the text span so
     fitMoon() can measure its true shrink-to-fit width (the wrap's
     own width:100% would otherwise always report the full footer
     width, not the text's real content width — the original bug). */
  .site-footer-moon{
    position: relative;
    z-index: 1;
    width: 100%;
    transform: translateY(0.22em); /* pre-reveal offset — "rises up slightly into place" */
    transition-property: transform;
  }
  .site-footer.is-revealed .site-footer-moon{ transform: translateY(0); }
  @media (prefers-reduced-motion: reduce){
    .site-footer-moon{ transform: translateY(0) !important; transition: none !important; }
  }
  .site-footer-moon-text{
    display: inline-block; /* shrink-to-fit — see the block comment above */
    white-space: nowrap;
    font-family: 'Aspekta Variable', 'DM Sans', sans-serif;
    font-weight: 800;
    letter-spacing: -0.01em;
    line-height: .85;
    color: var(--footer-text-color, var(--color-cream));
    font-size: var(--moon-fs, 14vw); /* fluid fallback before the script measures+fits the real width — see fitMoon in the script */
    opacity: 0;
  }
  .reduce-motion .site-footer-moon-text{ opacity: 1; }
  .site-footer-moon-canvas{
    position: absolute;
    inset: 0;
    z-index: 1;
    display: block;
    pointer-events: none;
  }
  .reduce-motion .site-footer-moon-canvas{ display: none; }
  /* rocket-mars-talking doodle — a normal (non-warped) image layered above
     the warp canvas, positioned in JS from the measured end of "Moon"
     (see positionRocketDoodle in the script): height/width/top/left
     are all set inline from there, not from CSS, since they depend on
     live text metrics. This rule only carries what doesn't change. */
  .site-footer-moon-doodle{
    position: absolute;
    z-index: 2;
    pointer-events: none;
  }

  .site-footer-bottom{
    position: relative;
    z-index: 1;
    display: flex;
    justify-content: space-between;
    align-items: center;
    font-family: 'Aspekta Variable', 'DM Sans', sans-serif;
    font-weight: 500;
    font-size: 14px;
    color: var(--color-cream);
  }

  @media (max-width: 900px){
    .site-footer-columns{ grid-template-columns: repeat(2, 1fr); }
  }
  @media (max-width: 560px){
    .site-footer-top{ flex-direction: column; gap: 24px; }
    .site-footer-columns{ grid-template-columns: 1fr; }
    .site-footer-bottom{ flex-direction: column; align-items: flex-start; gap: 8px; }
  }

