Zum Inhalt
← Seitenübergänge

Vorschaubild wird zur Hero (Shared-Element-Morph)

Das angeklickte Vorschaubild fliegt aus dem Raster genau an die Stelle des Hero-Bilds, erst danach steigt der Titel auf; nativ per View Transition oder mit GSAP Flip.

Klick auf ein Vorschaubild im Raster. Zuerst blenden alle anderen Inhalte aus (300 bis 500 ms). Dann löst sich das angeklickte Bild aus dem Raster und wächst in Position und Größe genau dorthin, wo auf der Detailseite das Hero-Bild sitzt. Dabei geht der Kartenradius auf 0, oft mit leichtem Innen-Zoom (Bild-Scale 1.1 auf 1.0). Erst danach erscheinen Titel und Inhalt der Detailseite. Zurück läuft alles spiegelbildlich ins Raster.

Objektpermanenz: Das Auge verfolgt ein einziges Objekt. Der Ortswechsel wird räumlich verständlich, und das Bild bekommt Gewicht. Der Übergang gilt als Königsdisziplin im Portfolio.

Wann einsetzen

Bei Portfolio-, Case-Study-, Shop- und Galerieseiten, auf denen ein Listenelement auf der Detailseite als Hero wiederkehrt.

Wann nicht

Wenn Vorschau und Hero unterschiedliche Motive zeigen. Bei stark abweichenden Seitenverhältnissen wirkt der Morph verzerrt, solange man nicht mit object-fit gegensteuert. Auch nicht, wenn das Vorschaubild beim Klick außerhalb des Viewports liegt.

Varianten

  • Nativ: gleicher view-transition-name auf Thumbnail und Hero (MPA oder SPA)
  • GSAP Flip: Das Element wird per Flip.getState und Flip.from in den Hero-Container umgehängt (mit Barba oder Taxi)
  • React <ViewTransition name=…> mit share-Animation
  • Mit Nested View Transition Groups (view-transition-group), damit das Bild im Karten-Container geclippt bleibt
  • Raster zu Slideshow oder Raster zu Vollbild innerhalb einer Seite (Codrops-Layout-Animationen)

Umsetzung

Nativ über Seiten hinweg: In beiden Seiten @view-transition { navigation: auto; }. Den Namen nur am angeklickten Vorschaubild setzen (im Klick-Handler oder im pageswap-Event). Auf der Zielseite trägt das Hero-Bild denselben Namen, z. B. view-transition-name: hero. In einer SPA genügt document.startViewTransition(() => seiteWechseln()). Feinjustieren mit ::view-transition-group(hero) { animation-duration: 1.3s; animation-timing-function: cubic-bezier(0.76, 0, 0.24, 1); overflow: clip; }. Bei abweichendem Seitenverhältnis ::view-transition-old(hero), ::view-transition-new(hero) { height: 100%; object-fit: cover; }. Zeigen beide Schnappschüsse dasselbe Motiv, den Crossfade abschalten (animation: none) und nur einen sichtbar lassen, sonst entstehen Doppelbilder. Radius und Innen-Zoom lassen sich nach transition.ready per document.documentElement.animate(…, { pseudoElement: '::view-transition-group(hero)' }) ergänzen. GSAP: const state = Flip.getState(img); heroContainer.appendChild(img); Flip.from(state, { duration: 1.3, ease: 'power3.inOut', absolute: true }). Fallstricke: Namen müssen pro Seite eindeutig sein (React meldet sonst einen Duplicate-Name-Fehler). Liegt das Element außerhalb des Viewports, bildet sich kein Paar. Ungeladene Bilder verzögern den Start, deshalb vorladen (Hover-Preload wie bei Thibault Guignand).

Robust umsetzen

Telefon. Funktioniert gut. Auf kleinen Bildschirmen ist der Weg kürzer, deshalb die Dauer auf 600 bis 900 ms senken (typisch).

Ohne Bewegung. Morph durch einen kurzen Crossfade ersetzen: Namen nicht vergeben oder Flip mit duration: 0.

Barrierefrei. Nach dem Wechsel den Fokus auf die H1 der Detailseite setzen, bei SPA-Routern ist das Pflicht. Beim Zurück den Fokus auf das Vorschaubild legen, von dem man kam. Alt-Texte von Vorschaubild und Hero gleich halten.

Code

<!DOCTYPE html>
<!--
  Vorschaubild wird zur Hero (Shared-Element-Morph)
  Fundus · Seitenübergänge · https://cecodesign.de/lab/fundus/seitenuebergaenge/vt-shared-element-morph/
  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>Vorschaubild wird zur Hero (Shared-Element-Morph) · 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: 'Zalando Sans'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/zalando-sans:vf@latest/latin-wdth-normal.woff2') format('woff2-variations'); font-weight: 200 900; font-stretch: 75% 125%; font-display: swap; }
@font-face { font-family: 'Fragment Mono'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/fragment-mono@latest/latin-400-normal.woff2') format('woff2'); font-weight: 400; font-display: swap; }

/* Rezept „Kollektion und Ritual“: warmes Off-White, Fast-Schwarz mit Rotstich, Farbe kommt nur aus der Ware */
:root {
  --grund: #f6f3ee; --tinte: #121010; --meta: #6f685f; --linie: #cecdc6; --flaeche: #ebe7e0;
  --morph-kurve: cubic-bezier(0.76, 0, 0.24, 1);   /* power3.inOut, gemessen bei Stas Bondar: Flip.from 1,3 s */
  --text-kurve: cubic-bezier(0.16, 1, 0.3, 1);     /* expo.out für Titelzeilen und Inhalt */
  --morph: 1300ms;                                 /* JS setzt 900ms unter 640 px Breite */
  --weg: 400ms;                                    /* übrige Inhalte blenden vor dem Morph aus */
  --radius: 6px;
  --rand: clamp(16px, 3.4vw, 48px);
}
* { box-sizing: border-box; margin: 0; padding: 0; }
html { background: var(--grund); color: var(--tinte); font-family: 'Zalando Sans', system-ui, sans-serif; font-stretch: 100%; -webkit-font-smoothing: antialiased; }
body { min-height: 100svh; overflow-x: clip; font-size: 15px; line-height: 1.5; }
a { color: inherit; text-decoration: none; }
.mono { font: 400 11px/1.5 'Fragment Mono', ui-monospace, monospace; letter-spacing: .08em; text-transform: uppercase; color: var(--meta); }
.preis, .ziffern { font-variant-numeric: tabular-nums; }
[hidden] { display: none !important; }

/* ---------- Kopfleiste (beiden Seiten gemeinsam) ---------- */
.nav { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 16px; height: 60px; padding: 0 var(--rand); }
.nav > :last-child { justify-self: end; }
.marke { font-size: 13px; font-weight: 500; font-stretch: 125%; letter-spacing: .16em; text-transform: uppercase; }
.nav .mono { color: var(--tinte); }
.zurueck { display: inline-flex; gap: .7em; align-items: center; }
.zurueck span { transition: translate .15s linear; }
.zurueck:hover span { translate: -4px 0; }
.zurueck:focus-visible, .karte a:focus-visible { outline: 1.5px solid var(--tinte); outline-offset: 6px; }

/* ---------- Das wandernde Bild: Kampagnenmotiv 3:4 auf seiner Wand- und Bodenfarbe ---------- */
/* contain + gleiche Wand/Boden-Teilung (82 %) im Hintergrund: im 3:4-Slot randlos, im breiten Hero setzt sich der Raum seitlich fort */
/* Der Raum liegt als Hintergrund auf dem img selbst, damit er beim Innen-Zoom (Flip skaliert nur das img) mitwandert */
.medium { --raum: linear-gradient(to bottom, var(--wand, #ddd) 0 82%, var(--boden, #ccc) 82% 100%);
  display: block; position: relative; width: 100%; height: 100%; overflow: hidden; border-radius: var(--radius); background: var(--raum); }
.medium img { display: block; width: 100%; height: 100%; object-fit: contain; background: var(--raum); transform: scale(1.1); transition: transform .9s var(--text-kurve); }
.held .medium { border-radius: 0; }
.held .medium img { transform: none; transition: none; }
.medium.fliegt img { transition: none !important; }

/* ---------- Übersicht: Lookbook ---------- */
.aufmacher { display: grid; grid-template-columns: minmax(0, 7fr) minmax(0, 4fr); align-items: end; gap: 16px 48px; padding: clamp(20px, 4.5vh, 64px) var(--rand) clamp(20px, 4vh, 44px); }
.aufmacher .mono { margin-bottom: 14px; }
.aufmacher .titel { font-size: clamp(2rem, 5vw, 4.5rem); font-weight: 450; font-stretch: 125%; line-height: 1.05; letter-spacing: -.01em; }
.titel em { font-style: normal; font-weight: 250; }
.titel:focus { outline: none; }
.intro { font-size: 15px; line-height: 1.5; max-width: 36ch; color: var(--meta); text-wrap: pretty; }
.abschnitt { display: flex; justify-content: space-between; margin: 0 var(--rand); padding: 10px 0 14px; border-top: 1px solid var(--linie); }
.raster { list-style: none; display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: clamp(10px, 1.4vw, 20px); padding: 0 var(--rand) 140px; align-items: start; }
.raster > li:nth-child(even) { margin-top: clamp(20px, 5vh, 64px); }
.karte a { display: grid; gap: 10px; }
.karte__slot { display: block; aspect-ratio: 3 / 4; }
.karte__text { display: grid; grid-template-columns: auto 1fr auto; align-items: baseline; gap: 1px 10px; }
.karte__text b { font-weight: 500; font-size: 15px; }
.karte__text .art { grid-column: 2 / 4; display: flex; justify-content: space-between; gap: 8px; }
.karte__text .art .preis { color: var(--tinte); }
.pfeil { opacity: 0; translate: -6px 0; transition: opacity .15s linear, translate .6s var(--text-kurve); }
@media (hover: hover) {
  .karte a:hover .medium img { transform: scale(1.04); }
  .karte a:hover .pfeil { opacity: 1; translate: 0 0; }
}
/* Schritt 2: alles außer dem gewählten Bild tritt ab */
.ueb > *, .karte__text, .raster > li { transition: opacity .6s var(--text-kurve), translate .6s var(--text-kurve); }
.raster > li:nth-child(2) { transition-delay: 40ms; } .raster > li:nth-child(3) { transition-delay: 80ms; } .raster > li:nth-child(4) { transition-delay: 120ms; }
.ueb.fokus > .aufmacher, .ueb.fokus > .abschnitt, .ueb.fokus .raster > li:not(.gewaehlt), .ueb.fokus .gewaehlt .karte__text {
  opacity: 0; translate: 0 18px; transition: opacity var(--weg) var(--morph-kurve), translate var(--weg) var(--morph-kurve); transition-delay: 0ms;
}

/* ---------- Produktdetail ---------- */
.held { position: relative; height: clamp(240px, 54svh, 620px); }
.held figcaption { position: absolute; z-index: 2; left: var(--rand); right: var(--rand); bottom: 14px; display: flex; justify-content: space-between; color: var(--ecke, var(--tinte)); pointer-events: none; }
.kopf { display: grid; grid-template-columns: minmax(0, 1fr) minmax(240px, 360px); gap: 24px 48px; align-items: end; padding: clamp(22px, 4vh, 48px) var(--rand) 0; }
.projekt .titel { margin-top: 12px; font-size: clamp(34px, 5.2vw, 84px); font-weight: 450; font-stretch: 125%; line-height: 1.02; letter-spacing: -.01em; }
.zeile { display: block; overflow: hidden; padding-bottom: .08em; margin-bottom: -.08em; }
.zeile > span { display: block; }
.unter { font-size: clamp(16px, 1.3vw, 19px); line-height: 1.3; margin-bottom: 16px; }
.unter .preis { display: block; margin-top: 4px; font-weight: 500; }
.fakten div { display: grid; grid-template-columns: 7.5em 1fr; gap: 12px; padding: 8px 0; border-top: 1px solid var(--linie); }
.fakten dd { color: var(--tinte); }
.text { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16px 48px; max-width: 980px; padding: clamp(28px, 5vh, 64px) var(--rand) 160px; font-size: 15px; line-height: 1.55; text-wrap: pretty; }
/* Schritt 4: erst nach dem Morph kommen Titel und Inhalt */
.projekt .zeile > span { translate: 0 110%; transition: translate var(--weg) var(--morph-kurve); }
.projekt [data-auf] { opacity: 0; translate: 0 20px; transition: opacity var(--weg) var(--morph-kurve), translate var(--weg) var(--morph-kurve); }
.projekt.zeigen .zeile > span { translate: 0 0; transition: translate .9s var(--text-kurve); }
.projekt.zeigen .zeile:nth-child(2) > span { transition-delay: 100ms; }
.projekt.zeigen [data-auf] { opacity: 1; translate: 0 0; transition: opacity .9s var(--text-kurve) var(--d, 200ms), translate .9s var(--text-kurve) var(--d, 200ms); }

/* ---------- Native View Transition ---------- */
::view-transition-group(hero) { overflow: clip; animation-duration: var(--morph); animation-timing-function: var(--morph-kurve); }
::view-transition-old(hero), ::view-transition-new(hero) { height: 100%; object-fit: cover; animation: none; }
/* Kein Crossfade: Es bleibt der Schnappschuss des Hero sichtbar, er zeigt im Kartenformat dasselbe wie das Vorschaubild */
:root.vt-auf::view-transition-old(hero), :root.vt-zu::view-transition-new(hero) { opacity: 0; }
::view-transition-old(root), ::view-transition-new(root) { animation-duration: var(--weg); animation-timing-function: var(--morph-kurve); }
::view-transition-group(dock) { animation: none; }

/* ---------- Demo-Steuerung ---------- */
.dock { view-transition-name: dock; position: fixed; z-index: 40; left: 50%; bottom: 16px; translate: -50% 0; width: max-content; max-width: calc(100% - 24px);
  display: grid; gap: 8px; padding: 10px 12px; border-radius: 4px; color: var(--grund);
  background: rgba(18, 16, 16, .84); -webkit-backdrop-filter: blur(14px) saturate(1.2); backdrop-filter: blur(14px) saturate(1.2); box-shadow: 0 10px 40px rgba(18, 16, 16, .2); }
.dock__zeile { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
.dock .mono { color: rgba(246, 243, 238, .58); }
.modi { display: flex; gap: 4px; }
.modi button { font: inherit; letter-spacing: inherit; text-transform: inherit; color: rgba(246, 243, 238, .72); background: none; border: 1px solid rgba(246, 243, 238, .18); border-radius: 2px; height: 28px; padding: 0 12px; cursor: pointer; transition: background-color .15s linear, color .15s linear; }
.modi button:hover { color: var(--grund); }
.modi button[aria-pressed='true'] { background: var(--grund); color: var(--tinte); border-color: var(--grund); }
.modi button:focus-visible { outline: 1.5px solid var(--grund); outline-offset: 2px; }
.werte { font-size: 10px; line-height: 1.6; letter-spacing: .04em; text-transform: none; }
.werte > span { display: block; white-space: nowrap; }
.werte b { color: var(--grund); font-weight: 400; }

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

/* Reduzierte Bewegung: kein Morph, nur Crossfade (im JS) */
:root.reduziert * { transition-duration: .01ms !important; transition-delay: 0ms !important; animation-duration: .01ms !important; }
.reduziert .medium img { transform: none; }

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

@media (hover: none) {
  .pfeil { opacity: 1; translate: 0 0; }
  .modi button { height: 34px; }
}
@media (max-width: 900px) {
  .aufmacher, .kopf { grid-template-columns: minmax(0, 1fr); }
  .raster { grid-template-columns: repeat(2, minmax(0, 1fr)); row-gap: 28px; }
  .text { grid-template-columns: minmax(0, 1fr); }
}
@media (max-width: 720px) { .werte > span { white-space: normal; } }
@media (max-width: 560px) {
  .nav { display: flex; justify-content: space-between; }
  .nav__mitte .mono[data-nur='ueb'], [data-nav-rechts].leer { display: none; }
  .held { height: 46svh; }
  .karte__text { column-gap: 8px; }
  .karte__text b { font-size: 14px; }
  .dock__zeile > span.mono, .werte .lang { display: none; }
  .dock { padding: 8px 10px; bottom: 12px; width: calc(100% - 24px); }
  .raster, .text { padding-bottom: 240px; }   /* alles lässt sich über die Steuerung hinaus scrollen */
  .modi { width: 100%; } .modi button { flex: 1; }
}
</style>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.15.0/dist/gsap.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.15.0/dist/Flip.min.js"></script>
</head>
<body>
<header class="nav">
  <span class="marke">Atelier Saum</span>
  <span class="nav__mitte">
    <span class="mono" data-nur="ueb">Herbst / Winter 26</span>
    <a class="zurueck mono" href="#kollektion" data-zurueck data-nur="projekt" hidden><span aria-hidden="true">←</span> Kollektion</a>
  </span>
  <span class="mono" data-nav-rechts>Wien</span>
</header>

<main>
  <section class="ueb" data-seite="ueb" aria-label="Lookbook">
    <div class="aufmacher">
      <div>
        <p class="mono">Kollektion Nr. 7 · Herbst / Winter 2026</p>
        <h1 class="titel" tabindex="-1">Fadenlauf <em>Lookbook</em></h1>
      </div>
      <p class="intro">Mäntel, Strick und Kleider aus Loden, Merino und Flanell. Entworfen und genäht in unserem Atelier in der Neubaugasse, in kleinen Auflagen.</p>
    </div>
    <div class="abschnitt mono"><span>Vier Looks</span><span class="ziffern">01 – 04</span></div>
    <ul class="raster" data-raster></ul>
  </section>

  <section class="projekt" data-seite="projekt" aria-label="Look" hidden>
    <figure class="held" data-held><figcaption class="mono" data-auf style="--d: 320ms" data-p="ecke"></figcaption></figure>
    <div class="kopf">
      <div>
        <p class="mono" data-auf style="--d: 0ms" data-p="kennung"></p>
        <h1 class="titel" tabindex="-1" data-p="titel"></h1>
      </div>
      <div>
        <p class="unter" data-auf style="--d: 250ms" data-p="unter"></p>
        <dl class="fakten mono" data-auf style="--d: 320ms" data-p="fakten"></dl>
      </div>
    </div>
    <div class="text" data-auf style="--d: 420ms" data-p="text"></div>
  </section>
</main>

<aside class="dock" aria-label="Demo-Steuerung">
  <div class="dock__zeile">
    <span class="mono">Technik</span>
    <div class="modi mono" role="group" aria-label="Technik des Morphs">
      <button type="button" data-variante="vt" aria-pressed="true">View Transition</button>
      <button type="button" data-variante="flip" aria-pressed="false">GSAP Flip</button>
    </div>
  </div>
  <p class="werte mono" data-werte></p>
</aside>
<div class="zeiger" aria-hidden="true"></div>

<script>
(() => {
  const html = document.documentElement;
  const reduziert = () => html.classList.contains('reduziert');
  const $ = s => document.querySelector(s);
  const ueb = $('[data-seite="ueb"]'), projekt = $('[data-seite="projekt"]'), raster = $('[data-raster]');
  const held = $('[data-held]'), zurueck = $('[data-zurueck]'), navRechts = $('[data-nav-rechts]');
  const werteFeld = $('[data-werte]'), zeiger = $('.zeiger'), main = $('main');
  const hatVT = typeof document.startViewTransition === 'function';
  const hatFlip = !!(window.gsap && window.Flip);
  if (hatFlip) gsap.registerPlugin(Flip);

  /* Werte: Morph 1,3 s power3.inOut (Stas Bondar '25, beobachtet), Titel +100 ms je Zeile */
  const KURVE = 'cubic-bezier(0.76, 0, 0.24, 1)';   // = power3.inOut
  const morphDauer = () => innerWidth < 640 ? 900 : 1300;   // kleiner Weg auf dem Telefon
  const WEG = 400;            // übrige Inhalte blenden aus, bevor das Bild fliegt
  const TITEL_START = 0.7;    // Titel beginnt bei 70 % des Morphs
  const SCALE = 1.1;          // Bild im Vorschaubild intern 1.1, im Hero 1.0
  const RADIUS = 6;           // Kartenradius → 0 im Hero
  const FADE = 200;           // reduzierte Bewegung: Crossfade

  /* ---------- Kampagnenmotive: eigene SVG-Illustrationen, 600 × 800, Boden ab 82 % (y 656) ---------- */
  const kopf = (haut, haar, vorne = '') => `${haar}<path d="M289 172h22v48h-22z" fill="${haut}"/><path d="M289 184q11 12 22 0v10q-11 9-22 0z" fill="#000" opacity=".14"/><ellipse cx="300" cy="146" rx="28" ry="35" fill="${haut}"/>${vorne}`;
  const raum = (wand, boden, licht, mitte) => `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 600 800"><defs>
      <radialGradient id="l" cx="300" cy="250" r="290" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#fff" stop-opacity="${licht}"/><stop offset="1" stop-color="#fff" stop-opacity="0"/></radialGradient>
      <radialGradient id="s"><stop offset="0" stop-color="#000" stop-opacity=".26"/><stop offset="1" stop-color="#000" stop-opacity="0"/></radialGradient>
      <linearGradient id="v" x1="0" x2="1"><stop offset="0" stop-color="#fff" stop-opacity=".14"/><stop offset=".55" stop-color="#fff" stop-opacity="0"/><stop offset="1" stop-color="#000" stop-opacity=".2"/></linearGradient>
      <pattern id="r" width="7" height="10" patternUnits="userSpaceOnUse"><rect width="2.6" height="10" fill="#000" opacity=".07"/></pattern>
    </defs><rect width="600" height="656" fill="${wand}"/><rect y="656" width="600" height="144" fill="${boden}"/><rect width="600" height="656" fill="url(#l)"/>${mitte}</svg>`;
  const schatten = (rx = 132) => `<ellipse cx="300" cy="710" rx="${rx}" ry="16" fill="url(#s)"/>`;
  const falte = (d, o = .16, w = 2) => `<path d="${d}" fill="none" stroke="#000" stroke-opacity="${o}" stroke-width="${w}" stroke-linecap="round"/>`;

  function motiv(n) {
    if (n === 0) { // Wickelmantel aus Loden, Moosgrün, vor eisblauer Wand mit Türrahmen
      const m = '#46523a', d = '#2f3826';
      const koerper = 'M262 216Q226 224 220 262L198 604Q197 616 210 616L390 616Q403 616 402 604L380 262Q374 224 338 216L300 232Z';
      return raum('#c8d5d6', '#aebdbd', .34, `
        <rect x="468" y="0" width="12" height="656" fill="#000" opacity=".05"/><rect x="456" y="0" width="3" height="656" fill="#000" opacity=".05"/>
        ${schatten()}
        <circle cx="300" cy="106" r="17" fill="#2a1d17"/>
        ${kopf('#e2bfa4', '', '<path d="M272 146q-4-46 29-48q31 0 27 46q-7-27-28-29q-22 1-28 31z" fill="#2a1d17"/>')}
        <path d="M274 616l-3 86h22l3-86zM304 616l3 86h22l-3-86z" fill="#23261e"/><rect x="264" y="698" width="32" height="13" rx="6" fill="#141412"/><rect x="304" y="698" width="32" height="13" rx="6" fill="#141412"/>
        <path d="${koerper}" fill="${m}"/><path d="${koerper}" fill="url(#v)"/>
        ${falte('M236 292L226 498')}${falte('M364 292L374 498')}${falte('M222 498h30', .22)}${falte('M348 498h30', .22)}
        <path d="M276 220L300 340L324 220Q312 212 300 212Q288 212 276 220Z" fill="#1c1e18"/>
        <path d="M262 216Q248 230 254 252L300 374V342L276 220Z" fill="${d}"/><path d="M338 216Q352 230 346 252L300 374V342L324 220Z" fill="${d}"/>
        ${falte('M300 374Q314 500 336 616', .26)}
        ${falte('M248 432Q244 520 238 610', .12)}${falte('M362 432Q364 520 370 612', .12)}${falte('M284 444L280 612', .1)}
        ${falte('M262 404Q280 380 300 400', .14)}${falte('M300 420Q318 446 344 430', .14)}
        <rect x="210" y="404" width="180" height="16" fill="${d}"/><rect x="318" y="401" width="22" height="22" rx="4" fill="#28301f"/>
        <path d="M324 421L313 502L323 504L334 423ZM334 421L351 494L360 491L343 421Z" fill="${d}"/>`);
    }
    if (n === 1) { // Rippstrickkleid aus Merino, Wollweiß, vor petrolfarbener Nische
      const m = '#ece4d6', haut = '#8a5a3f';
      const kleid = 'M300 212Q266 212 252 226Q240 234 240 256L248 330Q254 370 262 394Q240 470 210 612L390 612Q360 470 338 394Q346 370 352 330L360 256Q360 234 348 226Q334 212 300 212Z';
      const arme = 'M247 232Q230 242 228 268L222 470L241 470L254 296ZM353 232Q370 242 372 268L378 470L359 470L346 296Z';
      const haar = [[276,126,16],[290,112,18],[310,110,18],[326,124,16],[271,146,12],[330,144,12],[300,104,14]].map(([x,y,r]) => `<circle cx="${x}" cy="${y}" r="${r}" fill="#15100e"/>`).join('');
      return raum('#1f3a3d', '#152a2c', .12, `
        <path d="M176 656V312A124 124 0 0 1 424 312V656Z" fill="#fff" opacity=".06"/>
        ${schatten(120)}
        ${kopf(haut, '')}${haar}
        <path d="M276 612l-2 90q0 8-8 10h32l-2-100zM304 612v100h32q-8-2-8-10l-2-90z" fill="#3a2419"/>
        <path d="${kleid}" fill="${m}"/><path d="${kleid}" fill="url(#r)"/><path d="${kleid}" fill="url(#v)"/>
        <rect x="285" y="180" width="30" height="40" rx="8" fill="#e0d6c5"/><path d="M285 192h30M285 202h30" stroke="#000" stroke-opacity=".08" stroke-width="2"/>
        <path d="${arme}" fill="#e3dacb"/><path d="${arme}" fill="url(#r)"/>
        <ellipse cx="231" cy="482" rx="9" ry="14" fill="${haut}"/><ellipse cx="369" cy="482" rx="9" ry="14" fill="${haut}"/>
        ${falte('M286 404L262 612', .1)}${falte('M314 404L338 612', .1)}${falte('M300 410V612', .06)}
        ${falte('M262 394Q300 404 338 394', .12)}`);
    }
    if (n === 2) { // Plisseerock in Kobalt und Popelinhemd, vor rosa Wand mit heller Scheibe
      let falten = '';
      const N = 16;
      for (let i = 0; i < N; i++) {
        const w0 = 254 + i * 92 / N, w1 = 254 + (i + 1) * 92 / N, h0 = 186 + i * 228 / N, h1 = 186 + (i + 1) * 228 / N;
        falten += `<path d="M${w0} 374L${w1} 374L${h1} ${i % 2 ? 646 : 640}L${h0} ${i % 2 ? 640 : 646}Z" fill="${i % 2 ? '#22398f' : '#2d47ad'}"/>`;
      }
      const hemd = 'M300 214Q268 216 246 226Q226 234 222 258L212 330L236 336L240 300L240 384L360 384L360 300L364 336L388 330L378 258Q374 234 354 226Q332 216 300 214Z';
      return raum('#ecc9cc', '#dcb1b5', .3, `
        <circle cx="300" cy="300" r="186" fill="#fff" opacity=".18"/>
        ${schatten(140)}
        ${kopf('#f0d0bb', '<path d="M266 150q-4-50 34-52q38 2 34 52l2 34q-12 4-16-12q-2-38-20-40q-18 2-20 40q-4 16-16 12z" fill="#a4552c"/>', '<path d="M272 140q4-36 28-38q24 2 28 38q-12-22-28-22q-16 0-28 22z" fill="#a4552c"/>')}
        <path d="M280 640h14v60h-14zM306 640h14v60h-14z" fill="#f0d0bb"/><rect x="270" y="698" width="28" height="12" rx="5" fill="#161212"/><rect x="302" y="698" width="28" height="12" rx="5" fill="#161212"/>
        <path d="M214 330l22 6l-4 84h-16zM386 330l-22 6l4 84h16z" fill="#f0d0bb"/><ellipse cx="224" cy="430" rx="9" ry="13" fill="#f0d0bb"/><ellipse cx="376" cy="430" rx="9" ry="13" fill="#f0d0bb"/>
        <path d="${hemd}" fill="#f6f4ef"/><path d="${hemd}" fill="url(#v)"/>
        ${falte('M212 330l24 6', .12)}${falte('M388 330l-24 6', .12)}${falte('M300 246V368', .14)}
        ${[272, 300, 328, 356].map(y => `<circle cx="300" cy="${y}" r="2.2" fill="#c9c4bb"/>`).join('')}
        <path d="M286 214L300 236L314 214L328 226L300 248L272 226Z" fill="#fff"/>${falte('M272 226L300 248L328 226', .12, 1.5)}
        ${falten}<path d="M186 646L414 646" stroke="#000" stroke-opacity=".08" stroke-width="2"/>
        <rect x="252" y="366" width="96" height="16" fill="#22398f"/>`);
    }
    // Hosenanzug aus Wollflanell, Aubergine, vor lavendelgrauer Wand mit Bugholzstuhl
    const m = '#4f2d3f', d = '#3a2130', holz = '#2b1a14';
    const sakko = 'M300 214Q266 216 236 226Q222 232 222 254L218 474L382 474L378 254Q378 232 364 226Q334 216 300 214Z';
    const stuhl = (p, w = 7) => `<path d="${p}" fill="none" stroke="${holz}" stroke-width="${w}" stroke-linecap="round" stroke-linejoin="round"/>`;
    return raum('#cbc4d4', '#b4abc1', .3, `
      <ellipse cx="472" cy="708" rx="62" ry="9" fill="url(#s)"/>
      ${stuhl('M436 560Q428 470 446 420Q470 382 494 420Q512 470 504 560')}${stuhl('M452 474Q470 444 488 474Q490 520 470 540Q450 520 452 474', 5)}
      ${stuhl('M432 562L424 704M508 562L516 704M452 568L456 700M490 568L486 700', 6)}${stuhl('M430 640Q470 656 512 640', 5)}
      <ellipse cx="470" cy="560" rx="46" ry="12" fill="${holz}"/>
      ${schatten(128)}
      <path d="M318 148q24 22 14 84q-6 12-14 0q8-44-6-70z" fill="#1a1314"/>
      ${kopf('#c48e6a', '', '<path d="M272 148q-4-42 28-44q32 2 28 44q-8-26-28-28q-20 2-28 28z" fill="#1a1314"/>')}
      <path d="M244 470L230 712H296L300 524L304 712H370L356 470Z" fill="${m}"/><path d="M244 470L230 712H296L300 524L304 712H370L356 470Z" fill="url(#v)"/>
      ${falte('M268 484L262 712', .16)}${falte('M332 484L338 712', .16)}
      <ellipse cx="262" cy="714" rx="22" ry="5" fill="#141012"/><ellipse cx="338" cy="714" rx="22" ry="5" fill="#141012"/>
      <ellipse cx="231" cy="488" rx="9" ry="13" fill="#c48e6a"/><ellipse cx="369" cy="488" rx="9" ry="13" fill="#c48e6a"/>
      <path d="${sakko}" fill="${m}"/><path d="${sakko}" fill="url(#v)"/>
      ${falte('M242 262L238 472', .2)}${falte('M358 262L362 472', .2)}${falte('M220 474h22', .2)}${falte('M358 474h22', .2)}
      <path d="M282 216L300 300L318 216Q309 210 300 210Q291 210 282 216Z" fill="#efeaf2"/>
      <path d="M282 216L300 300L312 334L262 252L248 238L270 240L268 222Z" fill="${d}"/><path d="M318 216L300 300L288 334L338 252L352 238L330 240L332 222Z" fill="${d}"/>
      ${falte('M312 334Q308 404 292 474', .26)}
      ${[[282, 364], [322, 364], [282, 414], [322, 414]].map(([x, y]) => `<circle cx="${x}" cy="${y}" r="4.2" fill="#24121c"/>`).join('')}`);
  }
  const bildUrl = n => 'data:image/svg+xml,' + encodeURIComponent(motiv(n));

  /* ---------- Inhalte: Atelier Saum, Wien, Kollektion „Fadenlauf“ ---------- */
  const PROJEKTE = [
    { name: 'Wickelmantel Ilse', art: 'Mantel', stoff: 'Loden', preis: '690 €', wand: '#c8d5d6', boden: '#aebdbd', ecke: '#121010', farbname: 'Moosgrün',
      zeilen: ['Wickelmantel', '<em>Ilse</em>'], formel: 'Loden aus Tirol, 100 % Schurwolle',
      fakten: [['Farbe', 'Moosgrün'], ['Größen', '34 36 38 40 42 44'], ['Fertigung', 'Atelier Wien, Neubau']],
      text: ['Der Mantel hat keinen einzigen Knopf. Er wird gewickelt und mit einem breiten Gürtel gehalten. So passt er über dicken Strick genauso wie über ein leichtes Kleid.', 'Der Loden kommt aus einer Weberei in Tirol und wird gewalkt, bis er Wind und leichten Regen abhält. Wir schneiden jeden Mantel einzeln zu. Die Kanten bleiben offen und fransen trotzdem nicht.'] },
    { name: 'Strickkleid Grete', art: 'Kleid', stoff: 'Merino', preis: '340 €', wand: '#1f3a3d', boden: '#152a2c', ecke: '#f6f3ee', farbname: 'Wollweiß',
      zeilen: ['Strickkleid', '<em>Grete</em>'], formel: 'Rippstrick, 100 % Merino extrafein',
      fakten: [['Farbe', 'Wollweiß'], ['Größen', 'XS S M L XL'], ['Fertigung', 'Strickerei im Waldviertel']],
      text: ['Ein Rippstrick, der oben eng anliegt und ab der Hüfte weit fällt. Der Rollkragen ist doppelt gelegt und lässt sich umschlagen.', 'Gestrickt wird in einer kleinen Strickerei im Waldviertel, aus extrafeinem Merino, das nicht kratzt. Das Kleid verträgt die Wäsche bei 30 Grad im Wollprogramm.'] },
    { name: 'Plisseerock Hedi', art: 'Rock', stoff: 'Plissee', preis: '280 €', wand: '#ecc9cc', boden: '#dcb1b5', ecke: '#121010', farbname: 'Kobalt',
      zeilen: ['Plisseerock', '<em>Hedi</em>'], formel: 'Plissee, 100 % recyceltes Polyester',
      fakten: [['Farbe', 'Kobalt'], ['Größen', '34 36 38 40 42'], ['Fertigung', 'Atelier Wien, Neubau']],
      text: ['Hundertzwanzig Falten, die beim Gehen aufgehen und im Stehen wieder schließen. Der Rock hat einen breiten Bund und eine verdeckte Tasche in der Seitennaht.', 'Das Hemd aus fester Baumwollpopeline ist bewusst zu weit. Vorne steckt man es ein, hinten fällt es lose. Beide Teile gibt es auch einzeln.'] },
    { name: 'Hosenanzug Ottilie', art: 'Anzug', stoff: 'Flanell', preis: '520 €', wand: '#cbc4d4', boden: '#b4abc1', ecke: '#121010', farbname: 'Aubergine',
      zeilen: ['Hosenanzug', '<em>Ottilie</em>'], formel: 'Flanell, 98 % Schurwolle, 2 % Elasthan',
      fakten: [['Farbe', 'Aubergine'], ['Größen', '34 36 38 40 42 44'], ['Fertigung', 'Atelier Wien, Neubau']],
      text: ['Ein Zweireiher mit hohen Revers und eine Hose, die bis zum Boden reicht. Die Schultern sind weich und kommen ohne Polster aus.', 'Der Flanell ist leicht angeraut und knittert kaum. Sakko und Hose kürzen wir im Atelier auf Ihre Länge, ohne Aufpreis.'] }
  ];
  const nr2 = i => String(i + 1).padStart(2, '0');
  const farbe = (el, p) => { el.style.setProperty('--wand', p.wand); el.style.setProperty('--boden', p.boden); };
  raster.innerHTML = PROJEKTE.map((p, i) => `
    <li class="karte"><a href="#look-${nr2(i)}" data-projekt="${i}">
      <span class="karte__slot"><span class="medium" style="--wand: ${p.wand}; --boden: ${p.boden}"><img src="${bildUrl(i)}" alt="Look ${nr2(i)}: ${p.name} in ${p.farbname}, Illustration" width="600" height="800"></span></span>
      <span class="karte__text"><span class="mono ziffern">${nr2(i)}</span><b>${p.name}</b><span class="pfeil" aria-hidden="true">→</span><span class="mono art"><span>${p.stoff}</span><span class="preis">${p.preis}</span></span></span>
    </a></li>`).join('');
  const karten = [...raster.querySelectorAll('.karte')];
  const slot = i => karten[i].querySelector('.karte__slot');
  const medien = karten.map(k => k.querySelector('.medium'));
  const feld = n => projekt.querySelector(`[data-p="${n}"]`);

  function fuelle(i) {
    const p = PROJEKTE[i];
    feld('kennung').textContent = `Look ${nr2(i)} · ${p.art}`;
    feld('titel').innerHTML = p.zeilen.map(z => `<span class="zeile"><span>${z}</span></span>`).join('');
    feld('titel').setAttribute('aria-label', p.name);
    feld('unter').innerHTML = `${p.formel}<span class="preis">${p.preis}</span>`;
    feld('fakten').innerHTML = p.fakten.map(([a, b]) => `<div><dt>${a}</dt><dd class="ziffern">${b}</dd></div>`).join('');
    feld('text').innerHTML = p.text.map(t => `<p>${t}</p>`).join('');
    feld('ecke').innerHTML = `<span>${p.name.split(' ')[1]} · ${p.farbname}</span><span class="ziffern">${nr2(i)} / ${nr2(PROJEKTE.length - 1)}</span>`;
    held.style.setProperty('--ecke', p.ecke);
    projekt.setAttribute('aria-label', p.name);
  }
  // Seite wechseln (in der echten Website: Navigation zur Produktseite)
  function wechsle(ziel, i) {
    const aufProjekt = ziel === 'projekt';
    ueb.hidden = aufProjekt; projekt.hidden = !aufProjekt;
    document.querySelectorAll('[data-nur]').forEach(el => { el.hidden = el.dataset.nur !== ziel; });
    navRechts.textContent = aufProjekt ? `Tasche (0)` : 'Wien';
    navRechts.classList.toggle('leer', aufProjekt);   // Telefon: rechts steht dann „Kollektion“
  }

  /* ---------- Varianten und Werte-Anzeige ---------- */
  let variante = 'vt';
  function zeigeWerte() {
    const d = morphDauer();
    html.style.setProperty('--morph', d + 'ms');
    if (reduziert()) { werteFeld.innerHTML = `<span><b>reduziert</b> kein Morph, Seiten wechseln per Crossfade · opacity 0 → 1 · ${FADE} ms linear</span>`; return; }
    const kopfzeile = variante === 'vt'
      ? (hatVT ? `<span><b>view-transition-name: hero</b> · document.startViewTransition() · ::view-transition-group(hero) ${d} ms</span>`
               : '<span><b>Browser ohne View Transitions</b> · Ersatz per GSAP Flip</span>')
      : `<span><b>Flip.getState → appendChild → Flip.from</b> · duration ${d / 1000} · ease power3.inOut · absolute: true</span>`;
    werteFeld.innerHTML = kopfzeile
      + `<span><b>${KURVE}</b> · Bild scale ${SCALE} → 1 · Radius ${RADIUS} → 0 px</span>`
      + `<span class="lang">Rest blendet ${WEG} ms aus · Titel ab ${TITEL_START * 100} % des Morphs, 900 ms expo.out, +100 ms je Zeile</span>`;
  }
  function setzeVariante(name) {
    variante = name;
    document.querySelectorAll('[data-variante]').forEach(b => b.setAttribute('aria-pressed', String(b.dataset.variante === name)));
    zeigeWerte();
  }

  /* ---------- Der Übergang ---------- */
  let offen = -1, laeuft = false, gen = 0, vtAktiv = null, merkScroll = 0;
  const schlaf = ms => new Promise(r => setTimeout(r, ms));
  const nimmVT = () => variante === 'vt' && hatVT || !hatFlip && hatVT;

  // Native View Transition: dasselbe Element trägt vorher und nachher view-transition-name: hero
  async function perVT(medium, umbau, richtung) {
    const d = morphDauer();
    medium.style.viewTransitionName = 'hero';
    html.classList.add(richtung === 'auf' ? 'vt-auf' : 'vt-zu');
    const vt = document.startViewTransition(umbau);
    vtAktiv = vt;
    vt.ready.then(() => {
      const opt = { duration: d, easing: KURVE, fill: 'both' };
      const auf = richtung === 'auf';
      html.animate({ borderRadius: auf ? [RADIUS + 'px', '0px'] : ['0px', RADIUS + 'px'] }, { ...opt, pseudoElement: '::view-transition-group(hero)' });
      // Innen-Zoom: Hero kommt von 1.1 auf 1, beim Zurück geht das Hero-Bild auf 1.1
      if (auf) html.animate({ transform: [`scale(${SCALE})`, 'scale(1)'] }, { ...opt, pseudoElement: '::view-transition-new(hero)' });
      else html.animate({ transform: ['scale(1)', `scale(${SCALE})`] }, { ...opt, pseudoElement: '::view-transition-old(hero)' });
    }).catch(() => {});
    await vt.finished.catch(() => {});
    if (vtAktiv === vt) vtAktiv = null;
    html.classList.remove('vt-auf', 'vt-zu');
    medium.style.viewTransitionName = '';
  }
  // GSAP Flip: Zustand merken, Element umhängen, von alt nach neu animieren
  async function perFlip(medium, umbau, richtung) {
    const d = morphDauer() / 1000;
    const img = medium.querySelector('img');
    const state = Flip.getState(medium, { props: 'borderRadius' });
    medium.classList.add('fliegt');
    umbau();
    const auf = richtung === 'auf';
    const tl = gsap.timeline();
    tl.add(Flip.from(state, { duration: d, ease: 'power3.inOut', absolute: true, zIndex: 30, props: 'borderRadius' }), 0);
    tl.fromTo(img, { scale: auf ? SCALE : 1 }, { scale: auf ? 1 : SCALE, duration: d, ease: 'power3.inOut' }, 0);
    await tl.then();
    gsap.set(img, { clearProps: 'transform' });
    medium.classList.remove('fliegt');
  }
  function morph(medium, umbau, richtung) {
    if (nimmVT()) return perVT(medium, umbau, richtung);
    if (hatFlip) return perFlip(medium, umbau, richtung);
    umbau(); return Promise.resolve();
  }

  async function oeffnen(i, mitFokus) {
    if (laeuft || offen >= 0) return;
    laeuft = true; const g = ++gen;
    merkScroll = scrollY;
    fuelle(i);
    const medium = medien[i];
    const umbau = () => { wechsle('projekt', i); held.append(medium); scrollTo(0, 0); };
    zeigeWerte();
    if (reduziert()) {
      umbau(); projekt.classList.add('zeigen');
      main.animate({ opacity: [0, 1] }, { duration: FADE, easing: 'linear' });
    } else {
      // 1) Rest tritt ab  2) Bild morpht ins Hero  3) Titel und Inhalt folgen
      karten[i].classList.add('gewaehlt'); ueb.classList.add('fokus');
      await schlaf(WEG); if (g !== gen) return;
      const titel = setTimeout(() => { if (g === gen) projekt.classList.add('zeigen'); }, morphDauer() * TITEL_START);
      await morph(medium, umbau, 'auf'); if (g !== gen) { clearTimeout(titel); return; }
    }
    offen = i; laeuft = false;
    if (mitFokus) feld('titel').focus({ preventScroll: true });
  }

  async function schliessen(mitFokus) {
    if (laeuft || offen < 0) return;
    laeuft = true; const g = ++gen;
    const i = offen, medium = medien[i];
    const umbau = () => { wechsle('ueb', i); slot(i).append(medium); scrollTo(0, merkScroll); };
    if (reduziert()) {
      projekt.classList.remove('zeigen'); umbau(); ueb.classList.remove('fokus'); karten[i].classList.remove('gewaehlt');
      main.animate({ opacity: [0, 1] }, { duration: FADE, easing: 'linear' });
    } else {
      // spiegelbildlich: Inhalt weg, Bild zurück ins Raster, dann kommt der Rest
      projekt.classList.remove('zeigen');
      await schlaf(WEG); if (g !== gen) return;
      await morph(medium, umbau, 'zu'); if (g !== gen) return;
      ueb.classList.remove('fokus'); karten[i].classList.remove('gewaehlt');
    }
    offen = -1; laeuft = false;
    if (mitFokus) karten[i].querySelector('a').focus({ preventScroll: true });
  }

  // Sofort in den Ausgangszustand (für Neustart und Vorführung)
  function zuruecksetzen() {
    gen++;
    if (vtAktiv) { try { vtAktiv.skipTransition(); } catch (e) {} vtAktiv = null; }
    if (hatFlip) medien.forEach(m => { gsap.killTweensOf(m); gsap.killTweensOf(m.querySelector('img')); gsap.set([m, m.querySelector('img')], { clearProps: 'all' }); });
    medien.forEach((m, i) => { m.classList.remove('fliegt'); m.style.viewTransitionName = ''; farbe(m, PROJEKTE[i]); slot(i).append(m); });
    projekt.classList.remove('zeigen'); ueb.classList.remove('fokus');
    karten.forEach(k => k.classList.remove('gewaehlt'));
    wechsle('ueb', 0); scrollTo(0, 0);
    offen = -1; laeuft = false;
  }

  /* ---------- Bedienung ---------- */
  raster.addEventListener('click', e => {
    const a = e.target.closest('[data-projekt]'); if (!a) return;
    e.preventDefault(); stoppeVorfuehrung();
    oeffnen(+a.dataset.projekt, true);
  });
  zurueck.addEventListener('click', e => { e.preventDefault(); stoppeVorfuehrung(); schliessen(true); });
  addEventListener('keydown', e => { if (e.key === 'Escape' && offen >= 0) { stoppeVorfuehrung(); schliessen(true); } });
  document.querySelectorAll('[data-variante]').forEach(b => b.addEventListener('click', () => { stoppeVorfuehrung(); setzeVariante(b.dataset.variante); }));
  addEventListener('resize', zeigeWerte);

  /* ---------- Vorführung: gedachter Zeiger öffnet und schließt die Looks ---------- */
  let runde = 0, uhr = null, lief = false, schritt = 0, nr = 0, zx = 0, zy = 0;
  const warte = (ms, t) => new Promise(r => { uhr = setTimeout(() => r(t === runde), ms); });
  function zeigerZu(x, y, t) {
    zeiger.classList.add('an');
    zeiger.animate({ transform: [`translate(${zx}px, ${zy}px)`, `translate(${x}px, ${y}px)`] }, { duration: reduziert() ? 1 : 700, easing: 'cubic-bezier(0.16, 1, 0.3, 1)', fill: 'forwards' });
    zx = x; zy = y;
    return warte(reduziert() ? 200 : 760, t);
  }
  const klickPuls = () => zeiger.animate({ scale: [1, 0.65, 1] }, { duration: 260, easing: 'cubic-bezier(0.16, 1, 0.3, 1)' });
  async function spiel(t) {
    if (!(await warte(250, t))) return;
    while (t === runde) {
      if (offen < 0) {
        const r = slot(nr).getBoundingClientRect();
        if (!(await zeigerZu(Math.round(r.left + r.width * 0.55), Math.round(r.top + r.height * 0.5), t))) return;
        klickPuls(); setzeVariante(schritt % 2 ? 'flip' : 'vt');
        if (!(await warte(160, t))) return;
        zeiger.classList.remove('an');
        await oeffnen(nr, false); if (t !== runde) return;
        if (!(await warte(1800, t))) return;
      } else {
        const r = zurueck.getBoundingClientRect();
        if (!(await zeigerZu(Math.round(r.left + r.width * 0.6), Math.round(r.top + r.height * 0.5), t))) return;
        klickPuls();
        if (!(await warte(160, t))) return;
        zeiger.classList.remove('an');
        await schliessen(false); if (t !== runde) return;
        nr = (nr + 1) % PROJEKTE.length; schritt++;
        if (!(await warte(700, t))) return;
      }
    }
  }
  function stoppeVorfuehrung() { runde++; clearTimeout(uhr); lief = false; zeiger.classList.remove('an'); }

  function neustart() {
    stoppeVorfuehrung(); zuruecksetzen();
    const t = runde;
    setTimeout(() => { if (t === runde) oeffnen(0, false); }, 300);
  }
  function vorfuehren() {
    stoppeVorfuehrung(); zuruecksetzen();
    schritt = 0; nr = 0; lief = true;
    zx = Math.round(innerWidth * 0.66); zy = Math.round(innerHeight * 0.32);
    zeiger.animate({ transform: [`translate(${zx}px, ${zy}px)`] }, { duration: 1, fill: 'forwards' });
    zeiger.classList.add('an');
    spiel(runde);
  }
  function pause() { const war = lief; stoppeVorfuehrung(); lief = war; }
  function weiter() { if (lief && !laeuft) { const t = ++runde; spiel(t); } else if (lief) { const t = ++runde; const warten = () => laeuft ? setTimeout(warten, 100) : (t === runde && spiel(t)); warten(); } }

  setzeVariante('vt');

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