/* ============================================================
   about.css — scoped exclusively to about.html
   ============================================================

   Uses the shared design-system.css for tokens (fonts, colours,
   grid, type roles) but every layout / component here is unique
   to the About page. NOTHING in this file is used by the video
   hero, the game, or the panels on the main site.
============================================================ */

/* ── EYE-BLINK REVEAL ────────────────────────────────────────
   Full-viewport SVG overlay with an ellipse mask. When the
   ellipse is at scaleY(1), the mask is fully open → SVG is
   transparent. When scaleY(0), the mask collapses to a line →
   the surrounding black fills everything.
   Body starts with `.is-blinking .is-eye-closed` in HTML, so
   the About page renders with eyelids CLOSED at first paint.
   js/about-eye-open.js removes `.is-eye-closed` to trigger the
   500 ms open animation, then removes `.is-blinking` so the
   overlay stops intercepting pointer events.
   Timings + easing mirror styles.css for the main site's hero
   blink so the transition feels the same across pages. */
.eye-blink {
  position: fixed;
  inset: 0;
  width: 100vw;
  height: 100vh;
  z-index: 12000;
  pointer-events: none;
  display: block;
  opacity: 0;
  transition: opacity 180ms ease;
}
body.is-blinking .eye-blink { opacity: 1; }
.eye-blink__pupil {
  transform-box: fill-box;
  transform-origin: center;
  transform: scaleY(1);
  transition: transform 500ms cubic-bezier(0.7, 0, 0.2, 1);
}
body.is-eye-closed .eye-blink__pupil {
  transform: scaleY(0);
}

/* ── Night-mode toggle button ────────────────────────────
   Mirrors the main site's .night-toggle in styles.css — same
   38 × 38 button in the top-left, moon icon by default, sun
   icon under is-night-mode. Clicking flips body.is-night-mode
   (see about-night-mode.js). The token overrides below invert
   paper ↔ ink and swap the mid-gray to its lighter counterpart
   so the whole page reads as dark mode. */
.night-toggle {
  position: fixed;
  top: 40px;
  right: 60px;
  width: 38px;
  height: 38px;
  padding: 0;
  background: transparent;
  border: none;
  /* Colour is fixed white and blended with `mix-blend-mode:
     difference` so the icon inverts against whatever sits
     behind it - same trick .nav uses so the two chrome
     affordances read as a matching pair (per Arun 2026-08-27).
     On a paper ground the icon renders as ink; on an ink
     ground it stays paper. Works across all page sections
     (Approach paper, Everything ink, curtain ink, Contact
     ink, etc.) without any extra overrides. */
  color: #fff;
  mix-blend-mode: difference;
  cursor: pointer;
  display: grid;
  place-items: center;
  transition: opacity 200ms ease;
  z-index: 1200;                     /* above the fade-curtain (900), caption (1001), and nav (1100) */
}
.night-toggle:hover { opacity: 0.7; }
.night-toggle:focus { outline: none; }
.night-toggle:focus-visible {
  outline: 1px solid rgba(0, 0, 0, 0.5);
  outline-offset: 6px;
  border-radius: 999px;
}
body.is-night-mode .night-toggle:focus-visible {
  outline-color: rgba(255, 255, 255, 0.7);
}
.night-toggle__icon {
  grid-area: 1 / 1;                  /* stack both icons in the same cell */
  width: 22px;
  height: 22px;
  transition: opacity 300ms ease;
}
.night-toggle__icon--moon { opacity: 1; }
.night-toggle__icon--sun  { opacity: 0; }
body.is-night-mode .night-toggle__icon--moon { opacity: 0; }
body.is-night-mode .night-toggle__icon--sun  { opacity: 1; }

/* ── Night-mode token overrides ──────────────────────────
   Flips the design-system palette so the About page reads as
   dark mode: paper (white) ↔ ink (black), and mid-gray to its
   inverted counterpart (b3b3b3). Everything on the page that
   uses these tokens inverts automatically. Applied 2026-08-27
   per Arun. Bare-hex values in about.css that don't reach for
   the tokens (e.g. the fade-curtain's `background: #000`, the
   flair-trail's hint text) are handled individually below. */
body.is-night-mode {
  --paper: #000;
  --ink:   #fff;
  --mid:   #b3b3b3;
  transition: background 400ms ease, color 400ms ease;
}
/* The fade-curtain uses a hardcoded `background: #000` and
   should stay black regardless of day / night mode — so no
   override needed. Explicit note here so future edits don't
   accidentally introduce a token there. */

/* ── Night-mode fine-tunes (per Arun 2026-08-27) ──────────
   The token swap above flips most of the page automatically,
   but four spots need explicit night-mode overrides because
   they use non-token values or need to hold a specific hue
   regardless of mode. */

/* 1. `.names` client marquee in night mode: the section
      background drops to transparent (letting the black page
      ground show through - no white patch), and the text +
      logos flip to white. Day mode is unchanged (white ground,
      black logos). Per Arun 2026-08-27. */
body.is-night-mode .names {
  background: transparent;
  color: #fff;
}
body.is-night-mode .names__logo {
  /* brightness(0) forces the source SVG to black, then
     invert(1) flips it to white so it reads on the dark
     ground. */
  filter: brightness(0) invert(1);
}

/* 2. "A little bit of everything" section — its background
      uses `var(--ink)` which flips to white under the token
      swap. Arun wants this section to stay black-with-white
      regardless of day / night, so hardcode both. */
body.is-night-mode .everything {
  background: #000;
  color: #fff;
}
body.is-night-mode .everything__stack,
body.is-night-mode .everything__line {
  color: #fff;
}

/* 3. Process arrows — the SVG's stroke is `white`. Day mode
      shows it on a black tile, so it reads. In night mode the
      `.process__fill` tile becomes white (from the ink swap),
      so `brightness(0)` on the img forces the SVG to black to
      stay readable. */
body.is-night-mode .process__arrow img {
  filter: brightness(0);
}

/* 3b. Process tiles + arrows themselves — pre-sweep the tile is
       `background: transparent` (see .process__step base rule) so
       the section bg shows through. In day mode this is fine (white
       text is invisible on white section, sweep reveals it on black
       fill). In night mode the text and arrows are BLACK and the
       section is BLACK, so pre-sweep tiles appear entirely blank.
       Give tiles a solid white background in night mode so text
       and arrows always read — the fill's sweep still runs but is
       now white-on-white (invisible), a small loss of the reveal
       effect for a guaranteed pass on legibility. Per Arun 2026-08-30. */
body.is-night-mode .process__step,
body.is-night-mode .process__arrow {
  background: var(--ink);
}

/* 4. Reflect Dual Wave non-focused rows — day mode uses a
      hardcoded `rgba(0, 0, 0, 0.15)` (dim black) so unfocused
      rows sit as a soft ghost against the white paper ground.
      In night mode the ground is black and dim-black rows are
      invisible; swap to a dim-white so the ghost effect still
      reads. The `.focused` row inherits the paper/ink swap
      already (`var(--ink)` → white), so it stays legible. */
body.is-night-mode .reflect--wave .animated-text {
  color: rgba(255, 255, 255, 0.35);
}
/* Focused (centre-of-viewport) row lifts back to full white so
   the highlight still reads on the black ground. Higher-
   specificity selector (adds `.focused`) beats the base rule
   above (per Arun 2026-08-27). */
body.is-night-mode .reflect--wave .animated-text.focused {
  color: #fff;
}

/* 5. Contact ("So, what's next?") section is a DARK identity
      zone. Day: uses `var(--ink)` bg + `var(--paper)` text and
      the token swap would flip it to white/black; we hardcode
      to keep the section dark always (per Arun 2026-08-27:
      "no change in the colors required there"). The fixed
      caption on top uses `var(--paper)` for its text colour
      too, so pin it to white as well. */
body.is-night-mode .contact {
  background: #000;
  color: #fff;
}
body.is-night-mode .contact__caption {
  color: #fff;
}

/* 6. Case-study logos (Titan / Ascendion / LTTS / TBD) — the
      SVGs are native white so they read on the day-mode dark
      card body. In night mode the case card's `.case-sticky`
      background flips to white (from the ink token swap), so
      white-on-white makes the logos invisible. `brightness(0)`
      forces them to solid black regardless of source colour
      (per Arun 2026-08-27). */
body.is-night-mode .case-sticky__logo {
  filter: brightness(0);
}

/* ── Preloader dot (about-eye-preload.js) ─────────────────
   Optional addition per Arun 2026-08-27. A soft breathing dot
   parked at the centre of the closed-eye overlay so visitors
   know the page hasn't stalled while non-iframe assets finish
   loading. z-index 12100 puts it above the .eye-blink black
   ground (z 12000). The element is injected + removed by
   js/about-eye-preload.js — if that script tag is removed
   from about.html, this rule becomes dormant (nothing ever
   gets the class) and can be deleted too. */
.about-loading-dot {
  position: fixed;
  top: 50%;
  left: 50%;
  width: 10px;
  height: 10px;
  border-radius: 50%;
  background: var(--paper);
  transform: translate(-50%, -50%);
  z-index: 12100;
  pointer-events: none;
  opacity: 0.85;
  animation: about-loading-pulse 1.4s ease-in-out infinite;
}
@keyframes about-loading-pulse {
  0%, 100% { opacity: 0.35; transform: translate(-50%, -50%) scale(0.85); }
  50%      { opacity: 1;    transform: translate(-50%, -50%) scale(1.2);  }
}
@media (prefers-reduced-motion: reduce) {
  .about-loading-dot { animation: none; opacity: 0.7; }
}

/* ── NAV ───────────────────────────────────────────────────
   The pill background is gone. The floating menu now sits over
   a gradual-blur strip (see .about-blur below) that fades in
   backdrop-filter intensity toward the bottom of the viewport.
   Nav content just rides on top at a higher z-index. */
/* Values here mirror the main video site's `.hero__chrome` /
   `.brand` / `.hero__rule` / `.site-nav` so both surfaces feel
   like the same interface. Only the values are copied — none of
   the main site's CSS files are touched.

   mix-blend-mode lives on the nav container so the whole element
   composites as a group against whatever's behind. Children are
   coloured WHITE — the blend takes care of black-on-light and
   white-on-dark automatically. */
.nav {
  position: fixed;
  left: 60px;
  right: 60px;
  bottom: 40px;
  /* Above .fade-curtain (900) and .contact__caption (1001) so
     the nav + logo + close button stay visible + clickable
     during the Approach → Contact transition and afterwards. */
  z-index: 1100;
  display: flex;
  align-items: flex-end;         /* rule + nav sit on a baseline */
  gap: 24px;                     /* main site chrome gap */
  padding: 0;
  background: transparent;
  border-radius: 0;
  pointer-events: none;          /* clicks pass through empty gaps */
  mix-blend-mode: difference;
}
.nav > * { pointer-events: auto; }

/* Logo — 100 px wide, native aspect 186/148, same as .brand. */
.nav__logo {
  flex-shrink: 0;
  display: block;
  width: 100px;
  aspect-ratio: 186 / 148;
  transition: opacity 200ms ease;
}
.nav__logo:hover { opacity: 0.85; }
.nav__logo img {
  width: 100%;
  height: 100%;
  object-fit: contain;
  display: block;
  /* logo.png ships white on transparent — perfect input for the
     parent's difference blend. No filter needed. */
}

/* Rule — 1 px white line at 0.5 opacity, baseline-nudged to sit
   on the nav-link baseline (matches `.hero__rule` exactly). */
.nav__rule {
  flex: 1 1 auto;
  height: 1px;
  background: var(--paper);
  opacity: 0.5;
  border-radius: 0;
  margin-bottom: 4px;
}

/* Nav links — 20 px NewBlack uppercase, horizontal row, hover
   fades to 0.7. Font values inherited via `.type--display-xs` on
   the anchors themselves. */
.nav__links {
  flex-shrink: 0;
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  align-items: flex-end;
  gap: 40px;
}
.nav__links a {
  color: var(--paper);
  text-decoration: none;
  transition: opacity 200ms ease;
}
.nav__links a:hover { opacity: 0.7; }

/* Close button — 50 × 50 to match the Figma spec. Uses the shared
   images/close.svg (66×66 viewBox, white strokes) so the parent's
   difference blend inverts it correctly on light and dark grounds. */
.nav__close {
  flex-shrink: 0;
  width: 50px;
  height: 50px;
  display: grid;
  place-items: center;
  background: transparent;
  border: none;
  cursor: pointer;
  padding: 0;
  border-radius: 50%;
  transition: opacity 200ms ease;
}
.nav__close:hover     { opacity: 0.7; }
.nav__close img       { width: 100%; height: 100%; display: block; object-fit: contain; }

/* ── GRADUAL BLUR STRIP ─────────────────────────────────────
   Fixed strip at the bottom of the viewport. Contains N stacked
   divs, each with a different backdrop-filter blur value and a
   linear-gradient mask that shows only its portion of the strip.
   Together they produce a smooth gradient from 0 blur at the
   top of the strip to strong blur at the bottom, giving the
   nav a "fading glass" pane instead of a hard-edged pill.
   Divs are injected by js/about.js so the divCount / curve /
   strength are configurable — this file only owns the frame. */
.about-blur {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  height: 190px;                  /* -10% again — tighter fade-in */
  z-index: 90;                    /* below nav (100), above content */
  pointer-events: none;
  isolation: isolate;             /* so blend/blur only mixes inside */
}
.about-blur__inner {
  position: relative;
  width: 100%;
  height: 100%;
}
.about-blur__inner > div {
  -webkit-backdrop-filter: inherit;
          backdrop-filter: inherit;
}
/* No tint. Pure blur only — whatever's behind the strip gets
   blurred by the layer stack, nothing else added on top. */

/* ── SECTION 1 · INTRO — full viewport ──────────────────── */
.intro {
  min-height: 100vh;
  padding: 10vh var(--col-margin) clamp(140px, 16vw, 180px);
  display: grid;
  grid-template-columns: repeat(var(--cols), 1fr);
  column-gap: var(--col-gap);
  align-items: end;
  align-items: last baseline;
  align-content: start;
}
.intro__left  { grid-column: 1 / span 4; display: flex; flex-direction: column; gap: var(--space-1); min-width: 0; }
.intro__right { grid-column: 5 / span 2; align-self: last baseline; }
/* Tighter line-height on the hero mark — override the design
   system's 0.82 with 0.7 so "I'M" and "ARUN" sit closer together
   vertically. */
.intro__mark {
  text-align: right;
  text-wrap: balance;
  line-height: 0.7;
}

/* ── Effect 01 · Rise reveal (from the text-lab library) ─────
   When the mark opts into the rise effect, each line becomes its
   own overflow-hidden mask so chars translated to yPercent: 110
   are clipped cleanly below the baseline (invisible at rest) and
   the reveal animation slides them up into view. The container
   is hidden until JS runs Splitting + gsap.set so there's no
   flash of un-transformed text on load.
   line-height on the spans is bumped to 0.9 (just enough to
   contain uppercase glyphs so the mask doesn't clip their tops)
   and the visual gap between the two lines is restored to the
   original tight look via a small negative margin on the 2nd. */
.intro__mark[data-fx="rise"] {
  visibility: hidden;
}
.intro__mark__line {
  display: block;
  overflow: hidden;
  line-height: 0.9;
  padding-bottom: 0.03em;    /* tiny breathing room under the baseline */
}
.intro__mark__line + .intro__mark__line {
  margin-top: -0.2em;        /* pull line 2 up so overall spacing still reads as line-height 0.7 */
}
.intro__mark__line .char {
  display: inline-block;
  will-change: transform;
}

/* ── Effect 03 · Fade reveal (from the text-lab library) ──────
   Opt-in via [data-fx="fade-chars"] on the intro__right
   paragraph. Hidden until JS runs Splitting + gsap.set so there's
   no flash of un-faded text on load. inline-block on `.char` so
   each glyph is its own compositing box for opacity (default
   inline text can't be transformed / composited cleanly). */
.intro__right[data-fx="fade-chars"] {
  visibility: hidden;
}
.intro__right[data-fx="fade-chars"] .char {
  display: inline-block;
  will-change: opacity;
}

/* ── Work · Effect 01 · Rise reveal (from the text-lab library) ──
   Same masking recipe as .intro__mark__line but scoped to the
   Work heading. Parent .type--display-l runs at line-height 0.92
   (looser than the intro mark's 0.7) so the compensation is
   smaller: line-height 1.0 on the spans is enough to contain
   caps cleanly, and a -0.08em top margin on line 2 restores the
   original two-line stack spacing. */
.work__title[data-fx="rise"] {
  visibility: hidden;
}
.work__title__line {
  display: block;
  overflow: hidden;
  line-height: 1.0;
  padding-bottom: 0.04em;    /* room under the baseline for anti-aliasing */
}
.work__title__line + .work__title__line {
  margin-top: -0.08em;       /* pull line 2 up so overall spacing still reads as line-height 0.92 */
}
.work__title__line .char {
  display: inline-block;
  will-change: transform;
}

/* ── Work · Effect 02 · Paragraph reveal (from the lab library) ──
   Opt-in via [data-fx="paragraph-reveal"] on the work__intro
   paragraph. Same setup as the (removed) intro paragraph-reveal —
   hidden until JS runs Splitting + gsap.set, transform-origin
   pinned to the left edge so the un-skew pivots from the
   paragraph's beginning. */
.work__intro p[data-fx="paragraph-reveal"] {
  visibility: hidden;
  transform-origin: 0% 50%;
}
.work__intro p[data-fx="paragraph-reveal"] .word {
  will-change: opacity;
}

/* ── Approach · Effect 01 · Rise reveal ─────────────────────
   Same masking recipe as .work__title. Parent .type--display-l
   runs at line-height 0.92, spans use line-height 1.0 to contain
   caps; -0.08em on line 2 restores the original stack spacing. */
.approach__title[data-fx="rise"] { visibility: hidden; }
.approach__title__line {
  display: block;
  overflow: hidden;
  line-height: 1.0;
  padding-bottom: 0.04em;
}
.approach__title__line + .approach__title__line {
  margin-top: -0.08em;
}
.approach__title__line .char {
  display: inline-block;
  will-change: transform;
}

/* ── Names · Effect 01 · Rise reveal ────────────────────────
   Parent .type--display-s is already line-height: 1 (see
   design-system.css), so no negative margin is needed between
   the two lines — natural spacing already matches. */
.names__title[data-fx="rise"] { visibility: hidden; }
.names__title__line {
  display: block;
  overflow: hidden;
  line-height: 1.0;
  padding-bottom: 0.04em;
}
.names__title__line .char {
  display: inline-block;
  will-change: transform;
}

/* Effect 01 rise-reveal was applied to .contact__title in an
   earlier revision, then removed once the scroll-scrubbed
   section transition (js/about-contact-transition.js) took
   over the "text appears from below" mechanic. Kept as a note
   because .contact__title is still used as an identifier
   selector by the transition JS. */

/* ── Everything · Effect 01 · Rise reveal ───────────────────
   Each `.everything__line` is a dark backdrop-blur pill with
   padding — the h2 itself IS the mask. Adding overflow: hidden
   clips chars translated to yPercent 110 (they exit the pill
   below its bottom edge and get clipped there). The pill's own
   background + backdrop-filter render normally; only translated
   children get clipped. */
.everything__stack[data-fx="rise"] { visibility: hidden; }
.everything__stack[data-fx="rise"] .everything__line {
  overflow: hidden;
}
.everything__stack[data-fx="rise"] .everything__line .char {
  display: inline-block;
  will-change: transform;
}

/* Reduced-motion path: skip every effect, show text in place from
   the start so the copy is still readable. */
@media (prefers-reduced-motion: reduce) {
  /* Every [data-fx="rise"] container gets its visibility restored
     and its chars snapped to yPercent 0 (transform: none). One
     rule apiece covers every current + future rise application. */
  [data-fx="rise"]                                    { visibility: visible; }
  [data-fx="rise"] .char                              { transform: none !important; }

  .intro__right[data-fx="fade-chars"]                 { visibility: visible; }
  .intro__right[data-fx="fade-chars"] .char           { opacity: 1 !important; }
  .work__intro p[data-fx="paragraph-reveal"]          { visibility: visible; transform: none !important; }
  .work__intro p[data-fx="paragraph-reveal"] .word    { opacity: 1 !important; }

  /* Reflect · Dual Wave — no horizontal slide, no per-scroll updates.
     Rows sit in place; the focused-row colour transition stays (it's
     a color change, not a transform, so still allowed under reduce). */
  .reflect--wave .animated-text                       { transform: none !important; }
}

/* ── SECTION 2 · WORK OPENER ────────────────────────────── */
.work {
  padding: 0 var(--col-margin) clamp(60px, 12vw, 120px);
  display: flex;
  flex-direction: column;
}
/* Work section head + cases-promo BOTH sit on the same 6-column
   grid the intro uses (see `.intro` above). Matching spans across
   the two rows means the H2 ends at the same column-4 boundary as
   the promo caption below it, and the body copy / promo card both
   start at column 5. Anything else in the Work section that needs
   to align to those boundaries just uses `grid-column: 1 / span 4`
   or `grid-column: 5 / span 2`. */
.work__head {
  display: grid;
  grid-template-columns: repeat(var(--cols), 1fr);
  column-gap: var(--col-gap);
  row-gap: var(--space-3);
  align-items: start;              /* heading + body copy share the same top edge */
}
.work__head > h2         { grid-column: 1 / span 4; }
.work__head > .work__intro { grid-column: 5 / span 2; }
/* max-widths from the old `1fr auto` layout are no longer needed —
   the 2-column grid span already caps the paragraph's measure. */
.work__intro   { max-width: none; padding-right: 0; }
.work__intro p { max-width: none; }

/* ── SECTION 2b · CASES ─────────────────────────────────── */
.cases {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  padding-top: clamp(var(--space-4), 7vw, 70px);
}
.cases__promo {
  display: grid;
  grid-template-columns: repeat(var(--cols), 1fr);
  column-gap: var(--col-gap);
  row-gap: var(--space-3);
  align-items: end;
}
/* Match the work__head spans exactly so the two rows read as one
   column system: caption in cols 1-4, promo card in cols 5-6. */
.cases__promo > p            { grid-column: 1 / span 4; }
.cases__promo > .card--promo { grid-column: 5 / span 2; }
.card--promo {
  background: var(--ink);
  height: 200px;
  width: 100%;                    /* fill the 2-col grid slot instead of the old fixed 417px */
  padding: var(--space-3);
  display: flex;
  align-items: center;
  gap: var(--space-1);
}
.card--promo .placeholder-logo,
.card--promo .placeholder-image { flex: 1; height: 100%; min-width: 0; }

.card--project {
  background: var(--ink);
  color: var(--paper);
  padding: var(--space-3);
  display: flex;
  flex-direction: column;
  gap: var(--space-6);
}
.meta-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--space-3);
  width: 100%;
}
.meta-row {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: var(--space-2);
  padding: var(--space-1) 0;
  border-bottom: 1px solid var(--paper);
  font-family: var(--font-body);
  font-weight: 400;
  font-size: clamp(14px, 1.6vw, 18px);
  line-height: 1.5;
}
.meta-row--capped { border-top: 1px solid var(--paper); }
.meta-row__v      { text-align: right; }

.card__body {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--space-3);
  align-items: center;
}
.card__body .placeholder-logo,
.card__body .placeholder-image { min-height: 200px; }

.cases__row {
  padding-top: var(--space-3);
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  align-items: flex-start;
}
.card--wide {
  background: var(--ink);
  color: var(--paper);
  height: 250px;
  width: min(643px, 100%);
  padding: var(--space-3);
  display: flex;
  align-items: center;
  gap: var(--space-3);
}
.card--wide .placeholder-logo,
.card--wide .placeholder-image { flex: 1; min-width: 0; height: 100%; }
.card--split {
  background: var(--ink);
  color: var(--paper);
  height: 200px;
  width: min(417px, 100%);
  padding: var(--space-3);
  display: flex;
  gap: var(--space-1);
}
.card--split .placeholder-image { flex: 1; height: 100%; min-width: 0; }

/* ── Cases · Pinned scroll-jacked carousel ──────────────────
   Four case-study cards live inside a sticky-pinned stage. As
   the visitor scrolls through the wrapper's tall gutter, JS
   drives a GSAP timeline that repositions + resizes each card
   across the 5-slot layout defined in js/about-cases-carousel.js
   (2-col top-right → 3-col right → 6-col active → 3-col left →
   2-col bottom-left). Slot geometry lives in the JS, not here,
   so the sizing rules stay in one place. */
.cases__caption {
  /* Stays in normal flow above the pinned area — scrolls with
     the page as the user enters the section. No absolute
     positioning; the pinned carousel handles its own layout. */
  margin: 0;
  padding-bottom: var(--space-3);
}
/* ── Sticky-flip carousel (Codrops "Sticky Sections · Demo 6") ──
   Each .case-sticky section pins to the top of the viewport for
   its own scroll range; as the visitor scrolls past, its inner
   is tilted back on X-axis, scaled down and darkened by the
   scroll-scrubbed timeline in js/about-cases-carousel.js. The
   next sticky section slides up to take its place.

   `perspective` on the wrap sets up the 3D viewing frustum so
   the rotationX on the inner reads as a real fold-away rather
   than a squish. `perspective-origin: 50% 0%` puts the vanishing
   point at the top-centre so the fold hinges from the top edge —
   matching the transform-origin the JS sets on the inner. */
.cases-carousel-wrap {
  position: relative;
  perspective: 1000px;
  perspective-origin: 50% 0%;
}
.case-sticky {
  /* Sticky section is the SAME height as the card inside it —
     500 px — so when the section's top hits the viewport top the
     card visibly sticks there, instead of sitting at viewport
     centre inside a taller (100vh) sticky box. Sticky range for
     each section is then determined by the wrap's overall height
     and the number of sections stacked in it. */
  position: sticky;
  top: 0;
  width: 100%;
  height: 500px;
  /* Flex-centre the inner card horizontally so, when the scroll
     timeline shrinks its width mid-fold, it collapses toward the
     horizontal centre of the viewport instead of the left edge. */
  display: flex;
  justify-content: center;
  align-items: stretch;
}
/* 30 px gap between adjacent sticky cards — becomes visible
   between one card unsticking and the next one sticking. */
.case-sticky + .case-sticky {
  margin-top: 30px;
}
.case-sticky__inner {
  width: 100%;
  height: 100%;                     /* fills the 500 px sticky */
  background: var(--ink);
  color: var(--paper);
  /* Explicit 30 px on all four sides — same top / right / bottom
     / left inset, so the image never touches the black card edge. */
  padding: 30px;
  padding-bottom: 30px;             /* explicit reinforcement */
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  overflow: hidden;
  box-sizing: border-box;
  will-change: transform, filter, clip-path;
  /* Anchor the overlay pseudo-element (see ::after) so it sits
     flush inside the card. */
  position: relative;
  /* Overlay opacity — driven by the scroll-scrubbed timeline in
     about-cases-carousel.js. Starts at 0 (card fully visible)
     and animates to 1 (card fully blacked out) as the fold-away
     progresses. */
  --overlay-alpha: 0;
}
.case-sticky__inner::after {
  content: '';
  position: absolute;
  inset: 0;
  /* Overlay tint switched from pure ink (#000) to #4d4d4d per
     Arun 2026-08-27 — a new mid-grey being introduced as the
     third value in the palette (paper #fff, ink #000, mid #4d4d4d).
     rgba() form keeps `--overlay-alpha` driving opacity from
     about-cases-carousel.js unchanged. */
  background: rgba(77, 77, 77, var(--overlay-alpha));
  pointer-events: none;
  z-index: 2;   /* above meta + body */
}
.case-sticky__meta {
  display: grid;
  grid-template-columns: 1fr 1fr;
  /* Zero row-gap so the bottom border of Project/Company row
     doubles as the top boundary of Agency/Role, matching the
     earlier design. */
  gap: 0 var(--space-3);
}
.case-sticky__body {
  flex: 1;
  min-height: 0;
  display: grid;
  grid-template-columns: 1fr 1fr;    /* logo | image, 50 / 50 */
  /* Force the single grid row to be exactly the body's height —
     without this, the row auto-sizes to the tallest child
     (natural image height ≈ 800 px), which pushes the image
     past the body's bottom edge and into the card's padding,
     killing the visible padding-bottom under the image. */
  grid-template-rows: minmax(0, 1fr);
  gap: var(--space-3);
}
.case-sticky__body .placeholder-image { min-width: 0; }
.case-sticky__body .case-sticky__image,
.case-sticky__body .case-sticky__logo {
  min-height: 0;                     /* let each media child shrink to its cell */
}
/* Project preview thumbnail — fills its 50 % body slot in both
   dimensions. `object-fit: cover` may crop edges of the source
   image; switch to `contain` if you'd rather see the whole
   thumbnail with letterboxing. */
.case-sticky__image {
  width: 100%;
  height: 100%;
  min-width: 0;
  object-fit: cover;
  object-position: center;
  display: block;
}
.case-sticky__logo {
  /* Height ALWAYS fills the body slot; width scales to preserve
     the original aspect ratio. `max-width: 100%` caps very wide
     logos so they don't overflow the 50 % slot. Left-aligned by
     default (block image inside a grid cell defaults to
     justify-self: start). */
  height: 100%;
  width: auto;
  max-width: 100%;
  display: block;
  justify-self: start;
}

.names {
  margin-top: var(--space-3);
  /* Flipped from ink → paper 2026-08-27 per Arun: the section
     now reads as a white row of black logos aligned to the
     page's own col-margin (inherited from .work), so no
     horizontal padding on .names itself — the title lines up
     with the outer black-box edge visitors already know. */
  background: var(--paper);
  color: var(--ink);
  padding: clamp(var(--space-3), 6vw, var(--space-6)) 0;
  display: grid;
  grid-template-columns: auto 1fr;
  gap: clamp(var(--space-3), 6vw, var(--space-8));
  align-items: center;
}
/* Marquee viewport: masks whatever's inside .names__track and
   fades the edges out so logos appear + disappear cleanly rather
   than clipping at hard edges. */
.names__row {
  overflow: hidden;
  min-width: 0;
  -webkit-mask-image: linear-gradient(to right, transparent 0, black 5%, black 95%, transparent 100%);
          mask-image: linear-gradient(to right, transparent 0, black 5%, black 95%, transparent 100%);
}
/* The scrolling track. `width: max-content` lets it be as wide as
   its (duplicated) children; `animation` slides it left by 50 %,
   which lands the duplicate set exactly on the original set —
   creating a seamless loop. `js/about-names-marquee.js` clones
   every child so the -50 % offset always matches. */
.names__track {
  display: flex;
  align-items: center;
  gap: clamp(var(--space-2), 4vw, var(--space-6));
  width: max-content;
  animation: names-marquee 30s linear infinite;
  will-change: transform;
}
.names__track:hover {
  animation-play-state: paused;   /* let visitors pause on hover */
}
@keyframes names-marquee {
  from { transform: translate3d(0, 0, 0); }
  to   { transform: translate3d(-50%, 0, 0); }
}
.names__logo {
  /* Height-based sizing — every logo shares the same VERTICAL
     footprint, and width is determined by each SVG's natural
     aspect ratio (wide wordmarks stay wide, square marks stay
     square). Uniform height reads as one row without cramping
     the wide brands. */
  height: clamp(36px, 4.5vw, 60px);
  width: auto;
  flex-shrink: 0;              /* keep each logo at its declared size — don't compress in the flex track */
  object-fit: contain;
  /* Force every logo to solid black regardless of the original
     SVG fill colours. `brightness(0)` collapses every colour
     to black; no `invert(1)` here because the section now
     paints on `var(--paper)` and the mark needs to read as
     ink on white (per Arun 2026-08-27). */
  filter: brightness(0);
}

/* Per-logo size overrides. Base is
     height: clamp(36px, 4.5vw, 60px)
   Each override scales all three stops by the same factor so
   the responsive curve stays consistent — the logo just sits
   proportionally smaller than its neighbours. Set 2026-08-27
   per Arun's per-brand call-out on the .names marquee. */
.names__logo[src*="ibm"] {
  /* IBM read too large next to MG / Suzuki — trim by ~30 %. */
  height: clamp(25px, 3.15vw, 42px);
}
.names__logo[src*="vodafone"] {
  /* −20 %. */
  height: clamp(29px, 3.6vw, 48px);
}
.names__logo[src*="cyient"] {
  /* −50 %. */
  height: clamp(18px, 2.25vw, 30px);
}
.names__logo[src*="vh"] {
  /* Van Heusen — −30 %. */
  height: clamp(25px, 3.15vw, 42px);
}

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

/* Placeholders — labelled black boxes for real assets to drop in. */
.placeholder-logo,
.placeholder-image {
  display: grid;
  place-items: center;
  font-family: var(--font-body);
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.22em;
  text-transform: uppercase;
}
.placeholder-logo {
  background: rgba(255, 255, 255, 0.14);
  border: 1px dashed rgba(255, 255, 255, 0.3);
  color: rgba(255, 255, 255, 0.55);
}
.placeholder-logo::before  { content: 'Logo'; }
.placeholder-image {
  background: rgba(255, 255, 255, 0.06);
  border: 1px dashed rgba(255, 255, 255, 0.22);
  color: rgba(255, 255, 255, 0.4);
}
.placeholder-image::before { content: 'Image'; }

/* ── SECTION 3 · A LITTLE BIT OF EVERYTHING ─────────────── */
/* Sticky-scroll wrapper. Total height is what the visitor scrolls
   through while the sticky child (100vh) stays pinned to the top.
   Effective scroll-gutter past the pinned element = height − 100vh. */
.everything-wrap {
  position: relative;
  height: 170vh;
}
.everything {
  position: sticky;
  top: 0;
  height: 100vh;
  padding: clamp(80px, 12vw, 100px) var(--col-margin);
  display: flex;
  flex-direction: column;
  justify-content: flex-start;
  background: var(--ink);
  overflow: hidden;                   /* keep the iframe inside */
}
/* Placeholder ::before is now covered by the iframe — kept for reference
   but hidden. */
.everything::before {
  display: none;
}
/* The tube demo iframe as the section's background. Full-bleed under the
   title stack, ignores pointer events so the section still scrolls. */
.everything__scene {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  border: 0;
  z-index: 1;
  pointer-events: none;                /* let the page scroll through */
}
.everything__stack {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  z-index: 2;
}
.everything__line {
  background: rgba(0, 0, 0, 0.8);
  -webkit-backdrop-filter: blur(28px);
          backdrop-filter: blur(28px);
  color: var(--paper);
  padding: clamp(14px, 2.4vw, 20px);
}
.everything__line + .everything__line { margin-top: -20px; }

/* ── SECTION 4 · APPROACH ───────────────────────────────── */
/* Approach + everything after is a WHITE surface — the site
   flips from ink to paper here, and stays paper through Contact
   (already on paper). Text tokens invert accordingly. */
.approach {
  background: var(--paper);
  color: var(--ink);
  /* Bottom padding set to 40vh on 2026-08-27 per Arun (started
     as 400px, moved to a viewport-relative value so the white
     breathing space below "…something real." scales with the
     screen). Restore to `0` when the Contact + Flair-trail
     sections are un-hidden (they're wrapped in <template>
     tags in about.html). */
  padding: clamp(40px, 6vw, 80px) var(--col-margin) 40vh;
}
.approach__head  { padding-top: 0; text-align: center; }
.approach__title {
  display: inline-block;
  padding: 0;                    /* headline bottom = process grid top */
  color: var(--ink);
}
/* Bridge headline that sits between the process grid and the
   reflect wave. Same size / Effect 01 as .approach__title, but
   needs breathing room above so it doesn't crowd the process
   tiles and below so the reflect wave has room to breathe. */
.approach__title--wave {
  display: block;
  margin-top: clamp(80px, 12vw, 160px);
  text-align: center;
}

/* Process — Figma "stepped flow" layout.
   Five square black boxes across 5 columns: 3 text steps
   (Find / Build / Bring) interleaved with 2 arrow tiles.
   Each cell is vertically offset via align-self so the row reads
   as a wave — text-mid, arrow-bottom, text-mid, arrow-top, text-end.
   Text uses .type--body-xl-b (font body 45) inside each box. */
/* Process — Figma "stepped flow" layout.
   5 equal columns. Explicit 4-row grid so each tile can be placed at a
   precise Y — text tiles span 2 rows, arrow tiles occupy 1 row. Rows are
   sized to the arrow height, so every corner meeting between consecutive
   tiles lands exactly on a grid line:
       Find(BR) meets Arrow1(TL)
       Arrow1(TR) meets Build(BL)
       Build(TR)  meets Arrow2(BL)
       Arrow2(BR) meets Bring(TL)
*/
.process {
  /* Shrunk further per Arun 2026-08-27: middle stop 6vw → 4vw,
     and min + max scaled proportionally (120 → 80, 150 → 120)
     so the reduction is actually visible on desktop widths —
     with the old `min: 120px`, the middle stop was clamped out
     of range and the tiles stayed at 120px regardless of vw. */
  --tile-h: clamp(80px, 4vw, 120px);
  background: var(--paper);
  padding: 0 0 var(--space-6);          /* no top padding — headline bottom butts up to Arrow2 top */
  display: grid;
  grid-template-columns: repeat(5, 1fr);
  grid-template-rows: repeat(4, var(--tile-h));
  gap: 0;
}
.process__step,
.process__arrow {
  background: transparent;
  color: var(--paper);
  position: relative;
  overflow: hidden;
}
/* The mask curtain — a real DOM div (not a pseudo) so GSAP can target
   it directly. Starts at scaleX(0) anchored to the LEFT edge; when
   about-process-sweep.js animates it to scaleX(1) the black fill
   grows from left → right, revealing the tile like a horizontal wipe.
   Text/img inside sits above via z-index so it appears in sync with
   the black arriving beneath it. */
.process__fill {
  position: absolute;
  inset: 0;
  background: var(--ink);
  transform: scaleX(0);
  transform-origin: left center;
  will-change: transform;
  z-index: 0;
}
.process__step > h3,
.process__arrow > img {
  position: relative;
  z-index: 1;
}
/* Text tile — bottom-left copy with 30 px inner padding. */
.process__step {
  padding: 30px;
  display: flex;
  align-items: flex-end;
  justify-content: flex-start;
}
.process__step h3 {
  color: var(--paper);
  text-wrap: balance;
}

/* Effect 01 rise reveal on the three process step titles.
   Same mask recipe as .work__title / .names__title / etc.:
   each `data-fx-line` span is an overflow: hidden clip,
   char spans start below at yPercent 110 and rise to 0.
   Auto-wired by js/about-work-rise.js via the
   [data-fx="rise"] selector — no init call needed. */
.process__step__title[data-fx="rise"] { visibility: hidden; }
.process__step__title [data-fx-line] {
  display: block;
  overflow: hidden;
  line-height: 1.0;
  padding-bottom: 0.06em;
}
.process__step__title [data-fx-line] .char {
  display: inline-block;
  will-change: transform;
}
/* Arrow tile — r-arrow.svg centred inside the black tile. */
.process__arrow {
  display: grid;
  place-items: center;
}
.process__arrow img {
  /* Trimmed 50px → 35px (−30%) per Arun 2026-08-27 so the
     arrows read as a lighter connective mark between the text
     tiles instead of matching their weight. */
  max-height: 35px;
  width: auto;
  display: block;
}
/* Explicit column + row placements so corners land on grid lines. */
.process > *:nth-child(1) { grid-column: 1; grid-row: 2 / span 2; } /* Find     */
.process > *:nth-child(2) { grid-column: 2; grid-row: 4;         } /* ›  (bottom)*/
.process > *:nth-child(3) { grid-column: 3; grid-row: 2 / span 2; } /* Build    */
.process > *:nth-child(4) { grid-column: 4; grid-row: 4;         } /* ›  (bottom, matches arrow 1 per Arun 2026-08-27) */
.process > *:nth-child(5) { grid-column: 5; grid-row: 2 / span 2; } /* Bring    */

/* ── Reflect · Dual Wave text-on-scroll ─────────────────────
   Vanilla port of the Codrops Dual Wave animation. Two vertical
   columns of `.animated-text` rows — a sine-wave phase per row
   drives horizontal position, and the row closest to viewport
   centre lights up. Ground is paper white (parent .approach now
   uses `background: var(--paper)`), so rows fade from ink at low
   opacity → solid ink when focused. Init lives in
   js/about-reflect-wave.js. */
.reflect--wave {
  /* Only vertical padding — horizontal margin is already applied
     by the parent .approach section (`padding: … var(--col-margin) …`),
     so adding it again here would double-inset the wave columns. */
  padding: clamp(60px, 10vw, 100px) 0;
  display: flex;
  width: 100%;
  gap: 5vw;                          /* room between the two columns for the wave to travel */
  position: relative;
}
.reflect--wave .wave-column {
  flex: 1;
  display: flex;
  flex-direction: column;
  gap: 1.25rem;
  font-family: var(--font-display);
  font-weight: 700;
  /* Same clamp as the design system's .type--display-m so any
     future "display 50" role change ripples through automatically. */
  font-size: clamp(28px, 5vw, 50px);
  line-height: 0.9;                  /* tight stacking so the sine pattern reads as one composition */
  letter-spacing: 0.005em;
  text-transform: uppercase;
  position: relative;
  z-index: 2;
}
.reflect--wave .wave-column-left  { align-items: flex-start; }
.reflect--wave .wave-column-right { align-items: flex-end; }

/* Each wave row — dim by default; JS toggles `.focused` on the
   row closest to viewport centre, transitioning it to full ink
   black via CSS. `width: max-content` prevents wrapping when the
   x-translate applied by GSAP pushes the row horizontally. */
.reflect--wave .animated-text {
  width: max-content;
  color: rgba(0, 0, 0, 0.15);
  transition: color 300ms ease-out;
}
.reflect--wave .animated-text.focused {
  color: var(--ink);
}

/* ── SECTION 5 · WHAT'S NEXT ────────────────────────────── */
.contact {
  /* Static ink + paper. The Approach section above keeps its
     white ground; the transition between the two sections is
     no longer painted on .contact itself — a body-level fade
     curtain (`.fade-curtain`) fades in over the whole viewport
     when the "something real" line reaches 40vh from the
     viewport bottom, holds while Contact scrolls through, then
     fades back out before the flair-trail section arrives. See
     js/about-contact-transition.js. */
  background: var(--ink);
  color: var(--paper);
  /* No padding — .contact__hero handles vertical centering
     for the heading, and .contact__body is currently hidden
     (see the <template> wrapper around it in about.html), so
     there's nothing below the hero that needs breathing room.
     Restore the bottom padding to `clamp(200px, 16vw, 240px)`
     when the body / phone / email content comes back. */
  padding: 0 var(--col-margin) 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

/* ── Contact hero slot ──────────────────────────────────────
   Full-viewport-height container that centers the "So, what's
   next?" heading at the exact vertical middle of the viewport.
   Sized in `svh` so mobile browsers ignore address-bar toggles
   (else the heading dances as the URL bar collapses / expands
   during scroll). Padding-left/right is 0 because .contact's
   own padding already handles gutters — nesting them again
   would double the inset. */
.contact__hero {
  /* Scroll buffer only — the heading now lives outside this
     div as a position: fixed element (see .contact__title
     below). 200svh gives Contact ≈100vh of scroll after the
     curtain snap trigger completes, which the fall sequence
     eats through in three phases:
       • 0–96vh (approx)  snap-in trigger runs (curtain + title
                           fade in over first ~8vh, then hold
                           at 1 while the rest of the trigger
                           scrolls under the fixed heading)
       • 96–146vh         "nothing happens" — visitor scrolls
                           50vh in the black state, heading
                           stays put
       • 146–196vh        fall trigger runs — chars drop into
                           the mask as scroll advances (Effect
                           01 in reverse)
     Page bottom lands at ≈196vh past the snap trigger start
     so the fall completes exactly at the scroll ceiling. */
  min-height: 120svh;
  padding: 0;
}

/* Fixed slot that holds the title + subheadline stacked at
   viewport middle. All fixed-positioning + reveal (opacity)
   lives on this wrapper — the h2 and p sit inside as normal
   flow children so their vertical stacking + gap tokens come
   for free. Chars inside each `[data-fx-line]` handle the
   rise/fall via yPercent (see .contact__title__line and
   .contact__sub__line mask blocks below). */
.contact__caption {
  position: fixed;
  /* Centre in the viewport MINUS the nav footprint at the
     bottom. The .nav lives at `bottom: 40px` with a ~80px
     logo (100px × 148/186 aspect) → ~120px of chrome at the
     bottom. Shifting the fixed centre up by half of that
     (60px) puts the caption in the visual middle of the
     remaining usable area — the whole block reads centred
     against the nav bar as a stable ground line. Per Arun
     2026-08-27. */
  top: calc(50% - 60px);
  left: 50%;
  transform: translate(-50%, -50%);
  z-index: 1001;      /* above .fade-curtain (900) */
  opacity: 0;         /* JS flips to 1 after Splitting primes chars */
  max-width: calc(100vw - 2 * var(--col-margin));
  color: var(--paper);
  text-align: center;
  pointer-events: none;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: clamp(0.8rem, 1.8vw, 1.5rem);
}

.contact__title {
  /* No positioning here — .contact__caption owns viewport
     placement. Keep max-content so long strings don't span
     the whole caption width. */
  width: max-content;
  max-width: 100%;
  color: inherit;
}

.contact__sub {
  width: max-content;
  max-width: 100%;
  color: inherit;
}

/* Phone + email lines. Effect 02's transform-origin is the
   left-middle edge (0% 50%) so the un-skew reads as a
   paragraph straightening, matching the reference used in
   .work__intro. `will-change` lets the browser hoist both
   transform and opacity to their own layers during the scrub. */
.contact__line {
  width: max-content;
  max-width: 100%;
  color: inherit;
  transform-origin: 0% 50%;
  will-change: transform, opacity;
}
/* Anchors need pointer-events restored — .contact__caption
   sets `pointer-events: none` on the whole slot so the fixed
   overlay doesn't swallow clicks in the section above it. */
.contact__line a {
  color: inherit;
  text-decoration: none;
  pointer-events: auto;
  transition: opacity 200ms ease;
}
.contact__line a:hover { opacity: 0.6; }
/* Chat trigger — underlined so the phrase reads as an action
   (phone / email don't need it because the format itself
   telegraphs "link"). Applied to the anchor so the underline
   is CONTINUOUS across word breaks (per Arun 2026-08-27 -
   the per-word variant was leaving gaps at the whitespace).
   To keep the stroke from leaking pre-reveal, the whole <p>
   fades in via GSAP alongside the word-opacity tween (see
   about-contact-transition.js) - so underline opacity tracks
   paragraph opacity, and never shows above still-invisible
   words. */
.contact__line--chat a {
  text-decoration: underline;
  text-underline-offset: 3px;
  text-decoration-thickness: 1px;
}

/* Fall-out mask (Effect 01 in reverse).
   The .contact__title__line span acts as an `overflow: hidden`
   clip; per-char `.char` spans (added at runtime by Splitting.js
   in js/about-contact-transition.js) sit at yPercent: 0 until
   the fall trigger scrubs them down to yPercent: 110, pushing
   each char below the mask edge with a per-char stagger. Same
   recipe as .work__title__line / .approach__title__line, just
   run in reverse. `padding-bottom: 0.04em` gives the mask a
   sliver of extra room so ascenders / descenders don't clip
   before the tween is meant to hide them. */
.contact__title__line {
  display: block;
  overflow: hidden;
  line-height: 1.0;
  padding-bottom: 0.04em;
}
/* Desktop: hide the mobile-only line break inserted between
   "So, what's" and "next?" so desktop keeps the phrase on one
   line. The mobile block re-enables it via display: initial. */
.contact__title__break-mobile { display: none; }
.contact__title__line .char {
  display: inline-block;
  will-change: transform;
}

/* Same mask recipe for the subheadline, tuned for the body
   font's descender + line-height. type--body-l uses
   line-height 1.2; we tighten to 1.0 on the mask line so
   `overflow: hidden` clips cleanly at yPercent 110, and bump
   padding-bottom to 0.15em so descenders (the "y" in "stay")
   stay visible at yPercent 0. */
.contact__sub__line {
  display: block;
  overflow: hidden;
  line-height: 1.0;
  padding-bottom: 0.15em;
}
.contact__sub__line .char {
  display: inline-block;
  will-change: transform;
}

/* Reduced-motion path: skip the whole fixed-caption / curtain
   / rise / fall choreography. Contact renders as a static ink
   section with the caption in the natural DOM flow. The JS in
   js/about-contact-transition.js bails at boot when this pref
   is set, so this rule is what actually makes the title + sub
   readable in that case. */
@media (prefers-reduced-motion: reduce) {
  .contact__hero { min-height: auto; }
  .contact__caption {
    position: static;
    transform: none;
    max-width: 100%;
    opacity: 1;
    padding: clamp(60px, 10vw, 100px) var(--col-margin);
  }
  .contact__title__line,
  .contact__sub__line { overflow: visible; }
  .contact__line {
    transform: none;
    transform-origin: initial;
  }
  .contact__line .word { opacity: 1 !important; }
}

/* ── Fade curtain (Approach → Contact transition) ───────────
   Full-viewport black overlay driven by ScrollTrigger. Sits
   above the page content but below the nav / menu / eye-blink
   overlay so those keep their own affordances during the fade.
   `pointer-events: none` so it never intercepts clicks — the
   underlying page stays interactive throughout the transition
   (which matters most while the curtain is fading in/out at
   partial opacity). Starts fully transparent; opacity is
   entirely GSAP-controlled after that. */
.fade-curtain {
  position: fixed;
  inset: 0;
  background: #000;
  opacity: 0;
  z-index: 900;
  pointer-events: none;
  will-change: opacity;
}
@media (prefers-reduced-motion: reduce) {
  .fade-curtain { display: none; }
}
.contact__body {
  display: flex;
  flex-direction: column;
  gap: var(--space-6);
  font-family: var(--font-body);
  font-weight: 700;
  font-size: clamp(22px, 2.4vw, 30px);
  line-height: 1.2;
}
.contact__pair {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--space-3);
  align-items: start;
}
.contact__pair p { margin: 0; }
.contact__pair a {
  text-decoration: underline;
  text-underline-offset: 4px;
  text-decoration-thickness: 1px;
  transition: opacity 200ms ease;
}
.contact__pair a:hover { opacity: 0.55; }

/* ── SECTION 6 · FLAIR TRAIL ────────────────────────────────
   Bottom-of-page cursor-flair effect (see js/about-flair.js).
   The section itself just holds space + a soft "wiggle your
   mouse" hint centred in it; the .flair-trail__item images
   are fixed-positioned so JS can pop them anywhere in the
   viewport at the cursor location as the visitor moves the
   mouse over this section. */
.flair-trail {
  /* Full-viewport fixed overlay layered between the .fade-
     curtain (z 900) and the .contact__caption (z 1001). Comes
     alive only when the curtain reaches full black — see
     js/about-contact-transition.js, which fades opacity 0 → 1
     during the curtain snap and flips pointer-events to auto
     exactly at CURTAIN_SNAP so the visitor can wiggle their
     mouse over the black. Reverses cleanly on scroll-up via
     the scrub timeline. */
  position: fixed;
  inset: 0;
  z-index: 950;
  display: grid;
  place-items: center;
  overflow: hidden;                  /* keep landed flairs from spilling out of the viewport */
  isolation: isolate;                /* stacking context so icon z-index stays local */
  opacity: 0;
  pointer-events: none;              /* JS toggles to 'auto' once the curtain is fully black */
  color: var(--paper);               /* Wiggle-your-mouse hint reads white on the black curtain */
}
.flair-trail__hint {
  color: var(--mute);
  text-align: center;
  pointer-events: none;
  user-select: none;
}
.flair-trail__item {
  position: fixed;
  top: 0;
  left: 0;
  width: 50px;
  opacity: 0;
  pointer-events: none;
  z-index: 5;
  will-change: transform, opacity;
}

/* Reduced-motion path: hide the flair items entirely so no
   spinning stickers fly across the viewport. Hint text stays. */
@media (prefers-reduced-motion: reduce) {
  .flair-trail__item { display: none; }
}

/* ── RESPONSIVE ─────────────────────────────────────────── */
@media (max-width: 900px) {
  /* Same proportions as main site's mobile chrome — smaller edge
     margins, shorter logo, rule hidden to save space. */
  .nav          { left: 24px; right: 24px; bottom: 24px; gap: 14px; }
  .nav__logo    { width: 64px; }
  .nav__rule    { display: none; }
  .nav__links   { gap: 20px; }
  .nav__links a { font-size: 14px; }
  .nav__close   { width: 40px; height: 40px; }
  .nav__close svg { width: 20px; height: 20px; }

  .about-blur   { height: 146px; }         /* -10% again for mobile too */

  .intro         { grid-template-columns: 1fr; column-gap: 0; row-gap: var(--space-3); padding-top: clamp(120px, 20vw, 160px); }
  .intro__left,
  .intro__right  { grid-column: 1 / -1; }
  .intro__mark   { text-align: left; }
  /* Uniform vertical rhythm between the three intro blocks
     ("Alright…" → "I'M ARUN" → "I design brands…"). Desktop keeps
     its own tighter var(--space-1) inside .intro__left. Per Arun
     2026-08-30, mobile only. */
  .intro__left   { gap: var(--space-3); }
  /* Tight bottom padding on mobile so the Work section's heading
     ("So, here's what I've been up to.") sits directly under the
     intro block instead of ~180px below it. First-fold height
     shrunk to 90vh so the Work section peeks above the fold
     sooner. Per Arun 2026-08-30, mobile only. */
  .intro         { padding-bottom: var(--space-3); min-height: 90vh; }

  /* Tighten the gap between "And here's a little of / what came
     out of it." and the first case card. Desktop keeps its 60 px
     rhythm (caption padding-bottom 30 + .cases flex-gap 30). On
     mobile only the flex-gap (30 px) survives — the caption's own
     padding-bottom drops to 0 so the card sits ~30 px under the
     caption text. Per Arun 2026-08-30, mobile only. */
  .cases__caption { padding-bottom: 0; }

  /* ── MOBILE case-sticky card ─────────────────────────────
     Implements the Figma frame 338:77 mobile layout for the
     case-study cards. On desktop each card is: 4 meta rows
     (Project / Company / Agency / Role) sitting beside a
     logo + image grid inside a 500 px sticky. On mobile the
     meta rows are hidden entirely (per Arun 2026-08-30 —
     "remove all the details like company name, project,
     agency, everything") and the card compresses to a
     Figma-matched stack: wordmark logo on top, project
     image below, tight 12 px inner padding. Sticky height
     shrinks to 280 px so the scroll range matches the
     smaller card. */
  .case-sticky                { height: 280px; }
  /* 20 px vertical breathing room between adjacent cards —
     matches Figma frame 338:77's outer p-[20px] wrapper. */
  .case-sticky + .case-sticky { margin-top: 20px; }
  /* Explicit long-hand paddings so the shorthand can't be
     overridden by the desktop rule's own explicit
     padding-bottom: 30px reinforcement. Bottom padding is
     20px so a visible black strip stays below the image
     when the image's own bottom edge happens to be dark
     (12px black-on-dark reads as flush; 20px reads clearly
     as a separate band). */
  .case-sticky__inner {
    padding-top:    12px;
    padding-right:  12px;
    padding-bottom: 20px;
    padding-left:   12px;
    gap: 12px;
  }
  .case-sticky__meta          { display: none; }
  .case-sticky__body {
    grid-template-columns: 1fr;
    grid-template-rows: 60px minmax(0, 1fr);
    gap: 12px;
  }
  .case-sticky__logo {
    /* Wordmark: 42 px tall × up to 180 px wide, left-aligned
       inside the 60 px row (matches Figma frame 338:121). */
    height: 42px;
    width: auto;
    max-width: 180px;
    object-fit: contain;
    object-position: left center;
    justify-self: start;
    align-self: center;
  }

  /* Kill the pin-and-scroll on the "A little bit of everything"
     tube frame on mobile. Desktop keeps its 170 vh wrap (so the
     100 vh sticky pins for 70 vh of scroll while the tube spins);
     on mobile the wrap collapses to 100 vh so the section flows
     past like a normal one-viewport block — no pinning, tube
     rotates in-view then scrolls away with the rest of the page.
     Per Arun 2026-08-30, mobile only. */
  .everything-wrap { height: 100vh; }

  /* Both Work grids collapse to a single column at mobile. Reset
     the desktop `grid-column: 1 / span 4` and `5 / span 2` spans
     on the children so they take the full row instead of trying
     to span into columns that no longer exist. */
  .work__head    { grid-template-columns: 1fr; gap: var(--space-2); }
  .work__head > h2,
  .work__head > .work__intro,
  .cases__promo > p,
  .cases__promo > .card--promo { grid-column: 1 / -1; }

  .cases__promo  { grid-template-columns: 1fr; }
  .card--promo   { height: 160px; }
  .meta-grid     { grid-template-columns: 1fr; gap: var(--space-2); }
  .card__body    { grid-template-columns: 1fr; }
  .card__body .placeholder-logo,
  .card__body .placeholder-image { min-height: 140px; }
  .card--wide    { flex-direction: column; height: auto; }
  .card--wide .placeholder-logo,
  .card--wide .placeholder-image { width: 100%; height: 140px; }
  .card--split   { flex-direction: column; height: auto; }
  .card--split .placeholder-image { width: 100%; height: 140px; }

  .names         { grid-template-columns: 1fr; gap: var(--space-3); }
  /* Tighter marquee gap on mobile — .names__row is no longer the
     flex container itself; the track handles logo spacing. */
  .names__track  { gap: var(--space-3); }

  /* Process on mobile: keep the L→R sequential read from desktop
     by laying the 5 children out on a SINGLE row — step | arrow |
     step | arrow | step. Text shrinks to 14 px so the two-line
     phrases ("Find / the story" etc.) fit inside the narrower
     tiles; arrows scale to a light 18 px connective. Tile
     min-height drops from 90 px so the sequence reads as a
     compact stepper instead of a tall stack. Per Arun 2026-08-30,
     mobile only. */
  .process {
    grid-template-columns: 1fr auto 1fr auto 1fr;
    grid-template-rows: auto;
    gap: 0;                          /* tiles touch so the row reads as one continuous stepper strip, matches desktop */
    padding: var(--space-3) 0;
    min-height: 0;
  }
  .process__step {
    min-height: 90px;
    padding: 12px;
    align-items: center;             /* vertically centre in tile */
    justify-content: center;         /* horizontally centre the text block in tile */
  }
  .process__step h3 { text-align: left; }   /* inside the block, "Find" / "the story" stay left-aligned to each other */
  .process__step h3 {
    font-size: 14px;
    line-height: 1.15;
    letter-spacing: 0;
  }
  .process__arrow img {
    max-height: 18px;
    transform: none;                 /* horizontal, matches desktop */
  }
  .process > *:nth-child(1) { grid-column: 1; grid-row: 1; }
  .process > *:nth-child(2) { grid-column: 2; grid-row: 1; }
  .process > *:nth-child(3) { grid-column: 3; grid-row: 1; }
  .process > *:nth-child(4) { grid-column: 4; grid-row: 1; }
  .process > *:nth-child(5) { grid-column: 5; grid-row: 1; }

  /* Reflect wave on mobile: only the LEFT column ("I explore… /
     vibe coding") remains. The RIGHT column ("I ride…") is hidden
     entirely per Arun 2026-08-30 so mobile visitors get a single
     focused monologue instead of the desktop dual-column dance.
     about-reflect-wave.js still finds .wave-column-right (query
     selector reads through display: none) and initialises against
     it, but a display:none element has offsetWidth 0 so its wave
     range collapses to 0 — no visible effect, no error. */
  .reflect--wave {
    flex-direction: column;
    gap: 3rem;
  }
  .reflect--wave .wave-column { gap: 2rem; }
  .reflect--wave .wave-column-right { display: none; }

  /* Contact title break — the invisible <br class="contact__title__break-mobile">
     inserted in the h2 becomes an active line break on mobile so
     "So, what's" and "next?" stack on two lines. Desktop keeps the
     phrase on one line (rule below hides the <br>). */
  .contact__title__break-mobile { display: initial; }

  /* Mobile chrome z-index bump lives inside the ≤767 block below —
     that block sets z-index: 100 on each toggle, which would beat
     any rule placed here in the 900 block via source-order cascade. */

  /* Flair-trail on mobile: dead on touch (mousemove only, no touch
     equivalent), and the "Wiggle your mouse" copy reads as broken.
     Hide the whole section — better than a dead-end interaction. */
  .flair-trail { display: none; }

  .contact__pair { grid-template-columns: 1fr; gap: var(--space-3); }
  .contact__body { gap: var(--space-3); }
}

@media (max-width: 480px) {
  .nav__links a  { font-size: 12px; letter-spacing: 0.12em; }
  .nav__links    { gap: 10px; }
}

/* ── MOBILE FOOTER (About page) ─────────────────────────────
   Clones the home page mobile footer 1-to-1 in structure, then
   inverts the palette: white background, black logo, black
   icons/text. The classic `.nav` at the top is hidden on mobile
   so only this footer shows. Desktop is untouched. */

/* Everything below only paints on mobile; on desktop the whole
   home-footer cluster is display: none so the classic `.nav`
   remains unchanged. */
.hero__chrome,
.sound-toggle,
.scene-nav,
.about-mobile-close,
.about-hint-chat { display: none; }

@media (max-width: 767px) {
  /* Hide the classic top nav — the new footer replaces it. */
  .nav { display: none; }

  /* Bump the desktop `.about-blur` gradient-blur strip to fully cover
     the mobile footer (164 px) plus a small fade-in above. `.about-blur`
     lives at z-index 90 — the footer + toggles sit above it (z ≥ 100)
     and composite against it via mix-blend-mode. */
  .about-blur { height: 200px; }

  /* Footer chrome — transparent wrapper, mix-blend-mode: difference
     on the WRAPPER (matching the desktop `.nav` recipe on line 250)
     so all children auto-invert as a group against the backdrop. */
  .hero__chrome {
    display: grid;
    position: fixed;
    top: auto;
    bottom: 0;
    left: 0;
    right: 0;
    height: 164px;
    padding: 32px 20px 0;
    background: transparent;
    z-index: 1200;                           /* above .about-blur (90) AND .contact__caption (1001) so the Contact section doesn't hide the menu */
    grid-template-columns: 45px 1fr auto;
    grid-template-rows: 33px 1px;
    column-gap: 16px;
    row-gap: 20px;
    align-items: center;
    align-content: start;
    pointer-events: none;
    mix-blend-mode: difference;
  }
  .hero__chrome > * { pointer-events: auto; }

  /* Logo — plain white PNG; the wrapper's difference blend inverts it. */
  .brand {
    grid-column: 1;
    grid-row: 1;
    width: 45px;
    height: 33px;
    display: block;
  }
  .brand__img {
    display: block;
    width: 100%;
    height: 100%;
    object-fit: contain;
  }
  .hero__rule--audio {
    grid-column: 1 / -1;
    grid-row: 2;
    height: 1px;
    width: 100%;
    background: #fff;
    opacity: 0.5;
    pointer-events: none;
  }
  .hero__rule--audio .hero__rule__svg { display: none; }
  .site-nav {
    grid-column: 3;
    grid-row: 1;
    align-self: end;
    gap: 4px;
    display: flex;
    flex-direction: column;
    align-items: flex-end;
  }
  .site-nav a {
    font-family: var(--font-body);
    font-size: 12px;
    font-weight: 700;
    letter-spacing: 0.02em;
    color: #fff;
    text-decoration: none;
    /* Force "WHO'S RIDING" onto two right-aligned lines. `width: min-content`
       shrinks the anchor to the width of its longest word, so the space
       between the two words becomes a wrap point. Matches home mobile. */
    display: inline-block;
    width: min-content;
    text-align: right;
    line-height: 1.2;
  }

  /* Scene-nav arrows do not belong on the About page — the mobile
     close button (below) sits in the right arrow's slot instead. */
  .scene-nav { display: none; }

  /* Bottom row (from left to right): Sound at the extreme left, Night
     centred on the viewport, Close × at the extreme right. All at
     bottom: 32 px so the below-line gap (20 px) equals the above-line
     gap (20 px) — matches home mobile spacing. */
  .sound-toggle {
    display: grid;
    place-items: center;
    position: fixed;
    top: auto;
    bottom: 32px;
    left: 20px;
    right: auto;
    transform: none;
    width: 26px;
    height: 26px;
    padding: 0;
    background: transparent;
    border: none;
    color: #fff;
    mix-blend-mode: difference;
    cursor: default;
    z-index: 1200;                           /* above .contact__caption (1001) so the Contact section doesn't hide the menu */
  }
  .sound-toggle__icon {
    grid-area: 1 / 1;
    width: 20px;
    height: 20px;
    transition: opacity 200ms ease;
  }
  .sound-toggle__icon--off { opacity: 0; }

  .night-toggle {
    top: auto;
    bottom: 32px;
    left: 50%;
    right: auto;
    transform: translateX(-50%);
    width: 26px;
    height: 26px;
    z-index: 1200;                           /* above .contact__caption (1001) so the Contact section doesn't hide the menu */
  }
  .night-toggle__icon { width: 20px; height: 20px; }

  /* About-only close button — right corner, replaces the right arrow. */
  .about-mobile-close {
    display: grid;
    place-items: center;
    position: fixed;
    top: auto;
    bottom: 32px;
    right: 20px;
    left: auto;
    width: 26px;
    height: 26px;
    padding: 0;
    background: transparent;
    border: none;
    cursor: pointer;
    mix-blend-mode: difference;
    z-index: 1200;                           /* above .contact__caption (1001) so the Contact section doesn't hide the menu */
  }
  .about-mobile-close img {
    display: block;
    width: 22px;
    height: 22px;
    object-fit: contain;
  }

  /* Mobile-only desktop-hint chat — centred over the viewport, sits
     just above the 164 px footer. Bubbles fade + rise like the home
     hero-chat. */
  .about-hint-chat {
    display: flex;
    position: fixed;
    left: 50%;
    bottom: 200px;                        /* clears footer (164) + gap */
    transform: translateX(-50%);
    z-index: 200;
    flex-direction: column;
    align-items: center;
    gap: 8px;
    max-width: min(80vw, 300px);
    pointer-events: none;
  }
  .about-hint-chat__bubble {
    padding: 6px 12px;
    background: #fff;
    color: #111;
    border-radius: 12px;
    font-family: 'Inter', system-ui, sans-serif;
    font-weight: 400;
    font-size: 11px;
    line-height: 1.4;
    text-align: center;
    opacity: 0;
    transform: translateY(6px);
    transition: opacity 500ms ease, transform 500ms ease;
    will-change: transform, opacity;
    word-wrap: break-word;
  }
  .about-hint-chat__bubble.is-in  { opacity: 1; transform: none; }
  .about-hint-chat__bubble.is-out { opacity: 0; transform: translateY(6px); }
}

