/* ============================================================
   01 — HERO
   ============================================================ */

/* <main> carries tabindex="-1" so the skip link and the footer's "back to top"
   move focus, not just scroll. That focus is programmatic and must not paint —
   tokens.css rings :focus-visible with a box-shadow at the same specificity, so
   both properties are cleared here (this sheet loads after tokens). */
.hero-main:focus { outline: none; box-shadow: none; }

.hero {
  /* mint darkened until it clears 4.5:1 on --bg (5.46:1) — one source of truth */
  --hero-mint: color-mix(in srgb, var(--mint-500) 62%, var(--ink-900));
  /* resting stroke for every connector graphic — 3.07:1 on --bg, 3.23:1 on --surface,
     so the diagram still reads when nothing is animating (WCAG 1.4.11) */
  --hero-wire: color-mix(in srgb, var(--accent) 64%, transparent);
  position: relative;
  isolation: isolate;

  /* HEIGHT IS DECIDED HERE, and it is decided by the content plus a designed
     frame — never by a min-height the content cannot fill.

     The previous rule reserved `100svh - header - s-16` and centred the content
     in it. Height the content cannot fill is split evenly top and bottom, and
     the bottom half has nothing to bound it — the header closes the top gap,
     but below the last line there is only more --bg. At 1440×900 that was 109px
     of empty background under the copy landing straight on section 02's own
     --s-20, ~190px of nothing at the seam. The reserved height was the bug, so
     nothing is reserved now: the section is its content plus its padding, at
     every viewport height, and the only band under the copy is one the next
     section immediately claims.

     Padding is height-aware (svh, not --section-y, which is width-driven) so a
     short viewport collapses it instead of pushing the CTAs under the fold, and
     ASYMMETRIC 80/56: more air above the copy than below, so the hero sits on
     its base rather than floating, and the shorter bottom edge hands the seam
     to section 02's padding instead of doubling it.

     Measured, header ends at 128, desktop content 490:
       1280×720  → 587, trust line ends 672 — 48px clear of the fold
       1280×800  → 599, next section starts 727
       1440×900  → 624, next section starts 752
       1440×1080 → 626, and 1920×1080 → 626: one designed height, not a
                   stretched one, once the viewport stops being the constraint */
  padding-block:
    clamp(var(--s-10), 9svh, var(--s-20))
    clamp(var(--s-8), 6svh, var(--s-14));
}

/* ---------- Atmosphere ---------------------------------- */

.hero__atmos {
  position: absolute;
  inset: 0;
  z-index: 0;
  overflow: hidden;
  pointer-events: none;
}

.hero__wash {
  position: absolute;
  display: block;
  border-radius: var(--r-pill);
  filter: blur(var(--s-16));
}

.hero__wash--a {
  inset-block-start: -18%;
  inset-inline-end: -10%;
  width: min(58%, 40rem);
  aspect-ratio: 1.15;
  background: color-mix(in srgb, var(--violet-300) 62%, transparent);
  opacity: .55;
}

.hero__wash--b {
  inset-block-end: -22%;
  inset-inline-end: 18%;
  width: min(42%, 30rem);
  aspect-ratio: 1.3;
  background: color-mix(in srgb, var(--coral-400) 42%, transparent);
  opacity: .32;
}

.hero__dots {
  position: absolute;
  inset: 0;
  background-image: radial-gradient(circle at center, var(--border-strong) 1px, transparent 1.4px);
  background-size: var(--s-6) var(--s-6);
  -webkit-mask-image: radial-gradient(ellipse 62% 58% at 74% 46%, var(--text) 0%, transparent 74%);
  mask-image: radial-gradient(ellipse 62% 58% at 74% 46%, var(--text) 0%, transparent 74%);
  opacity: .7;
}

/* ---------- Layout -------------------------------------- */

.hero__inner {
  position: relative;
  z-index: var(--z-base);
  display: grid;
  gap: var(--s-14);
  justify-items: center;
  text-align: center;
}

/* The headline is sized off this box, not the viewport, so it fits its own column
   in both layouts. `width` is explicit because an inline-size container may not
   take its width from its contents. */
.hero__copy {
  container-type: inline-size;
  display: flex;
  flex-direction: column;
  align-items: center;
  width: 100%;
  max-width: 40rem;
}

@media (min-width: 64rem) {
  .hero__inner {
    grid-template-columns: minmax(0, 1.36fr) minmax(0, 1fr);
    align-items: center;
    gap: var(--s-16);
    justify-items: stretch;
    text-align: start;
  }
  .hero__copy { align-items: flex-start; max-width: none; }

  /* `align-items: center` lines the two BOXES up; it cannot line their mass up,
     and mass is what the eye reads as the centre.

     The copy column front-loads its weight — 160px of black display type in the
     upper half — then trails off into grey lead, buttons and a hairline trust
     note, so at 1440×900 its ink-weighted centroid lands at y≈424 against a box
     centre of 453. The visual is a saturated rectangle flanked by four cards:
     its centroid IS its box centre, y≈457. Box-aligned, the two columns are
     therefore ~33px out and the visual reads as riding low against the
     headline — the columns look like two blocks that happen to share a row.

     A bottom margin on a centred grid item lifts it by half the margin, so
     --s-12 buys the 24px that closes it (433 vs 424, inside a pixel or two of
     the eye's tolerance). It costs no row height — the row is still the 490px
     copy column — and it leaves [data-reveal]'s own transform free, which a
     translate here would not. */
  .hero__visual { margin-block-end: var(--s-12); }
}

/* ---------- Live pill ----------------------------------- */

.hero__pill {
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);
  padding: var(--s-2) var(--s-4) var(--s-2) var(--s-3);
  border-radius: var(--r-pill);
  background: var(--surface);
  box-shadow: inset 0 0 0 1px var(--border), var(--sh-xs), var(--sh-inset-hair);
  font-size: var(--fs-xs);
  line-height: var(--lh-normal);
  color: var(--text-muted);
  letter-spacing: var(--tracking-normal);
  white-space: nowrap;
}

.hero__live {
  position: relative;
  flex: none;
  width: var(--s-2);
  height: var(--s-2);
  border-radius: var(--r-pill);
  background: var(--hero-mint);
}

.hero__live::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background: inherit;
  animation: hero-ping 2.6s var(--ease-out) infinite;
}

/* Tabular figures + a reserved box stop the count-up from reflowing the pill.
   Left-aligned so the slack sits beside the following words and reads as tracking,
   not as a hole between the live dot and the number. */
.hero__count {
  display: inline-block;
  min-width: 4.6ch;
  text-align: start;
  font-variant-numeric: tabular-nums;
  font-weight: var(--fw-bold);
  color: var(--text);
}

.hero__pill-text { min-width: 0; }

@media (max-width: 24.9375rem) {
  .hero__pill { white-space: normal; text-align: start; }
}

/* ---------- Headline ------------------------------------ */

.hero__title {
  --line-step: 90ms;
  margin-top: var(--s-8);
  /* Fallback for engines without container queries. Sized off the viewport it
     cannot know the copy column is only ~50% of it at the 64rem breakpoint, so
     it is set low enough that the longest unbreakable token ("everywhere.",
     ~6.1em) always fits: 6.4vw is 65.5px in a 514px column at 1024. The
     headline wraps to three lines there instead of overflowing — the right
     trade for a pre-2023 engine. The old 13.4vw resolved to the full 100px
     --fs-display cap and pushed "everywhere." past the column edge. */
  font-size: min(var(--fs-display), 6.4vw);
  /* "Land everywhere." is the long line at ~8.5em. 11.7cqi fitted it with under
     1px to spare at every width — one rounding step, zoom level or hinting
     difference away from wrapping to three lines and shoving the CTAs down.
     11.2cqi is the same headline with ~20px of slack at the tightest width
     (1024, where the copy column is narrowest at ~514px). */
  font-size: min(var(--fs-display), 11.2cqi);
  line-height: var(--lh-tight);
  letter-spacing: var(--tracking-display);
  font-weight: var(--fw-black);
}

.hero__line {
  display: block;
  text-wrap: pretty;
  animation: hero-line var(--dur-slower) var(--ease-expo) both;
  animation-delay: calc((var(--i, 0) + 0.7) * var(--line-step));
}

/* the shared underlay is tuned for body copy; at display size it needs to be
   a rule, not a slab */
.hero__title .mark::after {
  height: .22em;
  bottom: .10em;
  left: -.03em;
  right: -.03em;
  opacity: .22;
}

/* ---------- Lead + actions ------------------------------ */

.hero__lead {
  margin-top: var(--s-6);
  max-width: 52ch;
  font-size: var(--fs-lead);
  line-height: var(--lh-normal);
  color: var(--text-muted);
}

.hero__actions {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--s-3);
  margin-top: var(--s-10);
}

@media (min-width: 64rem) {
  .hero__actions { justify-content: flex-start; }
}

/* tokens.css sets the ring on :focus-visible, but .btn--primary / .btn--secondary
   re-declare box-shadow at equal specificity and later in source order, so the ring
   has to be restated here alongside each button's own shadow. */
.hero__actions .btn:focus-visible {
  border-radius: var(--r-pill);
  box-shadow: var(--focus-ring);
}
.hero__actions .btn--primary:focus-visible {
  box-shadow: var(--focus-ring), var(--sh-accent);
}
.hero__actions .btn--secondary:focus-visible {
  box-shadow: var(--focus-ring), inset 0 0 0 1px var(--border-strong);
}

/* below ~344px the large secondary CTA is wider than the container gutter allows */
@media (max-width: 21.5rem) {
  .hero__actions .btn { padding-inline: var(--s-5); font-size: var(--fs-sm); }
}


.hero__trust {
  margin-top: var(--s-5);
  font-size: var(--fs-sm);
  line-height: var(--lh-normal);
  color: var(--text-muted);
}

.hero__tick {
  display: inline-block;
  vertical-align: -0.2em;
  margin-inline-end: var(--s-2);
  color: var(--hero-mint);
}

/* ============================================================
   Hero visual
   ============================================================ */

.hero__visual {
  --cycle: 10s;
  --stagger: .62s;
  width: 100%;
  /* without this the tiles' min-content pushes the whole visual out of the container gutter */
  min-width: 0;
  max-width: 30rem;
  margin-inline: auto;
}

@media (min-width: 48rem) {
  .hero__visual { max-width: 34rem; }
}

@media (min-width: 64rem) {
  .hero__visual { max-width: none; margin-inline: 0; }
}

/* Three columns: source | connector channel | destinations.
   The channel is a real column, so the wires never have to guess where the
   tiles are — their box IS the gap.

   The channel is deliberately wide. The rail spans the full tile stack, so a
   narrow channel forces the outer wires up at 5:1 and they read as hooks rather
   than a fan. Every breakpoint below keeps channel width above ~40% of the rail's
   vertical span, which lands the outer curves near 2:1. */
.hero__stage {
  --src-col: 0.62fr;
  --chan-col: 0.46fr;
  --tile-gap: var(--s-3);
  position: relative;
  display: grid;
  grid-template-columns: minmax(0, var(--src-col)) minmax(0, var(--chan-col)) minmax(0, 1fr);
  column-gap: 0;
  /* top: room for the "Captions kept" chip above the first tile.
     bottom: the source figcaption is out of flow and hangs past the grid row. */
  padding-block: var(--s-10);
  font-size: var(--fs-xs);
}

@media (min-width: 30rem) {
  .hero__stage {
    --src-col: 0.74fr;
    --chan-col: 0.56fr;
    font-size: var(--fs-sm);
  }
}

@media (min-width: 48rem) {
  .hero__stage { --src-col: 0.86fr; --chan-col: 0.66fr; }
}

/* two-column hero: the visual now shares the row, so the source frame gives width
   back to the tiles until there is enough room to restore it */
@media (min-width: 64rem) {
  .hero__stage { --src-col: 0.62fr; --chan-col: 0.52fr; }
}

/* Once there is room, the source frame — not the tile stack — sets the stage
   height. At 0.78fr the frame was 298px against a 309px stack, so the visual's
   ink was ~100px shorter than the copy column beside it and the pair read as
   two unrelated blocks floating at different scales. 0.9fr puts the frame at
   ~338px, the stack stretches to match, and the two columns come within ~35px
   of each other. Channel stays at 43% of the rail span, inside the >40% floor. */
@media (min-width: 78rem) {
  .hero__stage { --src-col: 0.9fr; --chan-col: 0.54fr; }
}

/* ---------- Connector channel --------------------------- */

.hero__wires {
  position: relative;
  z-index: calc(var(--z-base) + 1);
}

/* The rail spans exactly first-tile-centre → last-tile-centre, derived from the
   same gap token the tile grid uses. Endpoints are therefore 0 / 33.3 / 66.7 / 100
   in the SVG's own coordinates at every width, and its midpoint is the source port. */
.hero__rail {
  position: absolute;
  inset-inline: 0;
  inset-block: calc((100% - 3 * var(--tile-gap)) / 8);
}

.hero__flow {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  overflow: visible;
}

.hero__wire,
.hero__pulse {
  fill: none;
  vector-effect: non-scaling-stroke;
  stroke-linecap: round;
}

.hero__wire {
  stroke: var(--hero-wire);
  stroke-width: 2;
}

.hero__pulse {
  stroke: var(--accent);
  stroke-width: 3;
  stroke-dasharray: 14 86;
  stroke-dashoffset: 14;
  opacity: 0;
  animation: hero-pulse var(--cycle) linear infinite backwards;
  animation-delay: calc(var(--i, 0) * var(--stagger));
}

/* origin port — sits on the source frame's midline by construction */
.hero__port {
  position: absolute;
  inset-inline-start: 0;
  inset-block-start: 50%;
  width: var(--s-3);
  height: var(--s-3);
  translate: -50% -50%;
  border-radius: var(--r-pill);
  background: var(--surface);
  box-shadow: inset 0 0 0 2px var(--accent);
}

.hero__port::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background: var(--accent);
  opacity: 0;
  animation: hero-emit var(--cycle) var(--ease-out) infinite backwards;
}

/* landing nubs — outside the tiles, so nothing clips them */
.hero__nub {
  position: absolute;
  inset-inline-end: 0;
  inset-block-start: calc(var(--i, 0) * 100% / 3);
  width: var(--s-1);
  height: var(--s-6);
  translate: 50% -50%;
  border-radius: var(--r-pill);
  background: var(--hero-wire);
}

.hero__nub::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background: var(--accent);
  opacity: 0;
  animation: hero-land var(--cycle) var(--ease-out) infinite backwards;
  animation-delay: calc(var(--i, 0) * var(--stagger));
}

/* ---------- Source card --------------------------------- */

.hero__source {
  position: relative;
  z-index: var(--z-base);
  align-self: center;
}

.hero__frame {
  position: relative;
  aspect-ratio: 9 / 16;
  border-radius: var(--r-md);
  overflow: hidden;
  background: var(--grad-ink);
  box-shadow: inset 0 0 0 1px var(--border-strong), var(--sh-lg);
}

.hero__thumb {
  position: absolute;
  inset: 0;
  background:
    radial-gradient(60% 42% at 26% 22%, color-mix(in srgb, var(--coral-400) 88%, transparent), transparent 70%),
    radial-gradient(58% 44% at 82% 74%, color-mix(in srgb, var(--violet-300) 92%, transparent), transparent 72%),
    var(--grad-brand);
  animation: hero-drift var(--cycle) var(--ease-in-out) infinite;
}

.hero__thumb::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(180deg, transparent 44%, color-mix(in srgb, var(--ink-950) 62%, transparent) 100%);
}

.hero__ratio-badge {
  position: absolute;
  inset-block-start: var(--s-2);
  inset-inline-start: var(--s-2);
  padding: 0 var(--s-2);
  border-radius: var(--r-xs);
  background: color-mix(in srgb, var(--ink-950) 52%, transparent);
  color: var(--text-on-dark);
  font-family: var(--font-mono);
  font-size: var(--fs-eyebrow);
  line-height: var(--lh-relaxed);
  letter-spacing: 0;
}

.hero__wm {
  position: absolute;
  inset-block-end: calc(var(--s-6) + var(--s-1));
  inset-inline-end: var(--s-2);
  padding: 0 var(--s-2);
  border-radius: var(--r-xs);
  background: color-mix(in srgb, var(--surface) 22%, transparent);
  color: var(--text-on-dark);
  font-size: var(--fs-eyebrow);
  line-height: var(--lh-relaxed);
  opacity: 0;
  transform: scale(.92);
  animation: hero-wm var(--cycle) var(--ease-out) infinite;
}

.hero__scrub {
  position: absolute;
  inset-inline: var(--s-2);
  inset-block-end: var(--s-2);
  height: var(--s-1);
  border-radius: var(--r-pill);
  background: color-mix(in srgb, var(--surface) 28%, transparent);
  overflow: hidden;
}

.hero__playhead {
  display: block;
  height: 100%;
  border-radius: inherit;
  background: var(--surface);
  transform-origin: left center;
  transform: scaleX(1);
  animation: hero-scrub var(--cycle) linear infinite;
}

/* Out of flow, so the figure's box is exactly the frame's box. That is what makes
   `align-self: center` put the frame's midline on the stage's midline — and the
   connector origin with it — whichever column happens to be taller. */
.hero__srcline {
  position: absolute;
  inset-inline: 0;
  inset-block-start: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--s-2);
  margin-top: var(--s-3);
  font-size: var(--fs-eyebrow);
  font-weight: var(--fw-semibold);
  letter-spacing: var(--tracking-normal);
  color: var(--text-muted);
  text-align: center;
  text-wrap: balance;
}

.hero__srcline svg { flex: none; color: var(--text-muted); }

/* ---------- Destination tiles --------------------------- */

.hero__tiles {
  position: relative;
  z-index: var(--z-base);
  display: grid;
  grid-template-rows: repeat(4, 1fr);
  gap: var(--tile-gap);
  margin: 0;
}

.hero__tile {
  position: relative;
  overflow: hidden;
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  align-items: center;
  align-content: center;
  column-gap: var(--s-3);
  row-gap: var(--s-1);
  padding: var(--s-3);
  border-radius: var(--r-md);
  background: var(--surface);
  box-shadow: inset 0 0 0 1px var(--border), var(--sh-sm);
}

.hero__glyph {
  grid-row: span 2;
  display: grid;
  place-items: center;
  width: var(--s-8);
  height: var(--s-8);
  border-radius: var(--r-sm);
  background: var(--accent-soft);
  color: var(--accent);
}

.hero__glyph svg { width: 60%; height: auto; }

.hero__meta {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--s-2);
  min-width: 0;
}

.hero__name {
  font-size: var(--fs-xs);
  font-weight: var(--fw-semibold);
  color: var(--text);
  letter-spacing: var(--tracking-normal);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.hero__ratio {
  display: inline-flex;
  align-items: center;
  gap: var(--s-1);
  flex: none;
  font-family: var(--font-mono);
  font-size: var(--fs-eyebrow);
  color: var(--text-muted);
}

/* filled, not outlined — a 12px hairline outline at --border-strong is 1.4:1 on
   --surface and simply disappears */
.hero__ar {
  display: block;
  height: var(--s-3);
  aspect-ratio: var(--ar, 1);
  border-radius: calc(var(--r-xs) / 2);
  background: color-mix(in srgb, var(--accent) 18%, var(--surface));
  box-shadow: inset 0 0 0 1px var(--hero-wire);
}

/* Right at the two-column switch the visual loses ~170px in one step (it stops
   being a centred 34rem block and becomes a 1fr column), and the tile ends up
   6px short: "YouTube" ellipsises against the widest ratio label, "16:9". The
   swatch is a decorative restatement of text that is already beside it, so it
   goes until the column can afford it — the same trade this sheet already
   makes below 30rem. */
@media (min-width: 64rem) and (max-width: 68rem) {
  .hero__ar { display: none; }
}

.hero__state {
  grid-column: 2;
  display: grid;
  justify-items: start;
  min-width: 0;
}

.hero__st {
  grid-area: 1 / 1;
  display: inline-flex;
  align-items: center;
  gap: var(--s-1);
  font-size: var(--fs-eyebrow);
  font-weight: var(--fw-medium);
  line-height: var(--lh-normal);
  white-space: nowrap;
}

.hero__st svg { flex: none; }

/* `backwards` matters: without it the base style shows through the stagger delay
   and tiles 1–3 would display "Published" for up to 1.86s before flipping back
   to "Reframing". Base styles are the no-animation resting state (published). */
.hero__st--work {
  color: var(--text-muted);
  opacity: 0;
  transform: translateY(-.3em);
  animation: hero-work var(--cycle) var(--ease-out) infinite backwards;
  animation-delay: calc(var(--i, 0) * var(--stagger));
}

.hero__st--done {
  color: var(--hero-mint);
  font-weight: var(--fw-semibold);
  opacity: 1;
  animation: hero-done var(--cycle) var(--ease-out) infinite backwards;
  animation-delay: calc(var(--i, 0) * var(--stagger));
}

.hero__bar {
  position: absolute;
  inset-inline: 0;
  inset-block-end: 0;
  height: calc(var(--s-1) / 2);
  background: var(--accent-soft);
  opacity: 0;
  animation: hero-barbg var(--cycle) linear infinite backwards;
  animation-delay: calc(var(--i, 0) * var(--stagger));
}

.hero__bar-fill {
  display: block;
  height: 100%;
  background: var(--grad-brand);
  transform-origin: left center;
  transform: scaleX(1);
  animation: hero-bar var(--cycle) linear infinite backwards;
  animation-delay: calc(var(--i, 0) * var(--stagger));
}

/* ---------- Floating notes ------------------------------ */

.hero__note {
  position: absolute;
  z-index: calc(var(--z-base) + 2);
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);
  padding: var(--s-2) var(--s-3);
  border-radius: var(--r-pill);
  background: var(--surface);
  box-shadow: inset 0 0 0 1px var(--border), var(--sh-md), var(--sh-inset-hair);
  font-size: var(--fs-eyebrow);
  font-weight: var(--fw-semibold);
  line-height: 1;
  color: var(--text);
  white-space: nowrap;
  opacity: 0;
  animation: hero-note var(--cycle) var(--ease-out) infinite backwards;
  animation-delay: calc(var(--i, 0) * var(--stagger));
}

.hero__note svg { flex: none; color: var(--accent); }
.hero__note--a svg { color: var(--accent-2); }

/* pinned inside the source media's lower-left corner */
.hero__note--a {
  inset-inline-start: 0;
  inset-block-end: var(--s-10);
}

/* lives in the stage's top padding, clear of the first tile at every width */
.hero__note--b {
  inset-inline-end: 0;
  inset-block-start: 0;
}

@media (min-width: 30rem) {
  .hero__note--a { translate: -10% 0; }
}

@media (max-width: 21.5rem) {
  /* the callout would otherwise reach the first destination tile */
  .hero__note--a svg { display: none; }
}

@media (max-width: 29.9375rem) {
  .hero__note { padding: var(--s-1) var(--s-2); }
  .hero__note--a { inset-block-end: var(--s-6); }
  /* the handle is wider than the phone frame at this size and would clip */
  .hero__wm { font-size: calc(var(--fs-eyebrow) * 0.8); padding-inline: var(--s-1); }
  .hero__tile { padding: var(--s-2); column-gap: var(--s-2); }
  .hero__glyph { width: var(--s-6); height: var(--s-6); border-radius: var(--r-xs); }
  .hero__ar { display: none; }
  .hero__srcline svg { display: none; }
}

/* ============================================================
   Keyframes — all transform / opacity / dash only
   ============================================================ */

@keyframes hero-line {
  from { opacity: 0; transform: translate3d(0, .34em, 0); }
  to   { opacity: 1; transform: none; }
}

@keyframes hero-ping {
  0%        { transform: scale(1);   opacity: .55; }
  70%, 100% { transform: scale(2.8); opacity: 0; }
}

@keyframes hero-emit {
  0%, 1%    { opacity: 0; transform: scale(1); }
  4%        { opacity: .9; transform: scale(1); }
  22%, 100% { opacity: 0; transform: scale(2.2); }
}

@keyframes hero-pulse {
  0%        { stroke-dashoffset: 100; opacity: 0; }
  4%        { opacity: 1; }
  28%       { stroke-dashoffset: 20; opacity: 1; }
  32%, 100% { stroke-dashoffset: 14; opacity: 0; }
}

@keyframes hero-land {
  0%, 24%   { opacity: 0; transform: scaleY(.5); }
  30%, 94%  { opacity: 1; transform: scaleY(1); }
  100%      { opacity: 0; transform: scaleY(.5); }
}

/* t=0 is the start of the work phase, so a queued tile held at the 0% frame by
   `backwards` reads "Reframing" — the story only ever runs forwards. The 96–100%
   tail crossfades back to the 0% frame so the loop has no visible seam. */
@keyframes hero-work {
  0%, 30%   { opacity: 1; transform: none; }
  33%, 96%  { opacity: 0; transform: translateY(-.3em); }
  100%      { opacity: 1; transform: none; }
}

@keyframes hero-done {
  0%, 31%   { opacity: 0; transform: translateY(.3em); }
  34%, 96%  { opacity: 1; transform: none; }
  100%      { opacity: 0; transform: translateY(.3em); }
}

@keyframes hero-barbg {
  0%        { opacity: 0; }
  3%, 30%   { opacity: 1; }
  33%, 100% { opacity: 0; }
}

@keyframes hero-bar {
  0%        { transform: scaleX(0); }
  30%, 100% { transform: scaleX(1); }
}

@keyframes hero-scrub {
  0%        { transform: scaleX(0); }
  92%, 100% { transform: scaleX(1); }
}

@keyframes hero-wm {
  0%, 1%    { opacity: 0; transform: scale(1); }
  4%, 12%   { opacity: .8; transform: scale(1); }
  18%, 100% { opacity: 0; transform: scale(.92); }
}

@keyframes hero-drift {
  0%, 100% { transform: scale(1.04) translate3d(0, 0, 0); }
  50%      { transform: scale(1.07) translate3d(-1.6%, -1.2%, 0); }
}

@keyframes hero-note {
  0%, 6%    { opacity: 0; transform: translateY(.5em) scale(.96); }
  18%, 94%  { opacity: 1; transform: none; }
  100%      { opacity: 0; transform: translateY(.5em) scale(.96); }
}

/* ============================================================
   Pause when off-screen  /  reduced motion
   ============================================================ */

.hero__visual.is-paused,
.hero__visual.is-paused * { animation-play-state: paused; }
/* the universal selector above cannot reach pseudo-elements, and five of the
   infinite loops live on them (hero-emit, hero-land x4) */
.hero__visual.is-paused *::before,
.hero__visual.is-paused *::after { animation-play-state: paused; }

@media (prefers-reduced-motion: reduce) {
  .hero__line { animation: none; opacity: 1; transform: none; }
  .hero__live::after { display: none; }
  .hero__visual,
  .hero__visual * { animation: none !important; }
  .hero__st--work { opacity: 0; }
  .hero__st--done { opacity: 1; transform: none; }
  .hero__bar { opacity: 0; }
  .hero__wm { opacity: 0; }
  .hero__playhead { transform: scaleX(1); }
  /* freeze on the finished state: each packet parked at its destination
     (dasharray 14 86 + offset 14 puts the dash on the last 14% of the path) */
  .hero__pulse { opacity: 1; stroke-dashoffset: 14; }
  /* `.hero__visual *` above cannot reach pseudo-elements */
  .hero__port::after,
  .hero__nub::after { animation: none; }
  .hero__port::after { opacity: 0; }
  .hero__nub::after { opacity: 1; transform: none; }
  .hero__note { opacity: 1; transform: none; }
  .hero__thumb { transform: scale(1.04); }
}
