/* =========================================================================
   Hero v2 — centered stack + full-width transformation scene.
   Selectors prefixed .hero2 / #hero-anim-layer to avoid collisions with the
   legacy .hero block in site.css.
   Tokens consumed: --bg, --pad, --blue, --muted, --sb-ease (tokens.css).
   ========================================================================= */

/* Local fallback in case tokens.css isn't loaded for some reason. */
.hero2 { --sb-ease: var(--sb-ease, cubic-bezier(.4,0,.2,1)); }

.hero2 {
  /* Tight top padding: #stage already provides 200px below the floating nav
     (see styles/nav.css). Negative margin pulls content up closer to the
     pill so the hero doesn't feel like it's hanging in space.
     Bottom padding (128px) preserves the original "below the imagery"
     breathing room that the chevron-row previously occupied. */
  padding: 0 var(--pad) 128px;
  margin-top: -22px;
  background: var(--bg);
  position: relative;
  overflow: hidden;
}
.hero2__copy {
  max-width: 880px;
  margin: 0 auto;
  text-align: center;
}
.hero2__title {
  font: 500 64px/1.06 "Neue Montreal", system-ui, sans-serif;
  letter-spacing: -0.018em;
  color: #000;
  margin: 0;
  text-wrap: balance;
}
.hero2__work { color: var(--blue); }
.hero2__muted-accent { color: #4C6B5A; }

/* First line reads as the headline proper; the second is a supporting
   clause, so it's set a step smaller than the line above it. */
.hero2__line--lg { font-size: 1.2em; }

/* ---------------------------------------------------------------------
   Rotating hero keyword — see components/hero-word-rotator.js.

   .hero2__line pins the headline's line break. Left to itself,
   text-wrap: balance moves "for" between lines as the keyword grows, so the
   whole second line jumps every rotation. At >=641px each line is its own
   nowrap block, so only line 1's width changes and it breathes symmetrically
   about the centered column. Below 641px the spans stay inline and the
   headline wraps exactly as it always has (the rotator is off there — the
   longest word forces a third line at phone widths).

   No overflow clipping anywhere: at rotateX(+/-92deg) a face is edge-on and
   projects to nothing, so nothing needs masking — which also means
   "de-p-lo-y-ment"'s descenders can't be clipped and a word wider than the
   in-flight box isn't cut off on the right while the width catches up.
   --------------------------------------------------------------------- */
.hero2__line { display: inline; }
@media (min-width: 641px) {
  .hero2__line { display: block; white-space: nowrap; }
}

.wrot { white-space: nowrap; }
.wrot--on {
  position: relative;
  display: inline-block;
  width: var(--w);
  vertical-align: baseline;
}
/* Both faces are absolutely positioned, so the inline-block needs something
   in flow to give it a line box of the right height and, crucially, the right
   baseline. A zero-width space in a pseudo-element does that without adding a
   second copy of the word to the h1's textContent or to a text selection. */
.wrot--on::before { content: "\200B"; }
.wrot__mask {
  position: absolute;
  inset: 0;
}
/* Both faces share the mask's box exactly, so they rotate about one common
   axis with one common vanishing point. A word wider than the current box
   simply overhangs both edges until --w catches up. */
.wrot__face {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  text-align: center;
  white-space: nowrap;
  transform-origin: 50% 50%;
  backface-visibility: hidden;
  will-change: transform, opacity;
}
.hero2__sub {
  font: 400 18px/1.45 "Neue Montreal", system-ui, sans-serif;
  letter-spacing: -0.002em;
  color: var(--muted);
  margin: 22px auto 0;
  max-width: 560px;
  /* balance, not pretty: `pretty` only tidies the LAST line, which left the two
     body lines at 528px and 375px. `balance` evens them to 457/447. It can't
     change the line count, so the two-line guarantee below is unaffected, and
     browsers without it just fall back to normal wrapping (still two lines,
     merely less even). */
  text-wrap: balance;
}
/* Lead sentence sits on its own line with a touch more breathing room than a
   normal line break — subtler than paragraph spacing. */
.hero2__sub-lead {
  display: block;
  margin-bottom: 0.4em;
}
.hero2__cta {
  margin-top: 32px;
  display: flex;
  gap: 16px;
  justify-content: center;
  flex-wrap: wrap;
}
.hero2__cta .btn { min-width: 176px; }

.hero2__scene {
  max-width: 1357px;
  /* Breathing room between the copy block and the illustration. 12px left only
     a ~21px visual gap (the SVG carries ~9px of its own top whitespace) against
     the hero's 128px bottom padding, which read as cramped. 64px is half that
     bottom padding, so the scene sits in a balanced well.
     Safe to change: fitStage() scales #stage on VIEWPORT WIDTH only
     (vw / 1440), so extra height cannot shrink the hero. It does recompute the
     helix's compensating margin and the body height cap on resize and load. */
  margin: 64px auto 0;
  padding: 0 var(--pad);
}
.hero2__svg {
  display: block;
  width: 100%;
  height: auto;
  overflow: visible;
}

/* Responsive — aligned with the site's existing breakpoints (1280 / 860 / 480). */
@media (max-width: 1280px) {
  .hero2__title { font-size: 56px; }
  .hero2 { padding: 0 48px 120px; margin-top: -18px; }
  .hero2__scene { padding: 0 24px; }
}
@media (max-width: 860px) {
  .hero2 { padding: 0 22px 96px; margin-top: -10px; }
  .hero2__title { font-size: 42px; line-height: 1.07; letter-spacing: -0.01em; }
  .hero2__sub { font-size: 17px; margin-top: 18px; }
  .hero2__scene { margin-top: 32px; padding: 0 16px; }
}
@media (max-width: 480px) {
  .hero2__title { font-size: 34px; }
}

/* =====================================================================
   Entry-animation states — currently no-op (static composition). The
   .hv-init / .hv-play gate is preserved in the markup for future motion
   work; selectors below force the rest pose so nothing flashes.
   ===================================================================== */
.hero2 .hv-in,
.hero2 .hv-out,
.hero2 .hv-br,
.hero2 .hv-bracket,
.hero2 .hv-tag-anim,
.hero2 .hv-disc-anim {
  opacity: 1;
}
.hero2 .hv-arrow {
  stroke-dasharray: 3 4.5 !important;
  stroke-dashoffset: 0 !important;
  animation: none !important;
}
.hero2 .hv-data {
  stroke-dasharray: none !important;
  stroke-dashoffset: 0 !important;
}

/* Reduced-motion fallback for the static hv-* layer. */
@media (prefers-reduced-motion: reduce) {
  .hero2 * { animation: none !important; opacity: 1 !important; transform: none !important; }
  .hv-arrow { stroke-dashoffset: 12 !important; }
  .hv-data { stroke-dashoffset: 0 !important; }
  .hv-packet { opacity: 0 !important; }
}

/* =====================================================================
   Dot pipeline — dots emit from cubes, travel through Input pill, through
   the disc (picking up their output color), then along a single curve
   through the pill stack into the browser. Their destination pill pulses
   (soft breathing glow) as the dot passes.

   #hero-anim-layer is parked EARLY in the SVG so dots tuck UNDER the
   cubes, disc body, pills, and browser stack — visible only in gaps.
   ===================================================================== */
#hero-anim-layer .dot {
  pointer-events: none;
  stroke: rgba(0, 0, 0, 0.55);
  stroke-width: 0.9;
}
#hero-anim-layer .halo {
  pointer-events: none;
  stroke: none;
  /* Tight enough to read as light coming off the pill's own edge. The old
     10px over a 1.65x expansion smeared into a cloud that had left the pill
     behind. --peak-opacity / --peak-scale are set per-ripple from JS. */
  filter: blur(8px);
  /* `both`, NOT `forwards`. A ripple with animation-delay has no animated value
     until its delay elapses, so under `forwards` it rendered at the element's
     default opacity 1 — an opaque slab flashing behind the pill for the whole
     delay. The backwards fill pins it to the 0% keyframe (opacity 0) instead. */
  animation: heroBreath 2400ms both;
}
/* A breath, not a blip: bloom in over ~310ms, then a long exhale. The decay
   curve starts slow so the glow HOLDS near full before it goes, instead of
   dropping off the moment it peaks.
   --sx / --sy come from JS: per-axis scale derived from a spread in user units,
   so the halo grows the same distance on every side of a long, thin pill. */
@keyframes heroBreath {
  0% {
    transform: scale(1, 1);
    opacity: 0;
    animation-timing-function: cubic-bezier(0.33, 0, 0.24, 1);
  }
  13% {
    transform: scale(calc(1 + (var(--sx, 1.17) - 1) * 0.22),
                     calc(1 + (var(--sy, 1.59) - 1) * 0.22));
    opacity: var(--peak-opacity, 0.62);
    animation-timing-function: cubic-bezier(0.45, 0, 0.5, 1);
  }
  100% {
    transform: scale(var(--sx, 1.17), var(--sy, 1.59));
    opacity: 0;
  }
}

/* Reduced motion: hide the dot layer entirely; static scene reads correctly. */
@media (prefers-reduced-motion: reduce) {
  #hero-anim-layer { display: none; }
}
