/* ECO POWERTECH — motion.css
   Phase 6. The declarative half of the motion layer: everything except the hero
   video scrub, which is a <video> currentTime seek and lives in src/js/motion.js.

   Three rules govern this file.

   1. Every animation is scroll-driven. Nothing here runs on a clock, nothing
      fires on load, and nothing in this file listens to anything.
   2. Every animation is additive: the resting state is the static state, and a
      browser without scroll-driven animation support gets exactly the static
      page. Start states live inside @keyframes, never on the element.
   3. Tokens or nothing (rule 1 of ARCHITECTURE-ESSENTIALS).

   The whole file is wrapped in @media (prefers-reduced-motion: no-preference)
   and @supports (animation-timeline: view()). Under reduced motion this file
   contributes zero declarations — awwwards-standard hard gate #2, which
   overrides the emil-design-eng position of reducing rather than eliminating.
   See DIRECTION.md §9.

   WHAT CHANGED IN PHASE 6, and why
   --------------------------------
   Phase 5's hero had exactly one animation: `hero-aside`, which faded the
   eyebrow, the lead paragraph and BOTH CTAs to nothing between 46% and 70% of
   the track — 778px of scrolling holding a single static headline. The seven
   beats documented in assets/hero/manifest.json were read by nothing at all.

   The narrative is now built. The lead paragraph's three sentences are split
   into three beat lines timed to the footage, and the primary CTA never leaves
   the page — it dims across the pivot and comes back. A hero whose job is a
   conversion button does not delete the button for a third of its length.
   ------------------------------------------------------------------------- */

@media (prefers-reduced-motion: no-preference) {
@supports (animation-timeline: view()) {

  /* ══ Keyframes ══════════════════════════════════════════════════════════ */

  /* A block arriving. Opacity and transform only — both compositor properties,
     so this cannot cost a frame no matter how many run at once. */
  @keyframes reveal {
    from { opacity: 0; transform: translateY(var(--reveal-shift)); }
    to   { opacity: 1; transform: none; }
  }

  /* Two-tone headline (DIRECTION.md §9, second motion idea). Both ends are
     semantic tokens that already pass AA on their band, so every intermediate
     value a stalled scroll can land on sits between two passing colours. */
  @keyframes lit {
    from { color: var(--fg-muted); }
    to   { color: var(--fg); }
  }

  /* A hairline rule drawing itself in from the inline start edge. */
  @keyframes draw-rule {
    from { transform: scaleX(0); }
    to   { transform: scaleX(1); }
  }


  /* ══ 01 · HERO ═════════════════════════════════════════════════════════ */

  /* The scrub track. The hero is 100svh of stage plus --hero-track of travel;
     the sticky stage inside it is exactly 100svh, so the pin releases on the
     frame the next band's top edge reaches the viewport bottom.

     html.hero-live is set by the capability gate in <head>, BEFORE first paint,
     so this height is present at first style resolution and costs no layout
     shift. Miss any gate condition — under 1080px, reduced motion, no
     scroll-driven animation support, no JS — and the hero is one screen with a
     poster and no dead scroll. */
  .hero-live .hero {
    height: calc(100svh + var(--hero-track));
    view-timeline: --hero block;
  }

  /* `contain 0% -> contain 100%` is exactly the pin travel: for a subject
     taller than the scrollport, the contain phase runs from its top edge
     meeting the scrollport top to its bottom edge meeting the scrollport
     bottom. Keyframe percentages therefore map 1:1 onto the clip's own
     timeline, and the same fraction drives the video seek in motion.js — the
     two layers read the same number and cannot drift apart. */

  /* ---- The display line, beat by beat ------------------------------------
     THIS is what changes with the footage. Previously only a small caption
     under the headline moved while the headline itself never did, which is why
     the hero read as static no matter how much the clip was doing.

     Beat percentages come straight from assets/hero/manifest.json, converted
     from its 200-frame index space:

       machineIntact     0–6%     comesApart      6–37%
       pushThroughMark   38–48%   rampToBlack     48–50%
       black             51–52%   doorsEmerge     52–65%
       team              66–100%

     The two statements HAND OVER; they never cross-fade. They occupy one grid
     cell, so any moment where both are partly opaque superimposes two different
     sentences — illegible, and the same double exposure that makes cross-faded
     video frames ghost. The first is fully out before the second begins.

     Two, not three: the middle line was invented copy and has been removed with
     the rest of it. The claim now holds through the whole machine sequence and
     the ramp to black (0–52%), and the slogan lands on the team (66–100%).

     Between 52% and 60% the display is empty on purpose: the clip is on its
     manufactured black frames, and the held silence is the pivot. It is ~260px
     of scroll — long enough to read as deliberate, short enough that nobody
     thinks the page froze, and the lead and both CTAs stay on screen throughout
     so there is never a moment with nothing to act on. */

  @keyframes hero-line-1 {          /* the machine, intact then coming apart */
    0%,  48%  { opacity: 1; visibility: visible; transform: none; }
    52%, 100% { opacity: 0; visibility: hidden;
                transform: translateY(calc(var(--reveal-shift) * -0.35)); }
  }

  @keyframes hero-line-2 {          /* doors open, the team walks out */
    0%,  60%  { opacity: 0; visibility: hidden;
                transform: translateY(calc(var(--reveal-shift) * 0.6)); }
    66%, 100% { opacity: 1; visibility: visible; transform: none; }
  }

  /* The affordance. A four-viewport hero with no cue that it is scrubbable
     reads as a stuck page — this is the cheapest possible fix for that. */
  @keyframes hero-progress { to { transform: scaleX(1); } }
  @keyframes hero-cue {
    0%       { opacity: 1; }
    9%, 100% { opacity: 0; visibility: hidden; }
  }

  /* The corner ticks draw outward as the scrub begins — the frame "arming"
     itself. Scale only, from a visible size, never from zero. */
  @keyframes hero-tick {
    0%      { opacity: 0; transform: scale(0.4); }
    7%, 100% { opacity: 1; transform: none; }
  }

  .hero-live .hero__line,
  .hero-live .hero__progress::after,
  .hero-live .hero__cue,
  .hero-live .hero__tick {
    animation-timing-function: linear;
    animation-fill-mode: both;
    animation-timeline: --hero;
    animation-range: contain 0% contain 100%;
  }

  .hero-live .hero__line--1        { animation-name: hero-line-1; }
  .hero-live .hero__line--2        { animation-name: hero-line-2; }
  .hero-live .hero__progress::after { animation-name: hero-progress; }
  .hero-live .hero__cue            { animation-name: hero-cue; }
  .hero-live .hero__tick           { animation-name: hero-tick; }

  /* ══ Section headlines ═════════════════════════════════════════════════ */

  /* The timeline subject is the heading, not the word. A word span is one line
     tall, so its own view() range would be ~26px of scroll — the animation
     would be over before it was seen and the stagger would be invisible. */
  .h2 { view-timeline: --headline block; }

  /* Words stay display: inline with real whitespace between them. A screen
     reader reads the sentence; splitting into inline-blocks is what makes some
     of them read out a word list instead. */
  .h2 .w {
    display: inline;
    animation: lit linear both;
    animation-timeline: --headline;
    animation-range:
      cover calc(12% + var(--i, 0) * 3.5%)
      cover calc(38% + var(--i, 0) * 3.5%);
  }


  /* ══ Reveals ═══════════════════════════════════════════════════════════ */

  /* Ranged by LENGTH, not by a percentage of the subject.

     Phase 5 used `entry 8% entry 62%`, and the `entry` range is sized by the
     ELEMENT, so reveal duration scaled with element height: .enq__intro (793px)
     took 428px of scroll to fade in and translate 20px — under 0.05px of change
     per scrolled pixel, which is imperceptible. Short blocks snapped, tall
     blocks crawled, and the page had no consistent reveal rhythm.

     A fixed --reveal-run gives every block the same rhythm whatever its height:
     it finishes once it is 340px into view, full stop. */
  .sec-head,
  .pos__head,
  .pos__body,
  .jv__grid > *,
  .person,
  .co__body,
  .enq__intro,
  .enq__direct,
  .enq__fig,
  .ft__col {
    animation: reveal var(--ease-out) both;
    animation-timeline: view(block);
    animation-range:
      entry var(--reveal-delay)
      entry calc(var(--reveal-delay) + var(--reveal-run));
  }

  /* Phase 9b: .fields__list used to reveal as ONE block, which was right when
     it was eight text cells arriving together as a table. They are eight
     photographic cards now, and eight photographs appearing in perfect unison
     reads as a page-load artefact rather than as a set being laid out. --i is
     set per card in the markup, so adding a ninth trade cannot break the chain.
     Same --stagger-px the benefits ladder uses — a length, not a duration,
     because a scroll-driven range is measured in pixels. */
  .fields__list > li {
    animation: reveal var(--ease-out) both;
    animation-timeline: view(block);
    animation-range:
      entry calc(var(--reveal-delay) + var(--i, 0) * var(--stagger-px) * 0.5)
      entry calc(var(--reveal-delay) + var(--reveal-run) + var(--i, 0) * var(--stagger-px) * 0.5);
  }


  /* Row-internal stagger. Grid rows already stagger themselves — the second row
     enters later than the first — but items within a row share a Y and would
     otherwise arrive in perfect unison, which reads mechanical. One step across
     the row is enough; more would read as a wave. The offsets are a fraction of
     --reveal-run so the stagger scales with the reveal instead of fighting it. */
  /* Phase 8 retired .svc__grid and .models__pair with the sections they
     staggered; Phase 9 retired .model with #modelle itself. .ft__grid keeps
     its stagger — it is unchanged. */
  .ft__grid > :nth-child(2) {
    animation-range:
      entry calc(var(--reveal-delay) + var(--reveal-run) * 0.20)
      entry calc(var(--reveal-delay) + var(--reveal-run) * 1.20);
  }

  .ft__grid > :nth-child(3) {
    animation-range:
      entry calc(var(--reveal-delay) + var(--reveal-run) * 0.40)
      entry calc(var(--reveal-delay) + var(--reveal-run) * 1.40);
  }

  /* Deliberately NOT revealed: .proc__step. It is sticky, and a sticky element
     carrying its own view-timeline animation fights the pin it is already
     performing. The stacked-step mechanic from research/03-references.md is the
     effect there; a second one on top of it is noise. Form fields are not
     revealed either — an input that fades in is an input the user cannot yet
     click. (.rail was the other exception; that element no longer exists.) */


  /* ══ Hairline rules ════════════════════════════════════════════════════ */

  /* Scoped to .sec-head: the hero eyebrow is inside the pinned stage, where a
     view() progress is not the progress anyone means, and it is already carried
     by hero-eyebrow. */
  .sec-head .eyebrow--rule::after {
    transform-origin: left center;
    animation: draw-rule var(--ease-out) both;
    animation-timeline: view(block);
    animation-range:
      entry var(--reveal-delay)
      entry calc(var(--reveal-delay) + var(--reveal-run));
  }


  /* ══ Image reveals ═════════════════════════════════════════════════════
     A wipe plus a settle, not a fade. The frame opens from the top edge while
     the picture relaxes out of a 4% overscale, which reads as a camera coming
     to rest rather than an element switching on.

     clip-path is used rather than a covering panel because it needs no extra
     element and never touches layout. It is the one property here outside the
     transform/opacity pair; it is confined to four images, all of which are
     composited already, and it is not in the banned list in rule 6 (width,
     height, top, left, margin, filter, box-shadow) — those are banned because
     they force layout, which clip-path does not. */
  @keyframes img-reveal {
    from { clip-path: inset(0 0 100% 0); transform: scale(1.04); }
    to   { clip-path: inset(0 0 0 0);    transform: none; }
  }

  .person__img,
  .enq__fig img {
    animation: img-reveal var(--ease-out) both;
    animation-timeline: view(block);
    animation-range:
      entry var(--reveal-delay)
      entry calc(var(--reveal-delay) + var(--reveal-run) * 1.4);
  }
  /* The two portraits are a pair, so the second trails the first by a step —
     the same one-step rule used inside grid rows elsewhere. */
  .people > :nth-child(2) .person__img {
    animation-range:
      entry calc(var(--reveal-delay) + var(--reveal-run) * 0.22)
      entry calc(var(--reveal-delay) + var(--reveal-run) * 1.62);
  }


  /* ══ 04 · BENEFITS — the ladder writes itself ══════════════════════════
     Inherited from the retired .roles block, which staggered five rows on a
     per-item --i so the list read as being written rather than arriving as a
     table. The Berufsfelder list that replaced .roles is a wrapping row, not a
     stack, so it reveals as one block; this ladder is the composition that
     still earns the row-by-row treatment.

     --i is set per row in the markup, so the stagger is data, not a chain of
     nth-child rules that breaks the moment a statement is added. */
  @keyframes row-name {
    from { opacity: 0; transform: translateY(calc(var(--reveal-shift) * 0.55)); }
    to   { opacity: 1; transform: none; }
  }
  @keyframes row-n { from { opacity: 0; } to { opacity: 1; } }

  .ben__item .ben__t,
  .ben__item .ben__n {
    animation-timing-function: var(--ease-out);
    animation-fill-mode: both;
    animation-timeline: view(block);
    animation-range:
      entry calc(var(--reveal-delay) + var(--i, 0) * var(--stagger-px))
      entry calc(var(--reveal-delay) + var(--reveal-run) + var(--i, 0) * var(--stagger-px));
  }
  .ben__item .ben__t { animation-name: row-name; }
  .ben__item .ben__n { animation-name: row-n; }


  /* ══ 06 · PROCESS — the step number takes the accent ═══════════════════
     .proc__step is sticky and stacks, so it must NOT carry a reveal: a sticky
     element animating its own view() progress fights the pin it is already
     performing. The step NUMBER is not sticky-critical, so it can light up as
     its card takes the top of the stack. Colour only — no movement to fight
     the stack with. */
  @keyframes step-lit {
    from { color: var(--fg-muted); }
    to   { color: var(--accent); }
  }
  .proc__n {
    animation: step-lit linear both;
    animation-timeline: view(block);
    animation-range: entry 30% cover 50%;
  }


  /* ══ Page progress ═════════════════════════════════════════════════════
     A hairline across the top of the viewport tracking the whole document.
     scroll(root block) is the document timeline — not view(), which would be
     scoped to an element. Fixed, one pixel, transform-only, and it answers
     "how much of this is left" without a number. */
  @keyframes page-progress { to { transform: scaleX(1); } }

  .page-progress__bar {
    animation: page-progress linear both;
    animation-timeline: scroll(root block);
  }

}
}
