Zum Inhalt
← Seitenübergänge

3D-Kipp-Übergänge (Würfel, Ecke, Riss)

Die alte Seite kippt als Würfelfläche weg, schält sich von der Ecke ab oder reißt mittig auf, darunter liegt die neue.

Beim Seitenwechsel bewegt sich die alte Seite wie ein physisches Objekt. Sie dreht als Würfelfläche weg (rotateY 90°, perspective 1200–2000 px), schält sich wie Papier von einer Ecke ab oder reißt auf. Dahinter oder als Nachbarfläche erscheint die neue Seite.

Der Wechsel wird greifbar und überrascht. Heute wirkt das vor allem als bewusstes Retro-Zitat oder als Spieltrieb.

Wann einsetzen

Bei verspielten Kampagnen, Spielen, Experimenten und Studioseiten, deren Idee das Räumliche ist, etwa vier Seiten als vier Flächen eines Würfels.

Wann nicht

Bei seriösen Marken und Portfolios, die Zurückhaltung brauchen. Der Effekt erinnert schnell an Keynote-Folienübergänge.

Varianten

  • Würfel: zwei Flächen eines Würfels, der sich um 90° dreht (Interlude cube12, Osmo 3D Cube)
  • Ecke: die Seite löst sich von einer Ecke wie ein Blatt (Interlude peel08, corner13)
  • Riss: die Seite reißt mittig auf und fällt in zwei Hälften (Interlude tear14, dort per Shader)

Umsetzung

Würfel: Wrapper mit perspective, darin ein Würfel-Element mit transform-style: preserve-3d. Die alte Fläche bekommt translateZ(W/2), die neue rotateY(90deg) translateZ(W/2), beide backface-visibility: hidden. Der Würfel dreht von rotateY(0) auf rotateY(-90deg) und steht dabei auf translateZ(-W/2), damit die Fläche in Ruhe genau in der Bildebene liegt. Ecke: die alte Seite per clip-path an einer wandernden Faltlinie beschneiden, die Rückseite als gespiegeltes Polygon darüberlegen. Riss: die alte Seite zweimal zeigen, je mit gezacktem clip-path, und die Hälften auseinanderdrehen. Echte Papierkrümmung gibt es nur per WebGL-Plane mit Vertex-Deformation. Im echten Einsatz läuft das in einem Router (Barba, Astro, Swup) oder mit View Transitions: Die neue Seite wird geladen, beide Seiten liegen kurz übereinander, danach wird die alte entfernt. Die Demo simuliert das mit vier Seitenzuständen in einer Datei.

Robust umsetzen

Telefon. Der Würfel funktioniert, gern per Wischgeste. Ecke und Riss lieber weglassen.

Ohne Bewegung. Unbedingt ersetzen, Rotationen belasten stark. Stattdessen eine kurze Überblendung.

Barrierefrei. Rotationen lösen häufig Bewegungsübelkeit aus, prefers-reduced-motion strikt beachten. Nach dem Wechsel den Fokus auf die neue Überschrift setzen und den Seitenwechsel per aria-live ansagen. Kopien der alten Seite während des Übergangs inert und aria-hidden.

Code

<!DOCTYPE html>
<!--
  3D-Kipp-Übergänge (Würfel, Ecke, Riss)
  Fundus · Seitenübergänge · https://cecodesign.de/lab/fundus/seitenuebergaenge/3d-raum-uebergaenge/
  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>Kubatur · 3D-Kipp-Übergänge (Würfel, Ecke, Riss) · 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: 'TikTok Sans'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/tiktok-sans:vf@latest/latin-wdth-normal.woff2') format('woff2-variations'); font-weight: 300 900; font-stretch: 75% 150%; font-display: swap; }
@font-face { font-family: 'DM Mono'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/dm-mono@latest/latin-400-normal.woff2') format('woff2'); font-weight: 400; font-display: swap; }
@font-face { font-family: 'DM Mono'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/dm-mono@latest/latin-500-normal.woff2') format('woff2'); font-weight: 500; font-display: swap; }

:root {
  --schwarz: #0b0b0b;
  --grau: #f2f2f2;
  --weiss: #ffffff;
  --meta: #8a8a8a;
  --signal: #4b3bff;      /* einzige Signalfarbe: CTA, Hover-Fläche, Raum hinter dem Würfel, Rückseite des Blatts */
  --kurve: cubic-bezier(0.65, 0, 0.35, 1);   /* power2.inOut, für alle Übergänge */
  --ui: cubic-bezier(0.16, 1, 0.3, 1);       /* Hover und Bedienelemente, 200 ms */
  --rand: clamp(16px, 2.6vw, 40px);
  --kopf: 64px;
  --dock: 96px;
  --persp: 1600px;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
html, body { height: 100%; overflow: hidden; }
html { background: var(--schwarz); color: var(--weiss); font-family: 'TikTok Sans', 'Arial Narrow', system-ui, sans-serif; font-size: 16px; -webkit-font-smoothing: antialiased; }
a { color: inherit; text-decoration: none; }
svg { display: block; }
.mono { font: 400 12px/1.45 'DM Mono', ui-monospace, monospace; letter-spacing: .04em; text-transform: uppercase; font-variant-numeric: tabular-nums; }
.sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
.gross { font-stretch: 75%; font-weight: 800; letter-spacing: -0.03em; line-height: .9; }
h1:focus { outline: none; }
sup.hoch { font-size: .42em; vertical-align: top; line-height: 1; color: var(--signal); margin-left: .04em; }
.flaeche.dunkel sup.hoch { color: #7d71ff; }   /* hellere Stufe der Signalfarbe auf Schwarz (5,1:1) */

/* ---------- Raum: Bühne mit Perspektive, Grund in Signalfarbe ---------- */
.raum { position: fixed; inset: 0; overflow: hidden; perspective: var(--persp); perspective-origin: 50% 50%; background: var(--signal); touch-action: pan-y; }
.raum::before { content: ''; position: absolute; inset: 0; pointer-events: none;
  background: repeating-linear-gradient(90deg, transparent 0 calc(25% - 1px), rgba(255, 255, 255, .16) calc(25% - 1px) 25%); }
.raum__label { position: absolute; left: var(--rand); bottom: 18px; color: rgba(255, 255, 255, .78); }
.wuerfel { position: absolute; inset: 0; transform-style: preserve-3d; will-change: transform; }
.flaeche { position: absolute; inset: 0; overflow: hidden; display: grid; grid-template-rows: var(--kopf) minmax(0, 1fr);
  background: var(--bg); color: var(--fg); backface-visibility: hidden; -webkit-backface-visibility: hidden; }
.flaeche.dunkel { --bg: #0b0b0b; --fg: #ffffff; }
.flaeche.hell { --bg: #f2f2f2; --fg: #0b0b0b; }
.flaeche { --linie: color-mix(in srgb, var(--fg) 15%, transparent); --leise: color-mix(in srgb, var(--fg) 58%, transparent); }
.schatten { position: absolute; inset: 0; z-index: 5; pointer-events: none; background: #000; opacity: 0; }

/* Ecke: Rückseite des Blatts und Schattenwurf */
.blatt, .blatt-schatten { position: absolute; inset: 0; pointer-events: none; }
.blatt-schatten { filter: blur(16px); }
.blatt-schatten > div { position: absolute; inset: 0; background: rgba(0, 0, 0, .5); }
/* Riss: zwei Hälften mit Faserkante */
.haelfte { position: absolute; inset: 0; pointer-events: none; will-change: transform; }
.haelfte > .flaeche { position: absolute; inset: 0; }
.faser { position: absolute; inset: 0; background: #fbfaf6; }

/* ---------- Kopf jeder Fläche ---------- */
.kopf { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 16px; padding: 0 var(--rand); border-bottom: 1px solid var(--linie); }
.marke { justify-self: start; display: inline-flex; align-items: baseline; gap: 12px; }
.marke b { font: 800 26px/1 'TikTok Sans', sans-serif; font-stretch: 75%; letter-spacing: -0.02em; }
.marke small { color: var(--leise); }
.nav { display: flex; gap: clamp(16px, 2.6vw, 40px); }
.nav a { position: relative; padding: 8px 0; }
.nav a::after { content: ''; position: absolute; left: 0; right: 0; bottom: 4px; height: 1px; background: currentColor; transform: scaleX(0); transform-origin: right; transition: transform .2s var(--ui); }
.nav a[aria-current='page']::after { transform: scaleX(1); transform-origin: left; }
@media (hover: hover) { .nav a:hover::after { transform: scaleX(1); transform-origin: left; } }
.kopf__rechts { justify-self: end; display: flex; align-items: center; gap: 18px; }
.kopf__rechts .zaehler { color: var(--leise); }
.anfrage { display: inline-flex; align-items: center; height: 38px; padding: 0 16px; background: var(--signal); color: var(--weiss); transition: background-color .2s var(--ui); }
@media (hover: hover) { .anfrage:hover { background: var(--fg); color: var(--bg); } }

.innen { min-height: 0; padding: clamp(16px, 3.2vh, 36px) var(--rand) calc(var(--dock) + clamp(10px, 2vh, 24px)); display: grid; }

/* Start */
.f-start .innen { grid-template-rows: auto minmax(0, 1fr) auto; gap: clamp(10px, 2.4vh, 28px); }
.kopfzeile { display: flex; justify-content: space-between; gap: 16px; color: var(--leise); }
.held-box { container-type: size; min-height: 0; display: grid; align-items: center; }
.held { font-size: min(12.4vw, 52cqh); }
.held span { display: block; white-space: nowrap; }
.held span + span { padding-left: 25vw; }
.held em { font-style: normal; color: var(--signal); }
.start__unten { display: grid; grid-template-columns: minmax(0, 4fr) minmax(0, 5fr) minmax(0, 3fr); gap: 24px clamp(20px, 2.6vw, 40px); align-items: end; padding-top: 16px; border-top: 1px solid var(--linie); }
.lead { font-size: clamp(17px, 1.3vw, 20px); line-height: 1.4; max-width: 32ch; }
.lead + .mono { margin-top: 14px; color: var(--leise); }
.hinweis-touch { display: none; }
.hinweis-maus, .hinweis-touch { margin-top: 14px; color: var(--leise); }
.reel { display: grid; gap: 10px; }
.reel__bild { position: relative; aspect-ratio: 16 / 6.4; overflow: hidden; background: #161616; }
.reel__bild svg { width: 100%; height: 100%; }
.reel__play { position: absolute; left: 14px; bottom: 14px; display: inline-flex; align-items: center; gap: 10px; height: 34px; padding: 0 14px 0 10px; background: var(--weiss); color: var(--schwarz); }
.reel__play::before { content: ''; border-left: 9px solid currentColor; border-top: 6px solid transparent; border-bottom: 6px solid transparent; }
.reel .zeile { display: flex; justify-content: space-between; color: var(--leise); }
.weiterlink { display: grid; gap: 10px; justify-items: start; }
.weiterlink b { font: 800 clamp(34px, 3.4vw, 56px)/.9 'TikTok Sans', sans-serif; font-stretch: 75%; letter-spacing: -0.03em; display: flex; justify-content: space-between; width: 100%; padding-bottom: 10px; border-bottom: 2px solid currentColor; }
.weiterlink .mono { color: var(--leise); }

/* Arbeiten */
.f-arbeiten .innen { grid-template-rows: auto minmax(0, 1fr); gap: clamp(8px, 1.6vh, 18px); }
.titelzeile { display: flex; justify-content: space-between; align-items: end; gap: 16px; padding-bottom: clamp(8px, 1.6vh, 18px); border-bottom: 2px solid var(--fg); }
.titel { font-size: min(9vw, 15vh); }
.titel sup { font: 400 max(11px, .1em)/1 'DM Mono', monospace; letter-spacing: .04em; vertical-align: top; margin-left: .45em; color: var(--leise); }
.titelzeile > .mono { color: var(--leise); }
.liste { list-style: none; display: grid; grid-auto-rows: minmax(0, 1fr); align-content: stretch; }
.liste li { position: relative; isolation: isolate; display: grid; grid-template-columns: 3em minmax(0, 1fr) 16em 4em 132px; align-items: center; gap: 20px; padding: clamp(6px, 1.3vh, 14px) 0; border-bottom: 1px solid var(--linie); }
.liste li::before { content: ''; position: absolute; z-index: -1; inset: 0 calc(var(--rand) * -1); background: var(--signal); transform: scaleY(0); transform-origin: bottom; transition: transform .2s var(--ui); }
.liste .name { font: 800 min(4.2vw, 7.4vh)/.92 'TikTok Sans', sans-serif; font-stretch: 75%; letter-spacing: -0.03em; }
.liste .mono { color: var(--leise); }
.liste .daumen { width: 132px; aspect-ratio: 3 / 2; overflow: hidden; }
.liste .daumen svg { width: 100%; height: 100%; }
@media (hover: hover) {
  .liste li:hover::before { transform: scaleY(1); }
  .liste li:hover, .liste li:hover .mono { color: var(--weiss); }
}

/* Studio */
.f-studio .innen { grid-template-columns: minmax(0, 7fr) minmax(0, 5fr); grid-template-rows: minmax(0, 1fr) auto; gap: clamp(16px, 3vh, 40px) clamp(24px, 4vw, 64px); }
.aussage { font: 700 min(5.4vw, 9.4vh)/.95 'TikTok Sans', sans-serif; font-stretch: 75%; letter-spacing: -0.025em; text-wrap: balance; align-self: center; }
.aussage em { font-style: normal; color: var(--leise); }
.studio__rechts { display: grid; gap: 18px; align-content: center; font-size: 17px; line-height: 1.5; }
.studio__rechts p { max-width: 38ch; }
.leistungen { list-style: none; }
.leistungen li { display: flex; justify-content: space-between; gap: 12px; padding: 9px 0; border-top: 1px solid var(--linie); }
.leistungen li:last-child { border-bottom: 1px solid var(--linie); }
.leistungen span:last-child { color: var(--leise); text-align: right; }
.zahlen { grid-column: 1 / -1; display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 24px; padding-top: 14px; border-top: 2px solid var(--fg); }
.zahlen b { display: block; font: 800 min(5.4vw, 9vh)/.9 'TikTok Sans', sans-serif; font-stretch: 75%; letter-spacing: -0.03em; margin-bottom: 8px; }
.zahlen .mono { color: var(--leise); }

/* Kontakt */
.f-kontakt .innen { grid-template-rows: auto auto auto minmax(0, 1fr); gap: clamp(12px, 2.6vh, 28px); padding-bottom: 0; }
.frage { font-size: min(7vw, 12vh); padding-top: clamp(4px, 2vh, 28px); text-wrap: balance; max-width: 15ch; }
.mail { justify-self: start; display: inline-flex; align-items: center; gap: 14px; height: 54px; padding: 0 22px; background: var(--signal); color: var(--weiss); font: 700 clamp(18px, 1.6vw, 24px)/1 'TikTok Sans', sans-serif; letter-spacing: -0.01em; transition: background-color .2s var(--ui); }
@media (hover: hover) { .mail:hover { background: var(--fg); color: var(--bg); } }
.adresse { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 24px; padding-top: 14px; border-top: 1px solid var(--linie); font-size: 16px; line-height: 1.45; }
.adresse .mono { display: block; color: var(--leise); margin-bottom: 6px; }
.wortmarke-box { min-width: 0; align-self: end; }
.wortmarke { width: max-content; font-weight: 800; font-stretch: 75%; letter-spacing: -0.04em; line-height: .78; white-space: nowrap; margin-left: -.03em; }

/* ---------- Demo-Steuerung ---------- */
.dock { position: fixed; z-index: 40; left: 50%; bottom: 14px; translate: -50% 0; width: max-content; max-width: calc(100% - 24px);
  display: grid; gap: 8px; padding: 10px 12px; color: var(--grau);
  background: rgba(11, 11, 11, .9); border: 1px solid rgba(242, 242, 242, .16); -webkit-backdrop-filter: blur(12px); backdrop-filter: blur(12px); }
.dock__zeile { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
.dock .mono { color: rgba(242, 242, 242, .6); }
.modi { display: flex; gap: 4px; }
.modi button { font: inherit; letter-spacing: inherit; text-transform: inherit; color: rgba(242, 242, 242, .82); background: none; border: 1px solid rgba(242, 242, 242, .22); height: 30px; padding: 0 12px; cursor: pointer; transition: background-color .2s var(--ui), color .2s var(--ui); }
.modi button:hover { color: var(--weiss); }
.modi button[aria-pressed='true'] { background: var(--signal); color: var(--weiss); border-color: var(--signal); }
.modi button:focus-visible, .dock__pfeil:focus-visible { outline: 2px solid var(--signal); outline-offset: 2px; }
.dock__pfeil { font: inherit; color: rgba(242, 242, 242, .82); background: none; border: 1px solid rgba(242, 242, 242, .22); width: 30px; height: 30px; cursor: pointer; }
.werte { font-size: 10px; line-height: 1.6; letter-spacing: .02em; text-transform: none; }
.werte > span { display: block; white-space: nowrap; }
.werte b { color: #8f86ff; font-weight: 500; }

/* Reduzierte Bewegung: kein 3D, nur 200 ms Überblendung (im JS) */
:root.reduziert * { transition-duration: .01ms !important; animation-duration: .01ms !important; }

/* Galerie-Vorschau: nur die Bühne */
.vorschau body { overflow: hidden; }
:root.vorschau { --dock: 0px; }

@media (hover: none) {
  .modi button, .dock__pfeil { height: 36px; }
  .dock__pfeil { width: 36px; }
  .liste li:active::before { transform: scaleY(1); }
  .hinweis-maus { display: none; }
  .hinweis-touch { display: block; }
  /* Auf Touch nur der Würfel: Ecke und Riss brauchen große Flächen und genaue Zeiger */
  .modi [data-variante='ecke'], .modi [data-variante='riss'] { display: none; }
}
@media (max-width: 1100px) {
  .liste li { grid-template-columns: 2.6em minmax(0, 1fr) 13em 96px; }
  .liste .jahr { display: none; }
  .liste .daumen { width: 96px; }
}
@media (max-width: 900px) {
  .start__unten { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }
  .start__unten .weiterlink { display: none; }
  .zahlen, .adresse { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .adresse p:nth-child(4) { display: none; }
}
@media (max-width: 640px) {
  :root { --kopf: 56px; --persp: 1200px; --dock: 44px; }
  .kopf { grid-template-columns: auto 1fr; }
  .nav { justify-self: end; gap: 12px; font-size: 11px; }
  .kopf__rechts, .marke small, .nav .nav__start { display: none; }
  .marke b { font-size: 22px; }
  .kopfzeile span:last-child { display: none; }
  .held { font-size: min(20vw, 52cqh); }
  .held span + span { padding-left: 0; }
  .start__unten { grid-template-columns: minmax(0, 1fr); gap: 16px; }
  .start__unten .reel .zeile { display: none; }
  .titelzeile > .mono { display: none; }
  .titel { font-size: 21vw; }
  .liste li { grid-template-columns: 2em minmax(0, 1fr) 76px; gap: 12px; padding: 10px 0; }
  .liste .wer { display: none; }
  .liste .daumen { width: 76px; }
  .liste .name { font-size: 30px; }
  .f-studio .innen { grid-template-columns: minmax(0, 1fr); grid-template-rows: auto auto auto; align-content: start; }
  .aussage { font-size: 34px; }
  .studio__rechts { font-size: 16px; }
  .studio__rechts p { display: none; }
  .zahlen { gap: 14px 16px; }
  .zahlen b { font-size: 40px; }
  .frage { font-size: 13vw; }
  .adresse { gap: 14px 16px; font-size: 14px; }
  .adresse p:nth-child(3) { display: none; }
  .dock { padding: 8px 10px; bottom: 12px; }
  .dock__zeile > span.mono, .werte .lang { display: none; }
  .werte > span { white-space: normal; }
}
/* 200 % Zoom (640 × 400): Showreel-Bild weglassen, damit der Text der Fläche ganz im Bild steht */
@media (max-height: 560px) and (max-width: 640px) { .reel { display: none; } }
@media (forced-colors: active) {
  button, [role=button], [role=tab], [role=switch] { border: 1px solid ButtonText !important; }
}
@media (max-height: 560px) and (min-width: 641px) {
  .reel { display: none; }
  .start__unten { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }
}
</style>
</head>
<body>
<div class="raum" data-raum>
  <span class="raum__label mono" aria-hidden="true">Kubatur³ · Vier Flächen, ein Würfel</span>
</div>
<p class="sr" aria-live="polite" data-ansage></p>

<aside class="dock" aria-label="Demo-Steuerung">
  <div class="dock__zeile">
    <span class="mono">Übergang</span>
    <div class="modi mono" role="group" aria-label="Variante des Übergangs">
      <button type="button" data-variante="wuerfel" aria-pressed="true">Würfel</button>
      <button type="button" data-variante="ecke" aria-pressed="false">Ecke</button>
      <button type="button" data-variante="riss" aria-pressed="false">Riss</button>
    </div>
    <button type="button" class="dock__pfeil mono" data-schritt="-1" aria-label="Vorherige Seite">←</button>
    <button type="button" class="dock__pfeil mono" data-schritt="1" aria-label="Nächste Seite">→</button>
  </div>
  <p class="werte mono" data-werte></p>
</aside>

<script>
(() => {
  const html = document.documentElement;
  const reduziert = () => html.classList.contains('reduziert');
  const raum = document.querySelector('[data-raum]');
  const ansage = document.querySelector('[data-ansage]');
  const werteFeld = document.querySelector('[data-werte]');
  const dock = document.querySelector('.dock');

  /* ---------- Werte ----------
     Steckbrief: power2.inOut, 800–1100 ms, rotateY 90°, perspective 1200–2000 px.
     Eine Kurve für alle drei Varianten. */
  const KURVE_CSS = 'cubic-bezier(0.65, 0, 0.35, 1)';
  const VARIANTEN = {
    wuerfel: { dauer: 900,  titel: 'Würfel' },
    ecke:    { dauer: 1100, titel: 'Ecke' },
    riss:    { dauer: 1100, titel: 'Riss' }
  };
  const RUECKZUG = 0.28;   // Würfel weicht in der Mitte um 0,28 × Breite zurück (sin-Bogen)
  const SCHATTEN = 0.55;   // abgewandte Würfelfläche dunkelt bis 55 % ab
  const RISS = { drehen: 13, kippen: 24, weg: 0.75, fall: 0.12 };
  const FADE = 200;        // reduzierte Bewegung
  let variante = 'wuerfel';

  // cubic-bezier als Funktion (Newton, Bisektion als Rückfall)
  function bezier(x1, y1, x2, y2) {
    const k = (a1, a2) => [1 - 3 * a2 + 3 * a1, 3 * a2 - 6 * a1, 3 * a1];
    const [ax, bx, cx] = k(x1, x2), [ay, by, cy] = k(y1, y2);
    const X = t => ((ax * t + bx) * t + cx) * t, Y = t => ((ay * t + by) * t + cy) * t, dX = t => (3 * ax * t + 2 * bx) * t + cx;
    return x => {
      if (x <= 0) return 0; if (x >= 1) return 1;
      let t = x;
      for (let i = 0; i < 8; i++) { const d = dX(t); if (Math.abs(d) < 1e-6) break; const n = t - (X(t) - x) / d; if (n < 0 || n > 1) break; t = n; }
      if (Math.abs(X(t) - x) > 1e-4) { let lo = 0, hi = 1; for (let i = 0; i < 30; i++) { t = (lo + hi) / 2; if (X(t) < x) lo = t; else hi = t; } }
      return Y(t);
    };
  }
  const EASE = bezier(0.65, 0, 0.35, 1);

  function zeigeWerte() {
    const v = VARIANTEN[variante];
    let t;
    if (reduziert()) t = `<span><b>reduziert</b> kein 3D, keine Drehung · neue Seite opacity 0 → 1 · ${FADE} ms linear</span>`;
    else if (variante === 'wuerfel') t =
      `<span><b>Würfel</b> rotateY 0 → ∓90° · Flächen translateZ(W/2) · perspective ${getComputedStyle(html).getPropertyValue('--persp').trim()}</span>` +
      `<span><b>${v.dauer} ms</b> power2.inOut ${KURVE_CSS}</span>` +
      `<span class="lang"><b>Rückzug</b> translateZ −${RUECKZUG} W im Sinus-Bogen · <b>Schatten</b> abgewandte Fläche 0 → ${SCHATTEN}</span>`;
    else if (variante === 'ecke') t =
      `<span><b>Ecke</b> Faltlinie wandert von der Ecke über die Diagonale · clip-path je Bild</span>` +
      `<span><b>${v.dauer} ms</b> power2.inOut ${KURVE_CSS}</span>` +
      `<span class="lang"><b>Rückseite</b> Spiegelung an der Faltlinie, Signalfarbe · <b>Schatten</b> blur 16 px, 50 %</span>`;
    else t =
      `<span><b>Riss</b> zwei Hälften, gezackte Kante (clip-path) · Faserkante 3–9 px</span>` +
      `<span><b>${v.dauer} ms</b> power2.inOut ${KURVE_CSS}</span>` +
      `<span class="lang"><b>Hälften</b> rotate ±${RISS.drehen}° · rotateY ±${RISS.kippen}° · x ±${RISS.weg * 100} % · y +${RISS.fall * 100} %</span>`;
    werteFeld.innerHTML = t;
    html.style.setProperty('--dock', html.classList.contains('vorschau') ? '0px' : Math.ceil(dock.getBoundingClientRect().height + 14) + 'px');
  }

  /* ---------- Bilder: eigene SVG-Kompositionen ---------- */
  // Isometrische Würfel als Showreel-Standbild, einer in Signalfarbe
  function reel() {
    const w = 800, h = 320, s = 34, dx = s * Math.cos(Math.PI / 6), dy = s / 2;
    let g = `<rect width="${w}" height="${h}" fill="#141414"/>`;
    let n = 0;
    for (let r = -1; r < 12; r++) for (let c = -1; c < 14; c++) {
      const x = c * dx * 2 + (r % 2 ? dx : 0), y = r * s * 1.5;
      const hoch = ((c * 7 + r * 3) % 5) * 6;
      const sig = r === 4 && c === 8;
      n++;
      const top = `M${x} ${y - hoch} l${dx} ${dy} l${-dx} ${dy} l${-dx} ${-dy}z`;
      const li = `M${x - dx} ${y + dy - hoch} l${dx} ${dy} v${s + hoch} l${-dx} ${-dy}z`;
      const re = `M${x} ${y + s - hoch} l${dx} ${-dy} v${s + hoch} l${-dx} ${dy}z`;
      g += `<path d="${top}" fill="${sig ? '#7d71ff' : '#3a3a3a'}"/><path d="${li}" fill="${sig ? '#4b3bff' : '#262626'}"/><path d="${re}" fill="${sig ? '#2f22c9' : '#1c1c1c'}"/>`;
    }
    return `<svg viewBox="0 0 ${w} ${h}" preserveAspectRatio="xMidYMid slice" aria-hidden="true">${g}</svg>`;
  }
  function daumen(art) {
    const w = 300, h = 200;
    let g = '';
    if (art === 'hafenkino') {        // Leinwand am Wasser, Lichtkegel
      g = `<rect width="${w}" height="${h}" fill="#0f1a2b"/><rect y="132" width="${w}" height="68" fill="#16304f"/>`
        + `<rect x="92" y="40" width="116" height="66" fill="#f2f2f2"/><path d="M150 186 L92 106 L208 106z" fill="#f2f2f2" opacity=".14"/>`
        + Array.from({ length: 7 }, (_, i) => `<rect x="${40 + i * 34}" y="${148 + (i % 3) * 12}" width="${18 + (i % 2) * 10}" height="2" fill="#f2f2f2" opacity=".45"/>`).join('');
    } else if (art === 'stahlstich') { // Gravurlinien
      g = `<rect width="${w}" height="${h}" fill="#e7e2d8"/>`;
      for (let i = 0; i < 26; i++) g += `<path d="M0 ${20 + i * 7} Q ${w / 2} ${-30 + i * 9} ${w} ${20 + i * 7}" fill="none" stroke="#0b0b0b" stroke-width="${0.6 + (i % 4) * 0.35}"/>`;
    } else if (art === 'muehlleinen') { // Gewebe
      g = `<rect width="${w}" height="${h}" fill="#cfc6b4"/>`;
      for (let x = 0; x < w; x += 12) g += `<rect x="${x + 3}" y="0" width="6" height="${h}" fill="#b8ad97"/>`;
      for (let y = 0; y < h; y += 12) for (let x = 0; x < w; x += 24) g += `<rect x="${x + ((y / 12) % 2 ? 12 : 0)}" y="${y + 3}" width="12" height="6" fill="#efe9dc"/>`;
    } else {                           // Radlager: Speichen
      g = `<rect width="${w}" height="${h}" fill="#4b3bff"/>`;
      for (let i = 0; i < 18; i++) { const a = i / 18 * Math.PI; g += `<line x1="${150 - Math.cos(a) * 80}" y1="${100 - Math.sin(a) * 80}" x2="${150 + Math.cos(a) * 80}" y2="${100 + Math.sin(a) * 80}" stroke="#f2f2f2" stroke-width="1.2"/>`; }
      g += `<circle cx="150" cy="100" r="82" fill="none" stroke="#f2f2f2" stroke-width="7"/><circle cx="150" cy="100" r="10" fill="#0b0b0b"/>`;
    }
    return `<svg viewBox="0 0 ${w} ${h}" preserveAspectRatio="xMidYMid slice" aria-hidden="true">${g}</svg>`;
  }

  /* ---------- Inhalte: erfundenes Digitalstudio Kubatur, Linz ---------- */
  const ORDNUNG = ['start', 'arbeiten', 'studio', 'kontakt'];
  const TITEL = { start: 'Start', arbeiten: 'Arbeiten', studio: 'Studio', kontakt: 'Kontakt' };
  const HELL = { start: false, arbeiten: true, studio: false, kontakt: true };
  const PROJEKTE = [
    ['Hafenkino', 'Open-Air-Kino am Winterhafen, Linz', 'Kampagne, Ticketing', '2026', 'hafenkino'],
    ['Stahlstich', 'Druckerei Stahlstich, Steyr', 'Marke, Website', '2026', 'stahlstich'],
    ['Mühlleinen', 'Handweberei im Mühlviertel', 'Shop, Verpackung', '2025', 'muehlleinen'],
    ['Radlager', 'Werkstatt-Kollektiv, Urfahr', 'App, Marke', '2025', 'radlager']
  ];

  function kopf(ziel) {
    const nr = String(ORDNUNG.indexOf(ziel) + 1).padStart(2, '0');
    const nav = ORDNUNG.map(z => `<a class="${z === 'start' ? 'nav__start' : ''}" href="#${z}" data-ziel="${z}"${z === ziel ? ' aria-current="page"' : ''}>${TITEL[z]}</a>`).join('');
    return `<header class="kopf">
      <a class="marke" href="#start" data-ziel="start" aria-label="Kubatur, Startseite"><b>Kubatur<sup class="hoch">3</sup></b><small class="mono">Linz</small></a>
      <nav class="nav mono" aria-label="Hauptnavigation">${nav}</nav>
      <div class="kopf__rechts"><span class="mono zaehler">Fläche ${nr} / 04</span><a class="anfrage mono" href="#kontakt" data-ziel="kontakt">Projekt anfragen</a></div>
    </header>`;
  }
  const INHALT = {
    start: () => `<div class="innen">
      <p class="kopfzeile mono"><span>Digitalstudio für Marken, Räume und Bildschirme</span><span>Tabakfabrik Linz · seit 2016 · 11 Leute</span></p>
      <div class="held-box"><h1 class="held gross" tabindex="-1"><span>Marken mit</span><span>Volumen<em>.</em></span></h1></div>
      <div class="start__unten">
        <div><p class="lead">Kubatur baut Marken, Websites und Ausstellungen, die man anfassen möchte. Jede Seite dieser Website ist eine Fläche desselben Würfels.</p><p class="mono hinweis-maus">Nächste Fläche mit → oder über die Navigation</p><p class="mono hinweis-touch">Nach links wischen: nächste Fläche</p></div>
        <a class="reel" href="#arbeiten" data-ziel="arbeiten"><span class="reel__bild">${reel()}<span class="reel__play mono">Showreel</span></span><span class="zeile mono"><span>Showreel 2026</span><span>01:24</span></span></a>
        <a class="weiterlink" href="#arbeiten" data-ziel="arbeiten"><span class="mono">31 Projekte seit 2016</span><b><span>Arbeiten</span><span aria-hidden="true">→</span></b></a>
      </div>
    </div>`,
    arbeiten: () => `<div class="innen">
      <div class="titelzeile"><h1 class="titel gross" tabindex="-1">Arbeiten<sup>04 / 31</sup></h1><span class="mono">Auswahl 2025 bis 2026</span></div>
      <ul class="liste">${PROJEKTE.map(([n, k, l, j, a], i) => `<li>
        <span class="mono">${String(i + 1).padStart(2, '0')}</span><span class="name">${n}</span>
        <span class="mono wer">${k}<br>${l}</span><span class="mono jahr">${j}</span><span class="daumen">${daumen(a)}</span></li>`).join('')}</ul>
    </div>`,
    studio: () => `<div class="innen">
      <h1 class="sr" tabindex="-1">Studio</h1>
      <p class="aussage">Elf Leute im dritten Stock der Tabakfabrik. <em>Wir denken in Flächen, Kanten und Zeit.</em></p>
      <div class="studio__rechts">
        <p>Eine Marke hat mehr als eine Ansicht. Wir gestalten sie so, dass sie von jeder Seite trägt: auf dem Bildschirm, im Raum und auf Papier.</p>
        <ul class="leistungen mono"><li><span>Marke</span><span>Name, Zeichen, System</span></li><li><span>Web</span><span>Websites, Shops, Apps</span></li><li><span>Raum</span><span>Ausstellung, Messe, Leitsystem</span></li></ul>
      </div>
      <div class="zahlen">
        <div><b>11</b><span class="mono">Leute im Studio</span></div>
        <div><b>2016</b><span class="mono">gegründet in Linz</span></div>
        <div><b>31</b><span class="mono">Marken begleitet</span></div>
        <div><b>4</b><span class="mono">Seiten pro Würfel</span></div>
      </div>
    </div>`,
    kontakt: () => `<div class="innen">
      <h1 class="frage gross" tabindex="-1">Haben Sie etwas, das Tiefe braucht?</h1>
      <a class="mail" href="#kontakt" data-ziel="kontakt">hallo@kubatur.at <span aria-hidden="true">→</span></a>
      <div class="adresse">
        <p><span class="mono">Studio</span>Tabakfabrik, Bau 2, 3. Stock<br>4020 Linz</p>
        <p><span class="mono">Telefon</span>+43 732 90 41 77<br>Mo bis Do, 9 bis 17 Uhr</p>
        <p><span class="mono">Neue Projekte</span>Ida Kastner<br>Geschäftsführung</p>
        <p><span class="mono">Jobs</span>Wir suchen eine Motion-Designerin oder einen Motion-Designer.</p>
      </div>
      <div class="wortmarke-box"><p class="wortmarke" data-fit aria-hidden="true">Kubatur<sup class="hoch">3</sup></p></div>
    </div>`
  };
  function baueFlaeche(ziel) {
    const f = document.createElement('section');
    f.className = `flaeche f-${ziel} ${HELL[ziel] ? 'hell' : 'dunkel'}`;
    f.setAttribute('aria-label', TITEL[ziel]);
    f.innerHTML = kopf(ziel) + INHALT[ziel]();
    return f;
  }
  function passeAn(f) {
    f.querySelectorAll('[data-fit]').forEach(el => {
      el.style.fontSize = '100px';
      const b = el.parentElement.clientWidth, w = el.getBoundingClientRect().width;
      el.style.fontSize = Math.floor(100 * b / w * 10) / 10 + 'px';   // volle Breite, unten darf sie anschneiden
    });
  }

  let aktuell = 'start', flaeche = null;
  function setzeStill(ziel) {
    raum.querySelectorAll('.flaeche, .wuerfel, .blatt, .blatt-schatten, .haelfte').forEach(e => e.remove());
    flaeche = baueFlaeche(ziel);
    raum.appendChild(flaeche);
    aktuell = ziel;
    passeAn(flaeche);
    document.title = `Kubatur · ${TITEL[ziel]} · 3D-Kipp-Übergänge · Fundus-Demo`;
  }

  /* ---------- Uhr: eigene Zeit, damit pause() alles anhält ---------- */
  let pausiert = false, lauf = 0;
  // gueltig: Zahl (Lauf eines Übergangs) oder Funktion (Schleife der Vorführung)
  function tween(ms, fn, gueltig) {
    const ok = typeof gueltig === 'function' ? gueltig : () => gueltig === lauf;
    return new Promise(res => {
      let t = 0, letzte = performance.now();
      const schritt = now => {
        if (!ok()) return res(false);
        const dt = Math.min(64, now - letzte); letzte = now;
        if (!pausiert) t += dt;
        const p = Math.min(1, t / ms);
        fn(p);
        if (p < 1) requestAnimationFrame(schritt); else res(true);
      };
      fn(0);
      requestAnimationFrame(schritt);
    });
  }
  const warte = (ms, token) => tween(ms, () => {}, token);

  /* ---------- Würfel ---------- */
  async function wuerfel(alt, neu, dir, t) {
    const W = raum.clientWidth, h = W / 2;
    const w = document.createElement('div');
    w.className = 'wuerfel';
    raum.appendChild(w);
    w.append(alt, neu);
    alt.style.transform = `translateZ(${h}px)`;
    neu.style.transform = `rotateY(${90 * dir}deg) translateZ(${h}px)`;
    const sa = document.createElement('div'), sn = document.createElement('div');
    sa.className = sn.className = 'schatten';
    alt.appendChild(sa); neu.appendChild(sn);
    const ok = await tween(VARIANTEN.wuerfel.dauer, p => {
      const e = EASE(p);
      w.style.transform = `translateZ(${-h - RUECKZUG * W * Math.sin(Math.PI * e)}px) rotateY(${-90 * dir * e}deg)`;
      sa.style.opacity = (SCHATTEN * e).toFixed(3);
      sn.style.opacity = (SCHATTEN * (1 - e)).toFixed(3);
    }, t);
    sa.remove(); sn.remove();
    return ok;
  }

  /* ---------- Ecke: Faltlinie über die Diagonale ---------- */
  function schneide(poly, f) {   // Sutherland-Hodgman mit einer Halbebene f(p) >= 0
    const out = [];
    for (let i = 0; i < poly.length; i++) {
      const a = poly[i], b = poly[(i + 1) % poly.length], fa = f(a), fb = f(b);
      if (fa >= 0) out.push(a);
      if ((fa >= 0) !== (fb >= 0)) { const k = fa / (fa - fb); out.push([a[0] + (b[0] - a[0]) * k, a[1] + (b[1] - a[1]) * k]); }
    }
    return out;
  }
  const alsPoly = pts => pts.length < 3 ? 'polygon(0 0, 0 0, 0 0)' : `polygon(${pts.map(p => `${p[0].toFixed(1)}px ${p[1].toFixed(1)}px`).join(',')})`;
  async function ecke(alt, neu, dir, t) {
    const W = raum.clientWidth, H = raum.clientHeight;
    raum.insertBefore(neu, alt);
    const C = dir > 0 ? [W, H] : [0, H];                 // vorwärts unten rechts, zurück unten links
    let u = [dir > 0 ? -W : W, -H * 0.82]; const l = Math.hypot(u[0], u[1]); u = [u[0] / l, u[1] / l];
    const rect = [[0, 0], [W, 0], [W, H], [0, H]];
    const proj = p => (p[0] - C[0]) * u[0] + (p[1] - C[1]) * u[1];
    const ende = Math.max(...rect.map(proj)) + 2;
    const sn = document.createElement('div'); sn.className = 'schatten'; sn.style.opacity = 1; neu.appendChild(sn);
    const schatten = document.createElement('div'); schatten.className = 'blatt-schatten'; schatten.innerHTML = '<div></div>';
    const blatt = document.createElement('div'); blatt.className = 'blatt';
    raum.append(schatten, blatt);
    // Winkel und Lage des Verlaufs: Verlaufslinie läuft durch die Mitte, Länge L
    const ang = Math.atan2(u[0], -u[1]) * 180 / Math.PI;
    const L = Math.abs(W * u[0]) + Math.abs(H * u[1]);
    const basis = (C[0] - W / 2) * u[0] + (C[1] - H / 2) * u[1] + L / 2;   // Position der Ecke auf der Verlaufslinie
    const ok = await tween(VARIANTEN.ecke.dauer, p => {
      const d = EASE(p) * ende;
      const rest = schneide(rect, q => proj(q) - d);
      const weg = schneide(rect, q => d - proj(q));
      const klappe = weg.map(q => { const k = 2 * (d - proj(q)); return [q[0] + k * u[0], q[1] + k * u[1]]; });
      alt.style.clipPath = alsPoly(rest);
      blatt.style.clipPath = alsPoly(klappe);
      schatten.firstChild.style.clipPath = alsPoly(klappe.map(q => [q[0] + u[0] * 14, q[1] + u[1] * 14 + 10]));
      const s = basis + d;
      // Rückseite: helle Kante an der Falz, dann dunkel, zur Spitze heller
      blatt.style.background = `linear-gradient(${ang}deg, #8c83ff ${s}px, #2a1fc4 ${s + 3}px, #4b3bff ${s + Math.min(70, d * .22)}px, #7a6fff ${s + d}px)`;
      // Schlagschatten des angehobenen Blatts auf der neuen Seite, an der Falz am dunkelsten
      const a = 0.4 * (1 - EASE(p));
      sn.style.background = `linear-gradient(${ang}deg, rgba(0,0,0,${a.toFixed(3)}) ${s - 160}px, rgba(0,0,0,${(a + .32).toFixed(3)}) ${s}px)`;
    }, t);
    sn.remove(); blatt.remove(); schatten.remove();
    alt.style.clipPath = '';
    return ok;
  }

  /* ---------- Riss: zwei Hälften mit Faserkante ---------- */
  function zufall(seed) { let s = seed >>> 0; return () => { s = (s * 1664525 + 1013904223) >>> 0; return s / 4294967296; }; }
  async function riss(alt, neu, dir, t) {
    const W = raum.clientWidth, H = raum.clientHeight, r = zufall(lauf * 7919 + 17);
    raum.insertBefore(neu, alt);
    // gezackte Linie von oben nach unten
    const n = 34, linie = []; let x = W * (0.5 + (r() - .5) * .08);
    for (let i = 0; i <= n; i++) {
      x += (r() - .5) * W * .035; x = Math.max(W * .38, Math.min(W * .62, x));
      linie.push([x + (r() - .5) * 6, i / n * H]);
    }
    const fasern = linie.map(() => 3 + r() * 6);
    const lHalb = document.createElement('div'), rHalb = document.createElement('div');
    lHalb.className = rHalb.className = 'haelfte';
    const kopie = alt.cloneNode(true);
    kopie.setAttribute('aria-hidden', 'true'); kopie.inert = true;
    const faser = document.createElement('div'); faser.className = 'faser';
    alt.remove();
    lHalb.append(alt, faser); rHalb.append(kopie);
    alt.style.clipPath = alsPoly([[0, 0], ...linie, [0, H]]);
    kopie.style.clipPath = alsPoly([[W, 0], ...linie, [W, H]]);
    const unten = linie[n];
    lHalb.style.transformOrigin = rHalb.style.transformOrigin = `${unten[0]}px ${H}px`;
    const sn = document.createElement('div'); sn.className = 'schatten'; neu.appendChild(sn);
    raum.append(rHalb, lHalb);
    const ok = await tween(VARIANTEN.riss.dauer, p => {
      const e = EASE(p), w = Math.min(1, e * 6);
      faser.style.clipPath = alsPoly([...linie, ...linie.map((q, i) => [q[0] + fasern[i] * w, q[1]]).reverse()]);
      const dx = e * e * RISS.weg * W, dy = e * e * RISS.fall * H;
      lHalb.style.transform = `translate3d(${-dx}px, ${dy}px, 0) rotate(${-RISS.drehen * e}deg) rotateY(${-RISS.kippen * e}deg)`;
      rHalb.style.transform = `translate3d(${dx}px, ${dy * 1.15}px, 0) rotate(${RISS.drehen * 0.85 * e}deg) rotateY(${RISS.kippen * e}deg)`;
      sn.style.opacity = (0.6 * (1 - e)).toFixed(3);
    }, t);
    sn.remove(); lHalb.remove(); rHalb.remove();
    alt.style.clipPath = '';
    return ok;
  }

  /* ---------- Reduziert: nur Überblendung ---------- */
  async function blende(alt, neu, dir, t) {
    raum.appendChild(neu);
    return tween(FADE, p => { neu.style.opacity = p.toFixed(3); }, t);
  }

  let laeuft = false;
  async function uebergang(ziel, opts = {}) {
    if (laeuft || ziel === aktuell) return;
    laeuft = true;
    const t = ++lauf;
    const a = ORDNUNG.indexOf(aktuell), b = ORDNUNG.indexOf(ziel);
    const dir = (b - a + 4) % 4 === 3 ? -1 : 1;   // wie auf einem Würfel: kürzester Weg
    const alt = flaeche, neu = baueFlaeche(ziel);
    alt.inert = true;
    const art = reduziert() ? blende : { wuerfel, ecke, riss }[opts.variante || variante];
    zeigeWerte();
    passeAn(neu);   // vor dem Einhängen zählt die Breite noch nicht, darum unten noch einmal
    const fertig = art(alt, neu, dir, t);
    requestAnimationFrame(() => passeAn(neu));
    const ok = await fertig;
    if (!ok) return;
    setzeStillMit(neu, ziel);
    laeuft = false;
    ansage.textContent = 'Seite geladen: ' + TITEL[ziel];
    if (opts.fokus) { const h1 = neu.querySelector('h1'); if (h1) h1.focus({ preventScroll: true }); }
  }
  function setzeStillMit(neu, ziel) {
    raum.querySelectorAll('.flaeche, .wuerfel, .blatt, .blatt-schatten, .haelfte').forEach(e => { if (e !== neu) e.remove(); });
    neu.style.transform = ''; neu.style.opacity = ''; neu.style.clipPath = '';
    if (neu.parentElement !== raum) raum.appendChild(neu);
    flaeche = neu; aktuell = ziel;
    passeAn(neu);
    document.title = `Kubatur · ${TITEL[ziel]} · 3D-Kipp-Übergänge · Fundus-Demo`;
  }
  function abbrechen() { lauf++; laeuft = false; }

  function setzeVariante(v) {
    variante = v;
    document.querySelectorAll('[data-variante]').forEach(b => b.setAttribute('aria-pressed', String(b.dataset.variante === v)));
    zeigeWerte();
  }
  const schritt = d => ORDNUNG[(ORDNUNG.indexOf(aktuell) + d + 4) % 4];

  /* ---------- Bedienung ---------- */
  raum.addEventListener('click', e => {
    const a = e.target.closest('[data-ziel]');
    if (!a) return;
    e.preventDefault();
    stoppe();
    uebergang(a.dataset.ziel, { fokus: true });
  });
  document.querySelectorAll('[data-variante]').forEach(b => b.addEventListener('click', () => { stoppe(); setzeVariante(b.dataset.variante); }));
  document.querySelectorAll('[data-schritt]').forEach(b => b.addEventListener('click', () => { stoppe(); uebergang(schritt(+b.dataset.schritt)); }));
  addEventListener('keydown', e => {
    if (e.target.closest('input, textarea')) return;
    if (e.key === 'ArrowRight') { stoppe(); uebergang(schritt(1)); }
    if (e.key === 'ArrowLeft') { stoppe(); uebergang(schritt(-1)); }
  });
  // Touch: waagerecht wischen dreht den Würfel
  let start = null;
  raum.addEventListener('pointerdown', e => { if (e.pointerType !== 'mouse') start = [e.clientX, e.clientY]; });
  raum.addEventListener('pointerup', e => {
    if (!start) return;
    const dx = e.clientX - start[0], dy = e.clientY - start[1]; start = null;
    if (Math.abs(dx) > 50 && Math.abs(dx) > Math.abs(dy) * 1.4) { stoppe(); uebergang(schritt(dx < 0 ? 1 : -1), { variante: 'wuerfel' }); }
  });
  raum.addEventListener('pointercancel', () => { start = null; });
  addEventListener('resize', () => { if (!laeuft) { passeAn(flaeche); zeigeWerte(); } });

  /* ---------- Vorführung ---------- */
  let runde = 0, lief = false;
  const FOLGE = ['wuerfel', 'ecke', 'riss'];
  async function spiel(r, erst) {
    let i = 0;
    const gilt = () => r === runde;
    if (!(await warte(erst, gilt))) return;
    while (r === runde) {
      const v = FOLGE[i % FOLGE.length];
      setzeVariante(v);
      await uebergang(schritt(1));
      if (r !== runde) return;
      i++;
      if (!(await warte(reduziert() ? 1400 : 1700, gilt))) return;
    }
  }
  function stoppe() { runde++; lief = false; }

  function neustart() {
    stoppe(); abbrechen(); pausiert = false;
    setzeStill('start');
    setzeVariante(variante);
  }
  function vorfuehren() {
    stoppe(); abbrechen(); pausiert = false;
    setzeStill('start');
    lief = true;
    spiel(runde, 2100);
  }
  function pause() { pausiert = true; }
  function weiter() { pausiert = false; }

  setzeStill('start');
  setzeVariante('wuerfel');
  addEventListener('load', () => { zeigeWerte(); passeAn(flaeche); });
  if (document.fonts) document.fonts.ready.then(() => { zeigeWerte(); passeAn(flaeche); });

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