/* ═══════════════════════════════════════════════════════════════════
   Pojana v2 — Bewegungsschicht
   docs/design-spec.md Abschnitt 6 · Schritt 3 der Baureihenfolge

   Hier steht nur, was es ohne Bewegung nicht gäbe: Ladevorhang, Zeiger,
   die gefahrenen Fassungen von Register und Querreihe. Das Layout ohne
   Skript steht unverändert in komponenten.css und startseite.css — und
   es bleibt dort stehen. Diese Datei überschreibt es, sie ersetzt es
   nicht.

   Zwei Schalter entscheiden, ob etwas davon gilt:

   · `html.js`         setzt ein Inline-Skript im Kopf. Ohne JavaScript
                       fehlt die Klasse und nichts hier greift — die
                       Seite ist dann die ruhige Fassung aus 6.5.
   · `.is-gefahren`    setzt js/bewegung.js erst, nachdem der Pin
                       tatsächlich eingerichtet ist. So kann kein
                       Abschnitt in einem Zustand hängen bleiben, den
                       kein ScrollTrigger mehr bedient.

   Und über allem steht prefers-reduced-motion ganz unten: dort wird
   jeder dieser Schalter wieder ausgeknipst.
   ═══════════════════════════════════════════════════════════════════ */

/* ═══ Die Ladechoreografie läuft ohne GSAP ═════════════════════════
   Vorhang und Hero standen zuerst als GSAP-Timeline hier (so nennt es
   Spec 6.1). Gemessen hat die Startseite damit mobil im Schnitt 0,847
   — unter der Schwelle von 0,85, die Spec 10 setzt. Der Grund war
   nicht die Animation, sondern ihr Zeitpunkt: GSAP und ScrollTrigger
   sind zusammen 116 KB, und solange sie im Ladefenster mitgeholt
   wurden, nahmen sie dem Heldenbild die Bandbreite weg (FCP 1,5 → 2,5 s).

   Also andersherum, wie es Spec 10 und der Brief ausdrücklich erlauben
   („lazy-load heavy effects"): der Ladeteil der Choreografie ist reines
   CSS und läuft, sobald das Stylesheet da ist. GSAP holt js/bewegung.js
   erst nach `load` nach und bedient dann nur noch, was am Scroll und am
   Zeiger hängt. Sichtbar ist der Unterschied nicht — messbar schon.
   ═════════════════════════════════════════════════════════════════ */

/* ── 6.1 Ladevorhang ────────────────────────────────────────────────
   Ein Messingbalken wischt einmal über die Fläche, die Marke geht weg,
   fertig — zusammen 0,74 s. Die Recherche warnt ausdrücklich vor
   Ladebildschirmen, die Satz und Telefonnummer aufhalten; deshalb ist
   das hier ein Wisch und kein Vorhang, der sich Zeit lässt.

   Der Vorhang existiert nur mit JavaScript (html.js) — und er räumt
   sich selbst weg, ohne auf eine Zeile davon zu warten. Eine Seite,
   die hinter einer schwarzen Fläche verschwindet, weil ein Skript
   nicht ankam, darf es nicht geben.

   Und er läuft nur bei einer echten Ankunft: `html.ankunft` setzt das
   Inline-Skript der Startseite genau dann, wenn der Besucher nicht von
   einer Seite dieser Seite kommt. Wer von „Leistungen" zurückgeht, hat
   den Wisch schon gesehen; ihn erneut zu zeigen, wäre der Ladescreen
   zwischen zwei Seiten, den Spec 6.3 ausdrücklich ausschließt. Dort
   blendet der Seitenwechsel über (6.3), und das ist der ganze Übergang.

   --vorlauf ist der Vorsprung, den der Vorhang der Hero-Choreografie
   abverlangt: eine halbe Sekunde bei der Ankunft, null ohne ihn. Alle
   Verzögerungen darunter rechnen gegen diesen einen Wert, damit die
   Folge dieselbe bleibt und nur ihr Anfang wandert.                   */

.vorhang { display: none; }

html.js { --vorlauf: 0s; }
html.js.ankunft { --vorlauf: .5s; }

html.js.ankunft .vorhang {
  position: fixed;
  inset: 0;
  z-index: 150;
  display: grid;
  place-items: center;
  background: var(--ink);
  animation: vorhang-weg 1ms linear .74s forwards;
}
@keyframes vorhang-weg {
  to { visibility: hidden; pointer-events: none; }
}

.vorhang__mark {
  font-family: var(--disp);
  font-size: clamp(2.4rem, 9vw, 6rem);
  font-weight: 700;
  font-stretch: 112%;
  letter-spacing: -.045em;
  color: var(--cream);
  animation: vorhang-mark .2s linear .2s both;
}
@keyframes vorhang-mark { to { opacity: 0; } }

/* Der Balken wischt in einem Zug hin und wieder weg. Der Wechsel des
   transform-origin bei 46 % ist der Umschlagpunkt: bis dahin wächst er
   von links, danach zieht er sich nach rechts zurück. Beide Hälften
   tragen ihre eigene Kurve — power3.inOut, wie in der Spec. */
.vorhang__bar {
  position: absolute;
  inset: 0;
  background: var(--aqua);
  transform: scaleX(0);
  transform-origin: left center;
  animation: vorhang-wisch .74s both;
}
@keyframes vorhang-wisch {
  0%    { transform: scaleX(0); transform-origin: left center;
          animation-timing-function: cubic-bezier(.65, 0, .35, 1); }
  46%   { transform: scaleX(1); transform-origin: left center; }
  46.5% { transform: scaleX(1); transform-origin: right center;
          animation-timing-function: cubic-bezier(.65, 0, .35, 1); }
  100%  { transform: scaleX(0); transform-origin: right center; }
}

/* Solange der Vorhang liegt, steht der Scroll still: sonst scrollt man
   an der Hero-Choreografie vorbei, bevor sie angefangen hat. Die Klasse
   setzt und entfernt das Inline-Skript im Kopf. Es wird erst
   ausgeführt, wenn die Stylesheets da sind — also genau dann, wenn auch
   die Animationen oben anlaufen. */
html.lade, html.lade body { overflow: hidden; }

/* ── 6.1 Hero: drei Ebenen steigen aus der Zeilenmaske ──────────────
   Kein Einblenden, ein Aufsteigen: die Zeile ist schon da, die Maske
   gibt sie frei. Der Versatz von 75 ms je Zeile ist der Stagger aus
   der Spec; begonnen wird bei 0,5 s, also noch während der Balken sich
   zurückzieht — die beiden Bewegungen greifen ineinander, statt
   aufeinander zu warten.

   `backwards` und nicht `both`: die Zeile steht von der ersten Sekunde
   an unten in der Maske, aber sobald sie oben angekommen ist, gibt die
   Animation die Eigenschaft wieder frei. Ihr Endwert ist ohnehin der
   ruhende Zustand — und eine Animation, die mit `forwards` liegen
   bleibt, schlägt jede Inline-Angabe. Genau daran sind die magnetischen
   Knöpfe im Hero zuerst gescheitert: GSAP schrieb sein transform, und
   die noch füllende Ladeanimation überstimmte es lautlos.

   Ohne html.js greift nichts davon und jede Zeile steht sofort da.    */

html.js .hero .mask > span { animation: zeile-auf 1.05s var(--ease) backwards; }
@keyframes zeile-auf {
  from { transform: translateY(108%); }
  to   { transform: translateY(0); }
}

html.js .hero__vor  .mask > span              { animation-delay: var(--vorlauf); }
html.js .hero__h1 .mask:nth-child(1) > span   { animation-delay: calc(var(--vorlauf) + .075s); }
html.js .hero__h1 .mask:nth-child(2) > span   { animation-delay: calc(var(--vorlauf) + .15s); }
html.js .hero__lead .mask > span              { animation-delay: calc(var(--vorlauf) + .225s); }

html.js .hero__tun > * { animation: knopf-auf .7s var(--ease) backwards; }
html.js .hero__tun > *:nth-child(1) { animation-delay: calc(var(--vorlauf) + .30s); }
html.js .hero__tun > *:nth-child(2) { animation-delay: calc(var(--vorlauf) + .38s); }
@keyframes knopf-auf {
  from { transform: translateY(22px); opacity: 0; }
  to   { transform: translateY(0); opacity: 1; }
}

/* Die Breitenachse der Archivo öffnet einmal von 64 % auf 86 % — der
   typografische Zwilling des Wischens, aus eng wird weit.

   Nur ab 62 rem: font-stretch ist die eine Eigenschaft dieser Seite,
   die bei jedem Bild neu umbricht (Spec 10 verbietet so etwas sonst).
   Auf einem 390-px-Schirm ist die H1 3,3 rem groß, der Unterschied
   zwischen 64 % und 86 % kaum zu sehen — und die Layoutarbeit fiele
   genau in das Fenster, in dem das Handy ohnehin zu tun hat. Am
   Desktop ist die Zeile 13 rem hoch und die Bewegung der halbe Auftritt. */
@media (min-width: 62rem) {
  html.js .hero__h1 { animation: achse-auf 1.6s var(--ease) calc(var(--vorlauf) + .05s) backwards; }
  @keyframes achse-auf {
    from { font-stretch: 64%; }
    to   { font-stretch: 86%; }
  }
}

/* ── Aufmacher der Unterseiten (Spec 8.2) ──────────────────────────
   Derselbe Gedanke wie im Hero, nur kürzer: kein Vorhang geht voraus,
   also fängt die Seite sofort an. Die Zeilen steigen aus ihrer Maske,
   und das Bild wird von links freigegeben — der Wisch der Startseite
   als Auftritt eines einzelnen Bildes, in einem Zug und nach 0,9 s
   vorbei.

   clip-path und transform, sonst nichts (Spec 10). Ohne html.js steht
   alles sofort da.                                                    */

html.js .auf .mask > span { animation: zeile-auf .9s var(--ease) backwards; }
html.js .auf__vor  .mask > span            { animation-delay: .04s; }
html.js .auf__h1 .mask:nth-child(1) > span { animation-delay: .10s; }
html.js .auf__h1 .mask:nth-child(2) > span { animation-delay: .16s; }
/* Drei Zeilen auf den Leistungen, vier auf „Über uns" — jede Maske
   braucht ihre eigene Verzögerung, sonst bekäme sie 0 s und stiege als
   erste auf. Derselbe Fall wie der dritte Weg der 404 weiter unten.
   Die Selektoren gelten für beide Unterseiten; die Leistungsseite nutzt
   die vierte nicht, weil es dort keine vierte Maske gibt. */
html.js .auf__h1 .mask:nth-child(3) > span { animation-delay: .22s; }
html.js .auf__h1 .mask:nth-child(4) > span { animation-delay: .28s; }
/* Der Vorspann kommt nach der letzten Überschriftenzeile, nicht mit
   ihr. Die Leistungsseite (drei Zeilen) bekommt dadurch vor dem
   Vorspann 120 statt 60 ms — ein Takt zwischen Überschrift und
   Vorspann, keine Lücke. */
html.js .auf__lead .mask > span            { animation-delay: .34s; }

html.js .auf__tun > * { animation: knopf-auf .7s var(--ease) backwards; }
html.js .auf__tun > *:nth-child(1) { animation-delay: .42s; }
html.js .auf__tun > *:nth-child(2) { animation-delay: .50s; }
/* Die 404 trägt drei Wege statt zwei Knöpfe (Spec 8.7). Ohne diese
   Zeile bekäme der dritte die Verzögerung 0 und käme als erster. */
html.js .auf__tun > *:nth-child(3) { animation-delay: .58s; }

/* Der Messingstrich der 404 zieht sich mit auf — dieselbe Keyframe wie
   die Kante am Band (linie-auf, weiter unten), damit es die eine
   Bewegung bleibt und keine zweite. */
html.js .weg__strich {
  transform: scaleX(0);
  transform-origin: left center;
  animation: linie-auf 1.1s var(--ease) .24s forwards;
}

html.js .auf__bild { animation: bild-frei .9s var(--ease) .08s backwards; }
@keyframes bild-frei {
  from { clip-path: inset(0 100% 0 0); }
  to   { clip-path: inset(0 0 0 0); }
}

/* ── Aufmacher der Kontaktseite (Spec 8.4) ──────────────────────────
   Die Kontaktseite hat keinen Bildaufmacher; ihr Einstieg ist das Blatt
   selbst. Die drei Zeilen darüber steigen aus derselben Maske und in
   derselben Folge wie auf den anderen Unterseiten — dieselbe Keyframe,
   nur ein anderer Anker. Der eigentliche Bewegungsmoment dieser Seite
   ist der Marker im Anfragesatz, und der steht bei seinem Baustein in
   komponenten.css und braucht kein Skript.                            */

html.js .anfrage--seite .mask > span { animation: zeile-auf .9s var(--ease) backwards; }
html.js .blatt__vor .mask > span             { animation-delay: .04s; }
html.js .blatt__h1 .mask:nth-child(1) > span { animation-delay: .10s; }
html.js .blatt__h1 .mask:nth-child(2) > span { animation-delay: .16s; }
html.js .blatt__lead .mask > span            { animation-delay: .22s; }

/* ── 6.2 Der Wisch — die Signatur der Seite ─────────────────────────
   Die eine Idee der Seite (Spec 0): sie behauptet nicht, dass sauber
   gemacht wird, sie führt es vor. Das Heldenbild liegt stumpf da, und
   was der Besucher anfasst, wird klar.

   Zwei Fassungen, an verschiedene Geräte gebunden — keine davon ist der
   Rückfall der anderen:

   ab 62 rem, feiner Zeiger  WebGL. Das Canvas legt js/bewegung.js an,
                             der Shader steht in js/wisch.js. Unter
                             62 rem wird diese Datei nie geholt.
   unter 62 rem              clip-path am Scroll, hier drunter. Kein
                             Canvas, keine Schleife, 0 KB JavaScript.
   ruhige Fassung            nichts von beidem. Das Bild bleibt das
                             statische Bild aus Schritt 3 (Spec 6.5).  */

/* Das Canvas deckt das Bild ab, sobald der Shader wirklich steht — erst
   dann bekommt es `is-an`. Kippt der WebGL-Kontext später weg, nimmt
   js/bewegung.js die Klasse zurück und das <img> ist wieder da. */
.hero__canvas {
  position: absolute;
  inset: 0;
  z-index: 1;                 /* über dem Bild, unter dem Schleier */
  width: 100%;
  height: 100%;
  opacity: 0;
  transition: opacity .6s var(--ease);
}
.hero__canvas.is-an { opacity: 1; }

/* Der Schleier trägt die Lesbarkeit der H1 und bleibt deshalb die
   oberste Bildebene — über Canvas und über der klaren Kopie. */
.hero__schleier { z-index: 2; }

/* Der Hinweis. Eine Fläche, die man mit der Maus wischen kann, sieht
   nicht aus wie eine Fläche, die man wischen kann; ohne diesen Satz
   bleibt die Signatur der Seite für die meisten unentdeckt. Er steht
   nur da, wo der Wisch am Zeiger hängt: `.is-wischbar` setzt
   js/bewegung.js erst, wenn der Shader wirklich läuft. Unter 62 rem
   gibt es ihn nicht — dort wischt das Scrollen, und niemand muss zum
   Scrollen aufgefordert werden.                                       */
.hero__hin {
  position: absolute;
  right: var(--pad);
  bottom: clamp(1.4rem, 5vh, 3rem);
  z-index: 10;
  display: none;
  align-items: center;
  gap: .55rem;
  margin: 0;
  pointer-events: none;
  font-family: var(--disp);
  font-size: .76rem;
  font-weight: 700;
  font-stretch: 104%;
  letter-spacing: .05em;
  color: var(--mute);
}
.hero__hin-punkt {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--aqua);
  animation: hin-puls 2.4s infinite;
}
@keyframes hin-puls {
  0%, 100% { opacity: 1; transform: scale(1); }
  50%      { opacity: .4; transform: scale(.66); }
}
.hero.is-wischbar .hero__hin { display: flex; }

/* ── Unter 62 rem: clip-path am Scroll ──────────────────────────────
   Eine klare Kopie des Bildes liegt über der stumpfen und wird beim
   Scrollen von oben nach unten freigegeben. Eine zusammengesetzte
   Eigenschaft, keine GPU-Schleife (premium-2026.md, Bausteine 2 und 4)
   — so bleibt das mobile Budget frei.

   Die Kopie ist ein Pseudo-Element, kein zweites <img>: so entsteht sie
   nur, wo diese Regeln gelten. Ein Element im HTML stünde auch am
   Desktop im Baum, wo der Shader das Bild schon zweimal führt. Die
   Bilddatei ist `hero-960.webp` — genau die Fassung, die das srcset des
   Heros unter 62 rem ohnehin wählt, also derselbe Abruf, kein Byte mehr
   über die Leitung.

   Gefahren wird mit einer scrollgebundenen CSS-Animation: null
   JavaScript, und sie läuft auch ohne Skript. Kennt der Browser
   `scroll()` nicht, greift die ganze Gruppe nicht — dann bleibt das
   Bild klar, so wie in der ruhigen Fassung. `55svh`: nach gut der
   halben Herohöhe ist die Fläche frei, nicht erst am Seitenende.

   Die Endstufe ist --grade-ruhig, genau das Grading der Fassung ohne
   Bewegung. Der Wisch endet dort, wo die ruhige Seite anfängt.        */

@media (max-width: 61.99rem) and (prefers-reduced-motion: no-preference) {
  @supports (animation-timeline: scroll()) {
    .hero__bild img { filter: var(--grade-stumpf); }

    .hero__bild::after {
      content: "";
      position: absolute;
      inset: 0;
      z-index: 1;
      background: url("../images/hero-960.webp") center / cover no-repeat;
      filter: var(--grade-ruhig);
      clip-path: inset(0 0 100% 0);
      animation: wisch-frei linear both;
      animation-timeline: scroll(root block);
      animation-range: 0px 55svh;
    }
    @keyframes wisch-frei {
      from { clip-path: inset(0 0 100% 0); }
      to   { clip-path: inset(0 0 0 0); }
    }
  }
}

/* ── 6.1 Band: der Satz deckt sich wortweise auf ────────────────────
   Die Wörter zerlegt bewegung.js in Masken (zwanzig Zeilen, kein
   SplitText — Spec 10); die Bewegung selbst macht das CSS. Ausgelöst
   wird sie von einem IntersectionObserver, der `.is-auf` an das Band
   hängt — nicht von ScrollTrigger.

   Das ist der Grund, warum auf dem Handy gar keine Bibliothek geladen
   wird: das Aufdecken war dort das Einzige, wofür GSAP nötig gewesen
   wäre. 116 KB für eine Animation, die ein Beobachter und vier
   Keyframes genauso gut hinbekommen, gehen zu Lasten des Bildes, das
   der Besucher zuerst sehen will (Spec 10).

   Die Maske braucht unten etwas Luft, sonst schneidet sie die
   Unterlängen von g, p und j mit ab. */
.band-satz .wort {
  display: inline-block;
  overflow: hidden;
  vertical-align: top;
  padding-bottom: .16em;
  margin-bottom: -.16em;
}
.band-satz .wort > i {
  display: inline-block;
  font-style: inherit;
}
html.js .band-satz .wort > i { transform: translateY(105%); }
html.js .band.is-auf .band-satz .wort > i {
  /* --verzug setzt bewegung.js je Wort: der Stagger von 16 ms. */
  animation: wort-auf .85s var(--ease) var(--verzug, 0s) forwards;
}
@keyframes wort-auf { to { transform: translateY(0); } }

/* Die Linie zieht sich mit auf. Ohne Skript liegt sie sofort da —
   deshalb hängt der Startwert an html.js. */
html.js .band__linie {
  transform: scaleX(0);
  transform-origin: left center;
}
html.js .band.is-auf .band__linie { animation: linie-auf 1.2s var(--ease) forwards; }
@keyframes linie-auf { to { transform: scaleX(1); } }

/* ── 8.3 Der Bildaufzug — „Über uns" ───────────────────────────────
   Das große Bild steht zu groß da und zieht sich beim Scrollen auf
   seine Fläche zurück: scale(1.16) → scale(1), scrollgebunden. Nur
   transform, keine Layoutarbeit (Spec 10 verbietet width/height
   ausdrücklich).

   Die Strecke ist beide Male dieselbe: von dem Moment an, in dem die
   Fläche die Unterkante berührt, bis sie mittig im Bild steht. Danach
   liegt das Bild ruhig — ein Aufzug, der nie ankommt, ist kein Aufzug.

   Zwei Fassungen, wie beim Wisch (6.2), und aus demselben Grund:

   ab 62 rem     ScrollTrigger mit scrub (js/bewegung.js). GSAP ist auf
                 dieser Breite ohnehin geladen, und es ist die einzige
                 Fassung, die auch dort läuft, wo der Browser
                 `animation-timeline` noch nicht kennt.
   unter 62 rem  scrollgebundene CSS-Animation, 0 KB JavaScript. Auf dem
                 Handy wird GSAP bewusst nie geholt (Spec 10); 116 KB
                 für eine Skalierung wären genau der Tausch, den die
                 Messung in Schritt 3 verboten hat. Kennt der Browser
                 `view()` nicht, greift die Gruppe nicht und das Bild
                 liegt still — dieselbe Fassung wie ohne Skript.
   ruhige Fassung  nichts von beidem (Spec 6.5, unten).                */

@keyframes aufziehen {
  from { transform: scale(1.16); }
  to   { transform: scale(1); }
}

@media (max-width: 61.99rem) and (prefers-reduced-motion: no-preference) {
  @supports (animation-timeline: view()) {
    .aufzug__bild img {
      animation: aufziehen linear both;
      animation-timeline: view(block);
      animation-range: cover 0% cover 50%;
      will-change: transform;
    }
  }
}

/* Am Desktop schreibt GSAP das transform. Der Startwert steht bewusst
   nicht hier: die Fläche liegt beim Laden unter dem Aufmacher und damit
   außerhalb des Bildes, GSAP setzt sie erst nach `load` — sichtbar
   springt also nichts. Stünde scale(1.16) fest im CSS, bliebe das Bild
   zu groß stehen, falls die Bibliothek nie ankommt. */
@media (min-width: 62rem) and (prefers-reduced-motion: no-preference) {
  html.js .aufzug__bild img { will-change: transform; }
}

/* ── 8.3 Faktentabelle: Zeile für Zeile ────────────────────────────
   Kein Fade-up (Spec 6.1 verbietet es ausdrücklich): jede Zeile steigt
   aus ihrer Maske, und ihre Linie zieht sich dabei von links auf —
   dieselben zwei Bewegungen wie am Band, nur nacheinander. Die
   Staffelung ist hier inhaltlich begründet, weil sie Fakt für Fakt
   einführt.

   Ausgelöst wird sie wie das Band von einem IntersectionObserver in
   js/bewegung.js, nicht von ScrollTrigger: die Tabelle steht auch auf
   dem Handy, und dort gibt es kein GSAP. Der Verzug je Zeile kommt von
   dort als --verzug.

   `.is-stufig` setzt das Skript nur, wenn es die Zeilen wirklich noch
   aufdecken wird. Wer schon an der Tabelle vorbei ist, bekommt sie
   unverändert — verstecken, um gleich wieder aufzudecken, wäre ein
   Zucken ohne Grund, und eine Tabelle, die aus Versehen versteckt
   bleibt, wäre schlimmer als gar keine Bewegung.                      */

.fakten.is-stufig .mask > span   { transform: translateY(108%); }
.fakten.is-stufig .fakt::before  { transform: scaleX(0); }

.fakten.is-auf .mask > span  { animation: zeile-auf .8s var(--ease) var(--verzug, 0s) both; }
.fakten.is-auf .fakt::before { animation: linie-auf .9s var(--ease) var(--verzug, 0s) both; }

/* ── 6.1 Register: angepinnt und gescrubbt ─────────────────────────
   Erst ab 62 rem, erst mit Skript, erst nach eingerichtetem Pin. Dann
   liegen die fünf Einträge im selben Rasterfeld übereinander und nur
   der aktive ist sichtbar — über opacity UND visibility, sonst liegen
   unsichtbare Texte übereinander und man liest Buchstabensalat
   (Spec 5.7).

   Darunter, ohne Skript und in der ruhigen Fassung, bleibt es bei den
   fünf Blöcken untereinander. Das ist keine Notfassung: jeder Block hat
   sein eigenes Bild und seinen eigenen Zähler.                        */

.register__index,
.register__fortschritt { display: none; }

@media (min-width: 62rem) {
  /* Spec 5.7: 46 % Bild, daneben Etikett, Name, Zeile. Die 52 % aus
     komponenten.css gelten für die ungefahrene Fassung, in der rechts
     kein Index steht. Der Wert steht hier als Variable, weil er die
     Spalte des Eintrags und die Lage der Fortschrittsspur bestimmt. */
  .register.is-gefahren { --reg-bild: 46%; }

  .register.is-gefahren .register__buehne {
    position: relative;
    height: 100svh;
    display: grid;
    grid-template-columns: minmax(0, 1fr) 13rem;
    gap: clamp(2rem, 4vw, 4rem);
    align-items: center;
    padding-right: var(--pad);
    overflow: hidden;
  }

  .register.is-gefahren .register__liste {
    grid-column: 1;
    grid-row: 1;
    display: grid;
    height: 100svh;
    padding: 0;
  }
  .register.is-gefahren .reg {
    grid-area: 1 / 1;
    min-height: 0;
    height: 100svh;
    opacity: 0;
    visibility: hidden;
    transition: opacity .45s var(--ease), visibility .45s;
  }
  .register.is-gefahren .reg.is-an {
    opacity: 1;
    visibility: visible;
  }
  .register.is-gefahren .reg__bild { height: 100svh; }

  /* Der Text kommt eine Spur später als das Bild — ein Versatz, kein
     zweiter Auftritt.

     Der rechte Innenabstand aus komponenten.css fällt hier weg: in
     der gefahrenen Fassung trennt schon der Spalt der Bühne (bis
     4 rem) die Textspalte vom stehenden Index. Beides zusammen waren
     bei 1024 px 84 px Luft an einer 337 px breiten Spalte — und
     genau die fehlten dem Namen für seine Zeile
     („Unterhaltsreini-gung", „Teppich, / Baustaub"). Die Spalte
     deckelt sich weiterhin bei 36 rem.                              */
  .register.is-gefahren .reg__text {
    padding-right: 0;
    transform: translateY(16px);
    transition: transform .55s var(--ease);
  }
  .register.is-gefahren .reg.is-an .reg__text { transform: none; }

  /* Der stehende Index: er zeigt, wo im Register man ist, und dass es
     noch weitergeht. Unterschieden wird über Farbe und Gewicht, nie
     über Deckkraft — halbtransparenter Text fällt unter den
     Kontrastwert (Spec 2). Er ist aria-hidden, weil er die fünf
     Überschriften nur wiederholt. */
  .register.is-gefahren .register__index {
    grid-column: 2;
    grid-row: 1;
    align-self: center;
    display: grid;
    gap: .55rem;
    margin: 0;
    padding: 0 0 0 1.2rem;
    list-style: none;
    border-left: 1px solid var(--haze);
  }
  .register.is-gefahren .register__index li {
    font-family: var(--disp);
    font-size: .8rem;
    font-weight: 500;
    font-stretch: 104%;
    color: var(--mute);
    transition: color .4s var(--ease);
  }
  .register.is-gefahren .register__index li.is-an {
    color: var(--aqua);
    font-weight: 700;
  }

  /* Fortschritt (Spec 5.9): 2 px, Spur in --cream bei 14 %, Füllung in
     --aqua. Die Breite schreibt bewegung.js am Scrub.

     Die Spur liegt in der Textspalte, nicht über der ganzen Bühne.
     Über die volle Breite begann sie links unter dem Bild: beim ersten
     Eintrag lag die Füllung (0–25 %) komplett hinter den opaken 46 %
     des Bildes und war schlicht nicht da — sichtbar wurde sie erst,
     wenn sie rechts aus dem Bild herauswuchs, also ungefähr ab dem
     zweiten Eintrag. Die Spur in 14 % Creme ist ohnehin für den
     dunklen Grund gezeichnet und nicht für ein Foto.

     Das z-index hält sie zusätzlich über den Einträgen. Auf der
     Leistungen-Seite stapeln die sich für den Wischer (z-index 1/2);
     ohne eigene Schicht lägen beide über der Spur.                   */
  .register.is-gefahren .register__fortschritt {
    grid-area: 1 / 1;
    align-self: end;
    z-index: 3;
    margin: 0 var(--pad) 1.6rem calc(var(--reg-bild) + var(--reg-spalt));
    display: block;
    height: 2px;
    background: color-mix(in srgb, var(--cream) 14%, transparent);
  }
  .register.is-gefahren .register__fortschritt span {
    display: block;
    height: 100%;
    width: 0%;
    /* Am Anfang der Folge ist der Fortschritt null — ohne Mindestmaß
       stünde beim ersten Eintrag eine leere Spur, und der Balken
       fängt wieder erst beim zweiten an zu existieren. Der Stummel
       zeigt, dass es ihn gibt und wo man ist. */
    min-width: .75rem;
    background: var(--aqua);
  }

  /* ── Die Folge der Leistungen (Spec 8.2) ─────────────────────────
     Dieselbe Mechanik, ein anderer Bildwechsel: der Motion-Moment
     dieser Seite ist ausdrücklich clip-path und nicht ein
     Überblenden. Das neue Bild wird von links nach rechts
     freigegeben und wischt das alte dabei zu — ein Wischer, der über
     die Fläche geht, dieselbe Geste wie der Ladewisch und der Hero.

     Damit das eine Kante bleibt und kein Loch, sind drei Dinge
     nötig, und alle drei waren beim ersten Versuch falsch:

     1. Der aktive Eintrag liegt oben (z-index). Sonst wischt beim
        Zurückscrollen der neue Eintrag unter dem alten und man sieht
        nichts.
     2. Der alte Eintrag bleibt stehen, bis der neue ihn zugedeckt
        hat: seine Deckkraft wird gar nicht angefasst, und visibility
        fällt erst nach der Fahrt (Verzögerung .62 s). Blendete er
        mit, stünde in der Mitte der Fahrt eine halbe leere Fläche.
     3. Die Texte wechseln trotzdem hart nacheinander — erst geht der
        alte, dann kommt der neue. Liefen sie gleichzeitig, läge
        „Unterhaltsreinigung" für einen Moment lesbar über
        „Sonderreinigung": genau der Buchstabensalat, vor dem
        Spec 5.7 warnt. visibility bleibt dabei an beiden Stellen
        gesetzt, damit nie zwei Aufzählungen zugleich im Baum stehen.

     Die Namen sind hier länger als auf der Startseite
     („Treppenhausreinigung" gegen „Fenster und Glas"), deshalb ein
     schmalerer Index: 10 rem reichen für die vier Zeilen und geben
     dem Namen die Spaltenbreite, die er braucht. Das Bild behält
     seine 46 % aus Spec 5.7.                                        */
  .register--wisch.is-gefahren .register__buehne {
    grid-template-columns: minmax(0, 1fr) 10rem;
  }

  .register--wisch.is-gefahren .reg {
    opacity: 1;
    visibility: hidden;
    z-index: 1;
    transition: visibility 0s linear .62s;
  }
  .register--wisch.is-gefahren .reg.is-an {
    visibility: visible;
    z-index: 2;
    transition: visibility 0s;
  }

  .register--wisch.is-gefahren .reg__bild {
    clip-path: inset(0 100% 0 0);
    /* Zufallen erst, wenn der Eintrag ohnehin verdeckt ist: die
       Kante gehört dem hereinfahrenden Bild allein. */
    transition: clip-path 0s linear .62s;
  }
  .register--wisch.is-gefahren .reg.is-an .reg__bild {
    clip-path: inset(0 0 0 0);
    transition: clip-path .62s var(--ease);
    will-change: clip-path;
  }

  .register--wisch.is-gefahren .reg__text {
    opacity: 0;
    visibility: hidden;
    transform: translateY(16px);
    transition: opacity .18s var(--ease), visibility .18s,
                transform .18s var(--ease);
  }
  .register--wisch.is-gefahren .reg.is-an .reg__text {
    opacity: 1;
    visibility: visible;
    transform: none;
    transition: opacity .34s var(--ease) .24s, visibility 0s,
                transform .5s var(--ease) .24s;
  }
}

/* ── 6.1 Querfahrt ─────────────────────────────────────────────────
   Gefahren wird die Spur mit transform, nicht mit scrollLeft. Solange
   das läuft, darf sie nicht zusätzlich selbst scrollen: sonst kämpfen
   Scroll-Snap und Pin gegeneinander und die Reihe zuckt.
   Ohne Skript und unter 62 rem bleibt sie die native Wischfläche mit
   scroll-snap — auf dem Handy ist das ohnehin die bessere Bedienung.  */

@media (min-width: 62rem) {
  .quer.is-gefahren .quer__spur {
    overflow: visible;
    scroll-snap-type: none;
    will-change: transform;
  }
  /* Das Bild läuft gegen die Fahrtrichtung. Den Spielraum dafür bringt
     die Tafel mit (112 % Breite, −6 % Versatz, komponenten.css 5.8) —
     aber ±6 % der Bildbreite sind 6,7 % der Tafelbreite, und an den
     Enden stand dadurch ein 4-px-Streifen Hintergrund frei. Deshalb
     hier etwas mehr Bild: 116 % bei −8 % Versatz deckt die Fahrt mit
     Rand ab. Nur in der gefahrenen Fassung, sonst wäre es überflüssige
     Bildbreite. */
  .quer.is-gefahren .karte__bild img {
    width: 116%;
    margin-left: -8%;
    will-change: transform;
  }
}

/* ── 6.1 Zeiger ────────────────────────────────────────────────────
   Nur ab 62 rem mit feinem Zeiger. Er ist aria-hidden und nimmt keine
   Ereignisse an; in Formularen bleibt der Systemcursor stehen (Spec 9).
   Über dem Hero wird er groß — dort greift in Schritt 4 der Wisch an,
   und der Zeiger kündigt ihn an.                                     */

.cursor { display: none; }

@media (hover: hover) and (pointer: fine) and (min-width: 62rem) {
  html.js .cursor {
    display: block;
    position: fixed;
    top: 0;
    left: 0;
    z-index: 140;
    pointer-events: none;
    /* difference statt einer festen Farbe: der Ring bleibt über Foto,
       --ink und --aqua gleich gut sichtbar. Er trägt keine Information,
       deshalb ist die unberechenbare Farbe hier unschädlich — anders
       als beim Zähler auf dem Bild (Spec 5.9). */
    mix-blend-mode: difference;
    will-change: transform;
  }
  .cursor__ring {
    display: block;
    width: 18px;
    height: 18px;
    margin: -9px 0 0 -9px;
    border: 1.5px solid var(--cream);
    border-radius: 50%;
    transition: width .3s var(--ease), height .3s var(--ease),
                margin .3s var(--ease);
  }
  .cursor.is-link .cursor__ring { width: 44px; height: 44px; margin: -22px 0 0 -22px; }
  .cursor.is-wisch .cursor__ring { width: 84px; height: 84px; margin: -42px 0 0 -42px; }
}

/* ── Das Hauptmenü als Vollbild (Spec 5.1) ─────────────────────────
   Nur unter 62 rem und nur mit Skript. Darüber liegen die drei Links
   offen im Kopf (css/komponenten.css) und es gibt nichts zu öffnen;
   ohne Skript stehen sie dort in der zweiten Kopfzeile. Was diese
   Datei hinzufügt, ist deshalb genau das, was es ohne JavaScript nicht
   gäbe: der Menüknopf, die Fläche und ihre Bewegung.

   Richtung d auch hier: dunkler Grund (--ink, nicht --grund — die
   Fläche bleibt dunkel, auch wenn sie über der hellen Fassung von
   Impressum und Datenschutz liegt), große Typografie, Messing genau
   dort, wo die laufende Seite steht.

   Gefahren wird mit CSS, nicht mit GSAP: Impressum, Datenschutz und
   die 404 laden bewusst keine Bibliothek (Spec 10), und das Menü muss
   auf allen sieben Seiten dasselbe sein. Zwei Transitions und ein
   Keyframe sind 0 KB JavaScript.

   Der Zustand hängt an `.menue.is-auf`, gesetzt von js/menue.js. Liegt
   die Fläche, ist sie `visibility: hidden` — damit fällt sie aus der
   Tastaturreihenfolge und aus dem Baum für Bildschirmleser, ohne dass
   ein Skript Elemente ein- und ausbauen muss.                        */

@media (max-width: 61.99rem) {
  html.js .menue__auf {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    position: relative;
    width: 44px;
    min-height: 44px;
    margin: 0;
    padding: 0;
    border: 0;
    background: none;
    color: inherit;
    cursor: pointer;
  }

  /* Zwei Striche, nicht drei: derselbe knappe Satz wie überall. Sie
     liegen in --schrift und nicht in --cream, weil der Knopf im Kopf
     sitzt — und der ist auf Impressum und Datenschutz hell. Die
     Fläche, die er öffnet, ist dagegen überall dunkel. */
  html.js .menue__striche {
    display: block;
    width: 22px;
    height: 9px;
    border-top: 2px solid var(--schrift);
    border-bottom: 2px solid var(--schrift);
    transition: border-color .3s var(--ease);
  }
  html.js .menue__auf:hover .menue__striche { border-color: var(--signal); }

  /* Solange das Menü liegt, steigt der ganze Kopf über die
     Daumenleiste. Der z-index der Fläche allein reicht dafür nicht:
     `.kopf` ist positioniert und hat selbst einen z-index, also ist es
     ein eigener Stapelkontext und alles darin wird gegen die 60 des
     Kopfes gerechnet, nicht gegen die 90 der Leiste. Die Fläche lag
     deshalb unter dem „Anrufen"-Balken. */
  html.menue-auf .kopf { z-index: 100; }

  /* Die Fläche. Drei Zeilen: Schließer oben in der Ecke, in der eben
     noch der Menüknopf stand, die Wege in der Mitte, die Nummer unten.
     Innerhalb des Kopfes liegt sie über Marke und Nummer — solange das
     Menü offen ist, ist es die ganze Seite, und die Rufzeile darin ist
     der Ersatz für die verdeckte Daumenleiste. */
  html.js .menue__flaeche {
    position: fixed;
    inset: 0;
    z-index: 100;
    display: grid;
    grid-template-rows: auto 1fr auto;
    gap: var(--raum-3);
    padding: 1.1rem var(--pad) calc(var(--raum-3) + env(safe-area-inset-bottom, 0px));
    background: var(--ink);
    color: var(--cream);
    visibility: hidden;
    clip-path: inset(0 0 100% 0);
    transition: clip-path .46s var(--ease), visibility 0s linear .46s;
  }
  html.js .menue.is-auf .menue__flaeche {
    visibility: visible;
    clip-path: inset(0 0 0 0);
    transition: clip-path .46s var(--ease), visibility 0s;
  }

  html.js .menue__zu {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    position: relative;
    justify-self: end;
    width: 44px;
    min-height: 44px;
    margin: 0;
    padding: 0;
    border: 0;
    background: none;
    color: inherit;
    cursor: pointer;
  }
  html.js .menue__kreuz {
    display: block;
    position: relative;
    width: 22px;
    height: 22px;
  }
  html.js .menue__kreuz::before,
  html.js .menue__kreuz::after {
    content: "";
    position: absolute;
    top: 10px;
    left: 0;
    width: 22px;
    height: 2px;
    background: var(--cream);
    transition: background .3s var(--ease);
  }
  html.js .menue__kreuz::before { transform: rotate(45deg); }
  html.js .menue__kreuz::after  { transform: rotate(-45deg); }
  html.js .menue__zu:hover .menue__kreuz::before,
  html.js .menue__zu:hover .menue__kreuz::after { background: var(--aqua); }

  /* Die Wege sitzen unten, nicht mittig: so liegen sie und die
     Rufzeile zusammen im Daumenbereich, und die Luft steht da, wo
     ohnehin niemand hinlangt. */
  html.js .menue__liste {
    display: grid;
    gap: clamp(.4rem, 2.2vh, 1.1rem);
    align-content: end;
  }
  html.js .menue__liste a {
    display: flex;
    align-items: baseline;
    gap: .7rem;
    font-size: clamp(2.2rem, 11vw, 3.6rem);
    font-weight: 700;
    font-stretch: 88%;
    letter-spacing: -.035em;
    line-height: 1.02;
    white-space: normal;
    color: var(--cream);
  }
  html.js .menue__liste a:hover { color: var(--aqua); text-decoration: none; }
  html.js .menue__liste a.is-an { color: var(--aqua); font-weight: 800; }

  /* Die Nummer benennt den Platz im Menü, wie der Zähler im Register.
     aria-hidden: sie steht im Link und wäre sonst Teil seines Namens. */
  html.js .menue__nr {
    display: block;
    flex: none;
    width: 1.9em;
    font-size: .78rem;
    font-weight: 700;
    font-stretch: 104%;
    letter-spacing: .06em;
    color: var(--mute);
  }
  html.js .menue__liste a.is-an .menue__nr { color: var(--aqua); }

  html.js .menue__ruf {
    display: block;
    margin: 0;
    border-top: 1px solid var(--haze);
    padding-top: var(--raum-2);
  }
  html.js .menue__ruf a {
    font-family: var(--disp);
    font-size: 1.24rem;
    font-weight: 700;
    font-stretch: 100%;
    letter-spacing: -.01em;
    text-decoration: none;
    white-space: nowrap;
  }
  html.js .menue__ruf a:hover { color: var(--aqua); }
  html.js .menue__ort {
    display: block;
    margin: .35rem 0 0;
    font-size: .82rem;
    color: var(--mute);
  }

  /* Der Stagger: die drei Wege steigen nach dem Vorhang der Fläche
     nach. Derselbe Griff wie bei den Hero-Knöpfen (`knopf-auf`) —
     eine Bewegung beim Erscheinen, kein halbtransparenter Ruhezustand.
     `backwards` hält sie unten, bis ihre Verzögerung abgelaufen ist. */
  html.js .menue.is-auf .menue__liste li {
    animation: menue-auf .5s var(--ease) backwards;
  }
  html.js .menue.is-auf .menue__liste li:nth-child(1) { animation-delay: .16s; }
  html.js .menue.is-auf .menue__liste li:nth-child(2) { animation-delay: .23s; }
  html.js .menue.is-auf .menue__liste li:nth-child(3) { animation-delay: .30s; }
  html.js .menue.is-auf .menue__ruf {
    animation: menue-auf .5s var(--ease) .37s backwards;
  }
  @keyframes menue-auf {
    from { transform: translateY(26px); opacity: 0; }
    to   { transform: translateY(0); opacity: 1; }
  }
}

/* Solange das Menü liegt, scrollt die Seite darunter nicht mit. Nur
   die Hochachse — `overflow-x: clip` aus base.css ist die Sicherung
   gegen seitwärts und bleibt stehen. */
html.menue-auf, html.menue-auf body { overflow-y: hidden; }

/* ── 6.3 Seitenwechsel ─────────────────────────────────────────────
   View Transitions API, gleicher Origin, Opt-in auf beiden Dokumenten.
   Weil jede Seite diese Datei lädt, ist jede Seite opt-in — und nur
   dann blendet der Browser beim Navigieren über. Kein SPA, kein
   Router, kein Ladescreen zwischen Seiten: der Browser navigiert nach
   wie vor, er zeichnet den Wechsel nur weich.

   Kennt ein Browser `@view-transition` nicht, überliest er die Regel
   und navigiert normal. Es gibt nichts nachzubauen und nichts
   abzufangen — das ist der Grund, warum dieser Übergang hier steht und
   keine Bibliothek dafür geladen wird.

   0,32 s: deutlich unter den 400 ms aus Spec 6.3. Die Kurve steht
   ausgeschrieben und nicht als var(--ease), weil die
   ::view-transition-Pseudos in einem eigenen Baum über dem Dokument
   liegen und nicht jeder Browser Custom Properties dorthin vererbt.

   Der Wechsel zwischen dunkler Seite und heller Fassung (Impressum,
   Datenschutz) ist damit eine Überblendung von 0,32 s statt eines
   Hell-Dunkel-Sprungs — genau die Stelle, an der der Übergang etwas
   nützt und nicht nur hübsch ist.                                     */

@view-transition { navigation: auto; }

::view-transition-old(root),
::view-transition-new(root) {
  animation-duration: .32s;
  animation-timing-function: cubic-bezier(.22, 1, .36, 1);
}

/* ── 6.5 Die ruhige Fassung ────────────────────────────────────────
   Kein Ladevorhang, kein Zeiger, kein Pin, keine Magnete, keine
   Parallaxe. Das Abschalten steht hier doppelt: bewegung.js richtet
   nichts davon ein, und falls die Einstellung erst nach dem Laden
   umgestellt wird, nimmt das CSS es trotzdem zurück.

   Das Ergebnis ist die Seite aus Schritt 2 — fünf Registerblöcke
   untereinander, die Querreihe als scrollbarer Streifen, jeder Text
   sofort da. Eine eigene, gute Fassung, kein Notbehelf.               */

@media (prefers-reduced-motion: reduce) {
  /* Dieselbe Spezifität wie das Opt-in oben (html.js.ankunft .vorhang),
     sonst gewänne die Ankunftsregel und der Vorhang läge doch da. */
  html.js.ankunft .vorhang { display: none; }
  html.js .cursor { display: none; }
  /* Kein Shader, kein Wisch, kein Hinweis darauf. bewegung.js baut den
     Wisch hier gar nicht auf und reißt ihn ab, wenn die Einstellung
     erst später umgestellt wird — diese zwei Zeilen sind die Sicherung
     dahinter. Das Heldenbild bleibt das statische Bild aus Schritt 3. */
  .hero__canvas { display: none; }
  .hero .hero__hin, .hero.is-wischbar .hero__hin { display: none; }
  html.lade, html.lade body { overflow: visible; }
  html.js .band__linie { transform: none; }
  html.js .hero .mask > span,
  html.js .hero__tun > *,
  html.js .hero__h1,
  html.js .auf .mask > span,
  html.js .auf__tun > *,
  html.js .auf__bild,
  html.js .anfrage--seite .mask > span { animation: none; }
  /* Der Strich der 404 liegt sofort da, statt sich aufzuziehen. */
  html.js .weg__strich { animation: none; transform: none; }
  .mask > span { transform: none !important; }
  html.js .band-satz .wort > i { transform: none; }
  /* Spec 8.3: Bild statisch, Tabelle ohne Staffelung. js/bewegung.js
     richtet beides hier gar nicht erst ein — das sind die Sicherungen
     dahinter, falls die Einstellung erst nach dem Laden umgestellt wird. */
  .aufzug__bild img { animation: none; transform: none; will-change: auto; }
  .fakten.is-stufig .fakt::before { transform: none; }
  .fakten.is-auf .mask > span,
  .fakten.is-auf .fakt::before { animation: none; }
  .register.is-gefahren .reg,
  .register.is-gefahren .reg__text,
  .register.is-gefahren .reg__bild,
  .register.is-gefahren .register__index li { transition-duration: 1ms; }

  /* Das Hauptmenü, ruhig (Spec 6.5 sinngemäß): kein Vorhang über die
     Fläche, kein Nachsteigen der Wege. Das Menü ist sofort da und
     sofort wieder weg — und es ist dieselbe Fläche, dieselbe Größe,
     dieselbe Farbe. Nur der Weg dorthin fehlt. */
  html.js .menue__flaeche,
  html.js .menue.is-auf .menue__flaeche {
    clip-path: none;
    transition: visibility 0s;
  }
  html.js .menue.is-auf .menue__liste li,
  html.js .menue.is-auf .menue__ruf { animation: none; }

  /* Kein Seitenübergang (Spec 6.5). `@view-transition` selbst lässt
     sich nicht in einer @media-Abfrage abschalten — das Opt-in bleibt
     also stehen, der Übergang bekommt aber keine Animation mehr und
     der Browser tauscht die Seiten in einem Zug aus. Sichtbar ist das
     dasselbe wie ohne View Transitions. */
  ::view-transition-group(*),
  ::view-transition-old(*),
  ::view-transition-new(*) { animation: none !important; }
}
