/* =============================================================================
   RESPONSIVE BREAKPOINT MAP   (v26 — last updated 2026-05-23)
   -----------------------------------------------------------------------------
   WHY THIS EXISTS: the FG plates are HEIGHT-DRIVEN (--about-fg-w, 71vh, seat/MG
   %-of-width), so a single value across a width/aspect RANGE drifts as the
   viewport changes. Tunings are therefore SCOPED into bands, each targeting a
   device class. EDITING A SHARED BAND CHANGES EVERY DEVICE IN IT — the recurring
   footgun (e.g. an A16 seat tweak silently moved the iPad Pro 13"). When a single
   device needs different values from its band-mates, split by width OR aspect.

   PORTRAIT — width bands
     max-width:767 ........................ phones (390 / 375 / 360 ...) — base phone tier
     max-width:767 + max-height:700 ....... short phones (iPhone SE, 667 tall) — Projects seats drop
     740-767 portrait ..................... iPad mini (744): sub-768, pulled UP into the tablet
                                            treatment for Projects seats/marquee + Colour FG + About nudge
     740-1199 portrait .................... tablet-tier FLOOR (lowered 768->740 for the mini) —
                                            Projects seats/MG/marquee + About copy 30px nudge live here
     768-1199 portrait .................... 4:3 iPads (768 classic, 1024 Pro 13") at base tablet values
     800-900 portrait ..................... 11" iPads (Air/A16 820, Pro 834) + Galaxy Tab — fuller seats
       + max-aspect-ratio:13/20 ........... Galaxy Tab S9 (800, 16:10) ASPECT override (taller frame):
                                            bigger/repositioned seats + MG; excludes the ~0.69 iPads
     >=1200 ............................... desktop — About copy gap-centering (computed margin-left)

   ASPECT / ULTRAWIDE
     min-aspect-ratio:2/1 & min-width:2400  ultrawide 21:9 (3440) — scenes sink / recompose
     min-aspect-ratio:3/1 ................. 32:9 super-ultrawide (5120) — further scoped overrides:
                                            Projects seats/marquee, Cinema MG/BG, Colour FG
                                            (About "settled-left" fgLeftX is in app.js, same >=3:1 gate)

   LANDSCAPE — short viewport heights
     landscape max-height:600 ............. minor Projects MG nudge
     landscape max-height:500 ............. phone landscape, chrome-hidden (~390-430 tall)
     landscape max-height:520 + coarse .... phone landscape, real device — compact About/Cinema/Colour copy
     landscape max-height:360 + coarse .... phone landscape, Safari chrome SHOWN (~322 tall)
       ^ the +coarse landscape blocks DO NOT render in desktop Playwright (reports
         pointer:fine -> desktop fallback). VERIFY ON REAL iOS/Android (Layer-B device trip).

   OTHER
     hover:none / pointer:coarse .......... touch-device tweaks (16px form fields, etc.)
     prefers-reduced-motion:reduce ........ a11y — smooth-scroll off, atmosphere/char-wave/racks gated
                                            (accepted ~80% state per 2026-05-20 decision; pinned pan not gated)
   ============================================================================= */

/* Disable iOS Safari text auto-inflation (especially landscape) */
html{
  -webkit-text-size-adjust: 100%;
  text-size-adjust: 100%;
}

.cursor-dot{
  position: fixed;
  width: 10px;
  height: 10px;
  background: #c8a96e;
  border-radius: 50%;
  pointer-events: none;
  z-index: 9998;
  mix-blend-mode: exclusion;
  opacity: 0.7;
  transform: translate(-50%, -50%);
  left: -40px;
  top: -40px;
}
@media (hover: none) and (pointer: coarse){
  .cursor-dot{ display: none; }
}
@media (prefers-reduced-motion: reduce){
  .cursor-dot{ display: none; }
}
:root{
  --text: rgba(255,255,255,0.92);
  --bg: #000;
  --gray-18: #767676;
  --scene-vh: 100svh;
  /* Visible viewport height — JS overrides via window.visualViewport.height
     so rendered plate boxes shrink with iOS Safari chrome animation in
     real time, not after the dvh recompute lag. ONLY route rendered plate
     height through --vvh; pin offsets and structural layout must stay on
     --scene-vh or the pin system drifts. */
  --vvh: 100svh;
  --scene-copy-left: clamp(22px, 6vw, 84px);
  /* about/fg.avif intrinsic: 684×1380 — rendered width capped so it
     can't exceed ~42vw on tall/narrow viewports (iPad portrait) */
  --about-fg-w: min(49.57vh, 50vw);
  /* ── Camera-move easing system (2026-05-20) ──────────────────────────
     Project-specific curves replacing the shared cubic-bezier(.22,.61,.36,1)
     default (Benoist). Each models a real lens/camera move. All stay within
     0–1 on every control point — no overshoot — so transforms can't exceed
     the 6% plate overscan budget. Scope: discretionary CSS transitions only;
     scroll-scrubbed GSAP timelines stay ease:'none'. macOS contact-card
     min/expand keeps its Material curve by design (UI grammar, not a camera). */
  --ease-focus-pull: cubic-bezier(.62,.04,.34,1);  /* follow-focus: weighted off the mark, decel into the target plane */
  --ease-dolly-track: cubic-bezier(.45,.05,.25,1); /* lateral track: camera locks onto the new position, long settle */
  --ease-push-in: cubic-bezier(.34,.08,.28,1);     /* dolly-in: weighted start, strong decel, settles into the subject */
}

@supports (height: 100dvh){
  :root{
    --scene-vh: 100dvh;
    --vvh: 100dvh;
  }
}

*{ box-sizing:border-box; }
html,body{ height:100%; overscroll-behavior: none; }
body{
  margin:0;
  font-family: ui-serif, Georgia, "Times New Roman", serif;
  background: var(--bg);
  color: var(--text);
  overflow-x:hidden;
}

/* Cinema intro — just a black overlay now. Logo + progress bar live in
   .hero-overlay which sits above this z-index. */
.cinema-intro{
  position: fixed;
  inset: 0;
  z-index: 9999;
  background: #000;
  pointer-events: none;
  transition: opacity 900ms ease;
}
.cinema-intro.is-out{
  opacity: 0;
}
@media (prefers-reduced-motion: reduce){
  .cinema-intro{ transition: none; }
}

/* Top nav — logo-left / nav-right. Hidden at hero rest so the boot
   composition is uncluttered, slides down on first scroll, then stays
   visible permanently. The previous direction-sensitive tuck was flagged
   by craft review as fidgety — sleek nav grammar (Active Theory, Resn,
   Felix) is "appears once, behaves predictably," not "comes and goes." */
.topnav{
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  /* Nav (and its gold progress bar) sits above the marquee transit
     (10001) and the pinned hero overlay (10000). Function on top —
     navigation is never occluded by the foreground prop sliding past. */
  z-index: 10002;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0 clamp(16px, 3vw, 32px);
  /* Trimmed from clamp(44px, 5.4vh, 56px). Smaller logo (below) leaves
     air around the links without making them feel squished — links
     hold at 13px which is now ~28% of nav height vs the previous ~23%,
     i.e. they earn more proportional weight (function over identity). */
  height: clamp(40px, 4.8vh, 48px);
  background: rgba(0,0,0,0.55);
  backdrop-filter: blur(8px);
  box-shadow: 0 8px 22px rgba(0,0,0,0.32);
  transform: translateY(-100%);
  /* Cinematic entrance, not a UI snap. The bar eases down into position
     the moment the hero scene resolves (fired in app.js dismiss(), not on
     scroll) so visitors see the nav exists from the first beat — fixes the
     "hero feels like there's nothing to do" report. Softer/longer ease than
     the old 420ms; the wordmark and links stage in after via their own
     delayed transitions below, all off this single .is-revealed toggle. */
  transition: transform 1150ms cubic-bezier(0.22, 1, 0.36, 1);
}
.topnav.is-revealed{
  transform: translateY(0);
}
.logo{
  display: inline-flex;
  align-items: center;
  text-decoration: none;
  flex-shrink: 0;
  position: relative;   /* anchor for the gold wordmark overlay (.logo-gold) */
}
.logo img{
  /* Wordmark sized to roughly match the nav-link cap-height (links are
     13px Inter). Identity reads as a peer of the nav, not a billboard. */
  height: clamp(12px, 1.35vw, 17px);
  width: auto;
  display: block;
}
/* Gold wordmark twin — cross-fades in when the logo is the active nav item
   (hero scene) or on hover, mirroring the text links going #c8a96e. Same SVG
   shape in gold, stacked exactly over the white wordmark, so it reads as the
   wordmark recolouring, not a swap. Opacity fade matches the links' 180ms
   colour transition. The white original stays as the always-present fallback.
   2026-06-22. */
.logo .logo-gold{
  position: absolute;
  top: 0;
  left: 0;
  height: 100%;
  width: auto;
  opacity: 0;
  transition: opacity 180ms ease;
  pointer-events: none;
}
.logo:hover .logo-gold,
.logo.is-active .logo-gold{ opacity: 1; }
/* Cross-fade the WHITE wordmark OUT as the gold fades in, so only ONE copy is
   ever fully opaque at rest. Two stacked opaque copies let the white's
   anti-aliased edges peek a hair around the gold — a faint fringe, worst on
   Windows sub-pixel/ClearType rendering. (Scott flagged it 2026-06-22.) */
.logo img:not(.logo-gold){ transition: opacity 180ms ease; }
.logo:hover img:not(.logo-gold),
.logo.is-active img:not(.logo-gold){ opacity: 0; }
.topnav nav{
  display: flex;
  align-items: center;
  gap: clamp(14px, 3vw, 34px);
}
/* Timeline playhead — gold bar at bottom of nav */
.nav-progress{
  position: absolute;
  bottom: 0;
  left: 0;
  height: 1.5px;
  width: 0%;
  background: rgba(200, 169, 110, 0.7);
  transform-origin: 0 0;
  will-change: width;
  pointer-events: none;
  z-index: 1;
}
.topnav a{
  color: rgba(255,255,255,0.85);
  text-decoration: none;
  letter-spacing: 0.08em;
  font-family: 'Inter', ui-sans-serif, system-ui, -apple-system, sans-serif;
  font-size: 13px;
  transition: color 180ms ease;
}
.topnav a:hover{ color: #c8a96e; }
.topnav a.is-active{
  color: #c8a96e;
}

/* --- Cinematic nav entrance (staged) --------------------------------------
   After the bar eases down (above), the wordmark fades up and the links fade
   in left→right. Each beat is a CSS transition-delay relative to the single
   .is-revealed toggle — no JS animation loop. Delays play once (the nav stays
   revealed for the session), so repaints never re-trigger the entrance.
   Specificity note: .topnav .logo / .topnav nav a deliberately outrank the
   .topnav a colour-only transition above so opacity+transform aren't lost. */
.topnav .logo{
  opacity: 0;
  transform: translateY(-6px);
  transition: opacity 760ms ease,
              transform 760ms cubic-bezier(0.22, 1, 0.36, 1);
}
.topnav.is-revealed .logo{
  opacity: 1;
  transform: translateY(0);
  transition-delay: 620ms;   /* enters as the slower bar settles */
}
.topnav nav a{
  opacity: 0;
  transform: translateY(-4px);
  transition: opacity 620ms ease,
              transform 620ms cubic-bezier(0.22, 1, 0.36, 1),
              color 180ms ease;   /* preserve hover/active colour transition */
}
.topnav.is-revealed nav a{
  opacity: 1;
  transform: translateY(0);
}
/* Left→right stagger — PROJECTS first, CONTACT last. ~110ms between beats. */
.topnav.is-revealed nav a:nth-child(1){ transition-delay: 920ms; }
.topnav.is-revealed nav a:nth-child(2){ transition-delay: 1030ms; }
.topnav.is-revealed nav a:nth-child(3){ transition-delay: 1140ms; }
.topnav.is-revealed nav a:nth-child(4){ transition-delay: 1250ms; }
.topnav.is-revealed nav a:nth-child(5){ transition-delay: 1360ms; }

/* Reduced motion — present the nav fully, no slide/fade/stagger. */
@media (prefers-reduced-motion: reduce){
  .topnav{ transition: none; }
  .topnav .logo,
  .topnav nav a{
    opacity: 1;
    transform: none;
    transition: color 180ms ease;
    transition-delay: 0s;
  }
}

/* Scene */
.scene{
  position: relative;
  min-height: var(--scene-vh);
  overflow: hidden;
  padding-top: 0;
}

/* Hero → Projects horizontal pan wrap.
   Wrap: 100vw viewport window, clips horizontal overflow.
   Track: 200vw flex row holding #hero and #projects side-by-side.
   GSAP pins the wrap and scrubs the track's x translate for the dolly. */
.hero-projects-pan{
  position: relative;
  width: 100vw;
  height: var(--scene-vh);
  min-height: var(--scene-vh);
  overflow: hidden;
}
.hero-projects-pan-track{
  position: relative;
  display: flex;
  flex-direction: row;
  width: 300vw;
  height: 100%;
  will-change: transform;
  transform: translate3d(0,0,0);
}
.hero-projects-pan-track > .scene{
  flex: 0 0 100vw;
  width: 100vw;
  height: 100%;
  min-height: 100%;
  max-height: 100%;
}

.scene-media{ position:absolute; inset:0; z-index: 1; }

.layer{
  position:absolute;
  inset:0;
  width:100%;
  height:100%;
  object-fit: cover;
  will-change: transform;
  transform: translate3d(0,0,0);
}

/* <picture> is a wrapper for AVIF source + raster fallback only; it must
   never generate a box of its own, or in-flow images (marquee plate) would
   size against an inline picture instead of their real parent. display:contents
   makes the wrapper layout-transparent so the <img> behaves as a direct child
   of the picture's parent. (Selector matching is unaffected — the codebase
   uses descendant/class selectors, no direct-child img combinators.) */
picture{ display: contents; }

/* Plates bleed, always — 6% overscan on image/video layers absorbs
   dynamic viewport reflow (iOS Safari URL bar), sub-pixel rounding, and
   parallax transform offset. Dimension-based so it doesn't fight GSAP
   transform ownership. Div containers (.projects-mid-container etc.)
   are intentionally excluded — they're layout, not plates. */
img.layer, video.layer{
  inset: -3%;
  width: 106%;
  height: 106%;
}

.layer-mid, .layer-fg{ pointer-events:none; }

/* Overlay */
.scene-overlay{
  position: relative;
  z-index: 5;
  padding: clamp(22px, 6vh, 64px) clamp(16px, 6vw, 84px);
  max-width: 1200px;
  margin-inline: auto;
}

/* HERO brand overlay — SVG logo + tagline + boot progress bar.
   Position: fixed keeps it at viewport center regardless of pan-track
   translation (no counter-translation needed). z-index 10000 sits above
   .cinema-intro (9999) so the brand is visible during the preloader —
   seamless handoff: the logo doesn't move at all when the black dismisses. */
.hero-overlay{
  position: fixed;
  /* Vertical centering routed through --vvh so the wordmark sits at the
     visible viewport centre on iOS Safari with chrome shown — `50vh`
     resolves to the large (chrome-hidden) viewport on iOS, which centres
     against 430px on a phone where the visible area is 322px, dropping
     the wordmark to ~67% down the visible frame. --vvh tracks
     window.visualViewport.height in real time. Horizontal stays on vw —
     no chrome on the horizontal axis. */
  top: calc(var(--vvh) * 0.5);
  left: 50vw;
  text-align: center;
  padding: 0;
  margin: 0;
  pointer-events: none;
  z-index: 10000;
  will-change: transform, opacity;
}
/* SVG logo initial state for stroke-draw + gold-wave-fill animation.
   Paths start invisible (wave gradient stops hidden via GSAP-animated
   offset) with a white 1px stroke. GSAP sets stroke-dasharray +
   stroke-dashoffset on load and tweens the dashoffset to draw each
   letter, then tweens the linearGradient stops' offsets to slide a
   gold wave fill from left to right across the logo. When the wave
   reaches 100%, the .wave-complete class transitions stop-color from
   gold to white. */
svg.hero-logo path{
  fill: url(#logo-wave-fill);
  stroke: #fff;
  stroke-width: 1;
  vector-effect: non-scaling-stroke;
}
/* Gradient stops — gold by default, tween to white when wave completes */
#logo-wave-a, #logo-wave-b{
  stop-color: #c69a44;
  transition: stop-color 500ms ease;
}
.wave-complete #logo-wave-a, .wave-complete #logo-wave-b{
  stop-color: #ffffff;
}
/* Hero sub-slot — contains the tagline AND the boot progress bar stacked
   in the same position. Bar is absolutely positioned, tagline is in flow.
   Bar is visible during preload, then fades out as the tagline reveals
   via GSAP char stagger. */
.hero-sub{
  position: relative;
  margin-top: 16px;
  min-height: 16px;  /* reserve space for both */
}
/* Boot progress bar — 22 segmented blocks that fill left-to-right in a
   jittered sequence during loading. Width matches .hero-logo so the bar
   sits as a pill directly below the wordmark. Rounded ends + overflow
   hidden clip the block edges to the pill shape. */
.boot-progress{
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  display: flex;
  gap: 2px;
  width: clamp(220px, 32vw, 420px);
  height: 14px;
  border: 1px solid rgba(255, 255, 255, 0.4);
  border-radius: 999px;
  padding: 2px;
  overflow: hidden;
  opacity: 1;
  pointer-events: none;
}
.boot-progress-block{
  flex: 1 1 auto;
  /* Each block shows its slice of a shared gold gradient that spans the
     entire bar — background-image + background-size (2200%) + per-block
     background-position (set in JS) composes one continuous gradient
     across all 22 blocks. Initial state: opacity 0 (block empty). */
  background-image: linear-gradient(
    90deg,
    #6b4a12 0%,
    #c69a44 25%,
    #f5dd8e 50%,
    #c69a44 75%,
    #6b4a12 100%
  );
  background-size: 2200% 100%;
  background-repeat: no-repeat;
  opacity: 0;
  transition: opacity 120ms linear;
}
.boot-progress-block.on{
  opacity: 1;
}
/* Round the fill's 0% and 100% ends to match the pill's inner capsule (inner
   content height ≈ 8px → 4px radius), so the gold fill follows the rounded bar
   instead of ending with a hard square edge against the rounded ends. Internal
   block edges stay square (they read as the segment gaps). */
.boot-progress-block:first-child{
  border-top-left-radius: 4px;
  border-bottom-left-radius: 4px;
}
.boot-progress-block:last-child{
  border-top-right-radius: 4px;
  border-bottom-right-radius: 4px;
}
.hero-logo{
  width: clamp(220px, 32vw, 420px);
  height: auto;
  display: block;
  opacity: 0.92;
  cursor: pointer;  /* hover/click re-runs the lens-align rack */
  pointer-events: auto;
  margin: 0 auto;
  /* Round letters (S, O) touch the viewBox top edge (y=0), and the 1px
     non-scaling stroke is centred there — so the SVG's default overflow:hidden
     clips the top ~0.5px of those strokes, flattening the letter tops.
     overflow:visible lets the full stroke paint; no layout effect (the box is
     unchanged, the overlay around it has room). */
  overflow: visible;
}
.hero-tagline{
  font-family: 'Inter', ui-sans-serif, system-ui, -apple-system, sans-serif;
  font-weight: 400;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: rgba(255,255,255,0.5);
  margin: 0;
  width: 100%;
  font-size: clamp(7px, 0.98vw, 13px);
  line-height: 1;
  /* Chars split by JS into .tagline-char spans. Each char starts invisible
     and animates in via GSAP stagger when the preloader dismisses. */
}
.tagline-char{
  display: inline-block;
  opacity: 0;
  will-change: opacity, transform, filter;
}

#hero .layer-bg{
  z-index: 1;
  /* Lens-align intro start: BG sharp, MG + FG soft. GSAP tweens these
     filters through three focus beats over 1.8s after cinema-intro fades.
     Resting state: BG 1px, MG 0.25px, FG 2px (focus on the cliff). */
  filter: blur(0px);
  will-change: filter;
}
#hero .hero-atmo{
  position: absolute;
  inset: 0;
  z-index: 3;
  width: 100%;
  height: 100%;
  pointer-events: none;
  opacity: 0;
  mix-blend-mode: screen;
  -webkit-mask-image: linear-gradient(
    to bottom,
    rgba(0,0,0,0) 0%,
    rgba(0,0,0,0.16) 12%,
    rgba(0,0,0,0.3) 24%,
    rgba(0,0,0,0.5) 38%,
    rgba(0,0,0,1) 60%,
    rgba(0,0,0,0.82) 78%,
    rgba(0,0,0,0.14) 93%,
    rgba(0,0,0,0) 100%
  );
  mask-image: linear-gradient(
    to bottom,
    rgba(0,0,0,0) 0%,
    rgba(0,0,0,0.16) 12%,
    rgba(0,0,0,0.3) 24%,
    rgba(0,0,0,0.5) 38%,
    rgba(0,0,0,1) 60%,
    rgba(0,0,0,0.82) 78%,
    rgba(0,0,0,0.14) 93%,
    rgba(0,0,0,0) 100%
  );
}
/* Global film grain — fixed overlay across all scenes */
.hero-grain{
  position: fixed;
  inset: 0;
  z-index: 10;
  width: 100%;
  height: 100%;
  pointer-events: none;
  mix-blend-mode: normal;
  opacity: 0.08;
  filter: contrast(1.08);
  transition: opacity 800ms ease;
}
body.has-hero-atmo #hero .hero-atmo{
  opacity: 0.33;
}
body.projects-theatre .hero-grain{
  opacity: 0.03;
}
/* Hide topnav in theatre mode — nav is dead UI while watching the embed,
   and on short landscape viewports (iPhone with Safari chrome) it clipped
   the top of the BG + YouTube frame. Pointer-events: none so re-emergence
   doesn't trap clicks during the transition. */
body.projects-theatre .topnav{
  opacity: 0;
  pointer-events: none;
  transition: opacity 280ms ease;
}
@media (prefers-reduced-motion: reduce){
  .hero-grain{
    display: none;
  }
}

@media (max-width: 767px){
  #hero .hero-atmo{
    mix-blend-mode: normal;
    -webkit-mask-image: linear-gradient(
      to bottom,
      rgba(0,0,0,0) 0%,
      rgba(0,0,0,0.14) 10%,
      rgba(0,0,0,0.28) 22%,
      rgba(0,0,0,0.42) 36%,
      rgba(0,0,0,0.95) 58%,
      rgba(0,0,0,0.9) 76%,
      rgba(0,0,0,0.22) 92%,
      rgba(0,0,0,0) 100%  
    );
    mask-image: linear-gradient(
      to bottom,
      rgba(0,0,0,0) 0%,
      rgba(0,0,0,0.14) 10%,
      rgba(0,0,0,0.28) 22%,
      rgba(0,0,0,0.42) 36%,
      rgba(0,0,0,0.95) 58%,
      rgba(0,0,0,0.9) 76%,
      rgba(0,0,0,0.22) 92%,
      rgba(0,0,0,0) 100%
    );
  }
  body.has-hero-atmo #hero .hero-atmo{
    opacity: 0.18;
  }
}

/* Hero overlay z-index handled by the .hero-overlay rule above (z-index 5,
   which sits above the .hero-projects-fg-bridge plate at z-index 4). */


/* Hero ↔ Projects bridge — NOW STREAMING marquee plate (1186×3106, 0.38
   aspect). Lives in the pan track as a sibling of both scenes; anchored at
   the seam (track x=100vw) and centered on it via translateX(-50%), so the
   marquee straddles the hero→projects boundary. At hero rest the right edge
   of the marquee peeks into hero's right; at projects rest the marquee fully
   frames projects' left edge as the theatre entrance sign.
   Height-primary sizing: full viewport height, width follows plate aspect
   (≈38% of vh). ±8px overscan on top/bottom absorbs blur spread.
   z-index 4 keeps the marquee above MG. GSAP owns the horizontal parallax
   and theatre-mode motion (see app.js). */
.hero-projects-fg-bridge{
  /* Fixed (not absolute) so the marquee escapes the pan-track's stacking
     context — the pan-track has transform:translate3d which creates a
     stacking context that traps inner z-index values. Going fixed +
     z-index 10001 lets the marquee paint ABOVE .hero-overlay (10000)
     during the transit, so the pinned logo is physically occluded by
     the marquee plate as it slides through. GSAP drives the full
     transit-x from this fixed CSS anchor (was previously riding the
     pan-track's translation as a free additive offset). */
  position: fixed;
  top: -8px;
  bottom: -8px;
  /* Anchored just off the right edge at hero rest. GSAP transits it one
     full (viewport + plate) width leftward over hero→projects pan, so
     the LEFT edge ends one plate-width off the viewport-left at projects
     rest. Same on-screen trajectory as before, all motion now lives in
     GSAP since the pan-track translation no longer carries it. */
  left: 100vw;
  height: calc(100% + 16px);
  /* Wrapper width tracks plate aspect — replaces the auto-from-img
     behaviour the original <img> gave us via its 1186×3106 attrs. */
  aspect-ratio: 1186 / 3106;
  pointer-events: none;
  /* Above .hero-overlay (10000) so the marquee occludes the pinned logo
     during transit. */
  z-index: 10001;
  /* Load state — soft + dim. DOF racks blur 8px → 0.3px at midpoint then
     back to 8px by exit (the rack symmetric on both sides of the transit).
     Brightness ramps 0.4 → 1.0 over the first half only, then holds at 1.0
     through the projects entry — like a marquee bulb warming up as the
     camera locks focus on it, rather than pulsing on and off. Filter
     cascades to the inner img + label so the baked plate and the live
     type rack together as one optical element.
     Opacity starts at 0 — JS reveals it when cinema-intro fades. Prevents
     the marquee (z-index 10001) from painting above the cinema-intro
     black (z-index 9999) during the preloader. Transition duration matches
     cinema-intro's 900ms fade so they cross-dissolve cleanly. */
  filter: blur(8px) brightness(0.4);
  opacity: 0;
  transition: opacity 900ms ease;
  will-change: filter, transform;
}
.hero-projects-fg-bridge.is-revealed{
  opacity: 1;
}
/* After projects scene (Phase 4+ of panTL), JS adds body.marquee-faded
   to hide the marquee on about/cinema/colour/contact. The fixed-position
   marquee otherwise persists across scenes and its blur halo can bleed
   into later scenes' compositions. Returns to opacity 1 when scrolling
   back into hero/projects. */
body.marquee-faded .hero-projects-fg-bridge{
  opacity: 0;
}

.hero-projects-fg-bridge img{
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* Browser-rendered "NOW PLAYING" sitting on the marquee's lit panel.
   Letter-per-row vertical stack matches the original baked treatment:
   N / O / W / · / P / L / A / Y / I / N / G — eleven grid rows centered
   on the plate, one character per row, gap row between the two words.
   Type stays live (Benoist non-negotiable) and racks with the plate
   because the wrapper's blur filter cascades through to this element. */
.hero-projects-fg-bridge-label{
  position: absolute;
  /* Confine the grid to the lit panel area only — top/bottom 12% absorbs
     the proscenium caps so letters never bleed into the architectural
     hardware. Horizontal stays at 0; letters are centered by justify-items. */
  inset: 12% 0;
  display: grid;
  grid-template-rows: repeat(11, 1fr);
  align-items: center;
  justify-items: center;
  font-family: 'Inter', ui-sans-serif, system-ui, -apple-system, sans-serif;
  font-weight: 500;
  /* Letter size scales off the panel — each row gets ~7vh after the 12%
     insets, so font-size targets ~half-row for comfortable air. */
  font-size: clamp(14px, 3vh, 44px);
  letter-spacing: 0;
  line-height: 1;
  text-transform: uppercase;
  color: #2a1a0c;
  pointer-events: none;
  user-select: none;
  mix-blend-mode: multiply;
}
.hero-projects-fg-bridge-label .gap{
  /* Empty row between NOW and PLAYING — preserves the cross-strut grid
     spacing of the baked original. */
  display: block;
}

#hero .layer-mid{
  /* Bottom-anchored box approach (same pattern as iPad/iPhone portrait
     overrides). The new portrait-oriented plate (1446×1756) is sized into
     a smaller box so the cliff/tower doesn't dominate the desktop frame.
     Width 45% / height 75% is the desktop default; tablet portrait and
     phone portrait media queries override below with their own dimensions.
     Inset auto auto 0 0 anchors the box to the bottom-left corner so the
     cliff base sits flush with viewport bottom (no gap). */
  inset: auto auto 0 0;
  width: 72%;
  height: 88%;
  object-fit: contain;
  object-position: 0% 100%;
  transform-origin: 0% 100%;
  /* Lens-align load state — soft. GSAP tweens to 0.25px at the resting beat. */
  filter: blur(4px);
  will-change: filter;
  z-index: 2;
}

/* Radio-tower beacon lights. Overlay matches the rendered MG plate exactly
   (height 88% + plate aspect 1446/1756, bottom-left anchored) so each dot's
   left/top % maps onto the tower. z-index 4 sits above the atmosphere fog
   (z-3) so the lights punch through haze (realistic), below the wordmark
   overlay (z-5). BLOCKING PASS — static dots to dial positions; tracking +
   blink animation are follow-up phases. */
#hero .hero-beacon-layer{
  position: absolute;
  inset: auto auto 0 0;
  height: 88%;
  aspect-ratio: 1446 / 1756;
  z-index: 4;
  pointer-events: none;
  /* transform-origin matches the MG dolly anchor (0% 100%) so the dolly
     scale (mirrored in app.js runCameraDollyPullOut) keeps the lights glued
     to the tower. Resting blur matches the MG rest (0.25px); the lens-align
     rack drives both together via the shared mgT target. */
  transform-origin: 0% 100%;
  filter: blur(0.25px);
}
.hero-beacon{
  position: absolute;
  width: 0.5%;
  aspect-ratio: 1 / 1;
  border-radius: 50%;
  transform: translate(-50%, -50%);
}
/* Bright core + layered radial bloom — fakes a light source penetrating the
   dusk haze rather than a flat dot. Element opacity is what the blink
   animates, so core + glow dim/brighten as one unit. */
.hero-beacon--red{
  background: #ff6a52;
  box-shadow:
    0 0 1.5px 0.5px rgba(255, 120, 90, 0.95),
    0 0 4px 1px rgba(255, 70, 45, 0.55),
    0 0 9px 2px rgba(255, 45, 30, 0.28);
  animation: beacon-red 1.6s steps(1, end) infinite;
}
.hero-beacon--white{
  background: #fff6e9;
  box-shadow:
    0 0 2px 0.5px rgba(255, 248, 236, 0.95),
    0 0 6px 1.5px rgba(225, 238, 255, 0.55),
    0 0 13px 3px rgba(200, 225, 255, 0.28);
  animation: beacon-white 3.6s ease-in-out infinite;
}
/* Reds: distinct fast double-flash then a dark hold (aviation-style
   characterful pattern). steps(1) gives crisp on/off, not a soft pulse.
   All three share the animation + start together → in unison. */
@keyframes beacon-red{
  0%   { opacity: 1; }
  12%  { opacity: 0.12; }
  24%  { opacity: 1; }
  36%  { opacity: 0.12; }
  100% { opacity: 0.12; }
}
/* White apex: slower gentle breathe — steady presence vs the blinking reds. */
@keyframes beacon-white{
  0%, 100% { opacity: 0.6; }
  50%      { opacity: 1; }
}
@media (prefers-reduced-motion: reduce){
  #hero .hero-beacon-layer{ filter: none; }
  .hero-beacon{ animation: none; opacity: 1; }
}

/* Portrait phones: shrink the MG element to a bottom-anchored box that
   bleeds left off the viewport. Box bottom is flush with viewport bottom
   (no gap underneath). Width >100% with negative left lets the cliff
   content sit so the tower lands beside the SMAK logo on the left. */
@media (orientation: portrait) and (max-width: 767px){
  #hero .layer-mid{
    /* -35% (was -50%) gives the tower more breathing room from the
       left edge — composition reads as "tower in left third" rather
       than "tower clipped at edge". */
    inset: auto auto 0 -35%;
    width: 150%;
    height: 85%;
    object-fit: contain;
    object-position: 0% 100%;
  }
  /* Beacon overlay mirrors the phone MG box so the lights stay on the tower.
     aspect-ratio (inherited from base) derives height from width. */
  #hero .hero-beacon-layer{
    inset: auto auto 0 -35%;
    width: 150%;
    height: auto;
  }
}

/* Tablet portrait (iPad / iPad Pro): the hero plates need different
   anchoring than desktop because the viewport is taller and narrower.
   MG switches to cover so we have horizontal slack to slide the cliff
   further left; FG shifts right by 10vw so the tree trunk frames more
   aggressively (overflow off the right edge is clipped by #hero). */
@media (orientation: portrait) and (min-width: 768px) and (max-width: 1199px){
  #hero .layer-mid{
    /* Shrink the element to a bottom-left box so the whole plate (cliff +
       wider vista) renders smaller. Contain fits the full image inside the
       box — no clipping seam — and the box itself is smaller than viewport,
       so the cliff occupies a smaller framing footprint. Negative left
       slides the whole box past the viewport edge so the cliff sits
       further left without losing the bottom anchor. */
    inset: auto auto 0 -10vw;
    width: 92%;
    height: 84%;
    object-fit: contain;
    object-position: 0% 100%;
  }
  /* Beacon overlay mirrors the iPad MG box so the lights stay on the tower. */
  #hero .hero-beacon-layer{
    inset: auto auto 0 -10vw;
    width: 92%;
    height: auto;
  }
}

/* Hero FG — portrait tree silhouette plate (1047x1623) anchored to the
   right edge of the hero scene, full viewport height. Restored after the
   hero-projects-fg-bridge was disabled pending the NOW SHOWING marquee plate.
   Sizing mirrors the bridge's approach: height drives, width:auto follows
   the plate's intrinsic aspect — so on portrait viewports the trees stay
   full-height and the overflow bleeds left past the viewport (hero's
   overflow:hidden clips it) rather than shrinking into a small box.
   ±8px vertical overscan absorbs the blur spread so the edges don't
   dissolve into visible gaps.
   Load state blur(6px) = "lens loses focus" starting point for runLensAlign;
   the rack tweens through 1px (through-focus) → 0px (FG subject) → 2px (rest). */
#hero .layer-fg{
  /* -3vw on right pushes the asset's hard right edge off-screen so no
     seam reads at viewport-right. Portrait/tablet media queries below
     override the right value with their own shift. */
  inset: -8px -3vw -8px auto;
  width: auto;
  height: calc(100% + 16px);
  object-fit: contain;
  object-position: 100% 100%;
  transform-origin: 100% 100%;
  filter: blur(6px);
  will-change: filter, transform;
  z-index: 3;
  pointer-events: none;
}

/* Portrait phones: dense tree cluster bleeds further past right edge so
   only the outer trunks frame the viewport. */
@media (orientation: portrait) and (max-width: 767px){
  #hero .layer-fg{
    right: -40vw;
  }
}

/* iPad / tablet portrait: less rightward shift than phones — wider viewport
   means the trees read as a tighter frame at a smaller vw offset, so we let
   more of the cluster sit inside the viewport. */
@media (orientation: portrait) and (min-width: 768px) and (max-width: 1199px){
  #hero .layer-fg{
    right: -20vw;  /* -15vw → -20vw (2026-05-23): pushed the FG tree a touch further right to balance the left-shifted/smaller tower */
  }
}

/* layer-fg display now owned by per-layer rules in projects v2 theatre stack */

/* Projects ↔ About bridge — California redwood trunk slab (1275×2528).
   Structure mirrors .hero-projects-fg-bridge (the NOW STREAMING marquee):
   pan-track sibling of the scenes, parked at the projects→about seam
   (track x=200vw) with its LEFT edge at the seam so at projects rest it
   sits just off viewport right. GSAP parallax slides it one full width
   leftward during phase 4 (t=3.5 → 4.5) so it crosses viewport centre at
   t=4.0 and lands just off viewport left by about-rest. Always physically
   between the scenes; only in-viewport during the transit window.
   z-index 50 keeps it above any scene internals while passing through. */
.projects-about-fg-bridge{
  position: absolute;
  top: -8px;
  bottom: -8px;
  left: 200vw;
  height: calc(100% + 16px);
  width: auto;
  pointer-events: none;
  z-index: 50;
  /* Load-state blur matches the marquee's — DOF racks through sharpness at
     midpoint via the pan timeline. */
  filter: blur(8px);
  will-change: filter, transform;
}
/* ───────────────────────────────────────────────────────────────────
   PROJECTS — theatre scene (v2)
   Layer stack Z (back → front):
     layer-bg (1)  cove + walls + floor + blank screen
     triptych (2)  3 DOM panels inside cove screen rectangle
     screen-wrap (2) iframe trailer (above triptych in theatre mode)
     layer-mid/mg-shell (3)  outer shell with alpha cutout through cove
     layer-fg/fg-seats (4)   mid seat rows + aisle
     seats-close (5)         near-camera blurred seats
   ─────────────────────────────────────────────────────────────────── */
#projects{
  /* Clip to 100vw scene box — seats-close is 109% wide (3000/2752) for
     parallax headroom and would otherwise leak into hero/about territory
     inside the pan track. */
  overflow: hidden;
}
#projects .scene-media{
  z-index: auto;
}
#projects .layer-bg{
  z-index: 1;
  object-fit: cover;
  object-position: 50% 50%;
  transition: filter 700ms ease, opacity 700ms ease;
}
#projects .scene-overlay{
  display: none;
}

/* Plate-locked screen rectangle. Geometry mirrors .layer-bg's object-fit:
   cover with 6% overscan — uses max() to pick the larger of (scene+overscan
   width) or (scene+overscan height × plate aspect), maintains plate aspect
   via aspect-ratio. Children's percentage coords therefore resolve against
   the actual BG plate, so the triptych, theatre iframe, and debug box stay
   glued to the real screen rectangle on the BG at ANY viewport aspect.
   Plate natural 2752×1536, aspect 1.79167. */
#projects .projects-screen-rect{
  position: absolute;
  top: 50%;
  left: 50%;
  /* Centering (translate -50%,-50%) is owned SOLELY by GSAP via xPercent/yPercent
     on every screen-rect tween (app.js) — deliberately NOT set here. A CSS translate
     here double-stacked with GSAP's on desktop Chrome (which keeps the CSS transform,
     while iOS Safari discards it once GSAP owns the matrix), flinging the focal ~570px
     off the top of the screen on desktop. Single source = consistent on every browser.
     2026-06-22. */
  width: max(106vw, calc(1.06 * var(--scene-vh) * 2752 / 1536));
  aspect-ratio: 2752 / 1536;
  z-index: 2;
  pointer-events: none;
  transform-origin: 50% 50%;
  will-change: transform;
}
#projects .projects-screen-rect > *{ pointer-events: auto; }

/* Single focal slot — replaces the triptych (architecture revision
   2026-05-13). Three slots stacked at the same coords + crossfade on
   focal cycling (.is-active toggle). All slots preload so the swap is
   a clean crossfade, not a load flash. Plate-locked to BG cove screen.
   Opacity 0 by default; GSAP fades the wrapper in at end of panTL
   phase 2 (theatre lighting settles first). */
#projects .projects-focal{
  position: absolute;
  z-index: 3;
  left: 49.65%;
  top: 43.5%;
  width: 32%;
  height: 24.4%;
  transform: translate(-50%, -50%);
  overflow: hidden;
  pointer-events: none;
  opacity: 0;
}
#projects .projects-focal-slot{
  position: absolute;
  inset: 0;
  opacity: 0;
  /* Rack-focus crossfade (2026-05-20). Non-active slots sit defocused; the
     active slot pulls into focus. Because blur shares timing with opacity,
     the outgoing film defocuses as it fades and the incoming sharpens as it
     arrives — a focus pull between two film planes, not a flat dissolve.
     At the crossfade midpoint both slots are partially blurred (focal plane
     passing between them) per the rack-focus-physics rule. Tween lengthened
     380→460ms so the pull reads. */
  filter: blur(7px);
  transition: opacity 460ms var(--ease-focus-pull),
              filter 460ms var(--ease-focus-pull);
}
#projects .projects-focal-slot.is-active{
  opacity: 1;
  filter: blur(0);
}
/* Click-to-play screen (2026-06-10, minimal retry of the reverted 06-10
   attempt — no glyph/hover/backdrop-filter this time). Clickable ONLY while
   .is-browsing (JS toggles it with the focal cycle) — .is-active alone is
   true in the shipped markup even when the focal is invisible, which made
   the first attempt an invisible click trap over the screen rect. During
   theatre the slots go inert (rule below) so the click surface can never
   coexist with the live iframe — the old panel/iframe fullscreen contention
   is structurally impossible. */
#projects .projects-focal.is-browsing .projects-focal-slot.is-active{
  pointer-events: auto;
  cursor: pointer;
}
body.projects-theatre #projects .projects-focal .projects-focal-slot{
  pointer-events: none;
}
@media (prefers-reduced-motion: reduce){
  /* Keep the focal selection functional but drop the rack blur + lengthened
     tween — consistent with the reduced-motion decision (decorative motion
     off, functional selection stays). */
  #projects .projects-focal-slot{
    filter: none;
    transition: opacity 200ms ease;
    will-change: auto;
  }
  #projects .projects-focal-slot.is-active{ filter: none; }
}
#projects .projects-focal-thumb{
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  opacity: 0.55;
  filter: blur(0) brightness(0.6) saturate(0.95);
}
/* Film-card composition (2026-05-13): poster dominant on the left,
   meta (year + credits + logline) stacked vertically-centered in the
   center+right column. Thumb stays full-bleed dim behind both as the
   atmospheric still. Previously centered single-column with meta as a
   tight band at the bottom; rebuilt to read as a one-sheet info tile. */
#projects .projects-focal-poster{
  position: absolute;
  top: 8%;
  left: 4%;
  height: 84%;
  width: auto;
  max-width: 34%;
  object-fit: contain;
  object-position: left center;
  filter: drop-shadow(0 8px 16px rgba(0, 0, 0, 0.55));
}
#projects .projects-focal-meta{
  position: absolute;
  /* 58% → 54% (2026-06-10): with the big year display gone the block is
     shorter — anchor raised so the credits sit higher on the screen wrap
     instead of pooling at the bottom. Eyeball on live before locking. */
  top: 54%;
  /* 40% → 36% (2026-06-10): kill the dead air between poster and credits.
     Poster worst-case right edge is 38% (left 4% + max-width 34%) but only
     when fitting by width; on most viewports it fits by height and ends
     short of that. Eyeball for poster/text crowding on ultrawide. */
  left: 36%;
  right: 3%;
  transform: translateY(-50%);
  text-align: left;
  font-family: 'Inter', ui-sans-serif, system-ui, -apple-system, sans-serif;
  color: rgba(255, 232, 200, 0.92);
  text-shadow: 0 1px 4px rgba(0, 0, 0, 0.9);
  pointer-events: none;
}
/* Year demoted from display element to a meta line (2026-06-10): it was the
   largest type on the card but the least important fact — hierarchy now reads
   Director / Producer / Role / Year as uniform credit lines, freeing vertical
   room for the logline (mobile was cramped). Year line reuses
   .projects-focal-credits; the old .projects-focal-year rule is gone. */
#projects .projects-focal-credits{
  font-weight: 500;
  font-size: clamp(9px, 1.05vh, 12px);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  margin-bottom: 3%;
  line-height: 1.2;
  /* Credit lines are names, not prose — never wrap. Column width (36%→97%)
     comfortably holds the longest line at the clamp floor. */
  white-space: nowrap;
}
/* Credit-block labels (Director: / Role:). Dimmer + lighter so the label
   recedes and the name/role reads as the focal info. */
#projects .projects-focal-meta .meta-label{
  font-weight: 400;
  opacity: 0.5;
  margin-right: 0.4em;
}
/* SMAK's role on the film — distinct from the director credit. Slightly
   more amber than the base cream to read as a portfolio badge. */
#projects .projects-focal-role{
  font-weight: 600;
  font-size: clamp(8px, 0.9vh, 11px);
  letter-spacing: 0.18em;
  text-transform: uppercase;
  white-space: nowrap;
  color: rgba(255, 207, 145, 0.92);
  /* Was 5% when Role closed the credit stack; Year sits below it now, so
     Role matches the uniform 3% credit rhythm. */
  margin-bottom: 3%;
  line-height: 1.2;
}
#projects .projects-focal-logline{
  font-weight: 400;
  font-style: italic;
  font-size: clamp(9px, 1vh, 12px);
  letter-spacing: 0.01em;
  line-height: 1.4;
  opacity: 0.82;
  /* Cap to 2 lines now that the role line takes a slot in the meta
     column (was 3 lines before role was added). Loglines may need
     trimming to fit. */
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
/* Festival laurel (Do Not Disturb only). White-on-transparent badge sits to
   the RIGHT of the credit block as a standalone accolade accent; year +
   director + role + logline read down the left in .projects-focal-textcol.
   Scoped to the award meta so the other two films are untouched. */
/* Top row holds the credit block (left) + laurel (right). The logline then
   drops BELOW this row at full meta width, so its line length matches the
   other two films instead of being squeezed by the laurel column. */
#projects .projects-focal-toprow{
  display: flex;
  align-items: flex-start;
  gap: clamp(10px, 2.5%, 24px);
}
#projects .projects-focal-textcol{
  flex: 1 1 auto;
  min-width: 0;
}
#projects .projects-focal-award{
  flex: 0 0 auto;
  display: block;
  width: auto;
  max-width: min(34%, 88px);
  height: auto;
  opacity: 0.95;
}

/* Screen wrap — iframe trailer, covers focal slot during theatre mode.
   Must stay in lockstep with .projects-focal coords above. */
#projects .projects-screen-wrap{
  position: absolute;
  z-index: 2;
  left: 49.65%;
  top: 43.5%;
  width: 32%;
  height: 38%;
  transform: translate(-50%, -50%);
  overflow: hidden;
  opacity: 0;
  pointer-events: none;
  background: #060608;
}
/* Sizing on #projectsPlayer must hold even when the wrap is lifted out
   of #projects (iPad fullscreen workaround reparents the wrap to <body>
   so the iframe has zero transformed ancestors). Single-ID selector
   keeps the rule scoped to this iframe but not dependent on the wrap's
   DOM position. Same goes for the theatre opacity/pointer rules below. */
#projectsPlayer{
  width: 100%;
  height: 100%;
  border: 0;
  display: block;
  pointer-events: none;
}
body.projects-theatre #projectsScreenWrap{
  opacity: 1;
  pointer-events: auto;
  z-index: 4;
}
body.projects-theatre #projectsPlayer{
  pointer-events: auto;
}
body.projects-theatre #projects .projects-focal{
  opacity: 0;
  pointer-events: none;
}
/* Marquee slides down out of the frame during theatre — viewer is inside
   the trailer, the lobby signage descends with the house lights. Slide
   driven by GSAP (gsapEnterTheatre / gsapExitTheatre), not CSS, so the
   transform doesn't fight GSAP's matrix ownership. CSS just kills clicks. */
body.projects-theatre #projects .projects-marquee{
  pointer-events: none;
}

/* Theatre close affordance — gold × fixed to the top-right of the VIEWPORT
   (the projects scene fills it in theatre), not pinned to the video wrap. The
   topnav is hidden in theatre so the corner is clear. visibility toggle also
   pulls it out of the tab order when the trailer is closed. */
.projects-theatre-close{
  position: fixed;
  top: clamp(16px, 2.4vh, 30px);
  right: clamp(16px, 1.8vw, 34px);
  z-index: 20;
  width: clamp(32px, 2.6vw, 44px);
  height: clamp(32px, 2.6vw, 44px);
  display: grid;
  place-items: center;
  padding: 0;
  font-family: 'Inter', ui-sans-serif, system-ui, -apple-system, sans-serif;
  font-size: clamp(20px, 2vw, 28px);
  line-height: 1;
  color: rgba(200, 169, 110, 1);
  background: rgba(6, 6, 8, 0.5);
  border: 1px solid rgba(200, 169, 110, 0.45);
  border-radius: 50%;
  cursor: pointer;
  visibility: hidden;
  -webkit-tap-highlight-color: transparent;
  transition: background 0.2s ease, transform 0.18s ease, border-color 0.2s ease;
}
.projects-theatre-close:hover,
.projects-theatre-close:focus-visible{
  background: rgba(6, 6, 8, 0.82);
  border-color: rgba(200, 169, 110, 0.95);
  transform: scale(1.08);
  outline: none;
}
body.projects-theatre .projects-theatre-close{
  visibility: visible;
}

/* ─── Fullscreen ancestor-transform workaround ─────────────────────────────
   Chrome bails fullscreen on iframes nested in ancestors with continuously
   mutating transforms (GSAP rewrites .projects-screen-rect's scale every
   frame during theatre enter, and even at rest the translate(-50%, -50%)
   centering creates a containing block that interacts badly with the
   fullscreen API in some Chromium builds — the fullscreen target's
   containing block becomes unstable and the browser exits fullscreen one
   tick after entry).
   When the iframe is fullscreen, JS adds body.projects-iframe-fullscreen
   (see app.js onFsChange) and we neutralize transforms on the ancestor
   chain. The visual is moot during fullscreen since the iframe is in the
   browser's top-layer at viewport dimensions — no parent transform
   affects rendering. !important is required to beat GSAP's inline styles. */
body.projects-iframe-fullscreen #projects .projects-screen-rect,
body.projects-iframe-fullscreen #projects .projects-screen-wrap,
body.projects-iframe-fullscreen .hero-projects-pan-track{
  transform: none !important;
  filter: none !important;
  will-change: auto !important;
}

/* ─── Horizontal marquee — sole Play Trailer surface (2026-05-12) ─────────
   Pre-rotated marquee plate (2818×1077, aspect ~2.616) anchored to the
   bottom of the projects scene. Reads as the interior "now showing" sign
   in the lobby, completing the proscenium rhyme with the bridge marquee
   (vertical exterior signage at hero→projects transit).
   z 6 sits above the seats (z 5) so the sign reads in front of the
   front-row silhouettes. */
#projects .projects-marquee{
  /* Wide interior signage, sized between original 56vw and full-width
     (Scott calibrated split-difference 2026-05-13). Plate baked-cropped
     in Photoshop 2026-05-13 (was 2818×1077, now 2818×859) — bottom
     proscenium foot removed at the file level; parent aspect-ratio
     matches the file exactly, no CSS overflow crop. Anchored flush to
     viewport bottom — no gap. */
  position: absolute;
  left: 50%;
  bottom: 0;
  /* Transform owned by GSAP — xPercent: -50 for centering, yPercent for
     slide-up/down choreography. Default yPercent: 200 (off-bottom) is
     applied via gsap.set at boot. */
  width: clamp(460px, 55vw, 960px);
  aspect-ratio: 2818 / 859;
  z-index: 6;
  border: 0;
  padding: 0;
  background: transparent;
  cursor: pointer;
  pointer-events: auto;
  /* Soft drop shadow lifts the sign off the seats backdrop. */
  filter: drop-shadow(0 -8px 24px rgba(0, 0, 0, 0.55));
  transition: filter 280ms ease;
  will-change: transform;
}
#projects .projects-marquee:hover{
  filter: drop-shadow(0 -10px 28px rgba(0, 0, 0, 0.65));
}
#projects .projects-marquee:focus-visible{
  outline: 2px solid rgba(255, 230, 180, 0.85);
  outline-offset: 6px;
}
#projects .projects-marquee-plate{
  /* Renders at the file's natural aspect (Photoshop baked-cropped 2026-05-13
     to 2818×859). Parent .projects-marquee uses the same aspect, so no
     overflow crop is needed at the CSS layer. */
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 2818 / 859;
  pointer-events: none;
}

/* Lit panel titles — three film titles 3-across, two lines each.
   Architecture revision 2 (2026-05-13): bigger type, stacked across
   two rows of the lit panel.
   Plate is cropped to top ~67% of natural height (2818/720 aspect).
   Lit panel inside cropped view: ~12% top (chase strip), bottom edge
   meets the crop, ~12% left/right (bookend caps).
   Title row occupies the upper half of the lit panel; CTA sits in the
   lower half under the focal column. */
#projects .projects-marquee-titles{
  position: absolute;
  /* Titles centered at ~49% of marquee height (the marquee's true visual
     center including arch cap + lit panel + base trim). Previously 55%,
     which read low against the overall marquee form. Horizontal inset
     pulled 12% → 10% (2026-05-13) to widen the 3-column grid cells so
     SCENIC DRIVE fits on one line at the bumped-up title type. Lit panel
     on the plate runs from ~9% to ~94% of marquee width, so 10% inset
     is still inside the white band. */
  inset: 36% 10% 38% 10%;
  display: grid;
  grid-template-columns: 1fr 1fr 1fr;
  align-items: center;
  justify-items: center;
  font-family: 'Inter', ui-sans-serif, system-ui, -apple-system, sans-serif;
  font-weight: 500;
  /* Type scaled to fit DEATH ON / SCENIC DRIVE on two lines across the
     desktop band — SCENIC DRIVE is the longest second line of any title
     and was wrapping to a third line at the previous 1.85vw/max-32px. */
  font-size: clamp(15px, 1.78vw, 30px);
  letter-spacing: 0.05em;
  line-height: 1.1;
  text-transform: uppercase;
  text-align: center;
  color: #2a1a0c;
  pointer-events: none;
  user-select: none;
  mix-blend-mode: multiply;
}
#projects .projects-marquee-title{
  padding: 0 3%;
  opacity: 0.45;
  transition: opacity 320ms ease;
  /* Re-enable pointer events (parent .projects-marquee-titles has
     pointer-events: none so only specific title cells are click targets;
     gaps between cells fall through to the marquee button). Each title
     plays its own film via the marquee click handler in app.js. */
  pointer-events: auto;
  cursor: pointer;
}
#projects .projects-marquee-title:hover{
  opacity: 0.7;
}
#projects .projects-marquee-title.is-focal,
#projects .projects-marquee-title.is-focal:hover{
  opacity: 1;
}

/* Play Trailer CTA — INSIDE the lit panel, lower band, centered under
   the focal column. Tracks horizontally as focal cycles via --focal-col
   (0|1|2 → column centers 16.67% / 50% / 83.33%). */
#projects .projects-marquee-cta{
  position: absolute;
  /* Beneath the title row in the lit panel. Title row ends at ~62%
     (after the 2026-05-13 vertical shift); CTA sits at 74% — preserves
     the ~12% gap that paired titles + affordance previously. */
  top: 74%;
  /* Centered under the focal column. Titles band is inset 10% left/right
     from the marquee, so column centers in MARQUEE coords are:
       10% + (col + 0.5) × (80% / 3)
       = 10% + (col + 0.5) × 26.667%
     → 23.333% / 50% / 76.667% for cols 0/1/2. Must stay in lockstep with
     the .projects-marquee-titles inset above. */
  left: calc(10% + (var(--focal-col, 0) + 0.5) * 26.667%);
  /* Center the 2-line CTA block on (left, top) so the visual centroid
     stays pinned at top: 74% regardless of line count (was 1 line, now
     "Play" / "Trailer" stacked 2026-05-13). */
  transform: translate(-50%, -50%);
  font-family: 'Inter', ui-sans-serif, system-ui, -apple-system, sans-serif;
  font-weight: 600;
  font-size: clamp(11px, 1vw, 18px);
  letter-spacing: 0.22em;
  line-height: 1.15;
  text-transform: uppercase;
  color: #2a1a0c;
  opacity: 0;
  pointer-events: none;
  mix-blend-mode: multiply;
  transition: opacity 380ms ease,
              left 420ms var(--ease-dolly-track);
}
#projects .projects-marquee.is-cta-visible .projects-marquee-cta{
  opacity: 0.85;
}

/* iPhone landscape — short viewport. Marquee width matched to the
   desktop's 55vw proportion so the 3-column title grid stays wide enough
   to fit DEATH ON / SCENIC DRIVE on two lines (the previous 42vw was
   squashing SCENIC DRIVE into a third line and overflowing into the CTA
   row). Title letter-spacing eased slightly to add safety margin at the
   15px font floor. Touch device detection (hover:none + pointer:coarse)
   keeps this from triggering on desktop windows that happen to be short. */
@media (orientation: landscape) and (max-height: 520px) and (hover: none) and (pointer: coarse){
  #projects .projects-marquee{
    width: clamp(380px, 55vw, 620px);
    /* Pushed 20px below viewport bottom (Scott 2026-05-13) — at iPhone
       landscape the marquee was floating slightly tall in the auditorium
       frame; clipping a thin strip of the bottom architecture nestles
       the sign onto the floor without losing the lit-panel proportions. */
    bottom: -20px;
  }
  #projects .projects-marquee-titles{
    letter-spacing: 0.03em;
  }
}

/* MG: outer shell with alpha cutout. Scaled for cinematic presence and to
   provide canvas overscan for horizontal parallax travel during phases 1/4. */
#projects .projects-mg-shell{
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: 50% 50%;
  z-index: 3;
  pointer-events: none;
  transform: scale(1.05);
  transform-origin: 50% 50%;
  filter: blur(2px);
  will-change: filter, transform;
}

/* FG: seat mid-ground + aisle + rungs, bottom band (528px tall plate).
   inset:auto overrides the global img.layer inset:-3% so the plate sits
   at its natural aspect ratio anchored to the bottom edge.
   scale(1.05) widens the seat row for parallax travel — chairs get slightly
   larger and pick up ±2.5vw of left/right drift headroom. */
#projects .projects-fg-seats{
  position: absolute;
  inset: auto;
  left: 0;
  right: 0;
  bottom: 0;
  top: auto;
  width: 100%;
  height: auto;
  z-index: 4;
  pointer-events: none;
  transform: scale(1.12);
  transform-origin: 50% 100%;
  filter: blur(3px);
  will-change: filter, transform;
}

/* Seats: closest layer, widest canvas (3000px) for parallax room.
   Same inset:auto override. Width 109% matches the 3000/2752 plate extension.
   Centered via left:-4.5% (= (100%−109%)/2) instead of left:50% +
   translateX(-50%) — the GSAP parallax tweens own the transform matrix, and
   a CSS translateX inside that matrix gets overwritten the moment GSAP
   writes `x`. Using left for centering keeps GSAP's x:0 actually centered.
   DOF blur removed — was masking horizontal alignment; visual depth will come
   from parallax drift speed, not defocus. */
#projects .projects-seats-close{
  position: absolute;
  inset: auto;
  left: -7.5%;
  right: auto;
  bottom: -14%;
  top: auto;
  width: 115%;
  height: auto;
  transform: scale(1.55);
  transform-origin: 50% 100%;
  z-index: 5;
  pointer-events: none;
  /* Filter composed from CSS custom properties so GSAP can tween blur and
     brightness on independent timelines — blur follows a slow DOF fall-off
     (power3.in over the full rack) while brightness ramps up quickly at
     scene entry to lead the theatre-lights-up sequence. */
  --seats-blur: 0px;
  --seats-brightness: 0.5;
  filter: blur(var(--seats-blur)) brightness(var(--seats-brightness));
  will-change: filter, transform;
}

/* ─── Portrait (phone/tablet): fit the BG plate to show curtains on the
   sides but keep the theatre reading big — not tiny letterboxed. BG + MG
   shell switch cover → contain so the full plate aspect is preserved,
   then the layer CONTAINER is over-sized (inset/width/height) so the
   contained plate renders at ~150% of viewport. Dimension-based so
   GSAP's inline transform writes don't clobber the sizing (CSS `scale:`
   gets cleared by GSAP's CSSPlugin). Screen-rect uses matching math so
   triptych + iframe stay glued to the plate's screen rectangle. */
@media (orientation: portrait) and (max-width: 1199px){
  #projects img.layer-bg{
    object-fit: contain;
    inset: -60%;
    width: 220%;
    height: 220%;
  }
  /* MG shell is its own scale — sits smaller than the BG plate, shifted
     higher so the cove/ceiling frames the top of the view without
     smothering the BG screen area. */
  #projects .projects-mg-shell{
    object-fit: contain;
    inset: -90%;
    width: 280%;
    height: 280%;
    object-position: 50% 50%;
  }
  #projects .projects-screen-rect{
    width: min(220vw, calc(2.2 * var(--scene-vh) * 2752 / 1536));
  }
  /* FG seats rise from viewport bottom to sit closer to the BG plate's
     floor (letterboxed above the viewport bottom in portrait), and scale
     up so the aisle/rungs read at portrait size. */
  #projects .projects-fg-seats{
    width: 430%;
    left: -165%;
    right: auto;
    bottom: -3%;
  }
  /* Near seats scale up and rise so the close row fills the bottom and
     reads at portrait size. Natural strip is 3000×549 (very wide, short);
     default 115% width gets swamped in portrait. */
  #projects .projects-seats-close{
    width: 360%;
    left: -130%;
    bottom: -10%;
  }
}

/* iPhone SE / short phones (<=700px tall portrait): the -3% seats nudge tuned
   for the tall 390 (844px) rides too high on the short SE viewport (667px),
   clipping the bottom of the focal screen (seats top ~316 vs focal bottom ~360).
   Drop both rows ~9% so they clear the screen. Scoped to short phone portrait
   only -- 390/360 (>=800px tall) keep their -3%/-10%. */
@media (orientation: portrait) and (max-width: 767px) and (max-height: 700px){
  #projects .projects-fg-seats{ bottom: -15%; }
  #projects .projects-seats-close{ bottom: -22%; }
  /* Theatre mode expands the focal panel to fill the frame; drop the seats
     further so they clear the enlarged video on the short SE viewport. */
  body.projects-theatre #projects .projects-fg-seats{ bottom: -26%; }
  body.projects-theatre #projects .projects-seats-close{ bottom: -34%; }
}

/* Big phones (Plus / Pro Max, 430-440px portrait): the base phone tier's 430%
   seats were tuned for ~390px; at 440 they render ~1892px and over-fill the
   frame. Trim for >=414px portrait only -- 360/375/390/393 keep the base 430%. */
@media (orientation: portrait) and (min-width: 414px) and (max-width: 767px){
  #projects .projects-fg-seats{ width: 390%; left: -145%; }
  /* Theatre mode: drop the seats lower so they clear the enlarged focal video. */
  body.projects-theatre #projects .projects-fg-seats{ bottom: -10%; }
}

/* iPad portrait (768-1199): phone's 220%/430% scale factors are tuned for
   ~390px phones; on an 800-1024px tablet they render the BG at 2250+px and
   the seat plates at 4000+px — wildly over-zoomed (the seats balloon off the
   edges, leaving an empty central aisle). Ratchet the whole locked group
   (BG + MG + FG seats + seats-close + screen-rect) down so the plate reads
   less cropped. Triptych/iframe stay aligned because the screen-rect formula
   matches. NOTE: floor lowered 901→768 (2026-05-23) — the original 901 left
   the 11"/A16 iPads (820/834px portrait) falling through to the phone tier;
   this catches every tablet portrait width, not just the 13" Pro (1024).
   NOTE 2: floor lowered again 768->740 (2026-05-23) — the iPad mini A17 is
   744px portrait, BELOW the 768 tablet breakpoint, so it was falling through
   to the 430% phone seats + small marquee. No device sits between the widest
   phone (440px) and the mini (744px), so 740 is a safe floor. */
@media (orientation: portrait) and (min-width: 740px) and (max-width: 1199px){
  #projects img.layer-bg{
    inset: -30%;
    width: 160%;
    height: 160%;
  }
  #projects .projects-mg-shell{
    inset: -20%;
    width: 140%;
    height: 140%;
    object-position: 50% 37%;
  }
  #projects .projects-screen-rect{
    width: min(160vw, calc(1.6 * var(--scene-vh) * 2752 / 1536));
  }
  #projects .projects-fg-seats{
    width: 185%;
    left: -42%;
    bottom: 2%;
  }
  #projects .projects-seats-close{
    width: 190%;
    left: -45%;
    bottom: -8%;
  }
  /* Marquee reads small on tablet portrait — base clamp() hits its 460px
     floor here (55vw of an 820px iPad is below the floor), ~56% width against
     a tall full-height auditorium. Bump to ~72vw so the lobby sign carries the
     bottom of the frame the way it does on desktop. (2026-05-23) */
  #projects .projects-marquee{
    width: clamp(560px, 72vw, 800px);
  }
  /* Hero tower nudged left on tablet portrait (Scott 2026-05-23). Shift the MG
     box's LEFT edge (base is -3% from the img.layer overscan) — not a transform —
     so syncHeroBeacons, which reads offsetLeft, carries the beacon overlay with
     it and the lights stay glued to the tower. */
  #hero .layer-mid{ left: -20%; width: 93%; }
}

/* iPad mini A17 (744px portrait, the sub-768 tablet): the 185% seats tuned for
   the 800-1024 tablets read a touch small on the mini's narrower frame. Bump
   both rows up a notch; keep them centred via left = -(width-100)/2. Scoped to
   740-767 so the A16/Pro tablets (>=768) keep their 185%/190%. */
@media (orientation: portrait) and (min-width: 740px) and (max-width: 767px){
  #projects .projects-fg-seats{ width: 230%; left: -65%; bottom: -1%; }
  #projects .projects-seats-close{ width: 235%; left: -67.5%; bottom: -11%; }
  #projects .projects-marquee{ width: clamp(620px, 86vw, 720px); }
}

/* Taller-aspect tablets ~800-834 (Galaxy Tab S9 = 800/16:10, Air 11"/A16 = 820,
   Pro 11" = 834): want the seats fuller than the 4:3 iPads do. Same % over-fills
   the squarer 768/1024 frames, so scope the bigger seats to the 800-900 band —
   768 classic & 1024 Pro 13" keep the shared 185%/190%. Floor 800 (not 768) so
   the 4:3 classic iPad stays excluded. */
@media (orientation: portrait) and (min-width: 800px) and (max-width: 900px){
  #projects .projects-fg-seats{ width: 215%; left: -57.5%; bottom: -2.5%; }
  #projects .projects-seats-close{ width: 194%; left: -47%; bottom: -8%; }
}
/* Galaxy Tab S9 (800x1280, 16:10 / 0.625): taller than the 11" iPads (~0.69),
   so the mid-seats want to sit higher in the frame. Raise the bottom for 16:10
   tablets only — aspect <= 0.65 catches the Tab, excludes the iPads at 820/834. */
@media (orientation: portrait) and (min-width: 800px) and (max-width: 900px) and (max-aspect-ratio: 13/20){
  #projects .projects-fg-seats{ width: 250%; left: -75%; bottom: -1%; }
  #projects .projects-seats-close{ width: 204%; left: -52%; }
  #projects .projects-mg-shell{ width: 170%; height: 170%; inset: -35%; object-position: 50% 45.5%; }
}

/* ─── Landscape phones (short viewport heights): push FG seats + close
   seats further below the frame so they read as near-camera foreground
   instead of a disconnected strip at viewport bottom. 2026-05-13: with
   the marquee dropped 20px lower (above), the seats now have more room
   to read above the lit panel — both rows raised a notch so the
   silhouettes don't feel hidden under the marquee. */
@media (orientation: landscape) and (max-height: 500px){
  #projects .projects-fg-seats{
    bottom: -14%;
  }
  #projects .projects-seats-close{
    width: 130%;
    left: -15%;
    bottom: -54%;
  }
  #projects .projects-mg-shell{
    object-position: 50% 50%;
  }
}

/* iPhone landscape with Safari nav + favourites visible (~322px tall).
   Standard iPhone landscape rules at max-height: 500px were tuned for
   the chrome-hidden case (~414px); at this even shorter height the
   seats drop further and the marquee gets pushed lower (more off-bottom)
   to free vertical space for the focal screen. Titles + CTA shift UP
   within the marquee so they end up at the SAME viewport position
   despite the marquee box moving down — keeps text readable while the
   marquee architecture recedes into the floor. Marquee width inherits
   the 55vw rule from the iPhone landscape block (shrinking it broke
   SCENIC DRIVE's two-line fit). 2026-05-13. */
@media (orientation: landscape) and (max-height: 360px) and (hover: none) and (pointer: coarse){
  #projects .projects-fg-seats{
    bottom: -36%;
  }
  #projects .projects-seats-close{
    bottom: -82%;
  }
  #projects .projects-marquee{
    bottom: -52px;
  }
  #projects .projects-marquee-titles{
    inset: 25% 10% 50% 10%;
    font-size: clamp(13px, 1.55vw, 26px);
  }
  #projects .projects-marquee-cta{
    top: 58%;
    font-size: clamp(9px, 0.9vw, 14px);
  }
}

/* Ultrawide (≥2:1 aspect ratio). At 21:9 and wider, the seats + close
   seats scale up with viewport width and dominate the auditorium frame.
   Push both rows further down so the back floor reads and the cinema
   screen has presence at the top. BG + screen-rect left untouched —
   they're already glued together via shared screen-rect geometry; any
   object-position shift would desync the painted screen from the
   plate-locked focal slot. Scott 2026-05-13. */
@media (min-aspect-ratio: 2/1) and (min-width: 1200px){ /* gate lowered from 2400: 2:1 windows exist below it (e.g. 1536-wide laptop @ 2.1 aspect) */
  #projects .projects-fg-seats{
    bottom: -24%;
  }
  #projects .projects-seats-close{
    bottom: -51%;
  }
  #projects .projects-marquee{
    bottom: -40px;
    width: clamp(900px, 38vw, 1300px);
  }
}
/* 32:9 super-ultrawide (5120x1440, ~3.56 aspect): far wider than 21:9 (3440),
   so the seats scale up huge and sit too high at the shared -24%. Drop the fg
   row further for >=3:1 only — 3440 (2.39 aspect) keeps the -24% it was tuned at. */
@media (min-aspect-ratio: 3/1){
  #projects .projects-fg-seats{ bottom: -48%; }
  #projects .projects-seats-close{ bottom: -84%; }
  #projects .projects-marquee{ width: clamp(1400px, 33vw, 1800px); }
}
/* Cinematography ultrawide rules are placed AFTER the cinematography
   default block (which lives below) so source-order ties resolve in their
   favour — see the second @media (min-aspect-ratio: 2/1) further down
   the file. Scott 2026-05-13. */

/* DEBUG: red outline marking where we believe the BG screen rectangle is.
   Shown only when body has .projects-screen-debug. Coords mirror triptych. */
#projects .projects-screen-debug-box{
  display: none;
  position: absolute;
  z-index: 99;
  left: 49.65%;
  top: 43.5%;
  width: 32%;
  height: 38%;
  transform: translate(-50%, -50%);
  pointer-events: none;
  outline: 3px dashed #ff2a2a;
  outline-offset: -3px;
  box-shadow: 0 0 0 1px rgba(255,42,42,0.25);
}
body.projects-screen-debug #projects .projects-screen-debug-box{
  display: block;
}

/* PROJECTS — theatre mode overlay spacing handled by GSAP timelines */

/* PROJECTS — desktop MG */
@media (min-width: 1200px){
  #projects .layer-mid.projects-mid-container{
    bottom: 0;
  }
}

/* PROJECTS — tablet portrait: posters fill more of the tall canvas, MG fills the frame */
@media (min-width: 768px) and (max-width: 1199px) and (orientation: portrait) and (hover: none) and (pointer: coarse){
  .about-block p{
    max-width: none;
  }

  #projects .poster{
    width: 100%;
  }
  #projects .layer-mid.projects-mid-container{
    width: 180%;
    left: -40%;
    right: auto;
    bottom: clamp(0px, 1vh, 12px);
  }
}

/* PROJECTS — portrait desktop/laptop */
@media (orientation: portrait) and (hover: hover){
  #projects .layer-mid.projects-mid-container{
    bottom: 0;
  }
}

/* (dead: overlay is display:none — posters live in MG container now) */

/* PROJECTS — landscape / very short viewport: also push screen down to open sky */
@media (orientation: landscape) and (max-height: 600px){
  #projects .layer-mid.projects-mid-container{
    bottom: 0;
  }
}

/* Mobile landscape (short viewport) readability tuning */
@media (orientation: landscape) and (max-height: 520px) and (hover: none) and (pointer: coarse){
  #projects .poster-grid{
    gap: clamp(8px, 1.1vw, 14px);
  }
  #projects .layer-mid.projects-mid-container{
    left: 25%;
    right: auto;
    width: 50%;
    bottom: 0;
  }

  .topnav{
    height: clamp(34px, 4.5vh, 40px);
  }
  .logo img{
    height: clamp(13px, 3.5vh, 18px);
  }
  .topnav a{
    font-size: 11px;
    letter-spacing: 0.06em;
  }

  #about .about-overlay,
  #cinematography .cinema-overlay,
  #colour .colour-overlay{
    padding-top: clamp(72px, 16vh, 108px);
    padding-bottom: clamp(18px, 4vh, 40px);
    padding-left: calc(clamp(14px, 4vw, 22px) + env(safe-area-inset-left, 0px));
    padding-right: calc(clamp(10px, 2.8vw, 16px) + env(safe-area-inset-right, 0px));
  }
  /* About — landscape phone tuning */
  #about .about-overlay{
    justify-content: flex-end;
    padding-bottom: clamp(12px, 4vh, 36px);
  }
  .about-copy{
    text-align: center;
    width: 100% !important;
  }
  .about-block--intro{
    text-align: center;
  }
  #about .about-fg-layer{
    left: max(55vw, calc(100vw - 55vh));
  }
  .about-block h1,
  .about-block h2{
    font-size: clamp(18px, 2.6vw, 30px);
    margin-bottom: 10px;
  }
  .about-block p{
    max-width: none;
    font-size: clamp(11px, 1.4vw, 15px);
    line-height: 1.35;
    margin-bottom: 8px;
  }

  .cinema-copy{
    width: min(620px, 60vw);
    min-height: clamp(170px, 46vh, 320px);
    padding-bottom: clamp(28px, 8vh, 52px);
  }
  .cinema-block h2{
    font-size: clamp(18px, 2.6vw, 30px);
    margin-bottom: 8px;
  }
  .cinema-block p{
    max-width: none;
    font-size: clamp(11px, 1.4vw, 15px);
    line-height: 1.35;
    margin-bottom: 8px;
  }
  .cinema-link--fixed{
    bottom: clamp(14px, 3vh, 30px);
  }
  .colour-copy{
    width: min(700px, 80vw);
    min-height: clamp(170px, 44vh, 300px);
    padding-bottom: clamp(24px, 7vh, 46px);
  }
  .colour-block h2{
    font-size: clamp(18px, 2.6vw, 30px);
    margin-bottom: 8px;
  }
  .colour-block p{
    max-width: none;
    font-size: clamp(11px, 1.4vw, 15px);
    line-height: 1.35;
    margin-bottom: 8px;
  }
}

/* CINEMATOGRAPHY (desktop) — FG sizing now handled by base .cinema-fg-wrap clamp.
   No height-based override needed. DOF blur lives on base .layer-fg. */

/* Cinematography depth stacking */
#cinematography{
  position: relative;
  min-height: 230svh;
  overflow-x: clip;
  overflow-y: visible;
}

#cinematography .scene-media{
  position: sticky;
  top: 0;
  bottom: auto;
  /* Height on the STABLE --scene-vh (like hero/projects/contact), NOT the
     animated --vvh. 2026-05-23: reverted from --vvh — the animated height tween
     moved this scene's BOTTOM edge during iOS URL-bar toggles while the macOS
     transfer bridge at that seam stayed anchored to --scene-vh, so the two slid
     out of sync and flashed a seam (Scott, real iPad). Sitting on --scene-vh
     keeps the bottom static relative to the bridge → no seam at the root.
     Trade-off accepted: a barely-perceptible squish during chrome animation on
     this wide scene, which the neighbouring --scene-vh scenes already live with. */
  height: var(--scene-vh);
  overflow: visible;
  z-index: auto;
  background: linear-gradient(to bottom, #0a0f1a 0%, #111a2a 40%, #0d1520 100%);
}
/* ── Cinematography layer stack ── */
/* Sky gradient is the CSS background. Aurora/stars render on top of that.
   BG plate (treeline+ground with alpha sky) sits IN FRONT of aurora/stars,
   acting as a natural holdout matte — treeline occludes the atmosphere. */
#cinematography .cinema-aurora{ z-index: 1; }
#cinematography .cinema-stars{ z-index: 2; }

/* Seam feather (2026-05-20) — dissolves the About→Cinematography tonal seam.
   The seam is the aurora's *presence* boundary: nothing above ~38%, full teal
   below, which the eye reads as a horizontal line even through the aurora's
   soft mask. This is a full-width sky-toned veil band centred on the seam that
   pulls the aurora's bright top edge down into the dark upper sky. Transparent
   at both ends (no hard edges of its own). Sits at z-2 — above the aurora it
   veils, below the BG treeline (z-3) so trees stay untouched; DOM order puts
   stars after it so they paint on top. Tunable: --seam-y centres the
   band on the line, --seam-spread widens the falloff, --seam-strength is the
   veil opacity, --seam-tone matches the upper sky. */
#cinematography .cinema-seam-feather{
  --seam-y: 38%;        /* centre of the band — on the aurora's emerging edge */
  --seam-spread: 24%;   /* half-height of the soft falloff */
  --seam-tone: #0a0f1a; /* upper-sky tone (matches the BG gradient top) */
  --seam-strength: 0.7;
  position: absolute;
  inset: 0;
  z-index: 2;
  pointer-events: none;
  background: linear-gradient(
    to bottom,
    transparent calc(var(--seam-y) - var(--seam-spread)),
    var(--seam-tone) var(--seam-y),
    transparent calc(var(--seam-y) + var(--seam-spread))
  );
  opacity: var(--seam-strength);
}

/* BG: treeline + ground with alpha sky — anchored bottom, natural width like MG */
#cinematography .layer-bg{
  position: absolute;
  z-index: 3;
  inset: auto;
  bottom: 0;
  left: 0;
  width: 100%;
  height: auto;
  object-fit: cover;
}

/* MG: Train + track — anchored to bottom, spans width.
   Overscan left/right by 3% so the plate bleeds past viewport edges,
   matching the site-wide img.layer overscan rule that the per-scene
   .layer-mid override otherwise cancels. */
#cinematography .cinema-mg-wrap{
  position: absolute;
  z-index: 4;
  bottom: -3%;
  left: -3%;
  right: -3%;
  width: 106%;
  pointer-events: none;
}
#cinematography .layer-mid{
  position: relative;
  display: block;
  width: 100%;
  height: auto;
  pointer-events: none;
}
/* Locomotive headlight glow — locked to MG wrap, follows parallax */
#cinematography .cinema-mg-wrap::before{
  content: '';
  position: absolute;
  z-index: 1;
  bottom: 75%;
  left: 40%;
  width: 16%;
  height: 14%;
  background: radial-gradient(ellipse at center, rgba(255, 210, 80, 0.45) 0%, rgba(255, 180, 40, 0.2) 35%, rgba(255, 160, 20, 0.06) 65%, transparent 90%);
  filter: blur(22px);
  pointer-events: none;
}
/* Locomotive heat/exhaust haze — low rolling atmosphere at the wheels.
   Locked to MG wrap, sits at the base of the train. */
#cinematography .cinema-mg-wrap::after{
  content: '';
  position: absolute;
  z-index: 1;
  bottom: 50%;
  left: 38%;
  width: 42%;
  height: 18%;
  background: radial-gradient(ellipse at center, rgba(180, 210, 240, 0.12) 0%, rgba(160, 200, 230, 0.04) 50%, transparent 85%);
  filter: blur(24px);
  pointer-events: none;
}

/* MG-top: Film light — hung from top-right, partially out of frame like a rigged fixture.
   Bleed past right edge so the crane arm root is never flush to the viewport. */
#cinematography .cinema-mg-light-wrap{
  position: absolute;
  z-index: 6;
  top: clamp(-4%, calc(48vh - 20vw), 46%);
  right: -4%;
  width: clamp(300px, 34vw, 760px);
  pointer-events: none;
}
#cinematography .layer-mid-light{
  position: relative;
  display: block;
  width: 100%;
  height: auto;
  pointer-events: none;
}
/* Light beam — lives on scene-media so it has its own z-index in the scene stack.
   Positioned to originate from the fixture area (top-right) and cast down-left. */
/* Volumetric light beam — locked to the fixture wrap so it follows at every viewport.
   The wrap is z-index 6 (stacking context). ::after at z-index -1 renders behind
   the fixture image but the whole wrap is above MG(4), so beam shows over the train. */
/* Panel glow — wide linear source, not a point. Radial ellipse simulates
   the full LED bar emitting downward. Locked to the fixture wrap. */
/* Beam opacity driven by scroll — JS sets --beam-opacity on .cinema-mg-light-wrap */
#cinematography .cinema-mg-light-wrap::after{
  content: '';
  position: absolute;
  z-index: -1;
  top: 40%;
  left: -120%;
  width: 260%;
  height: 1400%;
  background: radial-gradient(
    ellipse 40% 38% at 50% 6%,
    transparent 0%,
    rgba(255, 248, 230, 0.28) 4%,
    rgba(255, 248, 230, 0.18) 18%,
    rgba(255, 248, 230, 0.08) 38%,
    rgba(255, 248, 230, 0.03) 55%,
    transparent 70%
  );
  opacity: var(--beam-opacity, 0.45);
  filter: blur(36px);
  /* screen (not normal alpha): the warm-white gradient ADDS light to the
     pixels beneath — lifts the train + lit ground, leaves near-black ground
     black — so it reads as a real light source spilling onto surfaces rather
     than a flat white veil. Also dissolves the old hard cutoff edge, since
     screen-of-near-black is invisible. (2026-05-23, Scott-approved A/B.) */
  mix-blend-mode: screen;
  pointer-events: none;
}

/* FG: Alexa — anchored bottom-right, foreground depth anchor.
   Sized by wrap width; img fills wrap naturally. No bleed past
   overflow-x:hidden on body. Subtle DOF blur is optical, not decorative. */
#cinematography .cinema-fg-wrap{
  position: absolute;
  z-index: 7;
  bottom: 0;
  right: 0;
  transform: translateX(clamp(10%, calc(32vh - 18vw), 22%));
  width: clamp(200px, 44vw, 1000px);
  pointer-events: none;
}
#cinematography .layer-fg{
  position: relative;
  display: block;
  width: 100%;
  height: auto;
  filter: blur(0.75px);
  pointer-events: none;
}

/* FG: Colour chart — anchored bottom-left, prominent */
#cinematography .cinema-fg-chart-wrap{
  position: absolute;
  z-index: 7;
  bottom: 0;
  left: clamp(16px, 10vw, 14%);
  width: clamp(140px, 18vw, 440px);
}
#cinematography .layer-fg-chart{
  position: relative;
  display: block;
  width: 100%;
  height: auto;
  filter: blur(0.5px);
  pointer-events: none;
}
/* Cinema CTA — left-justified below copy, inside overlay */
.cinema-cta{
  position: absolute;
  left: 0;
  top: clamp(155px, 21vh, 215px);
  z-index: 2;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 4px;
  text-decoration: none;
  pointer-events: auto;
  cursor: pointer;
}
.cinema-cta-launch{
  color: rgba(200, 169, 110, 1);
  font-family: 'Inter', ui-sans-serif, system-ui, -apple-system, sans-serif;
  font-size: 13px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}
.cinema-cta-logo{
  width: clamp(120px, 14vw, 200px);
  height: auto;
  opacity: 0.92;
}

#cinematography .scene-overlay{ z-index: 8; }

/* Ultrawide (≥2:1) cinematography — placed AFTER the default cinema
   layer rules above so source-order ties resolve in this block's favour.
   Plates sink so the train subject moves from upper-third into optical
   center; FG (Alexa) shrinks and drops so it doesn't dominate the
   foreground at extreme aspect ratios. Scott 2026-05-13. */
@media (min-aspect-ratio: 2/1) and (min-width: 1200px){ /* gate lowered from 2400: 2:1 windows exist below it (e.g. 1536-wide laptop @ 2.1 aspect) */
  #cinematography .layer-bg{
    bottom: -22%;
  }
  #cinematography .cinema-mg-wrap{
    bottom: -47%;
    left: -14%;
    width: 118%;
  }
  #cinematography .cinema-mg-light-wrap{
    top: 6%;
    width: clamp(360px, 42vw, 900px);
  }
  #cinematography .cinema-fg-wrap{
    bottom: -10%;
    width: clamp(440px, 52vw, 1100px);
  }
  #cinematography .cinema-fg-chart-wrap{
    bottom: -5%;
  }
}
/* 2:1 LAPTOP BAND (1200–2399, e.g. 1536-wide @ 2.1 aspect). The block above is
   tuned for 2400px+ where the FG/mg-light clamps hit their px cap. Below the cap
   the vw widths render proportionally larger, and the wrapped copy overruns the
   Launch CTA. Shrink the two plates and drop the CTA below the copy here only —
   2400px+ keeps the tuning above untouched. Scott 2026-06-17. */
@media (min-aspect-ratio: 2/1) and (min-width: 1200px) and (max-width: 2399px){
  #cinematography .cinema-fg-wrap{ width: clamp(440px, 42vw, 1000px); }
  #cinematography .cinema-mg-light-wrap{ width: clamp(360px, 34vw, 820px); }
  #cinematography .cinema-cta{ top: clamp(240px, 36vh, 340px); }
}
/* 32:9 super-ultrawide cinematography: the train MG (118% / -47%) reads too big
   and high in the extreme-wide frame. Shrink + drop it. >=3:1 only — 3440 (2.39)
   keeps the 2:1 values above. */
@media (min-aspect-ratio: 3/1){
  #cinematography .cinema-mg-wrap{ width: 95%; bottom: -60%; left: 9%; }
  #cinematography .layer-bg{ bottom: -48%; }
}


#about{
  position: relative;
  margin-top: -1px;
  min-height: 270svh;
  overflow: visible;
}
#about .scene-media{
  position: sticky;
  top: 0;
  width: 100%;
  /* --vvh per Bug 2 fix: rendered plate height tracks visualViewport.height
     so the composition inside doesn't squish when iOS Safari chrome
     reappears. Layer-stack negative margins (about-fg-wrap, about-overlay)
     stay on --scene-vh — those are pin offsets, not rendered plate height. */
  height: var(--vvh);
  transition: height 0.3s ease-out;
  overflow: hidden;
  z-index: 1;
}
#about .layer-bg{
  inset: 0;
  width: 100%;
  height: 100%;
  object-position: 50% 50%;
  transform: translate3d(0,0,0);
}
/* Night-mood floor — the about BG (room image + the video behind it) sits 10%
   darker, always. Reads as night rather than a bright daytime room and gives the
   overlaid copy a calmer base. GSAP only scales these layers (transform), never
   their filter, so this brightness is safe — it won't get overwritten. 2026-06-22. */
#about .about-bg-primary,
#about .about-bg-video{
  filter: brightness(0.9);
}
/* Cinema sky gradient — matches #cinematography .scene-media background.
   Crossfades in behind the full-frame FG curtain during block 1→2. */
#about .about-bg-cinema{
  position: absolute;
  inset: 0;
  opacity: 0;
  z-index: 3;
  /* Tonal recipe matched EXACTLY to #cinematography .scene-media (gradient) +
     its ::after vignette, so the About bridge and the real cinema sky read as
     one surface through the exit. Previously the vignette was centred mid-frame
     (50% 50%) which darkened the bottom far harder than cinema's high-centred
     (50% 35%) vignette, and the gradient bottomed out darker (#0a0f1a vs
     #0d1520) — that mismatch is what made the seam reappear as you scrolled in. */
  background:
    radial-gradient(ellipse at 50% 35%, transparent 45%, rgba(0,0,0,0.45) 100%),
    linear-gradient(to bottom, #0a0f1a 0%, #111a2a 40%, #0d1520 100%);
  will-change: opacity;
}
/* Stars inside the about cinema sky — same technique as .cinema-stars
   but scoped to the about section. Inherits the parent's opacity. */
.about-stars{
  position: absolute;
  inset: 0;
  pointer-events: none;
}
.about-stars::before,
.about-stars::after{
  content: '';
  position: absolute;
  top: 0; left: 0;
  width: 1px; height: 1px;
  border-radius: 50%;
}
.about-stars::before{
  box-shadow:
    42vw 4vh 0 0.6px rgba(255,255,255,0.8),
    87vw 9vh 0 0.5px rgba(255,255,255,0.7),
    15vw 18vh 0 0.6px rgba(255,255,255,0.75),
    63vw 3vh 0 0.5px rgba(255,255,255,0.65),
    28vw 34vh 0 0.6px rgba(255,255,255,0.7),
    74vw 28vh 0 0.5px rgba(255,255,255,0.6),
    51vw 12vh 0 0.6px rgba(255,255,255,0.8),
    8vw 7vh 0 0.5px rgba(255,255,255,0.65),
    93vw 22vh 0 0.6px rgba(255,255,255,0.75),
    35vw 2vh 0 0.5px rgba(255,255,255,0.6),
    69vw 42vh 0 0.6px rgba(255,255,255,0.8),
    22vw 15vh 0 0.5px rgba(255,255,255,0.65),
    80vw 36vh 0 0.6px rgba(255,255,255,0.7),
    57vw 6vh 0 0.6px rgba(255,255,255,0.8),
    4vw 30vh 0 0.5px rgba(255,255,255,0.65),
    91vw 40vh 0 0.6px rgba(255,255,255,0.75),
    33vw 55vh 0 0.6px rgba(255,255,255,0.8),
    76vw 14vh 0 0.5px rgba(255,255,255,0.6),
    18vw 44vh 0 0.6px rgba(255,255,255,0.7),
    60vw 25vh 0 0.5px rgba(255,255,255,0.65),
    85vw 52vh 0 0.6px rgba(255,255,255,0.6),
    11vw 10vh 0 0.6px rgba(255,255,255,0.8),
    48vw 38vh 0 0.5px rgba(255,255,255,0.65),
    71vw 5vh 0 0.6px rgba(255,255,255,0.75),
    54vw 58vh 0 0.6px rgba(255,255,255,0.8),
    39vw 32vh 0 0.6px rgba(255,255,255,0.7),
    96vw 11vh 0 0.5px rgba(255,255,255,0.65),
    65vw 50vh 0 0.6px rgba(255,255,255,0.6),
    2vw 46vh 0 0.5px rgba(255,255,255,0.7),
    31vw 8vh 0 0.6px rgba(255,255,255,0.65),
    78vw 45vh 0 0.5px rgba(255,255,255,0.75),
    50vw 1vh 0 0.6px rgba(255,255,255,0.6),
    16vw 38vh 0 0.5px rgba(255,255,255,0.7),
    88vw 26vh 0 0.6px rgba(255,255,255,0.65),
    43vw 54vh 0 0.5px rgba(255,255,255,0.6),
    67vw 16vh 0 0.6px rgba(255,255,255,0.8),
    24vw 3vh 0 0.5px rgba(255,255,255,0.7),
    95vw 35vh 0 0.6px rgba(255,255,255,0.65),
    /* bottom-half density — fills the 60–95vh void so the starfield reads
       uniform through the about→cinematography seam */
    12vw 62vh 0 0.6px rgba(255,255,255,0.75),
    73vw 65vh 0 0.5px rgba(255,255,255,0.6),
    38vw 68vh 0 0.6px rgba(255,255,255,0.7),
    89vw 71vh 0 0.6px rgba(255,255,255,0.8),
    25vw 73vh 0 0.5px rgba(255,255,255,0.65),
    56vw 76vh 0 0.6px rgba(255,255,255,0.7),
    6vw 78vh 0 0.5px rgba(255,255,255,0.6),
    82vw 80vh 0 0.6px rgba(255,255,255,0.75),
    44vw 83vh 0 0.5px rgba(255,255,255,0.65),
    68vw 86vh 0 0.6px rgba(255,255,255,0.7),
    19vw 88vh 0 0.5px rgba(255,255,255,0.6),
    94vw 91vh 0 0.6px rgba(255,255,255,0.75),
    32vw 93vh 0 0.5px rgba(255,255,255,0.65);
  animation: starTwinkleA 3s ease-in-out infinite alternate;
}
.about-stars::after{
  box-shadow:
    20vw 8vh 0 0.8px rgba(255,255,255,0.9),
    55vw 18vh 0 0.9px rgba(255,255,255,0.95),
    82vw 32vh 0 0.8px rgba(255,255,255,0.85),
    38vw 48vh 0 0.9px rgba(255,255,255,0.9),
    10vw 25vh 0 0.8px rgba(255,255,255,0.85),
    70vw 10vh 0 0.9px rgba(255,255,255,0.9),
    45vw 55vh 0 0.8px rgba(255,255,255,0.85),
    90vw 42vh 0 0.9px rgba(255,255,255,0.95),
    /* bright accents in bottom half */
    28vw 64vh 0 0.9px rgba(255,255,255,0.9),
    62vw 72vh 0 0.8px rgba(255,255,255,0.85),
    8vw 82vh 0 0.9px rgba(255,255,255,0.9),
    47vw 89vh 0 0.8px rgba(255,255,255,0.85),
    78vw 94vh 0 0.9px rgba(255,255,255,0.95);
  animation: starTwinkleB 4s ease-in-out infinite alternate;
}
#about .about-bg-video{
  z-index: 2;
}
@media (prefers-reduced-motion: reduce){
  #about .about-bg-video{ display: none; }
}
@media (min-width: 768px) and (max-width: 1199px) and (orientation: portrait){
  #about .layer-bg{ object-position: 38% 50%; }
  #about .about-overlay{
    padding-top: clamp(200px, 32vh, 360px);
    padding-left: clamp(24px, 6vw, 60px);
  }
  .about-block--right{
    left: 48%;
  }
}
@media (max-width: 767px) and (orientation: portrait){
  #about .layer-bg{ object-position: 27% 50%; }
  #cinematography .layer-bg{ bottom: 3%; }
}
#about .about-fg-wrap{
  position: sticky;
  top: 0;
  width: 100%;
  margin-top: calc(-1 * var(--scene-vh));
  height: var(--scene-vh);
  overflow: visible;
  z-index: 6;
  pointer-events: none;
}
#about .about-fg-layer{
  position: absolute;
  top: -3%;
  /* Asset 3392x5056, ratio 0.671. Height 106% (6% overscan) absorbs iOS
     Safari URL-bar reflow per the plates-bleed-always principle —
     dimension-based, not transform-based, so it doesn't fight GSAP
     transform ownership during the rolodex (scale up to 3.2). Rendered
     width ~71vh; left formula keeps trunk right of center with the
     studio cutout visible. */
  left: max(45vw, calc(100vw - 71vh));
  width: auto;
  height: 106%;
  transform-origin: 50% 50%;
  display: block;
}
#about .about-overlay{
  position: sticky;
  top: 0;
  margin-top: calc(-1 * var(--scene-vh));
  min-height: var(--scene-vh);
  max-width: none;
  margin-inline: 0;
  display: flex;
  flex-direction: column;
  justify-content: flex-start;
  align-items: flex-start;
  text-align: left;
  padding-top: calc(50svh - clamp(70px, 9svh, 100px));
  padding-bottom: clamp(36px, 5vh, 72px);
  padding-left: var(--scene-copy-left);
  padding-right: clamp(20px, 8vw, 320px);
  z-index: 8;
}
.about-copy{
  position: relative;
  width: min(620px, 50vw);
  min-height: clamp(220px, 38vh, 440px);
}
/* Derive copy width from FG's actual footprint at all tablet+ sizes.
   --about-fg-w uses min(49.57vh, 42vw) so it self-constrains on portrait. */
@media (min-width: 768px){
  .about-copy{
    --about-copy-w: min(calc(100vw - var(--about-fg-w) - var(--scene-copy-left) - 48px), 760px);
    width: var(--about-copy-w);
  }
}
/* Desktop (>=1200px): center the copy column in the valley gap (left edge ->
   redwood) instead of pinning it at --scene-copy-left. The redwood width is
   height-driven (--about-fg-w), so a fixed left-pad strands the copy further
   left as the viewport widens (fine at 1536, ~228px off at 1920, worse beyond).
   Centering on the computed gap self-corrects at every desktop width.
   Scoped to .about-copy only -- the JS-locked TV-credit block (.about-block--center)
   and the right-settled block (.about-block--right) live outside it, untouched. */
@media (min-width: 1200px){
  #about .about-copy{
    margin-left: calc((100vw - var(--about-fg-w) - var(--about-copy-w)) / 2 - var(--scene-copy-left));
  }
}
/* iPad portrait (768-1199): the >=1200 gap-centering above doesn't reach here,
   and at this aspect --about-fg-w (50vw) underestimates where the redwood trunk
   visually sits, so the copy reads ~30px left of the valley-gap centre. Measured
   nudge right, by eye. Scoped to tablet portrait only -- phones (<768), landscape,
   and desktop (>=1200, computed above) are untouched. Floor lowered 768->740
   (2026-05-23) so the iPad mini (744px portrait, below the 768 boundary) gets
   the nudge too -- it was falling back to base left-anchored like the seats did. */
@media (orientation: portrait) and (min-width: 740px) and (max-width: 1199px){
  #about .about-copy{
    margin-left: 30px;
  }
}
.about-block{
  position: absolute;
  inset: 0;
  opacity: 0;
  pointer-events: none;
  transition: opacity 420ms linear, filter 380ms ease;
  color: rgba(245, 232, 212, 0.93);
  text-shadow: 0 2px 20px rgba(0,0,0,0.45);
}
/* When about is inside the pinned pan, GSAP owns the block transforms —
   kill the CSS transition so we don't fight a 420ms class-based fade. */
.hero-projects-pan-track #about .about-block{
  transition: none;
}
.about-block.is-active{
  opacity: 1;
}
/* Block 2 ("Scott McIntyre, CSI") — plate-locked to the TV cutout in the
   about FG plate. Position (left/top/xPercent/yPercent) is set every frame
   by the FG state machine in app.js based on the plate's live transform
   (scale + translate) and the TV's normalised coords (52%, 40%) within
   the plate. CSS just provides structural defaults — the actual positioning
   is JS-driven. Width capped so multi-line body fits the TV area. */
.about-block--center{
  position: absolute;
  /* Explicitly clear inset: 0 from .about-block base rule so the block
     width can size to its content (max-content) instead of stretching
     between left:0 and right:0. JS-driven left/top via gsap.set. */
  top: 0;
  left: 0;
  right: auto;
  bottom: auto;
  text-align: center;
  width: max-content;
  max-width: min(560px, 86vw);
}
.about-block--center h2{
  white-space: nowrap;
}
/* Block 3 ("A studio, not a factory") — lives outside .about-copy as a
   direct child of .about-overlay. Center-aligned in the right half of
   the viewport when the FG tree shifts left. */
.about-block--right{
  position: absolute;
  top: 0;
  bottom: 0;
  left: 55%;
  right: 0;
  width: auto;
  text-align: center;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
}
.about-block h1,
.about-block h2{
  margin: 0 0 18px;
  font-family: 'Cormorant Garamond', Garamond, 'Times New Roman', serif;
  font-weight: 300;
  text-transform: uppercase;
  font-size: clamp(22px, 2.4vw, 40px);
  line-height: 1.16;
  letter-spacing: 0.08em;
  min-height: 1.96em;
}
.about-block p{
  margin: 0 0 14px;
  font-family: 'Inter', ui-sans-serif, system-ui, -apple-system, sans-serif;
  max-width: none;
  font-size: clamp(13px, 1vw, 17px);
  line-height: 1.45;
  letter-spacing: 0.04em;
}
.about-p--wide{ max-width: 56ch; }
/* Mobile-only line breaks — hidden by default, shown on phone portrait */
.mobile-br{ display: none; }
@media (max-width: 767px) and (orientation: portrait){
  .mobile-br{ display: inline; }
}

.about-block--intro{
  text-align: center;
}
/* No intro-specific h2 margin: the rolodex cycles three blocks one at a time,
   so the gap under each headline must read identically or the rhythm jumps as
   you scroll. The intro inherits the shared .about-block h2 spacing at every
   breakpoint (18px desktop / 10px portrait / 6px landscape). A standalone 22px
   override here previously only won at desktop widths, making the intro's gap
   4px looser than blocks 2 and 3. */
/* Desktop-only tighten for the "Smak exists..." lead paragraph */
.about-block--technology p:first-of-type{
  max-width: 27ch;
}

/* Colour section - sticky background progression */
#colour{
  position: relative;
  min-height: 300svh;
  overflow: visible;
}
#colour .scene-media{
  position: sticky;
  top: 0;
  width: 100%;
  /* Bug 2 — Safari chrome resize squish fix. Routed through --vvh
     (live visualViewport.height via JS) instead of --scene-vh so the
     rendered plate height shrinks WITH the URL bar animation, not a
     frame behind dvh recompute. Transition matches Safari's chrome
     animation timing so the plate eases between states instead of
     snapping. Verified on iPhone 2026-04-30; same pattern now on About
     + Cinematography. */
  height: var(--vvh);
  transition: height 0.3s ease-out;
  overflow: hidden;
  z-index: 1;
}

/* ── cinematography-scene vignette ── */
#cinematography .scene-media::after{
  content: '';
  position: absolute;
  inset: 0;
  z-index: 9;
  pointer-events: none;
  /* Ellipse centered high (50% 35%) so the top of the scene stays clean at
     the about→cinematography seam — the vignette falls on the lower half
     where the treeline occludes most of the sky anyway. */
  background: radial-gradient(ellipse at 50% 35%, transparent 45%, rgba(0,0,0,0.45) 100%);
}

/* ── cinematography-scene aurora ── */
#cinematography .cinema-aurora{
  position: absolute;
  inset: 0;
  z-index: 1;
  width: 100%;
  height: 100%;
  pointer-events: none;
  opacity: 0;
  mix-blend-mode: normal;
  filter: blur(1.5px);
  /* Smooth top ramp (0→full opacity over ~35% of viewport) kills the
     visible horizontal edge the aurora used to create at the seam between
     about's dark sky and cinematography's upper sky. */
  -webkit-mask-image: linear-gradient(
    to bottom,
    rgba(0,0,0,0) 0%,
    rgba(0,0,0,0.05) 6%,
    rgba(0,0,0,0.15) 12%,
    rgba(0,0,0,0.35) 20%,
    rgba(0,0,0,0.65) 28%,
    rgba(0,0,0,1) 38%,
    rgba(0,0,0,0.9) 52%,
    rgba(0,0,0,0.5) 65%,
    rgba(0,0,0,0.2) 78%,
    rgba(0,0,0,0) 90%
  );
  mask-image: linear-gradient(
    to bottom,
    rgba(0,0,0,0) 0%,
    rgba(0,0,0,0.05) 6%,
    rgba(0,0,0,0.15) 12%,
    rgba(0,0,0,0.35) 20%,
    rgba(0,0,0,0.65) 28%,
    rgba(0,0,0,1) 38%,
    rgba(0,0,0,0.9) 52%,
    rgba(0,0,0,0.5) 65%,
    rgba(0,0,0,0.2) 78%,
    rgba(0,0,0,0) 90%
  );
}
body.has-cinema-aurora #cinematography .cinema-aurora{
  opacity: 0.55;
}
@media (max-width: 767px){
  body.has-cinema-aurora #cinematography .cinema-aurora{
    opacity: 0.45;
  }
}

/* ── cinematography-scene stars ── */
.cinema-stars{
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  z-index: 2;
  pointer-events: none;
  /* Subtle top-only softening (2026-05-20): just feathers the very top ~14% of
     the star field so it doesn't hard-edge at the seam. Full density everywhere
     below — does NOT thin the scene or steal aurora/grade. */
  -webkit-mask-image: linear-gradient(to bottom, transparent 0%, rgba(0,0,0,0.5) 7%, #000 14%);
  mask-image: linear-gradient(to bottom, transparent 0%, rgba(0,0,0,0.5) 7%, #000 14%);
}
.cinema-stars::before,
.cinema-stars::after{
  content: '';
  position: absolute;
  top: 0;
  left: 0;
  width: 1px;
  height: 1px;
  border-radius: 50%;
  animation: starTwinkle 6s ease-in-out infinite alternate;
}
/* small stars — dense scatter across full sky, treeline occludes naturally */
.cinema-stars::before{
  box-shadow:
    42vw 4vh 0 0.6px rgba(255,255,255,0.8),
    87vw 9vh 0 0.5px rgba(255,255,255,0.7),
    15vw 18vh 0 0.6px rgba(255,255,255,0.75),
    63vw 3vh 0 0.5px rgba(255,255,255,0.65),
    28vw 34vh 0 0.6px rgba(255,255,255,0.7),
    74vw 28vh 0 0.5px rgba(255,255,255,0.6),
    51vw 12vh 0 0.6px rgba(255,255,255,0.8),
    8vw 7vh 0 0.5px rgba(255,255,255,0.65),
    93vw 22vh 0 0.6px rgba(255,255,255,0.75),
    35vw 2vh 0 0.5px rgba(255,255,255,0.6),
    69vw 42vh 0 0.6px rgba(255,255,255,0.8),
    22vw 15vh 0 0.5px rgba(255,255,255,0.65),
    80vw 36vh 0 0.6px rgba(255,255,255,0.7),
    46vw 48vh 0 0.5px rgba(255,255,255,0.6),
    57vw 6vh 0 0.6px rgba(255,255,255,0.8),
    4vw 30vh 0 0.5px rgba(255,255,255,0.65),
    91vw 40vh 0 0.6px rgba(255,255,255,0.75),
    33vw 55vh 0 0.6px rgba(255,255,255,0.8),
    76vw 14vh 0 0.5px rgba(255,255,255,0.6),
    18vw 44vh 0 0.6px rgba(255,255,255,0.7),
    60vw 25vh 0 0.5px rgba(255,255,255,0.65),
    85vw 52vh 0 0.6px rgba(255,255,255,0.6),
    11vw 10vh 0 0.6px rgba(255,255,255,0.8),
    48vw 38vh 0 0.5px rgba(255,255,255,0.65),
    71vw 5vh 0 0.6px rgba(255,255,255,0.75),
    26vw 20vh 0 0.5px rgba(255,255,255,0.6),
    54vw 58vh 0 0.6px rgba(255,255,255,0.8),
    39vw 32vh 0 0.6px rgba(255,255,255,0.7),
    96vw 11vh 0 0.5px rgba(255,255,255,0.65),
    65vw 50vh 0 0.6px rgba(255,255,255,0.6),
    2vw 46vh 0 0.5px rgba(255,255,255,0.7),
    31vw 8vh 0 0.6px rgba(255,255,255,0.65),
    78vw 45vh 0 0.5px rgba(255,255,255,0.75),
    50vw 1vh 0 0.6px rgba(255,255,255,0.6),
    16vw 38vh 0 0.5px rgba(255,255,255,0.7),
    88vw 26vh 0 0.6px rgba(255,255,255,0.65),
    43vw 54vh 0 0.5px rgba(255,255,255,0.6),
    67vw 16vh 0 0.6px rgba(255,255,255,0.8),
    24vw 3vh 0 0.5px rgba(255,255,255,0.7),
    95vw 35vh 0 0.6px rgba(255,255,255,0.65),
    /* additional density */
    6vw 41vh 0 0.5px rgba(255,255,255,0.6),
    38vw 11vh 0 0.6px rgba(255,255,255,0.7),
    82vw 47vh 0 0.5px rgba(255,255,255,0.65),
    14vw 53vh 0 0.6px rgba(255,255,255,0.7),
    72vw 21vh 0 0.5px rgba(255,255,255,0.6),
    47vw 43vh 0 0.6px rgba(255,255,255,0.75),
    89vw 33vh 0 0.5px rgba(255,255,255,0.65),
    21vw 27vh 0 0.6px rgba(255,255,255,0.7),
    58vw 51vh 0 0.5px rgba(255,255,255,0.6),
    3vw 17vh 0 0.6px rgba(255,255,255,0.75),
    41vw 60vh 0 0.5px rgba(255,255,255,0.6),
    77vw 8vh 0 0.6px rgba(255,255,255,0.7),
    29vw 46vh 0 0.5px rgba(255,255,255,0.65),
    64vw 37vh 0 0.6px rgba(255,255,255,0.7),
    92vw 54vh 0 0.5px rgba(255,255,255,0.6),
    10vw 33vh 0 0.6px rgba(255,255,255,0.75),
    55vw 41vh 0 0.5px rgba(255,255,255,0.65),
    83vw 19vh 0 0.6px rgba(255,255,255,0.7),
    36vw 57vh 0 0.5px rgba(255,255,255,0.6),
    70vw 31vh 0 0.6px rgba(255,255,255,0.7);
  animation: starTwinkleA 3s ease-in-out infinite alternate;
}
/* bright accent stars — scattered deep, offset twinkle */
.cinema-stars::after{
  box-shadow:
    20vw 8vh 0 0.8px rgba(255,255,255,0.9),
    55vw 18vh 0 0.9px rgba(255,255,255,0.95),
    82vw 5vh 0 0.8px rgba(255,255,255,0.85),
    37vw 14vh 0 0.7px rgba(255,255,255,0.9),
    68vw 38vh 0 0.8px rgba(255,255,255,0.8),
    12vw 26vh 0 0.9px rgba(255,255,255,0.9),
    44vw 3vh 0 0.8px rgba(255,255,255,0.85),
    90vw 16vh 0 0.7px rgba(255,255,255,0.8),
    7vw 42vh 0 0.8px rgba(255,255,255,0.9),
    59vw 32vh 0 0.9px rgba(255,255,255,0.95),
    73vw 10vh 0 0.8px rgba(255,255,255,0.85),
    30vw 48vh 0 0.7px rgba(255,255,255,0.8),
    47vw 6vh 0 0.8px rgba(255,255,255,0.9),
    84vw 24vh 0 0.9px rgba(255,255,255,0.85),
    5vw 55vh 0 0.8px rgba(255,255,255,0.9),
    /* additional bright stars */
    25vw 36vh 0 0.8px rgba(255,255,255,0.85),
    62vw 45vh 0 0.7px rgba(255,255,255,0.9),
    16vw 52vh 0 0.8px rgba(255,255,255,0.85),
    78vw 29vh 0 0.9px rgba(255,255,255,0.9),
    41vw 22vh 0 0.8px rgba(255,255,255,0.95),
    94vw 44vh 0 0.7px rgba(255,255,255,0.85),
    52vw 56vh 0 0.8px rgba(255,255,255,0.9),
    8vw 35vh 0 0.9px rgba(255,255,255,0.85);
  animation: starTwinkleB 4s ease-in-out infinite alternate;
  animation-delay: -1.5s;
}
@keyframes starTwinkleA{
  0%   { opacity: 0.15; }
  100% { opacity: 0.9;  }
}
@keyframes starTwinkleB{
  0%   { opacity: 0.1;  }
  100% { opacity: 1;    }
}
@media (prefers-reduced-motion: reduce){
  .cinema-stars::before,
  .cinema-stars::after{ animation: none; opacity: 0.6; }
}

/* ── colour-scene snow ── */
#colour .colour-snow{
  position: absolute;
  inset: 0;
  z-index: 3;
  width: 100%;
  height: 100%;
  pointer-events: none;
  opacity: 0;
}
body.has-colour-snow #colour .colour-snow{
  opacity: 1;
}
@media (prefers-reduced-motion: reduce){
  #colour .colour-snow{ display: none; }
}

#colour .layer-mid{
  z-index: 4;
  pointer-events: none;
  top: -8%;
  left: 0;
  width: 100%;
  height: 116%;
  object-fit: cover;
  object-position: center center;
}
.colour-fg-wrap{
  position: absolute;
  z-index: 8;
  bottom: -4%;
  left: 0;
  right: 0;
  width: 100%;
  pointer-events: none;
}
#colour .layer-fg{
  position: relative;
  display: block;
  width: 100%;
  height: auto;
  /* Override the site-wide img.layer overscan rule (inset: -3%) which
     shifts position:relative elements up-left by 3% of parent. The colour
     FG is wrapped in .colour-fg-wrap and needs to render at its natural
     position. */
  inset: auto;
  pointer-events: none;
}

/* Ultrawide (≥2:1) colour FG — push the monitor + console plate lower
   so the wide mountain backdrop has more vertical room above it. Only
   `bottom` changes here; the wrap stays full-width so the portal + CTA
   coords (percentage-based on the wrap) stay glued to the painted
   monitor on the FG plate. Scott 2026-05-13. */
@media (min-aspect-ratio: 2/1) and (min-width: 1200px){ /* gate lowered from 2400: 2:1 windows exist below it (e.g. 1536-wide laptop @ 2.1 aspect) */
  .colour-fg-wrap{
    bottom: -22%;
  }
}
/* 32:9 super-ultrawide colour: the full-width suite reads too big. Shrink it
   centered — portal/CTA are % of the wrap so they shrink glued. >=3:1 only;
   3440 keeps full-width. */
@media (min-aspect-ratio: 3/1){
  #colour .colour-fg-wrap{ width: 65%; left: 17.5%; }
}

.colour-screen-cta{
  position: absolute;
  /* Mapped to HX310 LCD panel within fg.avif (1794×721).
     Kept in sync with .colour-contact-portal so the DVD bounce zone
     matches the video box exactly. */
  left: 35.6%;
  top: 7.5%;
  width: 28.9%;
  height: 27.5%;
  overflow: hidden;
  text-decoration: none;
  pointer-events: auto;
  cursor: pointer;
}
/* Shader-treated preview of the contact scene, rendered onto the
   HX310 reference display. Sits above layer-fg, below the Launch CTA.
   Shares the exact coordinates of .colour-screen-cta. */
.colour-contact-portal{
  position: absolute;
  left: 35.6%;
  top: 7.5%;
  width: 28.9%;
  height: 27.5%;
  overflow: hidden;
  pointer-events: none;
  background: #000;
}
.colour-contact-portal-video{
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center center;
  display: block;
}
/* Contact peek-behind — during colour's pinned zoom, the real
   contact scene's scene-media is temporarily fixed to the viewport
   so it sits z-behind the fading colour layers. Hides the form/text
   overlay (still pointing at its natural scroll position). Class is
   toggled by app.js via ScrollTrigger onEnter/onLeave. */
#colour{ position: relative; z-index: 5; }
#contact.is-peeking .scene-media{
  position: fixed;
  inset: 0;
  z-index: 1;
}
.colour-screen-bounce{
  position: absolute;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
}
.colour-screen-launch{
  color: rgba(200, 169, 110, 1);
  font-family: 'Inter', ui-sans-serif, system-ui, -apple-system, sans-serif;
  font-size: clamp(10px, 1vw, 16px);
  letter-spacing: 0.7em;
  text-transform: uppercase;
  width: 100%;
  text-align: center;
  text-indent: 0.7em;
}
.colour-screen-logo{
  width: clamp(75px, 11vw, 165px);
  height: auto;
  opacity: 1;
}
#colour .colour-overlay{
  position: sticky;
  top: 0;
  margin-top: calc(-1 * var(--scene-vh));
  height: var(--scene-vh);
  max-width: none;
  margin-inline: 0;
  display: flex;
  justify-content: center;
  align-items: center;
  text-align: center;
  padding-top: 0;
  padding-right: clamp(20px, 5vw, 120px);
  padding-bottom: 42vh;
  padding-left: clamp(20px, 5vw, 120px);
  z-index: 6;
  pointer-events: none;
}
#colour .colour-overlay::before{
  content: "";
  position: absolute;
  inset: 0;
  background: radial-gradient(ellipse at 50% 30%, rgba(0,0,0,0.38) 0%, rgba(0,0,0,0.15) 60%, rgba(0,0,0,0) 100%);
  pointer-events: none;
  z-index: -1;
}

/* Ultrawide (≥2:1) colour overlay — lift the text block higher in the
   frame so it sits above the (now lower) FG plate rather than centered
   against it. padding-bottom in this scene is the vertical-positioning
   knob (overlay is flex with center alignment + padding-bottom shifts
   the content upward). Scott 2026-05-13. */
@media (min-aspect-ratio: 2/1) and (min-width: 1200px){ /* gate lowered from 2400: lift copy above the FG on 2:1 laptops too, not just ultrawide */
  #colour .colour-overlay{
    padding-bottom: 55vh;
  }
}

.colour-copy{
  position: relative;
  width: min(1060px, 88vw);
  min-height: clamp(200px, 35vh, 380px);
  max-height: 45vh;
  padding-bottom: clamp(16px, 3vh, 32px);
}
.colour-block{
  position: absolute;
  inset: 0;
  pointer-events: none;
  color: rgba(245, 232, 212, 0.93);
  text-shadow: 0 2px 20px rgba(0,0,0,0.45);
}
.colour-block .wave-line{
  display: block;
  will-change: transform;
}
.colour-block .wave-word{
  display: inline-block;
  white-space: nowrap;
}
.colour-block .wave-char{
  display: inline-block;
  opacity: 0;
}
.colour-block h2{
  font-family: 'Cormorant Garamond', Garamond, 'Times New Roman', serif;
  font-weight: 300;
  text-transform: uppercase;
  margin: 0 auto 22px;
  font-size: clamp(22px, 2.4vw, 40px);
  line-height: 1.16;
  letter-spacing: 0.08em;
  max-width: none;
  text-wrap: balance;
}
.colour-block p{
  margin: 0 auto 14px;
  max-width: none;
  font-family: 'Inter', ui-sans-serif, system-ui, -apple-system, sans-serif;
  font-size: clamp(13px, 1vw, 17px);
  line-height: 1.45;
  letter-spacing: 0.04em;
}

/* Posters */
.poster-grid{
  display:grid;
  /* Continuous scaling — factors in both width AND height so posters
     can't outgrow the sky space above the stage on shorter viewports */
  grid-template-columns: repeat(3, clamp(100px, min(18vw, 25vh), 260px));
  gap: clamp(14px, 3.5vw, 44px);
  justify-content: center;
  align-items: start;
  pointer-events: auto;
  /* Sky position — absolute within full-height MG container */
  position: absolute;
  top: clamp(10px, 3vh, 60px);
  left: 50%;
  transform: translateX(-50%);
  z-index: 2;  /* above stage/MG image */
}
.poster{
  border: none;
  background: transparent;
  padding: 0;
  text-align: left;
  cursor: pointer;
  color: var(--text);
  width: 100%;
  transition: opacity 380ms ease, filter 380ms ease;
}
.poster img{
  width:100%;
  height:auto;
  display:block;
  box-shadow: 0 20px 60px rgba(0,0,0,0.55);
  transition: transform 520ms var(--ease-push-in), opacity 380ms ease, filter 380ms ease, box-shadow 520ms ease;
}
.poster span{
  display:block;
  margin-top: 10px;
  letter-spacing: 0.12em;
  opacity: 0.9;
}
/* Theatre poster transitions owned by GSAP timelines in app.js */

/* CTA cards */
.cta-card{
  display:inline-block;
  margin-top: 18px;
  padding: 16px 18px;
  border: 1px solid rgba(255,255,255,0.18);
  background: rgba(0,0,0,0.35);
  backdrop-filter: blur(8px);
  color: var(--text);
  text-decoration: none;
  border-radius: 10px;
}
.cta-title{
  font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Arial, sans-serif;
  letter-spacing: 0.12em;
  text-transform: uppercase;
}
.cta-sub{ opacity: 0.85; margin-top: 6px; }

/* CINEMATOGRAPHY scroll blocks */
#cinematography .cinema-overlay{
  position: sticky;
  top: 0;
  margin-top: calc(-1 * var(--scene-vh));
  min-height: var(--scene-vh);
  max-width: none;
  margin-inline: 0;
  display: flex;
  align-items: flex-start;
  text-align: left;
  padding-top: clamp(80px, 10vh, 140px);
  padding-bottom: clamp(34px, 8vh, 96px);
  padding-left: var(--scene-copy-left);
  padding-right: clamp(22px, 6vw, 92px);
  z-index: 10;
}
/* beach-scene readability gradient removed — night scene needs no help */
.cinema-copy{
  position: relative;
  width: min(900px, 60vw);
  min-height: clamp(260px, 52vh, 600px);
  padding-bottom: clamp(24px, 5vh, 48px);
}
.cinema-block{
  position: absolute;
  inset: 0;
  opacity: 0;
  pointer-events: none;
  transition: opacity 420ms linear, filter 380ms ease;
  color: rgba(245, 232, 212, 0.93);
  text-shadow: 0 2px 20px rgba(0,0,0,0.45);
}
.cinema-block.is-active{
  opacity: 1;
}
.cinema-block h2{
  margin: 0 0 18px;
  font-family: 'Cormorant Garamond', Garamond, 'Times New Roman', serif;
  font-weight: 300;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  font-size: clamp(22px, 2.4vw, 40px);
  line-height: 1.16;
  white-space: nowrap;
}
@media (max-width: 767px){
  .cinema-block h2{
    white-space: normal;
  }
}
.cinema-block p{
  margin: 0 0 8px;
  max-width: none;
  font-family: 'Inter', ui-sans-serif, system-ui, -apple-system, sans-serif;
  font-weight: 400;
  font-size: clamp(13px, 1vw, 17px);
  line-height: 1.45;
  letter-spacing: 0.04em;
}
.cinema-block p + p{
  margin-top: 6px;
}
.cinema-link--fixed{
  position: absolute;
  left: 0;
  bottom: 0;
  margin-top: 0;
  z-index: 9;
  pointer-events: auto;
}


/* Contact */
@media (prefers-reduced-motion: reduce){
  #contact .contact-bg-video{ display: none; }
}
#contact .scene-overlay{
  /* padding-top bumped 2026-05-17 from clamp(140,19vh,224) → clamp(220,26vh,320)
     so contact card top sits comfortably below the fixed topnav at end-of-
     scroll (where the contact section bottom aligns with viewport bottom and
     the card would otherwise tuck under the nav at 48px). Mobile overrides
     below at .scene-overlay rules in the @media blocks are unchanged. */
  padding-top: clamp(220px, 26vh, 320px);
  max-width: none;
  margin-inline: 0;
}


/* Thesis copy block — lifted out of the contact-card 2026-05-17 so it
   reads as a composed scene block matching about/cinema/colour typography
   grammar, not as form preamble. Cormorant Garamond uppercase headline
   + Inter body. Same family of treatments as .colour-block / .cinema-block. */
.contact-copy{
  position: relative;
  width: min(640px, 60vw);
  margin-bottom: clamp(20px, 2.6vh, 36px);
  /* Sits in scene-overlay above .contact-card. Card's own margin-top:12px
     pulls form snugly under the headline without overdoing the gap. */
}
.contact-block{
  color: rgba(245, 232, 212, 0.93);
  text-shadow: 0 2px 20px rgba(0,0,0,0.45);
}
.contact-block h2{
  font-family: 'Cormorant Garamond', Garamond, 'Times New Roman', serif;
  font-weight: 300;
  text-transform: uppercase;
  margin: 0 0 16px;
  font-size: clamp(22px, 2.4vw, 40px);
  line-height: 1.16;
  letter-spacing: 0.08em;
  text-wrap: balance;
}
.contact-block p{
  margin: 0 0 12px;
  font-family: 'Inter', ui-sans-serif, system-ui, -apple-system, sans-serif;
  font-size: clamp(13px, 1vw, 17px);
  line-height: 1.45;
  letter-spacing: 0.04em;
}

/* Contact card — Oryzo-inspired soft blur panel flush to the left edge.
   Negative margin-left equal to the overlay's left padding breaks the
   card out to viewport edge; inner padding-left re-establishes type
   margin. Heavy backdrop-blur + low bg opacity so the plate behind
   reads through the panel. No rounded corners — cinematic rectangle. */
.contact-card{
  margin-top: 12px;
  margin-left: calc(-1 * clamp(16px, 6vw, 84px));
  max-width: min(520px, 44vw);
  /* Padding removed from top (now owned by .contact-card-titlebar) so the
     title bar bleeds edge-to-edge inside the card frame. */
  padding: 0 clamp(20px, 2.2vw, 28px) clamp(16px, 1.6vw, 22px) clamp(28px, 4vw, 44px);
  border-radius: 0;
  /* Bumped from 0.22 to 0.7 (2026-05-17) so WCAG AA contrast for placeholder
     text holds regardless of which frame of the autoplay BG video is on
     screen. At 0.7 + 34px backdrop-blur, worst-case bright frame still yields
     ~7:1 against the 0.92-white input text. Glass character preserved by the
     heavy backdrop blur. */
  background: rgba(8,8,10,0.7);
  border: 1px solid rgba(255,255,255,0.08);
  border-left: none;
  backdrop-filter: blur(34px) saturate(1.1);
  -webkit-backdrop-filter: blur(34px) saturate(1.1);
  /* Smooth collapse to title-bar-only when minimized. Body height handled
     in .contact-card-body — card itself just animates the trim. */
  transition: max-width 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}

/* ── macOS title bar (contact card) ──────────────────────────────────
   Recurring grammar with the SET→POST file-transfer bridge between
   cinema and colour. Traffic lights match bridge styling exactly.
   Title bar bleeds edge-to-edge inside the card (negative horizontal
   margins cancel card's padding-x). Click red light → minimize body.
   Click minimized card → restore. */
.contact-card-titlebar{
  display: flex;
  align-items: center;
  position: relative;
  padding: clamp(8px, 0.8vw, 12px) clamp(12px, 1.2vw, 20px);
  margin: 0 calc(-1 * clamp(20px, 2.2vw, 28px)) clamp(14px, 1.4vw, 22px) calc(-1 * clamp(28px, 4vw, 44px));
  background: #1E1E1E;
  border-bottom: 1px solid rgba(255,255,255,0.04);
  user-select: none;
}
.contact-card-traffic{
  display: flex;
  gap: clamp(5px, 0.4vw, 7px);
  align-items: center;
  /* Match bridge .transfer-traffic positioning offsets */
}
.contact-card-traffic .traffic-light{
  /* Inherits .traffic-light sizing from the bridge rules (clamp(7px,0.6vw,10px)).
     Both red (minimize) and yellow (expand) are functional <button>s.
     No gray light — empty slots feel like UI debris, every dot earns a role. */
  border: none;
  padding: 0;
  cursor: pointer;
  position: relative;
}
.contact-card-traffic button.traffic-light:focus-visible{
  outline: 2px solid rgba(255,255,255,0.6);
  outline-offset: 2px;
}
.contact-card-traffic .traffic-red:hover::after,
.contact-card-traffic .traffic-yellow:hover::after{
  /* Subtle macOS glyphs on hover. × for minimize, + for expand. */
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: clamp(8px, 0.65vw, 11px);
  font-weight: 700;
  color: rgba(0,0,0,0.55);
  line-height: 1;
}
.contact-card-traffic .traffic-red:hover::after{ content: "×"; }
.contact-card-traffic .traffic-yellow:hover::after{ content: "+"; }
.contact-card-title{
  flex: 1;
  text-align: center;
  font-family: 'Inter', ui-sans-serif, system-ui, -apple-system, sans-serif;
  font-size: clamp(11px, 0.85vw, 13px);
  font-weight: 500;
  letter-spacing: 0.01em;
  color: rgba(255,255,255,0.78);
  /* Compensate for traffic-lights width on the left so the title is
     visually centered to the card, not centered to the post-traffic space.
     Width approx: 2 dots × clamp(7,0.6vw,10) + 1 gap × clamp(5,0.4vw,7). */
  padding-right: calc(clamp(7px, 0.6vw, 10px) * 2 + clamp(5px, 0.4vw, 7px));
}

/* Body wraps intro + form so we can collapse it cleanly with a max-height
   transition. max-height 0 collapses; restoring sets a generous ceiling
   that exceeds any realistic body content height. */
.contact-card-body{
  overflow: hidden;
  max-height: 2000px;
  opacity: 1;
  transition: max-height 0.32s cubic-bezier(0.4, 0, 0.2, 1),
              opacity 0.2s ease,
              margin 0.32s cubic-bezier(0.4, 0, 0.2, 1);
}
.contact-card.is-minimized{
  cursor: pointer;
}
.contact-card.is-minimized .contact-card-body{
  max-height: 0;
  opacity: 0;
  margin-top: 0;
}
.contact-card.is-minimized .contact-card-titlebar{
  margin-bottom: 0;
  border-bottom-color: transparent;
}

.contact-form{ display:grid; gap:10px; font-family: 'Inter', ui-sans-serif, system-ui, -apple-system, sans-serif; }
.form-row{ display:grid; gap:6px; }
.contact-form input,
.contact-form textarea{
  width: 100%;
  border-radius: 0;
  border: 1px solid rgba(255,255,255,0.14);
  background: rgba(0,0,0,0.18);
  color: rgba(255,255,255,0.92);
  padding: 11px 12px;
  font-family: inherit;
  font-size: 14px;
  outline: none;
}
.contact-form textarea{ min-height: 88px; resize: vertical; }
.contact-form input::placeholder,
.contact-form textarea::placeholder{ color: rgba(255,255,255,0.62); letter-spacing: 0.01em; }
.contact-form input:focus,
.contact-form textarea:focus{ border-color: rgba(255,255,255,0.35); }
/* Invalid-state cue (email format, required empty after typing). Gated on
   :not(:placeholder-shown) so empty required fields on page load stay
   neutral — only shows red after the user has put something in. Tone
   matches the macOS title-bar red (#FF5F57) at low alpha for a calm
   "check this" signal rather than a panicked error. Browser still shows
   its native validation message on submit when the field is invalid. */
.contact-form input:not(:placeholder-shown):invalid,
.contact-form textarea:not(:placeholder-shown):invalid{
  border-color: rgba(255, 95, 87, 0.45);
}
.contact-form input:not(:placeholder-shown):invalid:focus,
.contact-form textarea:not(:placeholder-shown):invalid:focus{
  border-color: rgba(255, 95, 87, 0.7);
}

.form-actions{ display:flex; gap:12px; align-items:center; margin-top: 4px; }
.btn{
  border: 1px solid rgba(255,255,255,0.22);
  background: rgba(255,255,255,0.06);
  color: rgba(255,255,255,0.92);
  padding: 10px 18px;
  border-radius: 0;
  letter-spacing: .18em;
  text-transform: uppercase;
  font-size: 11px;
  cursor: pointer;
}
.btn:hover{ background: rgba(255,255,255,0.12); }
.form-status{ font-size: 13px; opacity: .85; }
.hp{ position:absolute; left:-9999px; width:1px; height:1px; overflow:hidden; }


/* ── SET → POST transfer bridge ──────────────────────────────────────
   macOS file-transfer dialog blown up to viewport scale. Layout stays
   left-anchored (native dialog proportion), progress bar stretches
   across the width to sell the scroll-time fill. Scroll scrubs the
   fill, counters, and ETA via ScrollTrigger. Pinned for ~2vh of
   scroll so the transfer has room to play as a narrative beat. */
/* Bridge is a thin flow element. The dialog inside spans the full viewport
   width and its top/bottom rounded corners bleed into the cinematography
   scene above and the colour scene below via negative margins equal to
   the corner radius. No left/right gutter, no section padding. */
.transfer-bridge{
  position: relative;
  z-index: 10;
  padding: 0;
  overflow: visible;
  line-height: 0;
}
.transfer-dialog{
  --r: clamp(16px, 1.4vw, 24px);
  position: relative;
  width: 100%;
  background: #1E1E1E;
  border-radius: var(--r);
  margin-top: calc(-1 * var(--r));
  margin-bottom: calc(-1 * var(--r));
  padding: clamp(16px, 1.4vw, 22px) clamp(22px, 2.4vw, 44px) clamp(8px, 0.7vw, 12px) clamp(10px, 1vw, 18px);
  line-height: 1.4;
  z-index: 2;
}
.transfer-traffic{
  position: absolute;
  top: clamp(7px, 0.7vw, 11px);
  left: clamp(12px, 1.2vw, 20px);
  display: flex;
  gap: clamp(5px, 0.4vw, 7px);
  align-items: center;
}
.traffic-light{
  width: clamp(7px, 0.6vw, 10px);
  height: clamp(7px, 0.6vw, 10px);
  border-radius: 50%;
  box-shadow: inset 0 0 0 1px rgba(0,0,0,0.2);
}
.traffic-red{ background: #FF5F57; }
.traffic-yellow{ background: #FEBC2E; }
.traffic-gray{ background: #8A8A8A; }

.transfer-main{
  display: flex;
  align-items: center;
  gap: clamp(10px, 1.2vw, 18px);
  width: 100%;
}
.transfer-icon{
  width: clamp(28px, 2.4vw, 42px);
  height: auto;
  flex-shrink: 0;
}
.transfer-content{
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: clamp(3px, 0.3vw, 5px);
}
.transfer-title{
  margin: 0;
  font-family: 'Inter', ui-sans-serif, system-ui, -apple-system, sans-serif;
  font-size: clamp(12px, 0.95vw, 15px);
  font-weight: 600;
  letter-spacing: -0.005em;
  color: rgba(255,255,255,0.95);
  line-height: 1.3;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.transfer-title .transfer-quote{
  font-weight: 400;
  color: rgba(255,255,255,0.75);
}
.transfer-progress{
  width: 100%;
}
.transfer-progress-track{
  width: 100%;
  height: clamp(4px, 0.35vw, 6px);
  background: rgba(255,255,255,0.14);
  border-radius: 999px;
  overflow: hidden;
}
.transfer-progress-fill{
  height: 100%;
  width: 100%;
  background: #0A84FF;
  border-radius: 999px;
  transform-origin: 0 50%;
  transform: scaleX(0);
  will-change: transform;
}
.transfer-status{
  margin: 0;
  font-family: 'Inter', ui-sans-serif, system-ui, -apple-system, sans-serif;
  font-size: clamp(11px, 0.85vw, 13px);
  font-weight: 400;
  color: rgba(255,255,255,0.55);
  letter-spacing: 0;
}

@media (max-width: 767px){
  .transfer-dialog{
    --r: 14px;
    padding: 15px 14px 8px 10px;
  }
  .transfer-traffic{
    top: 7px;
    left: 12px;
    gap: 5px;
  }
  .traffic-light{
    width: 9px;
    height: 9px;
  }
  .transfer-main{
    gap: 10px;
  }
  .transfer-icon{
    width: 26px;
  }
  .transfer-title{
    font-size: 8px;
    letter-spacing: 0.015em;
    font-weight: 500;
    white-space: normal;
  }
  .transfer-status{
    font-size: 8px;
    letter-spacing: 0.015em;
  }
  .transfer-progress-track{
    height: 4px;
  }
  .transfer-content{
    gap: 5px;
  }
}

/* Constellation Footer — unified across all SMAK properties. Mirrors the
   custom-css rules applied to the TheGem WordPress footer on SmakTalk,
   SmakColour, SmakCinema so the constellation reads the same everywhere:
   gold divider, very dark gray links, E0E0E0 highlight for current property. */
.constellation-footer{
  position: relative;
  /* Above the global film grain (.hero-grain, z-10) so the grain doesn't lift
     the near-black footer toward grey — keeps the footer matching the other
     SMAK property footers (WordPress/TheGem parity). Grain stays on the scenes;
     it just no longer covers this chrome bar. */
  z-index: 11;
  background: #0a0a0a;
  border-top: 1px solid rgba(200, 169, 110, 1);
  padding: 50px 0 40px;
  margin-top: 0;
  text-align: center;
}
.constellation-links{
  display: flex;
  justify-content: center;
  align-items: center;
  gap: 10px;
  margin-bottom: 12px;
}
.constellation-links a{
  font-family: 'Inter', ui-sans-serif, system-ui, -apple-system, sans-serif;
  font-size: 11px;
  letter-spacing: 0.15em;
  line-height: 28px;
  text-transform: uppercase;
  text-decoration: none;
  color: #484848;
  font-weight: 400;
  transition: color 0.2s;
}
.constellation-links a:hover{
  color: #E0E0E0;
}
.constellation-links a.active{
  color: #E0E0E0;
}
.constellation-links .sep{
  color: #333333;
  font-size: 8px;
  user-select: none;
}
.constellation-copy{
  font-family: 'Inter', ui-sans-serif, system-ui, -apple-system, sans-serif;
  font-size: 11px;
  letter-spacing: 0.15em;
  line-height: 28px;
  text-transform: uppercase;
  color: #333333;
}

@media (max-width: 767px){
  .constellation-footer{ padding: 5px; }
  .constellation-links{ gap: 6px; }
  .constellation-links a{
    font-size: 8px;
    letter-spacing: 0.025em;
    line-height: 24px;
  }
  .constellation-links .sep{ font-size: 6px; }
  .constellation-copy{
    font-size: 8px;
    letter-spacing: 0.025em;
    line-height: 24px;
  }
}

#contact .contact-mid-container{
  top: auto;
  right: -2%;
  bottom: -1%;
  left: auto;
  width: auto;
  height: 68%;
}
.contact-mid-image{
  display: block;
  height: 100%;
  width: auto;
  filter: blur(0.5px);
}
@media (max-width: 767px){
  .contact-mid-image{
    height: auto;
    width: 100%;
  }
}

/* ── lighthouse glow pulse ── */
/* Anchor fills the MG container so glow % tracks the image at every breakpoint */
.lighthouse-glow-anchor{
  position: absolute;
  inset: 0;
  pointer-events: none;
}
.lighthouse-glow{
  position: absolute;
  width: 16%;
  height: 0;
  padding-bottom: 16%;
  top: 1%;
  right: 37%;
  transform: translate(50%, -50%);
  z-index: 2;
  pointer-events: none;
  border-radius: 50%;
  background: radial-gradient(
    circle,
    rgba(255, 200, 100, 1) 0%,
    rgba(255, 175, 60, 0.7) 20%,
    rgba(255, 150, 40, 0.35) 45%,
    rgba(255, 130, 30, 0.1) 70%,
    transparent 100%
  );
  mix-blend-mode: screen;
  animation: lighthousePulse 8s ease-in-out infinite;
}
@keyframes lighthousePulse{
  0%, 100% { opacity: 0.65; transform: scale(0.92); }
  50%      { opacity: 1;    transform: scale(1.18); }
}
@media (prefers-reduced-motion: reduce){
  .lighthouse-glow{ animation: none; opacity: 0.5; }
}

/* Re-enable hover on MG + FG so the focus-rack mouseenter fires.
   Global rule at line 199 sets these to pointer-events: none. */
#contact .contact-mid-container,
#contact .layer-fg{
  pointer-events: auto;
}
#contact .layer-fg{
  filter: blur(1.25px);
  top: auto;
  right: -3%;
  bottom: -4%;
  left: auto;
  width: auto;
  height: 38%;
  object-fit: contain;
  object-position: 100% 100%;
  /* Feather the top edge so it blends into the BG instead of showing
     a hard horizontal seam where the form-panel plate ends. */
  -webkit-mask-image: linear-gradient(to bottom, transparent 0%, rgba(0,0,0,0.6) 10%, black 24%);
  mask-image: linear-gradient(to bottom, transparent 0%, rgba(0,0,0,0.6) 10%, black 24%);
}


/* ============================================================
   CONTACT layers — pin FG/MG to frame-right/bottom
   All breakpoints use height-based sizing; desktop uses larger
   percentages, tablet and mobile scale down from there.
   ============================================================ */
@media (max-width: 1199px){
  #contact .contact-mid-container{
    top: auto;
    left: auto;
    width: auto;
    right: -4%;
    bottom: 4%;
    height: 53%;
  }

  #contact .layer-fg{
    top: auto;
    left: auto;
    width: auto;
    height: 100%;
    object-fit: contain;
    object-position: 100% 100%;
  }

  #contact .layer-fg{
    right: -6%;
    bottom: -2%;
    height: 35%;
    filter: blur(1.25px);
  }
}

/* Mobile — tighter offsets to match original mobile tuning */
@media (max-width: 767px){
  #contact .layer-bg{
    object-position: 35% 50%;
  }
  #contact .contact-mid-container{
    right: -16%;
    bottom: 0;
    height: auto;
    width: 90%;
  }

  #contact .layer-fg{
    right: -20%;
    bottom: -4%;
    height: auto;
    width: 80%;
  }
}

/* Pin the contact scene to the stable (URL-bar-expanded) viewport height.
   The site-wide --scene-vh is 100dvh, which updates every time iOS Safari
   collapses/reveals its URL bar. On hero/projects/etc. that's absorbed by
   the 6% layer overscan, but contact's MG/FG use height-based percentages
   and sit directly above the footer — so the bar-toggle reflow shows up as
   a visible pop. 100svh locks the scene to a stable height; any spare
   space when the bar collapses is absorbed by the footer below. Fixes
   the pop on iPhone portrait AND landscape (where 844w falls into the
   tablet breakpoint and inherits height-based layer sizing). */
#contact.scene{
  min-height: 100svh;
}

/* Mobile */
@media (max-width: 767px){
  .topnav{
    height: clamp(36px, 10vw, 44px);
    padding: 0 clamp(10px, 3vw, 16px);
  }
  .logo img{
    height: clamp(11px, 3.2vw, 16px);
  }
  .topnav nav{
    gap: clamp(8px, 2.4vw, 16px);
  }
  .topnav a{
    font-size: clamp(8px, 2.2vw, 10px);
    letter-spacing: 0.04em;
  }

  /* HERO — iPhone portrait composition */
  .hero-tagline{ font-size: clamp(7px, 2.8vw, 11px); }
  .big-title{
    font-size: clamp(26px, 8vw, 42px);
    letter-spacing: 0.06em;
  }


  .cta-title{
    letter-spacing: 0.08em;
    font-size: 11px;
  }

  #contact .scene-overlay{
    padding-top: clamp(80px, 11vh, 120px);
    padding-left: clamp(12px, 4vw, 18px);
    padding-right: clamp(12px, 4vw, 18px);
  }
  #contact .contact-card{
    margin-left: calc(-1 * clamp(12px, 4vw, 18px) - env(safe-area-inset-left, 0px));
    width: min(420px, 86vw);
    max-width: 86vw;
    padding-left: clamp(20px, 5vw, 26px);
  }
  /* Match the title bar's left bleed to this block's padding-left — base margin
     is tuned to desktop padding and would overshoot the card's left edge here.
     (padding-right is unchanged from base, so margin-right is already correct.) */
  #contact .contact-card-titlebar{
    margin-left: calc(-1 * clamp(20px, 5vw, 26px));
  }

  /* COLOUR — iPhone portrait: scale desk up, center, anchor to bottom */
  .colour-fg-wrap{
    bottom: 0;
    left: 50%;
    right: auto;
    transform: translateX(-50%);
    width: 220%;
  }
  #colour .layer-fg{
    position: relative;
    display: block;
    width: 100%;
    height: auto;
  }

  /* PROJECTS — tall iPhone aspect ratios */
  #projects .poster-grid{
    gap: 8px;
    top: clamp(240px, 32vh, 400px); /* drop posters lower into the sky */
  }
  #projects .poster span{
    font-size: clamp(11px, 2.8vw, 14px);
    letter-spacing: 0.08em;
  }
  #projects .layer-mid.projects-mid-container{
    left: -42%;
    right: auto;
    width: 184%;
    bottom: 0;
  }
  #projects .projects-mid-image{
    object-position: 50% 70%;
  }
  /* screen-wrap stays at default top: 32.8% — GSAP handles reveal via opacity */
  /* Theatre poster transitions owned by GSAP */

  #cinematography .cinema-overlay{
    padding-top: clamp(80px, 14vh, 160px);
    padding-bottom: clamp(48px, 10vh, 80px);
    padding-left: clamp(14px, 4.2vw, 22px);
    padding-right: clamp(14px, 4.2vw, 22px);
    align-items: flex-start;
  }
  .cinema-copy{
    width: min(760px, 92vw);
    min-height: clamp(220px, 52vh, 500px);
  }
  .cinema-cta{
    top: clamp(260px, 38vh, 360px);
  }
  .cinema-block h2{
    margin-bottom: 10px;
    font-size: clamp(16px, 5vw, 24px);
    line-height: 1.1;
    letter-spacing: 0.06em;
  }
  .cinema-block p{
    max-width: none;
    font-size: clamp(11px, 3vw, 14px);
    line-height: 1.4;
    margin-bottom: 8px;
  }
  #cinematography .cinema-block p br{
    display: none;
  }


  /* CINEMATOGRAPHY (PICTURE) — mobile portrait layer tuning.
     Physics: taller crop shifts vertical framing — light drops into frame,
     train gains presence, Alexa pushes closer, chart recedes slightly. */
  #cinematography .cinema-fg-chart-wrap{
    width: clamp(110px, 14vw, 200px);
  }
  #cinematography .cinema-mg-wrap{
    bottom: -10%;
    width: 180%;
    left: -58%;
  }
  #cinematography .cinema-mg-light-wrap{
    right: -10%;
  }
  #cinematography .cinema-fg-wrap{
    width: clamp(220px, 52vw, 460px);
  }

}

@media (prefers-reduced-motion: reduce){
  .about-block,
  .cinema-block,
  .colour-block{
    transition: none;
  }
}


@media (max-width: 767px){
  .cinema-link--fixed{
    bottom: clamp(48px, 8.5vh, 96px);
  }

  #about{
    min-height: 235svh;
  }
  #about .about-fg-layer{
    height: 106%;
    right: auto;
    left: 52%;
  }
  #about .about-overlay{
    top: 0;
    min-height: var(--scene-vh);
    padding-top: clamp(50px, 12vh, 100px);
    padding-left: 0 !important;
    padding-right: clamp(16px, 5vw, 24px);
    align-items: flex-start;
  }
  .about-copy{
    width: min(80vw, 400px);
    min-height: clamp(180px, 32vh, 380px);
    transform: none;
    padding: 0;
    background: transparent;
    backdrop-filter: none;
  }
  .about-block--right{
    left: 48%;
    justify-content: flex-start;
    padding-top: 18vh;
  }
  /* Mobile portrait: positioning is JS-driven (plate-locked to TV via
     panTL onUpdate). Legacy top/left/margin-left/width !important overrides
     removed so gsap.set inline values win. */
  .about-block--center h2{
    font-size: clamp(12px, 3.4vw, 18px);
    margin-bottom: 8px;
  }
  .about-block--center p{
    font-size: clamp(9px, 2.2vw, 11px);
    line-height: 1.35;
    margin-bottom: 5px;
  }
  .about-block h1,
  .about-block h2{
    font-size: clamp(14px, 4vw, 20px);
    line-height: 1.1;
    letter-spacing: 0.06em;
    margin-bottom: 10px;
    max-width: none;
    min-height: 2.08em;
  }
  .about-block p{
    max-width: none;
    font-size: clamp(10px, 2.5vw, 12px);
    line-height: 1.4;
    margin-bottom: 6px;
  }
  .about-block p + p{
    margin-top: 4px;
  }

  #colour{
    min-height: 270svh;
  }
  #colour .colour-overlay{
    padding-top: clamp(82px, 12vh, 120px);
    padding-left: clamp(16px, 5vw, 24px);
    padding-right: clamp(16px, 5vw, 24px);
    align-items: flex-start;
  }
  .colour-copy{
    width: min(88vw, 400px);
    min-height: clamp(290px, 45vh, 420px);
    padding-bottom: clamp(40px, 6vh, 62px);
  }
  .colour-block h2{
    font-size: clamp(18px, 5.8vw, 26px);
    line-height: 1.04;
    letter-spacing: 0.015em;
    margin-bottom: 14px;
    max-width: none;
  }
  .colour-block p{
    max-width: none;
    font-size: clamp(11px, 3vw, 14px);
    line-height: 1.4;
    margin-bottom: 8px;
  }
  .colour-link{
    font-size: clamp(14px, 4vw, 17px);
    padding: 12px 16px;
  }
  .colour-link--fixed{
    left: 0;
    bottom: 0;
  }
  #about .about-overlay,
  #cinematography .cinema-overlay,
  #colour .colour-overlay{
    padding-left: clamp(16px, 5vw, 24px);
  }
}

/* PROJECTS — iPhone landscape composition pass (incl. 16 Pro Max class) */
@media (orientation: landscape) and (max-height: 520px) and (hover: none) and (pointer: coarse){
  #about .about-overlay{
    padding-bottom: clamp(40px, 8vh, 56px);
    align-items: flex-start;
  }
  #about .about-overlay,
  #cinematography .cinema-overlay,
  #colour .colour-overlay{
    padding-left: calc(clamp(14px, 4vw, 22px) + env(safe-area-inset-left, 0px));
    padding-right: calc(clamp(10px, 2.8vw, 16px) + env(safe-area-inset-right, 0px));
  }
  .about-copy{
    width: min(calc(100vw - var(--about-fg-w) - var(--scene-copy-left) - 40px), 480px);
    transform: none;
    padding: 0;
  }
  .about-block h1,
  .about-block h2{
    font-size: clamp(12px, 1.8vw, 18px);
    line-height: 1.1;
    margin-bottom: 6px;
  }
  .about-block p{
    max-width: none;
    font-size: clamp(9px, 1.1vw, 12px);
    line-height: 1.3;
    margin-bottom: 4px;
  }
  #cinematography .cinema-overlay{
    padding-top: clamp(52px, 10vh, 80px);
    padding-bottom: clamp(8px, 2vh, 16px);
    align-items: flex-start;
  }
  .cinema-copy{
    width: min(480px, 52vw);
    min-height: clamp(220px, 58vh, 340px);
    transform: translateY(clamp(4px, 1vh, 10px));
    padding-bottom: clamp(12px, 2vh, 22px);
  }
  .cinema-block h2{
    font-size: clamp(12px, 1.8vw, 18px);
    line-height: 1.1;
    margin-bottom: 6px;
  }
  .cinema-block p{
    max-width: none;
    font-size: clamp(9px, 1.1vw, 12px);
    line-height: 1.3;
    margin-bottom: 4px;
  }
  #cinematography .layer-bg{
    bottom: -24%;
  }
  #cinematography .cinema-mg-wrap{
    bottom: -27%;
  }
  #cinematography .cinema-fg-wrap{
    bottom: -10%;
    width: clamp(220px, 36vw, 460px);
  }
  #cinematography .cinema-fg-chart-wrap{
    bottom: -11%;
  }
  #cinematography .layer-mid{
    bottom: -15px;
  }
  .cinema-link--fixed{
    bottom: -26px;
  }
  .cinema-cta{
    top: clamp(108px, 29vh, 155px);
  }
  .colour-block h2{
    font-size: clamp(16px, 2.8vw, 24px);
    line-height: 1.02;
    margin-bottom: 8px;
  }
  .colour-block p{
    max-width: none;
    font-size: clamp(11px, 1.4vw, 15px);
    line-height: 1.3;
    margin-bottom: 6px;
  }
  /* About BG: anchor plate top to frame top so more sky / mountain peaks
     read at top of frame on iPhone landscape with chrome. Default 50% 50%
     centers the plate; this shifts the visible window upward toward the
     plate's top so the horizon drops in the visible frame. */
  #about .layer-bg{
    object-position: 50% 0%;
  }
  /* About FG: shift the redwood + studio cutout left and down for the
     iPhone landscape composition. Base rule's `left: max(45vw,
     calc(100vw - 71vh))` parks the plate right; widening the formula's
     vh constant (71→78) and dropping the floor (45→32vw) pulls it
     leftward. `top: 6%` (vs base -3%) shifts visible plate content
     downward by ~30px on the 322px visible viewport. */
  /* About intro block: push the "SMAK is a place to make films" copy
     down to the lower part of the frame on iPhone landscape. The block
     is position: absolute; inset: 0 — making it a flex column with
     flex-end aligns h2 + body to the bottom of the available space. */
  .about-block--intro{
    display: flex;
    flex-direction: column;
    justify-content: flex-end;
    padding-bottom: clamp(16px, 5vh, 36px);
  }

  /* iPhone landscape (chrome-hidden) — locked baseline for these scenes.
     Chrome-shown state gets tighter overrides via .chrome-shown selectors
     below — the .chrome-shown class is JS-toggled on <html> when
     visualViewport.height shrinks (URL bar + favourites visible). */
  /* Restore the overlay padding-top that the iPhone landscape rule at
     line ~1110 attempts but loses to the base rule (line ~2002) due to
     source-order cascade. Placed here so it's after the base. */
  #colour .colour-overlay{
    align-items: flex-start;
    padding-top: clamp(48px, 13vh, 64px);
    padding-bottom: clamp(120px, 38vh, 150px);
  }
  .colour-fg-wrap{
    left: 50%;
    right: auto;
    transform: translateX(-50%);
    bottom: -12%;
    width: 80%;
  }
  #contact .scene-overlay{
    padding-top: clamp(94px, 16vh, 128px);
    padding-bottom: clamp(44px, 9vh, 66px);
    padding-left: calc(clamp(10px, 2.8vw, 16px) + env(safe-area-inset-left, 0px));
    padding-right: calc(clamp(10px, 2.8vw, 16px) + env(safe-area-inset-right, 0px));
  }
  .contact-card{
    margin-top: 8px;
    margin-left: calc(-1 * clamp(10px, 2.8vw, 16px) - env(safe-area-inset-left, 0px));
    width: min(440px, 58vw);
    max-width: none;
    /* padding-top MUST stay 0 — the title bar owns the top edge and bleeds to
       it (like desktop). A non-zero top padding pushes the bar down off the
       card's top edge ("disconnected from the top"). */
    padding: 0 14px 12px clamp(18px, 3.6vw, 22px);
    border-radius: 0;
  }
  /* Re-cancel the title bar's bleed against THIS block's card padding. The
     base titlebar margins (-clamp(28px,4vw,44px) / -clamp(20px,2.2vw,28px))
     are tuned to the base padding; landscape shrinks the padding to
     clamp(18px,3.6vw,22px) / 14px, so without this the bar overshoots the
     card's left edge (visible once minimized leaves only the bar). */
  .contact-card-titlebar{
    margin-left: calc(-1 * clamp(18px, 3.6vw, 22px));
    margin-right: -14px;
  }
  .contact-form{
    gap: 8px;
  }
  .form-row{
    gap: 4px;
  }
  .contact-form input,
  .contact-form textarea{
    padding: 9px 10px;
    font-size: 13px;
    border-radius: 0;
  }
  .contact-form textarea{
    min-height: clamp(64px, 18vh, 90px);
  }
  .form-actions{
    gap: 8px;
  }
  .btn{
    padding: 9px 14px;
    border-radius: 0;
    font-size: 11px;
  }

  /* iPhone landscape WITH Safari chrome shown — tighter values that fit
     in the ~322px visible viewport. Scoped under .chrome-shown so the
     locked iPhone-landscape (chrome-hidden) state is unaffected. */
  .chrome-shown #cinematography .cinema-fg-wrap{
    width: clamp(180px, 32vw, 400px);
  }
  .chrome-shown #cinematography .cinema-mg-wrap{
    bottom: -35%;
    width: 80%;
    left: 12%;
    right: auto;
  }
  .chrome-shown #cinematography .cinema-mg-light-wrap{
    width: clamp(220px, 28vw, 520px);
    right: -8%;
  }
  .chrome-shown #cinematography .layer-bg{
    bottom: -38%;
  }
  .chrome-shown .colour-fg-wrap{
    bottom: -10%;
    width: 55%;
  }
  .chrome-shown #colour .colour-overlay{
    align-items: flex-start;
    padding-top: clamp(40px, 14vh, 56px);
    padding-bottom: clamp(140px, 44vh, 160px);
  }
  .chrome-shown #contact .scene-overlay{
    padding-top: clamp(48px, 14vh, 64px);
    padding-bottom: clamp(40px, 8vh, 56px);
  }
  .chrome-shown .contact-card{
    margin-top: 0;
    /* padding-top 0 — title bar owns the top edge (see note above). */
    padding: 0 14px clamp(10px, 1.4vh, 14px) clamp(18px, 3.6vw, 22px);
  }
  .chrome-shown .contact-copy{
    margin-bottom: clamp(8px, 1.5vh, 14px);
    width: 100%;
  }
  .chrome-shown .contact-block h2{
    font-size: clamp(13px, 2.4vh, 17px);
    margin-bottom: 6px;
    letter-spacing: 0.06em;
  }
  .chrome-shown .contact-block p{
    font-size: clamp(11px, 1.5vh, 12px);
    line-height: 1.35;
    margin: 0 0 4px;
  }
  .chrome-shown .contact-form{
    gap: 6px;
  }
  .chrome-shown .contact-form input,
  .chrome-shown .contact-form textarea{
    padding: 7px 10px;
  }
  .chrome-shown .contact-form textarea{
    min-height: 48px;
  }
  .chrome-shown .form-actions{
    margin-top: 2px;
  }
  /* Constellation footer mobile rules moved to @media (max-width: 767px)
     near desktop footer block — applies to portrait + landscape mobile. */

  #projects .poster-grid{
    grid-template-columns: repeat(3, clamp(72px, 9.4vw, 96px));
    justify-content: center;
    gap: clamp(18px, 2.1vw, 28px);
  }
  #projects .poster{
    width: 100%;
  }
  #projects .poster span{
    margin-top: 6px;
    font-size: clamp(10px, 1.5vw, 12px);
    letter-spacing: 0.07em;
  }

  #projects .layer-mid.projects-mid-container{
    left: 16%;
    right: auto;
    width: 68%;
    bottom: calc(7vh + 45px);
  }
  /* Theatre MG sizing owned by GSAP scale */
  #projects .projects-mid-image{
    object-position: 50% 82%;
  }
  /* screen-wrap + theatre poster transitions owned by GSAP */
}

/* iPad portrait final pass — ensure these win over generic >=768 rules */
@media (min-width: 768px) and (max-width: 1199px) and (orientation: portrait){
  /* 2026-05-23: dropped the (hover:none)+(pointer:coarse) touch gate — it was
     gratuitous (the block only sizes the FG wrap, nothing touch-specific) and
     it made this scene impossible to preview on desktop/Playwright while the
     un-gated projects block was trivial to tune. Now matches the projects
     block pattern: applies on any 768–1199 portrait viewport. */
  /* 2026-05-17 fix: previously used height: 50% on wrap with object-fit: cover
     on layer-fg + object-position: center 30%. That cover-crop scaled the
     plate horizontally larger than the wrap, breaking the portal/CTA
     percentage alignment with the painted HX310 screen — visible as black
     bezel around the contact preview on iPad portrait only.
     New approach: lock wrap to the plate's natural aspect ratio (no crop)
     and use a 160% width to keep the desk visually prominent. Plate
     overflows the viewport on left/right equally (centered via translateX),
     but the painted screen sits roughly centered and portal/CTA percentages
     stay perfectly glued because the wrap is now the plate. */
  .colour-fg-wrap{
    /* bottom -3% (was 0): the FG parallax lifts the wrap at scene-entry,
       exposing BG under its bottom edge until the scroll settles it; dropping
       the rest position keeps the bottom covered through the parallax-up
       extreme. */
    bottom: -3%;
    left: 50%;
    right: auto;
    transform: translateX(-50%);
    /* 160% (2026-05-23): dialed in live with Scott on a de-gated preview.
       Width-only sizing keeps height:auto natural aspect, so the HX310 portal
       + LAUNCH overlay stay glued (% of the wrap; wrap aspect = plate aspect). */
    width: 160%;
    height: auto;
  }
  #colour .layer-fg{
    position: relative;
    display: block;
    width: 100%;
    height: auto;
    inset: auto;
  }
  #contact .layer-fg{
    height: 22%;
  }
  #contact .contact-mid-container{
    bottom: 0;
  }
}

/* iPad mini A17 (744px portrait, sub-768): gets the phone Colour FG (220%, too
   big). Bring it down toward the tablet treatment, mirroring the tablet's
   natural-aspect width-only sizing so the HX310 portal/CTA stay glued. Started
   at 185% (above tablet's 160% — the narrower mini frame reads small at 160%,
   same lesson as the seats). Scoped 740-767; contact untouched. */
@media (orientation: portrait) and (min-width: 740px) and (max-width: 767px){
  #colour .colour-fg-wrap{
    width: 158%;
    height: auto;
    bottom: -3%;
    left: 50%;
    right: auto;
    transform: translateX(-50%);
  }
  #colour .layer-fg{
    position: relative;
    display: block;
    width: 100%;
    height: auto;
    inset: auto;
  }
}

/* iOS auto-zooms the page when a focused form field has font-size < 16px, and
   with this site's custom (Lenis) scroll + pinned-pan layout the zoom gets
   stuck — the user has to pinch back out to recover (and while zoomed, the nav
   reflows to its cropped/overflow look). The contact fields are 14px desktop /
   13px landscape-phone, both under the threshold. Forcing 16px on touch devices
   removes the zoom trigger entirely without disabling pinch-zoom (kept for
   accessibility / award a11y). Desktop (fine pointer) keeps the 14px design
   size — it never auto-zooms. Last in source so it wins over the smaller
   per-breakpoint values; same specificity, later position. */
@media (pointer: coarse) {
  .contact-form input,
  .contact-form textarea { font-size: 16px; }
}

/* ─── Low-power DOF gate ─────────────────────────────────────────────────────
   When the runtime frame-rate detector flags a weak device (body.is-low-power),
   neutralise the GPU-expensive DOF blur that racks during scene transitions —
   the between-scenes stutter on old hardware. GSAP keeps writing its inline
   filter values every scrub frame (negligible CPU), but the !important rule
   wins so the blur never actually renders. The transition — scoped to
   .is-low-power so normal scrubbing on capable devices is never affected — eases
   the blur out once over 0.4s instead of snapping abruptly. The gate is
   one-way/sticky, so this fade happens at most once per session.
   Note: CSS can't strip only the blur from a combined blur()+brightness() inline
   filter, so these layers also drop their brightness ramp on low-power; the
   seats keep brightness via the dedicated --seats-blur custom property. */
body.is-low-power .layer-bg,
body.is-low-power .layer-mid,
body.is-low-power .layer-fg,
body.is-low-power .cinema-mg-wrap,
body.is-low-power .projects-mg-shell,
body.is-low-power .projects-fg-seats,
body.is-low-power .hero-projects-fg-bridge,
body.is-low-power .projects-about-fg-bridge {
  transition: filter 0.4s ease;
  filter: none !important;
}
body.is-low-power .projects-seats-close {
  transition: filter 0.4s ease;
  --seats-blur: 0px !important;  /* keep the brightness ramp via --seats-brightness */
}

/* Low-power: drop the grain overlay entirely. Once frozen it costs ~0 to paint,
   but it's a full-screen position:fixed alpha layer the GPU still composites over
   every scroll frame — display:none removes it from compositing (a fill-rate win
   on an old GPU). opacity:0 would NOT help (the layer still composites). Last
   micro-lever of the low-power pass; the grain is subtle enough that its removal
   doesn't read as a pop. */
body.is-low-power .hero-grain { display: none !important; }

/* Low-power: also drop the resting soft-focus blurs the DOF-rack gate above
   doesn't cover — these are live CSS filters (not baked assets) and real
   compositing cost: the cinematography starfield (blur 3px), the mg-light glow
   wrap (1.2px), and the colourgrading FG wrap (3.5px, which blurs the FG image
   AND the screen-CTA/logo nested in it). With the DOF off everywhere else these
   read as accidentally out-of-focus; sharpening them keeps the low-power look
   consistent and removes the blurred-starfield / blurred-FG-subtree fill cost. */
body.is-low-power .cinema-stars,
body.is-low-power .cinema-mg-light-wrap,
body.is-low-power .colour-fg-wrap {
  transition: filter 0.4s ease;
  filter: none !important;
}

/* Low-power: remove the cinematography light-beam glow entirely. It's a radial
   gradient on a huge element (260% × 1400%) carrying filter:blur(36px) AND
   mix-blend-mode:screen — all three brutal on an old GPU. We can't keep the blur
   (samples that whole area every frame), and the gradient unblurred reads as a
   hard-edged smear rather than a soft glow — worse than nothing. So display:none
   the beam: drops the blur, the gradient fill, and the screen-blend composite in
   one go (the biggest single fill-rate win of the pass). The physical light
   fixture image (.layer-mid-light) stays; only the glow goes. */
body.is-low-power #cinematography .cinema-mg-light-wrap::after {
  display: none !important;
}
