/* ============================================================
   STICKER STAR — descent landing page
   Architecture: fixed full-screen layers, choreographed by GSAP.
   Mobile-first. Desktop breakpoint at 1100px.

   Z-stack (back → front):
     backdrop(1) < stars(1) < marquee(1) < CV1(2) < CV2(3) < signpost(4) < ground(5)
   ============================================================ */

*, *::before, *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

:root {
  --space:    #020204;
  --teal:     #00A89D;
  --cream:    #F5F0E8;
  --ink:      #1A1A2A;
  --red:      #D64045;
  --gold:     #E8B84B;
  --ground:   #4A6840;

  /* Muted-pastel palette (palette.jpg) — atomic-age soft tones for hero copy */
  --pal-steel:       #8294AB;
  --pal-periwinkle:  #C0CFE5;
  --pal-powder:      #C8DDED;
  --pal-mint:        #D9EFE5;
  --pal-rose:        #F4DEE8;
  --pal-coral:       #EAA4A6;
  --pal-peach:       #F0CCB7;
  --pal-creampeach:  #FFEBCF;

  /* Saturated complements — same hue family, deeper. Used as outline strokes
     around the muted fills so pastel text pops against light backgrounds. */
  --pal-steel-deep:  #2B4F7A;
  --pal-coral-deep:  #C03A47;

  /* Signal-broadcast green — muted sage so the transmission reads as
     atmospheric/subtle rather than electric. Distinct from cyan thruster
     rings but doesn't compete with the rest of the palette. */
  --signal-green:    #9FCFA2;
}

html, body {
  overflow-x: hidden;
}

body {
  background: var(--space);
  min-height: 100vh;
  font-family: 'Space Grotesk', system-ui, sans-serif;
}

/* ── Backdrop ───────────────────────────────────────────────
   CSS linear-gradient on a div. Element is 500vh tall — same as the
   scroll budget (5h). Stop percentages are relative to the gradient
   element's height. Use the table in the comment block below to
   map scroll position → visible gradient %.

   Visible-gradient-% by scroll (viewport center):
     scroll 0    → 10%        (top of descent)
     scroll 1h   → 26%
     scroll 2.5h → 50%        (mid descent)
     scroll 4h   → 74%
     scroll 5h   → 90%        (bottom of descent)

   To put a color at a specific descent moment, place its stop near
   the value above. Adjacent stops at close % give sharp transitions;
   wider gaps give long smooth fades. Easy to iterate — just change
   a hex value, save, refresh.  */

.backdrop {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 500vh;
  z-index: 1;
  pointer-events: none;
  will-change: transform;
  background: linear-gradient(180deg,
    #000000   0%,
    #000000  10%,    /* black headroom: scroll 0-10% region stays pure space */
    #5B316D  20%,
    #7AA2BE  35%,
    #236FA2  55%,
    #376D91  75%,
    #000000  90%,    /* black footroom: scroll 90-100% region stays dark for ground */
    #000000 100%
  );
}

/* ── Stars ──────────────────────────────────────────────────
   Jetsons-cartoon 4-point diamond stars (Gemini-logo silhouette).
   Each star is a CSS clip-path polygon with sharp narrow middle.
   Sizes, colors, positions, and twinkle delays are set via inline
   CSS variables on each .star element in HTML. */

.stars {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100dvh;
  z-index: 1;
  pointer-events: none;
  overflow: visible;
  will-change: transform;
}

.star {
  position: absolute;
  left: var(--x);
  top: var(--y);
  width: var(--size, 24px);
  height: var(--size, 24px);
  background: var(--color, #fff);
  clip-path: polygon(
    50% 0%, 55% 45%, 100% 50%, 55% 55%,
    50% 100%, 45% 55%, 0% 50%, 45% 45%
  );
  filter: drop-shadow(0 0 14px rgba(255, 255, 255, 0.7));
  animation: twinkle-cw var(--dur, 3.5s) ease-in-out infinite;
  animation-delay: var(--delay, 0s);
  will-change: opacity, transform;
}

/* Half the stars rotate the other direction — set --dir: ccw on those */
.star[style*="--dir: ccw"] {
  animation-name: twinkle-ccw;
}

@keyframes twinkle-cw {
  0%, 100% { opacity: 0.55; transform: scale(0.82) rotate(0deg); }
  50%      { opacity: 1.00; transform: scale(1.08) rotate(10deg); }
}
@keyframes twinkle-ccw {
  0%, 100% { opacity: 0.55; transform: scale(0.82) rotate(0deg); }
  50%      { opacity: 1.00; transform: scale(1.08) rotate(-10deg); }
}

/* ── Marquee logo ─────────────────────────────────────────
   Positioned in upper portion of viewport so it sits over the
   darker (deep-space) area of the backdrop gradient. */

/* Marquee — container parallaxes via GSAP; inner art bobs via CSS;
   thrusters emit ring animations. container-type lets ring size
   scale with marquee via container query inline-size units (cqi). */
.marquee {
  position: fixed;
  top: 25%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: min(380px, 88vw);
  z-index: 1;
  pointer-events: none;
  will-change: transform;
  container-type: inline-size;
}

.marquee-art {
  display: block;
  width: 100%;
  height: auto;
  animation: marquee-bob 3.2s ease-in-out infinite;
  will-change: transform;
}

@keyframes marquee-bob {
  0%, 100% { transform: translateY(0); }
  50%      { transform: translateY(-6px); }
}

/* Thrusters — positioned over the nozzle cones at the bottom of the
   marquee SVG. Positions are exact from user spec: SVG is 975px wide,
   left nozzle center at 172px (17.6%), right at 819px (84%).
   Rings are elliptical (wider than tall) for the "rocket exhaust seen
   from above" look. */
.thruster {
  position: absolute;
  bottom: 2%;
  width: 7cqi;
  aspect-ratio: 2.5 / 1;
  pointer-events: none;
  transform: translateX(-50%);
}

.thruster--left  { left: 17.6%; }
.thruster--right { left: 84%; }

.thruster span {
  position: absolute;
  inset: 0;
  border: 2px solid #00E5FF;
  border-radius: 50%;
  opacity: 0;
  box-shadow: 0 0 8px rgba(0, 229, 255, 0.5);
  animation: thruster-ring 1.2s ease-out infinite;
  will-change: transform, opacity;
}

.thruster span:nth-child(2) { animation-delay: 0.4s; }
.thruster span:nth-child(3) { animation-delay: 0.8s; }

@keyframes thruster-ring {
  0%   { transform: scale(0.3) translateY(0);    opacity: 1; }
  80%  {                                          opacity: 0.15; }
  100% { transform: scale(2.2) translateY(40px); opacity: 0; }
}

/* ── Tagline ─────────────────────────────────────────────
   "The peel good sticker company." stroked left-to-right by GSAP via
   stroke-dashoffset; WriteSprite tracks the leading edge. Parallaxes
   with the marquee/headlines. The SVG is injected from PeelGoodCo.svg
   into .tagline-svg-slot by JS; CSS overrides the inline cyan stroke
   to a palette color with a soft glow. */

.tagline-wrap {
  position: fixed;
  top: 47%;
  left: 50%;
  transform: translateX(-50%);
  width: min(420px, 85vw);
  aspect-ratio: 219.527 / 13.066;
  z-index: 1;
  pointer-events: none;
  will-change: transform;
}

.tagline-svg-slot,
.tagline-svg-slot svg {
  display: block;
  width: 100%;
  height: 100%;
  overflow: visible;
}

.tagline-svg-slot {
  /* JS animates --glow-* from "hot" (large, glowy) to "cool/crisp" (minimal).
     Stroke color is also animated by JS — cream-peach (laser-fresh) →
     coral (cooled ink). Default values here are the settled state.
     --mask-a/--mask-b define the soft reveal-band edges; JS animates them
     from before the slot (-10%/0%) to past it (100%/110%) to wipe in. */
  --glow-inner: 0.5px;
  --glow-outer: 0px;
  --mask-a: -10%;
  --mask-b: 0%;
  -webkit-mask-image: linear-gradient(90deg, black var(--mask-a), transparent var(--mask-b));
          mask-image: linear-gradient(90deg, black var(--mask-a), transparent var(--mask-b));
}

.tagline-svg-slot path {
  /* stroke-width is owned by the SVG file (PeelGoodCo.svg) — re-export
     with a different stroke-width to change tagline weight. */
  fill: none !important;
  filter:
    drop-shadow(0 0 var(--glow-inner) var(--pal-creampeach))
    drop-shadow(0 0 var(--glow-outer) var(--pal-coral));
}

.tagline-sprite-wrap {
  position: absolute;
  top: 50%;
  left: 0;
  width: 8.5%;
  aspect-ratio: 30 / 29;
  transform: translate(-50%, -50%);
  opacity: 0;
  will-change: transform, opacity;
  filter: drop-shadow(0 0 4px #FFF559) drop-shadow(0 0 10px rgba(255, 245, 89, 0.6));
}

.tagline-sprite {
  display: block;
  width: 100%;
  height: 100%;
  animation: sprite-spin 1.4s linear infinite;
  transform-origin: 50% 50%;
}

@keyframes sprite-spin {
  to { transform: rotate(360deg); }
}

@media (min-width: 1100px) {
  .tagline-wrap {
    top: 50%;
    width: min(620px, 48vw);
  }
}

/* ── Hero headlines ───────────────────────────────────────
   Three-phrase reveal below the marquee/tagline. JS splits each .word
   into per-character spans; CSS keyframe handles each char's "bulb
   light-up" when the .lit class is applied; GSAP timeline applies .lit
   with rhythmic stagger. Parallaxes with the marquee. */

.hero-headlines {
  /* Fixed-position top-level element. JS adds a parallax tween matching
     CV1's window/rate, so it rides with the cloud — but z=2 with DOM
     order AFTER #cloud-city stacks the headlines IN FRONT of the blimp.
     top: 35vw lands the headlines at ~28% of CV1's natural rendered
     height (125vw), matching the previous in-cloud anchor. */
  position: fixed;
  top: 35vw;
  left: 50%;
  transform: translateX(-50%);
  width: min(340px, 75vw);
  z-index: 2;
  pointer-events: none;
  font-family: 'Bungee', cursive;
  font-size: clamp(1.5rem, 6.5vw, 2.4rem);
  line-height: 1.2;
  will-change: transform;
}

.headline {
  margin: 0;
  text-align: left;
  /* Keep the whole "Good NOUN." phrase on one line — without this the space
     between .word-good and .word-noun was a valid break point at narrower
     widths, so "GOOD" stayed on line 1 and "STICKERS." dropped to line 2. */
  white-space: nowrap;
}

/* Belt-and-braces nowrap on the noun itself (also prevents the inline-block
   per-char model from breaking before the trailing period). */
.headline .word-noun {
  white-space: nowrap;
}

/* Colors tuned for white/light cloud background: muted pastel fills with
   a saturated same-hue outline. paint-order: stroke fill puts the stroke
   UNDER the fill so the muted color stays fully visible and the saturated
   edge frames it cleanly. */
.word-good,
.word-noun {
  paint-order: stroke fill;
  -webkit-text-stroke-width: 1.5px;
}
.word-good {
  color: var(--pal-steel);
  -webkit-text-stroke-color: var(--pal-steel-deep);
}
.word-noun {
  color: var(--pal-coral);
  -webkit-text-stroke-color: var(--pal-coral-deep);
}

.headline .char,
.headline .word-good {
  display: inline-block;
  opacity: 0.1;
  transform: scale(0.92);
}

.headline .char.lit,
.headline .word-good.lit {
  animation: bulb-on 220ms ease-out forwards;
}

/* Bulb-on without text-shadow — glow was designed for dark-space backdrop;
   on the white cloud body it just adds blur and hurts legibility.
   Personality now comes purely from the opacity + scale overshoot. */
@keyframes bulb-on {
  0%   { opacity: 0.1; transform: scale(0.92); }
  45%  { opacity: 1.0; transform: scale(1.10); }
  100% { opacity: 1.0; transform: scale(1.0);  }
}

@media (min-width: 1100px) {
  .hero-headlines {
    width: min(620px, 50vw);
    font-size: clamp(2.2rem, 4vw, 3.4rem);
  }
}

/* ── Cloud veil wraps ──────────────────────────────────────
   Wrap is sized to match the SVG's natural aspect (1600×2000),
   not 100dvh. This means on wider viewports the cloud is taller
   AND wider proportionally — no cropping anywhere, edge-to-edge
   horizontally, and the whole SVG (bumps + fill body) is always
   present in the wrap. GSAP translates the entire wrap through
   the viewport; on wider screens it moves more pixels per
   scroll-unit, producing the "bigger scrolls faster" effect. */

.veil-wrap {
  position: fixed;
  top: 0;
  left: 0;
  width: 100vw;
  height: auto;
  pointer-events: none;
  will-change: transform;
}

/* CV1 and CV2 have different aspect ratios — CV2 was shortened (2000→1400)
   to compress the descent. Keep each wrap matched to its SVG's viewBox. */
#veil-1 { z-index: 2; aspect-ratio: 1600 / 2000; }
#veil-2 {
  z-index: 3;
  aspect-ratio: 1600 / 1400;
  /* Fade CV2's bottom for seamless handoff to signpost */
  -webkit-mask-image: linear-gradient(180deg, black 0%, black 78%, transparent 100%);
          mask-image: linear-gradient(180deg, black 0%, black 78%, transparent 100%);
}

.veil-wrap img {
  display: block;
  width: 100%;
  height: 100%;
}

/* ── Cloud city ───────────────────────────────────────────
   Skyline that lives behind CV2 (z-2 with later DOM order so it stacks
   above CV1). Parallaxes at 1.2 × cloudDist (faster than CV2 at 1.0)
   to produce the "buildings rising up through the clouds" emergence
   as the user descends. CV2's scalloped mask hides the building legs. */

.cloud-city-wrap {
  position: fixed;
  top: 0;
  left: 0;
  width: 100vw;
  aspect-ratio: 1569 / 904;
  height: auto;
  z-index: 2;
  pointer-events: none;
  will-change: transform;
}

/* Direct-child selector ONLY — the blimp img inside the wrap should NOT
   inherit anything wrap-img-specific. */
.cloud-city-wrap > img {
  display: block;
  width: 100%;
  height: 100%;
  /* No inner scale. Wrap is 100vw, img fills it — entire cityscape is
     visible at every viewport width. (Previous scale(1.5) pushed 25vw
     off each side at all widths.) Want bigger buildings? Bump the
     wrap's width past 100vw at desktop instead of scaling the img. */
}

/* ── Blimp ────────────────────────────────────────────────
   Team Humanity easter egg. Lives INSIDE .cloud-city-wrap so it inherits
   the cityscape's vertical parallax automatically (no separate parallax
   tween needed — blimp rides above the city at the same scroll rate).
   Horizontal transit is an autonomous time-driven loop on .blimp-traverse
   (independent of scroll). The image inside flips on scaleX for the
   return trip; the flip happens off-screen during the pause. */

.blimp-traverse {
  position: absolute;
  top: -15vh;  /* above the top of the cityscape (clear of building tops) */
  left: 0;
  /* Fluid scaling like the marquee — grows linearly with viewport width
     up to a 380px cap. Replaces the old clamp() that pinned blimp to
     180px at viewports < 1000px. */
  width: min(380px, 30vw);
  aspect-ratio: 290 / 96;
  pointer-events: auto;
  cursor: pointer;
  text-decoration: none;
  display: block;
  will-change: transform;
}

/* .blimp-content wraps img + signal so the scaleX flip on direction reverse
   moves both as one unit (keeps signal arcs aligned with the dish on the
   flipped body). */
.blimp-content {
  position: relative;
  display: block;
  width: 100%;
  height: 100%;
}

.blimp-img {
  display: block;
  width: 100%;
  height: 100%;
}

/* Signal arcs — three semi-circular arcs broadcasting downward from the
   parabolic dish on the blimp's belly. Anchor at (150, 90) in the
   THBlimp 290×96 viewBox (6px up from bottom edge per user spec) →
   left 51.7%, top 93.75% of the blimp container. Arc path uses
   sweep-flag 0 so it curves DOWNWARD (concave-up "bowl" shape, with
   the convex outside toward the ground — radio-wave propagation). */

.blimp-signal {
  position: absolute;
  left: 51.7%;
  top: 93.75%;
  width: 80%;
  height: auto;
  transform: translateX(-50%);
  overflow: visible;
  pointer-events: none;
}

.signal-arc {
  fill: none;
  stroke: var(--signal-green);
  stroke-width: 3;
  stroke-linecap: round;
  vector-effect: non-scaling-stroke;
  /* The path d="M 0,0 M -92.4,..." extends the fill-box to include the
     dish point (0, 0). transform-origin: 50% 0% then lands at the
     dish — scaling expands the arcs outward from there. */
  transform-box: fill-box;
  transform-origin: 50% 0%;
  animation: signal-pulse 2.5s ease-out infinite;
}

.signal-arc-2 { animation-delay: 0.83s; }
.signal-arc-3 { animation-delay: 1.66s; }

@keyframes signal-pulse {
  0%   { transform: scale(0.15); opacity: 0.9; }
  80%  { opacity: 0.2; }
  100% { transform: scale(2.5); opacity: 0; }
}

/* ── Signpost ──────────────────────────────────────────────
   Single SVG signpost (pole + signs + decorations) sized as a
   factor of viewport width. Position:fixed directly to the viewport
   (no wrapper layer, no clipping container) — the signpost can extend
   above viewport (parts not yet scrolled in) and below it (pole base
   hidden by the ground when it rises). GSAP translates the signpost
   itself by its own offsetHeight, so motion always starts fully
   below viewport regardless of how tall the signpost is. */

.signpost-unit {
  position: fixed;
  bottom: 0;
  left: 50%;
  transform: translateX(-50%);
  width: min(380px, 90vw);
  aspect-ratio: 1106 / 2024;
  z-index: 4;
  pointer-events: auto;
  will-change: transform;
  container-type: inline-size;
}

.signpost-art {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
  pointer-events: none;
  user-select: none;
}

/* ── Foreground clouds ────────────────────────────────────
   Highest-z layer (closest to camera). Individual cloud imgs are
   absolutely positioned within the container and translated by GSAP
   from below viewport (y = h) to above (y = -img.height). Per-cloud
   left/width come from inline CSS vars set by JS. */

#foreground-clouds {
  position: fixed;
  inset: 0;
  z-index: 6;
  pointer-events: none;
  overflow: visible;
}

.fg-cloud {
  position: absolute;
  top: 0;
  left: var(--x);
  width: var(--size);
  height: auto;
  transform: translateX(-50%);
  will-change: transform;
}

/* ── Ground layer ──────────────────────────────────────────
   Surface (planet skin) at the bottom. Starla and CTA flank the
   pole horizontally — pole shows through the gap between them. */

.ground-layer {
  position: fixed;
  inset: 0;
  width: 100%;
  height: 100dvh;
  z-index: 5;
  pointer-events: auto;
  overflow: hidden;
  will-change: transform;
}

.surface {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 10vh;
  background: linear-gradient(180deg, #6F8B5C 0%, var(--ground) 60%, #2E4828 100%);
  box-shadow: 0 -4px 16px rgba(0, 0, 0, 0.3);
}

/* ── Ground city skyline ─────────────────────────────────
   Seven building SVGs absolutely positioned along the sidewalk (top of
   the surface band). Each is its own click target with a hover halo.
   Positions are %-based estimates from the user's layout reference; tune
   per-building left/width below to dial composition. */

.ground-city {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 10vh;  /* sit on top of the surface band */
  height: 50vh;
  pointer-events: none;  /* container is transparent to clicks; buildings opt back in */
  z-index: 0;
}

.gc-building {
  position: absolute;
  bottom: 0;
  height: auto;
  pointer-events: auto;
  cursor: pointer;
  transform: translateX(-50%);
  transition: filter 0.25s ease;
  will-change: filter;
}

/* Cyan halo on hover. Two drop-shadows compose a tight bright core plus a
   softer outer bloom in the same hue family as the marquee thrusters. */
.gc-building:hover {
  filter:
    drop-shadow(0 0 5px rgba(0, 229, 255, 0.85))
    drop-shadow(0 0 16px rgba(0, 229, 255, 0.55));
}

/* TRANS is the easter-egg radio dish — no hover halo (its "interactivity"
   is signaled by the propagating signal arcs, not a button halo). */
.gc-trans:hover { filter: none; }
.gc-trans { cursor: pointer; }  /* still clickable */

/* Per-building placement. Explicit z-index per DOM order so stacking is
   deterministic when adjacent buildings' rectangular bboxes overlap at
   the edges — otherwise the wider/taller building can steal hover for
   the area where its bbox encroaches on its neighbor. */
.gc-insta   { z-index: 1; left:  5%; width:  7vw; }
.gc-contact { z-index: 2; left: 15%; width: 13vw; }
.gc-gallery { z-index: 3; left: 25%; width:  8vw; }
.gc-legal   { z-index: 4; left: 37%; width: 13vw; bottom: 1px; }  /* +1px lift — LEGAL's SVG content extends ~1px lower than its neighbors' (was PRIVACY; same footprint) */
.gc-what    { z-index: 5; left: 50.5%; width: 11vw; }
.gc-why     { z-index: 6; left: 64%; width: 13vw; }
.gc-trans   { z-index: 7; left: 89%; width: 20vw; aspect-ratio: 329 / 290; }

/* GC_TRANS contains an img + the skyward signal arcs SVG. */
.gc-trans .gc-trans-img {
  display: block;
  width: 100%;
  height: 100%;
}

/* Signal SVG positioned over the dish origin point (128/329, 38/290).
   Width is generous (120% of wrap) so arcs can extend well past the dish
   at peak scale without being clipped by overflow. */
.trans-signal {
  position: absolute;
  left: 38.9%;
  top: 13.1%;
  width: 120%;
  height: auto;
  transform: translate(-50%, -100%) rotate(-28deg);  /* origin at dish bottom-center, rotated 28° CCW to align with feed-horn axis */
  transform-origin: 50% 100%;
  overflow: visible;
  pointer-events: none;
}

.trans-signal .signal-arc {
  fill: none;
  stroke: var(--signal-green);
  stroke-width: 3;
  stroke-linecap: round;
  vector-effect: non-scaling-stroke;
  /* Phantom "M 0,0" extends bbox down to the dish point; with
     transform-origin: 50% 100% the scale animation expands outward
     (upward) from the dish. */
  transform-box: fill-box;
  transform-origin: 50% 100%;
  animation: signal-pulse 2.5s ease-out infinite;
}

.trans-signal .signal-arc-2 { animation-delay: 0.83s; }
.trans-signal .signal-arc-3 { animation-delay: 1.66s; }

/* Starla — flying greeter on a levitation platform. Positioned directly
   above the PRIVACY building (centered on its 37% anchor), out of the
   GC_TRANS transmission cone path. The wrap holds the SVG + the thruster
   overlay (positioned at the platform underside via SVG-coordinate %).
   clamp() floor keeps her readable on mobile while letting her grow on
   desktop. container-type: inline-size lets the thruster scale with the
   wrap via container query inline units (cqi). */
.starla-wrap {
  position: absolute;
  bottom: 60vh;  /* airborne, above the shorter buildings */
  left: 37%;
  width: clamp(180px, 14vw, 220px);  /* capped at "mobile-version size" so she fits the headroom */
  aspect-ratio: 215 / 329;  /* matches starla.svg viewBox so thruster % math is exact */
  z-index: 1;
  pointer-events: none;
  container-type: inline-size;
  animation: starla-fly 3.4s ease-in-out infinite;
  will-change: transform;
}

.starla-img {
  display: block;
  width: 100%;
  height: 100%;
}

/* Y-axis bob only — no rotation per design feedback. translateX(-50%)
   centers the wrap on its `left` anchor and is preserved across keyframes. */
@keyframes starla-fly {
  0%, 100% { transform: translateX(-50%) translateY(0); }
  50%      { transform: translateX(-50%) translateY(-12px); }
}

/* Thruster ring source — at x=133, y=313 in the 215×329 starla.svg viewBox.
   In container-relative %: 133/215 = 61.9% left, 313/329 = 95.14% top
   (= 4.86% from bottom). Smaller than the marquee thruster (5cqi vs 7cqi). */
.starla-thruster {
  position: absolute;
  left: 61.9%;
  bottom: 4.86%;
  width: 5cqi;
  aspect-ratio: 2.5 / 1;
  pointer-events: none;
  transform: translateX(-50%);
}

/* Cape sway — gentle ±4° skewX, anchored at the cape's top edge (where it
   attaches to the body). Period matches starla-fly (3.4s) and the keyframes
   are phase-shifted by a quarter cycle so the cape's peak skew lands at the
   body's velocity peaks (mid-ascent at 25%, mid-descent at 75%). Models
   cloth inertia: cape lags toward the body during ascent and trails away
   during descent. */
.cape-sway {
  transform-box: fill-box;
  transform-origin: 50% 0%;
  animation: cape-sway 3.4s ease-in-out infinite;
}

@keyframes cape-sway {
  0%, 100% { transform: skewX( 0deg); }  /* base of bob — neutral */
  25%      { transform: skewX( 4deg); }  /* mid-ascent — cape trails away from body's upward acceleration */
  50%      { transform: skewX( 0deg); }  /* top of bob — transitioning */
  75%      { transform: skewX(-4deg); }  /* mid-descent — cape trails away from body's downward acceleration */
}

.starla-thruster span {
  position: absolute;
  inset: 0;
  border: 2px solid #00E5FF;
  border-radius: 50%;
  opacity: 0;
  box-shadow: 0 0 6px rgba(0, 229, 255, 0.5);
  animation: starla-thrust-ring 1.4s ease-out infinite;
  will-change: transform, opacity;
}

.starla-thruster span:nth-child(2) { animation-delay: 0.46s; }
.starla-thruster span:nth-child(3) { animation-delay: 0.92s; }

@keyframes starla-thrust-ring {
  0%   { transform: scale(0.3) translateY(0);    opacity: 1; }
  80%  {                                          opacity: 0.12; }
  100% { transform: scale(1.8) translateY(32px); opacity: 0; }
}

/* ── Debug scroll readout ─────────────────────────────────
   Always-on overlay at the bottom of the viewport showing current
   scroll position in h() units (matches the unit used in JS scroll
   triggers like `start: () => h() * 1.3`). Comment out the
   .debug-scroll element in index.html when you no longer need it. */

.debug-scroll {
  /* Hidden for production launch (2026-05-31). The element + the
     gsap.ticker readout logic in main.js are intentionally KEPT so the
     deferred altitude-readout feature (BUILD_PLAN §9) can reuse them —
     to re-enable for tuning, remove this `display: none`. */
  display: none;
  position: fixed;
  bottom: 8px;
  left: 50%;
  transform: translateX(-50%);
  z-index: 9999;
  padding: 4px 12px;
  background: rgba(0, 0, 0, 0.75);
  color: var(--signal-green);
  font-family: ui-monospace, 'SF Mono', Menlo, Consolas, monospace;
  font-size: 11px;
  line-height: 1;
  letter-spacing: 0.02em;
  border-radius: 4px;
  pointer-events: none;
  white-space: nowrap;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4);
}

/* ── Modal overlay ────────────────────────────────────────
   Generic overlay component. Theme variant via data-theme attribute.
   `.ss-modal.is-open` shows the modal; JS pauses ScrollSmoother on
   open and locks body overflow. Custom <div> rather than native
   <dialog> for full styling control.

   Transmission theme = Broadcast Bulletin. Cream paper with sharp
   corners (telegram/bulletin paper, not a rounded postcard). Sage
   concentric-arc letterhead reuses the same path vocabulary as the
   blimp + ground-dish broadcasts — same icon, same animation, just
   served as a printed mark and a single one-shot pulse on arrival.
   Slides up from below the viewport. Click anywhere on the frame,
   backdrop, or ESC dismisses. */

.ss-modal {
  position: fixed;
  inset: 0;
  z-index: 10000;
  display: flex;
  align-items: center;
  justify-content: center;
  pointer-events: none;
  opacity: 0;
  transition: opacity 240ms ease;
}

.ss-modal.is-open {
  opacity: 1;
  pointer-events: auto;
}

.ss-modal__backdrop {
  position: absolute;
  inset: 0;
  background: rgba(15, 18, 36, 0.62);
  backdrop-filter: blur(3px);
  -webkit-backdrop-filter: blur(3px);
}

.ss-modal__frame {
  position: relative;
  display: flex;
  flex-direction: column;
  width: min(620px, 92vw);
  max-height: min(82vh, 720px);
  /* Theme-overridable. Default = cream-peach (brand-light theme inherits
     this; transmission overrides to pale mint below). */
  background: var(--ss-modal-bg, var(--pal-creampeach));
  border: 2px solid var(--pal-steel-deep);
  box-shadow:
    0 1px 0 rgba(255, 255, 255, 0.6) inset,           /* top paper highlight */
    0 0 0 6px rgba(43, 79, 122, 0.10),                /* soft steel halo */
    0 30px 60px rgba(15, 18, 36, 0.45);
  /* Slide up from below the viewport on open. translateY is large enough
     that the frame is fully off-screen at start, regardless of size. */
  transform: translateY(72vh);
  transition: transform 520ms cubic-bezier(0.18, 0.7, 0.2, 1.04);
  cursor: pointer;          /* whole frame is the dismiss target */
}

/* Transmission theme: pale mint paper (lighter than --pal-mint #D9EFE5),
   ties to the sage-green atom emblem. Distinguishes TH from the
   cream-peach default used by WHAT / WHY. */
.ss-modal[data-theme="transmission"] {
  --ss-modal-bg: #ECF5F0;
}

/* Form theme (CONTACT): powder-tint paper — the "take action" sibling of
   the cream-peach brand-light modals, tied to the cyan building halos.
   The frame is NOT a dismiss target here (clicking fields/buttons must not
   close), so the cursor reverts to default and the ✕ button is shown. */
.ss-modal[data-theme="form"] {
  --ss-modal-bg: #E6F0FA;
  cursor: default;
}

.ss-modal.is-open .ss-modal__frame {
  transform: translateY(0);
}

/* Close button — hidden by default (read-only modals dismiss on any frame
   click). Shown only for the form theme. Steel-deep ✕ that spins a quarter
   turn and warms toward coral on hover. */
.ss-modal__close {
  position: absolute;
  top: 10px;
  right: 12px;
  z-index: 2;
  display: none;
  width: 34px;
  height: 34px;
  padding: 0;
  border: none;
  background: none;
  font-family: 'Space Grotesk', system-ui, sans-serif;
  font-size: 28px;
  line-height: 1;
  color: var(--pal-steel-deep);
  cursor: pointer;
  border-radius: 50%;
  transition: transform 280ms cubic-bezier(0.34, 1.4, 0.5, 1),
              color 200ms ease,
              background-color 200ms ease;
}

.ss-modal[data-theme="form"] .ss-modal__close {
  display: flex;
  align-items: center;
  justify-content: center;
}

.ss-modal__close:hover,
.ss-modal__close:focus-visible {
  transform: rotate(90deg);
  color: var(--pal-coral-deep);
  background-color: rgba(43, 79, 122, 0.08);
  outline: none;
}

/* Embedded Tally form — fills the body width, transparent so the powder
   paper shows through (transparentBackground=1 on the embed URL).
   dynamicHeight=1 + Tally's embed.js drive the height. */
.ss-modal__form-embed {
  display: block;
  width: 100%;
  border: 0;
  margin-top: 4px;
}

/* ── Decorative watermark (opt-in via openModal({ watermark })) ──
   A large faint single-color silhouette of the source SVG, masked so any
   multi-color art becomes a clean mono stencil (crisp at any scale), bled
   off the bottom-right corner and clipped by the frame.
   Rendered with `mix-blend-mode: multiply` over a NEUTRAL fill so it reads
   as ink printed on the paper — it darkens whatever the theme's paper color
   is (tone-on-tone), auto-following any background change rather than adding
   a fixed tint. Two tuning knobs: `background-color` (neutral fill — controls
   the multiply strength/warmth) and `opacity` (overall presence).
   pointer-events:none so it never blocks dismiss. */
.ss-modal.has-watermark .ss-modal__frame {
  overflow: hidden;            /* clip the bleed at the paper edge */
}

.ss-modal.has-watermark .ss-modal__frame::after {
  content: '';
  position: absolute;
  /* Box tracks the frame via insets — height = top→bottom = frame height, so
     the silhouette reaches the header consistently regardless of body length
     (was a fixed 320×309 px box, which fell short on taller modals). The
     12px right gap keeps the building's baked word from clipping off-edge. */
  top: 0;
  left: 0;
  right: 12px;
  bottom: -20px;               /* slight bottom bleed grounds it */
  background-color: #3a3a3a;   /* KNOB 1 — neutral fill; multiply darkens the paper tone-on-tone */
  mix-blend-mode: multiply;    /* blends into the paper (ink-on-paper), follows the bg color */
  -webkit-mask-image: var(--ss-modal-watermark);
          mask-image: var(--ss-modal-watermark);
  -webkit-mask-repeat: no-repeat;
          mask-repeat: no-repeat;
  /* `contain` fits each building within the box preserving its own aspect
     (WHY ≈ square, WHAT tall/narrow) — no distortion, works for any building.
     Anchored bottom-right so it rises from the corner and bleeds off. */
  -webkit-mask-size: contain;
          mask-size: contain;
  -webkit-mask-position: bottom right;
          mask-position: bottom right;
  opacity: 0.06;               /* KNOB 2 — overall presence; lower than the old 0.12 for less contrast */
  pointer-events: none;
  z-index: 0;
}

/* Keep the header/rule/body above the watermark. */
.ss-modal.has-watermark .ss-modal__head,
.ss-modal.has-watermark .ss-modal__rule,
.ss-modal.has-watermark .ss-modal__body {
  position: relative;
  z-index: 1;
}

/* ── Header strip ──
   Sage-arc letterhead + label. Sits flush against top of paper. The
   subtle bottom rule below the header marks the letterhead off from
   the body. */
.ss-modal__head {
  flex: 0 0 auto;
  display: flex;
  flex-direction: row;          /* emblem left of title — letterhead layout */
  align-items: center;
  gap: 14px;
  padding: 16px 24px 14px;
  font-family: 'Space Grotesk', system-ui, sans-serif;
  font-weight: 700;
  font-size: 12px;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--pal-steel-deep);
}

/* Emblem slot — flex item in the header. Children (the SVG atom or the
   letterMark img) get sized 50px tall to match the 2-line title height
   (20px font × 1.25 line-height × 2 lines). */
.ss-modal__emblem-slot {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  height: 50px;
}

.ss-modal__emblem-slot > .ss-modal__emblem {
  /* TH atomic emblem — aspect from viewBox 60/32. Width derived. */
  height: 50px;
  aspect-ratio: 60 / 32;
  overflow: visible;
}

.ss-modal__emblem-slot > .ss-modal__lettermark {
  /* Sticker Star letterMark for WHAT / WHY. Any aspect; rendered at
     50px tall, width auto-derived to preserve aspect. */
  height: 50px;
  width: auto;
  display: block;
}

.ss-modal__arc {
  fill: none;
  stroke: var(--signal-green);
  stroke-width: 2.5;
  stroke-linecap: round;
  transform-box: fill-box;
  transform-origin: 50% 50%;           /* ellipse center = TH logo center */
  opacity: 0;
}

/* Pulse-in: each arc fades in + scales up briefly, then settles to a
   static printed mark. Animation only runs while the modal is open;
   delays staggered ~120ms apart matching the blimp arc cadence. The
   animation-delay includes the slide-up duration (520ms) so arcs land
   AFTER the paper settles. */
.ss-modal.is-open .ss-modal__arc {
  animation: ss-arc-pulse 540ms cubic-bezier(0.2, 0.7, 0.2, 1) both;
}
.ss-modal.is-open .ss-modal__arc-1 { animation-delay: 580ms; }
.ss-modal.is-open .ss-modal__arc-2 { animation-delay: 700ms; }
.ss-modal.is-open .ss-modal__arc-3 { animation-delay: 820ms; }

@keyframes ss-arc-pulse {
  0%   { transform: scale(0.55); opacity: 0; }
  60%  {                          opacity: 1; }
  100% { transform: scale(1);    opacity: 0.95; }
}

.ss-modal__head-label {
  flex: 0 0 auto;
  max-width: 100%;
  font-size: 20px;                                  /* 25% larger than 16px body */
  letter-spacing: 0.12em;                           /* tighter than the 0.18em used at 12px label size */
  line-height: 1.25;                                /* controls spacing for the two-line title */
}

.ss-modal__rule {
  flex: 0 0 auto;
  border: none;
  border-top: 2px solid var(--pal-steel-deep);
  margin: 0 24px;
  height: 0;
}

/* ── Body ──
   Static content (no typewriter). Scrolls if the manifesto exceeds
   max-height — subtle steel-tinted scrollbar. */
.ss-modal__body {
  flex: 1 1 auto;
  padding: 22px 24px 26px;
  overflow-y: auto;
  font-family: 'Space Grotesk', system-ui, sans-serif;
  font-weight: 400;
  font-size: 16px;                                  /* title (20px) is 25% larger */
  line-height: 1.55;
  color: var(--ink);
  scrollbar-width: thin;
  scrollbar-color: rgba(43, 79, 122, 0.32) transparent;
}

.ss-modal__body::-webkit-scrollbar { width: 6px; }
.ss-modal__body::-webkit-scrollbar-thumb {
  background: rgba(43, 79, 122, 0.32);
  border-radius: 3px;
}

.ss-modal__body h2 {
  font-family: 'Space Grotesk', system-ui, sans-serif;
  font-weight: 700;
  font-size: 26px;
  line-height: 1.25;
  letter-spacing: 0.005em;
  color: var(--pal-steel-deep);
  margin-bottom: 18px;
}

.ss-modal__body p {
  margin-bottom: 14px;
}

.ss-modal__body p:last-child {
  margin-bottom: 0;
}

/* Inline TH cross-link used in the WHY modal body. Clicking transitions
   to the TH transmission modal. Styled as a discoverable inline link;
   sage-green on hover to signal its connection to the broadcast. */
.ss-modal__body .th-link {
  color: var(--pal-steel-deep);
  font-weight: 500;
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 3px;
  cursor: pointer;
  transition: color 160ms ease;
}

.ss-modal__body .th-link:hover,
.ss-modal__body .th-link:focus-visible {
  color: var(--signal-green);
  outline: none;
}

/* Benediction — the closing thesis line. Set apart by a top rule +
   extra breathing room + larger italic steel-deep treatment so it
   reads as a takeaway aphorism rather than just the next paragraph. */
.ss-modal__benediction {
  margin-top: 26px !important;
  padding-top: 20px;
  border-top: 1px solid rgba(43, 79, 122, 0.35);
  text-align: center;
  font-weight: 500;
  font-style: italic;
  font-size: 22px;
  line-height: 1.35;
  letter-spacing: 0.005em;
  color: var(--pal-steel-deep);
}

/* Body scroll lock applied when any modal is open. */
body.ss-modal-open {
  overflow: hidden;
}

/* ── Scroll driver ────────────────────────────────────────
   Empty tall element that creates document scroll height. All
   visual layers are position:fixed and animated by GSAP. */

.scroll-driver {
  height: 500vh;  /* max scrollY = 4h (matches end-of-descent triggers) */
  pointer-events: none;
}

/* ── Desktop ──────────────────────────────────────────────── */

@media (min-width: 1100px) {
  .marquee { width: min(960px, 70vw, 80vh); }

  /* Signpost scales up at desktop — no height cap; top of starburst and
     pole base may extend beyond viewport (intentional). */
  .signpost-unit {
    width: min(640px, 55vw);
  }

}
