/* Tetto — die Mechanik hinter den Bewegungen.
   ════════════════════════════════════════════════════════════════════════
   Gegenstück zu bewegung.js. Hier steht kein Inhalt, nur das Getriebe.

   Vier Regeln, alle vier nachgemessen (Chrome 148, siehe Notizen unten):

   1. `animation-timeline` MUSS nach der `animation`-Kurzform stehen — in
      derselben Regel weiter unten oder in einer späteren Regel. Die
      Kurzform setzt die Zeitachse still auf `auto` zurück; das Ergebnis
      ist eine Seite, auf der jede Animation sofort im Endzustand steht,
      ohne eine einzige Fehlermeldung.

   2. Kurzform ohne Zeitangabe schreiben, die Dauer erst danach setzen:
      `animation: name linear both` ergibt in Browsern ohne Scroll-
      Zeitachsen `animation-duration: 0s` — das Element springt auf den
      LETZTEN Keyframe. Ist der letzte Keyframe der neutrale Ruhezustand,
      ist der Rückfall damit von selbst richtig, ohne @supports, ohne
      JavaScript. Gemessen: opacity 1, transform none.

   3. Endet eine Animation NICHT neutral (Parallaxe, Drehung, gepinnte
      Szene), gilt das Gegenteil: Ohne Kapselung in @supports bleibt das
      Element für immer im Endzustand hängen. Gemessen: translateY(80px),
      dauerhaft. Solche Bewegungen stehen ausschliesslich im @supports-Block,
      und der Rückfall läuft über --p aus bewegung.js.

   4. Alles steht in `@media (prefers-reduced-motion: no-preference)` statt
      hinterher mit `!important` abgeräumt zu werden. Wer keine Bewegung
      will, bekommt gar nicht erst welche zugewiesen.

   Der Fortschritt --p kommt entweder vom Browser (Scroll-Zeitachse, läuft
   seit Safari 26.4 in allen drei Engines ausserhalb des Hauptthreads) oder
   von bewegung.js. Beide treiben dieselben Keyframes.                    */

/* ── Fähigkeitsschalter ───────────────────────────────────────────────────
   Genau diese Abfrage steht auch in bewegung.js. Sie prüft absichtlich
   BEIDE Eigenschaften: Firefox kann hinter seinem Schalter zwar view(),
   aber kein animation-range — mit nur einer Abfrage bekäme man dort das
   schlechteste Ergebnis, nämlich eine laufende Zeitachse ohne Bereich.  */

@supports (animation-timeline: view()) and (animation-range: contain 0% contain 100%) {
  :root { --hat-zeitachsen: 1; }
}

/* ── Die Autoren-Schnittstelle ────────────────────────────────────────────
   Eine Szene setzt drei Dinge und muss sonst nichts wissen:

     --bewegung : Name der Keyframes
     --von/--bis: welches Stück des Wegs (0…1) diese Bewegung bekommt
     --abschnitt: cover (Standard) oder contain (in gepinnten Szenen)

   Daraus wird für den Browser `animation-range: <abschnitt> X% <abschnitt> Y%`
   und für die Rückfallebene derselbe Ausschnitt als Rechnung auf --p.
   Beide Wege wurden gegeneinander gemessen: bei --von .25, --bis .6 und
   --p .425 ergibt der Rückfall exakt animation-delay: -0.5s, der Browser
   exakt `contain 25% contain 60%`. Dasselbe Bild, zwei Antriebe.        */

@property --p   { syntax: '<number>'; inherits: true;  initial-value: 0; }
@property --von { syntax: '<number>'; inherits: false; initial-value: 0; }
@property --bis { syntax: '<number>'; inherits: false; initial-value: 1; }

/* ── Das Getriebe ─────────────────────────────────────────────────────── */

@media (prefers-reduced-motion: no-preference) {

  /* Rückfallebene: pausierte Animation, per negativer Verzögerung auf den
     Fortschritt gespult. `animation-delay: calc(var(--pt) * -1s)` bei 1s
     Dauer heisst exakt "stehe bei pt". */
  .scrub {
    --pt: clamp(0, (var(--p, 0) - var(--von, 0)) / (var(--bis, 1) - var(--von, 0)), 1);
    animation: var(--bewegung) linear both paused;
    animation-duration: 1s;
    animation-delay: calc(var(--pt) * -1s);
  }

  /* Wo der Browser es selbst kann, übernimmt er. Diese Regel steht nach
     der Kurzform — siehe Regel 1. */
  @supports (animation-timeline: view()) and (animation-range: contain 0% contain 100%) {
    .scrub {
      animation-play-state: running;
      animation-duration: auto;
      animation-delay: 0s;
      animation-timeline: var(--zeitachse, view());
      animation-range:
        var(--abschnitt, cover) calc(var(--von, 0) * 100%)
        var(--abschnitt, cover) calc(var(--bis, 1) * 100%);
    }
  }

  /* ── Auftritte ────────────────────────────────────────────────────────
     Etwas erscheint einmal und bleibt. Bewusst NICHT über Scroll-Zeitachsen:
     ein Schalter plus Übergang verhält sich in jedem Browser gleich, ist an
     einer Stelle prüfbar und kostet nach dem Auftritt gar nichts mehr.   */

  [data-auftritt] {
    opacity: 0;
    transform: translate3d(0, 22px, 0);
    transition:
      opacity .7s cubic-bezier(.22,1,.36,1) var(--verzug, 0s),
      transform .7s cubic-bezier(.22,1,.36,1) var(--verzug, 0s);
  }
  [data-auftritt="seitlich"] { transform: translate3d(-26px, 0, 0); }
  [data-auftritt="zoom"]     { transform: scale(.94); }
  [data-auftritt="kippen"]   { transform: perspective(900px) rotateX(12deg) translate3d(0, 24px, 0); }
  [data-auftritt].ist-da     { opacity: 1; transform: none; }

  /* Ohne JavaScript wird nichts versteckt — sonst wäre die Seite leer.
     Beim Scrub gilt dasselbe: Kann der Browser keine Zeitachsen UND läuft
     kein JavaScript, bliebe --p auf 0 und die Szene stünde für immer im
     Anfangszustand. Dann lieber gar keine Animation und der Grundzustand
     der Elemente — deshalb ist der immer der lesbare. */
  html:not(.js) [data-auftritt] { opacity: 1; transform: none; }
  html:not(.js):not(.hat-timeline) .scrub { animation: none; }

  /* ── Wörter, die gestaffelt hochkommen ───────────────────────────────── */

  .wort {
    display: inline-block;
    opacity: 0;
    transform: translate3d(0, .62em, 0) rotate(1.2deg);
    transition:
      opacity .6s cubic-bezier(.22,1,.36,1) calc(var(--i) * var(--wort-schritt, 26ms)),
      transform .6s cubic-bezier(.22,1,.36,1) calc(var(--i) * var(--wort-schritt, 26ms));
  }
  .ist-da .wort { opacity: 1; transform: none; }

  /* ── Magnetische Knöpfe ──────────────────────────────────────────────── */

  [data-magnet] {
    transform: translate3d(var(--magnet-x, 0), var(--magnet-y, 0), 0);
    transition: transform .35s cubic-bezier(.22,1,.36,1);
  }
  [data-magnet]:active { transition-duration: .08s; }

}

/* Solange eine Szene ausser Sicht ist, darf der Browser ihre Ebene wieder
   hergeben. Dauerhaftes will-change kostet Speicher, und zwar auf genau
   den Geräten, auf denen er knapp ist.

   Hier steht ausdrücklich NUR `transform`. `will-change: opacity` an einem
   Element mit transform-style: preserve-3d — oder an einem seiner Vorfahren
   — flacht die Tiefe ab, und zwar lautlos: getComputedStyle meldet weiterhin
   preserve-3d, aber gerendert wird flach. Nachgemessen bei perspective 600
   und translateZ(200): mit `transform` sichtbare Breite 150 px, mit
   `transform, opacity` nur 100 px. Damit wäre jedes 3D auf dieser Seite
   genau dann tot, wenn es ins Bild kommt. Opacity braucht ohnehin kein
   will-change — sie wird auch so im Compositor animiert. */
[data-szene].sichtbar .scrub { will-change: transform; }

/* ── Gepinnte Szenen ──────────────────────────────────────────────────────
   Die Zeitachse gehört auf die HOHE Hülle, nicht auf das klebende Kind:
   Ein sticky-Element steht während des Klebens relativ zum Fenster still,
   seine eigene view()-Zeitachse würde einfrieren. Mit der Hülle als Geber
   deckt sich `contain 0% … contain 100%` genau mit der Klebedauer — die
   Strecke steuert man allein über die Höhe der Hülle.                    */

.pin {
  position: relative;
  height: var(--pin-laenge, 300svh);
}

.pin__buehne {
  position: sticky;
  top: 0;
  height: 100svh;      /* svh, damit die Adressleiste auf dem Handy die
                          Bühne nicht um 60 Pixel verschiebt */
  display: grid;
  place-items: center;
  overflow: clip;
}

@supports (animation-timeline: view()) and (animation-range: contain 0% contain 100%) {
  .pin { view-timeline: --pin block; }
  .pin .scrub {
    --zeitachse: --pin;
    --abschnitt: contain;
  }
}

/* ── Lesefortschritt ──────────────────────────────────────────────────── */

.lesebalken {
  position: fixed;
  inset: 0 0 auto 0;
  height: 2px;
  z-index: 60;
  background: linear-gradient(90deg, var(--terra), var(--amber));
  transform-origin: 0 50%;
  transform: scaleX(var(--p, 0));
  pointer-events: none;
}

@supports (animation-timeline: scroll()) {
  @keyframes balken-fuellen { from { transform: scaleX(0); } to { transform: scaleX(1); } }
  .lesebalken {
    animation: balken-fuellen linear both;
    animation-duration: auto;
    animation-timeline: scroll(root block);
  }
}


/* ── Wer keine Bewegung will ──────────────────────────────────────────────
   Hier steht fast nichts mehr, weil oben nichts zugewiesen wurde. Nur die
   Dinge, die auch ohne Animation räumlich wirken, werden aufgelöst.      */

@media (prefers-reduced-motion: reduce) {
  .pin { height: auto; }
  .pin__buehne { position: static; height: auto; padding: 4rem 0; }
  .lesebalken { display: none; }
}
