Zum Inhalt
← Scroll & Section-Animationen

Zoom-Parallax / Zoom-through

Eine Lage-Collage in einer gepinnten Bühne: Beim Scrollen wächst jede Kachel mit eigenem Endfaktor, die Ränder fliegen hinaus, das Kernbild füllt am Ende den Viewport.

  1. Eine Bühne ist gepinnt (sticky, 100vh) und liegt in einer Strecke von 300vh.
  2. Darin liegen mehrere Bilder als Collage um ein Kernbild.
  3. Beim Scrollen skalieren alle Bilder vom Zentrum aus, jedes mit eigenem Endfaktor (z. B. 4, 5, 6, 8, 9).
  4. Die Randbilder fliegen dadurch aus dem Bild, das Kernbild füllt am Ende den Viewport. Es fühlt sich an wie ein Flug durch die Collage.

Unterschiedliche Endfaktoren erzeugen Tiefe ohne 3D: Was schneller wächst, scheint näher zu sein. Der Übergang ist dramatisch und braucht wenig Code.

Wann einsetzen

Als Galerie-Intro, in Fotografie- und Architektur-Portfolios oder als Übergang in eine Detail-Section. Gut für einen einzigen großen Moment auf einer Seite.

Wann nicht

Bei niedrig aufgelösten Rasterbildern, denn Faktor 9 zeigt Pixel. Bei vielen schweren Bildern auf Mobilgeräten. Bei Werk- oder Objektfotos, deren Ausschnitt nicht verändert werden darf.

Varianten

  • Collage-Zoom mit unterschiedlichen Endfaktoren
  • Mit Unschärfe auf den Randbildern
  • Durch Typografie hindurch: Wörter flankieren das Kernbild und fliegen beim Zoom nach außen
  • 3D-Variante mit translateZ und perspective

Umsetzung

Jede Ebene liegt absolut über der ganzen Bühne (inset: 0; display: grid; place-items: center), die Kachel darin wird per translate versetzt. Skaliert wird die Ebene, nicht die Kachel: Dann wächst alles vom Viewport-Zentrum aus und der Versatz wandert mit nach außen. Den Fortschritt liefert eine Scroll-Zeitachse: .strecke { height: 300vh; view-timeline-name: --zoom } .buehne { animation: p linear both; animation-timeline: --zoom; animation-range: contain } mit @property --p und @keyframes p { to { --p: 1 } }. Die Ebenen rechnen scale: calc(1 + (var(--s) - 1) * var(--p)). Alternativ direkt @keyframes z { to { scale: var(--s) } } je Ebene. Ohne animation-timeline setzt ein Scroll-Listener --p. Der Kern endet bei 25vw × 25vh mal 4 genau formatfüllend. Vektorgrafiken bleiben scharf, Rasterbilder in hoher Auflösung liefern; will-change: transform nur während der Animation.

Robust umsetzen

Telefon. Weniger Bilder (3 bis 6) und kleinere Endfaktoren. Das Kernbild im Hochformat breiter anlegen (z. B. 40vw × 25vh), damit Faktor 4 den Bildschirm füllt. Bildgrößen per srcset.

Ohne Bewegung. Keine Strecke und keine Skalierung. Die Collage steht als statisches Raster, das Kernbild groß oben.

Barrierefrei. Dekorative Randbilder mit alt="" oder aria-hidden. Das Kernbild bekommt eine Beschreibung. Links im Endzustand erst klickbar machen, wenn sie sichtbar sind.

Code

<!DOCTYPE html>
<!--
  Zoom-Parallax / Zoom-through
  Fundus · Scroll & Section-Animationen · https://cecodesign.de/lab/fundus/scroll/zoom-parallax/
  Eigenständige Datei. Bilder liegen hier unter bilder/ – eigene einsetzen.
-->
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Zoom-Parallax / Zoom-through · Fundus-Demo</title>
<script>document.documentElement.classList.add('js');if(matchMedia('(prefers-reduced-motion: reduce)').matches)document.documentElement.classList.add('reduziert')</script>
<style>
@font-face { font-family: 'Libre Caslon Display'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/libre-caslon-display@latest/latin-400-normal.woff2') format('woff2'); font-weight: 400; font-display: swap; }
@font-face { font-family: 'Hanken Grotesk'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/hanken-grotesk:vf@latest/latin-wght-normal.woff2') format('woff2-variations'); font-weight: 100 900; font-display: swap; }
@font-face { font-family: 'Geist Mono'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/geist-mono:vf@latest/latin-wght-normal.woff2') format('woff2-variations'); font-weight: 100 900; font-display: swap; }

/* Fortschritt 0 → 1 über die Strecke; registriert, damit eine Scroll-Zeitachse ihn animieren kann */
@property --p { syntax: '<number>'; inherits: true; initial-value: 0; }

:root {
  --leinen: #fcfaf5; --sand: #efe8df; --marke: #02385c; --text: #313131; --linie: #b5b2af; --cta: #ed7f1a;
  --ui: cubic-bezier(0.165, 0.84, 0.44, 1);   /* easeOutQuart, nur für UI (400 ms) */
  --serif: 'Libre Caslon Display', 'Times New Roman', serif;
  --grotesk: 'Hanken Grotesk', system-ui, sans-serif;
}
* { box-sizing: border-box; margin: 0; }
html { background: var(--leinen); color: var(--text); font-family: var(--grotesk); -webkit-font-smoothing: antialiased; }
body { overflow-x: clip; }
.versal { font: 600 .75rem/1.3 var(--grotesk); letter-spacing: .09em; text-transform: uppercase; }
.ziffern { font-variant-numeric: tabular-nums lining-nums; }

/* ---------- Einstieg ---------- */
.einstieg { min-height: auto; display: grid; grid-template-rows: auto 1fr auto; gap: 40px; padding: clamp(18px, 3vw, 36px) clamp(20px, 4vw, 56px) 40px; }
.kopf { display: flex; justify-content: space-between; align-items: center; gap: 20px; padding-bottom: 18px; border-bottom: 1px solid rgba(2, 56, 92, .14); }
.marke { display: flex; align-items: baseline; gap: 12px; color: var(--marke); text-decoration: none; }
.marke b { font: 400 1.75rem/1 var(--serif); letter-spacing: -.005em; }
.marke span { color: #6b6864; }
.navi { display: flex; gap: clamp(14px, 2.2vw, 32px); list-style: none; padding: 0; }
.navi a { color: var(--text); text-decoration: none; font-size: .9375rem; transition: color .2s var(--ui); }
.navi a:hover { color: var(--marke); }
.navi a[aria-current] { color: var(--marke); text-decoration: underline; text-underline-offset: 6px; text-decoration-thickness: 1px; }
.einstieg__mitte { padding-top: clamp(40px, 11vh, 120px); display: grid; grid-template-columns: minmax(0, 7fr) minmax(0, 4fr); gap: clamp(28px, 6vw, 96px); align-items: end; }
.einstieg h1 { font: 400 clamp(2.25rem, 1.4rem + 3.2vw, 4.5rem)/1.08 var(--serif); letter-spacing: -.005em; color: var(--marke); margin: 18px 0 24px; max-width: 12ch; text-wrap: balance; }
.einstieg p.lead { font-size: 1.0625rem; line-height: 1.55; max-width: 46ch; }
.eckdaten { display: grid; gap: 0; border-top: 1px solid var(--linie); }
.eckdaten div { display: flex; justify-content: space-between; gap: 16px; padding: 12px 0; border-bottom: 1px solid rgba(181, 178, 175, .6); }
.eckdaten dt { color: #6e6b67; }
.eckdaten dd { color: var(--marke); font-weight: 500; text-align: right; }
.einstieg__fuss { display: flex; justify-content: space-between; color: #6b6864; }

/* ---------- Zoom-Strecke ---------- */
.strecke { position: relative; height: 300vh; background: var(--sand); view-timeline-name: --zoom; }
.buehne { position: sticky; top: 0; height: 100vh; height: 100svh; overflow: clip; --p: 0; }
.zeitachse .buehne { animation: fortschritt linear both; animation-timeline: --zoom; animation-range: contain 0% contain 100%; }
/* Vorführung: Fortschritt kommt aus JS, die Seite steht still */
.zeitachse .strecke.simuliert .buehne { animation: none; }
@keyframes fortschritt { from { --p: 0; } to { --p: 1; } }

/* Jede Ebene liegt über der ganzen Bühne und skaliert vom Bühnenzentrum aus.
   Skaliert wird die Ebene, nicht die Kachel: so wandert der Versatz mit nach außen. */
.ebene { position: absolute; inset: 0; display: grid; place-items: center; pointer-events: none;
  scale: calc(1 + (var(--s) - 1) * var(--p)); transition: opacity .4s var(--ui); }
.kachel { position: relative; width: var(--b); height: var(--h); translate: var(--x, 0) var(--y, 0); overflow: hidden; background: #d9d0c4; }
.kachel svg { display: block; width: 100%; height: 100%; }

/* Collage, Desktop: Kern 25vw × 25vh, Endfaktor 4 füllt genau den Viewport */
.e-kern { --s: 4; }   .k-kern { --b: 25vw; --h: 25vh; }
.e-alpen { --s: 5; }  .k-alpen { --b: 35vw; --h: 30vh; --x: 5vw; --y: -30vh; }
.e-steg { --s: 6; }   .k-steg { --b: 20vw; --h: 45vh; --x: -25vw; --y: -10vh; }
.e-takt { --s: 5; }   .k-takt { --b: 25vw; --h: 25vh; --x: 27.5vw; --y: 0vh; }
.e-plan { --s: 6; }   .k-plan { --b: 20vw; --h: 25vh; --x: 5vw; --y: 27.5vh; }
.e-lage { --s: 8; }   .k-lage { --b: 30vw; --h: 25vh; --x: -22.5vw; --y: 27.5vh; }
.e-holz { --s: 9; }   .k-holz { --b: 15vw; --h: 15vh; --x: 25vw; --y: 22.5vh; }

/* Variante: Unschärfe auf den Randbildern (wird mit der Ebene mitskaliert) */
.strecke[data-variante='unschaerfe'] .ebene:not(.e-kern) .kachel { filter: blur(calc(var(--p) * 1.4px)); }
/* Variante: durch die Schrift, nur Kernbild und Wörter */
.strecke[data-variante='schrift'] .ebene:not(.e-kern) { opacity: 0; }
.schrift { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; gap: calc(25vw + 3.2vw); pointer-events: none;
  --s: 5; scale: calc(1 + (var(--s) - 1) * var(--p)); opacity: 0; transition: opacity .4s var(--ui);
  font: 400 clamp(2.5rem, 7.4vw, 8.5rem)/1 var(--serif); letter-spacing: -.01em; color: var(--marke); white-space: nowrap; }
.schrift span:first-child { flex: 1; text-align: right; }
.schrift span:last-child { flex: 1; text-align: left; }
.strecke[data-variante='schrift'] .schrift { opacity: clamp(0, (.62 - var(--p)) * 4, 1); }

/* Kachel mit Kennzahl (HTML, skaliert über Container-Einheiten) */
.k-takt { background: var(--marke); color: var(--leinen); container-type: size; }
.takt { position: absolute; inset: 0; display: grid; grid-template-rows: auto 1fr auto; padding: 7cqmin 8cqmin; }
.takt .versal { font-size: max(7px, min(6.4cqh, 5cqw)); color: rgba(252, 250, 245, .72); }
.takt b { align-self: center; font: 400 min(50cqh, 30cqw)/.9 var(--serif); letter-spacing: -.02em; }
.takt b small { font-size: .3em; margin-left: .18em; letter-spacing: 0; }

/* Beschriftung auf der Bühne (skaliert nicht) */
.buehnen-label { position: absolute; z-index: 3; color: var(--marke); }
.bl-oben { top: clamp(16px, 3vh, 28px); left: clamp(20px, 3vw, 44px); display: grid; gap: 6px; }
.bl-oben b { font: 400 1.5rem/1 var(--serif); }
.bl-unten { right: clamp(20px, 3vw, 44px); bottom: clamp(16px, 3vh, 28px); text-align: right; opacity: clamp(0, 1 - var(--p) * 7, 1); }
.bl-oben { opacity: clamp(0, 1 - (var(--p) - .55) * 4, 1); }

/* Abschluss über dem formatfüllenden Kernbild */
.abschluss { position: absolute; inset: auto 0 0 0; z-index: 4; padding: clamp(24px, 6vh, 64px) clamp(20px, 4vw, 56px);
  display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 24px 48px; align-items: end; color: var(--leinen);
  --a: clamp(0, (var(--p) - .84) * 7, 1); opacity: var(--a); translate: 0 calc((1 - var(--a)) * 28px); pointer-events: none; }
.abschluss.aktiv { pointer-events: auto; }
.abschluss::before { content: ''; position: absolute; inset: -40vh 0 0 0; z-index: -1; background: linear-gradient(to top, rgba(9, 17, 22, .72), rgba(9, 17, 22, .38) 46%, rgba(9, 17, 22, 0)); }
.abschluss h2 { font: 400 clamp(2.25rem, 1.4rem + 3.2vw, 4.5rem)/1.08 var(--serif); letter-spacing: -.005em; margin: 10px 0 14px; }
.abschluss p { font-size: 1.0625rem; line-height: 1.55; max-width: 44ch; color: rgba(252, 250, 245, .86); }
.abschluss ul { list-style: none; padding: 0; display: flex; flex-wrap: wrap; gap: 6px 22px; margin-top: 18px; color: rgba(252, 250, 245, .92); }
.abschluss .tun { display: grid; justify-items: end; gap: 14px; }
.knopf { display: inline-flex; align-items: center; gap: 10px; height: 48px; padding: 0 22px; border-radius: 2px; text-decoration: none; font-weight: 600; font-size: .9375rem;
  background: var(--cta); color: #1a1a1a; transition: background-color .2s var(--ui); }
.knopf:hover { background: #f39243; }
.knopf:focus-visible, .navi a:focus-visible, .pult button:focus-visible, .leise-link:focus-visible { outline: 2px solid var(--marke); outline-offset: 3px; }
.abschluss .knopf:focus-visible { outline-color: var(--leinen); }
.leise-link { color: var(--leinen); font-size: .9375rem; text-underline-offset: 5px; }
.abschluss small { font: 500 10px/1.3 'Geist Mono', ui-monospace, monospace; letter-spacing: .14em; text-transform: uppercase; color: rgba(252, 250, 245, .6); }

/* Demo-Pult (nur innerhalb der Bühne sichtbar) */
.pult { position: absolute; z-index: 5; top: clamp(16px, 3vh, 28px); right: clamp(20px, 3vw, 44px); display: grid; gap: 8px; justify-items: end;
  font: 500 10.5px/1.3 'Geist Mono', ui-monospace, monospace; letter-spacing: .14em; text-transform: uppercase; color: rgba(2, 56, 92, .7); }
.pult__knoepfe { display: flex; gap: 4px; padding: 4px; border-radius: 999px; background: rgba(252, 250, 245, .78); -webkit-backdrop-filter: blur(10px); backdrop-filter: blur(10px); }
.pult button { font: inherit; letter-spacing: inherit; text-transform: inherit; color: inherit; background: none; border: 0; border-radius: 999px; height: 28px; padding: 0 12px; cursor: pointer;
  transition: background-color .2s var(--ui), color .2s var(--ui); }
.pult button:hover { color: var(--marke); }
.pult button[aria-pressed='true'] { background: var(--marke); color: var(--leinen); }
.pult__werte { display: grid; gap: 3px; text-align: right; padding: 6px 10px; border-radius: 6px; background: rgba(252, 250, 245, .62); }
.pult__werte b { color: var(--marke); font-weight: 600; }

/* ---------- Lage ---------- */
.lage { padding: clamp(64px, 12vh, 140px) clamp(20px, 4vw, 56px) 56px; display: grid; gap: 48px; }
.lage__kopf { display: grid; grid-template-columns: minmax(0, 7fr) minmax(0, 4fr); gap: clamp(28px, 6vw, 96px); align-items: end; }
.lage h2 { font: 400 clamp(2.25rem, 1.4rem + 2.4vw, 3.5rem)/1.08 var(--serif); color: var(--marke); margin-top: 14px; max-width: 18ch; }
.lage__kopf p { font-size: 1.0625rem; line-height: 1.55; }
.zahlen { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); border-top: 1px solid var(--linie); }
.zahlen div { padding: 22px 20px 0 0; display: grid; gap: 8px; }
.zahlen b { font: 400 clamp(2.25rem, 1.6rem + 1.8vw, 3.25rem)/1 var(--serif); color: var(--marke); }
.zahlen span { color: #5e5b57; font-size: .9375rem; line-height: 1.45; }
.pflicht { font-size: .8125rem; line-height: 1.6; color: #6e6b67; border-top: 1px solid rgba(181, 178, 175, .6); padding-top: 18px; }

/* ---------- Vorschau: nur die Bühne ---------- */
.vorschau body { overflow: hidden; }
.vorschau .einstieg, .vorschau .lage { display: none; }

/* ---------- Telefon ---------- */
@media (max-width: 640px) {
  .navi { display: none; }
  .einstieg__mitte, .lage__kopf { grid-template-columns: 1fr; }
  .einstieg { min-height: auto; gap: 32px; }
  .einstieg__fuss span:last-child { display: none; }
  .e-kern { --s: 4; }   .k-kern { --b: 40vw; --h: 25vh; }
  .e-alpen { --s: 4.5; } .k-alpen { --b: 70vw; --h: 18vh; --x: 5vw; --y: -30vh; }
  .e-steg { --s: 5; }   .k-steg { --b: 22vw; --h: 30vh; --x: -35vw; --y: -3vh; }
  .e-takt { --s: 5; }   .k-takt { --b: 24vw; --h: 22vh; --x: 35vw; --y: -4vh; }
  .e-plan { --s: 6; }   .k-plan { --b: 50vw; --h: 21vh; --x: -15vw; --y: 27vh; }
  .e-holz { --s: 6; }   .k-holz { --b: 26vw; --h: 15vh; --x: 30vw; --y: 22vh; }
  .e-lage { display: none; }
  .schrift { flex-direction: column; gap: calc(25vh + 2vh); font-size: 15vw; }
  .schrift span { flex: 0 0 auto !important; text-align: center !important; }
  .abschluss { grid-template-columns: 1fr; }
  .abschluss .tun { justify-items: start; }
  .abschluss ul { gap: 4px 16px; font-size: .9375rem; }
  .zahlen { grid-template-columns: repeat(2, minmax(0, 1fr)); row-gap: 28px; }
  .bl-unten { display: none; }
}
/* Touch ohne Hover (auch Tablets): weniger Bilder, kleinere Endfaktoren */
@media (hover: none) {
  .e-lage { display: none; }
  .e-holz { --s: 6; }
  .pult__werte span:nth-child(3) { display: none; }
}

/* ---------- Reduzierte Bewegung: statische Collage als Raster ---------- */
.reduziert .strecke { height: auto; }
.reduziert .buehne { position: relative; height: auto; overflow: visible; animation: none; --p: 0;
  display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 14px; padding: 96px clamp(20px, 4vw, 56px) 56px; }
.reduziert .ebene { position: static; display: block; place-items: normal; scale: none; opacity: 1 !important; transition: none; }
.reduziert .e-lage { display: block; }
.reduziert .kachel { width: 100%; height: auto; translate: none; aspect-ratio: 4 / 3; filter: none !important; }
.reduziert .e-kern { grid-column: 1 / -1; order: -1; }
.reduziert .k-kern { aspect-ratio: 16 / 8; }
.reduziert .schrift, .reduziert .bl-unten, .reduziert .pult__knoepfe { display: none; }
.reduziert .bl-oben { position: absolute; top: 32px; opacity: 1; }
.reduziert .pult { top: 32px; }
.reduziert .abschluss { position: relative; grid-column: 1 / -1; order: -1; margin-top: -14px; opacity: 1; translate: none; pointer-events: auto; background: #0f1c24; }
.reduziert .abschluss::before { display: none; }
@media (max-width: 640px) {
  .reduziert .buehne { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .reduziert .e-holz { display: none; }
}
@media (forced-colors: active) { button, [role=button], [role=tab], [role=switch] { border: 1px solid ButtonText !important; } }
</style>
</head>
<body>

<header class="einstieg">
  <div class="kopf">
    <a class="marke" href="#" aria-label="Haldenbach Immobilien, Startseite"><b>Haldenbach</b><span class="versal">Immobilien · Starnberg</span></a>
    <nav aria-label="Hauptnavigation">
      <ul class="navi">
        <li><a href="#">Kaufen</a></li>
        <li><a href="#">Verkaufen</a></li>
        <li><a href="#">Bewerten</a></li>
        <li><a href="#" aria-current="page">Neubau</a></li>
        <li><a href="#">Kontakt</a></li>
      </ul>
    </nav>
  </div>
  <div class="einstieg__mitte">
    <div>
      <p class="versal" style="color: var(--marke)">Neubau · Starnberg-Percha</p>
      <h1>Sechs Wohnungen, ein Ufer.</h1>
      <p class="lead">Das Uferhaus Percha entsteht auf einem Seegrundstück mit altem Buchenbestand, 150 Meter vom Wasser. Was die Lage ausmacht, sieht man erst aus der Nähe.</p>
    </div>
    <dl class="eckdaten ziffern">
      <div><dt>Wohnungen</dt><dd>6</dd></div>
      <div><dt>Wohnfläche</dt><dd>96–214 m²</dd></div>
      <div><dt>Zimmer</dt><dd>3–5</dd></div>
      <div><dt>Kaufpreis</dt><dd>ab 1.290.000 €</dd></div>
      <div><dt>Bezug</dt><dd>Herbst 2028</dd></div>
    </dl>
  </div>
  <div class="einstieg__fuss versal"><span>Näher heran ↓</span><span>Visualisierungen, Stand 10/2026</span></div>
</header>

<section class="strecke" data-variante="collage" aria-label="Lage des Uferhaus Percha">
  <div class="buehne">
    <div class="ebene e-kern"><figure class="kachel k-kern">
      <svg viewBox="0 0 1600 1000" preserveAspectRatio="xMidYMid slice" role="img" aria-label="Visualisierung: Uferhaus Percha von der Seeseite am Abend, beleuchtetes Erdgeschoss, Holzfassade, dahinter Buchen, rechts See und Berge">
        <defs>
          <linearGradient id="kh" x1="0" y1="0" x2="0" y2="520" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#1c3550"/><stop offset=".42" stop-color="#5f7f99"/><stop offset=".78" stop-color="#d4bfa6"/><stop offset="1" stop-color="#f2d3ab"/></linearGradient>
          <radialGradient id="kg" cx="1250" cy="575" r="520" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#ffe2b8" stop-opacity=".95"/><stop offset=".5" stop-color="#f6cf9f" stop-opacity=".35"/><stop offset="1" stop-color="#f6cf9f" stop-opacity="0"/></radialGradient>
          <linearGradient id="ks" x1="0" y1="575" x2="0" y2="1000" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#d9c2a4"/><stop offset=".18" stop-color="#8ea2ad"/><stop offset=".6" stop-color="#3f5a6e"/><stop offset="1" stop-color="#1d3142"/></linearGradient>
          <linearGradient id="kw" x1="0" y1="640" x2="0" y2="1000" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#2d3b30"/><stop offset="1" stop-color="#121a15"/></linearGradient>
          <linearGradient id="kl" x1="0" y1="470" x2="0" y2="640" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#fdebc6"/><stop offset=".55" stop-color="#f1c88e"/><stop offset="1" stop-color="#c98a52"/></linearGradient>
          <linearGradient id="klo" x1="0" y1="372" x2="0" y2="452" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#f8dcac"/><stop offset="1" stop-color="#d8a06a"/></linearGradient>
          <pattern id="klam" width="15" height="20" patternUnits="userSpaceOnUse"><rect width="15" height="20" fill="#4e3a2a"/><rect x="2" width="8" height="20" fill="#8b6a4b"/><rect x="2" width="2" height="20" fill="#a2805e"/></pattern>
          <linearGradient id="kschein" x1="0" y1="640" x2="0" y2="800" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#f6cf96" stop-opacity=".34"/><stop offset="1" stop-color="#f6cf96" stop-opacity="0"/></linearGradient>
          <linearGradient id="ksteg" x1="0" y1="640" x2="0" y2="1000" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#9c8a74"/><stop offset="1" stop-color="#5b4c3d"/></linearGradient>
        </defs>
        <rect width="1600" height="560" fill="url(#kh)"/>
        <g transform="translate(0 -90)">
        <rect y="-200" width="1600" height="820" fill="url(#kg)"/>
        <path d="M0 540 L90 522 L170 500 L250 512 L340 470 L420 492 L520 455 L610 488 L700 470 L780 500 L860 478 L960 512 L1600 512 L1600 590 L0 590Z" fill="#8a9dad" opacity=".6"/>
        <path d="M820 585 L905 505 L950 528 L1030 446 L1080 478 L1150 392 L1205 440 L1262 410 L1330 470 L1405 438 L1470 492 L1540 470 L1600 490 L1600 590 L820 590Z" fill="#6c8396"/>
        <path d="M1150 392 L1176 418 L1162 415 L1150 428 L1136 414 L1124 420Z M1262 410 L1284 432 L1270 430 L1258 440 L1247 426Z M1030 446 L1050 466 L1036 463 L1026 472 L1016 462Z" fill="#e3e9ec" opacity=".75"/>
        <path d="M760 590 L840 572 L930 580 L1040 566 L1160 578 L1290 564 L1420 576 L1600 568 L1600 600 L760 600Z" fill="#33485a"/>
        <rect y="592" width="1600" height="508" fill="url(#ks)"/>
        <ellipse cx="1250" cy="616" rx="320" ry="16" fill="url(#kg)" opacity=".6"/>
        <g fill="#ffe7c2" opacity=".5"><rect x="1080" y="640" width="120" height="2"/><rect x="1230" y="652" width="170" height="2"/><rect x="1150" y="668" width="90" height="2"/><rect x="1300" y="684" width="120" height="2"/><rect x="1190" y="704" width="70" height="2"/><rect x="1380" y="722" width="80" height="2"/></g>
        <g fill="#22323a"><ellipse cx="200" cy="470" rx="190" ry="210"/><ellipse cx="420" cy="380" rx="170" ry="230"/><ellipse cx="640" cy="410" rx="150" ry="200"/><ellipse cx="820" cy="470" rx="120" ry="160"/><ellipse cx="40" cy="520" rx="120" ry="160"/></g>
        <g fill="#1a262c"><ellipse cx="300" cy="520" rx="160" ry="150"/><ellipse cx="560" cy="520" rx="150" ry="140"/><ellipse cx="900" cy="560" rx="80" ry="90"/><ellipse cx="110" cy="590" rx="140" ry="110"/></g>
        <path d="M0 640 L980 640 C1080 640 1150 668 1230 712 C1330 768 1460 812 1600 830 L1600 1100 L0 1100Z" fill="url(#kw)"/>
        <path d="M1145 1090 L1290 1090 L1352 650 L1330 650Z" fill="url(#ksteg)"/>
        <g stroke="#3e3328" stroke-width="2" opacity=".6"><path d="M1166 950 L1296 950 M1180 880 L1306 880 M1192 820 L1315 820 M1203 770 L1322 770 M1212 728 L1328 728 M1219 696 L1334 696 M1225 672 L1340 672"/></g>
        <g fill="#2b231c"><rect x="1328" y="642" width="4" height="18"/><rect x="1350" y="642" width="4" height="18"/></g>
        <path d="M300 650 L1000 650 L1080 700 L240 700Z" fill="#b9afa1" opacity=".55"/>
        <path d="M330 650 L960 650 L1100 760 L180 760Z" fill="url(#kschein)"/>
        <rect x="360" y="470" width="580" height="172" fill="url(#kl)"/>
        <g fill="#5d3f2a" opacity=".45"><rect x="430" y="596" width="190" height="40" rx="6"/><rect x="452" y="578" width="150" height="24" rx="6"/><rect x="760" y="560" width="70" height="76" rx="3"/></g>
        <g stroke="#3a2c22" stroke-width="3" opacity=".55"><path d="M705 520 L705 636 M690 636 L720 636"/></g>
        <circle cx="705" cy="516" r="14" fill="#fff2d6"/>
        <rect x="660" y="500" width="70" height="6" fill="#3a2c22" opacity=".35"/>
        <g stroke="#2a221c" stroke-width="5"><path d="M440 470 V642 M520 470 V642 M600 470 V642 M680 470 V642 M760 470 V642 M840 470 V642"/></g>
        <rect x="356" y="466" width="588" height="6" fill="#2a221c"/>
        <rect x="340" y="456" width="616" height="12" fill="#e9e3d9"/>
        <rect x="410" y="350" width="520" height="106" fill="url(#klam)"/>
        <rect x="590" y="372" width="250" height="80" fill="url(#klo)"/>
        <g stroke="#2a221c" stroke-width="4"><path d="M673 372 V452 M756 372 V452"/></g>
        <rect x="590" y="372" width="250" height="80" fill="url(#klam)" opacity=".38"/>
        <rect x="396" y="338" width="548" height="12" fill="#ece6dc"/>
        <rect x="330" y="640" width="640" height="10" fill="#ddd5c9"/>
        <path d="M356 640 L356 470" stroke="#e9e3d9" stroke-width="8"/>
        </g>
      </svg>
    </figure></div>

    <div class="ebene e-alpen" aria-hidden="true"><div class="kachel k-alpen">
      <svg viewBox="0 0 800 500" preserveAspectRatio="xMidYMid slice">
        <defs><linearGradient id="ah" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#b7c7d2"/><stop offset=".7" stop-color="#ecdcc5"/><stop offset="1" stop-color="#f4e2c8"/></linearGradient>
        <linearGradient id="as" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#a9b9c3"/><stop offset="1" stop-color="#5e788b"/></linearGradient></defs>
        <rect width="800" height="500" fill="url(#ah)"/>
        <path d="M0 300 L70 250 L130 270 L210 190 L260 225 L330 150 L380 200 L450 170 L520 230 L590 196 L660 250 L740 220 L800 250 V420 H0Z" fill="#a6b6c3"/>
        <path d="M330 150 L352 172 L340 170 L330 180 L318 168Z M210 190 L228 206 L216 205 L208 212 L198 204Z M450 170 L468 186 L456 185 L446 194 L438 184Z" fill="#f2f4f4"/>
        <path d="M0 340 L90 300 L170 320 L260 270 L330 300 L420 262 L500 300 L600 280 L700 320 L800 300 V420 H0Z" fill="#7e94a6"/>
        <path d="M0 380 L120 352 L240 370 L360 345 L480 368 L620 350 L800 372 V420 H0Z" fill="#4f667a"/>
        <rect y="400" width="800" height="100" fill="url(#as)"/>
        <rect y="398" width="800" height="6" fill="#2f4252" opacity=".7"/>
        <path d="M560 446 L560 410 L578 444Z" fill="#fbf7ef"/><rect x="548" y="446" width="36" height="4" fill="#33424d"/>
        <g fill="#f8ead3" opacity=".55"><rect x="120" y="430" width="160" height="2"/><rect x="380" y="458" width="120" height="2"/><rect x="620" y="476" width="110" height="2"/></g>
      </svg>
    </div></div>

    <div class="ebene e-steg" aria-hidden="true"><div class="kachel k-steg">
      <svg viewBox="0 0 600 900" preserveAspectRatio="xMidYMid slice">
        <defs><linearGradient id="sh" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#eef0ee"/><stop offset="1" stop-color="#cbd5d7"/></linearGradient>
        <linearGradient id="sw" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#c3cfd2"/><stop offset="1" stop-color="#7f98a3"/></linearGradient>
        <linearGradient id="sn" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#fff" stop-opacity="0"/><stop offset=".5" stop-color="#fff" stop-opacity=".75"/><stop offset="1" stop-color="#fff" stop-opacity="0"/></linearGradient>
        <clipPath id="sc"><path d="M120 900 L480 900 L312 470 L288 470Z"/></clipPath></defs>
        <rect width="600" height="470" fill="url(#sh)"/>
        <path d="M0 450 C120 440 200 446 300 438 C420 430 500 444 600 436 V470 H0Z" fill="#a9b8bd" opacity=".7"/>
        <rect y="468" width="600" height="432" fill="url(#sw)"/>
        <rect y="400" width="600" height="120" fill="url(#sn)"/>
        <path d="M120 900 L480 900 L312 470 L288 470Z" fill="#6c5e50"/>
        <g clip-path="url(#sc)" stroke="#54483d" stroke-width="3"><path d="M0 860 H600 M0 800 H600 M0 748 H600 M0 704 H600 M0 666 H600 M0 634 H600 M0 606 H600 M0 582 H600 M0 562 H600 M0 545 H600 M0 530 H600 M0 517 H600 M0 506 H600 M0 496 H600 M0 488 H600 M0 481 H600"/></g>
        <g fill="#3f352c"><rect x="104" y="820" width="14" height="80"/><rect x="482" y="820" width="14" height="80"/><rect x="196" y="640" width="8" height="40"/><rect x="396" y="640" width="8" height="40"/><rect x="250" y="540" width="4" height="18"/><rect x="346" y="540" width="4" height="18"/></g>
        <rect y="610" width="600" height="80" fill="url(#sn)" opacity=".5"/>
      </svg>
    </div></div>

    <div class="ebene e-takt" aria-hidden="true"><div class="kachel k-takt">
      <div class="takt"><span class="versal">S6 · Starnberg Nord</span><b class="ziffern">31<small>Min.</small></b><span class="versal">bis Marienplatz</span></div>
    </div></div>

    <div class="ebene e-plan" aria-hidden="true"><div class="kachel k-plan" style="background: var(--leinen)">
      <svg viewBox="0 0 800 640" preserveAspectRatio="xMidYMid slice" font-family="Hanken Grotesk, sans-serif">
        <rect width="800" height="640" fill="#fcfaf5"/>
        <g fill="none" stroke="#b5b2af" stroke-width="1.5"><rect x="140" y="170" width="200" height="90" rx="10"/><rect x="420" y="430" width="120" height="70"/><circle cx="560" cy="210" r="34"/><rect x="200" y="420" width="110" height="140"/></g>
        <g fill="none" stroke="#02385c" stroke-width="10" stroke-linecap="square"><path d="M90 90 H710 V560 H90Z"/></g>
        <g fill="none" stroke="#02385c" stroke-width="6"><path d="M90 360 H360 M360 360 V560 M360 360 H420 M480 360 H710 M520 360 V90 M600 360 V560"/></g>
        <g fill="none" stroke="#02385c" stroke-width="1.5" opacity=".7"><path d="M420 360 A60 60 0 0 1 480 300"/><path d="M520 250 A50 50 0 0 0 470 300"/></g>
        <g stroke="#9fb6c4" stroke-width="5"><path d="M160 90 H300 M380 90 H470 M710 150 V300"/></g>
        <path d="M90 560 H710 V610 H90Z" fill="#efe8df" stroke="#02385c" stroke-width="3"/>
        <g fill="#02385c" font-size="22" font-weight="600"><text x="120" y="135">Wohnen · Essen</text><text x="545" y="135">Schlafen</text><text x="120" y="400">Kind</text><text x="385" y="400">Bad</text><text x="625" y="400">Kind</text></g>
        <g fill="#6e6b67" font-size="19" style="font-variant-numeric: tabular-nums"><text x="120" y="160">48,6 m²</text><text x="545" y="160">18,2 m²</text><text x="120" y="425">13,1 m²</text><text x="385" y="425">9,4 m²</text><text x="625" y="425">12,8 m²</text><text x="110" y="592">Loggia, Seeseite · 14,0 m²</text></g>
        <g fill="#02385c"><path d="M740 40 L752 70 L740 63 L728 70Z"/><text x="734" y="30" font-size="16" font-weight="600">N</text></g>
        <text x="90" y="40" font-size="17" fill="#6e6b67" letter-spacing="1.5">WOHNUNG 4 · 2. OG · 126 M²</text>
      </svg>
    </div></div>

    <div class="ebene e-lage" aria-hidden="true"><div class="kachel k-lage" style="background: var(--sand)">
      <svg viewBox="0 0 800 560" preserveAspectRatio="xMidYMid slice" font-family="Hanken Grotesk, sans-serif">
        <rect width="800" height="560" fill="#e9e1d5"/>
        <g stroke="#d6ccbe" stroke-width="1"><path d="M0 140 H800 M0 280 H800 M0 420 H800 M200 0 V560 M400 0 V560 M600 0 V560"/></g>
        <path d="M398 130 C392 70 410 40 404 0" fill="none" stroke="#9db6c4" stroke-width="5"/>
        <path d="M398 128 C366 150 352 210 344 270 C334 350 318 460 312 560 L520 560 C510 460 496 360 478 270 C462 200 440 140 398 128Z" fill="#9db6c4"/>
        <path d="M240 0 C300 60 330 100 352 140 C318 240 300 380 286 560" fill="none" stroke="#02385c" stroke-width="3" stroke-dasharray="10 8"/>
        <g fill="#02385c" font-size="18"><circle cx="352" cy="140" r="6"/><text x="236" y="135">Starnberg</text><circle cx="296" cy="420" r="5"/><text x="190" y="425">Possenhofen</text><text x="232" y="34" font-size="15" font-weight="600" letter-spacing="1.5">S6 MÜNCHEN</text></g>
        <circle cx="446" cy="150" r="26" fill="none" stroke="#ed7f1a" stroke-width="3"/>
        <circle cx="446" cy="150" r="8" fill="#02385c"/>
        <text x="482" y="146" font-size="21" font-weight="600" fill="#02385c">Uferhaus Percha</text>
        <text x="482" y="170" font-size="17" fill="#6e6b67">Nordufer, 150 m zum See</text>
        <text x="372" y="350" font-size="16" fill="#f4f7f8" letter-spacing="2" transform="rotate(-84 410 350)">STARNBERGER SEE</text>
        <g fill="#02385c"><path d="M740 40 L752 70 L740 63 L728 70Z"/><text x="734" y="30" font-size="16" font-weight="600">N</text></g>
        <g stroke="#02385c" stroke-width="2"><path d="M620 520 H720 M620 514 V526 M720 514 V526"/></g><text x="652" y="508" font-size="15" fill="#02385c">1 km</text>
      </svg>
    </div></div>

    <div class="ebene e-holz" aria-hidden="true"><div class="kachel k-holz">
      <svg viewBox="0 0 600 600" preserveAspectRatio="xMidYMid slice">
        <defs><pattern id="hl" width="40" height="40" patternUnits="userSpaceOnUse"><rect width="40" height="40" fill="#4a3727"/><rect x="4" width="24" height="40" fill="#9a7653"/><rect x="4" width="5" height="40" fill="#b38e69"/></pattern>
        <linearGradient id="hs" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#fff3dc" stop-opacity=".5"/><stop offset=".5" stop-color="#fff3dc" stop-opacity="0"/></linearGradient>
        <linearGradient id="hg" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#bcd0da"/><stop offset="1" stop-color="#4f6a7c"/></linearGradient></defs>
        <rect width="600" height="600" fill="url(#hl)"/>
        <rect x="330" y="120" width="190" height="360" fill="url(#hg)"/>
        <path d="M330 120 L520 120 L520 200 L330 330Z" fill="#fff" opacity=".22"/>
        <rect x="322" y="112" width="206" height="8" fill="#e8e2d8"/><rect x="322" y="480" width="206" height="10" fill="#e8e2d8"/>
        <path d="M0 0 H600 L0 600Z" fill="url(#hs)"/>
      </svg>
    </div></div>

    <p class="schrift" aria-hidden="true"><span>Wohnen</span><span>am See</span></p>

    <div class="buehnen-label bl-oben"><b>Uferhaus Percha</b><span class="versal">Lage · Starnberg-Percha</span></div>
    <p class="buehnen-label bl-unten versal">Weiter scrollen, näher heran ↓</p>

    <div class="abschluss" data-abschluss>
      <div>
        <small>Visualisierung Seeseite, unverbindlich</small>
        <h2>Uferhaus Percha</h2>
        <p>Sechs Eigentumswohnungen am Nordufer des Starnberger Sees. Jede mit Blick nach Süden, auf das Wasser und die Benediktenwand.</p>
        <ul class="ziffern"><li>96–214 m²</li><li>3–5 Zimmer</li><li>ab 1.290.000 €</li><li>Bezug Herbst 2028</li></ul>
      </div>
      <div class="tun">
        <a class="knopf" href="#">Exposé anfordern <span aria-hidden="true">→</span></a>
        <a class="leise-link" href="#">Grundrisse ansehen</a>
      </div>
    </div>

    <div class="pult">
      <div class="pult__knoepfe" role="group" aria-label="Variante">
        <button type="button" data-variante="collage" aria-pressed="true">Collage</button>
        <button type="button" data-variante="unschaerfe" aria-pressed="false">Mit Unschärfe</button>
        <button type="button" data-variante="schrift" aria-pressed="false">Durch die Schrift</button>
      </div>
      <div class="pult__werte" aria-live="off">
        <span>Fortschritt <b data-wert="p">0,00</b> · Kern <b data-wert="kern">×1,00</b></span>
        <span data-wert="faktoren">Endfaktoren 4 · 5 · 6 · 8 · 9</span>
        <span>Strecke 300vh · linear · <b data-wert="antrieb">CSS view-timeline</b></span>
      </div>
    </div>
  </div>
</section>

<section class="lage">
  <div class="lage__kopf">
    <div>
      <p class="versal" style="color: var(--marke)">Die Lage in Zahlen</p>
      <h2>Am Wasser, und trotzdem in der Stadt.</h2>
    </div>
    <p>Percha liegt am nördlichen Ende des Sees, zwischen Würm und Uferpromenade. Bäcker, Grundschule und S-Bahn sind zu Fuß erreichbar, der Badeplatz am Ende der Straße ebenfalls.</p>
  </div>
  <div class="zahlen ziffern">
    <div><b>150 m</b><span>bis zum Seeufer</span></div>
    <div><b>31 Min.</b><span>mit der S6 bis Marienplatz</span></div>
    <div><b>600 m</b><span>zur Grundschule Percha</span></div>
    <div><b>A+</b><span>Energieeffizienzklasse, Bedarfsausweis</span></div>
  </div>
  <p class="pflicht">Provisionsfrei direkt vom Bauträger. Energiebedarfsausweis: Endenergiebedarf 24 kWh/(m²·a), Wärmepumpe, Baujahr 2028, Effizienzklasse A+. Alle Abbildungen sind unverbindliche Visualisierungen. Haldenbach Immobilien GmbH, Hauptstraße 14, 82319 Starnberg, Erlaubnis nach § 34c GewO.</p>
</section>

<script>
(() => {
  const html = document.documentElement;
  const reduziert = () => html.classList.contains('reduziert');
  const strecke = document.querySelector('.strecke');
  const buehne = strecke.querySelector('.buehne');
  const kern = strecke.querySelector('.e-kern');
  const abschluss = strecke.querySelector('[data-abschluss]');
  const wert = n => strecke.querySelector(`[data-wert="${n}"]`);

  // Natives CSS (Scroll-Zeitachse animiert --p) oder JS setzt --p selbst
  const nativ = CSS.supports('animation-timeline: --a') && CSS.supports('view-timeline-name: --a');
  html.classList.toggle('zeitachse', nativ);
  wert('antrieb').textContent = reduziert() ? 'aus (reduziert)' : nativ ? 'CSS view-timeline' : 'JS-Fallback';

  const zahl = (n, s = 2) => n.toFixed(s).replace('.', ',');
  function fortschritt() {
    const r = strecke.getBoundingClientRect(), weg = r.height - innerHeight;
    return weg > 0 ? Math.min(1, Math.max(0, -r.top / weg)) : 0;
  }
  function faktoren() {
    const s = [...strecke.querySelectorAll('.ebene')].filter(e => getComputedStyle(e).display !== 'none').map(e => +getComputedStyle(e).getPropertyValue('--s'));
    wert('faktoren').textContent = 'Endfaktoren ' + [...new Set(s)].sort((a, b) => a - b).map(n => zahl(n, n % 1 ? 1 : 0)).join(' · ');
  }
  let bild = 0;
  function zeichnen() {
    bild = 0;
    const p = reduziert() ? 0 : simP !== null ? simP : fortschritt();
    if (!nativ && simP === null) buehne.style.setProperty('--p', p.toFixed(4));
    const s = +getComputedStyle(kern).getPropertyValue('--s') || 4;
    wert('p').textContent = zahl(p);
    wert('kern').textContent = '×' + zahl(1 + (s - 1) * p);
    abschluss.classList.toggle('aktiv', reduziert() || p > .93);
    abschluss.inert = !(reduziert() || p > .93);   /* Zugang: unsichtbar = nicht fokussierbar */
  }
  const anstossen = () => { if (!bild) bild = requestAnimationFrame(zeichnen); };
  addEventListener('scroll', anstossen, { passive: true });
  addEventListener('resize', () => { faktoren(); anstossen(); });

  function setzen(v) {
    strecke.dataset.variante = v;
    strecke.querySelectorAll('[data-variante]').forEach(b => { if (b.tagName === 'BUTTON') b.setAttribute('aria-pressed', String(b.dataset.variante === v)); });
  }
  strecke.querySelectorAll('button[data-variante]').forEach(b => b.addEventListener('click', () => setzen(b.dataset.variante)));

  // Vorführung: Kamerafahrt durch die Strecke und zurück. Der Fortschritt wird simuliert,
  // die Seite bleibt am Anfang der Strecke stehen (kein Ruckeln durch Programm-Scroll).
  const PHASEN = [900, 5200, 1700, 2600];          // halten, hinein, halten, zurück (ms)
  const RUNDE = PHASEN.reduce((a, b) => a + b, 0);
  const VARIANTEN = ['collage', 'schrift', 'unschaerfe'];
  const weich = t => 0.5 - 0.5 * Math.cos(Math.PI * t);  // nur das Tempo der Kamerafahrt; der Effekt selbst bleibt linear zum Fortschritt
  let uhr = null, t0 = 0, angehalten = null, einmal = false, letzteRunde = -1;
  let simP = null;
  function simuliere(p) {
    simP = p;
    strecke.classList.add('simuliert');
    buehne.style.setProperty('--p', p.toFixed(4));
    zeichnen();
  }
  function freigeben() { simP = null; strecke.classList.remove('simuliert'); if (nativ) buehne.style.removeProperty('--p'); zeichnen(); }
  function schritt(t) {
    const e = t - t0, runde = Math.floor(e / RUNDE), r = e % RUNDE;
    if (einmal && e >= PHASEN[0] + PHASEN[1]) { simuliere(1); uhr = null; return; }
    if (runde !== letzteRunde) { letzteRunde = runde; if (html.classList.contains('vorschau')) setzen(VARIANTEN[runde % 3]); }
    let q;
    if (r < PHASEN[0]) q = 0;
    else if (r < PHASEN[0] + PHASEN[1]) q = weich((r - PHASEN[0]) / PHASEN[1]);
    else if (r < PHASEN[0] + PHASEN[1] + PHASEN[2]) q = 1;
    else q = 1 - weich((r - PHASEN[0] - PHASEN[1] - PHASEN[2]) / PHASEN[3]);
    simuliere(q);
    uhr = requestAnimationFrame(schritt);
  }
  function starten(nurEinmal, ab = 0) {
    pause(); angehalten = null;
    const start = strecke.getBoundingClientRect().top + scrollY;
    scrollTo({ top: start, behavior: 'instant' });
    if (reduziert()) { freigeben(); return; }
    einmal = nurEinmal; letzteRunde = -1;
    t0 = performance.now() - ab;
    uhr = requestAnimationFrame(schritt);
  }
  function vorfuehren() { starten(false); }
  function neustart() { starten(true); }
  function pause() { if (uhr) { cancelAnimationFrame(uhr); uhr = null; angehalten = performance.now() - t0; } }
  function weiter() { if (angehalten !== null) starten(einmal, angehalten); else if (html.classList.contains('vorschau')) vorfuehren(); }

  // Eigene Eingaben beenden die Vorführung, danach gilt wieder der echte Scroll
  ['wheel', 'touchstart', 'keydown'].forEach(t => addEventListener(t, () => {
    if (html.classList.contains('vorschau') || simP === null) return;
    if (uhr) cancelAnimationFrame(uhr);
    uhr = null; angehalten = null; freigeben();
  }, { passive: true }));

  faktoren(); zeichnen();
  document.fonts && document.fonts.ready.then(zeichnen);

})();
</script>
</body>
</html>