/* ═══════════════════════════════════════════════════════════
   PIPER, fx-motion.css
   The motion layer only. Entrances, scroll-driven reveals, hover
   and focus states, transition curves. Loads after styles.css so
   it can retime and refine what is already there without owning
   color, type, grid or copy. Every selector below targets a class
   or id that already exists in index.html or styles.css.

   Two rules this whole file obeys. First, nothing here plays on a
   loop unless it is either a) gated under prefers-reduced-motion:
   no-preference, or b) a genuinely static hover transition, the
   same tolerance styles.css already extends to its own color
   transitions. Second, background-position is never animated on a
   full-viewport layer. That triggers a repaint plus a re-blend on
   every step, which is not the compositor-safe motion the brief
   asks for no matter what the animation is named. Every loop below
   moves with transform or opacity only.
   ═══════════════════════════════════════════════════════════ */

:root {
  /* A second, springier curve reserved for one-shot confirmations
     (copy, deck press) so those reads as a distinct register from
     the settle-in curve everything else uses. */
  --fx-ease-snap: cubic-bezier(.34, 1.56, .64, 1);
  --fx-dur-instrument: .7s;
}

/* ── GLOBAL INTERACTION DEFAULTS ─────────────────────────────
   Small transform lifts on the controls that only had color and
   border transitions before. Kept under 2px so they read as
   weight, not bounce, and are cheap enough to leave ungated, the
   same tolerance styles.css already gives its own hover colors. */
.btn,
.nav__tg,
.deck__btn,
.foot__social-link,
.term__link,
.market__src,
.tape__all {
  transition:
    transform .18s var(--ease),
    color .2s var(--ease),
    background-color .2s var(--ease),
    border-color .2s var(--ease);
}

.btn:hover:not(:active) { transform: translateY(-1px); }
.nav__tg:hover { transform: translateY(-1px); }
.deck__btn:hover { transform: translateY(-2px); }
.deck__btn:active { transform: translateY(0) scale(.92); }
.foot__social-link:hover { transform: translateY(-2px); }

/* The arrow-out icons nudge toward the direction they point,
   which is the same feedback idea as a link underline but reads
   correctly on financial-chrome rows that do not carry underlines. */
.term__link i,
.market__src i,
.tape__all i,
.vrow__name i {
  transition: transform .25s var(--ease);
}
.term__link:hover i,
.market__src:hover i,
.tape__all:hover i,
.vrow:hover .vrow__name i {
  transform: translate(2px, -2px);
}

/* ── NAV ──────────────────────────────────────────────────────
   An underline that draws in from the left on hover, transform
   only, replacing the plain color swap with something that reads
   as a designed state rather than a browser default. */
.nav__links a {
  position: relative;
}
.nav__links a::after {
  content: "";
  position: absolute;
  left: 0;
  right: 100%;
  bottom: -4px;
  height: 1px;
  background: var(--accent);
  transition: right .3s var(--ease);
}
.nav__links a:hover::after { right: 0; }

/* The drawer already toggles the [hidden] attribute in app.js.
   transition-behavior: allow-discrete lets a transition run across
   that display swap; where it is unsupported the drawer still
   opens instantly, which is exactly the same fallback the intro
   curtain already relies on for a blocked script. */
@supports (transition-behavior: allow-discrete) {
  @media (prefers-reduced-motion: no-preference) {
    .nav__drawer {
      transition: opacity .32s var(--ease), transform .32s var(--ease), display .32s allow-discrete;
      opacity: 1;
      transform: none;
    }
    .nav__drawer[hidden] { display: none; }
    @starting-style {
      .nav__drawer:not([hidden]) { opacity: 0; transform: translateY(-10px); }
    }

    .nav__drawer a { transition: opacity .28s var(--ease), transform .28s var(--ease); }
    .nav__drawer:not([hidden]) a { opacity: 1; transform: none; }
    @starting-style {
      .nav__drawer:not([hidden]) a { opacity: 0; transform: translateY(-6px); }
    }
    .nav__drawer a:nth-child(1) { transition-delay: .02s; }
    .nav__drawer a:nth-child(2) { transition-delay: .05s; }
    .nav__drawer a:nth-child(3) { transition-delay: .08s; }
    .nav__drawer a:nth-child(4) { transition-delay: .11s; }
    .nav__drawer a:nth-child(5) { transition-delay: .14s; }
  }
}

/* ── INTRO ────────────────────────────────────────────────────
   The exit becomes an aperture closing on itself rather than a
   flat wipe. Starting radius comfortably exceeds the viewport
   diagonal at any aspect ratio so the curtain is still a full
   opaque cover at 0%. Retimed earlier than the base curtain so it
   is still moving while the hero content below resolves, instead
   of finishing on a page that already stopped moving. */
@media (prefers-reduced-motion: no-preference) {
  .js .intro {
    animation: fx-intro-iris 1.05s var(--ease) 1.15s forwards;
  }
}
@keyframes fx-intro-iris {
  from { clip-path: circle(150% at 50% 42%); }
  to   { clip-path: circle(0% at 50% 42%); visibility: hidden; pointer-events: none; }
}

/* The loading number never actually counted, it just sat at 5.5
   under a bar that filled. A left-to-right reveal locked to the
   exact duration and delay of the rule fill makes the number look
   like it is arriving with the bar instead of having been there
   the whole time. Real digit counting needs the .count class on
   #introNum in the markup; this is the version available from CSS
   alone and it fixes the same complaint: right now, nothing moves. */
@media (prefers-reduced-motion: no-preference) {
  .js .intro__meter #introNum {
    display: inline-block;
    clip-path: inset(0 100% 0 0);
    animation: fx-intro-count 1.15s var(--ease) .55s forwards;
  }
}
@keyframes fx-intro-count { to { clip-path: inset(0 0 0 0); } }

/* ── ATMOSPHERE ───────────────────────────────────────────────
   The aura already drifts on a scroll timeline. Adding a small
   horizontal component keeps it a single light source, just one
   that wanders instead of only falling straight down. */
@media (prefers-reduced-motion: no-preference) {
  @supports (animation-timeline: scroll()) {
    .aura { will-change: transform; }
  }
}
@keyframes aura-drift {
  to { transform: translate3d(-4vw, 14vh, 0); opacity: .55; }
}

/* The grain layer was static. Breathing life into it without a
   repaint means moving the element itself with transform, never
   background-position. The box is oversized well past the fixed
   viewport frame so a translate of a few pixels never uncovers a
   seam at the edge, and the tile repeats so the pattern reads as
   continuous the whole time it drifts. */
.grain {
  inset: -32px;
  background-repeat: repeat;
}
@media (prefers-reduced-motion: no-preference) {
  .grain {
    will-change: transform;
    animation: fx-grain-crawl 11s steps(8) infinite;
  }
}
@keyframes fx-grain-crawl {
  0%   { transform: translate3d(0, 0, 0); }
  25%  { transform: translate3d(-9px, 6px, 0); }
  50%  { transform: translate3d(7px, -8px, 0); }
  75%  { transform: translate3d(-6px, -5px, 0); }
  100% { transform: translate3d(0, 0, 0); }
}

/* ── HERO ─────────────────────────────────────────────────────
   The whole entrance is retimed as one sequence instead of the
   base's separate short delays, so the copy resolves while the
   curtain is still opening rather than sitting finished and still
   underneath it. The keyframe names below are new; the geometry
   they animate to still matches what styles.css declares as each
   element's resting state, so nothing jumps when the animation
   hands off to the static rule. */
@media (prefers-reduced-motion: no-preference) {
  .hero__title.enter    { animation: enter-rise .85s var(--ease) 1.05s both; }
  .hero__sub.enter      { animation: enter-rise .85s var(--ease) 1.2s both; }
  .hero__cta.enter      { animation: enter-rise .85s var(--ease) 1.35s both; }
  .hero__asset.enter--asset { animation: enter-lift 1s var(--ease) 1.1s both; }

  /* She lands last, then keeps a small idle drift once she has
     settled. This is the one continuous loop on a living creature
     on the page, and it earns its keep: a static cutout of a
     newborn animal reads as a sticker, a very slightly breathing
     one reads as alive. Amplitude stays under 6px. */
  .hero__break.enter--break {
    animation:
      fx-break-in 1.25s var(--ease) 1.15s both,
      fx-hero-idle 6.4s ease-in-out 2.5s infinite;
  }
}
@keyframes fx-break-in {
  from { opacity: 0; transform: translateX(-58%) translateY(9%) rotate(-8deg) scale(.94); }
  to   { opacity: 1; transform: translateX(-58%) rotate(-3deg); }
}
@keyframes fx-hero-idle {
  0%, 100% { transform: translateX(-58%) rotate(-3deg) translateY(0); }
  50%      { transform: translateX(-58%) rotate(-2.4deg) translateY(-6px); }
}

/* The readout tile arrives after the asset has lifted into place,
   never before, so it reads as instrumentation landing on top of
   the footage rather than two unrelated things appearing at once. */
@media (prefers-reduced-motion: no-preference) {
  .quote-tile {
    animation: fx-tile-in .7s var(--ease) 1.75s both;
  }
}
@keyframes fx-tile-in {
  from { opacity: 0; transform: translateY(10px); }
  to   { opacity: 1; transform: none; }
}

/* Breaking out of a frame needs a frame. Below 860px styles.css
   hides her entirely rather than solve that, which drops the one
   signature move of the page for most of the site's traffic. This
   brings her back as a smaller corner cutout anchored to a fixed
   edge instead of the percentage math built for a two-column grid,
   which is what makes it hold up at narrow widths. */
@media (max-width: 860px) {
  .hero__break {
    display: block;
    left: auto;
    right: clamp(.5rem, 5vw, 1.25rem);
    bottom: -1.25rem;
    width: clamp(120px, 34vw, 190px);
    transform: none;
    filter:
      drop-shadow(0 10px 18px rgb(0 0 0 / .45))
      drop-shadow(0 26px 46px rgb(0 0 0 / .35));
  }

  @media (prefers-reduced-motion: no-preference) {
    .hero__break.enter--break {
      animation:
        fx-break-in-mobile 1s var(--ease) 1.1s both,
        fx-hero-idle-mobile 6.4s ease-in-out 2.3s infinite;
    }
  }
}
@keyframes fx-break-in-mobile {
  from { opacity: 0; transform: translateY(16px) rotate(-6deg) scale(.92); }
  to   { opacity: 1; transform: none; }
}
@keyframes fx-hero-idle-mobile {
  0%, 100% { transform: translateY(0); }
  50%      { transform: translateY(-5px); }
}

/* ── MARKET (live readout rail) ──────────────────────────────
   Chart, pressure bar and transfer tape currently just appear with
   the rest of the page. A view-timeline entrance gives the rail
   its own small arrival without any JS or class dependency, and it
   fails safe: outside @supports the animation never registers, so
   the elements simply render in their normal visible state. */
@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    .chart,
    .pressure,
    .tape {
      animation: fx-instrument-in var(--fx-dur-instrument) var(--ease) both;
      animation-timeline: view();
      animation-range: entry 0% entry 55%;
    }
  }
}
@keyframes fx-instrument-in {
  from { opacity: 0; transform: translateY(20px); }
  to   { opacity: 1; transform: none; }
}

/* Direction changes are a real state change on the page, the
   number itself just moved. A quick opacity pulse on whichever
   class newly applies mirrors the same "it moved because the data
   moved" logic the transfer tape already uses for is-new rows. */
.stat__value { transition: color .3s var(--ease); }
@media (prefers-reduced-motion: no-preference) {
  .stat__value.is-up,
  .stat__value.is-down {
    animation: fx-stat-pulse .5s var(--ease);
  }
}
@keyframes fx-stat-pulse {
  from { opacity: .35; }
  to   { opacity: 1; }
}

.tape__live,
.tape__live::before {
  transition: color .3s var(--ease), background-color .3s var(--ease);
}

/* ── LOG (quote and receipt) ─────────────────────────────────
   Both carry their own reveal already. Swapping the generic rise
   for the same print-wipe styles.css uses on section titles gives
   the editorial half of the page one consistent "printed" idiom
   instead of borrowing a different one for every card. */
@media (prefers-reduced-motion: no-preference) {
  .js .receipt.reveal,
  .js .log__quote.reveal {
    opacity: 1;
    transform: none;
    clip-path: inset(0 0 100% 0);
    transition: clip-path .9s var(--ease) var(--d, 0ms);
  }
  .js .receipt.reveal.in,
  .js .log__quote.reveal.in {
    clip-path: inset(0);
  }

  /* The perforation only makes sense once the receipt itself has
     printed in, so it draws after, not alongside. */
  .js .receipt.reveal .receipt__tear { transform: scaleX(0); }
  .js .receipt.reveal.in .receipt__tear {
    animation: fx-tear-draw .5s var(--ease) calc(var(--d, 0ms) + .5s) forwards;
  }
}
@keyframes fx-tear-draw { to { transform: scaleX(1); } }

/* ── THE FILE (bento) ────────────────────────────────────────
   A slow photographic drift on hover, the one place on the page a
   still photo is allowed to move under a pointer. */
.cell--photo img { transition: transform .7s var(--ease); }
.cell--photo:hover img { transform: scale(1.05); }

/* ── CAREERS (deck) ───────────────────────────────────────────
   Same idea as the bento photo, applied to the generated deck so
   the two photographic surfaces on the page share one hover
   grammar instead of two. */
.card img { transition: transform .6s var(--ease); }
.card:hover img { transform: scale(1.045); }

/* ── MOON BAND ────────────────────────────────────────────────
   Bounded parallax on the one full-bleed photograph that is not
   already carrying a caption. The image is scaled up only while
   the timeline is active, purely so the translate never reveals an
   edge; .band gets overflow hidden here because styles.css never
   needed it before this. */
.band { overflow: hidden; }
@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    .band img {
      animation: fx-band-parallax linear both;
      animation-timeline: view();
      animation-range: cover 0% cover 100%;
    }
  }
}
@keyframes fx-band-parallax {
  from { transform: scale(1.12) translateY(-4%); }
  to   { transform: scale(1.12) translateY(4%); }
}

/* ── MEMES (sticky stack) ─────────────────────────────────────
   The fan was too polite to read as a hand of real photographs.
   Widening the spread and letting the cast shadow grow with each
   card's index gives the stack actual depth instead of a uniform
   tilt. Both read from --i, which is already set inline per card
   in index.html, so nothing new needs adding to the markup. */
.meme__btn {
  transform: rotate(calc((var(--i) - 2.5) * 3.4deg));
  box-shadow:
    0 calc(14px + var(--i) * 6px) calc(48px + var(--i) * 10px)
    rgb(0 0 0 / calc(.5 + var(--i) * .03));
}
.meme__btn:hover {
  transform: rotate(calc((var(--i) - 2.5) * 3.4deg)) translateY(-4px);
}

/* The outgoing card now recedes by an amount tied to its own
   index too, so cards do not all peel away identically. */
@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    .meme__btn {
      animation: fx-meme-recede linear both;
      animation-timeline: view();
      animation-range: exit-crossing 0% exit-crossing 100%;
    }
  }
}
@keyframes fx-meme-recede {
  to {
    transform:
      rotate(0deg)
      scale(calc(.94 - var(--i) * .01))
      translateY(calc(-2vh - var(--i) * .6vh));
    opacity: .32;
  }
}

/* ── LIGHTBOX ─────────────────────────────────────────────────
   Native <dialog> has no built-in entrance. showModal() flips the
   [open] attribute, which is exactly the hook a CSS animation
   needs to run on arrival. */
@media (prefers-reduced-motion: no-preference) {
  .lb[open] { animation: fx-lb-in .38s var(--ease) both; }
  .lb[open]::backdrop { animation: fx-lb-backdrop-in .38s var(--ease) both; }
}
@keyframes fx-lb-in {
  from { opacity: 0; transform: scale(.96) translateY(14px); }
  to   { opacity: 1; transform: none; }
}
@keyframes fx-lb-backdrop-in {
  from { opacity: 0; }
  to   { opacity: 1; }
}

/* Both copy affordances on the page confirm the same way. A single
   snap-eased pulse is the whole vocabulary for "that worked". */
@media (prefers-reduced-motion: no-preference) {
  .ca__copy.is-done,
  .lb__act.is-done {
    animation: fx-copy-confirm .45s var(--fx-ease-snap) both;
  }
}
@keyframes fx-copy-confirm {
  0%   { transform: scale(1); }
  45%  { transform: scale(1.08); }
  100% { transform: scale(1); }
}

/* ── THE WILD ─────────────────────────────────────────────────
   This is the section the brief asks to hold the longest pause on
   the page. A slower, later reveal than every other .reveal group
   is the only lever available from a motion file, so it gets one:
   the kicker, the headline and the outbound link all settle in
   over a full second instead of the base 0.7s, arriving late
   enough that the silence before them actually reads as a pause. */
@media (prefers-reduced-motion: no-preference) {
  .js .wild .reveal {
    transition-duration: 1.1s, 1.1s;
    transition-delay: calc(var(--d, 0ms) + 120ms), calc(var(--d, 0ms) + 120ms);
  }

  /* The number itself gets one more beat after its container has
     already faded up, so 2,500 is the last thing to finish moving
     in a section that is otherwise done moving. */
  .js .wild__head.reveal.in .wild__figure .count {
    animation: fx-figure-emphasis .8s var(--ease) .2s both;
  }
}
@keyframes fx-figure-emphasis {
  from { opacity: 0; transform: translateY(10px) scale(.96); }
  to   { opacity: 1; transform: none; }
}

/* Bounded parallax on the last full-bleed photograph, same pattern
   as the moon band. .wild already clips overflow in styles.css, so
   the oversized box here has somewhere safe to hide its edges. */
@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    .wild__bg {
      inset: -6% -3%;
    }
    .wild__bg img {
      animation: fx-wild-parallax linear both;
      animation-timeline: view();
      animation-range: cover 0% cover 100%;
    }
  }
}
@keyframes fx-wild-parallax {
  from { transform: translateY(-4%); }
  to   { transform: translateY(4%); }
}

/* ── BUY (swap) ───────────────────────────────────────────────
   A focus glow that lives on opacity rather than an animated
   box-shadow, so the field can hint at being active without ever
   painting a shadow that changes shape every frame. */
.swap__field { position: relative; }
.swap__field::after {
  content: "";
  position: absolute;
  inset: -1px;
  border-radius: inherit;
  box-shadow: 0 0 0 3px rgb(194 251 94 / .18);
  opacity: 0;
  pointer-events: none;
  transition: opacity .25s var(--ease);
}
.swap__field:focus-within::after { opacity: 1; }

.flow__icon { transition: transform .25s var(--ease); }
.flow__step:hover .flow__icon { transform: translateY(-2px); }

/* ── REDUCED MOTION ───────────────────────────────────────────
   Almost everything above is already scoped inside its own
   prefers-reduced-motion: no-preference block, so it never
   registers here in the first place, and styles.css already
   flattens .reveal and .enter to their resting opacity and
   transform. What is left to handle explicitly is the two
   elements whose resting position depends on a transform that is
   not decorative motion but actual layout, where a blind
   transform: none would misplace them rather than just stop them
   moving. Those get their static geometry restored by hand. */
@media (prefers-reduced-motion: reduce) {
  .hero__break.enter--break {
    transform: translateX(-58%) rotate(-3deg);
  }

  @media (max-width: 860px) {
    .hero__break.enter--break { transform: none; }
  }

  /* The fan spread and its per-card shadow are a static layout
     choice, not motion, so reduced motion leaves them exactly as
     drawn. Only the scroll-linked recede is motion, and it is
     already unreachable here because it lives inside its own
     no-preference guard above. */
  .js .intro { display: none; }
}
