Zum Inhalt
← Seitenübergänge

Neue Seite schiebt sich über die alte (Stack / Slide-over mit Parallax)

Die neue Seite gleitet wie ein Blatt über die alte, die alte weicht zurück und dunkelt ab: Tiefe statt Überblendung, gemessen bei COLLINS.

Die neue Seite fährt von unten herein, oft mit abgerundeten oberen Ecken wie eine Karte. Gleichzeitig weicht die alte Seite zurück: Sie skaliert leicht auf 0.9 bis 0.95 oder wandert langsamer nach oben (etwa -20 bis -30 %) und dunkelt auf 40 bis 60 % ab. Am Ende liegt die neue Seite plan, die Eckradien gehen auf 0. COLLINS kombiniert translateY(50 %) mit einem mitlaufenden clip-path: Die Kante fährt über den ganzen Viewport, der Inhalt nur halb so weit.

Tiefenstaffelung wie bei gestapelten Karten oder einem iOS-Sheet. Man sieht, dass die neue Seite über der alten liegt und wohin man zurückkehrt. Das wirkt physisch und hochwertig, ohne laut zu sein.

Wann einsetzen

Bei Produkt-, App-, Studio- und Kulturseiten mit klarer Hierarchie von Übersicht und Detail, etwa Programm und Veranstaltung oder Projektliste und Case Study.

Wann nicht

Bei rein typografischen oder brutalistischen Seiten und bei Navigationen auf gleicher Ebene (Reiter, Filter): Die Tiefe würde eine Hierarchie behaupten, die es nicht gibt.

Varianten

  • Stapel: neue Seite 50 % → 0 mit mitlaufendem Clip, alte skaliert auf 0.95 vor schwarzem Grund (COLLINS)
  • Overlapping Parallax: neue Seite von ganz unten, alte weicht um etwa 25 % nach oben aus und dunkelt ab
  • Slide-over seitlich: neue Seite von rechts, für horizontale Hierarchien
  • Peel/Corner als 3D-Variante (siehe 3d-raum-uebergaenge)

Umsetzung

Nativ mit View Transitions: @view-transition { navigation: auto; } in beiden Seiten. Dazu ::view-transition-image-pair(root) { background: #000 }, ::view-transition-old(root) und ::view-transition-new(root) mit mix-blend-mode: normal und eigenen Keyframes, etwa neu von translateY(50%) und clip-path: xywh(0 50% 100% 100% round 1.5rem) auf none, alt auf scale(.95) mit round 1rem, 450 ms cubic-bezier(.5,.3,0,1). Für Zurück eigene, gespiegelte Keyframes schreiben und ::view-transition-old(root) z-index: 1 geben, damit die obere Seite wieder abfährt. Die Richtung setzt man im MPA per viewTransition.types in pageswap/pagereveal. Mit JS-Router: den neuen Container fixed über den alten legen, beide tweenen, danach die Position zurücksetzen und scrollTo(0, 0). Fallstrick: Beim Fixieren muss die Scroll-Position der alten Seite erhalten bleiben (top: -scrollY). View Transitions lösen das von selbst, weil sie nur den sichtbaren Ausschnitt fotografieren.

Robust umsetzen

Telefon. Sehr natürlich, weil es dem Sheet-Muster mobiler Betriebssysteme entspricht. Nur transform, clip-path und opacity animieren, dann läuft es auch auf schwachen Geräten flüssig.

Ohne Bewegung. Kein Weg und keine Skalierung. Die neue Seite blendet kurz über die stehende alte (etwa 200 ms), oder der Übergang entfällt ganz mit @media (prefers-reduced-motion: reduce) { @view-transition { navigation: none; } }.

Barrierefrei. Normales Fokus-Management: Nach dem Wechsel den Fokus auf die h1 der neuen Seite setzen und den Seitentitel aktualisieren. Bei echten Seitenaufrufen bleibt beides nativ. Während der Animation ist die Pseudo-Ebene nicht bedienbar, deshalb den Übergang kurz halten.

Code

<!DOCTYPE html>
<!--
  Neue Seite schiebt sich über die alte (Stack / Slide-over mit Parallax)
  Fundus · Seitenübergänge · https://cecodesign.de/lab/fundus/seitenuebergaenge/seite-schiebt-ueber-alte/
  Eigenständige Datei. Bilder liegen hier unter bilder/ – eigene einsetzen.
-->
<html lang="de" data-variante="stapel">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Neue Seite schiebt sich über die alte · 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: 'Schibsted Grotesk'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/schibsted-grotesk:vf@latest/latin-wght-normal.woff2') format('woff2-variations'); font-weight: 400 900; font-style: normal; font-display: swap; }
@font-face { font-family: 'Atkinson Hyperlegible Next'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/atkinson-hyperlegible-next:vf@latest/latin-wght-normal.woff2') format('woff2-variations'); font-weight: 200 800; font-display: swap; }
@font-face { font-family: 'IBM Plex Mono'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/ibm-plex-mono@latest/latin-400-normal.woff2') format('woff2'); font-weight: 400; font-display: swap; }
@font-face { font-family: 'IBM Plex Mono'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/ibm-plex-mono@latest/latin-500-normal.woff2') format('woff2'); font-weight: 500; font-display: swap; }

/* Rezept „kultur-programm-haltung“: Schwarz auf Off-White, eine laute Hausschrift,
   Spartenfarben als System (immer mit Text-Label), Programm vor Image. */
:root {
  --tinte: #0b0b0b; --grund: #efede9; --leise: #55534f; --linie: rgba(11, 11, 11, .16);
  --musik: #c4421a; --tanz: #3d5f9a; --kunst: #2f7d4a; --film: #b0186b;
  --rand: clamp(16px, 3.2vw, 44px);
  /* Übergang: gemessen bei COLLINS (::view-transition, entry.DdWfn7dd.css) */
  --kurve: cubic-bezier(0.5, 0.3, 0, 1);
  --dauer: 450ms;
  --ui: cubic-bezier(0.215, 0.61, 0.355, 1);   /* Rezept: kurze Out-Kurve für UI */
}
html[data-variante='parallax'], html[data-variante='seitlich'] { --dauer: 600ms; }
* { box-sizing: border-box; margin: 0; padding: 0; }
html { background: var(--grund); color: var(--tinte); font-family: 'Atkinson Hyperlegible Next', system-ui, sans-serif; -webkit-font-smoothing: antialiased; }
body { min-height: 100svh; }
a { color: inherit; text-decoration: none; }
button { font: inherit; color: inherit; background: none; border: 0; cursor: pointer; }
ul { list-style: none; }
.mono { font-family: 'IBM Plex Mono', ui-monospace, monospace; font-size: 13px; line-height: 1.4; font-variant-numeric: tabular-nums; }
.label { font: 500 11.5px/1.3 'IBM Plex Mono', ui-monospace, monospace; letter-spacing: .14em; text-transform: uppercase; }
:focus-visible { outline: 3px solid var(--tinte); outline-offset: 3px; }

/* ---------- Kopfleiste (sticky, 1px Unterkante, Tickets immer erreichbar) ---------- */
.kopf { position: sticky; top: 0; z-index: 10; display: grid; grid-template-columns: auto 1fr auto; align-items: center; gap: 24px;
  height: 72px; padding: 0 var(--rand); background: var(--grund); border-bottom: 1px solid var(--tinte); }
.marke { display: inline-flex; align-items: center; gap: 10px; font: 850 23px/1 'Schibsted Grotesk', sans-serif; letter-spacing: -.035em; }
.marke svg { width: 26px; height: 26px; flex: none; }
.marke small { font: 500 11px/1.25 'IBM Plex Mono', monospace; letter-spacing: .06em; text-transform: uppercase; color: var(--leise); margin-left: 4px; }
.nav { display: flex; gap: clamp(14px, 1.8vw, 28px); font: 600 15.5px/1 'Schibsted Grotesk', sans-serif; letter-spacing: -.005em; }
.nav a { padding: 6px 0; border-bottom: 2px solid transparent; transition: border-color .15s var(--ui); }
.nav a[aria-current='page'], .nav a:hover { border-color: currentColor; }
.kopf__rechts { display: flex; align-items: center; gap: 16px; }
.kopf__rechts .mono { font-size: 12.5px; color: var(--leise); }
.knopf { display: inline-flex; align-items: center; gap: 8px; height: 40px; padding: 0 18px; border-radius: 999px; background: var(--tinte); color: var(--grund);
  font: 700 15px/1 'Schibsted Grotesk', sans-serif; transition: transform .15s var(--ui), background-color .15s var(--ui); }
.knopf:hover { background: #2a2a2a; }
.knopf:active { transform: scale(.97); }

/* ---------- Programmseite ---------- */
.aufmacher { display: grid; grid-template-columns: minmax(0, 1fr) minmax(240px, 340px); align-items: end; gap: 20px 48px; padding: clamp(28px, 6vh, 72px) var(--rand) clamp(22px, 4vh, 40px); }
.titel { font: 900 clamp(2.75rem, 7vw, 7rem)/.94 'Schibsted Grotesk', sans-serif; letter-spacing: -.035em; text-wrap: balance; }
.titel:focus { outline: none; }
.aufmacher .label { display: block; margin-bottom: 18px; }
.aufmacher__text { font-size: 18px; line-height: 1.5; max-width: 34ch; }
.aufmacher__text .mono { display: block; margin-top: 12px; color: var(--leise); }
.sparten { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 22px; padding: 14px var(--rand); border-top: 1px solid var(--tinte); border-bottom: 1px solid var(--linie); }
.sparten .label:first-child { color: var(--leise); margin-right: 6px; }
.sparte { display: inline-flex; align-items: center; gap: 8px; font: 500 11.5px/1.3 'IBM Plex Mono', monospace; letter-spacing: .12em; text-transform: uppercase; }
.sparte i { width: 11px; height: 11px; background: var(--s); flex: none; }
.sparte b { font-weight: 400; color: var(--leise); letter-spacing: 0; }
.raster { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: clamp(28px, 4vh, 44px) clamp(14px, 1.6vw, 24px); padding: clamp(18px, 3vh, 28px) var(--rand) 0; }
.karte { display: grid; gap: 12px; align-content: start; cursor: pointer; text-align: left; }
.karte__bild { display: block; aspect-ratio: 3 / 2; overflow: hidden; background: var(--s); }
.karte__bild svg { display: block; width: 100%; height: 100%; transition: transform .25s var(--ui); }
.karte__zeile { display: flex; justify-content: space-between; align-items: center; gap: 12px; padding-bottom: 10px; border-bottom: 1px solid var(--linie); }
.karte h2 { font: 750 clamp(1.375rem, 2.2vw, 2rem)/1.04 'Schibsted Grotesk', sans-serif; letter-spacing: -.025em; text-wrap: balance; }
.karte p { font-size: 16px; line-height: 1.45; color: var(--leise); max-width: 40ch; }
.karte__fuss { display: flex; justify-content: space-between; align-items: center; gap: 12px; }
.karte__fuss .mono { color: var(--leise); }
.ticket { font: 700 14.5px/1 'Schibsted Grotesk', sans-serif; padding-bottom: 3px; border-bottom: 2px solid currentColor; white-space: nowrap; }
@media (hover: hover) {
  .karte:hover .karte__bild svg { transform: scale(1.03); }
  .karte:hover h2 { text-decoration: underline; text-decoration-thickness: 2px; text-underline-offset: .12em; }
}
.karte.gedrueckt .karte__bild svg { transform: scale(.985); }
.besuch { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 20px; margin: clamp(56px, 12vh, 120px) var(--rand) 0; padding: 18px 0 150px; border-top: 1px solid var(--tinte); font-size: 16px; line-height: 1.5; }
.besuch .label { display: block; margin-bottom: 8px; color: var(--leise); }

/* ---------- Veranstaltungsseite ---------- */
.held { background: var(--s); color: #fff; }
.held__innen { display: grid; grid-template-columns: minmax(0, 1.25fr) minmax(0, 1fr); gap: 28px clamp(24px, 4vw, 64px); padding: 18px var(--rand) clamp(28px, 5vh, 52px); }
.blaettern { grid-column: 1 / -1; display: flex; justify-content: space-between; align-items: center; gap: 16px; padding-bottom: 14px; border-bottom: 1px solid rgba(255, 255, 255, .4); }
.blaettern button { display: inline-flex; align-items: center; gap: 10px; min-height: 36px; font: 650 15px/1.2 'Schibsted Grotesk', sans-serif; text-align: left; }
.blaettern button span { transition: transform .15s var(--ui); }
.blaettern button:hover span { transform: translateX(-3px); }
.blaettern button.vor:hover span { transform: translateX(3px); }
.blaettern .vor small { font: 500 11px/1 'IBM Plex Mono', monospace; letter-spacing: .12em; text-transform: uppercase; opacity: .8; }
.held .sparte i { background: #fff; }
.held .titel { margin: 14px 0 18px; font-size: clamp(2.75rem, 6.4vw, 6.25rem); }
.held__unter { font: 500 clamp(1.25rem, 1.9vw, 1.75rem)/1.22 'Schibsted Grotesk', sans-serif; letter-spacing: -.012em; max-width: 26ch; }
.fakten { display: flex; flex-wrap: wrap; gap: 6px 22px; margin-top: clamp(20px, 3.4vh, 34px); padding-top: 14px; border-top: 1px solid rgba(255, 255, 255, .4); }
.held__bild { aspect-ratio: 4 / 3; align-self: start; overflow: hidden; outline: 1px solid rgba(255, 255, 255, .25); }
.held__bild svg { display: block; width: 100%; height: 100%; }
.held .knopf { background: #fff; color: var(--tinte); margin-top: 22px; }
.koerper { display: grid; grid-template-columns: minmax(0, 68ch) minmax(240px, 360px); justify-content: space-between; gap: 40px clamp(28px, 5vw, 80px); padding: clamp(32px, 6vh, 64px) var(--rand) 160px; }
.lesetext { font-size: 1.125rem; line-height: 1.6; display: grid; gap: 1em; text-wrap: pretty; }
.lesetext .einstieg { font: 600 clamp(1.25rem, 1.7vw, 1.5rem)/1.35 'Schibsted Grotesk', sans-serif; letter-spacing: -.01em; }
.mitwirkende { display: grid; grid-template-columns: max-content 1fr; gap: 6px 20px; margin-top: 10px; padding-top: 14px; border-top: 1px solid var(--linie); font-size: 16px; }
.mitwirkende dt { font: 500 11.5px/1.9 'IBM Plex Mono', monospace; letter-spacing: .12em; text-transform: uppercase; color: var(--leise); }
.seite__aside { display: grid; gap: 30px; align-content: start; }
.seite__aside h3 { font: 500 11.5px/1.3 'IBM Plex Mono', monospace; letter-spacing: .14em; text-transform: uppercase; color: var(--leise); padding-bottom: 10px; border-bottom: 1px solid var(--tinte); }
.termine li { display: grid; grid-template-columns: 1fr auto; align-items: center; gap: 4px 12px; padding: 12px 0; border-bottom: 1px solid var(--linie); }
.termine b { font: 650 17px/1.2 'Schibsted Grotesk', sans-serif; }
.termine small { grid-column: 1; font-size: 14.5px; color: var(--leise); }
.termine .ticket { grid-row: 1 / span 2; grid-column: 2; }
.zugang { font-size: 16px; line-height: 1.5; display: grid; gap: .5em; }

/* ---------- View Transitions: dieselben Regeln gelten mit @view-transition { navigation: auto; } zwischen echten Seiten ---------- */
::view-transition-image-pair(root) { background: #000; }           /* COLLINS: schwarzer Grund hinter dem Stapel */
::view-transition-old(root), ::view-transition-new(root) { mix-blend-mode: normal; animation-duration: var(--dauer); animation-timing-function: var(--kurve); animation-fill-mode: both; }
html[data-richtung='zurueck']::view-transition-old(root) { z-index: 1; }   /* zurück: die obere Seite fährt wieder ab */

/* Stapel (gemessen bei COLLINS): neue Seite 50 % → 0 mit mitlaufendem Clip, alte skaliert auf .95 */
html[data-variante='stapel'][data-richtung='vor']::view-transition-new(root)     { animation-name: stapel-neu; }
html[data-variante='stapel'][data-richtung='vor']::view-transition-old(root)     { animation-name: stapel-alt; }
html[data-variante='stapel'][data-richtung='zurueck']::view-transition-old(root) { animation-name: stapel-neu-ab; }
html[data-variante='stapel'][data-richtung='zurueck']::view-transition-new(root) { animation-name: stapel-alt-auf; }
@keyframes stapel-neu { from { transform: translateY(50%); clip-path: xywh(0 50% 100% 100% round 1.5rem); } to { transform: none; clip-path: xywh(0 0 100% 100% round 0); } }
@keyframes stapel-alt { from { transform: none; clip-path: xywh(0 0 100% 100% round 0); } to { transform: scale(.95); clip-path: xywh(0 0 100% 100% round 1rem); } }
@keyframes stapel-neu-ab { from { transform: none; clip-path: xywh(0 0 100% 100% round 0); } to { transform: translateY(50%); clip-path: xywh(0 50% 100% 100% round 1.5rem); } }
@keyframes stapel-alt-auf { from { transform: scale(.95); clip-path: xywh(0 0 100% 100% round 1rem); } to { transform: none; clip-path: xywh(0 0 100% 100% round 0); } }

/* Parallax: neue Seite von ganz unten, alte weicht langsamer nach oben aus und dunkelt ab */
html[data-variante='parallax'][data-richtung='vor']::view-transition-new(root)     { animation-name: parallax-neu; }
html[data-variante='parallax'][data-richtung='vor']::view-transition-old(root)     { animation-name: parallax-alt; }
html[data-variante='parallax'][data-richtung='zurueck']::view-transition-old(root) { animation-name: parallax-neu-ab; }
html[data-variante='parallax'][data-richtung='zurueck']::view-transition-new(root) { animation-name: parallax-alt-auf; }
@keyframes parallax-neu { from { transform: translateY(100%); clip-path: inset(0 round 1.5rem 1.5rem 0 0); } to { transform: none; clip-path: inset(0 round 0); } }
@keyframes parallax-alt { to { transform: translateY(-25%); filter: brightness(.5); } }
@keyframes parallax-neu-ab { from { transform: none; clip-path: inset(0 round 0); } to { transform: translateY(100%); clip-path: inset(0 round 1.5rem 1.5rem 0 0); } }
@keyframes parallax-alt-auf { from { transform: translateY(-25%); filter: brightness(.5); } }

/* Seitlich: Slide-over von rechts, horizontale Hierarchie */
html[data-variante='seitlich'][data-richtung='vor']::view-transition-new(root)     { animation-name: seitlich-neu; }
html[data-variante='seitlich'][data-richtung='vor']::view-transition-old(root)     { animation-name: seitlich-alt; }
html[data-variante='seitlich'][data-richtung='zurueck']::view-transition-old(root) { animation-name: seitlich-neu-ab; }
html[data-variante='seitlich'][data-richtung='zurueck']::view-transition-new(root) { animation-name: seitlich-alt-auf; }
@keyframes seitlich-neu { from { transform: translateX(100%); clip-path: inset(0 round 1.5rem 0 0 1.5rem); } to { transform: none; clip-path: inset(0 round 0); } }
@keyframes seitlich-alt { to { transform: translateX(-30%); filter: brightness(.5); } }
@keyframes seitlich-neu-ab { from { transform: none; clip-path: inset(0 round 0); } to { transform: translateX(100%); clip-path: inset(0 round 1.5rem 0 0 1.5rem); } }
@keyframes seitlich-alt-auf { from { transform: translateX(-30%); filter: brightness(.5); } }

/* Reduzierte Bewegung: keine Wege, keine Skalierung, nur die neue Seite blendet in 200 ms ein */
html.reduziert::view-transition-old(root) { animation: none; }
html.reduziert::view-transition-new(root) { animation: einblenden 200ms linear both; }
@keyframes einblenden { from { opacity: 0; } }

/* ---------- Demo-Steuerung (liegt über dem Übergang) ---------- */
.dock { position: fixed; z-index: 40; left: 50%; bottom: 16px; translate: -50% 0; width: max-content; max-width: calc(100% - 24px);
  display: grid; gap: 8px; padding: 10px 12px; border-radius: 12px; color: #f1efeb;
  background: rgba(14, 14, 14, .86); -webkit-backdrop-filter: blur(14px); backdrop-filter: blur(14px); box-shadow: 0 10px 40px rgba(0, 0, 0, .25);
  view-transition-name: dock; }
.dock__zeile { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
.dock .label { font-size: 10.5px; color: rgba(241, 239, 235, .58); }
.modi { display: flex; gap: 4px; }
.modi button { font: 500 10.5px/1 'IBM Plex Mono', monospace; letter-spacing: .12em; text-transform: uppercase; color: rgba(241, 239, 235, .76); border: 1px solid rgba(241, 239, 235, .2); border-radius: 999px; height: 28px; padding: 0 12px; transition: background-color .15s var(--ui), color .15s var(--ui); }
.modi button:hover { color: #fff; }
.modi button[aria-pressed='true'] { background: #f1efeb; color: var(--tinte); border-color: #f1efeb; }
.modi button:focus-visible { outline: 2px solid #fff; outline-offset: 2px; }
.reduziert .modi { opacity: .55; }
.werte { font: 400 10.5px/1.6 'IBM Plex Mono', monospace; letter-spacing: .02em; color: rgba(241, 239, 235, .6); }
.werte > span { display: block; white-space: nowrap; }
.werte b { color: #fff; font-weight: 500; }
.werte i { font-style: normal; }
::view-transition-group(dock) { animation: none; z-index: 5; }
::view-transition-old(dock) { display: none; }
::view-transition-new(dock) { animation: none; }

/* Gedachter Zeiger, nur in der Vorführung */
.zeiger { position: fixed; z-index: 50; left: 0; top: 0; width: 26px; height: 26px; margin: -13px 0 0 -13px; border-radius: 50%; border: 1.5px solid #fff; background: rgba(255, 255, 255, .22); mix-blend-mode: difference; pointer-events: none; opacity: 0; transition: opacity .15s linear; view-transition-name: zeiger; }
.zeiger.an { opacity: 1; }
::view-transition-group(zeiger) { animation: none; z-index: 6; }
::view-transition-old(zeiger) { display: none; }
::view-transition-new(zeiger) { animation: none; }

:root.reduziert * { transition-duration: .01ms !important; }

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

@media (hover: none) {
  .modi button { height: 34px; }
  .blaettern button { min-height: 44px; }
}
@media (max-width: 1180px) {
  .nav a:nth-child(n+4) { display: none; }
  .kopf__rechts .mono { display: none; }
}
@media (max-width: 900px) {
  .nav { display: none; }
  .kopf { grid-template-columns: 1fr auto; }
  .aufmacher { grid-template-columns: minmax(0, 1fr); }
  .raster { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .held__innen { grid-template-columns: minmax(0, 1fr); }
  .held__bild { order: -1; grid-row: 2; aspect-ratio: 16 / 9; max-height: 340px; width: 100%; }
  .koerper { grid-template-columns: minmax(0, 1fr); }
  .besuch { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 560px) {
  .kopf { height: 60px; gap: 12px; }
  .marke { font-size: 21px; }
  .marke small { display: none; }
  .knopf { height: 38px; padding: 0 15px; }
  .raster { grid-template-columns: minmax(0, 1fr); }
  .aufmacher__text { font-size: 17px; }
  .blaettern .vor small { display: none; }
  .held__bild { aspect-ratio: 16 / 9; }
  .besuch { grid-template-columns: minmax(0, 1fr); }
  .dock { width: calc(100% - 24px); bottom: 12px; padding: 8px 10px; }
  .dock__zeile > .label { display: none; }
  .modi { width: 100%; }
  .modi button { flex: 1; padding: 0 6px; }
  .werte .z, .werte .lang { display: none; }
}
</style>
</head>
<body>
<div id="buehne"></div>

<aside class="dock" aria-label="Demo-Steuerung">
  <div class="dock__zeile">
    <span class="label">Übergang</span>
    <div class="modi" role="group" aria-label="Variante des Übergangs">
      <button type="button" data-variante="stapel" aria-pressed="true">Stapel</button>
      <button type="button" data-variante="parallax" aria-pressed="false">Parallax</button>
      <button type="button" data-variante="seitlich" aria-pressed="false">Seitlich</button>
    </div>
  </div>
  <p class="werte" data-werte></p>
</aside>
<div class="zeiger" aria-hidden="true"></div>

<script>
(() => {
  const html = document.documentElement;
  const reduziert = () => html.classList.contains('reduziert');
  const buehne = document.getElementById('buehne');
  const zeiger = document.querySelector('.zeiger');
  const werteFeld = document.querySelector('[data-werte]');
  const kannVT = typeof document.startViewTransition === 'function';

  /* Werte: Kurve und Stapel-Dauer gemessen bei COLLINS (.45s cubic-bezier(.5,.3,0,1)) */
  const KURVE = 'cubic-bezier(0.5, 0.3, 0, 1)';
  const VARIANTEN = {
    stapel:   { dauer: 450, zeilen: ['<b>neu</b> translateY(50%) → 0 · clip-path xywh(0 50% 100% 100% round 1.5rem) → xywh(0 0 100% 100% round 0)', '<b>alt</b> scale(1) → scale(.95) · Ecken 0 → 1rem · Grund #000'] },
    parallax: { dauer: 600, zeilen: ['<b>neu</b> translateY(100%) → 0 · obere Ecken 1.5rem → 0', '<b>alt</b> translateY(0) → translateY(-25%) · brightness(1) → brightness(.5)'] },
    seitlich: { dauer: 600, zeilen: ['<b>neu</b> translateX(100%) → 0 · linke Ecken 1.5rem → 0', '<b>alt</b> translateX(0) → translateX(-30%) · brightness(1) → brightness(.5)'] }
  };
  let variante = 'stapel';

  function zeigeWerte() {
    const v = VARIANTEN[variante];
    werteFeld.innerHTML = reduziert()
      ? '<span><b>reduziert</b> neue Seite opacity 0 → 1 · 200 ms linear · kein Weg, keine Skalierung</span>'
      : v.zeilen.map(z => `<span class="z">${z}</span>`).join('') + `<span><b>${v.dauer} ms</b> · ${KURVE}<i class="lang"> · zurück spiegelbildlich, die obere Seite fährt ab</i></span>`;
  }
  function setzeVariante(name) {
    variante = name;
    html.dataset.variante = name;
    document.querySelectorAll('[data-variante]').forEach(b => { if (b !== html) b.setAttribute('aria-pressed', String(b.dataset.variante === name)); });
    zeigeWerte();
  }

  /* ---------- Programm (erfunden: Blauhalle, Kulturzentrum an der Blau, Ulm) ---------- */
  const SPARTEN = {
    musik: { name: 'Musik', farbe: 'var(--musik)', hex: '#c4421a' },
    tanz:  { name: 'Theater / Tanz', farbe: 'var(--tanz)', hex: '#3d5f9a' },
    kunst: { name: 'Kunst', farbe: 'var(--kunst)', hex: '#2f7d4a' },
    film:  { name: 'Film', farbe: 'var(--film)', hex: '#b0186b' }
  };
  const PROGRAMM = [
    { id: 'quellgeraeusch', sparte: 'musik', bild: 'kreise', tag: '06', titel: 'Quellgeräusch', unter: 'Elektroakustische Nacht mit dem Ensemble Unterlauf',
      datum: 'Fr 06.11.', zeit: '21:00', ort: 'Halle 2', dauer: 'ca. 2 Std.', preis: '16 €, ermäßigt 8 €', ab: 'Tickets ab 8 €',
      einstieg: 'Hydrofone an drei Wehren der Blau, dazu Kontrabass, Modularsynthesizer und zwei Stimmen.',
      text: ['Das Ensemble Unterlauf hat den Fluss einen Sommer lang aufgenommen. Aus den Spuren entsteht an diesem Abend ein Konzert, das sich langsam verdichtet und nie ganz laut wird.', 'Gespielt wird im Liegen. Matten und Decken liegen bereit, Sitzplätze am Rand gibt es auch.'],
      mit: [['Ensemble', 'Unterlauf'], ['Klangregie', 'Theresa Wöhrle']],
      termine: [['Fr 06.11.', '21:00 · Halle 2']], zugang: ['Stufenlos erreichbar, 4 Rollstuhlplätze.', 'Für Hörgeräte steht eine Induktionsschleife bereit.'] },
    { id: 'fischerinnen', sparte: 'tanz', bild: 'wellen', tag: '07', titel: 'Die Fischerinnen', unter: 'Ein Tanzstück über das Wasser, die Arbeit und die Frauen aus dem Fischerviertel',
      datum: 'Sa 07.11.', zeit: '19:30', ort: 'Große Halle', dauer: '75 Min., keine Pause', preis: '18 €, ermäßigt 9 €', ab: 'Tickets ab 9 €',
      einstieg: 'Sieben Tänzerinnen, ein Becken mit zwölf Zentimetern Wasser und eine Tonspur aus Gesprächen mit älteren Ulmerinnen.',
      text: ['Es geht ums Netzeflicken, um Hochwasser und um die Frage, wem ein Fluss gehört. Die Choreografin Mira Hallstein hat die Gespräche im vergangenen Winter geführt.', 'Die Bewegungen entstehen aus Arbeitsgesten: Wringen, Ziehen, Tragen. Aus ihnen wird nach und nach ein Tanz, der lauter wird, als es das Viertel je war.'],
      mit: [['Choreografie', 'Mira Hallstein'], ['Musik', 'Jonas Feuerle'], ['Bühne', 'Ana Lutz']],
      termine: [['Sa 07.11.', '19:30 · Große Halle'], ['So 08.11.', '18:00 · mit Audiodeskription'], ['Sa 14.11.', '19:30 · Große Halle']],
      zugang: ['Stufenlos erreichbar, 6 Rollstuhlplätze.', 'Am 08.11. mit Audiodeskription, Tastführung um 17:00.'] },
    { id: 'ufer', sparte: 'kunst', bild: 'raster', tag: '12', titel: 'Ufer, unscharf', unter: 'Fotografie von der Donau, 2006 bis 2026',
      datum: '12.11. bis 24.01.', zeit: 'Di–So 11–18', ort: 'Galerie, 1. OG', dauer: 'Eröffnung Do 12.11., 19:00', preis: 'Eintritt frei', ab: 'Eintritt frei',
      einstieg: 'Zwanzig Jahre lang hat Lene Brandauer dieselben vierzehn Uferstellen fotografiert. Immer im November, immer bei Nebel.',
      text: ['Die Bilder hängen nicht nach Jahren, sondern nach Wasserstand. So rücken Aufnahmen zusammen, zwischen denen ein Jahrzehnt liegt.', 'Zur Ausstellung erscheint ein Heft mit Texten von Anwohnerinnen und Anwohnern. Es liegt kostenlos aus.'],
      mit: [['Fotografie', 'Lene Brandauer'], ['Kuratiert von', 'Okan Demir']],
      termine: [['Do 12.11.', '19:00 · Eröffnung'], ['So 06.12.', '15:00 · Führung mit der Künstlerin']], zugang: ['Aufzug zum 1. OG, Sitzhocker zum Ausleihen.', 'Alle Bildtexte auch in Leichter Sprache.'] },
    { id: 'kurzfilm', sparte: 'film', bild: 'kegel', tag: '13', titel: 'Kurzfilmnacht am Wehr', unter: 'Elf Filme aus Baden-Württemberg, im Hof unter dem Vordach',
      datum: 'Fr 13.11.', zeit: '20:30', ort: 'Kino im Hof', dauer: 'ca. 100 Min.', preis: '7 €', ab: 'Tickets 7 €',
      einstieg: 'Elf Kurzfilme zwischen drei und achtzehn Minuten, ausgewählt von einer Jury aus Ulmer Schülerinnen und Schülern.',
      text: ['Gezeigt wird draußen unter dem großen Vordach. Decken liegen bereit, an der Bar gibt es heißen Apfelsaft.', 'Bei Regen ziehen wir in Halle 2 um. Die Entscheidung steht ab 17 Uhr auf dieser Seite.'],
      mit: [['Auswahl', 'Jugendjury Ulm'], ['Moderation', 'Pia Kastl']],
      termine: [['Fr 13.11.', '20:30 · Kino im Hof']], zugang: ['Alle Filme mit deutschen Untertiteln.', 'Hof ebenerdig, Plätze für Rollstühle vorne.'] },
    { id: 'bruecken', sparte: 'tanz', bild: 'boegen', tag: '19', titel: 'Zwölf Brücken', unter: 'Ein Hörstück zum Gehen, von der Blau bis an die Donau',
      datum: 'Do 19.11.', zeit: '18:00', ort: 'Treffpunkt Foyer', dauer: '90 Min. zu Fuß', preis: '12 €', ab: 'Tickets 12 €',
      einstieg: 'Mit Kopfhörern über zwölf Brücken. An jeder erzählt eine andere Stimme, was unter ihr einmal los war.',
      text: ['Der Weg ist 2,4 Kilometer lang und eben. Kopfhörer und Regencapes werden gestellt.', 'Das Stück endet am Donauufer. Von dort sind es zehn Minuten zurück zur Blauhalle.'],
      mit: [['Text und Regie', 'Kollektiv Strömung'], ['Ton', 'Malte Ruoff']],
      termine: [['Do 19.11.', '18:00 · Foyer'], ['Sa 21.11.', '15:00 · Foyer']], zugang: ['Strecke stufenlos, mit Rollstuhl möglich.', 'Textfassung zum Mitlesen auf Anfrage.'] },
    { id: 'laeuten', sparte: 'musik', bild: 'pfeifen', tag: '21', titel: 'Läuten', unter: 'Glocken, Orgelpfeifen und Elektronik',
      datum: 'Sa 21.11.', zeit: '20:00', ort: 'Große Halle', dauer: 'ca. 70 Min.', preis: '22 €, ermäßigt 11 €', ab: 'Tickets ab 11 €',
      einstieg: 'Dreißig ausgebaute Orgelpfeifen, vier Glocken aus einer abgerissenen Kirche und ein Raum, der sehr lange nachklingt.',
      text: ['Die Komponistin Hanna Ilić spielt die Pfeifen mit Druckluft und lässt die Glocken von Motoren anschlagen. Dazwischen arbeitet sie mit Live-Elektronik.', 'Ein Abend für alle, die tiefe Töne mögen. Gehörschutz liegt am Eingang aus.'],
      mit: [['Komposition', 'Hanna Ilić'], ['Licht', 'Sven Barth']],
      termine: [['Sa 21.11.', '20:00 · Große Halle']], zugang: ['Stufenlos erreichbar, 6 Rollstuhlplätze.', 'Vibrationswesten für gehörlose Gäste ausleihbar.'] }
  ];
  const nach = id => PROGRAMM.find(e => e.id === id);

  /* ---------- Plakate: eigene SVG-Kompositionen je Programmpunkt ---------- */
  const r1 = n => Math.round(n * 10) / 10;
  function plakat(e) {
    const W = 400, H = 300, f = SPARTEN[e.sparte].hex, unten = e.bild === 'kreise' || e.bild === 'kegel';
    // Wichtiges liegt zwischen y 40 und 260, damit 4:3, 3:2 und 16:9 per slice funktionieren; oben links bleibt Platz für die Zeile
    let s = `<rect width="${W}" height="${H}" fill="${f}"/>`;
    if (e.bild === 'kreise') {                     // Schallringe aus der Ecke
      for (let i = 1; i <= 16; i++) s += `<circle cx="40" cy="252" r="${i * 26}" fill="none" stroke="#fff" stroke-width="${i % 4 === 0 ? 5 : 1.2}" stroke-opacity="${i % 4 === 0 ? .95 : .55}"/>`;
      s += `<circle cx="40" cy="252" r="16" fill="#0b0b0b"/>`;
    } else if (e.bild === 'wellen') {              // Wasserlinien über einer Scheibe
      s += `<circle cx="222" cy="122" r="60" fill="#efede9"/>`;
      for (let i = 0; i < 15; i++) {
        const y = 92 + i * 15, amp = 5 + (i % 3) * 2.5, ph = i * .7;
        let d = `M-10 ${y}`;
        for (let x = 0; x <= 420; x += 20) d += ` L${x} ${r1(y + Math.sin(x / 38 + ph) * amp)}`;
        s += `<path d="${d} V310 H-10Z" fill="${f}"/><path d="${d}" fill="none" stroke="#fff" stroke-width="${i === 4 || i === 10 ? 4 : 1.4}" stroke-opacity="${i === 4 || i === 10 ? 1 : .7}"/>`;
      }
    } else if (e.bild === 'raster') {              // Uferstellen als Kontaktbogen, eine scharf
      for (let r = 0; r < 3; r++) for (let c = 0; c < 6; c++) {
        const x = 26 + c * 59, y = 100 + r * 54, o = .12 + ((r * 7 + c * 3) % 5) * .07, scharf = r === 1 && c === 3;
        s += `<rect x="${x}" y="${y}" width="51" height="46" fill="${scharf ? '#efede9' : '#fff'}" fill-opacity="${scharf ? 1 : o}"/>`;
        s += `<rect x="${x}" y="${y + 26 - (c % 3) * 4}" width="51" height="${20 + (c % 3) * 4}" fill="${scharf ? '#0b0b0b' : '#000'}" fill-opacity="${scharf ? .85 : .12}"/>`;
      }
    } else if (e.bild === 'kegel') {               // Projektionskegel über einem Filmstreifen
      s += `<defs><linearGradient id="k${e.id}" x1="0" x2="1"><stop offset="0" stop-color="#fff" stop-opacity=".95"/><stop offset="1" stop-color="#fff" stop-opacity=".08"/></linearGradient></defs>`;
      s += `<path d="M40 145 L400 40 V250 Z" fill="url(#k${e.id})"/><circle cx="40" cy="145" r="14" fill="#0b0b0b"/>`;
      for (let x = 8; x < W; x += 28) s += `<rect x="${x}" y="22" width="14" height="10" rx="2" fill="#0b0b0b" fill-opacity=".55"/><rect x="${x}" y="268" width="14" height="10" rx="2" fill="#0b0b0b" fill-opacity=".55"/>`;
    } else if (e.bild === 'boegen') {              // zwölf Bögen ineinander, darunter ihr Spiegelbild
      for (let i = 1; i <= 12; i++) {
        const rr = 14 + i * 12, dick = i % 4 === 0;
        s += `<path d="M${200 - rr} 232 A${rr} ${rr} 0 0 1 ${200 + rr} 232" fill="none" stroke="#fff" stroke-width="${dick ? 5 : 1.6}" stroke-opacity="${dick ? 1 : .75}"/>`;
        s += `<path d="M${200 - rr} 240 A${rr} ${r1(rr * .32)} 0 0 0 ${200 + rr} 240" fill="none" stroke="#fff" stroke-opacity=".32" stroke-width="1.6" stroke-dasharray="3 6"/>`;
      }
      s += `<rect x="0" y="230" width="400" height="5" fill="#fff"/>`;
    } else if (e.bild === 'pfeifen') {             // Orgelpfeifen als Klangsäulen
      for (let i = 0; i < 22; i++) {
        const h = 46 + Math.abs(Math.sin(i * .62)) * 92 + (i % 5) * 2, x = 18 + i * 17;
        s += `<rect x="${x}" y="${r1(250 - h)}" width="10" height="${r1(h)}" fill="#fff" fill-opacity="${i % 6 === 2 ? 1 : .62}"/><path d="M${x} ${r1(250 - h * .22)} h10 l-5 6z" fill="${f}"/>`;
      }
      s += `<rect x="0" y="250" width="400" height="3" fill="#0b0b0b"/>`;
    }
    s += `<text x="384" y="${unten ? 244 : 92}" text-anchor="end" font-family="Schibsted Grotesk, sans-serif" font-weight="900" font-size="64" letter-spacing="-3" fill="#fff">${e.tag}</text>`;
    const zeile = 'BLAUHALLE · ' + (e.id === 'ufer' ? '11/26–01/27' : '11/26');
    s += `<rect x="10" y="48" width="${r1(zeile.length * 7.4 + 12)}" height="20" fill="${f}"/>`;
    s += `<text x="16" y="62" font-family="IBM Plex Mono, monospace" font-weight="500" font-size="10" letter-spacing="1.4" fill="#fff" fill-opacity=".92">${zeile}</text>`;
    return `<svg viewBox="0 0 ${W} ${H}" preserveAspectRatio="xMidYMid slice" role="img" aria-label="Plakatmotiv zu ${e.titel}">${s}</svg>`;
  }

  /* ---------- Seiten ---------- */
  const MARKE = `<svg viewBox="0 0 26 26" aria-hidden="true"><rect width="26" height="26" fill="#0b0b0b"/><path d="M4 9.5q4.5-3 9 0t9 0M4 15q4.5-3 9 0t9 0M4 20.5q4.5-3 9 0t9 0" fill="none" stroke="#efede9" stroke-width="2.2"/></svg>`;
  const kopf = aktiv => `<header class="kopf">
      <a class="marke" href="#programm" data-programm>${MARKE}Blauhalle<small>Kulturzentrum<br>an der Blau, Ulm</small></a>
      <nav class="nav" aria-label="Hauptnavigation">
        <a href="#programm" data-programm${aktiv ? ' aria-current="page"' : ''}>Programm</a><a href="#">Ausstellungen</a><a href="#">Besuch</a><a href="#">Haus</a><a href="#">Leichte Sprache</a><a href="#">Vorlesen</a>
      </nav>
      <div class="kopf__rechts"><span class="mono">Heute 11–18 Uhr</span><a class="knopf" href="#">Tickets</a></div>
    </header>`;
  const sparte = (key, zusatz = '') => `<span class="sparte" style="--s:${SPARTEN[key].farbe}"><i></i>${SPARTEN[key].name}${zusatz}</span>`;

  function seiteProgramm() {
    const anzahl = k => PROGRAMM.filter(e => e.sparte === k).length;
    return `${kopf(true)}
    <main>
      <section class="aufmacher">
        <div><span class="label">Spielzeit 26/27 · Programm</span><h1 class="titel" tabindex="-1">November an&nbsp;der&nbsp;Blau</h1></div>
        <p class="aufmacher__text">Tanz im Wasserbecken, Konzerte im Liegen, Kurzfilme unter dem Vordach. Sechs Abende, eine Ausstellung.<span class="mono">Abendkasse jeweils ab 18:30 · Foyer</span></p>
      </section>
      <div class="sparten"><span class="label">Sparten</span>${Object.keys(SPARTEN).map(k => sparte(k, ` <b>${anzahl(k)}</b>`)).join('')}</div>
      <ul class="raster">${PROGRAMM.map(e => `
        <li><a class="karte" href="#${e.id}" data-oeffne="${e.id}" style="--s:${SPARTEN[e.sparte].farbe}">
          <span class="karte__bild">${plakat(e)}</span>
          <span class="karte__zeile">${sparte(e.sparte)}<span class="mono">${e.datum}</span></span>
          <h2>${e.titel}</h2>
          <p>${e.unter}</p>
          <span class="karte__fuss"><span class="mono">${e.zeit} · ${e.ort}</span><span class="ticket">${e.ab}</span></span>
        </a></li>`).join('')}
      </ul>
      <section class="besuch" aria-label="Besuch planen">
        <div><span class="label">Adresse</span>Blauhalle<br>Am Blauwehr 3<br>89073 Ulm</div>
        <div><span class="label">Öffnungszeiten</span>Di bis So 11–18 Uhr<br>Abendkasse ab 18:30</div>
        <div><span class="label">Barrierefreiheit</span>Alle Säle stufenlos.<br>Infos in Leichter Sprache.</div>
        <div><span class="label">Kasse</span><span class="mono">0731 / 140 26 70</span><br>kasse@blauhalle.example</div>
      </section>
    </main>`;
  }

  function seiteVeranstaltung(id) {
    const e = nach(id), i = PROGRAMM.indexOf(e), naechste = PROGRAMM[(i + 1) % PROGRAMM.length];
    const tiefe = stapel.length;
    const zurueckZiel = tiefe > 1 ? nach(stapel[tiefe - 2].seite) : null;
    return `${kopf(false)}
    <main>
      <section class="held" style="--s:${SPARTEN[e.sparte].farbe}">
        <div class="held__innen">
          <div class="blaettern">
            <button type="button" data-zurueck><span aria-hidden="true">←</span>${zurueckZiel ? zurueckZiel.titel : 'Programm'}</button>
            <button type="button" class="vor" data-oeffne="${naechste.id}"><small>Danach</small>${naechste.titel}<span aria-hidden="true">→</span></button>
          </div>
          <div>
            ${sparte(e.sparte)}
            <h1 class="titel" tabindex="-1">${e.titel}</h1>
            <p class="held__unter">${e.unter}</p>
            <p class="fakten mono"><span>${e.datum}</span><span>${e.zeit}</span><span>${e.ort}</span><span>${e.dauer}</span></p>
            <a class="knopf" href="#">Tickets · ${e.preis}</a>
          </div>
          <div class="held__bild">${plakat(e)}</div>
        </div>
      </section>
      <div class="koerper">
        <div class="lesetext">
          <p class="einstieg">${e.einstieg}</p>
          ${e.text.map(t => `<p>${t}</p>`).join('')}
          <dl class="mitwirkende">${e.mit.map(([k, v]) => `<dt>${k}</dt><dd>${v}</dd>`).join('')}</dl>
        </div>
        <aside class="seite__aside">
          <div><h3>Termine</h3><ul class="termine">${e.termine.map(([d, z]) => `<li><b>${d}</b><small>${z}</small><a class="ticket" href="#">Tickets</a></li>`).join('')}</ul></div>
          <div class="zugang"><h3>Barrierefreiheit</h3>${e.zugang.map(z => `<p>${z}</p>`).join('')}</div>
        </aside>
      </div>
    </main>`;
  }

  /* ---------- Navigation: Verlaufsstapel, View Transition, Fokus ---------- */
  let stapel = [{ seite: 'programm', y: 0 }];
  let laufend = null;

  function zeichne() {
    const oben = stapel[stapel.length - 1];
    buehne.innerHTML = oben.seite === 'programm' ? seiteProgramm() : seiteVeranstaltung(oben.seite);
    document.title = (oben.seite === 'programm' ? 'Programm' : nach(oben.seite).titel) + ' · Blauhalle Ulm';
  }

  function gehe(richtung, ziel) {
    if (laufend) laufend.skipTransition();
    const update = () => {
      if (richtung === 'vor') { stapel[stapel.length - 1].y = scrollY; stapel.push({ seite: ziel, y: 0 }); }
      else if (ziel === 'programm') stapel = [{ seite: 'programm', y: stapel[0].y }];
      else stapel.pop();
      zeichne();
      scrollTo(0, stapel[stapel.length - 1].y);
      buehne.querySelector('h1').focus({ preventScroll: true });
    };
    if (!kannVT) { update(); return Promise.resolve(); }
    html.dataset.richtung = richtung;
    const vt = document.startViewTransition(update);
    laufend = vt;
    return vt.finished.catch(() => {}).then(() => { if (laufend === vt) { laufend = null; delete html.dataset.richtung; } });
  }

  buehne.addEventListener('click', ev => {
    const auf = ev.target.closest('[data-oeffne]'), zu = ev.target.closest('[data-zurueck]'), prog = ev.target.closest('[data-programm]');
    if (auf) { ev.preventDefault(); gehe('vor', auf.dataset.oeffne); }
    else if (zu) { gehe('zurueck', stapel.length > 2 ? 'eins' : 'programm'); }
    else if (prog) { ev.preventDefault(); if (stapel.length > 1) gehe('zurueck', 'programm'); }
    else if (ev.target.closest('a[href="#"]')) ev.preventDefault();
  });
  addEventListener('keydown', ev => { if (ev.key === 'Escape' && stapel.length > 1) gehe('zurueck', 'eins'); });

  document.querySelectorAll('.modi [data-variante]').forEach(b => b.addEventListener('click', () => { if (html.classList.contains('vorfuehrung')) pause(); setzeVariante(b.dataset.variante); }));

  /* ---------- Vorführung: gedachter Zeiger klickt sich durch ---------- */
  let schleife = null, lauf = 0;
  const warte = ms => new Promise(r => { schleife = setTimeout(r, ms); });
  async function fahreZu(el) {
    if (!el) return;
    const b = el.getBoundingClientRect();
    zeiger.classList.add('an');
    const x = b.left + Math.min(b.width * .5, 160), y = b.top + Math.min(b.height * .5, 120);
    if (reduziert()) zeiger.style.transform = `translate(${x}px, ${y}px)`;
    else zeiger.animate([{ transform: getComputedStyle(zeiger).transform === 'none' ? `translate(${innerWidth * .5}px, ${innerHeight * .8}px)` : getComputedStyle(zeiger).transform }, { transform: `translate(${x}px, ${y}px)` }], { duration: 650, easing: 'cubic-bezier(0.215, 0.61, 0.355, 1)', fill: 'forwards' });
    await warte(reduziert() ? 300 : 750);
  }
  async function druecke(el) {
    if (!el) return;
    el.classList.add('gedrueckt');
    await warte(140);
    el.classList.remove('gedrueckt');
  }
  const VARIANTEN_FOLGE = ['stapel', 'parallax', 'seitlich'];
  async function film(meinLauf) {
    const aktuell = () => meinLauf === lauf;
    await warte(700); if (!aktuell()) return;
    let el = buehne.querySelector('[data-oeffne="fischerinnen"]');
    await fahreZu(el); if (!aktuell()) return;
    await druecke(el); if (!aktuell()) return;
    await gehe('vor', 'fischerinnen');
    await warte(1500); if (!aktuell()) return;
    el = buehne.querySelector('.blaettern .vor');
    await fahreZu(el); if (!aktuell()) return;
    await druecke(el); if (!aktuell()) return;
    await gehe('vor', 'ufer');
    await warte(1500); if (!aktuell()) return;
    el = buehne.querySelector('[data-zurueck]');
    await fahreZu(el); if (!aktuell()) return;
    await gehe('zurueck', 'eins');
    await warte(1100); if (!aktuell()) return;
    el = buehne.querySelector('[data-zurueck]');
    await fahreZu(el); if (!aktuell()) return;
    await gehe('zurueck', 'programm');
    zeiger.classList.remove('an');
    await warte(900); if (!aktuell()) return;
    if (html.classList.contains('vorschau')) setzeVariante(VARIANTEN_FOLGE[(VARIANTEN_FOLGE.indexOf(variante) + 1) % 3]);
    film(meinLauf);
  }

  function neustart() {
    clearTimeout(schleife); lauf++;
    if (laufend) laufend.skipTransition();
    stapel = [{ seite: 'programm', y: 0 }];
    zeichne(); scrollTo(0, 0);
    zeiger.classList.remove('an');
    zeigeWerte();
  }
  function vorfuehren() {
    neustart();
    html.classList.add('vorfuehrung');
    film(lauf);
  }
  function pause() { clearTimeout(schleife); lauf++; zeiger.classList.remove('an'); html.classList.remove('vorfuehrung'); }
  function weiter() { vorfuehren(); }


  setzeVariante('stapel');
  zeichne();
  new MutationObserver(zeigeWerte).observe(html, { attributes: true, attributeFilter: ['class'] });
})();
</script>
</body>
</html>