Raster-zu-Vollbild mit geteiltem Element (Lightbox 2026)
Ein Projektbild wächst aus dem ruhigen Raster in die Detailansicht, die Nachbarn treten nach Abstand gestaffelt zurück; Schließen spielt alles zurück zur Kachel.
Architektur, Studios, Freelancer, Manufaktur: viele gleichrangige Projekte im Raster, Detail mit Daten und Text. Unterumschalter Übergang (Flip, View Transition, Kopien) und Ziel (Vollbild, Panel).
Ein Klick auf ein Rasterbild merkt sich Position und Größe. Dann wandert dasselbe Bild in die Detailebene und wächst dorthin (GSAP Flip oder View Transition). Die übrigen Kacheln schrumpfen und blenden aus, je weiter weg, desto später. Erst danach erscheinen Titel und Text per Masken-Reveal. Schließen (Esc, Knopf, Zurück) spielt die Bewegung zurück zur Kachel, die Nachbarn kehren gestaffelt zurück. Die URL kann pro Bild mitwechseln.
Objektpermanenz: Das Auge verliert das Bild nie, es gibt keinen Kontextbruch wie beim klassischen Modal. Der Versatz nach Abstand lässt das Raster wie eine zusammenhängende Fläche reagieren.
Wann einsetzen
Jede Bildergalerie mit Detailansicht: Portfolio, Architektur, Fotografie, Produktbilder im Shop, Projektliste zu Projektseite.
Wann nicht
Bei sehr großen Bildmengen mit Endlos-Laden ohne stabile Positionen. Wenn die Detailseite ein eigenes Dokument ohne gemeinsamen Zustand ist, Cross-Document View Transitions prüfen.
Varianten
- GSAP Flip: Flip.getState, DOM ändern, Flip.from mit absolute und zIndex
- Native View Transition: gleicher view-transition-name auf Kachel- und Großbild, nur während des Übergangs gesetzt
- Bildkopien: 6 Kopien wandern gestaffelt mit clip-path vom Raster zum Ziel
- Panel an der dem Klick gegenüberliegenden Seite statt Vollbild
- Aus einem 3D-Karussell heraus: Karussell dreht weg, Detailraster blendet ein
- Produktgalerie: Hauptbild mit Daumenleiste, Klick öffnet das Vollbild mit Daumen, Schließen bringt das zuletzt gesehene Bild zurück ins Hauptbild
- Serie als Spalte (Lookbook): Leuchtkasten auf dunklem Grund, Blättern nur mit Deckkraft, beim Schließen springt die Seite zur aktuellen Stelle und das Bild fliegt dorthin zurück
- Zoom im Vollbild: Doppeltippen oder Pinch bis Faktor 3, Ziehen verschiebt, Knöpfe + und −, Pfeiltasten in 10-%-Schritten; am Desktop zusätzlich eine runde Lupe am Produktbild (nur hover: hover und pointer: fine, Faktor 2,5); das große Bild lädt erst beim Öffnen. Ersetzt das geplante Muster produktbild-lupe
Umsetzung
Flip: state = Flip.getState(bild); Bild in die Detailebene verschieben; Flip.from(state, { duration, ease, absolute: true, zIndex }). Elemente ggf. per data-flip-id zuordnen, box-sizing border-box, Transforms nur über GSAP setzen. Der Rahmen der Kachel behält sein Seitenverhältnis, damit das Raster nicht springt. View Transitions: document.startViewTransition(update); den view-transition-name erst kurz vor dem Start setzen und danach entfernen. Bei Wechsel des Seitenverhältnisses ::view-transition-old mit object-fit contain und ::view-transition-new mit cover und height 100%. view-transition-class und Types: Chrome/Edge 125, Firefox 144, Safari 18/18.2. Fallstricke: Namen müssen eindeutig sein, sonst entfällt der Übergang; nur eine View Transition gleichzeitig; Flip kann keine 3D-Transforms. In der Demo ist der Seitenwechsel als Zustand simuliert; auf einer echten Seite wechselt die URL per History API oder als Cross-Document View Transition.
Robust umsetzen
Telefon. Vollbild statt Panel, Bild in voller Breite. Nach unten wischen schließt (Schwelle etwa 15 % der Höhe).
Ohne Bewegung. Überblendung von 200 bis 500 ms ohne Positionsanimation (Codrops 3D Carousel: 0,5 s power1.inOut). View Transitions per Media Query auf Dauer 0 oder reinen Fade setzen.
Barrierefrei. Detailansicht als Dialog mit Fokusfalle. Fokus beim Öffnen auf Schließen oder die Überschrift, beim Schließen zurück auf die auslösende Kachel. Esc schließt, Pfeiltasten blättern. Alt-Texte bleiben am Großbild erhalten.
Code
<!DOCTYPE html>
<!--
Raster-zu-Vollbild mit geteiltem Element (Lightbox 2026)
Fundus · Galerien & Slider · https://cecodesign.de/lab/fundus/galerien-slider/galerie-shared-element-vollbild/
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>Raster-zu-Vollbild mit geteiltem Element · 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: '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; }
@font-face { font-family: 'Instrument Serif'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/instrument-serif@latest/latin-400-normal.woff2') format('woff2'); font-weight: 400; font-display: swap; }
@font-face { font-family: 'Instrument Sans'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/instrument-sans:vf@latest/latin-wght-normal.woff2') format('woff2-variations'); font-weight: 400 700; font-display: swap; }
@font-face { font-family: 'Young Serif'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/young-serif@latest/latin-400-normal.woff2') format('woff2'); font-weight: 400; 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; }
/* Kofler Ganster Architekten, Graz – Rezept „Architektur: ruhiges Raster“:
eine Familie in zwei Gewichten, Weiß und kühles Hellgrau, die Bilder liefern die Farbe. */
:root {
--grund: #ffffff; --flaeche: #e1e3e3; --text: #231f20; --leise: #76767a; --hauch: #c5c7c7; --dunkel: #1f2323;
--fokus: #0022ff;
--bewegung: cubic-bezier(0.65, 0, 0.35, 1); /* power2.inOut, Kurve des Musters: Bild, Nachbarn, Panel */
--text-ein: cubic-bezier(0.16, 1, 0.3, 1); /* expo.out, gemessen für den Text */
--ui: 250ms; /* UI laut Rezept */
--rand: clamp(18px, 3.4vw, 48px);
}
* { box-sizing: border-box; margin: 0; }
html { background: var(--grund); color: var(--text); font-family: 'Hanken Grotesk', system-ui, sans-serif; font-weight: 400; letter-spacing: -0.012em; -webkit-font-smoothing: antialiased; }
body { min-height: 100svh; padding: 0 var(--rand) 96px; }
button { font: inherit; color: inherit; letter-spacing: inherit; background: none; border: 0; padding: 0; cursor: pointer; }
a { color: inherit; text-decoration: none; }
:focus-visible { outline: 2px solid var(--fokus); outline-offset: 3px; }
.offen-seite { overflow: hidden; }
/* Kopf: höchstens fünf Punkte */
.kopf { display: flex; align-items: baseline; gap: 32px; padding: 26px 0 0; font-size: 15px; }
.marke { margin-right: auto; font-weight: 500; letter-spacing: -0.02em; }
.marke span { color: var(--leise); font-weight: 400; }
.nav { display: flex; gap: 22px; list-style: none; padding: 0; }
.nav a { color: var(--leise); transition: color var(--ui) var(--bewegung); }
.nav a:hover, .nav a[aria-current] { color: var(--text); }
.sprache { color: var(--leise); }
/* Einleitung */
.einleitung { display: grid; grid-template-columns: repeat(12, minmax(0, 1fr)); column-gap: 24px; align-items: end; padding: clamp(40px, 7vh, 88px) 0 clamp(28px, 5vh, 56px); }
h1 { grid-column: 1 / span 6; font-weight: 450; font-size: clamp(1.953rem, 1.2rem + 2.4vw, 3.052rem); line-height: 1; letter-spacing: -0.02em; }
h1 sup { font-size: .32em; vertical-align: top; position: relative; top: .25em; margin-left: .2em; color: var(--leise); font-weight: 400; letter-spacing: 0; }
.einleitung p { grid-column: 8 / span 5; max-width: 50ch; font-size: 1.0625rem; line-height: 1.45; color: var(--leise); text-wrap: pretty; }
.einleitung p em { color: var(--text); }
/* Raster: Bilder unbeschnitten, Zeilen auf gemeinsamer Unterkante */
.raster { list-style: none; padding: 0; display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: clamp(36px, 6vh, 64px) clamp(20px, 2.8vw, 40px); align-items: end; }
.kachel { min-width: 0; }
.kachel .oeffnen { display: block; width: 100%; text-align: left; }
.rahmen { position: relative; aspect-ratio: var(--ar); width: var(--breite, 100%); }
.bild { position: relative; width: 100%; height: 100%; overflow: hidden; background: var(--flaeche); }
.bild svg { display: block; width: 100%; height: 100%; }
.kachel .titel { display: block; margin-top: 12px; font-size: .8rem; line-height: 1.35; }
.kachel .titel span { color: var(--leise); }
.kachel .titel i { font-style: normal; }
.kachel .oeffnen:hover .titel, .kachel .oeffnen:focus-visible .titel { text-decoration: underline; text-decoration-thickness: 8%; text-underline-offset: .35ex; }
.r-hoch { --ar: 4 / 5; --breite: 72%; }
.r-quer { --ar: 3 / 2; }
.r-quadrat { --ar: 1 / 1; --breite: 84%; }
/* Detailebene: liegt über dem Raster, eigener Grund nur im Panel */
.detail { position: fixed; inset: 0; z-index: 20; display: grid; grid-template-columns: repeat(12, minmax(0, 1fr)); column-gap: 24px; padding: 88px var(--rand) var(--rand); pointer-events: none; visibility: hidden; }
.detail.an { pointer-events: auto; visibility: visible; }
.detail .flaeche { position: absolute; inset: 0; background: var(--grund); opacity: 0; }
.zone { position: relative; grid-column: 1 / span 7; grid-row: 1; min-height: 0; display: flex; align-items: flex-start; justify-content: flex-start; }
.slot { position: relative; flex: none; }
.slot .bild { position: absolute; inset: 0; }
.info { position: relative; grid-column: 9 / span 4; grid-row: 1; display: flex; flex-direction: column; min-height: 0; }
.info-kopf { display: flex; justify-content: space-between; align-items: baseline; font-size: .8rem; color: var(--leise); }
.zu { display: inline-flex; gap: 10px; align-items: baseline; color: var(--text); font-size: .8rem; }
.zu kbd { font: inherit; color: var(--leise); }
.zu:hover { text-decoration: underline; text-decoration-thickness: 8%; text-underline-offset: .35ex; }
.maske { display: block; overflow: hidden; padding-bottom: .12em; margin-bottom: -.12em; }
.maske > span { display: block; }
.info h2 { margin-top: auto; padding-top: 24px; font-weight: 450; font-size: clamp(1.953rem, 1.2rem + 2.4vw, 3.052rem); line-height: 1.02; letter-spacing: -0.02em; text-wrap: balance; }
.info .ortjahr { margin-top: 10px; font-size: 1.0625rem; color: var(--leise); }
.info .ortjahr i { font-style: italic; }
.info dl { margin-top: 28px; display: grid; grid-template-columns: 8.5em 1fr; font-size: .8rem; line-height: 1.35; border-top: 1px solid var(--flaeche); }
.info dt, .info dd { padding: 7px 0; border-bottom: 1px solid var(--flaeche); }
.info dt { color: var(--leise); }
.info .beschreibung { margin-top: 22px; max-width: 50ch; font-size: 1.0625rem; line-height: 1.45; text-wrap: pretty; }
.blaettern { display: flex; justify-content: space-between; margin-top: 28px; font-size: .8rem; }
.blaettern button:hover { text-decoration: underline; text-decoration-thickness: 8%; text-underline-offset: .35ex; }
.blaettern span { color: var(--leise); }
/* Variante Panel: an der dem Klick gegenüberliegenden Seite */
.detail.panel { padding: 32px; column-gap: 0; grid-template-columns: 1fr; grid-template-rows: minmax(0, 1fr) auto; width: 50%; }
.detail.panel.rechts { left: 50%; }
.detail.panel .flaeche { background: var(--flaeche); opacity: 1; }
.detail.panel .zone { grid-column: 1; grid-row: 1; justify-content: center; align-items: center; }
.detail.panel .info { grid-column: 1; grid-row: 2; padding-top: 22px; display: grid; grid-template-columns: 1fr 1fr; column-gap: 24px; align-items: start; }
.detail.panel .info-kopf { grid-column: 1 / -1; order: -1; }
.detail.panel .info h2 { grid-column: 1; padding-top: 14px; font-size: clamp(1.563rem, 1rem + 1.4vw, 1.953rem); }
.detail.panel .ortjahr { grid-column: 1; font-size: .8rem; }
.detail.panel dl { grid-column: 2; grid-row: 2 / span 2; margin-top: 18px; }
.detail.panel .beschreibung { grid-column: 1 / -1; margin-top: 16px; font-size: .9375rem; }
.detail.panel .blaettern { grid-column: 1 / -1; margin-top: 16px; }
.detail.panel dl dd, .detail.panel dl dt { border-color: rgba(31, 35, 35, .12); }
.detail.panel dl { border-color: rgba(31, 35, 35, .12); }
/* Kopien-Übergang */
.kopie { position: fixed; z-index: 30; overflow: hidden; pointer-events: none; background: var(--flaeche); }
.kopie svg { display: block; width: 100%; height: 100%; }
/* Native View Transition: Namen setzt das Skript nur während des Übergangs */
::view-transition-old(root) { display: none; }
::view-transition-new(root) { animation: none; }
::view-transition-group(projektbild) { animation-duration: 700ms; animation-timing-function: var(--bewegung); }
::view-transition-old(projektbild), ::view-transition-new(projektbild) { animation: none; height: 100%; }
::view-transition-old(projektbild) { display: none; }
::view-transition-group(.kachel) { animation: none; }
.vt-auf::view-transition-old(.kachel) { animation: vt-weg 300ms var(--bewegung) both; }
.vt-auf::view-transition-new(.kachel) { display: none; }
.vt-zu::view-transition-old(.kachel) { display: none; }
.vt-zu::view-transition-new(.kachel) { animation: vt-her 300ms var(--bewegung) both; }
@keyframes vt-weg { to { opacity: 0; scale: .8; } }
@keyframes vt-her { from { opacity: 0; scale: .8; } }
/* Bedienleiste der Demo */
.pult { position: fixed; z-index: 15; left: 50%; bottom: 18px; translate: -50% 0; display: flex; align-items: center; gap: 18px; padding: 8px 10px 8px 16px; border: 1px solid var(--flaeche); border-radius: 999px; background: rgba(255, 255, 255, .92); -webkit-backdrop-filter: blur(10px); backdrop-filter: blur(10px); font: 500 10.5px/1 'Geist Mono', ui-monospace, monospace; letter-spacing: .14em; text-transform: uppercase; color: var(--leise); white-space: nowrap; transition: opacity var(--ui) var(--bewegung); }
.offen-seite .pult { display: none; }
.gruppe { display: flex; align-items: center; gap: 3px; }
.gruppe > span { margin-right: 6px; }
.gruppe button { height: 26px; padding: 0 10px; border-radius: 999px; text-transform: inherit; letter-spacing: inherit; color: var(--leise); transition: background-color var(--ui) var(--bewegung), color var(--ui) var(--bewegung); }
.gruppe button:hover { color: var(--text); }
.gruppe button[aria-pressed='true'] { background: var(--text); color: #fff; }
.werte { color: var(--leise); }
.werte b { color: var(--text); font-weight: 500; }
/* Vorführung: gezeichneter Zeiger */
.zeiger { position: fixed; left: 0; top: 0; z-index: 40; width: 22px; height: 22px; pointer-events: none; opacity: 0; transition: transform .7s var(--bewegung), opacity .25s; }
.vorfuehrung .zeiger { opacity: 1; }
.zeiger path { transition: transform 150ms; transform-origin: 3px 2px; }
.zeiger.drueckt path { transform: scale(.82); }
/* =========================================================================
Varianten (Regel 13): ?v=basis | produkt | serie
========================================================================= */
html[data-v="produkt"] [data-welt]:not([data-welt="produkt"]), html:not([data-v="produkt"]) [data-welt="produkt"],
html[data-v="zoom"] [data-welt]:not([data-welt="zoom"]), html:not([data-v="zoom"]) [data-welt="zoom"], html[data-v="serie"] [data-welt]:not([data-welt="serie"]), html:not([data-v="serie"]) [data-welt="serie"] { display: none !important; }
html:not([data-v="basis"]) .pult [data-teil="basis"] { display: none; }
.gruppe-v button[aria-pressed='true'] { background: var(--text); }
/* ---------- Produkt: Hauptbild mit Daumen, Kaufbox, Vollbild mit Daumenleiste (Rezept mode-beauty-kollektion) ---------- */
html[data-v="produkt"] { --grund: #f6f3ee; --text: #121010; --leise: #8c857b; --flaeche: #ebe7e0; --fokus: #121010; background: #f6f3ee; }
.pd { --k: cubic-bezier(0.25, 1, 0.5, 1); font-family: 'Zalando Sans', system-ui, sans-serif; font-stretch: 100%; color: #121010; letter-spacing: 0; }
.pd-kopf { display: flex; justify-content: space-between; align-items: center; gap: 20px; padding: 22px 0 18px; border-bottom: 1px solid #cecdc6; font-size: 14px; }
.pd-kopf b { font-weight: 500; font-stretch: 125%; letter-spacing: .12em; font-size: 15px; }
.pd-kopf nav { display: flex; gap: 22px; color: #8c857b; }
.pd-pfad { padding: 14px 0 0; font: 400 11px/1.4 'Fragment Mono', monospace; letter-spacing: .06em; text-transform: uppercase; color: #8c857b; }
.pd-buehne { display: grid; grid-template-columns: 76px minmax(0, 1.25fr) minmax(0, 1fr); gap: clamp(14px, 2vw, 28px); align-items: start; padding: 18px 0 40px; }
.pd-daumen { display: grid; gap: 8px; list-style: none; padding: 0; position: sticky; top: 18px; }
.pd-daumen button { display: block; width: 100%; aspect-ratio: 4 / 5; overflow: hidden; background: #ebe7e0; outline-offset: 2px; opacity: .62; transition: opacity 200ms var(--k), box-shadow 200ms var(--k); }
.pd-daumen button:hover { opacity: 1; }
.pd-daumen button[aria-current='true'] { opacity: 1; box-shadow: 0 0 0 1.5px #121010; }
.pd-daumen svg, .pd-bild svg { display: block; width: 100%; height: 100%; }
.pd-haupt { position: relative; aspect-ratio: 4 / 5; background: #ebe7e0; cursor: zoom-in; }
.pd-haupt > button { position: absolute; inset: 0; width: 100%; height: 100%; cursor: zoom-in; }
.pd-bild { position: absolute; inset: 0; overflow: hidden; background: #ebe7e0; }
.pd-lupe { position: absolute; right: 14px; bottom: 14px; z-index: 2; display: inline-flex; align-items: center; gap: 8px; height: 34px; padding: 0 12px; border-radius: 999px; background: rgba(246, 243, 238, .9); font: 400 11px/1 'Fragment Mono', monospace; letter-spacing: .06em; text-transform: uppercase; pointer-events: none; }
.pd-box { position: sticky; top: 18px; display: grid; gap: 16px; padding-top: 6px; }
.pd-box .marke { font: 400 11px/1.4 'Fragment Mono', monospace; letter-spacing: .08em; text-transform: uppercase; color: #8c857b; }
.pd-box h1 { font: 400 clamp(2rem, 1.2rem + 2.4vw, 3rem)/1.05 'Zalando Sans', sans-serif; font-stretch: 125%; letter-spacing: -.01em; grid-column: auto; }
.pd-preis { display: flex; align-items: baseline; gap: 12px; flex-wrap: wrap; font-variant-numeric: tabular-nums; }
.pd-preis b { font-weight: 500; font-size: 1.25rem; }
.pd-preis span { font-size: .8125rem; color: #8c857b; }
.pd-text { font-size: .9375rem; line-height: 1.55; color: #3d3836; max-width: 44ch; }
.pd-groesse { display: flex; gap: 8px; }
.pd-groesse button { min-width: 72px; height: 40px; padding: 0 14px; border: 1px solid #cecdc6; font-size: .875rem; font-variant-numeric: tabular-nums; transition: border-color 200ms var(--k); }
.pd-groesse button[aria-pressed='true'] { border-color: #121010; }
.pd-kaufen { height: 50px; background: #121010; color: #f6f3ee; font-weight: 500; font-size: .9375rem; letter-spacing: .02em; transition: background-color 200ms var(--k); }
.pd-kaufen:hover { background: #2a2524; }
.pd-liefer { font-size: .8125rem; color: #3d3836; display: flex; gap: 8px; align-items: center; }
.pd-liefer::before { content: ''; width: 7px; height: 7px; border-radius: 50%; background: #5c7a4f; }
.pd-wirk { list-style: none; padding: 0; border-top: 1px solid #cecdc6; font-size: .875rem; }
.pd-wirk li { display: flex; justify-content: space-between; gap: 12px; padding: 10px 0; border-bottom: 1px solid #cecdc6; }
.pd-wirk span { font: 400 11px/1.6 'Fragment Mono', monospace; letter-spacing: .06em; text-transform: uppercase; color: #8c857b; }
/* Vollbild */
.pd-voll { position: fixed; inset: 0; z-index: 40; display: grid; grid-template-columns: minmax(0, 1fr); grid-template-rows: auto minmax(0, 1fr) auto; visibility: hidden; pointer-events: none; }
.pd-voll.an { visibility: visible; pointer-events: auto; }
.pd-voll .grund { position: absolute; inset: 0; background: #f6f3ee; opacity: 0; }
.pd-voll-kopf { position: relative; display: flex; justify-content: space-between; align-items: center; padding: 16px clamp(16px, 3vw, 32px); font: 400 11px/1 'Fragment Mono', monospace; letter-spacing: .08em; text-transform: uppercase; color: #8c857b; }
.pd-voll-kopf button { display: inline-flex; align-items: center; gap: 8px; height: 36px; padding: 0 14px; border: 1px solid #cecdc6; border-radius: 999px; color: #121010; }
.pd-voll-zone { position: relative; display: grid; place-items: center; min-height: 0; padding: 0 clamp(56px, 8vw, 120px); }
.pd-voll-slot { position: relative; }
.pd-voll-slot .pd-bild { background: #ebe7e0; }
.pd-pfeil { position: absolute; top: 50%; translate: 0 -50%; width: 48px; height: 48px; border: 1px solid #cecdc6; border-radius: 50%; background: #f6f3ee; font-size: 18px; transition: background-color 200ms var(--k); }
.pd-pfeil:hover { background: #ebe7e0; }
.pd-pfeil.l { left: clamp(10px, 2vw, 32px); } .pd-pfeil.r { right: clamp(10px, 2vw, 32px); }
.pd-voll-leiste { position: relative; display: flex; justify-content: center; gap: 8px; padding: 14px 16px 18px; }
.pd-voll-leiste button { width: 44px; aspect-ratio: 4 / 5; overflow: hidden; opacity: .55; background: #ebe7e0; transition: opacity 200ms var(--k), box-shadow 200ms var(--k); }
.pd-voll-leiste button[aria-current='true'] { opacity: 1; box-shadow: 0 0 0 1.5px #121010; }
.pd-voll-leiste svg { display: block; width: 100%; height: 100%; }
.pd-voll .wisch { display: none; }
/* ---------- Zoom: Produktbild mit Lupe (Desktop), Vollbild mit Doppeltippen, Pinch, Ziehen, +/−, Pfeiltasten (Rezept d2c-produkt-zuerst) ---------- */
html[data-v="zoom"] { --grund: #F5F4F1; --text: #212121; --leise: #696968; --flaeche: #ECE9E2; --fokus: #212121; background: #F5F4F1; }
.zm { --zk: cubic-bezier(0.16, 1, 0.3, 1); --zk-aus: cubic-bezier(0.7, 0, 0.84, 0); --zl: rgba(33, 33, 33, .12); font-family: 'Instrument Sans', system-ui, sans-serif; color: #212121; letter-spacing: 0; }
.zm-kopf { display: flex; align-items: center; gap: 28px; padding: 20px 0 16px; border-bottom: 1px solid var(--zl); font-size: .9375rem; }
.zm-marke { margin-right: auto; font: 400 1.625rem/1 'Young Serif', Georgia, serif; letter-spacing: -.01em; }
.zm-kopf nav { display: flex; gap: 22px; color: #696968; }
.zm-pfad { padding: 14px 0 0; font-size: .75rem; letter-spacing: .06em; text-transform: uppercase; color: #696968; }
.zm-buehne { display: grid; grid-template-columns: minmax(0, 7fr) minmax(0, 5fr); gap: clamp(20px, 3vw, 48px); align-items: start; padding: 16px 0 48px; }
.zm-galerie { display: grid; grid-template-columns: 72px minmax(0, 1fr); gap: 12px; align-items: start; }
.zm-daumen { display: grid; gap: 8px; list-style: none; padding: 0; position: sticky; top: 16px; }
.zm-daumen button { display: block; width: 100%; aspect-ratio: 4 / 5; overflow: hidden; border-radius: 8px; background: #ECE9E2; box-shadow: 0 0 0 1px transparent; transition: box-shadow 150ms var(--zk); }
.zm-daumen button[aria-current='true'] { box-shadow: 0 0 0 1.5px #212121; }
.zm-daumen img { display: block; width: 100%; height: 100%; object-fit: cover; }
.zm-haupt { position: relative; aspect-ratio: 4 / 5; max-height: calc(100svh - 120px); margin-inline: auto; width: 100%; overflow: hidden; border-radius: 12px; background: #ECE9E2; }
.zm-haupt > img { display: block; width: 100%; height: 100%; object-fit: cover; }
.zm-lupe { position: absolute; left: 0; top: 0; z-index: 2; width: 200px; height: 200px; margin: -100px 0 0 -100px; border-radius: 50%; pointer-events: none; opacity: 0; scale: .9;
background-repeat: no-repeat; box-shadow: 0 0 0 1px rgba(255, 255, 255, .9), 0 0 0 2px rgba(33, 33, 33, .35), 0 14px 34px rgba(0, 0, 0, .22);
transition: opacity 200ms var(--zk), scale 200ms var(--zk); }
.zm-lupe.an { opacity: 1; scale: 1; }
.zm-oeffnen { position: absolute; right: 14px; top: 14px; z-index: 3; display: inline-flex; align-items: center; gap: 8px; height: 44px; padding: 0 16px 0 14px; border-radius: 999px; background: rgba(245, 244, 241, .94); color: #212121; font-size: .875rem; font-weight: 500; transition: background-color 150ms var(--zk); }
.zm-oeffnen:hover { background: #fff; }
.zm-oeffnen svg { width: 16px; height: 16px; }
@media (hover: hover) and (pointer: fine) { .zm-haupt { cursor: zoom-in; } }
.zm-box { position: sticky; top: 16px; display: grid; gap: 16px; padding-top: 4px; }
.zm-box .zm-hersteller { font-size: .75rem; letter-spacing: .06em; text-transform: uppercase; color: #696968; }
.zm-box h1 { grid-column: auto; font: 600 clamp(1.5rem, 1.1rem + 1.4vw, 2.25rem)/1.1 'Instrument Sans', sans-serif; letter-spacing: -.015em; }
.zm-preis { display: flex; align-items: baseline; flex-wrap: wrap; gap: 4px 12px; font-variant-numeric: tabular-nums; }
.zm-preis b { font-weight: 600; font-size: 1.125rem; }
.zm-preis span { font-size: .8125rem; color: #696968; }
.zm-text { font-size: 1rem; line-height: 1.55; max-width: 46ch; }
.zm-wahl { display: flex; flex-wrap: wrap; gap: 8px; }
.zm-wahl button { height: 44px; padding: 0 16px; border-radius: 999px; border: 1px solid var(--zl); font-size: .875rem; font-variant-numeric: tabular-nums; transition: border-color 150ms var(--zk); }
.zm-wahl button[aria-pressed='true'] { border-color: #212121; box-shadow: inset 0 0 0 .5px #212121; }
.zm-kaufen { height: 52px; border-radius: 999px; background: #212121; color: #F5F4F1; font-weight: 600; font-size: .9375rem; transition: background-color 150ms var(--zk); }
.zm-kaufen:hover { background: #000; }
.zm-liefer { font-size: .8125rem; display: flex; gap: 8px; align-items: center; }
.zm-liefer::before { content: ''; width: 7px; height: 7px; border-radius: 50%; background: #10714E; }
.zm-daten { display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 0 16px; border-top: 1px solid var(--zl); font-size: .875rem; }
.zm-daten dt, .zm-daten dd { padding: 9px 0; border-bottom: 1px solid var(--zl); }
.zm-daten dt { font-size: .75rem; letter-spacing: .06em; text-transform: uppercase; color: #696968; padding-top: 11px; }
/* Vollbild als natives dialog */
.zm-dlg { width: 100%; height: 100%; max-width: none; max-height: none; margin: 0; padding: 0; border: 0; background: transparent; color: #212121; overflow: hidden; font-family: 'Instrument Sans', system-ui, sans-serif; }
.zm-dlg[open] { display: grid; grid-template-rows: auto minmax(0, 1fr) auto; }
.zm-dlg::backdrop { background: transparent; }
.zm-grund { position: absolute; inset: 0; background: #F5F4F1; }
.zm-dlg-kopf, .zm-dlg-fuss { position: relative; z-index: 2; display: flex; align-items: center; gap: 12px 20px; flex-wrap: wrap; padding: 14px clamp(14px, 3vw, 32px); }
.zm-dlg-kopf { justify-content: space-between; }
.zm-dlg-kopf p { display: flex; gap: 12px; align-items: baseline; font-size: .9375rem; }
.zm-dlg-kopf p span { font-size: .75rem; letter-spacing: .06em; color: #696968; font-variant-numeric: tabular-nums; }
.zm-stufen { display: flex; align-items: center; gap: 4px; padding: 3px; border-radius: 999px; border: 1px solid var(--zl); background: #F5F4F1; }
.zm-stufen button { width: 40px; height: 40px; border-radius: 50%; font-size: 1.25rem; line-height: 1; transition: background-color 150ms var(--zk); }
.zm-stufen button:hover:not(:disabled) { background: #ECE9E2; }
.zm-stufen button:disabled { color: rgba(33, 33, 33, .35); cursor: default; }
.zm-stufen output { min-width: 4.2em; text-align: center; font-size: .8125rem; font-variant-numeric: tabular-nums; }
.zm-zu { display: inline-flex; align-items: center; gap: 8px; height: 44px; padding: 0 16px; border-radius: 999px; border: 1px solid var(--zl); font-size: .875rem; }
.zm-zu kbd { font: inherit; font-size: .6875rem; color: #696968; }
.zm-feld { position: relative; z-index: 1; display: grid; place-items: center; min-height: 0; overflow: hidden; touch-action: none; user-select: none; outline: none; cursor: zoom-in; }
.zm-feld.gezoomt { cursor: grab; }
.zm-feld.zieht { cursor: grabbing; }
.zm-feld:focus-visible { box-shadow: inset 0 0 0 2px #212121; }
.zm-flaeche { position: relative; border-radius: 12px; overflow: hidden; background: #ECE9E2; transform-origin: 50% 50%; will-change: transform; }
.zm-feld.gezoomt .zm-flaeche { border-radius: 0; }
.zm-flaeche.gleitet { transition: transform 300ms var(--zk); }
.zm-flaeche img { display: block; width: 100%; height: 100%; object-fit: cover; -webkit-user-drag: none; pointer-events: none; }
.zm-laedt { position: absolute; left: 50%; bottom: 14px; translate: -50% 0; z-index: 2; padding: 6px 12px; border-radius: 999px; background: rgba(33, 33, 33, .82); color: #F5F4F1; font-size: .75rem; letter-spacing: .04em; }
.zm-laedt[hidden] { display: none; }
.zm-dlg-fuss { justify-content: center; }
.zm-dlg-fuss .zm-pfeil { width: 44px; height: 44px; border-radius: 50%; border: 1px solid var(--zl); font-size: 1.125rem; }
.zm-dlg-fuss .zm-reihe { display: flex; gap: 8px; }
.zm-dlg-fuss .zm-reihe button { width: 44px; aspect-ratio: 4 / 5; overflow: hidden; border-radius: 6px; background: #ECE9E2; opacity: .7; transition: opacity 150ms var(--zk), box-shadow 150ms var(--zk); }
.zm-dlg-fuss .zm-reihe button[aria-current='true'] { opacity: 1; box-shadow: 0 0 0 1.5px #212121; }
.zm-dlg-fuss .zm-reihe img { display: block; width: 100%; height: 100%; object-fit: cover; }
.zm-hinweis { width: 100%; text-align: center; font-size: .75rem; color: #696968; }
.zm-dlg :focus-visible { outline: 2px solid #212121; outline-offset: 3px; }
@media (hover: hover) and (pointer: fine) { .zm-hinweis .touch { display: none; } }
@media (hover: none), (pointer: coarse) { .zm-hinweis .maus { display: none; } }
@media (max-width: 900px) {
.zm-buehne { grid-template-columns: minmax(0, 1fr); }
.zm-box { position: static; }
.zm-galerie { grid-template-columns: minmax(0, 1fr); }
.zm-daumen { position: static; order: 2; grid-template-columns: repeat(4, minmax(0, 1fr)); }
.zm-haupt { max-height: none; }
}
@media (max-width: 640px) {
.zm-kopf nav { display: none; }
.zm-dlg-kopf { padding: 10px 12px; }
.zm-dlg-kopf p b { display: none; }
.zm-dlg-fuss { padding: 10px 12px 14px; }
.zm-dlg-fuss .zm-pfeil { display: none; }
}
@media (hover: none) { .zm-zu kbd { display: none; } }
@media (max-width: 360px) { .zm-zu kbd { display: none; } .zm-stufen output { min-width: 3.4em; } }
.vorschau .zm-kopf nav, .vorschau .zm-pfad, .vorschau .zm-daten, .vorschau .zm-text { display: none; }
.vorschau .zm-haupt { max-height: 610px; }
@media (forced-colors: active) { .zm button { border: 1px solid ButtonText !important; } .zm-lupe { forced-color-adjust: none; } }
/* ---------- Serie: Lookbook als Spalte, Leuchtkasten mit Rückweg an die neue Stelle (Rezept fotograf-film-bild-zuerst) ---------- */
html[data-v="serie"] { --grund: #f4f4f2; --text: #141414; --leise: #8a8a8a; --flaeche: #e6e6e6; --fokus: #141414; background: #f4f4f2; }
.se { --k: cubic-bezier(0.455, 0.03, 0.515, 0.955); font-family: 'Instrument Sans', system-ui, sans-serif; color: #141414; letter-spacing: 0; }
.se-kopf { display: flex; justify-content: space-between; align-items: baseline; gap: 20px; padding: 24px 0 0; }
.se-kopf b { font: 400 1.375rem/1 'Instrument Serif', serif; }
.se-kopf nav { display: flex; gap: 20px; font-size: .9375rem; color: #8a8a8a; }
.se-titel { padding: clamp(48px, 9vh, 110px) 0 clamp(30px, 6vh, 64px); text-align: center; }
.se-titel h1 { grid-column: auto; font: 400 clamp(2.25rem, 1.4rem + 3vw, 3.5rem)/1.05 'Instrument Serif', serif; letter-spacing: -.01em; }
.se-titel p { margin-top: 12px; font: 400 .75rem/1.5 'Fragment Mono', monospace; letter-spacing: .02em; color: #8a8a8a; font-variant-numeric: tabular-nums; }
.se-spalte { list-style: none; padding: 0; display: grid; gap: clamp(64px, 12vh, 140px); max-width: 1100px; margin: 0 auto; }
.se-spalte li { width: var(--b, 60%); justify-self: var(--x, center); }
.se-spalte li:nth-child(4n+1) { --x: start; --b: 54%; } .se-spalte li:nth-child(4n+2) { --x: end; --b: 40%; }
.se-spalte li:nth-child(4n+3) { --x: center; --b: 46%; } .se-spalte li:nth-child(4n) { --x: start; --b: 72%; margin-left: 10%; }
.se-spalte button { display: block; width: 100%; text-align: left; cursor: zoom-in; }
.se-rahmen { position: relative; aspect-ratio: var(--f); background: #e6e6e6; }
.se-bild { position: absolute; inset: 0; overflow: hidden; background: #e6e6e6; }
.se-bild svg { display: block; width: 100%; height: 100%; }
.se-unter { display: flex; justify-content: space-between; gap: 12px; margin-top: 10px; font: 400 .75rem/1.4 'Fragment Mono', monospace; letter-spacing: .02em; color: #8a8a8a; font-variant-numeric: tabular-nums; }
.se-unter b { font: 400 1rem/1.2 'Instrument Serif', serif; color: #141414; letter-spacing: 0; }
.se-ende { padding: 120px 0 40px; text-align: center; font: 400 .75rem/1.5 'Fragment Mono', monospace; color: #8a8a8a; }
.se-kasten { position: fixed; inset: 0; z-index: 40; display: grid; grid-template-columns: minmax(0, 1fr); grid-template-rows: auto minmax(0, 1fr) auto; color: #f2f2f2; visibility: hidden; pointer-events: none; }
.se-kasten.an { visibility: visible; pointer-events: auto; }
.se-kasten .grund { position: absolute; inset: 0; background: #141414; opacity: 0; }
.se-kasten-kopf, .se-kasten-fuss { position: relative; display: flex; justify-content: space-between; align-items: center; gap: 16px; padding: 18px clamp(16px, 3vw, 32px); font: 400 .75rem/1.4 'Fragment Mono', monospace; letter-spacing: .02em; color: #a3a3a3; font-variant-numeric: tabular-nums; }
.se-kasten-kopf button, .se-kasten-fuss button { height: 36px; padding: 0 14px; border: 1px solid rgba(242, 242, 242, .25); border-radius: 999px; color: #f2f2f2; font: inherit; }
.se-kasten-kopf button:hover, .se-kasten-fuss button:hover { border-color: #f2f2f2; }
.se-kasten-fuss b { font: 400 1.25rem/1.2 'Instrument Serif', serif; color: #f2f2f2; }
.se-kasten-zone { position: relative; display: grid; place-items: center; min-height: 0; padding: 0 clamp(16px, 4vw, 56px); }
.se-slot { position: relative; }
.se-kasten .grund, .pd-voll .grund { pointer-events: none; }
.se-kasten :focus-visible, .pd-voll :focus-visible { outline: 2px solid currentColor; outline-offset: 3px; }
@media (max-width: 900px) {
.pd-buehne { grid-template-columns: minmax(0, 1fr); }
.pd-daumen { position: static; order: 2; grid-template-columns: repeat(5, minmax(0, 1fr)); }
.pd-haupt { order: 1; } .pd-box { order: 3; position: static; }
.se-spalte li { --b: 86% !important; margin-left: 0 !important; }
.se-spalte li:nth-child(odd) { justify-self: start; } .se-spalte li:nth-child(even) { justify-self: end; }
}
@media (max-width: 640px) {
.pd-kopf nav, .se-kopf nav { display: none; }
.pd-voll-zone { padding: 0 12px; } .pd-pfeil { display: none; }
.pd-voll .wisch, .se-kasten .wisch { display: block; }
.se-spalte { gap: 64px; }
}
.wisch { display: none; text-align: center; font: 400 11px/1 'Fragment Mono', monospace; letter-spacing: .06em; text-transform: uppercase; opacity: .6; }
@media (hover: none) { .pd-daumen button:hover { opacity: .62; } }
.vorschau .pd-kopf nav, .vorschau .pd-pfad, .vorschau .pd-wirk, .vorschau .pd-text, .vorschau .se-kopf { display: none; }
.vorschau .pd-buehne { padding-top: 20px; grid-template-columns: 70px minmax(0, 1fr) minmax(0, 1fr); }
.vorschau .pd-haupt { max-height: 600px; }
.vorschau .se-titel { padding: 40px 0 30px; }
/* Vorschau: nur Raster */
.vorschau body { overflow: hidden; height: 100svh; padding-bottom: 0; }
.vorschau .einleitung { padding: 34px 0 26px; }
.vorschau .einleitung p { font-size: .9375rem; }
/* Reduzierte Bewegung: Überblendung statt Positionsanimation */
:root.reduziert *, :root.reduziert ::view-transition-group(*) { transition-duration: .01ms !important; animation-duration: .01ms !important; }
@media (max-width: 1100px) { .pult .werte { display: none; } }
@media (max-width: 900px) {
.raster { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.einleitung h1 { grid-column: 1 / -1; }
.einleitung p { grid-column: 1 / span 10; margin-top: 18px; }
.sprache { display: none; }
}
@media (max-width: 640px) {
body { padding-bottom: 72px; }
.kopf { gap: 16px; padding-top: 18px; }
.nav { display: none; }
.sprache { display: inline; }
.einleitung { padding: 36px 0 28px; }
.einleitung p { grid-column: 1 / -1; font-size: 1rem; }
.raster { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 32px 14px; }
.r-hoch { --breite: 86%; } .r-quadrat { --breite: 92%; }
.kachel .titel { font-size: .75rem; }
.kachel .titel span { display: block; margin-top: 2px; } .kachel .titel .trenner { display: none; }
/* Detail mobil: immer Vollbild, Bild in voller Breite, Text darunter */
.detail, .detail.panel { left: 0; width: auto; display: block; padding: 0 0 40px; overflow-y: auto; overscroll-behavior: contain; }
.detail .flaeche, .detail.panel .flaeche { position: fixed; background: var(--grund); opacity: 0; }
.zone, .detail.panel .zone { display: block; touch-action: pan-x; }
.info, .detail.panel .info { display: flex; padding: 18px var(--rand) 0; }
.detail.panel .info h2, .info h2 { margin-top: 0; padding-top: 18px; font-size: 1.953rem; }
.detail.panel .ortjahr { font-size: 1rem; }
.detail.panel dl { margin-top: 22px; }
.detail.panel .beschreibung { font-size: 1rem; }
.info-kopf .zu kbd { display: none; }
.wischhinweis { display: block; }
}
.wischhinweis { display: none; position: absolute; left: 50%; bottom: 10px; translate: -50% 0; width: 36px; height: 4px; border-radius: 4px; background: rgba(255, 255, 255, .8); box-shadow: 0 0 0 1px rgba(0, 0, 0, .08); }
@media (hover: none) {
.nav a:hover { color: var(--leise); }
.kachel .oeffnen:hover .titel { text-decoration: none; }
}
</style>
</head>
<body>
<header class="kopf" data-welt="basis">
<a class="marke" href="#start">Kofler Ganster <span>Architekten</span></a>
<ul class="nav">
<li><a href="#buero">Büro</a></li>
<li><a href="#projekte" aria-current="page">Projekte</a></li>
<li><a href="#team">Team</a></li>
<li><a href="#aktuelles">Aktuelles</a></li>
<li><a href="#kontakt">Kontakt</a></li>
</ul>
<a class="sprache" href="#en">EN</a>
</header>
<main id="projekte" data-welt="basis">
<section class="einleitung">
<h1>Projekte<sup>08</sup></h1>
<p>Wohnhäuser, Schulen und Orte der Stille in Graz und der Steiermark. <em>Eine Auswahl</em> aus zwölf Jahren Büroarbeit.</p>
</section>
<ul class="raster" data-raster>
<li class="kachel r-quadrat"><button class="oeffnen" type="button" aria-haspopup="dialog"><div class="rahmen"><div class="bild">
<svg viewBox="0 0 500 500" role="img" aria-label="Wohnbau mit tiefen Loggien unter hellem Himmel, daneben ein Baukran">
<defs><linearGradient id="a1h" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#aebdc8"/><stop offset=".75" stop-color="#e2e3de"/></linearGradient></defs>
<rect width="500" height="500" fill="url(#a1h)"/>
<path d="M418 34v330M352 46h140M418 46l-56 0 56-12 74 12" stroke="#a39782" stroke-width="3" fill="none"/><path d="M362 46v58" stroke="#a39782" stroke-width="1.5"/><rect x="356" y="104" width="12" height="9" fill="#a39782"/>
<path d="M58 132h290l70 22v276H58z" fill="#d8d2c6"/><path d="M348 132l70 22v276h-70z" fill="#aca597"/>
<g fill="#6f6a62"><rect x="74" y="150" width="46" height="36"/><rect x="134" y="150" width="46" height="36"/><rect x="194" y="150" width="46" height="36"/><rect x="254" y="150" width="46" height="36"/><rect x="74" y="206" width="46" height="36"/><rect x="134" y="206" width="46" height="36"/><rect x="194" y="206" width="46" height="36"/><rect x="254" y="206" width="46" height="36"/><rect x="74" y="262" width="46" height="36"/><rect x="134" y="262" width="46" height="36"/><rect x="194" y="262" width="46" height="36"/><rect x="254" y="262" width="46" height="36"/><rect x="74" y="318" width="46" height="36"/><rect x="134" y="318" width="46" height="36"/><rect x="194" y="318" width="46" height="36"/><rect x="254" y="318" width="46" height="36"/></g>
<g fill="#c9a77d" opacity=".55"><rect x="78" y="168" width="38" height="18"/><rect x="198" y="224" width="38" height="18"/><rect x="138" y="336" width="38" height="18"/><rect x="258" y="280" width="38" height="18"/></g>
<g fill="#e9e5dc"><rect x="70" y="184" width="236" height="4"/><rect x="70" y="240" width="236" height="4"/><rect x="70" y="296" width="236" height="4"/><rect x="70" y="352" width="236" height="4"/></g>
<g fill="#8f897d"><rect x="362" y="170" width="12" height="30"/><rect x="390" y="178" width="12" height="30"/><rect x="362" y="226" width="12" height="30"/><rect x="390" y="234" width="12" height="30"/><rect x="362" y="282" width="12" height="30"/><rect x="390" y="290" width="12" height="30"/></g>
<rect x="58" y="372" width="360" height="58" fill="#b9b2a4"/><rect x="74" y="384" width="80" height="46" fill="#4d4f4b"/><rect x="214" y="384" width="120" height="46" fill="#4d4f4b"/>
<rect y="430" width="500" height="70" fill="#a7a69d"/><path d="M0 446h500" stroke="#c4c2b9" stroke-width="2"/>
<circle cx="30" cy="404" r="34" fill="#6f7d5f"/><circle cx="470" cy="396" r="40" fill="#5f6d50"/><path d="M30 430v-20M470 430v-24" stroke="#4b4a40" stroke-width="4"/>
</svg></div></div><span class="titel">Wohnbau Reininghaus Q7 <span><i class="trenner">• </i>Graz-Eggenberg • 2026</span></span></button></li>
<li class="kachel r-hoch"><button class="oeffnen" type="button" aria-haspopup="dialog"><div class="rahmen"><div class="bild">
<svg viewBox="0 0 400 500" role="img" aria-label="Lesesaal mit runden Stützen, Oberlichtbändern und schrägen Lichtstreifen">
<defs><linearGradient id="a2b" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#d4c8b3"/><stop offset="1" stop-color="#a99577"/></linearGradient><linearGradient id="a2s" x1="0" y1="0" x2="1" y2="0"><stop offset="0" stop-color="#cfcabe"/><stop offset=".45" stop-color="#f7f5f0"/><stop offset="1" stop-color="#bdb7aa"/></linearGradient></defs>
<rect width="400" height="500" fill="#ece8df"/>
<path d="M0 0h400v70H0z" fill="#dcd6ca"/><g fill="#fbfaf6"><rect x="30" y="18" width="340" height="10"/><rect x="30" y="42" width="340" height="10"/></g>
<rect y="300" width="400" height="200" fill="url(#a2b)"/>
<g fill="#cfc6b6"><rect x="0" y="150" width="400" height="150"/></g>
<g fill="#5b4e3f"><rect x="14" y="168" width="110" height="120"/><rect x="276" y="168" width="110" height="120"/></g>
<g fill="#b98a5a"><rect x="20" y="174" width="8" height="34"/><rect x="31" y="178" width="6" height="30"/><rect x="40" y="172" width="9" height="36"/><rect x="58" y="176" width="7" height="32"/><rect x="74" y="174" width="10" height="34"/><rect x="96" y="178" width="7" height="30"/><rect x="288" y="174" width="9" height="34"/><rect x="304" y="178" width="7" height="30"/><rect x="320" y="172" width="8" height="36"/><rect x="342" y="176" width="10" height="32"/><rect x="362" y="174" width="7" height="34"/></g>
<g fill="#8c6f50"><rect x="24" y="218" width="9" height="30"/><rect x="44" y="214" width="7" height="34"/><rect x="66" y="218" width="10" height="30"/><rect x="90" y="216" width="8" height="32"/><rect x="292" y="216" width="8" height="32"/><rect x="314" y="218" width="10" height="30"/><rect x="336" y="214" width="7" height="34"/><rect x="356" y="218" width="9" height="30"/></g>
<g fill="#d9b27c"><rect x="22" y="256" width="10" height="28"/><rect x="50" y="254" width="8" height="30"/><rect x="80" y="258" width="9" height="26"/><rect x="298" y="256" width="9" height="28"/><rect x="330" y="254" width="8" height="30"/><rect x="352" y="258" width="10" height="26"/></g>
<path d="M60 70 150 70 250 500 120 500z" fill="#fffdf6" opacity=".35"/><path d="M230 70 300 70 400 430 400 500 330 500z" fill="#fffdf6" opacity=".28"/>
<rect x="70" y="70" width="34" height="380" fill="url(#a2s)"/><rect x="296" y="70" width="34" height="380" fill="url(#a2s)"/><rect x="183" y="70" width="34" height="290" fill="url(#a2s)"/>
<ellipse cx="200" cy="420" rx="120" ry="14" fill="#8a7559" opacity=".5"/>
<rect x="110" y="380" width="180" height="10" fill="#efe9dd"/><path d="M122 390v52M278 390v52" stroke="#3e3a33" stroke-width="5"/>
<path d="M168 380c-2-24 4-40 14-44 10 4 12 22 10 44z" fill="#2f2c27"/><circle cx="182" cy="326" r="9" fill="#2f2c27"/>
</svg></div></div><span class="titel">Stadtbibliothek Gleisdorf <span><i class="trenner">• </i>Gleisdorf • 2025</span></span></button></li>
<li class="kachel r-quer"><button class="oeffnen" type="button" aria-haspopup="dialog"><div class="rahmen"><div class="bild">
<svg viewBox="0 0 600 400" role="img" aria-label="Straßenfassade mit großem Atelierfenster in Stahlrahmen zwischen zwei Altbauten">
<rect width="600" height="400" fill="#c9d3d8"/>
<rect x="0" y="40" width="180" height="300" fill="#d4b588"/><path d="M0 40h180l-10-14H10z" fill="#9d7a54"/>
<g fill="#5c4d3c"><rect x="22" y="76" width="34" height="56"/><rect x="74" y="76" width="34" height="56"/><rect x="126" y="76" width="34" height="56"/><rect x="22" y="168" width="34" height="56"/><rect x="74" y="168" width="34" height="56"/><rect x="126" y="168" width="34" height="56"/><rect x="60" y="262" width="60" height="78"/></g>
<g fill="#e9dcc4"><rect x="18" y="132" width="42" height="5"/><rect x="70" y="132" width="42" height="5"/><rect x="122" y="132" width="42" height="5"/><rect x="18" y="224" width="42" height="5"/><rect x="70" y="224" width="42" height="5"/><rect x="122" y="224" width="42" height="5"/></g>
<rect x="180" y="58" width="240" height="282" fill="#ece9e2"/><rect x="180" y="58" width="240" height="8" fill="#d6d2c9"/>
<rect x="212" y="96" width="176" height="150" fill="#2b2e30"/>
<g fill="#9fb2bc"><rect x="218" y="102" width="52" height="66"/><rect x="274" y="102" width="52" height="66"/><rect x="330" y="102" width="52" height="66"/><rect x="218" y="172" width="52" height="68"/><rect x="274" y="172" width="52" height="68"/><rect x="330" y="172" width="52" height="68"/></g>
<path d="M218 102h52v30l-52 36zM274 140l52-38v40l-52 36zM330 172h52v20l-52 48z" fill="#c4d2d8" opacity=".7"/>
<rect x="262" y="270" width="76" height="70" fill="#2b2e30"/><rect x="268" y="276" width="64" height="64" fill="#7e8d93"/>
<rect x="420" y="30" width="180" height="310" fill="#d8b7a6"/><path d="M420 30h180V18H430z" fill="#a8806c"/>
<g fill="#4c4038"><rect x="446" y="66" width="32" height="54"/><rect x="496" y="66" width="32" height="54"/><rect x="546" y="66" width="32" height="54"/><rect x="446" y="156" width="32" height="54"/><rect x="496" y="156" width="32" height="54"/><rect x="546" y="156" width="32" height="54"/><rect x="446" y="250" width="132" height="70"/></g>
<rect y="340" width="600" height="60" fill="#8f8d87"/><rect y="340" width="600" height="12" fill="#b4b1aa"/><path d="M0 376h600" stroke="#7d7b75" stroke-width="2" stroke-dasharray="26 18"/>
<path d="M0 14 600 4" stroke="#3a3c3d" stroke-width="1.5"/>
</svg></div></div><span class="titel">Atelierhaus Lend <span><i class="trenner">• </i>Graz-Lend • 2024</span></span></button></li>
<li class="kachel r-hoch"><button class="oeffnen" type="button" aria-haspopup="dialog"><div class="rahmen"><div class="bild">
<svg viewBox="0 0 400 500" role="img" aria-label="Betonkubus am Hang in der Abenddämmerung, ein großes Fenster leuchtet warm">
<defs><linearGradient id="a4h" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#93a8bd"/><stop offset=".55" stop-color="#d7c2ad"/><stop offset=".7" stop-color="#ecc9a4"/></linearGradient><linearGradient id="a4w" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#6f7e55"/><stop offset="1" stop-color="#4d5b37"/></linearGradient></defs>
<rect width="400" height="500" fill="url(#a4h)"/>
<path d="M0 312c60-22 140-30 220-20s130 2 180-8v60H0z" fill="#7b8775" opacity=".7"/>
<path d="M0 350c90-26 210-40 400-22v172H0z" fill="url(#a4w)"/>
<path d="M118 176h150v168H118z" fill="#cdc4b6"/><path d="M268 176l52 18v156l-52-6z" fill="#8f8a82"/><path d="M118 176h150l52 18H170z" fill="#e2dbcf"/>
<rect x="140" y="246" width="104" height="70" fill="#e9b56b"/><rect x="140" y="246" width="104" height="70" fill="none" stroke="#3d3a35" stroke-width="3"/><path d="M192 246v70" stroke="#3d3a35" stroke-width="2"/>
<rect x="150" y="286" width="22" height="30" fill="#b9874c"/><rect x="206" y="270" width="28" height="4" fill="#fbe2b4"/>
<rect x="140" y="204" width="40" height="18" fill="#5a5751"/>
<path d="M100 344h240" stroke="#cfc6b6" stroke-width="5"/>
<path d="M168 500c10-50 30-110 44-152" stroke="#c7b99c" stroke-width="7" fill="none" opacity=".8"/>
<ellipse cx="54" cy="290" rx="34" ry="62" fill="#3f4a35"/><path d="M54 300v58" stroke="#2f3628" stroke-width="5"/><ellipse cx="352" cy="300" rx="26" ry="48" fill="#46523a"/><path d="M352 310v44" stroke="#2f3628" stroke-width="4"/><ellipse cx="380" cy="270" rx="22" ry="44" fill="#3a4532"/>
</svg></div></div><span class="titel">Haus am Ruckerlberg <span><i class="trenner">• </i>Graz-Waltendorf • 2023</span></span></button></li>
<li class="kachel r-quer"><button class="oeffnen" type="button" aria-haspopup="dialog"><div class="rahmen"><div class="bild">
<svg viewBox="0 0 600 400" role="img" aria-label="Langes flaches Uferdach auf schlanken Stützen am Fluss, gespiegelt im Wasser">
<defs><linearGradient id="a5w" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#7d969a"/><stop offset="1" stop-color="#4e676c"/></linearGradient></defs>
<rect width="600" height="400" fill="#dfe4e3"/>
<path d="M0 150c40-16 70-20 110-12s60-18 110-14 70 16 120 10 80-14 130-8 70 10 130 4v80H0z" fill="#7f8f78"/>
<path d="M0 176c80-10 200-12 320-6s200 4 280-2v52H0z" fill="#66775f"/>
<rect y="214" width="600" height="18" fill="#c9c3b5"/>
<rect x="150" y="146" width="300" height="10" fill="#efede8"/><rect x="150" y="156" width="300" height="5" fill="#3f4443"/>
<rect x="176" y="161" width="248" height="53" fill="#e9c78d" opacity=".9"/><path d="M176 161h248v53H176z" fill="none" stroke="#2f3332" stroke-width="2"/><path d="M238 161v53M300 161v53M362 161v53" stroke="#2f3332" stroke-width="1.5"/>
<g fill="#3d3f3a"><rect x="252" y="182" width="6" height="32"/><circle cx="255" cy="178" r="5"/><rect x="330" y="186" width="6" height="28"/><circle cx="333" cy="182" r="5"/></g>
<path d="M150 214h300l14 18H136z" fill="#b5ae9f"/>
<rect y="232" width="600" height="168" fill="url(#a5w)"/>
<g opacity=".42"><rect x="150" y="297" width="300" height="8" fill="#efede8"/><rect x="176" y="250" width="248" height="47" fill="#e9c78d"/><path d="M0 240c80 6 200 8 320 4s200-2 280 4" stroke="#66775f" stroke-width="16" fill="none"/></g>
<g stroke="#a9bcbd" stroke-width="2" opacity=".6"><path d="M40 330h120M220 352h90M360 320h140M90 372h70M420 380h120"/></g>
</svg></div></div><span class="titel">Uferdach an der Mur <span><i class="trenner">• </i>Graz-Gries • 2022</span></span></button></li>
<li class="kachel r-hoch"><button class="oeffnen" type="button" aria-haspopup="dialog"><div class="rahmen"><div class="bild">
<svg viewBox="0 0 400 500" role="img" aria-label="Weiße Aufbahrungshalle mit steilem Satteldach zwischen Birken auf Kies">
<rect width="400" height="500" fill="#e5e9ea"/>
<path d="M0 330c80-12 200-16 400-6v176H0z" fill="#cfc6b4"/>
<g fill="#b9af9b"><circle cx="40" cy="400" r="2"/><circle cx="90" cy="430" r="2"/><circle cx="150" cy="390" r="2"/><circle cx="220" cy="452" r="2"/><circle cx="300" cy="410" r="2"/><circle cx="350" cy="470" r="2"/><circle cx="120" cy="476" r="2"/><circle cx="260" cy="380" r="2"/></g>
<path d="M110 190 200 96 290 190v150H110z" fill="#f4f2ed"/><path d="M200 96 290 190 324 202 236 108z" fill="#d9d4ca"/><path d="M290 190l34 12v134l-34 4z" fill="#bdb7ab"/>
<rect x="194" y="214" width="12" height="96" fill="#4b4a46"/><rect x="180" y="300" width="40" height="40" fill="#3e3d39"/>
<path d="M110 340h214l40 30H70z" fill="#bfb5a2" opacity=".6"/>
<g stroke="#f7f6f2" stroke-width="5"><path d="M48 500V170M70 500V210M352 500V190"/></g>
<g stroke="#59574f" stroke-width="2" stroke-dasharray="3 14"><path d="M48 480V180M70 490V220M352 488V200"/></g>
<g fill="#9eaa82" opacity=".75"><ellipse cx="44" cy="186" rx="26" ry="38"/><ellipse cx="78" cy="226" rx="22" ry="30"/><ellipse cx="356" cy="200" rx="28" ry="40"/></g>
<rect x="236" y="398" width="70" height="6" fill="#6e6a60"/><path d="M242 404v16M300 404v16" stroke="#6e6a60" stroke-width="4"/>
</svg></div></div><span class="titel">Aufbahrungshalle St. Peter <span><i class="trenner">• </i>Graz-St. Peter • 2019</span></span></button></li>
<li class="kachel r-quer"><button class="oeffnen" type="button" aria-haspopup="dialog"><div class="rahmen"><div class="bild">
<svg viewBox="0 0 600 400" role="img" aria-label="Langer zweigeschossiger Schulbau mit senkrechten Holzlamellen und Pausenhof">
<rect width="600" height="400" fill="#d2dadd"/>
<circle cx="70" cy="200" r="58" fill="#73845f"/><circle cx="540" cy="190" r="64" fill="#687a55"/>
<rect x="60" y="120" width="480" height="160" fill="#b38c5e"/><rect x="60" y="112" width="480" height="10" fill="#e4e0d6"/>
<rect x="60" y="150" width="480" height="34" fill="#353b3a"/><rect x="60" y="222" width="480" height="34" fill="#353b3a"/>
<g stroke="#8b673f" stroke-width="3"><path d="M72 122v158M86 122v158M100 122v158M114 122v158M128 122v158M142 122v158M156 122v158M170 122v158M184 122v158M198 122v158M212 122v158M226 122v158M240 122v158M254 122v158M268 122v158M282 122v158M296 122v158M310 122v158M324 122v158M338 122v158M352 122v158M366 122v158M380 122v158M394 122v158M408 122v158M422 122v158M436 122v158M450 122v158M464 122v158M478 122v158M492 122v158M506 122v158M520 122v158M534 122v158"/></g>
<rect x="250" y="226" width="100" height="54" fill="#f0e6d2"/><path d="M250 226h100v54H250z" fill="none" stroke="#2c302f" stroke-width="3"/>
<rect y="280" width="600" height="120" fill="#a9a8a1"/>
<g fill="none" stroke="#e8e3d6" stroke-width="3"><circle cx="420" cy="340" r="34"/><path d="M0 300h600"/><path d="M120 320l20 0 0 20-20 0z"/></g>
<g fill="#2f2f2b"><circle cx="190" cy="318" r="5"/><rect x="186" y="324" width="8" height="18" rx="3"/><circle cx="214" cy="322" r="4"/><rect x="211" y="327" width="7" height="14" rx="3"/></g>
</svg></div></div><span class="titel">Volksschule Liebenau <span><i class="trenner">• </i>Graz-Liebenau • 2021</span></span></button></li>
<li class="kachel r-quadrat"><button class="oeffnen" type="button" aria-haspopup="dialog"><div class="rahmen"><div class="bild">
<svg viewBox="0 0 500 500" role="img" aria-label="Weingut als langer Betonriegel im Hang zwischen herbstlichen Rebzeilen">
<defs><linearGradient id="a8h" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#d6d9d2"/><stop offset="1" stop-color="#ecdcbf"/></linearGradient></defs>
<rect width="500" height="500" fill="url(#a8h)"/>
<path d="M0 170c70-30 140-40 220-22s150 10 280-24v120H0z" fill="#9aa07a"/>
<path d="M0 230c100-40 250-46 500-18v288H0z" fill="#b6a24e"/>
<g fill="none" stroke="#8d8237" stroke-width="5" stroke-linecap="round"><path d="M-20 262c120-30 300-36 540-12"/><path d="M-20 300c120-28 300-32 540-8"/><path d="M-20 342c130-24 300-26 540-2"/><path d="M-20 388c130-20 300-20 540 4"/><path d="M-20 436c130-16 300-14 540 10"/><path d="M-20 486c130-12 300-8 540 16"/></g>
<g fill="none" stroke="#c97d3c" stroke-width="3" stroke-dasharray="6 22" stroke-linecap="round"><path d="M-20 262c120-30 300-36 540-12"/><path d="M-20 342c130-24 300-26 540-2"/><path d="M-20 436c130-16 300-14 540 10"/></g>
<path d="M120 236h280v44H120z" fill="#cbc5b8"/><path d="M110 228h300v10H110z" fill="#7c5a3a"/><rect x="140" y="246" width="200" height="30" fill="#2e302d"/><rect x="200" y="250" width="70" height="26" fill="#e3b46a" opacity=".85"/>
<path d="M120 280h280l20 12H100z" fill="#a49a7f"/>
<path d="M444 236c-8-40-6-90 4-124 10 34 12 84 4 124z" fill="#5e6a3c"/><path d="M448 236v20" stroke="#4a4630" stroke-width="3"/>
</svg></div></div><span class="titel">Weingut Harrer <span><i class="trenner">• </i>Kitzeck im Sausal • 2018</span></span></button></li>
</ul>
</main>
<div class="detail" data-detail role="dialog" aria-modal="true" aria-labelledby="d-titel" data-welt="basis">
<div class="flaeche" data-flaeche></div>
<div class="zone" data-zone><div class="slot" data-slot></div><span class="wischhinweis" aria-hidden="true"></span></div>
<div class="info" data-info>
<div class="info-kopf"><span class="maske"><span data-zaehler>01 / 08</span></span><button class="zu" type="button" data-zu>Schließen <kbd>Esc</kbd></button></div>
<h2 id="d-titel"><span class="maske"><span data-titel></span></span></h2>
<p class="ortjahr"><span class="maske"><span data-ortjahr></span></span></p>
<dl data-daten></dl>
<p class="beschreibung"><span class="maske"><span data-text></span></span></p>
<nav class="blaettern" aria-label="Projekte blättern"><button type="button" data-zurueck>← Vorheriges</button><span aria-hidden="true">← →</span><button type="button" data-vor>Nächstes →</button></nav>
</div>
</div>
<!-- ===== Variante Produkt: Hauptbild mit Daumen, Vollbild mit Daumenleiste ===== -->
<div class="pd" data-welt="produkt">
<header class="pd-kopf"><b>SALBEIGRUND</b><nav><span>Gesicht</span><span>Körper</span><span>Haar</span><span>Über uns</span></nav><span>Warenkorb (0)</span></header>
<p class="pd-pfad">Gesicht / Öle / Nachtöl No. 3</p>
<section class="pd-buehne" aria-label="Produkt">
<ul class="pd-daumen" data-pd-daumen aria-label="Produktbilder"></ul>
<div class="pd-haupt" data-pd-haupt>
<button type="button" data-pd-oeffnen aria-haspopup="dialog" aria-label="Bild vergrößern"></button>
<span class="pd-lupe" aria-hidden="true">+ Vergrößern</span>
</div>
<div class="pd-box">
<p class="marke">Salbeigrund · Wendland</p>
<h1>Nachtöl No. 3</h1>
<p class="pd-preis"><b data-pd-preis>38,00 €</b><span data-pd-grund>126,67 € / 100 ml · inkl. MwSt.</span></p>
<p class="pd-text">Hagebutte und Sanddorn, kalt gepresst. Drei Tropfen am Abend, für trockene und müde Haut. Zieht ein, ohne zu glänzen.</p>
<div class="pd-groesse" role="group" aria-label="Größe"><button type="button" data-ml="15" aria-pressed="false">15 ml</button><button type="button" data-ml="30" aria-pressed="true">30 ml</button></div>
<button type="button" class="pd-kaufen">In den Warenkorb</button>
<p class="pd-liefer">Auf Lager · Lieferung in 2 bis 3 Werktagen</p>
<ul class="pd-wirk"><li><span>Wirkstoffe</span>Hagebuttenkernöl, Sanddornöl</li><li><span>Duft</span>leicht herb, ohne Parfum</li><li><span>Haut</span>trocken, reif, empfindlich</li></ul>
</div>
</section>
<div class="pd-voll" role="dialog" aria-modal="true" aria-label="Produktbilder groß" data-pd-voll>
<div class="grund" data-pd-grund-flaeche></div>
<div class="pd-voll-kopf"><span data-pd-zaehler>1 / 5</span><button type="button" data-pd-zu>Schließen <kbd>Esc</kbd></button></div>
<div class="pd-voll-zone" data-pd-zone>
<button type="button" class="pd-pfeil l" data-pd-zurueck aria-label="Vorheriges Bild">←</button>
<div class="pd-voll-slot" data-pd-slot></div>
<button type="button" class="pd-pfeil r" data-pd-vor aria-label="Nächstes Bild">→</button>
</div>
<div class="pd-voll-leiste" data-pd-leiste aria-label="Bilder"></div>
<p class="wisch">Wischen zum Blättern · nach unten zum Schließen</p>
</div>
</div>
<!-- ===== Variante Zoom: Lupe am Produktbild (nur Maus), Vollbild mit Zoom bis Faktor 3 ===== -->
<div class="zm" data-welt="zoom">
<header class="zm-kopf"><b class="zm-marke">Brede</b><nav><span>Plaids</span><span>Kissen</span><span>Meterware</span><span>Webhaus</span></nav><span>Warenkorb (0)</span></header>
<p class="zm-pfad">Plaids / Fischgrat / Anthrazit und Natur</p>
<section class="zm-buehne" aria-label="Produkt">
<div class="zm-galerie">
<ul class="zm-daumen" aria-label="Produktbilder">
<li><button type="button" data-zm-daumen="0" aria-current="true" aria-label="Bild 1: Gesamtansicht"><img src="bilder/galerie-shared-element-vollbild-plaid-gesamt.webp" alt="" width="800" height="1000" loading="lazy" decoding="async"></button></li>
<li><button type="button" data-zm-daumen="1" aria-current="false" aria-label="Bild 2: Webart"><img src="bilder/galerie-shared-element-vollbild-plaid-webart.webp" alt="" width="800" height="1000" loading="lazy" decoding="async"></button></li>
<li><button type="button" data-zm-daumen="2" aria-current="false" aria-label="Bild 3: Fransen"><img src="bilder/galerie-shared-element-vollbild-plaid-fransen.webp" alt="" width="800" height="1000" loading="lazy" decoding="async"></button></li>
<li><button type="button" data-zm-daumen="3" aria-current="false" aria-label="Bild 4: Etikett"><img src="bilder/galerie-shared-element-vollbild-plaid-etikett.webp" alt="" width="800" height="1000" loading="lazy" decoding="async"></button></li>
</ul>
<div class="zm-haupt" data-zm-haupt>
<img data-zm-bild src="bilder/galerie-shared-element-vollbild-plaid-gesamt.webp" alt="Plaid in Fischgrat, gefaltet, mit eingenähtem Etikett und geknoteten Fransen auf hellem Grund" width="800" height="1000" fetchpriority="high" decoding="async" draggable="false">
<span class="zm-lupe" data-zm-lupe aria-hidden="true"></span>
<button type="button" class="zm-oeffnen" data-zm-oeffnen aria-haspopup="dialog"><svg viewBox="0 0 16 16" aria-hidden="true"><circle cx="7" cy="7" r="4.6" fill="none" stroke="currentColor" stroke-width="1.4"/><path d="m10.4 10.4 3.6 3.6M7 4.8v4.4M4.8 7h4.4" stroke="currentColor" stroke-width="1.4"/></svg>Vergrößern</button>
</div>
</div>
<div class="zm-box">
<h1>Plaid Fischgrat</h1>
<p class="zm-preis"><b data-zm-preis>149,00 €</b><span>inkl. MwSt., zzgl. Versand</span></p>
<p class="zm-text">Schurwolle in der Kette, Leinen im Schuss. Gewebt auf einem Webstuhl von 1962, die Fransen knoten wir von Hand. Dicht genug fürs Sofa, leicht genug für den Sommer.</p>
<div class="zm-wahl" role="group" aria-label="Größe"><button type="button" data-zm-groesse="149,00 €" aria-pressed="true">130 × 180 cm</button><button type="button" data-zm-groesse="189,00 €" aria-pressed="false">150 × 200 cm</button></div>
<button type="button" class="zm-kaufen">In den Warenkorb</button>
<p class="zm-liefer">Auf Lager · Versand in 1 bis 2 Werktagen</p>
<dl class="zm-daten">
<dt>Material</dt><dd>70 % Schurwolle, 30 % Leinen</dd>
<dt>Gewicht</dt><dd>1 050 g (130 × 180 cm)</dd>
<dt>Pflege</dt><dd>Handwäsche 30 °C, liegend trocknen</dd>
<dt>Hersteller</dt><dd>Webhaus Brede, Nordhorn · hallo@brede.example</dd>
</dl>
</div>
</section>
<dialog class="zm-dlg" data-zm-dlg aria-labelledby="zm-titel">
<div class="zm-grund" data-zm-grund></div>
<div class="zm-dlg-kopf" data-zm-chrom>
<p><span data-zm-zahl>1 / 4</span><b id="zm-titel" data-zm-titel>Gesamtansicht</b></p>
<div class="zm-stufen" role="group" aria-label="Zoom">
<button type="button" data-zm-minus aria-label="Verkleinern">−</button>
<output data-zm-faktor aria-live="polite">100 %</output>
<button type="button" data-zm-plus aria-label="Vergrößern">+</button>
</div>
<button type="button" class="zm-zu" data-zm-zu>Schließen <kbd>Esc</kbd></button>
</div>
<div class="zm-feld" data-zm-feld tabindex="0" role="group" aria-roledescription="Zoomansicht" aria-label="Bild groß. Plus und Minus zoomen, Pfeiltasten verschieben den Ausschnitt oder blättern.">
<div class="zm-flaeche" data-zm-flaeche><img data-zm-gross alt="" draggable="false"></div>
<p class="zm-laedt" data-zm-laedt hidden>Detailbild lädt …</p>
</div>
<div class="zm-dlg-fuss" data-zm-chrom>
<button type="button" class="zm-pfeil" data-zm-schritt="-1" aria-label="Vorheriges Bild">←</button>
<div class="zm-reihe">
<button type="button" data-zm-reihe="0" aria-current="true" aria-label="Bild 1: Gesamtansicht"><img src="bilder/galerie-shared-element-vollbild-plaid-gesamt.webp" alt="" width="800" height="1000" loading="lazy" decoding="async"></button>
<button type="button" data-zm-reihe="1" aria-current="false" aria-label="Bild 2: Webart"><img src="bilder/galerie-shared-element-vollbild-plaid-webart.webp" alt="" width="800" height="1000" loading="lazy" decoding="async"></button>
<button type="button" data-zm-reihe="2" aria-current="false" aria-label="Bild 3: Fransen"><img src="bilder/galerie-shared-element-vollbild-plaid-fransen.webp" alt="" width="800" height="1000" loading="lazy" decoding="async"></button>
<button type="button" data-zm-reihe="3" aria-current="false" aria-label="Bild 4: Etikett"><img src="bilder/galerie-shared-element-vollbild-plaid-etikett.webp" alt="" width="800" height="1000" loading="lazy" decoding="async"></button>
</div>
<button type="button" class="zm-pfeil" data-zm-schritt="1" aria-label="Nächstes Bild">→</button>
<p class="zm-hinweis"><span class="touch">Doppeltippen oder mit zwei Fingern zoomen, ziehen verschiebt</span><span class="maus">Doppelklick zoomt, ziehen verschiebt, Pfeiltasten in 10-%-Schritten</span></p>
</div>
</dialog>
</div>
<!-- ===== Variante Serie: Lookbook als Spalte, Leuchtkasten ===== -->
<div class="se" data-welt="serie">
<header class="se-kopf"><b>Wiebke Lorenzen</b><nav><span>Serien</span><span>Aufträge</span><span>Kontakt</span></nav></header>
<section class="se-titel"><h1>Moorlicht</h1><p>Teufelsmoor bei Worpswede · Winter 2025/26 · 8 Bilder</p></section>
<ol class="se-spalte" data-se-spalte></ol>
<p class="se-ende">Ende der Serie · Abzüge auf Anfrage</p>
<div class="se-kasten" role="dialog" aria-modal="true" aria-label="Bild groß" data-se-kasten>
<div class="grund" data-se-grund></div>
<div class="se-kasten-kopf"><span data-se-zaehler>01 / 08</span><button type="button" data-se-zu>Schließen</button></div>
<div class="se-kasten-zone" data-se-zone><div class="se-slot" data-se-slot></div></div>
<div class="se-kasten-fuss"><button type="button" data-se-zurueck aria-label="Vorheriges Bild">←</button><b data-se-titel></b><button type="button" data-se-vor aria-label="Nächstes Bild">→</button></div>
<p class="wisch" style="padding-bottom:12px">Wischen zum Blättern · nach unten zum Schließen</p>
</div>
</div>
<div class="pult">
<div class="gruppe gruppe-v" role="group" aria-label="Variante"><span>Variante</span>
<button type="button" data-v-wahl="basis" aria-pressed="true">Raster</button>
<button type="button" data-v-wahl="produkt" aria-pressed="false">Produkt</button>
<button type="button" data-v-wahl="serie" aria-pressed="false">Serie</button>
<button type="button" data-v-wahl="zoom" aria-pressed="false">Zoom</button>
</div>
<div class="gruppe" role="group" aria-label="Übergang" data-teil="basis"><span>Übergang</span>
<button type="button" data-technik="flip" aria-pressed="true">Flip</button>
<button type="button" data-technik="vt" aria-pressed="false">View Transition</button>
<button type="button" data-technik="kopien" aria-pressed="false">Kopien</button>
</div>
<div class="gruppe" role="group" aria-label="Ziel" data-teil="basis"><span>Ziel</span>
<button type="button" data-ziel="vollbild" aria-pressed="true">Vollbild</button>
<button type="button" data-ziel="panel" aria-pressed="false">Panel</button>
</div>
<p class="werte" data-werte></p>
</div>
<svg class="zeiger" data-zeiger viewBox="0 0 22 22" aria-hidden="true"><path d="M3 2 19 11.2l-7.2 1.5-3.4 6.8Z" fill="#231f20" stroke="#fff" stroke-width="1.4" stroke-linejoin="round"/></svg>
<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>
<script>
(() => {
gsap.registerPlugin(Flip);
const html = document.documentElement;
const reduziert = () => html.classList.contains('reduziert');
const telefon = matchMedia('(max-width: 640px)');
// Alle Werte an einer Stelle (Messwerte: Codrops Repeating Image Transition)
const W = {
kurve: 'power2.inOut', // Bild, Panel, Nachbarn: cubic-bezier(0.65, 0, 0.35, 1)
bild: 0.7, // Bild wächst in 700 ms
nachbar: 0.3, nachbarStagger: 0.3, nachbarScale: 0.8, // Distanz normalisiert × 0,3 s
text: 1.0, textY: 25, textKurve: 'expo.out', textStart: 0.6, textStagger: 0.06,
textAus: 0.25, // UI-Dauer laut Rezept
kopien: 6, kopieDauer: 0.35, kopieAbstand: 0.05,
wechsel: 0.5, // Blättern im Detail: Überblendung
fade: 0.5, fadeKurve: 'power1.inOut', // reduzierte Bewegung
wischen: 0.15, // Schwelle: 15 % der Höhe
};
const PROJEKTE = [
{ slug: 'reininghaus-q7', titel: 'Wohnbau Reininghaus Q7', ort: 'Graz-Eggenberg', jahr: '2026', typ: 'Wohnen, 84 geförderte Wohnungen', bauherr: 'Wohnbaugenossenschaft Mürztal', flaeche: '9 400 m² BGF', status: 'Im Bau, Bezug Herbst 2027', text: 'Ein Block um einen offenen Hof. Jede Wohnung hat eine Loggia, tief genug für einen Tisch. Der Kalkputz der Fassade altert ruhig mit.' },
{ slug: 'bibliothek-gleisdorf', titel: 'Stadtbibliothek Gleisdorf', ort: 'Gleisdorf', jahr: '2025', typ: 'Kultur, Umbau einer Markthalle', bauherr: 'Stadtgemeinde Gleisdorf', flaeche: '1 850 m² BGF', status: 'Fertiggestellt, 1. Preis Wettbewerb 2021', text: 'Die alte Markthalle bleibt stehen. Zwei Oberlichtbänder holen Tageslicht bis an die Regale, die runden Stützen tragen das neue Dach.' },
{ slug: 'atelierhaus-lend', titel: 'Atelierhaus Lend', ort: 'Graz-Lend', jahr: '2024', typ: 'Umbau, Arbeiten und Wohnen', bauherr: 'privat', flaeche: '640 m² BGF', status: 'Fertiggestellt', text: 'Ein Gründerzeithaus in der Zeile bekommt ein Atelierfenster über zwei Geschosse. Dahinter: Werkstätten unten, Wohnungen oben.' },
{ slug: 'haus-ruckerlberg', titel: 'Haus am Ruckerlberg', ort: 'Graz-Waltendorf', jahr: '2023', typ: 'Wohnen, Einfamilienhaus', bauherr: 'privat', flaeche: '260 m² BGF', status: 'Fertiggestellt', text: 'Ein Kubus aus Dämmbeton am Hang. Das große Fenster rahmt den Blick nach Westen, der Rest des Hauses bleibt still.' },
{ slug: 'uferdach-mur', titel: 'Uferdach an der Mur', ort: 'Graz-Gries', jahr: '2022', typ: 'Öffentlicher Raum, Kiosk und Bootsverleih', bauherr: 'Stadt Graz, Abteilung für Grünraum', flaeche: '180 m² BGF', status: 'Fertiggestellt', text: 'Ein Dach von 30 Metern Länge auf zwölf schlanken Stützen. Die Stufen führen bis ans Wasser, im Sommer wird das Dach zur Bühne.' },
{ slug: 'aufbahrungshalle-st-peter', titel: 'Aufbahrungshalle St. Peter', ort: 'Graz-St. Peter', jahr: '2019', typ: 'Sakralbau', bauherr: 'Pfarre St. Peter', flaeche: '320 m² BGF', status: 'Fertiggestellt', text: 'Ein weißer Raum unter einem steilen Dach. Ein einziger schmaler Schlitz bringt Licht auf den Sarg, die Birken bleiben stehen.' },
{ slug: 'volksschule-liebenau', titel: 'Volksschule Liebenau', ort: 'Graz-Liebenau', jahr: '2021', typ: 'Bildung, zwölf Klassen', bauherr: 'Stadt Graz', flaeche: '4 200 m² BGF', status: 'Fertiggestellt', text: 'Zwei lange Geschosse in Holz, jede Klasse mit eigenem Zugang ins Freie. Die Lamellen beschatten im Sommer und lassen im Winter Sonne herein.' },
{ slug: 'weingut-harrer', titel: 'Weingut Harrer', ort: 'Kitzeck im Sausal', jahr: '2018', typ: 'Gewerbe, Keller und Verkostung', bauherr: 'Familie Harrer', flaeche: '1 100 m² BGF', status: 'Fertiggestellt', text: 'Der Keller liegt im Hang, nur der Verkostungsraum schaut heraus. Von dort reicht der Blick über die Rebzeilen bis nach Slowenien.' },
];
const raster = document.querySelector('[data-raster]');
const einleitung = document.querySelector('.einleitung');
const kacheln = [...raster.querySelectorAll('.kachel')];
const rahmen = kacheln.map(k => k.querySelector('.rahmen'));
const bilder = kacheln.map(k => k.querySelector('.bild'));
const knoepfe = kacheln.map(k => k.querySelector('.oeffnen'));
const detail = document.querySelector('[data-detail]');
const flaeche = document.querySelector('[data-flaeche]');
const zone = document.querySelector('[data-zone]');
const slot = document.querySelector('[data-slot]');
const info = document.querySelector('[data-info]');
const zuKnopf = document.querySelector('[data-zu]');
const F = {
zaehler: document.querySelector('[data-zaehler]'), titel: document.querySelector('[data-titel]'),
ortjahr: document.querySelector('[data-ortjahr]'), daten: document.querySelector('[data-daten]'), text: document.querySelector('[data-text]'),
};
const technikKnoepfe = [...document.querySelectorAll('[data-technik]')];
const zielKnoepfe = [...document.querySelectorAll('[data-ziel]')];
const werte = document.querySelector('[data-werte]');
const zeiger = document.querySelector('[data-zeiger]');
let technik = 'flip', ziel = 'vollbild', aktiv = -1, busy = false, offen = false;
const vtMoeglich = !!document.startViewTransition;
knoepfe.forEach((b, i) => b.setAttribute('aria-label', `${PROJEKTE[i].titel}, ${PROJEKTE[i].ort}, ${PROJEKTE[i].jahr}: Detail öffnen`));
function zeigeWerte() {
if (typeof WERTE_WELT !== 'undefined' && html.dataset.v && html.dataset.v !== 'basis') { werte.innerHTML = reduziert() ? 'Reduziert <b>Überblendung 500 ms</b>' : WERTE_WELT[html.dataset.v]; return; }
werte.innerHTML = reduziert()
? 'Reduziert <b>Überblendung 500 ms</b>'
: technik === 'kopien'
? `Kopien <b>6 × 350 ms</b> · Abstand <b>50 ms</b> · <b>power2.inOut</b>`
: `${technik === 'vt' ? (vtMoeglich ? 'startViewTransition' : 'kein VT, Flip') : 'Flip.from'} · <b>700 ms power2.inOut</b> · Nachbarn <b>× 0,3 s</b> · Text <b>y 25, 1 s expo</b>`;
}
// ---- Inhalt der Detailebene
function fuellen(i) {
const p = PROJEKTE[i];
F.zaehler.textContent = `${String(i + 1).padStart(2, '0')} / ${String(PROJEKTE.length).padStart(2, '0')}`;
F.titel.textContent = p.titel;
F.ortjahr.innerHTML = `${p.ort} • <i>${p.jahr}</i>`;
F.daten.innerHTML = [['Typologie', p.typ], ['Bauherr', p.bauherr], ['Fläche', p.flaeche], ['Stand', p.status]]
.map(([a, b]) => `<dt><span class="maske"><span>${a}</span></span></dt><dd><span class="maske"><span>${b}</span></span></dd>`).join('');
F.text.textContent = p.text;
}
const textZeilen = () => [...info.querySelectorAll('.maske > span'), ...info.querySelectorAll('.blaettern')];
// ---- Ziel: Vollbild oder Panel an der Gegenseite des Klicks
function lageSetzen(i) {
const panel = ziel === 'panel' && !telefon.matches;
detail.classList.toggle('panel', panel);
if (panel) {
const r = rahmen[i].getBoundingClientRect();
detail.classList.toggle('rechts', r.left + r.width / 2 < innerWidth / 2);
} else detail.classList.remove('rechts');
}
function slotSetzen(i) {
const ar = bilder[i].querySelector('svg').viewBox.baseVal;
const q = ar.width / ar.height;
let w, h;
if (telefon.matches) { w = innerWidth; h = Math.min(w / q, innerHeight * 0.62); w = h * q; }
else {
const z = zone.getBoundingClientRect();
w = z.width; h = w / q;
if (h > z.height) { h = z.height; w = h * q; }
}
slot.style.width = w + 'px'; slot.style.height = h + 'px';
slot.style.marginInline = telefon.matches ? 'auto' : '';
}
// ---- Distanzbasierter Versatz der Nachbarn (normalisiert × 0,3 s)
function abstaende(i) {
const m = r => [r.left + r.width / 2, r.top + r.height / 2];
const [x0, y0] = m(rahmen[i].getBoundingClientRect());
const d = kacheln.map(k => { const [x, y] = m(k.getBoundingClientRect()); return Math.hypot(x - x0, y - y0); });
const max = Math.max(...d) || 1;
return d.map(v => v / max * W.nachbarStagger);
}
// ---- Text: Masken-Reveal y 25 → 0, 1 s expo.out, erst wenn das Bild fast steht
function textEin(verz) {
gsap.killTweensOf(textZeilen());
gsap.set(info, { opacity: 1 });
if (reduziert()) return gsap.fromTo(textZeilen(), { y: 0, opacity: 0 }, { opacity: 1, duration: W.fade, ease: W.fadeKurve, delay: verz });
return gsap.fromTo(textZeilen(), { y: W.textY, opacity: 0 }, { y: 0, opacity: 1, duration: W.text, ease: W.textKurve, stagger: W.textStagger, delay: verz });
}
function textVerstecken() { gsap.killTweensOf(textZeilen()); gsap.set(textZeilen(), { opacity: 0 }); }
// ---- Öffnen
function oeffnen(i, sofort = false) {
if (busy || offen) return;
busy = true; offen = true; aktiv = i;
fuellen(i); textVerstecken();
html.classList.add('offen-seite');
detail.classList.add('an');
lageSetzen(i); slotSetzen(i);
const panel = detail.classList.contains('panel');
const d = abstaende(i);
history.replaceState(null, '', '#' + PROJEKTE[i].slug);
const fertig = () => { busy = false; if (!html.classList.contains('vorfuehrung')) zuKnopf.focus({ preventScroll: true }); };
if (sofort || reduziert()) {
slot.appendChild(bilder[i]);
gsap.set([einleitung, ...kacheln], { opacity: 0 });
if (panel) gsap.set(flaeche, { clipPath: 'inset(0% 0% 0% 0%)' });
if (sofort) { gsap.set(textZeilen(), { opacity: 1, y: 0 }); gsap.set(info, { opacity: 1 }); return fertig(); }
gsap.fromTo(detail, { opacity: 0 }, { opacity: 1, duration: W.fade, ease: W.fadeKurve });
gsap.fromTo(raster, { opacity: 1 }, { opacity: 0, duration: W.fade, ease: W.fadeKurve, onComplete: () => gsap.set(raster, { opacity: 1 }) });
gsap.set(textZeilen(), { opacity: 1, y: 0 });
return gsap.delayedCall(W.fade, fertig);
}
// Panel wischt von seiner Seite herein (gleiche Kurve, gleiche Dauer)
if (panel) {
const rechts = detail.classList.contains('rechts');
gsap.fromTo(flaeche, { clipPath: rechts ? 'inset(0% 0% 0% 100%)' : 'inset(0% 100% 0% 0%)' }, { clipPath: 'inset(0% 0% 0% 0%)', duration: W.bild, ease: W.kurve });
}
if (technik === 'vt' && vtMoeglich) return vtOeffnen(i, d, fertig);
// Nachbarn schrumpfen und blenden aus, je nach Abstand verzögert
kacheln.forEach((k, j) => gsap.to(k, { opacity: 0, scale: W.nachbarScale, duration: W.nachbar, ease: W.kurve, delay: d[j] }));
gsap.to(einleitung, { opacity: 0, duration: W.nachbar, ease: W.kurve });
if (technik === 'kopien') {
const dauer = W.kopieAbstand * (W.kopien - 1) + W.kopieDauer * 2;
kopienFahrt(rahmen[i], slot, bilder[i], () => { slot.appendChild(bilder[i]); });
textEin(dauer - 0.1);
return gsap.delayedCall(dauer, fertig);
}
const stand = Flip.getState(bilder[i]);
slot.appendChild(bilder[i]);
Flip.from(stand, { duration: W.bild, ease: W.kurve, absolute: true, zIndex: 25 });
textEin(W.textStart);
gsap.delayedCall(W.bild, fertig);
}
// ---- Schließen: alles läuft zurück zur Kachel
function schliessen() {
if (busy || !offen) return;
busy = true;
const i = aktiv, d = abstaende(i), panel = detail.classList.contains('panel');
const ende = () => {
offen = false; busy = false;
detail.classList.remove('an', 'panel', 'rechts');
html.classList.remove('offen-seite');
gsap.set([detail, slot, flaeche, ...textZeilen()], { clearProps: 'opacity,transform,clipPath' });
history.replaceState(null, '', location.pathname + location.search);
if (!html.classList.contains('vorfuehrung')) knoepfe[i].focus({ preventScroll: true });
};
if (reduziert()) {
gsap.to(detail, { opacity: 0, duration: W.fade, ease: W.fadeKurve, onComplete: () => { rahmen[i].appendChild(bilder[i]); gsap.set([einleitung, ...kacheln], { opacity: 1, scale: 1 }); gsap.fromTo(raster, { opacity: 0 }, { opacity: 1, duration: W.fade, ease: W.fadeKurve }); ende(); } });
return;
}
gsap.to(textZeilen(), { opacity: 0, duration: W.textAus, ease: W.kurve });
if (panel) {
const rechts = detail.classList.contains('rechts');
gsap.to(flaeche, { clipPath: rechts ? 'inset(0% 0% 0% 100%)' : 'inset(0% 100% 0% 0%)', duration: W.bild, ease: W.kurve, delay: W.textAus * 0.6 });
}
gsap.delayedCall(W.textAus * 0.6, () => {
if (technik === 'vt' && vtMoeglich) return vtSchliessen(i, d, ende);
kacheln.forEach((k, j) => gsap.to(k, { opacity: 1, scale: 1, duration: W.nachbar, ease: W.kurve, delay: W.bild * 0.4 + d[j] }));
gsap.to(einleitung, { opacity: 1, duration: W.nachbar, ease: W.kurve, delay: W.bild * 0.4 });
if (technik === 'kopien') {
kopienFahrt(slot, rahmen[i], bilder[i], () => { rahmen[i].appendChild(bilder[i]); });
return gsap.delayedCall(W.kopieAbstand * (W.kopien - 1) + W.kopieDauer * 2, ende);
}
const stand = Flip.getState(bilder[i]);
rahmen[i].appendChild(bilder[i]);
Flip.from(stand, { duration: W.bild, ease: W.kurve, absolute: true, zIndex: 25 });
gsap.delayedCall(W.bild, ende);
});
}
// ---- Native View Transition (Namen nur während des Übergangs)
const vtStil = document.createElement('style'); document.head.appendChild(vtStil);
function vtNamen(i, d, an) {
bilder[i].style.viewTransitionName = an ? 'projektbild' : '';
kacheln.forEach((k, j) => { k.style.viewTransitionName = an ? `kachel-${j}` : ''; k.style.viewTransitionClass = an ? 'kachel' : ''; });
einleitung.style.viewTransitionName = an ? 'einleitung' : ''; einleitung.style.viewTransitionClass = an ? 'kachel' : '';
vtStil.textContent = an ? d.map((v, j) => `:root.vt-auf::view-transition-old(kachel-${j}),:root.vt-zu::view-transition-new(kachel-${j}){animation-delay:${Math.round(v * 1000)}ms}`).join('') : '';
}
function vtOeffnen(i, d, fertig) {
vtNamen(i, d, true);
html.classList.add('vt-auf');
const vt = document.startViewTransition(() => { slot.appendChild(bilder[i]); gsap.set([einleitung, ...kacheln], { opacity: 0 }); });
textEin(W.textStart);
vt.finished.finally(() => { vtNamen(i, d, false); html.classList.remove('vt-auf'); fertig(); });
}
function vtSchliessen(i, d, ende) {
vtNamen(i, d.map(v => v + W.bild * 0.4), true);
html.classList.add('vt-zu');
const vt = document.startViewTransition(() => {
rahmen[i].appendChild(bilder[i]); gsap.set([einleitung, ...kacheln], { opacity: 1, scale: 1 });
});
vt.finished.finally(() => { vtNamen(i, d, false); html.classList.remove('vt-zu'); ende(); });
}
// ---- Kopien: 6 Bildkopien wandern gestaffelt mit clip-path vom Start zum Ziel
function kopienFahrt(von, nach, bild, amEnde) {
const a = von.getBoundingClientRect(), b = nach.getBoundingClientRect();
const svg = bild.querySelector('svg');
bild.style.visibility = 'hidden';
const kopien = [];
const tlk = gsap.timeline({ onComplete: () => { amEnde(); bild.style.visibility = ''; kopien.forEach(k => k.remove()); } });
for (let s = 0; s < W.kopien; s++) {
const t = s / (W.kopien - 1);
const k = document.createElement('div'); k.className = 'kopie';
k.style.left = a.left + (b.left - a.left) * t + 'px'; k.style.top = a.top + (b.top - a.top) * t + 'px';
k.style.width = a.width + (b.width - a.width) * t + 'px'; k.style.height = a.height + (b.height - a.height) * t + 'px';
const c = svg.cloneNode(true); c.removeAttribute('role'); c.removeAttribute('aria-label'); c.setAttribute('aria-hidden', 'true');
k.appendChild(c); document.body.appendChild(k); kopien.push(k);
tlk.fromTo(k, { clipPath: 'inset(100% 0% 0% 0%)' }, { clipPath: 'inset(0% 0% 0% 0%)', duration: W.kopieDauer, ease: W.kurve }, s * W.kopieAbstand);
if (s < W.kopien - 1) tlk.to(k, { clipPath: 'inset(0% 0% 100% 0%)', duration: W.kopieDauer, ease: W.kurve }, s * W.kopieAbstand + W.kopieDauer);
}
}
// ---- Blättern im offenen Zustand (Pfeiltasten, Knöpfe)
function blaettern(r) {
if (busy || !offen) return;
busy = true;
const alt = aktiv, neu = (aktiv + r + PROJEKTE.length) % PROJEKTE.length;
gsap.to([bilder[alt], ...textZeilen()], { opacity: 0, duration: W.textAus, ease: W.kurve, onComplete: () => {
rahmen[alt].appendChild(bilder[alt]); gsap.set(bilder[alt], { clearProps: 'opacity' });
aktiv = neu; fuellen(neu); textVerstecken(); slotSetzen(neu);
slot.appendChild(bilder[neu]);
gsap.fromTo(bilder[neu], { opacity: 0 }, { opacity: 1, duration: W.wechsel, ease: W.kurve });
textEin(0.1);
history.replaceState(null, '', '#' + PROJEKTE[neu].slug);
gsap.delayedCall(W.wechsel, () => { busy = false; });
} });
}
knoepfe.forEach((b, i) => b.addEventListener('click', () => oeffnen(i)));
zuKnopf.addEventListener('click', schliessen);
document.querySelector('[data-vor]').addEventListener('click', () => blaettern(1));
document.querySelector('[data-zurueck]').addEventListener('click', () => blaettern(-1));
document.querySelectorAll('.kopf a').forEach(a => a.addEventListener('click', e => e.preventDefault()));
addEventListener('keydown', e => {
if (!offen) return;
if (e.key === 'Escape') { e.preventDefault(); schliessen(); }
else if (e.key === 'ArrowRight') { e.preventDefault(); blaettern(1); }
else if (e.key === 'ArrowLeft') { e.preventDefault(); blaettern(-1); }
else if (e.key === 'Tab') { // Fokus bleibt im Dialog
const f = [...detail.querySelectorAll('button')];
const idx = f.indexOf(document.activeElement);
if (e.shiftKey && idx <= 0) { e.preventDefault(); f[f.length - 1].focus(); }
else if (!e.shiftKey && idx === f.length - 1) { e.preventDefault(); f[0].focus(); }
}
});
addEventListener('resize', () => { if (offen && !busy) { lageSetzen(aktiv); slotSetzen(aktiv); } });
// ---- Mobil: nach unten wischen schließt (Schwelle 15 % der Höhe)
let wisch = null;
zone.addEventListener('pointerdown', e => {
if (!offen || busy || !telefon.matches || detail.scrollTop > 4) return;
wisch = { y: e.clientY, dy: 0 }; zone.setPointerCapture(e.pointerId);
});
zone.addEventListener('pointermove', e => {
if (!wisch) return;
wisch.dy = Math.max(0, e.clientY - wisch.y);
gsap.set(slot, { y: wisch.dy * 0.9 }); gsap.set(info, { opacity: 1 - wisch.dy / (innerHeight * 0.3) });
});
const wischEnde = () => {
if (!wisch) return;
const weit = wisch.dy > innerHeight * W.wischen; wisch = null;
if (weit) { gsap.set(slot, { y: 0 }); schliessen(); }
else { gsap.to(slot, { y: 0, duration: 0.3, ease: W.kurve }); gsap.to(info, { opacity: 1, duration: 0.3, ease: W.kurve }); }
};
zone.addEventListener('pointerup', wischEnde); zone.addEventListener('pointercancel', wischEnde);
// ---- Varianten
function setzeTechnik(t) { technik = t; technikKnoepfe.forEach(b => b.setAttribute('aria-pressed', String(b.dataset.technik === t))); zeigeWerte(); }
function setzeZiel(z) { ziel = z; zielKnoepfe.forEach(b => b.setAttribute('aria-pressed', String(b.dataset.ziel === z))); }
technikKnoepfe.forEach(b => b.addEventListener('click', () => setzeTechnik(b.dataset.technik)));
zielKnoepfe.forEach(b => b.addEventListener('click', () => setzeZiel(b.dataset.ziel)));
new MutationObserver(zeigeWerte).observe(html, { attributes: true, attributeFilter: ['class'] });
// ---- Vorführung: gezeichneter Zeiger öffnet und schließt reihum
const ABLAUF = [[3, 'flip', 'vollbild'], [0, 'kopien', 'panel'], [5, 'vt', 'vollbild'], [6, 'flip', 'panel'], [1, 'kopien', 'vollbild'], [4, 'vt', 'panel']];
let uhren = [], schritt = 0, laeuft = false;
const spaeter = (fn, ms) => uhren.push(setTimeout(fn, ms));
function zeigeAuf(el, fx = 0.5, fy = 0.5) { const r = el.getBoundingClientRect(); zeiger.style.transform = `translate(${r.left + r.width * fx}px, ${r.top + r.height * fy}px)`; }
function klick(fn) { zeiger.classList.add('drueckt'); fn(); spaeter(() => zeiger.classList.remove('drueckt'), 160); }
function runde() {
if (!laeuft) return;
const [i, t, z] = ABLAUF[schritt]; schritt = (schritt + 1) % ABLAUF.length;
setzeTechnik(t); setzeZiel(z);
zeigeAuf(rahmen[i], 0.55, 0.6);
spaeter(() => klick(() => oeffnen(i)), 800);
spaeter(() => zeigeAuf(zuKnopf, 0.4, 0.6), 3300);
spaeter(() => klick(schliessen), 4100);
spaeter(runde, 5900);
}
function grundzustand() {
uhren.forEach(clearTimeout); uhren = []; laeuft = false;
gsap.globalTimeline.getChildren(true, true, true).forEach(t => t.progress(1));
if (offen) {
rahmen[aktiv].appendChild(bilder[aktiv]);
offen = false;
detail.classList.remove('an', 'panel', 'rechts'); html.classList.remove('offen-seite');
}
document.querySelectorAll('.kopie').forEach(k => k.remove());
bilder.forEach(b => { b.style.visibility = ''; });
gsap.set([einleitung, ...kacheln, ...bilder, raster, detail, slot, flaeche, info, ...textZeilen()], { clearProps: 'all' });
busy = false;
}
function neustart() { html.classList.remove('vorfuehrung'); grundzustand(); weltReset(); }
function vorfuehren() {
grundzustand(); weltReset();
html.classList.add('vorfuehrung');
zeiger.style.transform = `translate(${innerWidth * 0.5}px, ${innerHeight * 0.92}px)`;
schritt = 0; laeuft = true;
spaeter(welt === 'basis' ? runde : rundeWelt, 400);
}
function pause() { uhren.forEach(clearTimeout); uhren = []; laeuft = false; }
function weiter() {
if (laeuft || !html.classList.contains('vorfuehrung')) return;
laeuft = true;
if (welt !== 'basis') { weltReset(); return rundeWelt(); }
if (offen) { klick(schliessen); spaeter(runde, 1800); } else runde();
}
/* =====================================================================
Varianten nach Regel 13: ?v=basis | produkt | serie
Beide neuen Varianten nutzen dasselbe Prinzip: Flip.getState → Bild umhängen → Flip.from.
===================================================================== */
const WELTEN = ['basis', 'produkt', 'serie', 'zoom'];
let welt = 'basis';
const WERTE_WELT = {
produkt: 'Flip <b>700 ms power3.out</b> · Daumen <b>300 ms</b> Überblendung',
serie: 'Flip <b>800 ms power1.inOut</b> · Blättern <b>250 / 500 ms</b> Deckkraft',
zoom: 'Zoom <b>1 · 1,5 · 2 · 3</b> · Lupe <b>× 2,5</b> · Öffnen <b>300 ms</b> / Schließen <b>200 ms</b>',
};
// ---- Bilder der Varianten (eigene SVG, keine Fotos)
const FLASCHE = (x, y, s, etikett = true) => `<g transform="translate(${x} ${y}) scale(${s})">
<ellipse cx="0" cy="372" rx="118" ry="16" fill="#3b2a1c" opacity=".14"/>
<rect x="-26" y="-150" width="52" height="96" rx="10" fill="#1d1a18"/><rect x="-20" y="-140" width="10" height="76" rx="5" fill="#3a3633"/>
<rect x="-40" y="-62" width="80" height="44" rx="6" fill="#141210"/>
<path d="M-30 -20h60v20c48 10 74 40 74 92v236c0 24-18 42-42 42h-124c-24 0-42-18-42-42V92c0-52 26-82 74-92z" fill="#9a5a1e"/>
<path d="M-30 -20h60v20c48 10 74 40 74 92v236c0 24-18 42-42 42h-124c-24 0-42-18-42-42V92c0-52 26-82 74-92z" fill="url(#gl-glanz)"/>
<path d="M-82 120c0-40 10-60 22-70" stroke="#f3c98c" stroke-width="8" stroke-linecap="round" opacity=".55" fill="none"/>
${etikett ? `<rect x="-78" y="150" width="156" height="150" rx="4" fill="#f4efe6"/>
<text x="0" y="196" text-anchor="middle" font-family="Zalando Sans, sans-serif" font-size="15" letter-spacing="3" fill="#121010">SALBEIGRUND</text>
<path d="M-40 212h80" stroke="#121010" stroke-width="1"/>
<text x="0" y="246" text-anchor="middle" font-family="Zalando Sans, sans-serif" font-size="22" fill="#121010">Nachtöl</text>
<text x="0" y="272" text-anchor="middle" font-family="Fragment Mono, monospace" font-size="11" fill="#8c857b">No. 3 · 30 ml</text>` : ''}
</g>`;
const DEF_GLAS = `<defs><linearGradient id="gl-glanz" x1="0" x2="1"><stop offset="0" stop-color="#fff" stop-opacity=".18"/><stop offset=".3" stop-color="#fff" stop-opacity="0"/><stop offset=".85" stop-color="#000" stop-opacity=".22"/></linearGradient>
<radialGradient id="gl-licht" cx=".3" cy=".25" r=".9"><stop offset="0" stop-color="#f7f2ea"/><stop offset="1" stop-color="#e3ddd2"/></radialGradient></defs>`;
const PRODUKT_BILDER = [
{ alt: 'Braunglasflasche mit Pipette und cremefarbenem Etikett vor hellem Grund', svg: `<svg viewBox="0 0 800 1000">${DEF_GLAS}<rect width="800" height="1000" fill="url(#gl-licht)"/><rect y="760" width="800" height="240" fill="#d9d1c3"/>${FLASCHE(400, 420, 1.15)}</svg>` },
{ alt: 'Nahaufnahme der Pipette, an der ein goldener Tropfen hängt', svg: `<svg viewBox="0 0 800 1000">${DEF_GLAS}<rect width="800" height="1000" fill="#e9e2d6"/><circle cx="560" cy="250" r="190" fill="#f3ecdf"/>
<rect x="352" y="-20" width="96" height="300" rx="18" fill="#1d1a18"/><path d="M372 280h56l-6 330c0 18-8 30-22 30s-22-12-22-30z" fill="#e8dccb" opacity=".75"/><path d="M380 300h40l-4 300c0 10-6 18-16 18s-16-8-16-18z" fill="#c98a3a" opacity=".85"/>
<path d="M400 660c-26 40-40 70-40 94a40 40 0 0 0 80 0c0-24-14-54-40-94z" fill="#d48a2a"/><path d="M388 720c-6 14-8 24-6 34" stroke="#ffe0a8" stroke-width="7" stroke-linecap="round" fill="none"/>
<ellipse cx="400" cy="930" rx="120" ry="22" fill="#c2b49d" opacity=".5"/></svg>` },
{ alt: 'Goldene Öltropfen in Ringen auf heller Fläche, von oben gesehen', svg: `<svg viewBox="0 0 800 1000"><rect width="800" height="1000" fill="#efe6d6"/>
<g fill="#d99a3c" opacity=".9"><circle cx="300" cy="380" r="150"/><circle cx="560" cy="640" r="96"/><circle cx="250" cy="760" r="58"/><circle cx="600" cy="300" r="40"/><circle cx="470" cy="860" r="26"/></g>
<g fill="none" stroke="#f6d39b" stroke-width="10" opacity=".8"><path d="M210 300a120 120 0 0 1 120-60"/><path d="M500 590a70 70 0 0 1 60-30"/><path d="M220 730a40 40 0 0 1 30-14"/></g>
<g fill="#b56f1e" opacity=".35"><circle cx="330" cy="420" r="120"/><circle cx="575" cy="660" r="70"/></g></svg>` },
{ alt: 'Hagebutten, Sanddornbeeren und Blätter flach ausgelegt', svg: `<svg viewBox="0 0 800 1000"><rect width="800" height="1000" fill="#e7dfd1"/>
<g fill="#7c8c63"><path d="M120 200c80-60 180-40 230 30-90 30-170 20-230-30z"/><path d="M520 820c60-80 170-90 230-40-70 60-150 70-230 40z"/><path d="M560 160c40-50 120-60 170-20-60 40-120 50-170 20z"/></g>
<g fill="#b8331f"><ellipse cx="300" cy="460" rx="58" ry="74"/><ellipse cx="430" cy="400" rx="52" ry="66" transform="rotate(20 430 400)"/><ellipse cx="380" cy="570" rx="54" ry="70" transform="rotate(-15 380 570)"/></g>
<g fill="#2d221a"><path d="M300 386l-10-22 10 6 10-6z"/><path d="M445 336l-4-24 12 10 10-4z"/></g>
<g fill="#ffffff" opacity=".35"><ellipse cx="282" cy="430" rx="12" ry="20"/><ellipse cx="414" cy="374" rx="10" ry="16"/><ellipse cx="362" cy="540" rx="11" ry="18"/></g>
<g fill="#e8892a">${[[560,500],[590,530],[620,505],[575,565],[612,580],[645,545],[540,600],[600,615],[665,600],[630,650]].map(([x,y])=>`<circle cx="${x}" cy="${y}" r="17"/>`).join('')}</g>
<path d="M520 470c60 40 110 110 150 200" stroke="#6b5a3e" stroke-width="5" fill="none"/></svg>` },
{ alt: 'Faltschachtel und Flasche nebeneinander auf einem Holzbrett', svg: `<svg viewBox="0 0 800 1000">${DEF_GLAS}<rect width="800" height="1000" fill="#ece5d9"/><rect y="700" width="800" height="300" fill="#c9a77d"/><path d="M0 700h800" stroke="#a9875e" stroke-width="4"/>
<g><path d="M120 260h230v440H120z" fill="#f4efe6"/><path d="M350 260l50-30v440l-50 30z" fill="#ddd5c6"/><path d="M120 260l50-30h230l-50 30z" fill="#fbf8f2"/>
<text x="235" y="400" text-anchor="middle" font-family="Zalando Sans, sans-serif" font-size="16" letter-spacing="3" fill="#121010">SALBEIGRUND</text><text x="235" y="450" text-anchor="middle" font-family="Zalando Sans, sans-serif" font-size="28" fill="#121010">Nachtöl</text><text x="235" y="480" text-anchor="middle" font-family="Fragment Mono, monospace" font-size="12" fill="#8c857b">Hagebutte · Sanddorn</text></g>
${FLASCHE(560, 330, .95)}</svg>` },
];
// Serie „Moorlicht“: acht Bilder in verschiedenen Formaten
const MOOR = [
{ t: 'Nebelbank am Hamme-Deich', f: '3/2', svg: `<svg viewBox="0 0 900 600"><defs><linearGradient id="m1" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#d9d4c7"/><stop offset=".6" stop-color="#ebe6d9"/><stop offset="1" stop-color="#9c9a83"/></linearGradient></defs><rect width="900" height="600" fill="url(#m1)"/><path d="M0 380c160-20 300-14 450-4s300 10 450-6v230H0z" fill="#8a8b72"/><path d="M0 410c200-12 420-8 900 6v184H0z" fill="#6d6f58"/><rect y="330" width="900" height="60" fill="#f2efe6" opacity=".7"/><g fill="#4d4f3e"><path d="M640 400v-70M630 345l10-18 10 18"/><rect x="636" y="330" width="8" height="74"/><rect x="700" y="350" width="5" height="54"/></g><circle cx="250" cy="200" r="46" fill="#f6f2e8" opacity=".9"/></svg>` },
{ t: 'Birke im ersten Frost', f: '4/5', svg: `<svg viewBox="0 0 640 800"><rect width="640" height="800" fill="#e4e3dd"/><rect y="560" width="640" height="240" fill="#c9cabd"/><g fill="#f7f6f1"><rect x="300" y="60" width="34" height="520"/></g><g fill="#3a3a33"><rect x="300" y="140" width="34" height="6"/><rect x="306" y="230" width="22" height="5"/><rect x="300" y="330" width="34" height="7"/><rect x="310" y="430" width="18" height="5"/><rect x="300" y="500" width="34" height="6"/></g><g stroke="#8e8d80" stroke-width="3" fill="none"><path d="M317 180c-60-40-120-60-190-70M317 260c70-50 130-70 210-80M317 120c40-40 80-60 140-70M317 360c-70-20-140-20-220 0"/></g><g fill="#fff">${Array.from({length:40},(_,i)=>`<circle cx="${(i*97)%640}" cy="${600+(i*53)%190}" r="2.5"/>`).join('')}</g></svg>` },
{ t: 'Torfstich mit Spiegelung', f: '1/1', svg: `<svg viewBox="0 0 700 700"><rect width="700" height="700" fill="#cfc7b6"/><rect y="250" width="700" height="450" fill="#5e5442"/><rect x="80" y="330" width="540" height="260" fill="#2a2e2f"/><path d="M80 330h540l-30 260H110z" fill="#3c4446"/><rect x="80" y="330" width="540" height="40" fill="#b9b7ad" opacity=".5"/><g fill="#a19579"><rect x="0" y="230" width="700" height="24"/></g><g stroke="#8b8467" stroke-width="3"><path d="M120 250v-60M140 250v-40M560 250v-70M580 250v-45"/></g><path d="M300 460h120" stroke="#c9c6bb" stroke-width="3" opacity=".6"/></svg>` },
{ t: 'Bohlenweg zum Weyerberg', f: '16/9', svg: `<svg viewBox="0 0 960 540"><rect width="960" height="540" fill="#dcd3bf"/><path d="M0 300c200-30 400-40 960-20v260H0z" fill="#a49a72"/><path d="M0 330c300-10 600-6 960 10v200H0z" fill="#8b8357"/><path d="M430 300h100l210 240H220z" fill="#7b5e3f"/><g stroke="#5b432b" stroke-width="3">${Array.from({length:12},(_,i)=>{const y=310+i*20,w=50+i*20;return `<path d="M${480-w} ${y}h${w*2}"/>`}).join('')}</g><path d="M560 280c20-60 50-90 90-110" stroke="#5a5a46" stroke-width="5" fill="none"/><circle cx="160" cy="130" r="30" fill="#f4ecd7"/></svg>` },
{ t: 'Kiefer allein', f: '4/5', svg: `<svg viewBox="0 0 640 800"><defs><linearGradient id="m5" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#c9b9a0"/><stop offset="1" stop-color="#efe3cc"/></linearGradient></defs><rect width="640" height="800" fill="url(#m5)"/><rect y="600" width="640" height="200" fill="#7f7a5b"/><rect x="314" y="300" width="14" height="310" fill="#3c3227"/><g fill="#3f4733"><ellipse cx="320" cy="290" rx="120" ry="44"/><ellipse cx="280" cy="230" rx="80" ry="30"/><ellipse cx="360" cy="190" rx="70" ry="26"/><ellipse cx="300" cy="350" rx="60" ry="20"/></g><circle cx="470" cy="520" r="34" fill="#f1b56c" opacity=".9"/></svg>` },
{ t: 'Schilf, gegen das Licht', f: '3/2', svg: `<svg viewBox="0 0 900 600"><rect width="900" height="600" fill="#e8d9b8"/><circle cx="640" cy="260" r="120" fill="#f5e7c4"/><g stroke="#6e5a35" stroke-width="4" stroke-linecap="round" fill="none">${Array.from({length:26},(_,i)=>{const x=40+i*33,h=220+(i*71)%180;return `<path d="M${x} 600q${(i%3-1)*14} -${h/2} ${(i%2?12:-10)} -${h}"/>`}).join('')}</g><g fill="#7a643a">${Array.from({length:10},(_,i)=>`<ellipse cx="${70+i*86}" cy="${340-(i*37)%120}" rx="7" ry="26"/>`).join('')}</g></svg>` },
{ t: 'Moorgraben im Februar', f: '1/1', svg: `<svg viewBox="0 0 700 700"><rect width="700" height="700" fill="#d7d9d4"/><path d="M0 330h700v370H0z" fill="#9aa08d"/><path d="M260 330c40 120 20 240-80 370h260c-60-120-80-250-60-370z" fill="#4f5a5c"/><path d="M290 340c20 100 10 200-50 340" stroke="#c7cdca" stroke-width="5" fill="none" opacity=".6"/><g fill="#f4f5f1" opacity=".85"><path d="M0 330h700v18H0z"/></g><g fill="#6e735f"><rect x="560" y="250" width="6" height="90"/><rect x="590" y="270" width="5" height="70"/></g></svg>` },
{ t: 'Letztes Licht über Worpswede', f: '16/9', svg: `<svg viewBox="0 0 960 540"><defs><linearGradient id="m8" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#6f7891"/><stop offset=".55" stop-color="#e3a86f"/><stop offset=".7" stop-color="#f3d39d"/></linearGradient></defs><rect width="960" height="540" fill="url(#m8)"/><path d="M0 380c120-40 240-60 360-40s180-40 280-50 200 20 320 30v220H0z" fill="#3a3a33"/><path d="M0 420h960v120H0z" fill="#2b2b27"/><rect x="0" y="430" width="960" height="18" fill="#e9b678" opacity=".55"/><g fill="#2f2f2a"><rect x="700" y="300" width="6" height="90"/><path d="M690 310l13-40 13 40z"/></g></svg>` },
];
const $q = (sel, el = document) => el.querySelector(sel);
const fadeK = 'power1.inOut';
// ---------- Produkt ----------
const pd = {
haupt: $q('[data-pd-haupt]'), oeffnen: $q('[data-pd-oeffnen]'), daumen: $q('[data-pd-daumen]'), voll: $q('[data-pd-voll]'),
slot: $q('[data-pd-slot]'), leiste: $q('[data-pd-leiste]'), zaehler: $q('[data-pd-zaehler]'), grund: $q('[data-pd-grund-flaeche]'),
zu: $q('[data-pd-zu]'), zone: $q('[data-pd-zone]'), aktiv: 0, offen: false, busy: false, bild: null,
};
const pdKurve = 'power3.out';
function pdNeuesBild(i) { const d = document.createElement('div'); d.className = 'pd-bild'; d.innerHTML = PRODUKT_BILDER[i].svg; d.setAttribute('role', 'img'); d.setAttribute('aria-label', PRODUKT_BILDER[i].alt); return d; }
pd.daumen.innerHTML = PRODUKT_BILDER.map((b, i) => `<li><button type="button" aria-label="Bild ${i + 1}: ${b.alt}" aria-current="${i === 0}">${b.svg}</button></li>`).join('');
pd.leiste.innerHTML = PRODUKT_BILDER.map((b, i) => `<button type="button" aria-label="Bild ${i + 1}" aria-current="${i === 0}">${b.svg}</button>`).join('');
// SVG-ids in den Kopien eindeutig machen (Verläufe)
let idz = 0;
document.querySelectorAll('.pd-daumen svg, .pd-voll-leiste svg').forEach(svg => { idz++; svg.querySelectorAll('[id]').forEach(el => { const neu = el.id + '-k' + idz; svg.innerHTML = svg.innerHTML.split(`url(#${el.id})`).join(`url(#${neu})`).replace(`id="${el.id}"`, `id="${neu}"`); }); });
pd.bild = pdNeuesBild(0); pd.haupt.prepend(pd.bild);
const pdDaumenKnoepfe = [...pd.daumen.querySelectorAll('button')], pdLeisteKnoepfe = [...pd.leiste.querySelectorAll('button')];
function pdMarkieren() {
pdDaumenKnoepfe.forEach((b, k) => b.setAttribute('aria-current', String(k === pd.aktiv)));
pdLeisteKnoepfe.forEach((b, k) => b.setAttribute('aria-current', String(k === pd.aktiv)));
pd.zaehler.textContent = `${pd.aktiv + 1} / ${PRODUKT_BILDER.length}`;
}
// Bildwechsel ohne Bewegung: neues Bild blendet über das alte (300 ms)
function pdZeige(i) {
if (i === pd.aktiv) return;
pd.aktiv = i; pdMarkieren();
const alt = pd.bild, neu = pdNeuesBild(i);
alt.after(neu); pd.bild = neu;
if (reduziert()) { alt.remove(); return; }
gsap.fromTo(neu, { opacity: 0 }, { opacity: 1, duration: 0.3, ease: fadeK, onComplete: () => alt.remove() });
}
pdDaumenKnoepfe.forEach((b, i) => b.addEventListener('click', () => { if (!pd.offen) pdZeige(i); }));
pdLeisteKnoepfe.forEach((b, i) => b.addEventListener('click', () => pdZeige(i)));
const pdChrom = () => [...pd.voll.querySelectorAll('.pd-voll-kopf, .pd-pfeil, .pd-voll-leiste, .wisch')];
// Slot im Format 4:5 in die freie Fläche einpassen
function pdSlotSetzen() {
const z = pd.zone.getBoundingClientRect(), cs = getComputedStyle(pd.zone);
let w = z.width - parseFloat(cs.paddingLeft) - parseFloat(cs.paddingRight), h = w * 1.25;
if (h > z.height) { h = z.height; w = h * 0.8; }
pd.slot.style.width = w + 'px'; pd.slot.style.height = h + 'px';
}
addEventListener('resize', () => { if (pd.offen && !pd.busy) pdSlotSetzen(); });
function pdAuf() {
if (pd.offen || pd.busy) return;
pd.offen = true; pd.busy = true;
html.classList.add('offen-seite'); pd.voll.classList.add('an'); pdMarkieren(); pdSlotSetzen();
const st = Flip.getState(pd.bild);
pd.slot.appendChild(pd.bild); pd.haupt.querySelectorAll('.pd-bild').forEach(b => b.remove());
const fertig = () => { pd.busy = false; if (!html.classList.contains('vorfuehrung')) pd.zu.focus({ preventScroll: true }); };
if (reduziert()) { gsap.fromTo(pd.voll, { opacity: 0 }, { opacity: 1, duration: 0.5, ease: fadeK }); gsap.set(pd.grund, { opacity: 1 }); gsap.set(pdChrom(), { opacity: 1 }); return gsap.delayedCall(0.5, fertig); }
gsap.to(pd.grund, { opacity: 1, duration: 0.5, ease: fadeK });
gsap.fromTo(pdChrom(), { opacity: 0 }, { opacity: 1, duration: 0.3, ease: fadeK, delay: 0.4 });
Flip.from(st, { duration: 0.7, ease: pdKurve, absolute: true, zIndex: 45 });
gsap.delayedCall(0.7, fertig);
}
function pdZu() {
if (!pd.offen || pd.busy) return;
pd.busy = true;
const ende = () => {
pd.offen = false; pd.busy = false; pd.voll.classList.remove('an'); html.classList.remove('offen-seite');
gsap.set([pd.voll, pd.grund, pd.bild, ...pdChrom()], { clearProps: 'opacity,transform' });
if (!html.classList.contains('vorfuehrung')) pd.oeffnen.focus({ preventScroll: true });
};
if (reduziert()) { gsap.to(pd.voll, { opacity: 0, duration: 0.5, ease: fadeK, onComplete: () => { pd.haupt.prepend(pd.bild); gsap.set(pd.grund, { opacity: 0 }); ende(); } }); return; }
gsap.to(pdChrom(), { opacity: 0, duration: 0.2, ease: fadeK });
gsap.to(pd.grund, { opacity: 0, duration: 0.5, ease: fadeK, delay: 0.15 });
gsap.set(pd.bild, { y: 0 });
const st = Flip.getState(pd.bild);
pd.haupt.prepend(pd.bild);
Flip.from(st, { duration: 0.7, ease: pdKurve, absolute: true, zIndex: 45 });
gsap.delayedCall(0.7, ende);
}
const pdBlaettern = r => pdZeige((pd.aktiv + r + PRODUKT_BILDER.length) % PRODUKT_BILDER.length);
pd.oeffnen.addEventListener('click', () => pdAuf());
pd.zu.addEventListener('click', () => pdZu());
$q('[data-pd-vor]').addEventListener('click', () => pdBlaettern(1));
$q('[data-pd-zurueck]').addEventListener('click', () => pdBlaettern(-1));
$q('[data-ml="15"]').parentElement.addEventListener('click', e => {
const b = e.target.closest('[data-ml]'); if (!b) return;
document.querySelectorAll('[data-ml]').forEach(x => x.setAttribute('aria-pressed', String(x === b)));
const p15 = b.dataset.ml === '15';
$q('[data-pd-preis]').textContent = p15 ? '22,00 €' : '38,00 €';
$q('[data-pd-grund]').textContent = (p15 ? '146,67 €' : '126,67 €') + ' / 100 ml · inkl. MwSt.';
});
// ---------- Serie ----------
const se = { spalte: $q('[data-se-spalte]'), kasten: $q('[data-se-kasten]'), slot: $q('[data-se-slot]'), zone: $q('[data-se-zone]'),
grund: $q('[data-se-grund]'), zaehler: $q('[data-se-zaehler]'), titel: $q('[data-se-titel]'), zu: $q('[data-se-zu]'), aktiv: -1, offen: false, busy: false };
const n2 = i => String(i + 1).padStart(2, '0');
se.spalte.innerHTML = MOOR.map((b, i) => `<li><button type="button" aria-haspopup="dialog" aria-label="${b.t}, Bild ${i + 1} von ${MOOR.length}, groß ansehen"><div class="se-rahmen" style="--f:${b.f}"><div class="se-bild">${b.svg}</div></div><p class="se-unter"><b>${b.t}</b><span>${n2(i)} / ${n2(MOOR.length - 1)}</span></p></button></li>`).join('');
const seKnoepfe = [...se.spalte.querySelectorAll('button')], seRahmen = seKnoepfe.map(b => b.querySelector('.se-rahmen')), seBilder = seKnoepfe.map(b => b.querySelector('.se-bild'));
const seKurve = 'power1.inOut';
const seChrom = () => [...se.kasten.querySelectorAll('.se-kasten-kopf, .se-kasten-fuss, .wisch')];
function seSlot(i) {
const [a, b] = MOOR[i].f.split('/').map(Number), q = a / b, z = se.zone.getBoundingClientRect();
const cs = getComputedStyle(se.zone);
let w = z.width - parseFloat(cs.paddingLeft) - parseFloat(cs.paddingRight) - (innerWidth <= 640 ? 0 : 80), h = w / q;
if (h > z.height - 8) { h = z.height - 8; w = h * q; }
se.slot.style.width = w + 'px'; se.slot.style.height = h + 'px';
}
function seText(i) { se.zaehler.textContent = `${n2(i)} / ${n2(MOOR.length - 1)}`; se.titel.textContent = MOOR[i].t; }
function seAuf(i) {
if (se.offen || se.busy) return;
se.offen = true; se.busy = true; se.aktiv = i;
html.classList.add('offen-seite'); se.kasten.classList.add('an'); seText(i); seSlot(i);
const st = Flip.getState(seBilder[i]);
se.slot.appendChild(seBilder[i]);
const fertig = () => { se.busy = false; if (!html.classList.contains('vorfuehrung')) se.zu.focus({ preventScroll: true }); };
if (reduziert()) { gsap.fromTo(se.kasten, { opacity: 0 }, { opacity: 1, duration: 0.5, ease: fadeK }); gsap.set([se.grund, ...seChrom()], { opacity: 1 }); return gsap.delayedCall(0.5, fertig); }
gsap.to(se.grund, { opacity: 1, duration: 0.6, ease: seKurve });
gsap.fromTo(seChrom(), { opacity: 0 }, { opacity: 1, duration: 0.5, ease: seKurve, delay: 0.5 });
Flip.from(st, { duration: 0.8, ease: seKurve, absolute: true, zIndex: 45 });
gsap.delayedCall(0.8, fertig);
}
// Blättern: nur Deckkraft, das Werk bewegt sich nicht
function seBlaettern(r) {
if (!se.offen || se.busy) return;
se.busy = true;
const alt = se.aktiv, neu = (alt + r + MOOR.length) % MOOR.length;
const tausch = () => {
seRahmen[alt].appendChild(seBilder[alt]); gsap.set(seBilder[alt], { clearProps: 'opacity' });
se.aktiv = neu; seSlot(neu); seText(neu); se.slot.appendChild(seBilder[neu]);
if (reduziert()) { se.busy = false; return; }
gsap.fromTo(seBilder[neu], { opacity: 0 }, { opacity: 1, duration: 0.5, ease: seKurve, onComplete: () => { se.busy = false; } });
};
if (reduziert()) return tausch();
gsap.to(seBilder[alt], { opacity: 0, duration: 0.25, ease: seKurve, onComplete: tausch });
}
// Schließen: Seite springt unsichtbar zur aktuellen Stelle, dann fliegt das Bild in seinen Rahmen
function seZu() {
if (!se.offen || se.busy) return;
se.busy = true;
const i = se.aktiv;
const r = seRahmen[i].getBoundingClientRect();
scrollTo(0, scrollY + r.top + r.height / 2 - innerHeight / 2);
const ende = () => {
se.offen = false; se.busy = false; se.kasten.classList.remove('an'); html.classList.remove('offen-seite');
gsap.set([se.kasten, se.grund, seBilder[i], ...seChrom()], { clearProps: 'opacity,transform' });
if (!html.classList.contains('vorfuehrung')) seKnoepfe[i].focus({ preventScroll: true });
};
if (reduziert()) { gsap.to(se.kasten, { opacity: 0, duration: 0.5, ease: fadeK, onComplete: () => { seRahmen[i].appendChild(seBilder[i]); gsap.set(se.grund, { opacity: 0 }); ende(); } }); return; }
gsap.to(seChrom(), { opacity: 0, duration: 0.2, ease: seKurve });
gsap.to(se.grund, { opacity: 0, duration: 0.6, ease: seKurve, delay: 0.2 });
gsap.set(seBilder[i], { y: 0 });
const st = Flip.getState(seBilder[i]);
seRahmen[i].appendChild(seBilder[i]);
Flip.from(st, { duration: 0.8, ease: seKurve, absolute: true, zIndex: 45 });
gsap.delayedCall(0.8, ende);
}
seKnoepfe.forEach((b, i) => b.addEventListener('click', () => seAuf(i)));
se.zu.addEventListener('click', () => seZu());
$q('[data-se-vor]').addEventListener('click', () => seBlaettern(1));
$q('[data-se-zurueck]').addEventListener('click', () => seBlaettern(-1));
// Tastatur und Fokusfalle für beide Dialoge
addEventListener('keydown', e => {
const d = pd.offen ? { box: pd.voll, zu: pdZu, b: pdBlaettern } : se.offen ? { box: se.kasten, zu: seZu, b: seBlaettern } : null;
if (!d) return;
if (e.key === 'Escape') { e.preventDefault(); d.zu(); }
else if (e.key === 'ArrowRight') { e.preventDefault(); d.b(1); }
else if (e.key === 'ArrowLeft') { e.preventDefault(); d.b(-1); }
else if (e.key === 'Tab') {
const f = [...d.box.querySelectorAll('button')].filter(x => x.offsetParent);
const idx = f.indexOf(document.activeElement);
if (e.shiftKey && idx <= 0) { e.preventDefault(); f[f.length - 1].focus(); }
else if (!e.shiftKey && idx === f.length - 1) { e.preventDefault(); f[0].focus(); }
}
});
// Touch: waagerecht wischen blättert, nach unten ziehen schließt (ab 15 % der Höhe)
function wischen(zone, getBild, offen, blaettern, zu) {
let w = null;
zone.addEventListener('pointerdown', e => { if (!offen() || e.pointerType === 'mouse') return; w = { x: e.clientX, y: e.clientY, dx: 0, dy: 0 }; });
zone.addEventListener('pointermove', e => { if (!w) return; w.dx = e.clientX - w.x; w.dy = Math.max(0, e.clientY - w.y); if (w.dy > Math.abs(w.dx)) gsap.set(getBild(), { y: w.dy * 0.9 }); });
const ende = () => {
if (!w) return; const { dx, dy } = w; w = null;
if (dy > innerHeight * 0.15 && dy > Math.abs(dx)) return zu();
gsap.to(getBild(), { y: 0, duration: 0.3, ease: 'power2.out' });
if (Math.abs(dx) > 50 && Math.abs(dx) > dy) blaettern(dx < 0 ? 1 : -1);
};
zone.addEventListener('pointerup', ende); zone.addEventListener('pointercancel', ende);
}
wischen(pd.zone, () => pd.bild, () => pd.offen, pdBlaettern, pdZu);
wischen(se.zone, () => seBilder[se.aktiv], () => se.offen, seBlaettern, seZu);
addEventListener('resize', () => { if (se.offen && !se.busy) seSlot(se.aktiv); });
// ---------- Zoom: Lupe am Produktbild (nur hover:hover und pointer:fine), Vollbild mit Zoom bis Faktor 3 ----------
// Werte: Swiper Zoom (maxRatio 3, toggle per Doppeltippen); Lupe Faktor 2,5 (eigene Festlegung, Pipeline-Theme zoomt 1,5 in 200 ms);
// Kurve token-toggle-asymmetrisch aus dem Rezept d2c-produkt-zuerst: öffnen 300 ms expo.out, schließen 200 ms Anlauf-Kurve.
const ZM_B = [
{ n: 'gesamt', t: 'Gesamtansicht', a: 'Plaid in Fischgrat, gefaltet, mit eingenähtem Etikett und geknoteten Fransen auf hellem Grund' },
{ n: 'webart', t: 'Webart', a: 'Nahaufnahme der Fischgratbindung: dunkle Wollfäden und helle Leinenfäden im Köper, die Richtung wechselt alle acht Fäden' },
{ n: 'fransen', t: 'Fransen', a: 'Kante des Plaids mit von Hand geknoteten, gedrehten Fransen' },
{ n: 'etikett', t: 'Etikett', a: 'Gewebtes Etikett mit der Aufschrift Brede, Webhaus Nordhorn, 70 % Schurwolle, 30 % Leinen, mit Steppstich aufgenäht' },
];
const ZM_PFAD = 'bilder/galerie-shared-element-vollbild-plaid-';
const ZW = { stufen: [1, 1.5, 2, 3], max: 3, lupe: 2.5, lupeD: 200, schritt: 0.1, auf: 300, zu: 200, k: 'cubic-bezier(0.16, 1, 0.3, 1)', kAus: 'cubic-bezier(0.7, 0, 0.84, 0)', doppel: 300 };
const zm = {
haupt: $q('[data-zm-haupt]'), bild: $q('[data-zm-bild]'), lupe: $q('[data-zm-lupe]'), oeffnen: $q('[data-zm-oeffnen]'), dlg: $q('[data-zm-dlg]'),
grund: $q('[data-zm-grund]'), feld: $q('[data-zm-feld]'), flaeche: $q('[data-zm-flaeche]'), gross: $q('[data-zm-gross]'), laedt: $q('[data-zm-laedt]'),
faktor: $q('[data-zm-faktor]'), plus: $q('[data-zm-plus]'), minus: $q('[data-zm-minus]'), zu: $q('[data-zm-zu]'), zahl: $q('[data-zm-zahl]'), titel: $q('[data-zm-titel]'),
aktiv: 0, s: 1, tx: 0, ty: 0, fw: 0, fh: 0, offen: false, busy: false, geladen: new Set(),
};
const zmDaumen = [...document.querySelectorAll('[data-zm-daumen]')], zmReihe = [...document.querySelectorAll('[data-zm-reihe]')];
const zmChrom = () => [...zm.dlg.querySelectorAll('[data-zm-chrom]')];
const zmMaus = matchMedia('(hover: hover) and (pointer: fine)');
const grossUrl = i => `${ZM_PFAD}${ZM_B[i].n}-gross.webp`, kleinUrl = i => `${ZM_PFAD}${ZM_B[i].n}.webp`;
// Hochaufgelöstes Bild erst bei Bedarf laden (Öffnen der Vollansicht oder erstes Überfahren mit der Lupe)
function zmLaden(i) {
const url = grossUrl(i);
if (zm.geladen.has(url)) return Promise.resolve(url);
const im = new Image(); im.decoding = 'async'; im.src = url;
return im.decode().then(() => { zm.geladen.add(url); return url; }, () => url);
}
function zmMarkieren() {
zmDaumen.forEach((b, k) => b.setAttribute('aria-current', String(k === zm.aktiv)));
zmReihe.forEach((b, k) => b.setAttribute('aria-current', String(k === zm.aktiv)));
zm.zahl.textContent = `${zm.aktiv + 1} / ${ZM_B.length}`; zm.titel.textContent = ZM_B[zm.aktiv].t;
}
function zmHauptbild(i) {
zm.aktiv = i; zmMarkieren();
zm.bild.src = kleinUrl(i); zm.bild.alt = ZM_B[i].a;
if (!reduziert()) zm.bild.animate([{ opacity: 0.4 }, { opacity: 1 }], { duration: 200, easing: ZW.k });
}
zmDaumen.forEach((b, i) => b.addEventListener('click', () => zmHauptbild(i)));
document.querySelectorAll('[data-zm-groesse]').forEach(b => b.addEventListener('click', () => {
document.querySelectorAll('[data-zm-groesse]').forEach(x => x.setAttribute('aria-pressed', String(x === b)));
$q('[data-zm-preis]').textContent = b.dataset.zmGroesse;
}));
// --- Lupe: runder Ausschnitt mit Faktor 2,5 folgt dem Zeiger
function zmLupeAn(x, y) {
const r = zm.haupt.getBoundingClientRect(), L = zm.lupe, url = zm.geladen.has(grossUrl(zm.aktiv)) ? grossUrl(zm.aktiv) : kleinUrl(zm.aktiv);
const bx = Math.max(0, Math.min(r.width, x)), by = Math.max(0, Math.min(r.height, y));
L.style.backgroundImage = `url("${url}")`;
L.style.backgroundSize = `${r.width * ZW.lupe}px ${r.height * ZW.lupe}px`;
L.style.backgroundPosition = `${-(bx * ZW.lupe - ZW.lupeD / 2)}px ${-(by * ZW.lupe - ZW.lupeD / 2)}px`;
L.style.translate = `${bx}px ${by}px`;
L.classList.add('an');
}
const zmLupeAus = () => zm.lupe.classList.remove('an');
zm.haupt.addEventListener('pointerenter', e => { if (e.pointerType === 'mouse' && zmMaus.matches) zmLaden(zm.aktiv); });
zm.haupt.addEventListener('pointermove', e => {
if (e.pointerType !== 'mouse' || !zmMaus.matches || e.target.closest('.zm-oeffnen') || html.classList.contains('vorfuehrung')) { zmLupeAus(); return; }
const r = zm.haupt.getBoundingClientRect(); zmLupeAn(e.clientX - r.left, e.clientY - r.top);
});
zm.haupt.addEventListener('pointerleave', () => { if (!html.classList.contains('vorfuehrung')) zmLupeAus(); });
zm.haupt.addEventListener('click', e => { if (!e.target.closest('.zm-oeffnen') && zmMaus.matches) zmAuf(); });
// --- Zoomfläche: Größe im Format 4:5 einpassen, Ausschnitt begrenzen
function zmEinpassen() {
const f = zm.feld.getBoundingClientRect();
let w = f.width - (innerWidth > 640 ? 48 : 0), h = w * 1.25;
if (h > f.height - 8) { h = f.height - 8; w = h * 0.8; }
zm.fw = w; zm.fh = h; zm.flaeche.style.width = w + 'px'; zm.flaeche.style.height = h + 'px';
}
function zmGrenzen() {
const f = zm.feld.getBoundingClientRect();
const mx = Math.max(0, (zm.fw * zm.s - f.width) / 2), my = Math.max(0, (zm.fh * zm.s - f.height) / 2);
zm.tx = Math.max(-mx, Math.min(mx, zm.tx)); zm.ty = Math.max(-my, Math.min(my, zm.ty));
}
function zmSetzen(gleiten) {
zmGrenzen();
zm.flaeche.classList.toggle('gleitet', !!gleiten && !reduziert());
zm.flaeche.style.transform = `translate(${zm.tx}px, ${zm.ty}px) scale(${zm.s})`;
zm.feld.classList.toggle('gezoomt', zm.s > 1.001);
zm.faktor.value = zm.faktor.textContent = `${Math.round(zm.s * 100)} %`;
zm.minus.disabled = zm.s <= 1.001; zm.plus.disabled = zm.s >= ZW.max - 0.001;
}
// Zoom auf einen Punkt (px, py relativ zur Feldmitte): der Punkt bleibt unter dem Finger bzw. Zeiger
function zmZoom(s2, px = 0, py = 0, gleiten = true) {
s2 = Math.max(1, Math.min(ZW.max, s2));
const q = s2 / zm.s; zm.tx = px - (px - zm.tx) * q; zm.ty = py - (py - zm.ty) * q; zm.s = s2;
if (s2 === 1) { zm.tx = 0; zm.ty = 0; }
zmSetzen(gleiten);
}
const zmStufe = r => { const st = ZW.stufen; const n = r > 0 ? st.find(x => x > zm.s + 0.01) : [...st].reverse().find(x => x < zm.s - 0.01); zmZoom(n ?? (r > 0 ? ZW.max : 1)); };
const zmPunkt = e => { const f = zm.feld.getBoundingClientRect(); return [e.clientX - f.left - f.width / 2, e.clientY - f.top - f.height / 2]; };
zm.plus.addEventListener('click', () => zmStufe(1));
zm.minus.addEventListener('click', () => zmStufe(-1));
// Gesten: ein Finger zieht (nur gezoomt), zwei Finger zoomen, Doppeltippen oder Doppelklick schaltet 1 ↔ 3
const zg = { p: new Map(), start: null, tipp: null };
zm.feld.addEventListener('pointerdown', e => {
if (zm.busy) return;
zm.feld.setPointerCapture(e.pointerId); zg.p.set(e.pointerId, [e.clientX, e.clientY]);
const pts = [...zg.p.values()];
if (pts.length === 2) {
const [a, b] = pts; const f = zm.feld.getBoundingClientRect();
zg.start = { d: Math.hypot(a[0] - b[0], a[1] - b[1]), s: zm.s, tx: zm.tx, ty: zm.ty, m: [(a[0] + b[0]) / 2 - f.left - f.width / 2, (a[1] + b[1]) / 2 - f.top - f.height / 2], pinch: true };
} else zg.start = { x: e.clientX, y: e.clientY, tx: zm.tx, ty: zm.ty, zeit: performance.now(), weg: 0 };
});
zm.feld.addEventListener('pointermove', e => {
if (!zg.p.has(e.pointerId) || !zg.start) return;
zg.p.set(e.pointerId, [e.clientX, e.clientY]);
const pts = [...zg.p.values()], st = zg.start;
if (st.pinch && pts.length === 2) {
const [a, b] = pts, f = zm.feld.getBoundingClientRect();
const m = [(a[0] + b[0]) / 2 - f.left - f.width / 2, (a[1] + b[1]) / 2 - f.top - f.height / 2];
const s2 = Math.max(1, Math.min(ZW.max, st.s * Math.hypot(a[0] - b[0], a[1] - b[1]) / st.d)), q = s2 / st.s;
zm.s = s2; zm.tx = m[0] - (st.m[0] - st.tx) * q; zm.ty = m[1] - (st.m[1] - st.ty) * q; zmSetzen(false);
} else if (!st.pinch) {
st.weg = Math.max(st.weg, Math.hypot(e.clientX - st.x, e.clientY - st.y));
if (zm.s > 1.001) { zm.feld.classList.add('zieht'); zm.tx = st.tx + e.clientX - st.x; zm.ty = st.ty + e.clientY - st.y; zmSetzen(false); }
}
});
function zmLoslassen(e) {
if (!zg.p.has(e.pointerId)) return;
zg.p.delete(e.pointerId); zm.feld.classList.remove('zieht');
const st = zg.start;
if (st && st.pinch) { zg.start = zg.p.size === 1 ? (() => { const [p] = [...zg.p.values()]; return { x: p[0], y: p[1], tx: zm.tx, ty: zm.ty, zeit: 0, weg: 99 }; })() : null; if (zm.s < 1.05) zmZoom(1); return; }
zg.start = null;
if (!st || st.weg > 8 || performance.now() - st.zeit > 300 || e.type === 'pointercancel') return;
// Wischen ohne Zoom: blättern
const jetzt = performance.now(), [px, py] = zmPunkt(e);
if (zg.tipp && jetzt - zg.tipp.zeit < ZW.doppel && Math.hypot(px - zg.tipp.x, py - zg.tipp.y) < 30) { zg.tipp = null; zmZoom(zm.s > 1.001 ? 1 : ZW.max, px, py); }
else zg.tipp = { zeit: jetzt, x: px, y: py };
}
zm.feld.addEventListener('pointerup', zmLoslassen); zm.feld.addEventListener('pointercancel', zmLoslassen);
// Wischen zum Blättern, solange nicht gezoomt (Touch, ab 50 px waagerecht)
let zmWisch = null;
zm.feld.addEventListener('pointerdown', e => { zmWisch = e.pointerType !== 'mouse' && zm.s <= 1.001 ? [e.clientX, e.clientY] : null; });
zm.feld.addEventListener('pointerup', e => {
if (!zmWisch || zg.p.size) { zmWisch = null; return; }
const dx = e.clientX - zmWisch[0], dy = e.clientY - zmWisch[1]; zmWisch = null;
if (Math.abs(dx) > 50 && Math.abs(dx) > Math.abs(dy) * 1.5 && zm.s <= 1.001) zmBlaettern(dx < 0 ? 1 : -1);
});
// Trackpad-Pinch kommt als wheel mit ctrlKey
zm.feld.addEventListener('wheel', e => { if (!e.ctrlKey) return; e.preventDefault(); const [px, py] = zmPunkt(e); zmZoom(zm.s * Math.exp(-e.deltaY * 0.01), px, py, false); }, { passive: false });
async function zmBildImDialog(i) {
const klein = kleinUrl(i), gross = grossUrl(i);
zm.gross.alt = ZM_B[i].a;
zm.gross.src = zm.geladen.has(gross) ? gross : klein;
if (zm.geladen.has(gross)) { zm.laedt.hidden = true; return; }
zm.laedt.hidden = false;
await zmLaden(i);
if (zm.aktiv === i && zm.offen) { zm.gross.src = gross; zm.laedt.hidden = true; }
}
function zmBlaettern(r) {
if (!zm.offen || zm.busy) return;
const i = (zm.aktiv + r + ZM_B.length) % ZM_B.length;
zm.s = 1; zm.tx = zm.ty = 0; zmSetzen(false);
zm.aktiv = i; zmMarkieren(); zmBildImDialog(i);
if (!reduziert()) zm.gross.animate([{ opacity: 0 }, { opacity: 1 }], { duration: ZW.auf, easing: ZW.k });
}
zmReihe.forEach((b, i) => b.addEventListener('click', () => zmBlaettern(i - zm.aktiv)));
document.querySelectorAll('[data-zm-schritt]').forEach(b => b.addEventListener('click', () => zmBlaettern(+b.dataset.zmSchritt)));
// Tastatur im Vollbild: + und − zoomen, 0 setzt zurück, Pfeile verschieben gezoomt um 10 % des Felds, sonst blättern; Esc schließt
zm.dlg.addEventListener('keydown', e => {
if (zm.busy) return;
if (e.key === '+' || e.key === '=') { e.preventDefault(); zmStufe(1); return; }
if (e.key === '-' || e.key === '_') { e.preventDefault(); zmStufe(-1); return; }
if (e.key === '0') { e.preventDefault(); zmZoom(1); return; }
const d = { ArrowLeft: [1, 0], ArrowRight: [-1, 0], ArrowUp: [0, 1], ArrowDown: [0, -1] }[e.key];
if (!d || e.target.closest('.zm-reihe')) return;
e.preventDefault();
if (zm.s > 1.001) { const f = zm.feld.getBoundingClientRect(); zm.tx += d[0] * f.width * ZW.schritt; zm.ty += d[1] * f.height * ZW.schritt; zmSetzen(true); }
else if (d[0]) zmBlaettern(-d[0]);
});
zm.dlg.addEventListener('cancel', e => { e.preventDefault(); zmZu(); });
zm.zu.addEventListener('click', () => zmZu());
zm.oeffnen.addEventListener('click', () => zmAuf());
// Öffnen: das Hauptbild wächst in die Vollansicht (FLIP von Hand, 300 ms), Grund und Bedienung blenden ein
function zmFlipVon(r0) {
const r1 = zm.flaeche.getBoundingClientRect();
return `translate(${r0.left + r0.width / 2 - (r1.left + r1.width / 2)}px, ${r0.top + r0.height / 2 - (r1.top + r1.height / 2)}px) scale(${r0.width / r1.width})`;
}
function zmAuf() {
if (zm.offen || zm.busy) return;
zm.offen = true; zm.busy = true; zmLupeAus();
html.classList.add('offen-seite');
zm.dlg.showModal();
if (html.classList.contains('vorfuehrung')) zm.dlg.append(zeiger);
zmMarkieren(); zmEinpassen(); zm.s = 1; zm.tx = zm.ty = 0; zmSetzen(false);
zmBildImDialog(zm.aktiv);
const fertig = () => { zm.busy = false; if (!html.classList.contains('vorfuehrung')) zm.feld.focus({ preventScroll: true }); };
if (reduziert()) { zm.dlg.animate([{ opacity: 0 }, { opacity: 1 }], { duration: 150 }); fertig(); return; }
const r0 = zm.haupt.getBoundingClientRect();
zm.bild.style.visibility = 'hidden';
zm.grund.animate([{ opacity: 0 }, { opacity: 1 }], { duration: ZW.auf, easing: ZW.k });
zmChrom().forEach(c => c.animate([{ opacity: 0 }, { opacity: 1 }], { duration: 200, delay: 150, easing: ZW.k, fill: 'backwards' }));
zm.flaeche.animate([{ transform: zmFlipVon(r0), borderRadius: '12px' }, { transform: 'none' }], { duration: ZW.auf, easing: ZW.k })
.finished.then(() => { zm.bild.style.visibility = ''; fertig(); });
}
// Schließen: schneller (200 ms) mit Anlauf-Kurve zurück in das Hauptbild, Fokus zurück auf „Vergrößern“
function zmZu(sofort = false) {
if (!zm.offen || zm.busy) return;
zm.busy = true;
if (zm.bild.src.indexOf(ZM_B[zm.aktiv].n) < 0) { zm.bild.src = kleinUrl(zm.aktiv); zm.bild.alt = ZM_B[zm.aktiv].a; }
const ende = () => {
zm.dlg.close(); zm.offen = false; zm.busy = false; html.classList.remove('offen-seite');
zm.bild.style.visibility = ''; zm.s = 1; zm.tx = zm.ty = 0; zmSetzen(false);
if (zeiger.parentNode === zm.dlg) document.body.append(zeiger);
if (!html.classList.contains('vorfuehrung')) zm.oeffnen.focus({ preventScroll: true });
};
if (sofort || reduziert()) { ende(); return; }
zm.bild.style.visibility = 'hidden';
const von = zm.flaeche.style.transform || 'none';
zm.flaeche.classList.remove('gleitet');
const r0 = zm.haupt.getBoundingClientRect();
zm.flaeche.style.transform = 'none'; const nach = zmFlipVon(r0); zm.flaeche.style.transform = von;
zmChrom().forEach(c => c.animate([{ opacity: 1 }, { opacity: 0 }], { duration: 120, easing: ZW.kAus, fill: 'forwards' }));
zm.grund.animate([{ opacity: 1 }, { opacity: 0 }], { duration: ZW.zu, easing: ZW.kAus, fill: 'forwards' });
zm.flaeche.animate([{ transform: von }, { transform: nach, borderRadius: '12px' }], { duration: ZW.zu, easing: ZW.kAus, fill: 'forwards' })
.finished.then(() => { zm.flaeche.getAnimations().forEach(a => a.cancel()); zm.grund.getAnimations().forEach(a => a.cancel()); zmChrom().forEach(c => c.getAnimations().forEach(a => a.cancel())); ende(); });
}
addEventListener('resize', () => { if (zm.offen && !zm.busy) { zmEinpassen(); zmSetzen(false); } });
function zmReset() { if (zm.offen) { zm.busy = false; zmZu(true); } zmLupeAus(); if (zm.aktiv !== 0) zmHauptbild(0); }
// Vorführung: gedachte Maus fährt mit der Lupe über Etikett und Stoff, öffnet, zoomt per Doppelklick auf das Etikett, verschiebt, blättert, schließt
let zmLupeLauf = 0;
function zmLupeFahrt(punkte, dauer) {
const id = ++zmLupeLauf, t0 = performance.now(), r = zm.haupt.getBoundingClientRect();
const schritt = jetzt => {
if (id !== zmLupeLauf || !laeuft) return;
const t = Math.min(1, (jetzt - t0) / dauer), n = punkte.length - 1, k = Math.min(n - 1, Math.floor(t * n)), f = t * n - k;
const e = f * f * (3 - 2 * f), x = punkte[k][0] + (punkte[k + 1][0] - punkte[k][0]) * e, y = punkte[k][1] + (punkte[k + 1][1] - punkte[k][1]) * e;
zmLupeAn(x * r.width, y * r.height);
zeiger.style.transition = 'none'; zeiger.style.transform = `translate(${r.left + x * r.width}px, ${r.top + y * r.height}px)`;
if (t < 1) requestAnimationFrame(schritt); else zeiger.style.transition = '';
};
requestAnimationFrame(schritt);
}
function zmRunde() {
if (!laeuft) return;
zmReset();
const mausDa = zmMaus.matches || html.classList.contains('vorschau');
let t = 300;
if (mausDa) {
zmLaden(0);
zeigeAuf(zm.haupt, 0.3, 0.25);
spaeter(() => zmLupeFahrt([[0.3, 0.25], [0.36, 0.24], [0.5, 0.45], [0.7, 0.6], [0.55, 0.86]], 2600), 700);
spaeter(() => zmLupeAus(), 3400);
t = 3500;
}
spaeter(() => zeigeAuf(zm.oeffnen, 0.5, 0.5), t);
spaeter(() => klick(zmAuf), t + 800);
// Doppelklick auf das Etikett (Bildkoordinate 0,28 / 0,22)
spaeter(() => { const r = zm.flaeche.getBoundingClientRect(); zeiger.style.transform = `translate(${r.left + r.width * 0.28}px, ${r.top + r.height * 0.22}px)`; }, t + 1600);
spaeter(() => klick(() => { const r = zm.flaeche.getBoundingClientRect(), f = zm.feld.getBoundingClientRect(); zmZoom(ZW.max, r.left + r.width * 0.28 - f.left - f.width / 2, r.top + r.height * 0.22 - f.top - f.height / 2); }), t + 2400);
spaeter(() => zm.dlg.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true })), t + 3800);
spaeter(() => zm.dlg.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowRight', bubbles: true })), t + 4500);
spaeter(() => zeigeAuf(zm.minus, 0.5, 0.6), t + 5000);
spaeter(() => klick(() => zmStufe(-1)), t + 5700);
spaeter(() => klick(() => zmStufe(-1)), t + 6500);
spaeter(() => zeigeAuf(zmReihe[1], 0.5, 0.5), t + 7200);
spaeter(() => klick(() => zmBlaettern(1)), t + 7900);
spaeter(() => zeigeAuf(zm.plus, 0.5, 0.6), t + 8600);
spaeter(() => klick(() => zmStufe(1)), t + 9300);
spaeter(() => klick(() => zmStufe(1)), t + 10000);
spaeter(() => zeigeAuf(zm.zu, 0.4, 0.6), t + 11000);
spaeter(() => klick(() => zmZu()), t + 11700);
spaeter(rundeWelt, t + 13200);
}
// ---------- Umschalten ----------
const vKnoepfe = [...document.querySelectorAll('[data-v-wahl]')];
function weltReset() {
if (pd.offen) { pd.haupt.prepend(pd.bild); pd.offen = false; pd.voll.classList.remove('an'); }
if (se.offen) { seRahmen[se.aktiv].appendChild(seBilder[se.aktiv]); se.offen = false; se.kasten.classList.remove('an'); }
zmReset();
pd.busy = se.busy = false;
gsap.set([pd.voll, pd.grund, pd.bild, ...pdChrom(), se.kasten, se.grund, ...seBilder, ...seChrom()], { clearProps: 'all' });
if (welt !== 'basis') html.classList.remove('offen-seite');
}
function setzeWelt(w, url = true) {
grundzustand(); weltReset();
welt = w; html.dataset.v = w;
vKnoepfe.forEach(b => b.setAttribute('aria-pressed', String(b.dataset.vWahl === w)));
if (url) { const u = new URL(location.href); u.searchParams.set('v', w); history.replaceState(history.state, '', u); }
scrollTo(0, 0); zeigeWerte();
}
vKnoepfe.forEach(b => b.addEventListener('click', () => { html.classList.remove('vorfuehrung'); setzeWelt(b.dataset.vWahl); }));
// ---------- Vorführung der Varianten ----------
function rundeWelt() {
if (!laeuft) return;
if (welt === 'zoom') { zmRunde(); return; }
if (welt === 'produkt') {
pdZeige(0);
zeigeAuf(pdDaumenKnoepfe[2]); spaeter(() => klick(() => pdZeige(2)), 700);
spaeter(() => zeigeAuf(pdDaumenKnoepfe[3]), 1500); spaeter(() => klick(() => pdZeige(3)), 2200);
spaeter(() => zeigeAuf(pd.haupt, .5, .55), 3000); spaeter(() => klick(pdAuf), 3800);
spaeter(() => zeigeAuf($q('[data-pd-vor]')), 5000); spaeter(() => klick(() => pdBlaettern(1)), 5700);
spaeter(() => klick(() => pdBlaettern(1)), 6900);
spaeter(() => zeigeAuf(pd.zu, .4, .6), 7900); spaeter(() => klick(pdZu), 8700);
spaeter(rundeWelt, 10400);
return;
}
scrollTo(0, 0);
zeigeAuf(seRahmen[0], .5, .5); spaeter(() => klick(() => seAuf(0)), 900);
spaeter(() => zeigeAuf($q('[data-se-vor]')), 2200);
spaeter(() => klick(() => seBlaettern(1)), 2900); spaeter(() => klick(() => seBlaettern(1)), 4100);
spaeter(() => zeigeAuf(se.zu, .4, .6), 5300); spaeter(() => klick(seZu), 6100);
spaeter(() => zeigeAuf(seRahmen[2], .5, .5), 7300);
spaeter(() => { if (!html.classList.contains('vorschau')) return; gsap.to(window, { duration: 0, onComplete: () => scrollTo(0, 0) }); }, 9000);
spaeter(rundeWelt, 9400);
}
zeigeWerte();
// Deep-Link: #slug öffnet direkt
const ausUrl = new URLSearchParams(location.search).get('v');
if (WELTEN.includes(ausUrl) && ausUrl !== 'basis') setzeWelt(ausUrl, false); else { html.dataset.v = 'basis'; zeigeWerte(); }
const start = welt !== 'basis' ? -1 : PROJEKTE.findIndex(p => '#' + p.slug === location.hash);
if (start >= 0) document.fonts.ready.then(() => oeffnen(start, true));
})();
</script>
</body>
</html>