/* ============================================================
   1. Variables
   ============================================================ */
:root {
  /* Brand palette (source colors) */
  --color-orange: #FF6D29;
  --color-brown: #453027;
  --color-black: #161316;
  --color-grey: #BABABA;
  --color-white: #FFFFFF;

  /* Role tokens */
  --background: var(--color-white);
  --surface: #F4F2F0;
  --surface-strong: #E7E3DF;
  --text: var(--color-black);
  --muted-text: #767077;
  --accent: var(--color-orange);
  --accent-dark: var(--color-brown);
  --line: rgba(22, 19, 22, 0.12);

  /* Scene surfaces (used behind character/browser scenes) */
  --scene-bg: var(--color-black);
  --scene-bg-alt: var(--color-brown);
  --scene-text: var(--color-white);

  /* Type */
  --font-display: "Fraunces", "Iowan Old Style", Georgia, serif;
  --font-body: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;

  /* Motion */
  --ease-standard: cubic-bezier(0.22, 1, 0.36, 1);
  --ease-soft: cubic-bezier(0.16, 1, 0.3, 1);
  --duration-fast: 300ms;
  --duration-medium: 700ms;
  --duration-slow: 1200ms;
  /* A real damped-spring curve (stiffness 120 / damping 14 / mass 1),
     sampled into a native linear() easing — gentle single overshoot,
     settles clean. No animation library involved, just WAAPI/CSS's
     own linear() easing function fed pre-computed spring samples. */
  --ease-spring: linear(0.0000, 0.1029, 0.3393, 0.6151, 0.8630, 1.0459, 1.1528, 1.1912, 1.1790, 1.1370, 1.0843, 1.0349, 0.9970, 0.9736, 0.9638, 0.9646, 0.9718, 0.9818, 0.9916, 0.9994, 1.0044, 1.0068, 1.0070, 1.0058, 1.0039);

  /* Layout */
  --container-max: 1200px;
  --gutter: clamp(1.25rem, 4vw, 3.5rem);

  /* Scroll progress (set per-act by JS) */
  --progress: 0;
}

/* ============================================================
   2. Reset
   ============================================================ */
*,
*::before,
*::after {
  box-sizing: border-box;
}

html,
body {
  height: 100%;
}

body {
  margin: 0;
  background: var(--background);
  color: var(--text);
  font-family: var(--font-body);
  -webkit-font-smoothing: antialiased;
  overflow-x: hidden;
}

img,
svg {
  display: block;
  max-width: 100%;
}

a {
  color: inherit;
}

button {
  font: inherit;
  color: inherit;
  background: none;
  border: none;
  cursor: pointer;
}

ul {
  margin: 0;
  padding: 0;
  list-style: none;
}

h1,
h2,
h3,
p {
  margin: 0;
}

/* ============================================================
   3. Base typography
   ============================================================ */
body {
  font-size: 1rem;
  line-height: 1.6;
}

h1,
h2,
h3 {
  font-family: var(--font-display);
  font-weight: 600;
  letter-spacing: -0.01em;
  line-height: 1.08;
}

.eyebrow {
  font-family: var(--font-body);
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--muted-text);
}

.act-copy .eyebrow {
  color: var(--color-grey);
}

.display-1 {
  font-size: clamp(2.75rem, 7vw, 6rem);
}

.display-2 {
  font-size: clamp(2.1rem, 5vw, 4rem);
}

.body-lead {
  font-size: clamp(1.05rem, 1.6vw, 1.35rem);
  color: var(--muted-text);
  font-weight: 400;
}

.text-on-dark {
  color: var(--scene-text);
}

.text-on-dark .body-lead {
  color: rgba(255, 255, 255, 0.72);
}

/* Rationed accent — the one emphasised line per headline */
.accent-line {
  color: var(--accent);
  font-style: italic;
}

.was-line {
  font-size: 0.82rem;
  color: var(--muted-text);
}

.text-on-dark .was-line {
  color: rgba(255, 255, 255, 0.55);
}

/* Sequential per-word color wipe — white to black, one word at a
   time in reading order — driven by --word-progress (set per-span
   by applyWordScrub() in script.js from the act's own progress). */
.word-scrub-word {
  color: color-mix(in srgb, black calc(var(--word-progress, 0) * 100%), white calc((1 - var(--word-progress, 0)) * 100%));
}

.gain-tag {
  display: inline-flex;
  align-items: center;
  font-size: 0.8rem;
  letter-spacing: 0.02em;
  color: var(--accent);
}

.service-strip {
  display: grid;
  gap: 0.5rem;
  padding-top: 0.3rem;
  font-size: 0.9rem;
  color: var(--muted-text);
}

.text-on-dark .service-strip {
  color: rgba(255, 255, 255, 0.68);
}

.service-strip strong {
  color: inherit;
  font-weight: 600;
}

/* ============================================================
   4. Layout utilities
   ============================================================ */
.container {
  max-width: var(--container-max);
  margin-inline: auto;
  padding-inline: var(--gutter);
}

.skip-link {
  position: absolute;
  left: -999px;
  top: 0;
  background: var(--accent);
  color: var(--color-white);
  padding: 0.75em 1.25em;
  z-index: 1000;
  border-radius: 0 0 6px 0;
}

.skip-link:focus {
  left: 0;
}

/* ============================================================
   5. Header
   ============================================================ */
.site-header {
  position: fixed;
  inset: 0 0 auto 0;
  z-index: 100;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 1.25rem var(--gutter);
  mix-blend-mode: difference;
  transform: translateY(0);
  transition: transform var(--duration-medium) var(--ease-standard);
}

/* Auto-hide on scroll down, reappear on scroll up — toggled in
   script.js. Reduced-motion keeps it always visible (no transform),
   consistent with motion never gating access to navigation. */
.site-header--hidden {
  transform: translateY(-100%);
}

.brand {
  display: flex;
  flex-direction: column;
  line-height: 1;
  font-family: var(--font-display);
  font-size: 1.05rem;
  font-weight: 600;
  letter-spacing: 0.02em;
  text-decoration: none;
  color: var(--color-white);
}

.brand-line2 {
  color: var(--accent);
}

.site-nav {
  display: flex;
  gap: 2rem;
}

.site-nav a {
  font-size: 0.85rem;
  font-weight: 500;
  text-decoration: none;
  color: var(--color-white);
  position: relative;
}

.site-nav a::after {
  content: "";
  position: absolute;
  left: 0;
  bottom: -4px;
  width: 100%;
  height: 1px;
  background: currentColor;
  transform: scaleX(0);
  transform-origin: left;
  transition: transform var(--duration-fast) var(--ease-standard);
}

.site-nav a:hover::after,
.site-nav a:focus-visible::after {
  transform: scaleX(1);
}

.header-cta {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.6rem 1.1rem;
  border: 1px solid var(--color-white);
  border-radius: 999px;
  font-size: 0.82rem;
  font-weight: 600;
  text-decoration: none;
  color: var(--color-white);
  transition: background var(--duration-fast) var(--ease-standard), color var(--duration-fast) var(--ease-standard);
}

.header-cta:hover {
  background: var(--color-white);
  color: var(--color-black);
}

@media (max-width: 720px) {
  .site-nav {
    display: none;
  }
}

/* ============================================================
   6. Story acts (sticky sections)
   ============================================================ */
main {
  position: relative;
}

.act {
  position: relative;
  min-height: 100vh;
}

/* Each act reserves scroll distance; the inner sticky pane pins while progress advances */
.act--scroll {
  height: 320vh;
}

/* Act 0's camera push-in gets more scroll distance to move through,
   so the same keyframes play out more gradually. Camera path, layer
   positions, and depth values are unchanged — only the pacing. */
.act[data-act="0"].act--scroll {
  height: 420vh;
}

.act-sticky {
  position: sticky;
  top: 0;
  height: 100vh;
  display: grid;
  grid-template-columns: 1fr;
  align-items: center;
  overflow: hidden;
}

.act-scene {
  position: absolute;
  inset: 0;
  z-index: 0;
  display: flex;
  align-items: center;
  justify-content: flex-end;
  padding: 7rem var(--gutter) 2rem clamp(2rem, 18vw, 30rem);
  pointer-events: none;
  overflow: hidden;
}

/* The edge-fade lives on the scene's decorative content, never on
   .act-scene itself — that element also carries the theme
   background-color that gives text-on-dark copy its contrast, and
   must stay solid through act-to-act transitions. Excludes
   .scene-image — the illustration itself should stay fully visible
   the whole time, not dim/fade as the visitor scrolls past a
   section — and .panel-orbit (Act 3's orbiting panels), which have
   their own scrubbed position/opacity already frozen in place by
   act3PanelOrbit once its own ScrollTrigger ends; without this
   exclusion this generic fade was still quietly taking them to
   opacity 0 near the end of Act 3's scroll regardless. Only the
   smaller decorative extras (scrims, captions, credit badges, etc.)
   still cross-fade at act boundaries. */
.act-scene>*:not(.scene-image):not(.panel-orbit) {
  opacity: var(--scene-edge, 1);
  transition: opacity var(--duration-medium) linear;
}

/* Full-bleed variant: the scene occupies the entire viewport as
   layered, camera-driven depth planes instead of a single block
   reserved to the right of the copy column. */
.act-scene--immersive {
  padding: 0;
  justify-content: center;
  align-items: stretch;
}

.scene-layer {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  transform:
    translate3d(calc(var(--cam-x, 0) * var(--depth, 1) * 1px), calc(var(--cam-y, 0) * var(--depth, 1) * 1px), 0) scale(calc(1 + (var(--cam-scale, 1) - 1) * var(--depth, 1)));
}

.scene-scrim {
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  background: linear-gradient(100deg, rgba(0, 0, 0, 0.6) 0%, rgba(0, 0, 0, 0.22) 40%, transparent 62%);
}

.act[data-theme="light"] .scene-scrim {
  background: linear-gradient(100deg, rgba(255, 255, 255, 0.75) 0%, rgba(255, 255, 255, 0.3) 40%, transparent 62%);
}

/* Hero layer stack (Act 0): a fixed-aspect stage holding the
   separated character/scene PNGs, each parallaxed by its own
   --depth against the shared camera. The stage keeps the source
   composition's own proportions so the layers stay registered to
   each other regardless of viewport size; the viewport clips and
   centers the stage without distorting it. */
.hero-viewport {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
}

.hero-stage {
  position: relative;
  width: min(100%, 150vh);
  aspect-ratio: 3 / 2;
  flex-shrink: 0;
}

.hero-layer {
  position: absolute;
  display: block;
  pointer-events: none;
  transform:
    translate3d(calc(var(--cam-x, 0) * var(--depth, 1) * 1px), calc(var(--cam-y, 0) * var(--depth, 1) * 1px), 0)
    translate3d(calc(var(--layer-x, 0) * 1px), calc(var(--layer-y, 0) * 1px), 0)
    scale(calc(1 + (var(--cam-scale, 1) - 1) * var(--depth, 1)));
}

.hero-layer--bg {
  top: 0;
  left: 0;
  width: 100%;
  height: auto;
}

/* Full-bleed backdrop, sized to .hero-viewport (which is inset:0 on
   the sticky pane, i.e. the actual viewport) rather than the smaller
   registered-composition box .hero-stage uses — cover-fit so there is
   never an exposed gap at the edges regardless of viewport ratio. */
.hero-layer--midground-full {
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.hero-layer--arvind {
  top: 1%;
  left: 30%;
  width: 38%;
  height: auto;
}

.hero-layer--samyu {
  top: 20%;
  left: 66%;
  width: 34%;
}

.hero-layer--laptop {
  top: 25%;
  left: 47%;
  width: 18%;
  height: auto;
}

.hero-layer--foreground {
  left: 5%;
  bottom: -8%;
  width: 92%;
  height: auto;
}

.hero-layer--foreground-blur {
  top: -4%;
  left: -6%;
  width: 46%;
  height: auto;
}

.act-copy {
  position: relative;
  z-index: 2;
  padding: 0 var(--gutter);
  max-width: 46rem;
  display: flex;
  flex-direction: column;
  gap: 1.1rem;
}

/* Beats variant: text is split into crossfading zones driven by
   act progress rather than a single one-shot reveal. */
.act-copy--beats {
  display: block;
  min-height: 46vh;
}

.beat {
  position: absolute;
  inset: 0;
  padding: 0 var(--gutter);
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 1.1rem;
  opacity: var(--beat-visible, 0);
  transform: translateY(calc((1 - var(--beat-visible, 0)) * 22px));
  pointer-events: none;
}

.act[data-theme="dark"] .act-scene {
  background: var(--scene-bg);
}

/* Act 0's hero backdrop is warm-toned to match the architecture,
   rather than the flat black used behind other acts' simpler scenes. */
.act[data-act="0"] .act-scene {
  background: #2a1c12;
}

/* Act 0's scene is a full photographic composition, not empty
   space — the text column narrows so it doesn't run into Arvind,
   and the display size steps down so that narrower column doesn't
   wrap into an overflowing stack of lines. */
.act[data-act="0"] .act-copy {
  max-width: 30rem;
}

/* Act 1's illustration is now wide enough to reach into the old
   736px text column — narrow it the same way Act 0's was, so the
   two don't overlap. */
.act[data-act="1"] .act-copy {
  max-width: 25rem;
}

/* Push the illustration flush to the right edge of the viewport
   instead of leaving the usual gutter margin, and ground it to
   the bottom of the scene instead of vertically centering it —
   the image is shorter than the full sticky viewport, so
   centering left an empty band below it. */
.act[data-act="1"] .act-scene {
  padding-right: 0;
  /* Must clear act-copy's own right edge (25rem max-width + its
     gutter) — the default clamp() here undershoots that at most
     viewport widths, which is what let the image's left edge
     land under the text once it was sized to fill the box. */
  padding-left: 32rem;
  align-items: flex-end;
}

/* Sized by width with auto height, the image couldn't reach the
   bottom of the section on taller viewports — align-items alone
   just moved the leftover gap around, it didn't remove it.
   Filling the available box on both axes and letting object-fit
   crop the image (rather than letting its natural aspect ratio
   dictate an oversized width) reaches top and bottom without
   spilling past the padding into the text column. */
.act[data-act="1"] .scene-image {
  width: 100%;
  height: 100%;
  max-width: none;
  object-fit: cover;
  object-position: right center;
}

/* Report Zero <-> Digital Foundation: one continuous camera. Replaces
   the base .scene-image transform (script.js still writes --cam-x/-y/
   -scale for these two acts like every other act, but nothing here
   reads them any more) with --pair-cam-x/-y/-scale, a single damped
   value spanning both acts (see script.js) with no reset at the
   boundary and no per-act entrance-settle term.

   --act1-exit-scale/-y are folded into the same transform (rather
   than composed as a second, separate transform on some wrapper
   element) so the Act 1 -> Act 2 exit tween in gsap-effects.js has
   somewhere to put its own scale/drift without writing to `transform`
   itself — see the comment on that tween for why that split matters.
   They default to 1/0 (a no-op) and are only ever set for Act 1's own
   exit, so Act 2's image is completely unaffected by them.

   transition: none, for BOTH transform and filter, on both acts —
   dropping the base rule's transitions entirely rather than keeping
   filter's. Both properties here are already driven by a continuously
   updating source every frame (script.js's damped pair-camera; and,
   during the Act 1 exit, gsap-effects.js's scrub-driven filter tween)
   — confirmed while testing this: with the base rule's 700ms filter
   transition still in place, the rendered blur visibly lagged behind
   GSAP's own scrub-driven target (chasing a constantly-moving value
   with a 700ms transition produces a persistent, growing lag, not a
   one-time ease). A transition only ever helps when the underlying
   value changes in discrete jumps; nothing here does. */
.act[data-act="1"] .scene-image,
.act[data-act="2"] .scene-image {
  transform:
    translate3d(calc(var(--pair-cam-x, 0) * 1px), calc(var(--pair-cam-y, 0) * 1px + var(--act1-exit-y, 0) * 1vh), 0)
    scale(calc(var(--pair-cam-scale, 1) * var(--act1-exit-scale, 1)));
  transition: none;
  /* Same reasoning as --act1-exit-scale/-y above, and for the same
     underlying issue: gsap-effects.js's exit tween needed to add its
     own blur without taking over `filter` outright. It first animated
     filter directly, which (confirmed while testing) permanently
     pins this element's filter to GSAP's "from" value — blur(0px), no
     brightness term — for this element's ENTIRE lifetime, not just
     during the tween's own active window: a scrubbed fromTo renders
     its "from" state at rest for any scroll position before its
     trigger starts too. That silently disabled the existing
     --audit-scan-dim dim/restore pulse (base rule, reproduced here)
     for the whole time the audit sequence is actually visible.
     --act1-exit-blur is 0 outside the exit (a no-op, so the formula
     below is identical to the base rule for the entire audit
     sequence, and for Act 2 always) and only climbs during the exit
     itself, adding on top of rather than replacing the existing
     entrance/scan-dim terms. */
  filter:
    blur(calc((1 - var(--blur-edge, 1)) * 8px + var(--act1-exit-blur, 0) * 1px))
    brightness(calc(1 - var(--audit-scan-dim, 0) * 0.05));
}

/* Previously hard-pinned to opacity:1 "always, including through the
   Act 1 -> Act 2 transition". That is now exactly what the Act 1 ->
   Act 2 transition block in gsap-effects.js (exitTl) animates away
   from, via inline opacity — inline styles win over this rule, so
   the override below is only the resting/fallback value: before
   JS runs, under reduced motion (gsap-effects.js returns early and
   never touches this element), or for any part of Act 1's scroll
   range outside exitTl's own start/end, the illustration is fully
   visible exactly as before. */
.act[data-act="1"] .act-scene > .scene-image {
  opacity: 1;
}

/* Soft seam instead of a hard cut where Report Zero hands off to
   Digital Foundation's dark theme (light -> dark; see the matching
   overlay at .act[data-act="2"] .act-scene::after). A pre-echo of
   the dark tone in Act 1's own bottom edge, not a redesign of Act 1
   — it stays fully invisible (opacity 0, --act1-exit's resting
   value) for effectively all of Act 1's own reading time, and only
   ramps in via --act1-exit, the same property gsap-effects.js's
   exitTl already drives across the Act 1 -> Act 2 transition (see
   PHASE 4 there) — previously set but read by nothing, now finally
   wired up. Confined to the bottom 14vh, well below every audit
   marker/panel position, and semi-transparent, so even the moment
   it does appear it doesn't compete with anything still legible. */
.act[data-act="1"] .act-scene::after {
  content: "";
  position: absolute;
  inset: auto 0 0 0;
  height: 14vh;
  background: linear-gradient(to bottom, transparent, rgba(22, 19, 22, 0.55));
  opacity: var(--act1-exit, 0);
  pointer-events: none;
}

/* Report-cover open (Act 1 only) — see the act1ReportCover block in
   gsap-effects.js. Sits above both .act-scene and .act-copy (z-index,
   not DOM order) while the whole .act-sticky is small; the scale/
   radius on .act-sticky itself and this cover's own fade are both
   GSAP-driven inline styles, so there's no default here beyond
   layout/positioning — without JS (or under reduced motion, where
   this whole file returns early) the cover just sits at opacity 1
   forever, permanently hiding the real content behind it, which is
   exactly why the reduced-motion rule below removes it outright. */
.act1-report-cover {
  position: absolute;
  inset: 0;
  z-index: 5;
  pointer-events: none;
  background: var(--background);
  border: 1.5px dashed rgba(22, 19, 22, 0.3);
}

.act1-report-cover-label {
  position: absolute;
  top: 1.5rem;
  margin: 0;
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--muted-text);
}

.act1-report-cover-label--left {
  left: 1.5rem;
}

.act1-report-cover-label--right {
  right: 1.5rem;
}

.act1-report-cover-title {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  margin: 0;
  font-family: var(--font-display);
  font-size: clamp(2.25rem, 6vw, 4.5rem);
  font-weight: 600;
  color: var(--text);
  white-space: nowrap;
}

.act1-report-cover-status {
  position: absolute;
  left: 1.5rem;
  bottom: 1.5rem;
  margin: 0;
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--muted-text);
}

body.reduced-motion .act1-report-cover {
  display: none !important;
}

/* Act 2 — full-bleed treatment: image fills the entire scene, copy
   overlays directly on top (scrim behind it for contrast) instead
   of sitting in a separate reserved column like Acts 1, 3-6. */
.act[data-act="2"] .act-copy {
  max-width: 26rem;
}
.act[data-act="2"] .act-scene {
  padding: 0;
}
.act[data-act="2"] .scene-image {
  width: 100%;
  height: 100%;
  max-width: none;
  object-fit: cover;
  object-position: center;
}

/* Mirrors Act 1's bottom-edge overlay: a soft light pre-echo at Act
   2's own top edge, meeting Act 1's own dark pre-echo just above it,
   so the seam blends rather than cuts. Reuses --blur-edge, already computed
   every frame for every act's own blur-to-sharp entrance (script.js);
   calc(1 - blur-edge) is exactly that same entrance window run in
   reverse — strong right as Act 2's own pin engages, gone within the
   same ~6% of Act 2's progress the blur-to-sharp reveal already uses,
   so no new scroll-mapped property was needed for this half. */
.act[data-act="2"] .act-scene::after {
  content: "";
  position: absolute;
  inset: 0 0 auto 0;
  height: 14vh;
  background: linear-gradient(to top, transparent, rgba(255, 255, 255, 0.30));
  opacity: calc(1 - var(--blur-edge, 1));
  pointer-events: none;
}

.scene-caption {
  position: absolute;
  z-index: 1;
  margin: 0;
  font-family: var(--font-display);
  font-style: italic;
  font-size: 1rem;
  line-height: 1.3;
  color: rgba(255, 255, 255, 0.8);
  text-align: right;
}
.scene-caption--top-right { top: 6rem; right: 2.5rem; }
.scene-caption--bottom-right { bottom: 4rem; right: 2.5rem; }

/* Act 3 — full-bleed treatment, same pattern as Act 2/6: image fills
   the entire scene (scrim behind the copy for contrast) instead of
   sitting in a separate reserved column. Switched from the boxed
   light-theme layout to match a reference composition. */
.act[data-act="3"] .act-copy {
  max-width: 26rem;
}
.act[data-act="3"] .act-scene {
  padding: 0;
}
.act[data-act="3"] .scene-image {
  width: 100%;
  height: 100%;
  max-width: none;
  object-fit: cover;
  /* object-position can't actually shift anything here — at this box/
     image ratio, cover's height match is already exact (zero vertical
     overflow to shift within), only width overflows. To reveal
     headroom above the characters, this overrides the shared
     .scene-image transform (same formula, camera/entrance-settle
     terms untouched) with an added constant downward shift, plus a
     compensating extra zoom so the shift doesn't expose empty edges. */
  transform:
    translate3d(0, calc((1 - var(--progress, 0)) * 20px + 60px), 0)
    translate3d(calc(var(--cam-x, 0) * 1px), calc(var(--cam-y, 0) * 1px), 0)
    scale(calc((0.94 + var(--progress, 0) * 0.06) * var(--cam-scale, 1) * 1.14));
}

/* Bold condensed caps headline, last word in solid accent orange —
   matches the reference look. Replaces the old word-by-word white-to-
   black scrub (that only made sense on the previous light background;
   dropped along with the light theme). */
.act[data-act="3"] .display-2 {
  font-family: "Anton", var(--font-display);
  font-weight: 400;
  text-transform: uppercase;
  letter-spacing: 0.01em;
  line-height: 0.98;
}
.act[data-act="3"] .accent-line {
  font-style: normal;
}

/* Floating panel cutouts — orbit in 3D via act3PanelOrbit in
   gsap-effects.js (desktop only). perspective + preserve-3d here so
   the browser does real depth compositing/scaling; the panels' own
   translateZ (set per frame by the script) is what actually drives
   the near/far feel. Dormant/centered by default so there's no
   layout impact if the effect never runs (mobile, reduced motion,
   before JS). */
.panel-orbit {
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  perspective: 1400px;
  transform-style: preserve-3d;
}
.orbit-panel {
  position: absolute;
  top: 21%;
  left: 50%;
  width: 17rem;
  will-change: transform, filter, opacity;
}

/* Acts 4-6 — same flat-illustration pattern. */
.act[data-act="4"] .act-copy,
.act[data-act="5"] .act-copy {
  max-width: 25rem;
}
/* Act 6 has by far the most content (headline + body + 6-item
   service list + CTA) — 25rem made it wrap tall enough to overflow
   above the viewport (clipped behind the header). Wider column and
   tighter list spacing keep it within one screen's height. */
.act[data-act="6"] .act-copy {
  max-width: 28rem;
  gap: 0.85rem;
}
.act[data-act="6"] .service-strip {
  gap: 0.3rem;
  font-size: 0.82rem;
}

/* Act 6 closing headline — punchy poster typography instead of the
   site's usual serif display font: a heavy condensed sans for "From
   invisible" and a bold brush script for the accent line, matching
   the reference treatment. Scoped to this act only — every other
   act's headline/.accent-line keeps the standard Fraunces + italic
   look untouched. The character-sweep spans (gsap-effects.js) just
   inherit whichever font-family is set here, so the reveal still
   works unchanged. */
.act[data-act="6"] .display-2 {
  font-family: "Anton", var(--font-display);
  font-weight: 400;
  letter-spacing: 0.01em;
  line-height: 0.92;
}
.act[data-act="6"] .accent-line {
  position: relative;
  font-family: "Caveat", cursive;
  font-weight: 700;
  font-style: normal;
  font-size: 1.05em;
  line-height: 1;
  display: inline-block;
  white-space: nowrap;
}

/* Brush-swash underline, approximating the reference's hand-drawn
   stroke — a CSS/SVG ribbon shape, not a pixel match, per the earlier
   heads-up. Static (not tied to the character-sweep reveal above). */
.act[data-act="6"] .accent-line::after {
  content: "";
  position: absolute;
  left: -3%;
  right: -3%;
  bottom: -0.2em;
  height: 0.22em;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 300 30' preserveAspectRatio='none'%3E%3Cpath d='M2,18 C60,26 120,8 180,14 C220,18 250,10 280,4 L292,10 C260,18 225,26 180,22 C120,16 60,32 4,24 Z' fill='%23FF6D29'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-size: 100% 100%;
  pointer-events: none;
}
.act[data-act="4"] .act-scene,
.act[data-act="5"] .act-scene {
  padding-right: 0;
  padding-left: 32rem;
  align-items: flex-end;
}
.act[data-act="4"] .scene-image,
.act[data-act="5"] .scene-image {
  width: 100%;
  height: 100%;
  max-width: none;
  object-fit: cover;
  object-position: right center;
}

/* Act 6 — full-bleed treatment, same pattern as Act 2: image fills the
   entire scene (scrim behind the copy for contrast) instead of sitting
   in a separate reserved column. */
.act[data-act="6"] .act-scene {
  padding: 0;
}
.act[data-act="6"] .scene-image {
  width: 100%;
  height: 100%;
  max-width: none;
  object-fit: cover;
  object-position: center;
}

/* Ambient money-fall layer for Act 6 — sits between the image and the
   scrim/credit (same z-index band as .scene-scrim; DOM order puts it
   on top). Particles are plain emoji spans, positioned/animated
   entirely by act6MoneyFall in gsap-effects.js. */
.money-fall {
  position: absolute;
  inset: 0;
  z-index: 1;
  overflow: hidden;
  pointer-events: none;
}

.money-fall__note {
  position: absolute;
  top: 0;
  left: 0;
  font-size: 1.6rem;
  line-height: 1;
  opacity: 0;
  will-change: transform, opacity;
}

body.reduced-motion .money-fall {
  display: none;
}

/* Small brand credit over the final scene — real text, not part of
   the generated image, so it stays legible and easy to edit. */
.scene-credit {
  position: absolute;
  z-index: 1;
  bottom: 1.5rem;
  right: 1.5rem;
  margin: 0;
  padding: 0.5rem 1rem;
  border-radius: 999px;
  background: rgba(22, 19, 22, 0.45);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
  color: rgba(255, 255, 255, 0.85);
  font-size: 0.78rem;
  letter-spacing: 0.02em;
}
.scene-credit strong {
  color: var(--accent);
  font-weight: 600;
}

/* Audit panel floats over the illustration near Arvind, per the
   reference — taken out of the flex flow so it overlaps the image
   instead of sitting beside it. Dormant by default (invisible, clipped
   shut, softly blurred) — the GSAP diagnostic timeline in
   gsap-effects.js (act1AuditTimeline) owns opacity/clip-path/transform/
   filter directly via inline styles for its entire assemble-then-
   dismiss lifecycle, so this is only ever what a visitor sees before
   JS runs or how reduced-motion forces it open (see the override
   near the other body.reduced-motion rules). No rotation/tilt here
   anymore — a precise instrument assembling via clip-path reads better
   upright than the old flip's jaunty angle. */
.browser-window--float {
  position: absolute;
  /* Clears the fixed site header (~82px) with room to spare — it used
     to sit at -4rem (above the viewport entirely), overlapping the
     header once the panel assembled. */
  top: 6rem;
  left: 24rem;
  z-index: 1;
  font-size: 0.68rem;
  box-shadow: 0 30px 70px -30px rgba(0, 0, 0, 0.3);
  /* .act-scene (its containing block) is pointer-events: none so it
     doesn't block clicks through to content behind the illustration —
     re-enable it here so the traffic-light hover below (and anything
     else interactive in the panel) actually works. */
  pointer-events: auto;
  opacity: 0;
  clip-path: inset(0 0 100% 0 round 12px);
  filter: blur(6px);
  will-change: opacity, clip-path, filter, transform;
}
/* .browser-window's own width (min(46vw, 560px), meant for the
   full-size mockups elsewhere) is an equal-specificity, later-declared
   rule that would otherwise win this element's width — same cascade
   issue as the transform fix above. Two classes beats one regardless
   of source order. */
.browser-window.browser-window--float {
  width: 15rem;
}
.browser-window--float .browser-content {
  padding: 0.9rem;
  gap: 0.35rem;
}
.browser-window--float .audit-row {
  padding: 0.2rem 0;
}

.act[data-act="0"] .display-1 {
  font-size: clamp(2.25rem, 4.4vw, 3.75rem);
}

.act[data-theme="dark"] .act-copy {
  color: var(--scene-text);
}

.act[data-theme="dark"] .act-copy .body-lead {
  color: rgba(255, 255, 255, 0.72);
}

.act[data-theme="light"] .act-scene {
  background: var(--background);
}

/* Beats: text/objects that reveal once during the act, independent of continuous progress */
[data-reveal] {
  opacity: 0;
  transform: translateY(34px);
  filter: blur(6px);
  transition: opacity var(--duration-slow) var(--ease-spring), transform var(--duration-slow) var(--ease-spring), filter var(--duration-medium) var(--ease-standard);
}

[data-reveal].is-revealed {
  opacity: 1;
  transform: translateY(0);
  filter: blur(0);
}

/* Sideways variants — same mechanism as the default up-reveal above
   (still just [data-reveal], one-shot IntersectionObserver in
   script.js), only the resting/travel axis changes. Used for small
   kicker lines (was-line/gain-tag) that read better sliding in from
   the edge of their own text block than rising from below. */
[data-reveal="left"] {
  transform: translateX(-46px);
}

[data-reveal="left"].is-revealed {
  transform: translateX(0);
}

[data-reveal="right"] {
  transform: translateX(46px);
}

[data-reveal="right"].is-revealed {
  transform: translateX(0);
}

[data-reveal="stagger"]>* {
  opacity: 0;
  transform: translateY(28px);
  filter: blur(6px);
  transition: opacity var(--duration-slow) var(--ease-spring), transform var(--duration-slow) var(--ease-spring), filter var(--duration-medium) var(--ease-standard);
}

[data-reveal="stagger"].is-revealed>* {
  opacity: 1;
  transform: translateY(0);
  filter: blur(0);
}

[data-reveal="stagger"].is-revealed>*:nth-child(1) {
  transition-delay: 0ms;
}

[data-reveal="stagger"].is-revealed>*:nth-child(2) {
  transition-delay: 90ms;
}

[data-reveal="stagger"].is-revealed>*:nth-child(3) {
  transition-delay: 180ms;
}

[data-reveal="stagger"].is-revealed>*:nth-child(4) {
  transition-delay: 270ms;
}

/* Scroll progress indicator (per act) */
.act-progress {
  position: absolute;
  right: var(--gutter);
  bottom: 2.5rem;
  z-index: 3;
  width: 2px;
  height: 96px;
  background: rgba(255, 255, 255, 0.18);
}

.act[data-theme="light"] .act-progress {
  background: var(--line);
}

.act-progress::after {
  content: "";
  position: absolute;
  left: 0;
  top: 0;
  width: 100%;
  height: calc(var(--progress) * 100%);
  background: var(--accent);
}

/* ============================================================
   7. Characters (placeholder pose scrubber)
   ============================================================ */
.character-stage {
  display: flex;
  align-items: flex-end;
  justify-content: center;
  gap: clamp(1rem, 4vw, 3rem);
  flex-shrink: 0;
}

.character {
  position: relative;
  width: clamp(140px, 16vw, 240px);
  transform:
    translate3d(0, calc((1 - var(--progress, 0)) * 24px), 0) scale(calc(0.96 + var(--progress, 0) * 0.04));
  transition: transform var(--duration-fast) linear;
}

.character__pose {
  position: absolute;
  inset: 0;
  opacity: 0;
  transition: opacity var(--duration-medium) var(--ease-soft);
}

.character__pose.is-active {
  opacity: 1;
  position: relative;
}

.character__pose:not(.is-active) {
  position: absolute;
}

/* Single flat scene illustration — used by acts that haven't been
   split into parallax layers yet (see Act 0's .hero-layer system
   for the pattern this will eventually move to). */
.scene-image {
  width: min(62vw, 980px);
  height: auto;
  border-radius: 12px;
  display: block;
  /* Entrance settle (quick, tied to --progress) composed with a
     continuous camera pan/zoom across the whole act (--cam-x/-y/
     -scale — the same per-act system Act 0's hero uses, just
     applied directly to one flat image instead of depth layers).
     Acts 1 and 2 override this whole rule below (Report Zero <->
     Digital Foundation continuous camera) — this formula (and the
     transition below) only ever actually renders for Acts 3, 4, 5, 6. */
  transform:
    translate3d(0, calc((1 - var(--progress, 0)) * 20px), 0)
    translate3d(calc(var(--cam-x, 0) * 1px), calc(var(--cam-y, 0) * 1px), 0)
    scale(calc((0.94 + var(--progress, 0) * 0.06) * var(--cam-scale, 1)));
  /* Blur-to-sharp reveal. Uses its own --blur-edge ramp rather than
     the shared --scene-edge (which also drives the cross-act opacity
     crossfade) so the image can stay sharp a little longer after the
     visitor has scrolled past the section before it starts to blur,
     without changing the opacity crossfade's own timing. --audit-scan-dim
     (0-1, Act 1 only — set by act1AuditTimeline's "scan" phase in
     gsap-effects.js) layers in a barely-there dim/restore so the scene
     itself seems to react while it's being read, without touching any
     other act (falls back to 0, a no-op, everywhere else). */
  filter: blur(calc((1 - var(--blur-edge, 1)) * 8px)) brightness(calc(1 - var(--audit-scan-dim, 0) * 0.05));
  transition: transform var(--duration-fast) linear, filter var(--duration-medium) var(--ease-standard);
}

/* ============================================================
   8. Browser mockup
   ============================================================ */
.browser-window {
  width: min(46vw, 560px);
  border-radius: 12px;
  overflow: hidden;
  background: var(--color-white);
  box-shadow: 0 30px 80px -30px rgba(0, 0, 0, 0.5);
  transform:
    translate3d(0, calc((1 - var(--progress, 0)) * 20px), 0) scale(calc(0.94 + var(--progress, 0) * 0.06));
  transition: transform var(--duration-fast) linear;
}

/* .browser-window--float's opacity/clip-path/filter/transform are now
   owned end-to-end (assemble → hold → dismiss) by the GSAP timelines
   in gsap-effects.js (act1AuditTimeline for the entrance, the Act 1 →
   Act 2 timeline for the exit) via direct inline styles, rather than
   this CSS class-toggle + custom-property-bridge approach — inline
   styles already beat this rule's equal-specificity, later-declared
   base transform regardless of source order, so no override rule is
   needed here the way one was for the old flip reveal.  */

.browser-toolbar {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  padding: 0.65rem 0.9rem;
  background: var(--surface-strong);
}

.browser-controls {
  display: flex;
  gap: 6px;
}

.browser-controls span {
  width: 9px;
  height: 9px;
  border-radius: 50%;
  background: var(--color-grey);
  transition: background-color var(--duration-fast) var(--ease-standard);
}

/* macOS-style traffic lights on hover: close / minimize / maximize.
   Triggered by hovering the whole window mockup, not just the dots
   themselves — matches how a real window's controls light up as soon
   as the cursor is anywhere over the titlebar/window. */
.browser-window:hover .browser-controls span:nth-child(1) {
  background: #ff5f57;
}
.browser-window:hover .browser-controls span:nth-child(2) {
  background: #febc2e;
}
.browser-window:hover .browser-controls span:nth-child(3) {
  background: #28c840;
}

.browser-address {
  flex: 1;
  height: 20px;
  border-radius: 999px;
  background: var(--color-white);
  font-size: 0.7rem;
  color: var(--muted-text);
  display: flex;
  align-items: center;
  padding: 0 0.7rem;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.browser-content {
  min-height: 320px;
  padding: 1.5rem;
  display: grid;
  gap: 0.9rem;
}

.browser-stage {
  display: none;
  animation: fadeIn var(--duration-medium) var(--ease-soft);
}

.browser-stage.is-active {
  display: grid;
  gap: 0.7rem;
}

@keyframes fadeIn {
  from {
    opacity: 0;
    transform: translateY(6px);
  }

  to {
    opacity: 1;
    transform: translateY(0);
  }
}

.stage-block {
  border-radius: 8px;
  background: var(--surface);
}

.stage-block--accent {
  background: var(--accent);
}

.stage-block--dark {
  background: var(--color-black);
}

/* Report Zero audit interface (Act 1 browser mockup) */
.audit-status-line {
  font-size: 0.72rem;
  font-weight: 600;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--muted-text);
}

.audit-status-line--done {
  color: var(--accent);
}

/* Dormant by default — act1AuditTimeline's "rows" phase reveals each
   one (opacity/y/blur) in sequence; reduced-motion forces all of this
   open (see the body.reduced-motion block above). min-width: 0
   overrides the grid item default of min-width: auto — without it,
   .browser-content's implicit grid track grows to fit this row's
   unshrinkable content (the label has flex-shrink: 0) instead of
   respecting the panel's own fixed width, so the row silently laid
   out wider than the panel and got clipped by its overflow: hidden. */
.audit-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
  min-width: 0;
  padding: 0.55rem 0;
  border-bottom: 1px solid var(--line);
  font-size: 0.85rem;
  opacity: 0;
  transform: translateY(14px);
  filter: blur(4px);
}

.audit-row:last-child {
  border-bottom: none;
}

.audit-label {
  color: var(--text);
  font-weight: 500;
  flex-shrink: 0;
}

.audit-bar {
  position: relative;
  flex: 0 1 1.4rem;
  min-width: 0.4rem;
  height: 8px;
  border-radius: 999px;
  background: var(--surface-strong);
  overflow: hidden;
}

/* Fills left-to-right as the row's value resolves — a real element
   (not a background-size trick) so act1AuditTimeline can scaleX it
   independently of the row's own entrance. */
.audit-bar-fill {
  position: absolute;
  inset: 0;
  background: var(--accent);
  border-radius: inherit;
  transform: scaleX(0);
  transform-origin: left center;
}

/* Fixed box so the pending "—" and the real value can cross-fade in
   place without the row's width jumping between them. */
.audit-value {
  position: relative;
  display: inline-block;
  min-width: 5.4rem;
  height: 1.2em;
  flex-shrink: 0;
}

.audit-value-pending,
.audit-finding {
  position: absolute;
  top: 0;
  right: 0;
  white-space: nowrap;
}

.audit-value-pending {
  color: var(--muted-text);
  opacity: 1;
}

.audit-finding {
  color: var(--muted-text);
  text-align: right;
  font-size: 0.85em;
  opacity: 0;
  filter: blur(4px);
}

/* Status dot: neutral until act1AuditTimeline sets --row-resolve
   (0-1) on the row during the "resolve" phase, blending it to the
   accent color as that row's value locks in. */
.audit-row::before {
  content: "";
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: color-mix(in srgb, var(--accent) calc(var(--row-resolve, 0) * 100%), var(--surface-strong) calc((1 - var(--row-resolve, 0)) * 100%));
  margin-right: 0.4rem;
  flex-shrink: 0;
}

/* ============================================================
   Act 1 diagnostic scan — a thin editorial sweep + transient
   position markers "reading" the scene before the audit panel
   exists. Dormant by default; driven by act1AuditTimeline's
   "scan"/"markers" phases (see gsap-effects.js). Deliberately
   restrained: a single accent-colored hairline and small muted
   pill labels, not a sci-fi HUD.
   ============================================================ */
.audit-scan-line {
  position: absolute;
  left: 4%;
  right: 4%;
  top: 0;
  height: 1px;
  background: linear-gradient(90deg, transparent, var(--accent) 25%, var(--accent) 75%, transparent);
  box-shadow: 0 0 10px 0 rgba(255, 109, 41, 0.35);
  opacity: 0;
  pointer-events: none;
  z-index: 2;
  will-change: transform, opacity;
}

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

.audit-scan-marker {
  position: absolute;
  display: inline-flex;
  align-items: center;
  font-family: var(--font-body);
  font-size: 0.68rem;
  font-weight: 600;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--color-white);
  background: rgba(22, 19, 22, 0.55);
  padding: 0.3rem 0.65rem;
  border-radius: 999px;
  opacity: 0;
  will-change: transform, opacity, filter;
}

.audit-scan-marker::before {
  content: "";
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: var(--accent);
  margin-right: 0.45rem;
  flex-shrink: 0;
}

/* ============================================================
   9. Indicator chips / metrics
   ============================================================ */
.chip-row {
  display: flex;
  flex-wrap: wrap;
  gap: 0.6rem;
}

.chip {
  padding: 0.45rem 0.85rem;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.08);
  border: 1px solid rgba(255, 255, 255, 0.16);
  font-size: 0.78rem;
  color: var(--scene-text);
}

.act[data-theme="light"] .chip {
  background: var(--surface);
  border-color: var(--line);
  color: var(--text);
}

.metric {
  display: flex;
  align-items: baseline;
  gap: 0.6rem;
}

.metric__value {
  font-family: var(--font-display);
  font-size: clamp(2.5rem, 6vw, 4.5rem);
  color: var(--accent);
}

.metric__label {
  font-size: 0.85rem;
  color: var(--muted-text);
}

/* Before/after split */
.split {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 1px;
  background: var(--line);
  width: min(58vw, 620px);
  border-radius: 12px;
  overflow: hidden;
}

.split>div {
  padding: 1.75rem;
  background: var(--background);
}

.split .split--before {
  opacity: calc(1 - var(--progress, 0) * 0.35);
}

.split .split--after {
  background: var(--color-black);
  color: var(--scene-text);
  transform: scaleX(calc(1 + var(--progress, 0) * 0.02));
  transform-origin: left;
}

.split h3 {
  font-size: 1rem;
  margin-bottom: 0.9rem;
}

.split ul {
  display: grid;
  gap: 0.55rem;
  font-size: 0.9rem;
  color: var(--muted-text);
}

.split .split--after ul {
  color: rgba(255, 255, 255, 0.75);
}

@media (max-width: 720px) {
  .split {
    grid-template-columns: 1fr;
  }
}

/* ============================================================
   10. Scroll animations / reveal helpers
   ============================================================ */
.fade-parallax {
  opacity: calc(0.4 + var(--progress, 0) * 0.6);
  transform: translate3d(0, calc((1 - var(--progress, 0)) * 30px), 0);
}

/* ============================================================
   11. CTA
   ============================================================ */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  padding: 0.95rem 1.75rem;
  border-radius: 999px;
  font-size: 0.9rem;
  font-weight: 600;
  text-decoration: none;
  transition: transform var(--duration-fast) var(--ease-standard), background var(--duration-fast) var(--ease-standard);
}

/* Dark text, not white — white-on-accent measures ~2.81:1, well under
   WCAG AA's 4.5:1 floor for text this size. var(--text) on the same
   background measures ~6.56:1. Keeps the orange itself untouched
   (no compromise on brand color), just the label color on top of it. */
.btn--primary {
  background: var(--accent);
  color: var(--text);
}

.btn--primary:hover {
  transform: translateY(-2px);
}

.btn--ghost {
  border: 1px solid currentColor;
  color: inherit;
}

.btn--ghost:hover {
  background: rgba(255, 255, 255, 0.1);
}

.cta-row {
  display: flex;
  flex-wrap: wrap;
  gap: 0.9rem;
  padding-top: 0.4rem;
}

.contact-section {
  padding: clamp(4rem, 10vw, 8rem) var(--gutter);
  background: var(--color-black);
  color: var(--scene-text);
}

.contact-grid {
  max-width: var(--container-max);
  margin-inline: auto;
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: clamp(2rem, 6vw, 5rem);
  align-items: start;
}

@media (max-width: 860px) {
  .contact-grid {
    grid-template-columns: 1fr;
  }
}

.field {
  display: grid;
  gap: 0.4rem;
  margin-bottom: 1.1rem;
}

.field label {
  font-size: 0.78rem;
  letter-spacing: 0.04em;
  color: rgba(255, 255, 255, 0.65);
}

.field-optional {
  color: rgba(255, 255, 255, 0.4);
  text-transform: none;
  letter-spacing: 0;
}

.field input,
.field select,
.field textarea {
  background: rgba(255, 255, 255, 0.06);
  border: 1px solid rgba(255, 255, 255, 0.18);
  border-radius: 8px;
  padding: 0.75rem 0.9rem;
  color: var(--scene-text);
  font: inherit;
  appearance: none;
}

.field select {
  background-image: linear-gradient(45deg, transparent 50%, rgba(255, 255, 255, .5) 50%), linear-gradient(135deg, rgba(255, 255, 255, .5) 50%, transparent 50%);
  background-position: calc(100% - 20px) center, calc(100% - 14px) center;
  background-size: 6px 6px, 6px 6px;
  background-repeat: no-repeat;
}

.field input:focus,
.field select:focus,
.field textarea:focus {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

.field--checkbox {
  display: flex;
  flex-direction: row;
  align-items: flex-start;
  gap: 0.6rem;
  margin-bottom: 1.3rem;
}

.field--checkbox input {
  margin-top: 0.2rem;
  accent-color: var(--accent);
}

.field--checkbox span {
  font-size: 0.85rem;
  color: rgba(255, 255, 255, 0.65);
}


.qualifier {
  font-size: 0.88rem;
  color: rgba(255, 255, 255, 0.55);
  border-top: 1px solid rgba(255, 255, 255, 0.14);
  padding-top: 1.1rem;
  margin-top: 0.6rem;
}

.form-note {
  font-size: 0.8rem;
  color: rgba(255, 255, 255, 0.45);
  margin-top: 0.9rem;
}

.form-status {
  font-size: 0.85rem;
  color: var(--accent);
  min-height: 1.2em;
}

/* ============================================================
   12. Footer
   ============================================================ */
.site-footer {
  padding: 3rem var(--gutter) 3.5rem;
  background: var(--color-black);
  color: rgba(255, 255, 255, 0.55);
  display: grid;
  gap: 0.9rem;
  font-size: 0.82rem;
  max-width: var(--container-max);
  margin-inline: auto;
}

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

.site-footer a:hover {
  color: var(--accent);
}

.footer-brand {
  color: rgba(255, 255, 255, 0.85);
}

.footer-address {
  font-style: normal;
  line-height: 1.7;
}

.footer-links {
  display: flex;
  gap: 1.2rem;
}

.footer-legal {
  line-height: 1.7;
  color: rgba(255, 255, 255, 0.4);
  font-size: 0.76rem;
}

/* ============================================================
   13. Responsive styles
   ============================================================ */
@media (max-width: 860px) {
  .act--scroll {
    height: 240vh;
  }

  .act[data-act="0"].act--scroll {
    height: 310vh;
  }

  .act-sticky {
    padding-top: 4rem;
  }

  .act-scene {
    justify-content: center;
    align-items: flex-end;
    padding: 5rem var(--gutter) 1.5rem;
  }

  .act-scene>* {
    opacity: 0.5;
  }

  /* The desktop-only padding-left/floating-panel overrides for
     Act 1 don't fit a mobile viewport — reset them so the
     illustration stays centered and visible, and hide the panel
     rather than cram it in. */
  .act[data-act="1"] .act-scene {
    padding-left: var(--gutter);
  }
  .act[data-act="1"] .act-scene > * {
    opacity: 0.5 !important;
  }
  .browser-window--float,
  .audit-scan-line,
  .audit-scan-markers,
  .panel-orbit {
    display: none;
  }
  .act[data-act="4"] .act-scene,
  .act[data-act="5"] .act-scene {
    padding-left: var(--gutter);
  }

  .character-stage {
    gap: 1rem;
  }

  .character {
    width: clamp(110px, 30vw, 160px);
  }

  .act-progress {
    display: none;
  }
}

@media (max-width: 540px) {
  .act--scroll {
    height: 200vh;
  }

  .act[data-act="0"].act--scroll {
    height: 260vh;
  }

  .browser-content {
    padding: 1rem;
    min-height: 240px;
  }
}

/* ============================================================
   13.5 Preloader
   Screen-load intro: brand mark + progress bar, held over
   everything else, dismissed by gsap-effects.js once the page's
   fonts/images are ready (or immediately under reduced motion —
   see the reduced-motion block below). Fixed/full-viewport so it
   sits above the pinned hero rather than scrolling away from it.
   ============================================================ */
.preloader {
  position: fixed;
  inset: 0;
  z-index: 999;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--scene-bg);
}

.preloader-stage {
  position: relative;
  width: min(90vw, 720px);
  display: flex;
  align-items: center;
  justify-content: center;
}

/* One word at a time, swapped and re-split into [.preloader-char] spans
   by gsap-effects.js as it cycles through the story arc (invisible →
   growing → undeniable) — see the intro sequence there. Anton (loaded
   in index.html) rather than the site's own display/body fonts: a
   condensed poster weight reads at this size without needing a huge
   clamp() range, and it's used nowhere else on the page, so it stays
   feeling like part of the loading moment rather than the brand voice. */
.preloader-word {
  display: block;
  font-family: "Anton", var(--font-display);
  font-weight: 400;
  text-transform: lowercase;
  font-size: clamp(2.75rem, 9vw, 6rem);
  color: var(--scene-text);
  letter-spacing: 0.01em;
  line-height: 1;
}

.preloader-char {
  display: inline-block;
}

.preloader-index,
.preloader-tag {
  position: absolute;
  font-family: var(--font-body);
  font-size: 0.75rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: rgba(255, 255, 255, 0.4);
}

.preloader-index {
  left: 0;
  bottom: -2.5rem;
}

.preloader-tag {
  right: 0;
  top: -2.5rem;
}

body.preloader-locked {
  overflow: hidden;
  height: 100%;
}

/* ============================================================
   14. Reduced motion
   ============================================================ */
@media (prefers-reduced-motion: reduce) {

  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

body.reduced-motion .act--scroll {
  height: auto;
}

body.reduced-motion .act-sticky {
  position: relative;
  height: auto;
  min-height: 100vh;
  padding: 5rem 0;
}

body.reduced-motion .character,
body.reduced-motion .browser-window,
body.reduced-motion .split,
body.reduced-motion .scene-layer {
  transform: none !important;
}

body.reduced-motion [data-reveal],
body.reduced-motion [data-reveal="stagger"]>* {
  opacity: 1 !important;
  transform: none !important;
  filter: none !important;
}

body.reduced-motion .preloader {
  display: none !important;
}

/* Act 1's diagnostic sequence (gsap-effects.js act1AuditTimeline) never
   runs under reduced motion (guarded at the top of that file) — force
   its dormant/transient/mid-resolve CSS defaults straight to the
   finished state instead, so the audit panel still shows real values
   rather than sitting clipped shut or stuck on "—". */
body.reduced-motion .browser-window--float {
  opacity: 1 !important;
  clip-path: none !important;
  filter: none !important;
}
body.reduced-motion .audit-row {
  opacity: 1 !important;
  transform: none !important;
  filter: none !important;
}
body.reduced-motion .audit-bar-fill {
  transform: scaleX(1) !important;
}
body.reduced-motion .audit-value-pending {
  display: none !important;
}
body.reduced-motion .audit-value-resolved,
body.reduced-motion .audit-finding {
  opacity: 1 !important;
  filter: none !important;
}
body.reduced-motion .audit-scan-line,
body.reduced-motion .audit-scan-markers,
body.reduced-motion .panel-orbit {
  display: none !important;
}

body.reduced-motion .act-progress {
  display: none;
}

body.reduced-motion .act-copy--beats {
  min-height: auto;
  display: flex;
  flex-direction: column;
  gap: 2.5rem;
}

body.reduced-motion .beat {
  position: static;
  opacity: 1 !important;
  transform: none !important;
}

body.reduced-motion .act-scene--immersive {
  position: relative;
  height: 60vh;
}

body.reduced-motion .act-scene>* {
  opacity: 1 !important;
}

body.reduced-motion .scene-scrim {
  display: none;
}

/* The scene no longer stretches behind the whole act in this
   stacked static layout, so give the act itself the theme
   backdrop directly — otherwise text-on-dark copy loses contrast
   against the page's default background. */
body.reduced-motion .act[data-theme="dark"] .act-sticky {
  background: var(--scene-bg);
}

body.reduced-motion .act[data-theme="light"] .act-sticky {
  background: var(--background);
}
