Zum Inhalt
← Seitenübergänge

Wipe mit gebogener Kante (Curved Wipe)

Ein dunkles Panel fährt mit gewölbter Vorderkante über die Seite und gibt die neue mit hängender Hinterkante frei.

Ein Panel fährt von unten über die Seite. Seine Vorderkante ist nicht gerade, sondern gewölbt. Die Wölbung wächst während der Fahrt. Ist die Seite bedeckt, fährt das Panel weiter nach oben und gibt die neue Seite frei. Jetzt hängt die Hinterkante nach unten durch und flacht zum Ende hin ab.

Die Wölbung wirkt wie Trägheit, als würde Stoff oder Flüssigkeit gezogen. Der Wechsel fühlt sich dadurch weich und körperlich an, nicht mechanisch.

Wann einsetzen

Bei warmen, verspielten oder lifestyle-orientierten Marken, oft in der Markenfarbe. Auch bei ruhigen Marken, wenn die Kurve zum Inhalt passt, etwa bei einem Büro, das gekrümmte Tragwerke baut.

Wann nicht

Bei technisch-kühlen oder brutalistischen Marken. Nicht auf Seiten, die oft und schnell gewechselt werden, etwa in Shops oder Dokumentationen.

Varianten

  • SVG-Pfad mit animiertem Kontrollpunkt (klassisch, z. B. mit GSAP)
  • CSS shape() in clip-path, mit @property rein in CSS animierbar (Interop 2026)
  • Doppelte Kurve: zwei Flächen in zwei Farben, leicht versetzt

Umsetzung

SVG: ein fixiertes <svg> über der Seite mit einem <path>. Die Vorderkante ist ein quadratischer Bézier: M0 y Q50% (y - 2 × Wölbung) 100% y. Der Kontrollpunkt liegt doppelt so weit draußen wie die sichtbare Wölbung. GSAP tweent y und Wölbung, onUpdate schreibt den d-Wert neu. Gemessen bei Dennis Snellenberg: Panel rein 0,5 s, Wölbung 0 auf 10vh in 0,4 s mit power4.in; Panel raus 0,8 s, Wölbung 10vh auf 0 in 0,85 s mit power3.inOut. CSS: clip-path: shape(from 0% var(--kante), curve to 100% var(--kante) with 50% calc(var(--kante) - 2 * var(--bogen)), line to 100% var(--fuss), curve to 0% var(--fuss) with 50% calc(var(--fuss) + 2 * var(--fussbogen)), close). Die vier Variablen per @property registrieren und mit Transition oder Web Animations animieren. shape() vorher mit CSS.supports prüfen, sonst gerader inset()-Wipe. Bei echten Seitenwechseln deckt das Panel beim Klick ab, die neue Seite lädt (Barba, Swup oder Framework-Router) und das Panel gibt sie danach frei. Die Demo simuliert das mit Seitenzuständen in einer Datei.

Robust umsetzen

Telefon. Wölbung halb so hoch (5vh statt 10vh), weil das Hochformat sie optisch verstärkt.

Ohne Bewegung. Keine Wölbung und keine Fahrt. Das Panel blendet kurz ein und wieder aus (gerader Fade, etwa 200 ms).

Barrierefrei. Das Panel mit dem Seitennamen ist aria-hidden. Den Seitenwechsel über eine aria-live-Region ansagen und den Fokus nach dem Wechsel auf die neue Überschrift setzen. Der Übergang darf Klicks nicht länger als etwa 1,5 s blockieren.

Code

<!DOCTYPE html>
<!--
  Wipe mit gebogener Kante (Curved Wipe)
  Fundus · Seitenübergänge · https://cecodesign.de/lab/fundus/seitenuebergaenge/gebogener-wipe-shape/
  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>Wipe mit gebogener Kante (Curved Wipe) · 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: '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-style: normal; font-display: swap; }
@font-face { font-family: 'Hanken Grotesk'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/hanken-grotesk:vf@latest/latin-wght-italic.woff2') format('woff2-variations'); font-weight: 100 900; font-style: italic; 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; }

/* Die Kante des CSS-Panels: vier animierbare Längen (Variante shape()) */
@property --kante     { syntax: '<length-percentage>'; inherits: true; initial-value: 100%; }
@property --bogen     { syntax: '<length>';            inherits: true; initial-value: 0px; }
@property --fuss      { syntax: '<length-percentage>'; inherits: true; initial-value: 100%; }
@property --fussbogen { syntax: '<length>';            inherits: true; initial-value: 0px; }

:root {
  /* Rezept architektur-ruhiges-raster: achromatisch, die Bilder liefern die Farbe */
  --grund: #ffffff; --flaeche: #e1e3e3; --text: #231f20; --meta: #76767a; --tertiaer: #c5c7c7;
  --dunkel: #1f2323; --fokus: #0022ff;
  --kurve: cubic-bezier(0.1, 0.6, 0.4, 1);       /* Rezept-Default */
  --kurve-aus: cubic-bezier(0.1, 0.9, 0.3, 1);   /* Rezept: Öffnen / Einblenden */
  --rand: clamp(16px, 3.4vw, 48px);
  --kopf: 68px;
  --dock: 120px;
  --s-1: .8rem; --s1: 1.25rem; --s2: 1.563rem; --s3: 1.953rem; --s4: 2.441rem; --s5: 3.052rem;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
html { background: var(--grund); color: var(--text); font-family: 'Hanken Grotesk', system-ui, sans-serif; font-weight: 400; -webkit-font-smoothing: antialiased; }
body { min-height: 100svh; background: var(--grund); overflow-x: clip; font-size: 1.0625rem; line-height: 1.45; letter-spacing: -.012em; }
a { color: inherit; text-decoration: none; }
a:focus-visible, button:focus-visible { outline: 2px solid var(--fokus); outline-offset: 3px; }
svg { display: block; }
em { font-style: italic; }
.meta { font-size: var(--s-1); line-height: 1.4; color: var(--meta); letter-spacing: 0; }
.sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
.unterstrichen { text-decoration: underline; text-underline-offset: .35ex; text-decoration-thickness: 8%; }

/* ---------- Kopf ---------- */
.kopf { position: fixed; z-index: 10; inset: 0 0 auto; height: var(--kopf); display: grid; grid-template-columns: repeat(12, minmax(0, 1fr)); column-gap: 24px; align-items: center; padding: 0 var(--rand); background: var(--grund); }
.marke { grid-column: 1 / 6; display: inline-flex; align-items: center; gap: 12px; justify-self: start; font-weight: 500; letter-spacing: -.02em; }
.marke svg { width: 30px; height: 20px; flex: none; }
.marke span { color: var(--meta); font-weight: 400; }
.nav { grid-column: 7 / 12; display: flex; gap: clamp(18px, 2.4vw, 36px); }
.nav a { padding: 8px 0; color: var(--meta); transition: color .25s var(--kurve); }
.nav a:hover, .nav a[aria-current='page'] { color: var(--text); }
.nav a[aria-current='page'] { text-decoration: underline; text-underline-offset: .35ex; text-decoration-thickness: 8%; }
.kopf__ort { grid-column: 12 / 13; justify-self: end; color: var(--meta); }

/* ---------- Seite ---------- */
#seite { padding-top: var(--kopf); will-change: transform; }
.s { min-height: calc(100svh - var(--kopf)); padding: clamp(28px, 6vh, 72px) var(--rand) calc(var(--dock) + 32px); display: grid; grid-template-columns: repeat(12, minmax(0, 1fr)); column-gap: 24px; align-content: start; }
h1 { font-weight: 450; letter-spacing: -.02em; line-height: 1.1; text-wrap: balance; }
h1:focus { outline: none; }
.h1 { font-size: clamp(1.953rem, 1.2rem + 2.4vw, 3.052rem); }
.lead { font-size: var(--s1); line-height: 1.4; max-width: 34ch; }
.text { max-width: 50ch; text-wrap: pretty; }
.bild { background: var(--flaeche); overflow: hidden; }
.bild svg { width: 100%; height: 100%; }
figcaption { margin-top: 10px; }
.pfeil { display: inline-flex; gap: .5em; align-items: baseline; }
.pfeil i { font-style: normal; transition: translate .25s var(--kurve); }
@media (hover: hover) { .pfeil:hover i { translate: 4px 0; } }

/* Büro */
.b-text { grid-column: 1 / 6; display: grid; gap: 28px; align-content: start; padding-top: clamp(0px, 2vh, 24px); }
.b-bild { grid-column: 7 / 13; }
.b-bild .bild { aspect-ratio: 3 / 2; }
.aktuell { grid-column: 1 / 13; display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 24px; margin-top: clamp(48px, 9vh, 112px); }
.aktuell a { display: grid; gap: 6px; align-content: start; }
.aktuell b { font-weight: 450; font-size: var(--s1); letter-spacing: -.02em; line-height: 1.25; }

/* Projekte */
.p-kopf { grid-column: 1 / 13; display: flex; justify-content: space-between; align-items: end; gap: 24px; flex-wrap: wrap; padding-bottom: clamp(24px, 4vh, 44px); }
.p-kopf h1 sup { font-size: var(--s-1); color: var(--meta); vertical-align: top; margin-left: .4em; letter-spacing: 0; font-weight: 400; }
.filter { display: flex; gap: 6px; flex-wrap: wrap; }
.filter span { font-size: var(--s-1); height: 30px; padding: 0 12px; display: inline-flex; align-items: center; border: 1px solid var(--tertiaer); border-radius: 999px; color: var(--meta); }
.filter span:first-child { background: var(--text); border-color: var(--text); color: var(--grund); }
.liste { grid-column: 1 / 13; list-style: none; }
.liste a { display: grid; grid-template-columns: 3em minmax(0, 5fr) minmax(0, 3fr) minmax(0, 2fr) 4em 96px; gap: 24px; align-items: center; padding: 12px 0; border-top: 1px solid var(--flaeche); }
.liste li:last-child a { border-bottom: 1px solid var(--flaeche); }
.liste b { font-weight: 450; font-size: var(--s2); letter-spacing: -.02em; line-height: 1.15; }
.liste .daumen { width: 96px; aspect-ratio: 3 / 2; }
.liste .nr, .liste .ort, .liste .typ, .liste .jahr { font-size: var(--s-1); color: var(--meta); }
.liste b { transition: color .25s var(--kurve); }
@media (hover: hover) { .liste a:hover b { color: var(--meta); } }

/* Projekt */
.pr-bild { grid-column: 1 / 8; }
.pr-bild .bild { aspect-ratio: 4 / 3; }
.pr-text { grid-column: 9 / 13; display: grid; gap: 24px; align-content: start; }
.fakten { display: grid; }
.fakten div { display: grid; grid-template-columns: 9em minmax(0, 1fr); gap: 12px; padding: 7px 0; border-top: 1px solid var(--flaeche); font-size: var(--s-1); }
.fakten dt { color: var(--meta); }
.naechstes { display: grid; gap: 4px; padding-top: 14px; border-top: 1px solid var(--text); }
.naechstes b { font-weight: 450; font-size: var(--s2); letter-spacing: -.02em; line-height: 1.15; }

/* Kontakt */
.k-titel { grid-column: 1 / 9; font-size: clamp(2.441rem, 1.2rem + 4.6vw, 4.768rem); line-height: 1.02; }
.k-titel em { color: var(--meta); }
.k-bild { grid-column: 10 / 13; grid-row: 1 / 3; align-self: start; }
.k-bild .bild { aspect-ratio: 4 / 5; }
.k-daten { grid-column: 1 / 9; display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 24px; margin-top: clamp(40px, 8vh, 96px); }
.k-daten p { display: grid; gap: 6px; align-content: start; }
.karte { grid-column: 1 / 13; margin-top: clamp(48px, 9vh, 112px); background: var(--dunkel); color: #e9eaea; padding: 28px; display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 24px; }
.karte .meta { color: #9a9c9c; }
.karte svg { width: 36px; height: 24px; }

/* ---------- Die Blende: SVG-Pfad bzw. CSS shape() ---------- */
.blende { position: fixed; inset: 0; z-index: 30; pointer-events: none; visibility: hidden; }
.blende.an { visibility: visible; }
.blende__svg { position: absolute; inset: 0; width: 100%; height: 100%; }
.blende__svg path { fill: var(--dunkel); }
.blende__svg .p-hell { fill: var(--tertiaer); }
.blende__css, .blende__flach { position: absolute; inset: 0; background: var(--dunkel); display: none; }
.blende__css {
  /* Vorderkante oben, Hinterkante unten; der Kontrollpunkt liegt doppelt so weit draußen wie die sichtbare Wölbung */
  clip-path: shape(from 0% var(--kante),
    curve to 100% var(--kante) with 50% calc(var(--kante) - 2 * var(--bogen)),
    line to 100% var(--fuss),
    curve to 0% var(--fuss) with 50% calc(var(--fuss) + 2 * var(--fussbogen)),
    close);
}
.ohne-shape .blende__css { clip-path: inset(var(--kante) 0 calc(100% - var(--fuss)) 0); }   /* Fallback: gerader Wipe */
.blende[data-v='css'] .blende__css, .blende[data-v='flach'] .blende__flach { display: block; }
.blende[data-v='css'] .blende__svg, .blende[data-v='flach'] .blende__svg { display: none; }
.blende__text { position: absolute; inset: 0; display: grid; place-items: center; color: #e9eaea; }
.blende[data-v='css'] .blende__text { clip-path: shape(from 0% var(--kante), curve to 100% var(--kante) with 50% calc(var(--kante) - 2 * var(--bogen)), line to 100% var(--fuss), curve to 0% var(--fuss) with 50% calc(var(--fuss) + 2 * var(--fussbogen)), close); }
.ohne-shape .blende[data-v='css'] .blende__text { clip-path: inset(var(--kante) 0 calc(100% - var(--fuss)) 0); }
.blende__mitte { display: grid; justify-items: center; gap: 14px; text-align: center; }
.blende__mitte .meta { color: #9a9c9c; }
.blende__name { overflow: hidden; padding: 0 .06em .08em; }
.blende__name span { display: block; transform: translateY(105%); font-weight: 450; font-size: clamp(2.441rem, 1rem + 5.4vw, 5.96rem); letter-spacing: -.03em; line-height: 1.04; }
.blende__ecke { position: absolute; left: var(--rand); right: var(--rand); bottom: 28px; display: flex; justify-content: space-between; gap: 16px; }
.blende__ecke .meta { color: #9a9c9c; }

/* ---------- Demo-Steuerung ---------- */
.mono { font: 500 11px/1.4 'Geist Mono', ui-monospace, monospace; letter-spacing: .14em; text-transform: uppercase; }
.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; background: rgba(255, 255, 255, .94); border: 1px solid var(--flaeche); box-shadow: 0 8px 28px rgba(31, 35, 35, .08); -webkit-backdrop-filter: blur(10px); backdrop-filter: blur(10px); }
.dock__zeile { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
.dock__zeile > span { color: var(--meta); }
.modi { display: flex; gap: 4px; flex-wrap: wrap; }
.modi button { font: inherit; letter-spacing: inherit; text-transform: inherit; color: var(--meta); background: none; border: 1px solid var(--flaeche); height: 30px; padding: 0 12px; cursor: pointer; transition: background-color .15s linear, color .15s linear, border-color .15s linear; }
.modi button:hover { color: var(--text); }
.modi button[aria-pressed='true'] { background: var(--dunkel); border-color: var(--dunkel); color: #fff; }
.werte { font: 400 10.5px/1.6 'Geist Mono', ui-monospace, monospace; letter-spacing: .02em; color: var(--meta); min-height: 5em; }
.werte > span { display: block; white-space: nowrap; }
.werte b { color: var(--text); font-weight: 500; }
.werte .live { color: var(--text); }

.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, .25); mix-blend-mode: difference; pointer-events: none; opacity: 0; transition: opacity .15s linear; }
.zeiger.an { opacity: 1; }

.reduziert .blende__name span { transform: none; }
:root.reduziert * { transition-duration: .01ms !important; animation-duration: .01ms !important; }
.vorschau body { overflow: hidden; }
.vorschau .werte { display: none; }

@media (hover: none) { .modi button { height: 36px; } }
@media (max-width: 1100px) {
  .liste a { grid-template-columns: 2.4em minmax(0, 5fr) minmax(0, 3fr) 3.4em 80px; }
  .liste .typ { display: none; }
  .liste .daumen { width: 80px; }
  .b-text { grid-column: 1 / 7; }
  .b-bild { grid-column: 7 / 13; }
}
@media (max-width: 760px) {
  :root { --kopf: 58px; }
  .kopf { grid-template-columns: auto 1fr; }
  .marke { grid-column: auto; }
  .marke span, .kopf__ort { display: none; }
  .nav { grid-column: auto; justify-self: end; gap: 16px; }
  .s { grid-template-columns: minmax(0, 1fr); padding-top: 24px; }
  .s > * { grid-column: 1 / -1 !important; grid-row: auto !important; }
  .b-text { gap: 20px; }
  .b-bild { margin-top: 32px; }
  .aktuell { grid-template-columns: minmax(0, 1fr); gap: 0; margin-top: 40px; }
  .aktuell a { padding: 14px 0; border-top: 1px solid var(--flaeche); }
  .liste a { grid-template-columns: minmax(0, 1fr) 72px; gap: 4px 14px; padding: 12px 0; }
  .liste .nr, .liste .jahr { display: none; }
  .liste b { grid-column: 1; font-size: var(--s1); }
  .liste .ort { grid-column: 1; grid-row: 2; }
  .liste .daumen { grid-column: 2; grid-row: 1 / 3; width: 72px; }
  .pr-text { margin-top: 24px; }
  .k-bild { margin-top: 32px; max-width: 320px; }
  .k-daten { grid-template-columns: minmax(0, 1fr); gap: 18px; }
  .karte { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .dock { width: calc(100% - 24px); bottom: 12px; padding: 8px 10px; }
  .dock__zeile > span { display: none; }
  .werte > span { white-space: normal; }
  .werte .lang, .werte .live { display: none; }
  .werte { min-height: 0; }
  .blende__ecke span:last-child { display: none; }
}
</style>
</head>
<body>
<header class="kopf">
  <a class="marke" href="#buero" data-ziel="buero" aria-label="Ahlers Haack Architekten, Büro">
    <svg viewBox="0 0 30 20" aria-hidden="true"><path d="M1.5 15.5Q15 1 28.5 15.5" fill="none" stroke="currentColor" stroke-width="1.6"/><path d="M1.5 18.5H28.5" stroke="currentColor" stroke-width="1.6"/></svg>
    Ahlers Haack <span>Architekten</span>
  </a>
  <nav class="nav" aria-label="Hauptnavigation">
    <a href="#buero" data-ziel="buero">Büro</a>
    <a href="#projekte" data-ziel="projekte">Projekte</a>
    <a href="#kontakt" data-ziel="kontakt">Kontakt</a>
  </nav>
  <span class="kopf__ort meta">Rostock</span>
</header>

<main id="seite"></main>
<p class="sr" aria-live="polite" data-ansage></p>

<div class="blende" data-v="svg" aria-hidden="true">
  <svg class="blende__svg" preserveAspectRatio="none"><path class="p-hell" d=""/><path class="p-dunkel" d=""/></svg>
  <div class="blende__css"></div>
  <div class="blende__flach"></div>
  <div class="blende__text">
    <div class="blende__mitte">
      <span class="meta" data-b-nr>02 / 04</span>
      <div class="blende__name"><span data-b-name>Projekte</span></div>
    </div>
    <div class="blende__ecke"><span class="meta">Ahlers Haack Architekten</span><span class="meta">Am Strande 3, Rostock</span></div>
  </div>
</div>

<aside class="dock" aria-label="Demo-Steuerung">
  <div class="dock__zeile mono">
    <span>Kante</span>
    <div class="modi" role="group" aria-label="Variante">
      <button type="button" data-variante="svg" aria-pressed="true">SVG-Pfad</button>
      <button type="button" data-variante="css" aria-pressed="false">CSS shape()</button>
      <button type="button" data-variante="doppel" aria-pressed="false">Doppelkurve</button>
    </div>
  </div>
  <p class="werte" data-werte></p>
</aside>
<div class="zeiger" aria-hidden="true"></div>

<script src="https://cdn.jsdelivr.net/npm/gsap@3.15.0/dist/gsap.min.js"></script>
<script>
(() => {
  const html = document.documentElement;
  const reduziert = () => html.classList.contains('reduziert');
  const $ = s => document.querySelector(s);
  const seite = $('#seite'), blende = $('.blende'), svg = $('.blende__svg');
  const pHell = $('.p-hell'), pDunkel = $('.p-dunkel'), bText = $('.blende__text');
  const bName = $('[data-b-name]'), bNr = $('[data-b-nr]'), ansage = $('[data-ansage]');
  const werteEl = $('[data-werte]'), zeiger = $('.zeiger');
  const kannShape = CSS.supports('clip-path', 'shape(from 0% 0%, line to 100% 100%)');
  if (!kannShape) html.classList.add('ohne-shape');

  /* ---------- Werte (gemessen bei dennissnellenberg.com, index-new.js) ---------- */
  const T = {
    rein: 500, bogenAuf: 400,      // Panel rein .5 s, Wölbung 0 → 10vh in .4 s, Power4.easeIn
    raus: 800, bogenZu: 850,       // Panel raus .8 s, Wölbung 10vh → 0 in .85 s, Power3.easeInOut
    halten: 380, name: 500,        // eigene Wahl: Seitenname steht kurz, steigt mit der Rezept-Kurve ein
    versatz: 100,                  // Doppelkurve: Abstand der beiden Flächen
    fade: 200, seite: 48           // reduziert: gerader Fade · Seite wandert 48 px mit
  };
  const K = { p4in: 'power4.in', p3io: 'power3.inOut', css: 'cubic-bezier(0.65, 0, 0.35, 1)', aus: 'cubic-bezier(0.1, 0.9, 0.3, 1)' };
  const mobil = () => matchMedia('(max-width: 760px)').matches;
  const bogenVh = () => mobil() ? 5 : 10;
  const bogenPx = () => innerHeight * bogenVh() / 100;

  /* ---------- Bildwelt: eigene Ansichten als SVG ---------- */
  let uid = 0;
  const verlauf = (id, a, b, x2 = 0, y2 = 1) => `<linearGradient id="${id}" x1="0" y1="0" x2="${x2}" y2="${y2}"><stop offset="0" stop-color="${a}"/><stop offset="1" stop-color="${b}"/></linearGradient>`;
  const leute = (pos, f = '#2e3336') => pos.map(([x, y, s = 1]) => `<g transform="translate(${x} ${y}) scale(${s})" fill="${f}"><circle cx="0" cy="-15.5" r="2.1"/><path d="M-2.2 -13h4.4l.6 7.5h-1.1L1 0h-.9L0-5.5-.1 0H-1l-.7-5.5h-1.1z"/></g>`).join('');
  const SZENEN = {
    pavillon(u) {
      return `<defs>${verlauf(u + 'h', '#d4dadd', '#f2f1ee')}${verlauf(u + 'm', '#8a979d', '#aeb7bb')}${verlauf(u + 's', '#e8e2d7', '#d3cabb')}${verlauf(u + 'd', '#ffffff', '#dcdfe0')}${verlauf(u + 'u', '#b9bec0', '#e3e5e5')}${verlauf(u + 'g', '#4a5458', '#2c3336')}</defs>
        <rect width="600" height="400" fill="url(#${u}h)"/>
        <rect y="236" width="600" height="28" fill="url(#${u}m)"/><path d="M0 236H600" stroke="#7d8a90" stroke-width=".6"/>
        <rect y="262" width="600" height="138" fill="url(#${u}s)"/>
        <ellipse cx="300" cy="320" rx="236" ry="9" fill="#1f2323" opacity=".12"/>
        <rect x="132" y="246" width="336" height="74" fill="url(#${u}g)"/>
        <g stroke="#6b777c" stroke-width="1">${[166, 200, 234, 266, 334, 366, 400, 434].map(x => `<path d="M${x} 246V320"/>`).join('')}</g>
        <path d="M132 262H468" stroke="#5d686d" stroke-width=".8"/><rect x="132" y="246" width="336" height="16" fill="#fff" opacity=".07"/>
        <path d="M70 200Q185 262 300 226Q415 262 530 200L526 210Q415 270 300 238Q185 270 74 210Z" fill="url(#${u}d)"/>
        <path d="M74 210Q185 270 300 238Q415 270 526 210L468 246H132Z" fill="url(#${u}u)"/>
        <path d="M184 238 196 238 204 320 176 320Z M404 238 416 238 424 320 396 320Z" fill="#e4e6e6"/><path d="M196 238 204 320H198L192 240Z M416 238 424 320H418L412 240Z" fill="#c3c8ca"/>
        <path d="M300 238 304 320H296Z" fill="#d5d9da"/>
        ${leute([[498, 330], [512, 334, 1.06], [92, 352, 1.2]])}
        <g stroke="#a59c88" stroke-width="1" fill="none" opacity=".8"><path d="M30 384q4-18 2-30M38 386q2-16 9-26M46 388q-1-14-6-24M560 380q2-14-3-24M568 382q4-12 10-20"/></g>`;
    },
    halle(u) {
      return `<defs>${verlauf(u + 'h', '#cfd5d8', '#eeeeeb')}${verlauf(u + 'w', '#a5afb3', '#7f8b90')}${verlauf(u + 'b', '#c1a585', '#8e7457')}<clipPath id="${u}c"><path d="M92 292Q300 96 508 292Z"/></clipPath></defs>
        <rect width="600" height="400" fill="url(#${u}h)"/>
        <rect y="292" width="600" height="108" fill="url(#${u}w)"/>
        <path d="M60 292Q300 74 540 292Z" fill="url(#${u}b)"/>
        <g clip-path="url(#${u}c)"><rect x="80" y="90" width="440" height="210" fill="#2f3739"/>${[150, 196, 242, 288, 334, 380, 426, 472].map(x => `<path d="M${x} 90V300" stroke="#58646a" stroke-width="1.4"/>`).join('')}<path d="M80 230H520" stroke="#58646a"/><rect x="80" y="90" width="440" height="80" fill="#fff" opacity=".06"/></g>
        <g fill="none" stroke="#6f5a43" stroke-width="1.2" opacity=".55"><path d="M76 292Q300 85 524 292"/><path d="M84 292Q300 91 516 292"/></g>
        <path d="M300 292 288 292 300 260 312 292Z" fill="#cdd2d3"/>
        <g opacity=".22" transform="matrix(1 0 0 -.45 0 424)"><path d="M60 292Q300 74 540 292Z" fill="#8e7457"/></g>
        <path d="M0 304H600M40 318H560M0 334H600" stroke="#ffffff" stroke-width=".7" opacity=".35"/>
        <path d="M232 300h120l-8 7H240z" fill="#efefec"/>${leute([[318, 300, .9]])}`;
    },
    gitter(u) {
      return `<defs>${verlauf(u + 'h', '#d6dbdc', '#f1f0ec')}${verlauf(u + 'g', '#bcc2b2', '#a3aa98')}${verlauf(u + 'd', '#d8c6aa', '#b49d7e')}<clipPath id="${u}c"><path d="M48 300C110 150 490 150 552 300Z"/></clipPath></defs>
        <rect width="600" height="400" fill="url(#${u}h)"/>
        <g fill="#9ea596" opacity=".75"><circle cx="40" cy="250" r="44"/><circle cx="92" cy="262" r="34"/><circle cx="560" cy="246" r="48"/><circle cx="512" cy="266" r="30"/></g>
        <rect y="296" width="600" height="104" fill="url(#${u}g)"/>
        <path d="M48 300C110 150 490 150 552 300Z" fill="url(#${u}d)"/>
        <g clip-path="url(#${u}c)" stroke="#86705a" stroke-width="1.1" opacity=".75">${Array.from({ length: 26 }, (_, i) => `<path d="M${i * 34 - 180} 300L${i * 34 + 20} 140M${i * 34 - 180} 140L${i * 34 + 20} 300"/>`).join('')}</g>
        <path d="M150 300V262Q300 236 450 262V300Z" fill="#2f3739"/><g stroke="#5b676c">${[200, 250, 300, 350, 400].map(x => `<path d="M${x} 300V252"/>`).join('')}</g>
        <ellipse cx="300" cy="302" rx="270" ry="5" fill="#1f2323" opacity=".12"/>
        ${leute([[118, 322, .8], [130, 324, .62], [470, 330, .95]])}`;
    },
    wohnen(u) {
      const haus = (x, w) => `<path d="M${x} 312V200Q${x + w / 2} 150 ${x + w} 200V312Z" fill="url(#${u}z)"/><path d="M${x} 200Q${x + w / 2} 150 ${x + w} 200" fill="none" stroke="#e7e4df" stroke-width="3"/>
        ${[0, 1].map(r => [0.2, 0.56].map(c => `<rect x="${x + w * c}" y="${222 + r * 44}" width="${w * .24}" height="30" fill="#2f3739"/>`).join('')).join('')}`;
      return `<defs>${verlauf(u + 'h', '#d1d6d8', '#efeeea')}${verlauf(u + 'z', '#a98676', '#8c6e61')}${verlauf(u + 'p', '#c7c8c4', '#adaeaa')}</defs>
        <rect width="600" height="400" fill="url(#${u}h)"/>
        ${haus(40, 120)}${haus(168, 120)}${haus(296, 120)}${haus(424, 120)}
        <rect y="312" width="600" height="88" fill="url(#${u}p)"/>
        <g stroke="#9c9d99" stroke-width=".8" opacity=".6">${Array.from({ length: 9 }, (_, i) => `<path d="M0 ${322 + i * 9}H600"/>`).join('')}</g>
        <path d="M0 312H600" stroke="#7f817d"/>
        ${leute([[238, 340], [254, 344, .9], [480, 352, 1.15]])}`;
    },
    faltwerk(u) {
      const zacken = Array.from({ length: 9 }, (_, i) => `${80 + i * 55} ${i % 2 ? 196 : 168}`).join(' L');
      return `<defs>${verlauf(u + 'h', '#d0d6d9', '#f0efec')}${verlauf(u + 'w', '#a3adb1', '#86939a')}</defs>
        <rect width="600" height="400" fill="url(#${u}h)"/>
        <rect y="270" width="600" height="130" fill="url(#${u}w)"/>
        <path d="M${zacken} L520 210 L80 210Z" fill="#f4f4f2"/>
        ${Array.from({ length: 8 }, (_, i) => `<path d="M${80 + i * 55} ${i % 2 ? 196 : 168}L${135 + i * 55} ${i % 2 ? 168 : 196}L${135 + i * 55} 210L${80 + i * 55} 210Z" fill="${i % 2 ? '#dcdfe0' : '#c9cdcf'}"/>`).join('')}
        <rect x="104" y="210" width="392" height="56" fill="#323a3d"/>${[160, 216, 272, 328, 384, 440].map(x => `<path d="M${x} 210V266" stroke="#5f6b70"/>`).join('')}
        <rect x="80" y="266" width="440" height="8" fill="#dadcdc"/>
        <g fill="#cfd2d2">${[96, 300, 504].map(x => `<rect x="${x - 3}" y="274" width="6" height="40"/>`).join('')}</g>
        <g opacity=".2" transform="matrix(1 0 0 -.5 0 410)"><path d="M${zacken} L520 210 L80 210Z" fill="#f4f4f2"/></g>
        <path d="M0 290H600M30 306H570M0 326H600" stroke="#fff" stroke-width=".7" opacity=".3"/>
        ${leute([[180, 266, .9], [194, 266, .85], [420, 266, .9]])}`;
    },
    kapelle(u) {
      return `<defs>${verlauf(u + 'h', '#c9d0cf', '#e9ebe7')}${verlauf(u + 'z', '#b08a77', '#8f6d5d')}${verlauf(u + 'g', '#a9ae9f', '#8f9586')}</defs>
        <rect width="600" height="400" fill="url(#${u}h)"/>
        <g stroke="#7d847a" opacity=".55">${[[30, 3], [70, 2], [118, 4], [160, 2], [430, 3], [474, 2], [512, 4], [566, 3]].map(([x, w]) => `<path d="M${x} 0V312" stroke-width="${w}"/>`).join('')}</g>
        <g fill="#8f998b" opacity=".35"><ellipse cx="80" cy="60" rx="110" ry="70"/><ellipse cx="520" cy="50" rx="120" ry="72"/></g>
        <rect y="306" width="600" height="94" fill="url(#${u}g)"/>
        <path d="M190 312Q300 -10 410 312Z" fill="url(#${u}z)"/>
        <g stroke="#7b5d50" stroke-width=".7" opacity=".5"><clipPath id="${u}k"><path d="M190 312Q300 -10 410 312Z"/></clipPath><g clip-path="url(#${u}k)">${Array.from({ length: 24 }, (_, i) => `<path d="M180 ${306 - i * 12}H420"/>`).join('')}</g></g>
        <path d="M262 312Q300 170 338 312Z" fill="#2a2f31"/>
        <path d="M190 312Q300 -10 410 312Z" fill="none" stroke="#c9ab9b" stroke-width="1.2"/>
        <ellipse cx="300" cy="314" rx="140" ry="5" fill="#1f2323" opacity=".14"/>
        ${leute([[352, 330, 1.05], [366, 332, .9]])}`;
    }
  };
  const szene = (art, label) => `<svg viewBox="0 0 600 400" preserveAspectRatio="xMidYMid slice" ${label ? `role="img" aria-label="${label}"` : 'aria-hidden="true"'}>${SZENEN[art]('sz' + (++uid))}</svg>`;

  /* ---------- Inhalte ---------- */
  const PROJEKTE = [
    { id: 'strandpavillon', name: 'Strandpavillon Markgrafenheide', kurz: 'Strandpavillon', ort: 'Rostock-Markgrafenheide', typ: 'Gastronomie', jahr: 2025, szene: 'pavillon',
      lead: 'Drei gekrümmte Betonflächen, an den Rändern sieben Zentimeter dünn. Darunter ein Raum aus Glas, der zur See hin ganz aufgeht.',
      text: 'Die Schale steht nur auf drei Punkten. So bleibt der Blick vom Strandweg auf die Ostsee frei. Gegossen wurde sie in einer Schalung aus geraden Brettern, denn die Fläche lässt sich aus Geraden erzeugen.',
      fakten: [['Bauherr', 'Hansestadt Rostock, Tourismuszentrale'], ['Fläche', '310 m² BGF'], ['Tragwerk', 'Spritzbetonschale, 7 bis 12 cm'], ['Leistung', 'LPH 1 bis 8'], ['Fertig', 'Mai 2025']] },
    { id: 'bootshalle', name: 'Bootshalle Gehlsdorf', kurz: 'Bootshalle', ort: 'Rostock-Gehlsdorf', typ: 'Sport', jahr: 2024, szene: 'halle',
      lead: 'Ein einziger Bogen aus Brettschichtholz überspannt 32 Meter. Die Ruderboote liegen darunter, ohne eine Stütze im Weg.',
      text: 'Der Verein wollte eine Halle, die man vom anderen Ufer der Warnow erkennt. Die Giebel sind verglast, abends leuchtet die Halle über das Wasser.',
      fakten: [['Bauherr', 'Rostocker Ruderclub von 1885 e. V.'], ['Fläche', '840 m² BGF'], ['Tragwerk', 'BSH-Bögen, Spannweite 32 m'], ['Leistung', 'LPH 1 bis 9'], ['Fertig', 'September 2024']] },
    { id: 'duenenhaus', name: 'Kita Dünenhaus', kurz: 'Dünenhaus', ort: 'Warnemünde', typ: 'Bildung', jahr: 2023, szene: 'gitter',
      lead: 'Eine Gitterschale aus Lärchenlatten, flach wie eine Düne. Innen ein einziger, heller Raum für 90 Kinder.',
      text: 'Die Latten wurden flach verlegt und erst auf der Baustelle in Form gebogen. Das spart Material und hält das Dach leicht.',
      fakten: [['Bauherr', 'Kita gGmbH Rostock'], ['Fläche', '1.120 m² BGF'], ['Tragwerk', 'Holz-Gitterschale, Lärche'], ['Leistung', 'LPH 2 bis 8'], ['Fertig', 'August 2023']] },
    { id: 'holzhafen', name: 'Wohnhöfe am Holzhafen', kurz: 'Holzhafen', ort: 'Rostock-Mitte', typ: 'Wohnen', jahr: 2022, szene: 'wohnen',
      lead: '48 Wohnungen unter Tonnendächern. Die Rundung gibt den Dachwohnungen Höhe, ohne das Haus höher zu machen.',
      text: 'Klinker aus der Region, Fenster in einem einzigen Format. Die Höfe sind öffentlich und führen bis ans Hafenbecken.',
      fakten: [['Bauherr', 'Wohnungsgenossenschaft Warnow eG'], ['Fläche', '5.400 m² BGF'], ['Tragwerk', 'Massivbau, Tonnen in Holz'], ['Leistung', 'LPH 1 bis 5'], ['Fertig', 'November 2022']] },
    { id: 'kabutzenhof', name: 'Fähranleger Kabutzenhof', kurz: 'Kabutzenhof', ort: 'Rostock', typ: 'Infrastruktur', jahr: 2021, szene: 'faltwerk',
      lead: 'Ein gefaltetes Dach aus Betonfertigteilen. Die Falten tragen und führen das Regenwasser ab.',
      text: 'Wartende stehen trocken und sehen die Fähre kommen. Drei Stützen genügen, der Rest hängt im Faltwerk.',
      fakten: [['Bauherr', 'Hafen-Entwicklungsgesellschaft Rostock'], ['Fläche', '260 m² überdacht'], ['Tragwerk', 'Faltwerk, Fertigteile'], ['Leistung', 'LPH 1 bis 8'], ['Fertig', 'April 2021']] },
    { id: 'kapelle', name: 'Waldkapelle Rövershagen', kurz: 'Waldkapelle', ort: 'Rövershagen', typ: 'Sakral', jahr: 2019, szene: 'kapelle',
      lead: 'Ein Parabelbogen aus Ziegeln, gemauert ohne Stahl. Die Form folgt der Kraft, darum trägt sie sich selbst.',
      text: 'Gemauert hat ein Team aus der Region in elf Wochen. Die Kapelle hat keine Heizung und keinen Strom, nur eine Tür.',
      fakten: [['Bauherr', 'Ev.-Luth. Kirchengemeinde Rövershagen'], ['Fläche', '64 m²'], ['Tragwerk', 'Ziegel-Parabelbogen'], ['Leistung', 'LPH 1 bis 9'], ['Fertig', 'Juni 2019']] }
  ];
  const projekt = id => PROJEKTE.find(p => p.id === id);
  const SEITEN = {
    buero: { nr: '01 / 03', name: 'Büro', titel: 'Ahlers Haack Architekten' },
    projekte: { nr: '02 / 03', name: 'Projekte', titel: 'Projekte' },
    kontakt: { nr: '03 / 03', name: 'Kontakt', titel: 'Kontakt' }
  };
  const bildunterschrift = p => `${p.name} • ${p.ort} • ${p.jahr}`;

  const htmlBuero = () => { const p = PROJEKTE[0]; return `<section class="s">
      <div class="b-text">
        <p class="meta">Architektur und Tragwerk · seit 1998</p>
        <h1 class="h1" tabindex="-1">Wir bauen Dächer, die tragen, <em>weil</em> sie gekrümmt sind.</h1>
        <p class="text">Schalen, Bögen und Faltwerke brauchen weniger Material als gerade Decken. Wir planen sie für Orte an der Küste, vom Strandpavillon bis zur Bootshalle, und begleiten sie bis zur Abnahme.</p>
        <a class="pfeil unterstrichen" href="#projekte" data-ziel="projekte">Werkverzeichnis <i aria-hidden="true">→</i></a>
      </div>
      <figure class="b-bild"><a href="#${p.id}" data-ziel="${p.id}" class="bild" style="display:block">${szene(p.szene, 'Ansicht ' + p.name)}</a><figcaption class="meta">${bildunterschrift(p)}</figcaption></figure>
      <div class="aktuell">
        <a href="#bootshalle" data-ziel="bootshalle"><span class="meta">Oktober 2026 · Auszeichnung</span><b>Bootshalle Gehlsdorf erhält den Holzbaupreis Mecklenburg-Vorpommern</b></a>
        <a href="#duenenhaus" data-ziel="duenenhaus"><span class="meta">August 2026 · Vortrag</span><b>„Dünn bauen“: Werkbericht an der Hochschule Wismar</b></a>
        <a href="#kontakt" data-ziel="kontakt"><span class="meta">Laufend · Büro</span><b>Wir suchen eine Architektin oder einen Architekten für die Ausführungsplanung</b></a>
      </div>
    </section>`; };
  const htmlProjekte = () => `<section class="s">
      <div class="p-kopf">
        <h1 class="h1" tabindex="-1">Projekte<sup>${PROJEKTE.length}</sup></h1>
        <div class="filter" aria-label="Typologie"><span>Alle</span><span>Kultur</span><span>Wohnen</span><span>Bildung</span><span>Sport</span><span>Infrastruktur</span></div>
      </div>
      <ul class="liste">${PROJEKTE.map((p, i) => `<li><a href="#${p.id}" data-ziel="${p.id}">
        <span class="nr">${String(i + 1).padStart(2, '0')}</span><b>${p.name}</b><span class="ort">${p.ort}</span><span class="typ">${p.typ}</span><span class="jahr">${p.jahr}</span>
        <span class="daumen bild">${szene(p.szene, '')}</span></a></li>`).join('')}</ul>
    </section>`;
  const htmlProjekt = p => { const n = PROJEKTE[(PROJEKTE.indexOf(p) + 1) % PROJEKTE.length]; return `<section class="s">
      <figure class="pr-bild"><div class="bild">${szene(p.szene, 'Ansicht ' + p.name)}</div><figcaption class="meta">${bildunterschrift(p)}</figcaption></figure>
      <div class="pr-text">
        <p class="meta">${p.typ} · ${p.ort} · ${p.jahr}</p>
        <h1 class="h1" tabindex="-1">${p.name}</h1>
        <p class="lead">${p.lead}</p>
        <dl class="fakten">${p.fakten.map(([a, b]) => `<div><dt>${a}</dt><dd>${b}</dd></div>`).join('')}</dl>
        <p class="text">${p.text}</p>
        <a class="naechstes" href="#${n.id}" data-ziel="${n.id}"><span class="meta">Nächstes Projekt</span><b class="pfeil">${n.name} <i aria-hidden="true">→</i></b></a>
      </div>
    </section>`; };
  const marke = '<svg viewBox="0 0 30 20" aria-hidden="true"><path d="M1.5 15.5Q15 1 28.5 15.5" fill="none" stroke="currentColor" stroke-width="1.6"/><path d="M1.5 18.5H28.5" stroke="currentColor" stroke-width="1.6"/></svg>';
  const htmlKontakt = () => `<section class="s">
      <h1 class="k-titel" tabindex="-1">Am Strande 3<br><em>18055 Rostock</em></h1>
      <figure class="k-bild"><div class="bild">${szene('faltwerk', 'Fähranleger Kabutzenhof, fünf Minuten vom Büro')}</div><figcaption class="meta">Fähranleger Kabutzenhof, fünf Minuten zu Fuß</figcaption></figure>
      <div class="k-daten">
        <p><span class="meta">Telefon</span><a href="#kontakt" data-ziel="kontakt">0381 203 47 60</a></p>
        <p><span class="meta">E-Mail</span><a class="unterstrichen" href="#kontakt" data-ziel="kontakt">buero@lueders-haack.de</a></p>
        <p><span class="meta">Bürozeiten</span>Mo bis Do 8.30 bis 17 Uhr<br>Fr bis 14 Uhr</p>
        <p><span class="meta">Anreise</span>Straßenbahn 1, 2, 5, 6<br>Haltestelle Steintor</p>
      </div>
      <div class="karte">
        <p>${marke}</p>
        <p><span class="meta">Büro</span><br>Ahlers Haack Architekten PartGmbB</p>
        <p><span class="meta">Partner</span><br>Ina Ahlers, Jonas Haack</p>
        <p><span class="meta">Kammer</span><br>Architektenkammer M-V, Nr. 2214-03</p>
      </div>
    </section>`;

  function info(ziel) {
    const p = projekt(ziel);
    if (p) return { nr: `Projekt ${String(PROJEKTE.indexOf(p) + 1).padStart(2, '0')} / ${String(PROJEKTE.length).padStart(2, '0')}`, name: p.kurz, titel: p.name, nav: 'projekte' };
    return { ...SEITEN[ziel], nav: ziel };
  }
  let aktuell = 'buero';
  function zeige(ziel) {
    const p = projekt(ziel), i = info(ziel);
    seite.innerHTML = p ? htmlProjekt(p) : ({ buero: htmlBuero, projekte: htmlProjekte, kontakt: htmlKontakt })[ziel]();
    document.querySelectorAll('.nav a').forEach(a => a.dataset.ziel === i.nav ? a.setAttribute('aria-current', 'page') : a.removeAttribute('aria-current'));
    document.title = (ziel === 'buero' ? 'Ahlers Haack Architekten' : i.titel + ' · Ahlers Haack Architekten') + ' · Fundus-Demo';
    aktuell = ziel;
  }

  /* ---------- Varianten und Werte-Anzeige ---------- */
  const VARIANTEN = {
    svg: () => [
      ['Technik', 'SVG-Pfad, GSAP tweent die Kontrollpunkte'],
      ['rein', `${T.rein} ms power4.in · Wölbung 0 → ${bogenVh()}vh in ${T.bogenAuf} ms power4.in`],
      ['raus', `${T.raus} ms power3.inOut · Wölbung ${bogenVh()}vh → 0 in ${T.bogenZu} ms power3.inOut`]],
    css: () => [
      ['Technik', kannShape ? 'clip-path: shape() + @property, ohne Bibliothek' : 'shape() fehlt hier · Fallback inset(), gerade Kante'],
      ['rein', `${T.rein} ms · Wölbung 0 → ${bogenVh()}vh → 0 · cubic-bezier(0.65, 0, 0.35, 1)`],
      ['raus', `${T.raus} ms · Hinterkante 0 → ${bogenVh()}vh → 0 · gleiche Kurve`]],
    doppel: () => [
      ['Technik', `zwei SVG-Pfade #C5C7C7 und #1F2323 · Versatz ${T.versatz} ms`],
      ['rein', `${T.rein} ms power4.in · Wölbung 0 → ${bogenVh()}vh in ${T.bogenAuf} ms`],
      ['raus', `${T.raus} ms power3.inOut · Wölbung ${bogenVh()}vh → 0 in ${T.bogenZu} ms`]]
  };
  let variante = 'svg', liveText = 'live · wartet auf einen Klick';
  function zeigeWerte() {
    const z = reduziert() ? [['reduziert', `gerader Fade ${T.fade} ms linear, keine Wölbung`]] : VARIANTEN[variante]();
    werteEl.innerHTML = z.map(([a, b], i) => `<span class="${i === 0 ? '' : 'lang'}"><b>${a}</b> ${b}</span>`).join('') + `<span class="live">${liveText}</span>`;
  }
  let liveUhr = 0;
  function live(kante, bogen) {
    liveText = `live · Kante ${Math.round(kante)} % · Wölbung ${bogen.toFixed(1).replace('.', ',')} vh`;
    const now = performance.now();
    if (now - liveUhr > 50) { liveUhr = now; const l = werteEl.querySelector('.live'); if (l) l.textContent = liveText; }
  }
  function setzeVariante(v) {
    variante = v;
    document.querySelectorAll('[data-variante]').forEach(b => b.setAttribute('aria-pressed', String(b.dataset.variante === v)));
    zeigeWerte();
  }

  /* ---------- Geometrie der Kante ---------- */
  // Vorderkante: Ecken auf y = kante, Mitte um "bogen" höher. Hinterkante: Ecken auf y = fuss, Mitte um "fussbogen" tiefer.
  // Beim quadratischen Bézier liegt die Kurvenmitte auf halbem Weg zum Kontrollpunkt, darum steht der Kontrollpunkt bei 2 × Wölbung.
  const W = () => innerWidth, H = () => innerHeight;
  const pfad = s => { const w = W(), m = w / 2; return `M0 ${s.kante.toFixed(1)} Q${m} ${(s.kante - 2 * s.bogen).toFixed(1)} ${w} ${s.kante.toFixed(1)} L${w} ${s.fuss.toFixed(1)} Q${m} ${(s.fuss + 2 * s.fussbogen).toFixed(1)} 0 ${s.fuss.toFixed(1)} Z`; };
  const leer = () => ({ kante: H(), bogen: 0, fuss: H(), fussbogen: 0 });
  function groesse() { svg.setAttribute('viewBox', `0 0 ${W()} ${H()}`); }
  groesse();
  addEventListener('resize', () => { groesse(); zeigeWerte(); });

  /* ---------- Der Übergang ---------- */
  let laeuft = false, lauf = 0, gsapLaeufe = [], waapi = [], loeser = [];
  const warte = (n, t) => new Promise(r => { const id = setTimeout(() => r(t === lauf), n); loeser.push(() => { clearTimeout(id); r(false); }); });
  const fertigTl = tl => new Promise(r => { tl.eventCallback('onComplete', () => r(true)); loeser.push(() => r(false)); });
  const fertigA = list => Promise.all(list.map(a => a.finished.then(() => true, () => false)));
  const anim = (el, kf, o) => { const a = el.animate(kf, { fill: 'both', ...o }); waapi.push(a); return a; };
  const tl = o => { const t = gsap.timeline(o); gsapLaeufe.push(t); return t; };

  function aufraeumen() {
    gsapLaeufe.forEach(t => t.kill()); gsapLaeufe = [];
    waapi.forEach(a => a.cancel()); waapi = [];
    loeser.forEach(f => f()); loeser = [];
    blende.classList.remove('an');
    pHell.setAttribute('d', ''); pDunkel.setAttribute('d', ''); bText.style.clipPath = '';
    laeuft = false; liveText = 'live · wartet auf einen Klick';
  }
  function abbrechen() { lauf++; aufraeumen(); }

  // Ausgehende Seite wandert mit der Vorderkante nach oben, die neue kommt mit der Hinterkante von unten nach
  const seiteRaus = (dauer, ease) => anim(seite, { transform: ['translateY(0)', `translateY(-${T.seite}px)`] }, { duration: dauer, easing: ease });
  const seiteRein = (dauer, ease) => anim(seite, { transform: [`translateY(${T.seite}px)`, 'translateY(0)'] }, { duration: dauer, easing: ease });
  const nameRein = () => anim(bName, { transform: ['translateY(105%)', 'translateY(0)'] }, { duration: T.name, easing: K.aus });

  async function uebergang(ziel, mitFokus) {
    if (laeuft) return;
    laeuft = true;
    const t = ++lauf, i = info(ziel), b = bogenPx(), vh = H() / 100;
    bName.textContent = i.name; bNr.textContent = i.nr;
    zeigeWerte();
    const tauschen = () => {
      waapi.filter(a => a.effect.target === seite).forEach(a => a.cancel());
      zeige(ziel); scrollTo(0, 0);
      ansage.textContent = 'Seite geladen: ' + i.titel;
    };

    if (reduziert()) {
      blende.dataset.v = 'flach'; blende.classList.add('an');
      await fertigA([anim(blende, { opacity: [0, 1] }, { duration: T.fade, easing: 'linear' })]);
      if (t !== lauf) return;
      tauschen();
      if (!(await warte(T.halten, t))) return;
      await fertigA([anim(blende, { opacity: [1, 0] }, { duration: T.fade, easing: 'linear' })]);
    } else if (variante === 'css') {
      // Reines CSS: die vier Längen sind per @property registriert und werden direkt animiert
      blende.dataset.v = 'css'; blende.classList.add('an');
      const ease = K.css, bb = b + 'px';
      const rein = anim(blende, { '--kante': ['100%', '0%'] }, { duration: T.rein, easing: ease });
      anim(blende, [{ '--bogen': '0px' }, { '--bogen': bb, offset: .5 }, { '--bogen': '0px' }], { duration: T.rein, easing: ease });
      seiteRaus(T.rein, ease);
      const leser = () => { if (t !== lauf || !laeuft) return; const cs = getComputedStyle(blende); const f = parseFloat(cs.getPropertyValue('--fuss')), k = parseFloat(cs.getPropertyValue('--kante')); const kp = f < 100 ? f : k; const kb = parseFloat(cs.getPropertyValue(f < 100 ? '--fussbogen' : '--bogen')); live(kp, kb / vh); requestAnimationFrame(leser); };
      requestAnimationFrame(leser);
      await fertigA([rein]);
      if (t !== lauf) return;
      tauschen(); nameRein();
      if (!(await warte(T.halten, t))) return;
      const raus = anim(blende, { '--fuss': ['100%', '0%'] }, { duration: T.raus, easing: ease });
      anim(blende, [{ '--fussbogen': '0px' }, { '--fussbogen': bb, offset: .5 }, { '--fussbogen': '0px' }], { duration: T.raus, easing: ease });
      seiteRein(T.raus, ease);
      await fertigA([raus]);
    } else {
      // SVG: GSAP tweent Zahlen, der Pfad wird in jedem Frame neu geschrieben
      const doppel = variante === 'doppel';
      blende.dataset.v = 'svg'; blende.classList.add('an');
      const sD = leer(), sH = leer();
      const malen = () => {
        const d = pfad(sD);
        pDunkel.setAttribute('d', d); bText.style.clipPath = `path("${d}")`;
        pHell.setAttribute('d', doppel ? pfad(sH) : '');
        const aktiv = sD.fuss < H() - .5 ? [sD.fuss, sD.fussbogen] : [sD.kante, sD.bogen];
        live(aktiv[0] / H() * 100, aktiv[1] / vh);
      };
      malen();
      const a = tl({ onUpdate: malen });
      const vD = doppel ? T.versatz / 1000 : 0;
      if (doppel) a.to(sH, { kante: 0, duration: T.rein / 1000, ease: K.p4in }, 0).to(sH, { bogen: b, duration: T.bogenAuf / 1000, ease: K.p4in }, 0);
      a.to(sD, { kante: 0, duration: T.rein / 1000, ease: K.p4in }, vD).to(sD, { bogen: b, duration: T.bogenAuf / 1000, ease: K.p4in }, vD);
      seiteRaus(T.rein + (doppel ? T.versatz : 0), 'cubic-bezier(0.64, 0, 0.78, 0)');
      if (!(await fertigTl(a)) || t !== lauf) return;
      tauschen(); nameRein();
      if (!(await warte(T.halten, t))) return;
      // Hinterkante: Ecken steigen, die Wölbung hängt nach unten und läuft länger aus als das Panel (850 statt 800 ms)
      sD.fussbogen = b; sH.fussbogen = b;
      const z = tl({ onUpdate: malen });
      z.to(sD, { fuss: 0, duration: T.raus / 1000, ease: K.p3io }, 0).to(sD, { fussbogen: 0, duration: T.bogenZu / 1000, ease: K.p3io }, 0);
      if (doppel) z.to(sH, { fuss: 0, duration: T.raus / 1000, ease: K.p3io }, vD).to(sH, { fussbogen: 0, duration: T.bogenZu / 1000, ease: K.p3io }, vD);
      seiteRein(T.raus, 'cubic-bezier(0.65, 0, 0.35, 1)');
      if (!(await fertigTl(z))) return;
    }
    if (t !== lauf) return;
    aufraeumen(); zeigeWerte();
    if (mitFokus) { const h = seite.querySelector('h1'); if (h) h.focus({ preventScroll: true }); }
  }

  document.addEventListener('click', e => {
    const a = e.target.closest('[data-ziel]');
    if (!a) return;
    e.preventDefault();
    if (a.dataset.ziel === aktuell || laeuft) return;
    stoppeVorfuehrung();
    uebergang(a.dataset.ziel, true);
  });
  document.querySelectorAll('[data-variante]').forEach(b => b.addEventListener('click', () => { stoppeVorfuehrung(); setzeVariante(b.dataset.variante); }));

  /* ---------- Vorführung: ein gedachter Zeiger klickt sich durch das Büro ---------- */
  const PFAD = [
    ['.nav [data-ziel="projekte"]', 'css'],
    ['.liste [data-ziel="strandpavillon"]', 'svg'],
    ['.naechstes', 'doppel'],
    ['.nav [data-ziel="kontakt"]', 'css'],
    ['.marke', 'svg'],
    ['.aktuell [data-ziel="duenenhaus"]', 'doppel'],
    ['.nav [data-ziel="buero"]', 'css']
  ];
  let runde = 0, uhr = null, lief = false, schritt = 0, zx = 0, zy = 0;
  const pause_ = (n, r) => new Promise(res => { uhr = setTimeout(() => res(r === runde), n); });
  function zeigerZu(x, y, r) {
    const von = `translate(${zx}px, ${zy}px)`, nach = `translate(${x}px, ${y}px)`;
    zx = x; zy = y;
    zeiger.animate({ transform: [von, nach] }, { duration: reduziert() ? 1 : 650, easing: K.aus, fill: 'forwards' });
    return pause_(reduziert() ? 150 : 680, r);
  }
  const dauer = () => reduziert() ? 2 * T.fade + T.halten : T.rein + T.halten + T.bogenZu + (variante === 'doppel' ? 2 * T.versatz : 0);
  async function spiel(r, erstPause) {
    if (!(await pause_(erstPause, r))) return;
    while (r === runde) {
      const [sel, v] = PFAD[schritt % PFAD.length];
      let el = document.querySelector(sel);
      if (!el || !el.getClientRects().length) el = document.querySelector('.marke');
      const rc = el.getBoundingClientRect();
      const x = Math.round(rc.left + Math.min(rc.width * .5, 140)), y = Math.round(Math.min(rc.top + rc.height * .5, innerHeight - 160));
      if (!(await zeigerZu(x, y, r))) return;
      zeiger.animate({ scale: [1, .7, 1] }, { duration: 240, easing: K.aus });
      setzeVariante(v);
      schritt++;
      const ziel = el.dataset.ziel;
      if (ziel !== aktuell) {
        uebergang(ziel, false);
        if (!(await pause_(dauer() + 1100, r))) return;
      }
    }
  }
  let nachStart = null;
  function stoppeVorfuehrung() { runde++; clearTimeout(uhr); clearTimeout(nachStart); lief = false; zeiger.classList.remove('an'); }

  function neustart() {
    stoppeVorfuehrung(); abbrechen();
    zeige('buero'); scrollTo(0, 0); zeigeWerte();
    nachStart = setTimeout(() => uebergang('projekte', false), 450);
  }
  function vorfuehren() {
    stoppeVorfuehrung(); abbrechen();
    zeige('buero'); scrollTo(0, 0);
    schritt = 0; lief = true;
    zx = Math.round(innerWidth * .52); zy = Math.round(innerHeight * .4);
    zeiger.animate({ transform: [`translate(${zx}px, ${zy}px)`] }, { duration: 1, fill: 'forwards' });
    zeiger.classList.add('an');
    spiel(runde, 560);
  }
  function pause() {
    const war = lief; stoppeVorfuehrung(); lief = war;
    gsapLaeufe.forEach(t => t.pause()); waapi.forEach(a => a.playState === 'running' && a.pause());
  }
  function weiter() {
    gsapLaeufe.forEach(t => t.paused() && t.resume()); waapi.forEach(a => a.playState === 'paused' && a.play());
    if (lief) { const r = ++runde; zeiger.classList.add('an'); spiel(r, dauer() + 600); }
  }

  function dockMessen() { html.style.setProperty('--dock', Math.ceil($('.dock').getBoundingClientRect().height) + 'px'); }
  setzeVariante('svg');
  zeige('buero');
  addEventListener('load', dockMessen);
  addEventListener('resize', dockMessen);
  if (document.fonts) document.fonts.ready.then(dockMessen);

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