Karussell-Steuerung und Anzeige
Fünf Steuerungen für ein natives Scroll-Snap-Karussell in fünf Branchen: Zähler „03 / 08“, Fortschrittsleiste per scroll-timeline, Daumen als ::scroll-marker, Selbstlauf mit Restzeit-Ring und Pause, Anker-Links mit eigener URL je Folie.
Hotel (Zimmer), Fotograf, Kultur, Kanzlei: ruhige Reihen mit 5 bis 10 Folien, bei denen die Menge zählt.
Die Bedienung und Anzeige für ein natives Scroll-Snap-Karussell, getrennt von seinem Aufbau. Fünf belegte Formen: Zähler „03 / 08“ neben den Pfeilen, eine 2-px-Fortschrittsleiste unter der Reihe, Vorschaubilder als Marker, Selbstlauf mit sichtbarer Restzeit und Pause-Knopf, und Anker-Links, mit denen jede Folie eine eigene Adresse bekommt. Das Band selbst bleibt ein echter Scroller nach css-native-karussell. Alle Formen nutzen dieselben Rollen nach dem WAI-ARIA-Karussellmuster.
Punkte sind ein schwacher Hinweis: Sie sagen weder, wie viel noch kommt, noch was. Ein Zähler nennt die Menge, eine Leiste zeigt die Strecke, Vorschaubilder zeigen den Inhalt, eine Adresse macht eine Folie teilbar. Selbstlauf ist nur mit sichtbarer Restzeit und Pause erträglich. Wer die Steuerung nach Inhalt wählt statt nach Gewohnheit, macht aus einem Karussell eine lesbare Strecke.
Wann einsetzen
Überall, wo css-native-karussell steht und die Punkte nicht reichen. Zähler für Zimmer, Serien und ruhige Reihen. Fortschrittsleiste ab etwa 6 Karten (Lagen, Produkte, Objekte). Vorschaubilder für Bildgalerien eines Objekts oder Produkts. Selbstlauf nur auf dem Desktop für Programm- oder Bildbühnen mit höchstens 5 Folien. Anker-Links, wenn einzelne Folien verschickt werden (Wohnung im Exposé, Zimmer, Lage).
Wann nicht
Nicht mehrere Anzeigen stapeln (Punkte plus Zähler plus Leiste). Kein Selbstlauf auf dem Telefon, kein Selbstlauf ohne Pause-Knopf, kein Intervall unter 5 Sekunden. Keine abgeschnittene Daumenleiste. Keine Anker-Links, die bei jedem Wechsel einen neuen Verlaufseintrag anlegen. Wenn der Inhalt in ein Raster passt, lieber raster-zu-wischreihe oder gar kein Karussell.
Varianten
- zaehler: „03 / 08“ in Tabellenziffern zwischen den Pfeilen, die Ziffer rollt in einer Maske von 1lh; Folien heißen „3 von 8“
- fortschrittsleiste: 2-px-Balken unter der Reihe, Füllung vom sichtbaren Anteil bis 1 per scroll-timeline und timeline-scope, JS-Rückfall
- daumen: Vorschaubilder als Marker, ::scroll-marker mit Bild in Chromium, sonst Buttons mit aria-current; alle Daumen sichtbar, mobil 4 × 2
- autoplay-pause: Selbstlauf ab 5000 ms (Demo 6000) mit Restzeit-Ring, Pause-Knopf zuerst, Hover pausiert, Fokus stoppt, kein Start auf dem Telefon
- ankerlinks: jede Folie mit eigener URL (#wohnung-3), Marker als Links, history.replaceState nach dem Einrasten, hashchange springt
Umsetzung
Basis ist ein echter Scroller nach css-native-karussell (overflow-x auto, scroll-snap-type x mandatory). Die Steuerung liest den Zustand aus der Scrollposition: aktive Folie = nächster Einrastpunkt, aktualisiert bei scroll und scrollend. Pfeile und Marker rufen eine Gleitfunktion mit der Kurve des Rezepts auf (Snap während der Bewegung aus, danach wieder an). Zähler: zwei Zahlen in tabular-nums, die Ziffer rollt in einer Maske in lh. Fortschrittsleiste: scroll-timeline am Band, timeline-scope am gemeinsamen Vorfahren, animation-timeline an der Füllung (scale 0 auf 1); ohne Unterstützung setzt ein Scroll-Handler die Skalierung. Daumen: per @supports selector(::scroll-marker) ::scroll-marker mit background-image aus einer Variable je Folie, sonst eine Reihe Buttons mit aria-current. Selbstlauf: setTimeout-Kette mit Restzeit, die bei Hover anhält und weiterläuft, bei Fokus endet; Pause-Knopf als erstes Element mit aria-pressed. Anker-Links: Folien mit id, Marker als a href="#id", Klick gleitet und setzt history.replaceState, beim Laden und bei hashchange springt das Band.
Robust umsetzen
Telefon. Wischen bleibt die Hauptbedienung. Zähler und Leiste bleiben sichtbar, Pfeile 44 px. Daumen brechen in zwei Reihen um statt abgeschnitten zu werden. Selbstlauf startet auf dem Telefon nie von selbst, der Knopf bleibt da.
Ohne Bewegung. Pfeile und Marker springen ohne Gleiten, die Zähler-Ziffer rollt nicht. Selbstlauf startet nicht, der Pause-Knopf steht auf „angehalten“. Die Fortschrittsleiste folgt weiter der Position, weil sie keine eigene Bewegung ist.
Barrierefrei. Container als region (section mit aria-label) oder group, aria-roledescription „Karussell“; Folien role group, aria-roledescription „Folie“, Name „3 von 8: Seezimmer“. Pfeile mit aria-label und aria-controls. Zähler als Live-Region nur im Stand, Fortschrittsleiste aria-hidden. Daumen mit Namen und aria-current. Selbstlauf: Pause-Knopf zuerst, aria-pressed, aria-live des Bands off während der Rotation, polite im Stand, Folien außerhalb der Sicht inert. Anker-Links mit aria-current und sprechendem Text.
Code
<!DOCTYPE html>
<!--
Karussell-Steuerung und Anzeige
Fundus · Galerien & Slider · https://cecodesign.de/lab/fundus/galerien-slider/karussell-steuerung/
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>Karussell-Steuerung und Anzeige · 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: 'Newsreader'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/newsreader:vf@latest/latin-opsz-normal.woff2') format('woff2-variations'); font-weight: 200 800; font-style: normal; font-display: swap; }
@font-face { font-family: 'Newsreader'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/newsreader:vf@latest/latin-opsz-italic.woff2') format('woff2-variations'); font-weight: 200 800; font-style: italic; font-display: swap; }
@font-face { font-family: 'Mona Sans'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/mona-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: 'EB Garamond'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/eb-garamond:vf@latest/latin-wght-normal.woff') format('woff'); font-weight: 400 800; font-style: normal; font-display: swap; }
@font-face { font-family: 'EB Garamond'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/eb-garamond:vf@latest/latin-wght-italic.woff2') format('woff2-variations'); font-weight: 400 800; font-style: italic; font-display: swap; }
@font-face { font-family: 'DM Mono'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/dm-mono@latest/latin-500-normal.woff2') format('woff2'); font-weight: 500; font-display: swap; }
@font-face { font-family: 'Libre Caslon Display'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/libre-caslon-display@latest/latin-400-normal.woff2') format('woff2'); font-weight: 400; font-display: swap; }
@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: 'Schibsted Grotesk'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/schibsted-grotesk:vf@latest/latin-wght-normal.woff2') format('woff2-variations'); font-weight: 400 900; font-display: swap; }
@font-face { font-family: 'Atkinson Hyperlegible Next'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/atkinson-hyperlegible-next:vf@latest/latin-wght-normal.woff2') format('woff2-variations'); font-weight: 200 800; font-display: swap; }
@font-face { font-family: 'IBM Plex Mono'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/ibm-plex-mono@latest/latin-500-normal.woff2') format('woff2'); font-weight: 500; 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; }
/* ============ Gemeinsam ============ */
:root { --rand: clamp(20px, 3.6vw, 52px); --g: #F3EEE7; --t: #3E3A37; --l: #6A6C67; --ln: #D5D1C8; --fokus: #6E5A47; }
* { box-sizing: border-box; margin: 0; padding: 0; }
html { background: var(--g); color: var(--t); -webkit-font-smoothing: antialiased; }
body { min-height: 100svh; overflow-x: hidden; display: grid; grid-template-rows: minmax(0, 1fr) auto; grid-template-columns: minmax(0, 1fr); }
a { color: inherit; text-decoration: none; }
button { font: inherit; color: inherit; }
img { display: block; }
:focus-visible { outline: 2px solid var(--fokus); outline-offset: 3px; }
:root:has(:focus-visible) { scroll-behavior: auto; }
.sr { position: absolute !important; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
.welt { display: flex; flex-direction: column; min-width: 0; }
.welt[hidden] { display: none; }
/* Das Band: echter Scroller, Gleiten kommt aus JS mit der Kurve des Rezepts */
.band { position: relative; list-style: none; display: grid; grid-auto-flow: column; overflow-x: auto; overflow-y: hidden; overscroll-behavior-x: contain; scrollbar-width: none; scroll-snap-type: x mandatory; scroll-behavior: auto; outline-offset: 4px; }
.band::-webkit-scrollbar { display: none; }
.band > li { scroll-snap-align: start; min-width: 0; }
.pfeil svg { width: 18px; height: 18px; display: block; }
/* Pult im Fuß: Varianten und verwendete Werte */
.fuss { display: flex; justify-content: space-between; align-items: center; gap: 10px 28px; flex-wrap: wrap; padding: 12px var(--rand) 16px; border-top: 1px solid var(--ln); background: var(--g); }
.mono { font: 500 10.5px/1.45 'Geist Mono', ui-monospace, monospace; letter-spacing: .14em; text-transform: uppercase; color: var(--l); }
.modi { display: flex; gap: 4px; flex-wrap: wrap; align-items: center; }
.modi > span { margin-right: 6px; }
.modi button { font: inherit; letter-spacing: inherit; text-transform: inherit; color: var(--l); background: none; border: 1px solid var(--ln); border-radius: 999px; min-height: 30px; padding: 0 12px; cursor: pointer; transition: color 150ms ease-out, background-color 150ms ease-out, border-color 150ms ease-out; }
.modi button:hover { color: var(--t); border-color: var(--l); }
.modi button[aria-pressed=true] { background: var(--t); color: var(--g); border-color: var(--t); }
.modi[hidden] { display: none; }
.pultspalte { display: grid; gap: 8px; }
.werte { max-width: 700px; text-align: right; }
.werte b { color: var(--t); font-weight: 500; }
/* Vorführung: gezeichneter Zeiger */
.zeiger { position: fixed; left: 0; top: 0; z-index: 30; width: 22px; height: 22px; pointer-events: none; display: none; transition: transform 700ms cubic-bezier(0.33, 1, 0.68, 1); }
.vorfuehrung .zeiger { display: block; }
.vorschau body { overflow: hidden; }
/* ============ Variante „Zähler“: Haus Wiek, Rezept boutique-hotel-gastgeber ============ */
html[data-v='zaehler'] { --g: #F3EEE7; --t: #3E3A37; --l: #6A6C67; --ln: #D5D1C8; --fokus: #6E5A47; }
.w-z { --kurve: cubic-bezier(0.37, 0, 0.63, 1); --holz: #6E5A47; --creme: #FEF2E3; --z-breite: clamp(280px, 44vw, 640px); font-family: 'Mona Sans', system-ui, sans-serif; }
.z-kopf { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 16px; padding: 16px var(--rand); border-bottom: 1px solid var(--ln); }
.z-kopf nav { display: flex; gap: 26px; }
.z-kopf nav a, .z-label, .z-buchen, .z-daten, .z-ges { font: 500 .75rem/1.25 'Mona Sans', sans-serif; font-stretch: 125%; letter-spacing: .06em; text-transform: uppercase; }
.z-kopf nav a[aria-current] { text-decoration: underline; text-underline-offset: 6px; text-decoration-thickness: 1px; }
.z-marke { justify-self: center; font: 400 28px/1 'Newsreader', Georgia, serif; letter-spacing: -.015em; }
.z-marke i { font-style: italic; }
.z-buchen { justify-self: end; min-height: 40px; padding: 0 18px; display: inline-flex; align-items: center; border-radius: 2px; background: var(--holz); color: var(--creme); transition: background-color 300ms var(--kurve); }
.z-buchen:hover { background: #59483a; }
.z-main { flex: 1; display: grid; align-content: center; padding: clamp(18px, 5svh, 60px) 0 clamp(16px, 4svh, 44px) var(--rand); }
.z-karussell { display: grid; grid-template-columns: minmax(0, 3.3fr) minmax(0, 8.7fr); gap: clamp(24px, 4vw, 64px); align-items: start; }
.z-text { display: grid; gap: 18px; align-content: start; padding-top: 4px; }
.z-label { color: var(--l); }
.z-text h2 { font: 300 clamp(2.2rem, 1.3rem + 2.6vw, 4rem)/1.04 'Newsreader', Georgia, serif; letter-spacing: -.015em; text-wrap: balance; }
.z-text h2 em { font-style: italic; }
.z-lauf { font-size: 1rem; line-height: 1.6; color: var(--l); max-width: 30ch; }
.z-steuer { display: flex; align-items: center; gap: 18px; margin-top: 12px; }
.z-pfeil { width: 48px; height: 48px; flex: none; border-radius: 50%; border: 1px solid rgba(62, 58, 55, .4); background: transparent; display: grid; place-items: center; cursor: pointer; transition: background-color 300ms var(--kurve), color 300ms var(--kurve), border-color 300ms var(--kurve), opacity 300ms var(--kurve), scale 300ms var(--kurve); }
.z-pfeil:hover:not(:disabled), .z-pfeil.druck { background: var(--holz); border-color: var(--holz); color: var(--creme); }
.z-pfeil.druck { scale: .92; }
.z-pfeil:disabled { opacity: .32; cursor: default; }
/* Der Zähler: Tabellenziffern, die aktuelle Zahl rollt in einer Maske von 1lh */
.z-zaehler { display: flex; align-items: baseline; gap: 10px; font-variant-numeric: tabular-nums; min-width: 6.4em; justify-content: center; }
.z-jetzt { display: inline-grid; overflow: hidden; height: 1lh; font: 300 46px/1.08 'Mona Sans', sans-serif; font-stretch: 112%; letter-spacing: -.02em; font-variant-numeric: tabular-nums; }
.z-jetzt > span { grid-area: 1 / 1; }
.z-trenn { width: 26px; height: 1px; background: var(--t); align-self: center; transform: rotate(-58deg); }
.z-ges { color: var(--l); letter-spacing: .08em; font-variant-numeric: tabular-nums; }
.z-band { grid-auto-columns: var(--z-breite); gap: clamp(16px, 2vw, 28px); padding: 0 calc(100% - var(--z-breite)) 6px 0; }
.z-folie { display: grid; gap: 16px; align-content: start; }
.z-folie figure { aspect-ratio: 3 / 2; overflow: hidden; border-radius: 2px; background: #e6ded2; }
.z-folie img { width: 100%; height: 100%; object-fit: cover; transition: scale 1200ms var(--kurve); }
@media (hover: hover) { .z-folie:hover img { scale: 1.03; } }
.z-info { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 6px 16px; align-items: baseline; }
.z-info h3 { font: 400 clamp(22px, 1.9vw, 30px)/1.1 'Newsreader', Georgia, serif; letter-spacing: -.01em; }
.z-info h3 i { font-style: italic; }
.z-preis { font-size: 14px; color: var(--l); white-space: nowrap; }
.z-preis b { font: 400 21px/1 'Newsreader', Georgia, serif; color: var(--t); }
.z-daten { grid-column: 1 / -1; color: var(--l); }
.z-folie a { text-decoration: underline; text-underline-offset: 4px; text-decoration-thickness: 1px; }
/* ============ Variante „Fortschrittsleiste“: Weingut Kalkofen, Rezept weingut-hofladen-herkunft ============ */
html[data-v='fortschritt'] { --g: #F1ECE2; --t: #22211F; --l: #5A564F; --ln: #D8CFC0; --fokus: #8A3A2B; }
.w-f { --kurve: cubic-bezier(0.37, 0, 0.63, 1); --loess: #E6DCCB; --schiefer: #2F3236; --signal: #8A3A2B; --f-breite: clamp(232px, 21.5vw, 330px); font-family: 'EB Garamond', Georgia, serif; }
.f-kopf { display: flex; justify-content: space-between; align-items: center; gap: 16px 28px; padding: 16px var(--rand); border-bottom: 1px solid var(--ln); }
.f-marke { display: flex; align-items: baseline; gap: 14px; }
.f-marke b { font: 500 26px/1 'EB Garamond', Georgia, serif; letter-spacing: -.01em; }
.f-marke b i { font-weight: 400; }
.f-marke span, .f-kopf nav a, .f-label, .f-nr, .f-karte dt, .f-paket, .f-anzahl { font: 500 .75rem/1.3 'DM Mono', ui-monospace, monospace; letter-spacing: .1em; text-transform: uppercase; }
.f-marke span, .f-label, .f-nr, .f-karte dt, .f-anzahl { color: var(--l); }
.f-kopf nav { display: flex; gap: 26px; margin-left: auto; }
.f-kopf nav a[aria-current] { color: var(--signal); }
.f-paket { min-height: 40px; display: inline-flex; align-items: center; padding: 0 16px; background: var(--signal); color: #fff; border-radius: 2px; transition: background-color 150ms var(--kurve); }
.f-paket:hover { background: #722f23; }
.f-main { flex: 1; display: grid; align-content: center; padding: clamp(18px, 4svh, 48px) 0 clamp(16px, 3.4svh, 40px); }
.f-karussell { display: grid; gap: clamp(16px, 3svh, 32px); timeline-scope: --flur; }
.f-titel { display: flex; justify-content: space-between; align-items: flex-end; gap: 18px 40px; padding-inline: var(--rand); }
.f-label { display: block; margin-bottom: 12px; }
.f-titel h2 { font: 400 clamp(2rem, 1.4rem + 2.4vw, 3.6rem)/1.02 'EB Garamond', Georgia, serif; letter-spacing: -.015em; }
.f-titel h2 i { font-style: italic; }
.f-seitlich { display: grid; justify-items: end; gap: 14px; text-align: right; }
.f-seitlich p { font-size: 1.125rem; line-height: 1.45; color: var(--l); max-width: 34ch; }
.f-steuer { display: flex; gap: 8px; }
.f-pfeil { width: 46px; height: 46px; border-radius: 2px; border: 1px solid rgba(34, 33, 31, .35); background: transparent; display: grid; place-items: center; cursor: pointer; transition: background-color 150ms var(--kurve), color 150ms var(--kurve), border-color 150ms var(--kurve), opacity 150ms var(--kurve); }
.f-pfeil:hover:not(:disabled), .f-pfeil.druck { background: var(--schiefer); color: var(--g); border-color: var(--schiefer); }
.f-pfeil:disabled { opacity: .3; cursor: default; }
.f-band { grid-auto-columns: var(--f-breite); gap: clamp(14px, 1.6vw, 24px); padding: 0 var(--rand) 4px; scroll-padding-inline: var(--rand); scroll-timeline: --flur x; }
.f-karte { display: grid; gap: 10px; align-content: start; }
.f-karte figure { aspect-ratio: 4 / 3; overflow: hidden; background: var(--loess); border-radius: 1px; }
.f-karte img { width: 100%; height: 100%; object-fit: cover; }
.f-nr { margin-top: 4px; }
.f-karte h3 { font: italic 400 clamp(24px, 2vw, 30px)/1.05 'EB Garamond', Georgia, serif; }
.f-karte dl { display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 3px 12px; padding-top: 10px; border-top: 1px solid var(--ln); font-size: 1.0625rem; line-height: 1.35; }
.f-karte dt { line-height: 1.9; }
/* Die Leiste: 2 px, Füllung = gesehener Anteil der Reihe, per scroll-timeline */
.f-fussleiste { display: flex; align-items: center; gap: 18px; padding-inline: var(--rand); }
.f-leiste { position: relative; flex: 1; height: 2px; background: var(--ln); overflow: hidden; }
.f-fuellung { position: absolute; inset: 0; background: var(--schiefer); transform-origin: 0 50%; scale: var(--anteil, .3) 1; }
@supports (animation-timeline: scroll()) {
.f-fuellung { animation: f-fuellen linear both; animation-timeline: --flur; }
}
@keyframes f-fuellen { from { scale: var(--anteil, .3) 1; } to { scale: 1 1; } }
/* ============ Variante „Daumen“: Vonderau Immobilien, Rezept immobilien-diskret-hochwertig ============ */
html:is([data-v='daumen'], [data-v='ankerlinks']) { --g: #FCFAF5; --t: #313131; --l: #716F6D; --ln: #D9D5CF; --fokus: #02385C; }
.w-d { --daumen-h: clamp(44px, 4.6vw, 64px); }
.w-d, .w-k { --kurve: cubic-bezier(0.16, 1, 0.3, 1); --sand: #EFE8DF; --marke: #02385C; --signal: #ED7F1A; --t2: #5d5b59; font-family: 'Hanken Grotesk', system-ui, sans-serif; }
.i-kopf { display: flex; align-items: center; gap: 16px 30px; padding: 16px var(--rand); border-bottom: 1px solid var(--ln); }
.i-marke { display: flex; align-items: baseline; gap: 12px; color: var(--marke); }
.i-marke b { font: 400 26px/1 'Libre Caslon Display', Georgia, serif; letter-spacing: -.005em; }
.i-marke span, .i-kopf nav a, .i-label, .d-zeile, .d-daten dt, .k-lage, .k-daten dt, .k-adresse-label, .k-etage-name, .k-legende { font: 500 .75rem/1.3 'Hanken Grotesk', sans-serif; letter-spacing: .09em; text-transform: uppercase; }
.i-marke span { color: var(--l); }
.i-kopf nav { display: flex; gap: 26px; margin-left: auto; }
.i-kopf nav a[aria-current] { color: var(--marke); box-shadow: 0 1px 0 var(--marke); }
.i-cta { min-height: 42px; display: inline-flex; align-items: center; padding: 0 18px; background: var(--signal); color: #1d1c1a; font-weight: 600; font-size: .9375rem; border-radius: 1px; transition: background-color 200ms var(--kurve); }
.i-cta:hover { background: #f28f33; }
.i-label { color: var(--l); }
.d-main { flex: 1; display: grid; grid-template-columns: minmax(0, 8fr) minmax(0, 3.6fr); gap: clamp(20px, 3.4vw, 56px); align-content: center; align-items: start; padding: clamp(16px, 3.4svh, 40px) var(--rand); }
.d-galerie { display: grid; gap: 12px; }
.d-zeile { display: flex; align-items: center; justify-content: space-between; gap: 12px; color: var(--l); }
.d-zeile b { color: var(--t); font-weight: 600; font-variant-numeric: tabular-nums; }
.d-zeile .d-titel { color: var(--t); text-transform: none; letter-spacing: 0; font-size: .9375rem; font-weight: 500; }
.d-steuer { display: flex; gap: 6px; }
.d-pfeil { width: 44px; height: 44px; border-radius: 50%; border: 1px solid rgba(49, 49, 49, .32); background: transparent; display: grid; place-items: center; cursor: pointer; transition: background-color 200ms var(--kurve), color 200ms var(--kurve), border-color 200ms var(--kurve), opacity 200ms var(--kurve); }
.d-pfeil:hover:not(:disabled), .d-pfeil.druck { background: var(--marke); border-color: var(--marke); color: #fff; }
.d-pfeil:disabled { opacity: .3; cursor: default; }
.d-band { grid-auto-columns: 100%; width: 100%; aspect-ratio: 16 / 10; max-height: 54svh; background: var(--sand); }
.d-band > li { scroll-snap-stop: always; }
.d-band > li { position: relative; overflow: hidden; }
.d-band img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
/* Daumen nativ: ::scroll-marker mit Bild aus einer Variable je Folie (Chrome/Edge ab 135) */
.d-band[data-marker='nativ'] { scroll-marker-group: after; }
/* Fallstrick Chrome 153: die Gruppe rechnet ohne feste Höhe 0 und nimmt keinen Platz ein */
.d-band::scroll-marker-group { display: grid; grid-template-columns: repeat(8, minmax(0, 1fr)); grid-auto-rows: var(--daumen-h, 58px); gap: 8px; height: var(--daumen-h, 58px); margin-top: 12px; }
.d-band > li::scroll-marker { content: '' / attr(data-titel); display: block; height: var(--daumen-h, 58px); background: var(--daumen) center / cover no-repeat, var(--sand); border-radius: 1px; opacity: .72; cursor: pointer; outline: 1px solid transparent; outline-offset: 3px; transition: opacity 200ms var(--kurve), outline-color 200ms var(--kurve); }
.d-band > li::scroll-marker:hover { opacity: 1; }
.d-band > li::scroll-marker:target-current { opacity: 1; outline: 2px solid var(--marke); }
.d-band > li::scroll-marker:focus-visible { outline: 2px solid var(--marke); outline-offset: 5px; }
/* Rückfall: eigene Buttons mit aria-current, alle Daumen sichtbar (Baymard) */
.d-daumen { display: grid; grid-template-columns: repeat(8, minmax(0, 1fr)); gap: 8px; margin-top: 0; }
.d-daumen[hidden] { display: none; }
.d-daumen button { position: relative; height: var(--daumen-h, 58px); padding: 0; border: 0; background: var(--sand); cursor: pointer; border-radius: 1px; outline: 1px solid transparent; outline-offset: 3px; transition: outline-color 200ms var(--kurve); }
.d-daumen img { width: 100%; height: 100%; object-fit: cover; opacity: .72; transition: opacity 200ms var(--kurve); }
.d-daumen button:hover img, .d-daumen button[aria-current='true'] img { opacity: 1; }
.d-daumen button[aria-current='true'] { outline: 2px solid var(--marke); }
.d-daumen button:focus-visible { outline: 2px solid var(--marke); outline-offset: 5px; }
.d-daten { display: grid; gap: 16px; align-content: start; }
.d-daten h2 { font: 400 clamp(2rem, 1.3rem + 2vw, 3.2rem)/1.08 'Libre Caslon Display', Georgia, serif; color: var(--marke); letter-spacing: -.005em; text-wrap: balance; }
.d-daten dl { display: grid; grid-template-columns: auto minmax(0, 1fr); border-top: 1px solid var(--ln); }
.d-daten dl > div { display: contents; }
.d-daten dt, .d-daten dd { padding: 9px 0; border-bottom: 1px solid var(--ln); }
.d-daten dt { color: var(--l); padding-right: 16px; line-height: 1.7; }
.d-daten dd { font-size: 1rem; font-variant-numeric: tabular-nums; text-align: right; }
.d-daten dd.d-preis { font: 400 1.5rem/1.2 'Libre Caslon Display', Georgia, serif; color: var(--marke); }
.d-fein { font-size: .8125rem; line-height: 1.5; color: var(--l); }
/* ============ Variante „Selbstlauf mit Pause“: Kulturhaus Ziegelei, Rezept kultur-programm-haltung ============ */
html[data-v='autoplay'] { --g: #efede9; --t: #0b0b0b; --l: #666564; --ln: #CBC9C5; --fokus: #0b0b0b; }
.w-a { --kurve: cubic-bezier(0.25, 1, 0.5, 1); --musik: #c4421a; --theater: #3d5f9a; --kunst: #2f7d4a; --film: #b0186b; font-family: 'Atkinson Hyperlegible Next', system-ui, sans-serif; }
.a-kopf { display: flex; align-items: center; gap: 16px 30px; padding: 14px var(--rand); border-bottom: 2px solid var(--t); }
.a-marke { font: 800 22px/1 'Schibsted Grotesk', sans-serif; letter-spacing: -.02em; }
.a-kopf nav { display: flex; gap: 24px; margin-left: auto; font-size: 1rem; }
.a-kopf nav a[aria-current] { text-decoration: underline; text-decoration-thickness: 2px; text-underline-offset: 6px; }
.a-tickets { min-height: 40px; display: inline-flex; align-items: center; padding: 0 16px; background: var(--t); color: var(--g); font-weight: 700; }
.a-main { flex: 1; display: grid; align-content: center; padding: clamp(14px, 3svh, 36px) var(--rand) clamp(14px, 3svh, 32px); }
.a-karussell { display: grid; grid-template-columns: minmax(0, 1fr) auto; grid-template-areas: 'titel steuer' 'band band' 'wahl wahl'; gap: 14px 24px; align-items: end; }
.a-karussell > h2 { grid-area: titel; font: 800 clamp(1.9rem, 1.2rem + 2.4vw, 3.4rem)/1 'Schibsted Grotesk', sans-serif; letter-spacing: -.025em; }
.a-karussell > h2 span { font: 500 .8125rem/1 'IBM Plex Mono', monospace; letter-spacing: .02em; vertical-align: .9em; margin-left: 10px; color: var(--l); }
.a-steuer { grid-area: steuer; display: flex; align-items: center; gap: 8px; }
/* Pause-Knopf mit Restzeit-Ring; zuerst in der Tab-Reihenfolge */
.a-pause { display: inline-flex; align-items: center; gap: 10px; min-height: 48px; padding: 0 16px 0 2px; border: 0; background: none; cursor: pointer; font: 500 .8125rem/1 'IBM Plex Mono', monospace; letter-spacing: .02em; }
.a-ring { position: relative; width: 44px; height: 44px; display: grid; place-items: center; border-radius: 50%; }
.a-ring svg { position: absolute; inset: 0; width: 44px; height: 44px; transform: rotate(-90deg); }
.a-ring circle { fill: none; stroke-width: 2; }
.a-ring .a-spur { stroke: var(--ln); }
.a-ring .a-rest { stroke: var(--t); stroke-linecap: butt; }
.a-symbol { width: 14px; height: 14px; position: relative; }
.a-symbol::before, .a-symbol::after { content: ''; position: absolute; top: 0; bottom: 0; width: 4px; background: var(--t); }
.a-symbol::before { left: 2px; } .a-symbol::after { right: 2px; }
.a-pause[aria-pressed='true'] .a-symbol::before { left: 3px; width: 0; height: 0; background: none; border-style: solid; border-width: 7px 0 7px 11px; border-color: transparent transparent transparent var(--t); }
.a-pause[aria-pressed='true'] .a-symbol::after { display: none; }
.a-pause:hover .a-ring { background: rgba(11, 11, 11, .06); }
/* Gedrückt = angehalten: gefüllter Ring, helles Symbol */
.a-pause[aria-pressed='true'] .a-ring { background: var(--t); }
.a-pause[aria-pressed='true'] .a-symbol::before { border-left-color: var(--g); }
.a-pause[aria-pressed='true'] .a-rest { stroke: var(--t); }
.a-status { min-width: 9.5em; font: 500 .75rem/1.3 'IBM Plex Mono', monospace; color: var(--l); }
.a-pfeil { width: 44px; height: 44px; border: 1.5px solid var(--t); background: transparent; display: grid; place-items: center; cursor: pointer; transition: background-color 200ms var(--kurve), color 200ms var(--kurve); }
.a-pfeil:hover, .a-pfeil.druck { background: var(--t); color: var(--g); }
.a-band { grid-area: band; grid-auto-columns: 100%; height: clamp(300px, 54svh, 500px); }
.a-band > li { scroll-snap-stop: always; }
.a-folie { height: 100%; display: grid; grid-template-columns: minmax(0, 5fr) minmax(0, 7fr); }
.a-block { display: grid; align-content: space-between; gap: 16px; padding: clamp(18px, 2.6vw, 36px); color: #fff; background: var(--sparte); }
.a-block .a-sparte { font: 500 .8125rem/1.3 'IBM Plex Mono', monospace; letter-spacing: .02em; }
.a-block h3 { font: 800 clamp(2rem, 1rem + 3.4vw, 4.6rem)/.98 'Schibsted Grotesk', sans-serif; letter-spacing: -.03em; text-wrap: balance; }
.a-block p { font-size: 1.0625rem; line-height: 1.5; max-width: 30ch; }
.a-unten { display: flex; justify-content: space-between; align-items: end; gap: 12px 20px; flex-wrap: wrap; }
.a-datum { font: 500 .9375rem/1.3 'IBM Plex Mono', monospace; font-variant-numeric: tabular-nums; }
.a-block a { font-weight: 700; text-decoration: underline; text-decoration-thickness: 2px; text-underline-offset: 5px; }
.a-block a:focus-visible { outline-color: #fff; }
.a-folie figure { position: relative; overflow: hidden; background: #d9d6d0; }
.a-folie img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
.a-wahl { grid-area: wahl; display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: 8px; }
.a-wahl button { position: relative; text-align: left; padding: 12px 0 4px; border: 0; background: none; cursor: pointer; font: 500 .8125rem/1.3 'IBM Plex Mono', monospace; color: var(--l); border-top: 2px solid var(--ln); transition: color 200ms var(--kurve), border-color 200ms var(--kurve); }
.a-wahl button b { font-weight: 500; color: var(--t); }
.a-wahl button:hover { color: var(--t); }
.a-wahl button[aria-current='true'] { color: var(--t); border-top-color: var(--t); }
/* ============ Variante „Anker-Links“: Vonderau Immobilien, Neubau Am Kornhaus ============ */
.k-main { flex: 1; display: grid; align-content: center; padding: clamp(14px, 3svh, 36px) var(--rand); }
.k-karussell { display: grid; grid-template-columns: minmax(0, 3.4fr) minmax(0, 8.6fr); gap: clamp(20px, 3.4vw, 56px); align-items: start; }
.k-links { display: grid; gap: 14px; align-content: start; }
.k-links h2 { font: 400 clamp(2rem, 1.3rem + 2vw, 3.2rem)/1.05 'Libre Caslon Display', Georgia, serif; color: var(--marke); }
.k-lauf { font-size: .9375rem; line-height: 1.55; color: var(--l); max-width: 34ch; }
/* Gebäudeschnitt als Navigation: jede Wohnung ein Link auf #wohnung-n */
.k-haus { margin-top: 6px; }
.k-etagen { list-style: none; display: grid; gap: 0; border-top: 3px solid var(--marke); }
.k-etage { display: grid; grid-template-columns: 44px minmax(0, 1fr); align-items: stretch; border-bottom: 1px solid var(--ln); }
.k-etage-name { display: grid; align-items: center; color: var(--l); font-size: .6875rem; }
.k-whgs { display: grid; grid-auto-flow: column; grid-auto-columns: minmax(0, 1fr); gap: 4px; padding: 4px 0; }
.k-etage--dg .k-whgs { margin-inline: 0 26%; }
.k-whg { position: relative; display: grid; gap: 2px; padding: 8px 10px; min-height: 52px; border: 1px solid rgba(2, 56, 92, .35); background: #fff; transition: background-color 200ms var(--kurve), color 200ms var(--kurve), border-color 200ms var(--kurve); }
.k-whg b { font: 400 1.125rem/1 'Libre Caslon Display', Georgia, serif; color: var(--marke); transition: color 200ms var(--kurve); }
.k-whg span:not(.sr) { font-size: .75rem; color: var(--t2); font-variant-numeric: tabular-nums; transition: color 200ms var(--kurve); }
.k-whg::after { content: ''; position: absolute; top: 9px; right: 9px; width: 7px; height: 7px; border-radius: 50%; background: #3f7d4a; }
.k-whg[data-status='reserviert']::after { background: none; box-shadow: inset 0 0 0 1.5px #b36b12; }
.k-whg[data-status='verkauft'] { background: repeating-linear-gradient(135deg, var(--sand) 0 6px, #fff 6px 9px); }
.k-whg[data-status='verkauft']::after { background: var(--l); }
.k-whg:hover { border-color: var(--marke); }
.k-whg[aria-current='true'] { background: var(--marke); border-color: var(--marke); }
.k-whg[aria-current='true'] b, .k-whg[aria-current='true'] span:not(.sr) { color: #fff; }
.k-boden { height: 10px; background: repeating-linear-gradient(135deg, var(--ln) 0 1px, transparent 1px 6px); }
.k-legende { display: flex; flex-wrap: wrap; gap: 6px 14px; color: var(--l); font-size: .6875rem; }
.k-legende span { display: inline-flex; align-items: center; gap: 6px; }
.k-legende span::before { content: ''; width: 7px; height: 7px; border-radius: 50%; background: #3f7d4a; }
.k-legende .r::before { background: none; box-shadow: inset 0 0 0 1.5px #b36b12; }
.k-legende .v::before { background: var(--l); }
.k-rechts { display: grid; gap: 12px; min-width: 0; }
.k-zeile { display: flex; justify-content: space-between; align-items: center; gap: 12px; }
.k-band { grid-auto-columns: 100%; }
.k-band > li { scroll-snap-stop: always; }
.k-folie { display: grid; grid-template-columns: minmax(0, 7fr) minmax(0, 4.4fr); background: var(--sand); min-height: 100%; }
.k-plan { padding: clamp(10px, 1.6vw, 22px); display: grid; align-content: center; gap: 6px; background: #fff; border: 1px solid var(--ln); margin: 10px; }
.k-plan svg { width: 100%; height: auto; max-height: 48svh; display: block; }
.k-plan figcaption { display: flex; justify-content: space-between; gap: 12px; font-size: .6875rem; letter-spacing: .08em; text-transform: uppercase; color: var(--t2); }
.k-daten { display: grid; gap: 14px; align-content: start; padding: clamp(16px, 2vw, 28px) clamp(16px, 2vw, 28px) clamp(16px, 2vw, 28px) clamp(6px, 1vw, 12px); }
.k-lage { color: var(--t2); }
.k-daten h3 { font: 400 clamp(1.6rem, 1.1rem + 1.4vw, 2.4rem)/1.05 'Libre Caslon Display', Georgia, serif; color: var(--marke); }
.k-daten dl { display: grid; grid-template-columns: auto minmax(0, 1fr); border-top: 1px solid rgba(49, 49, 49, .18); }
.k-daten dl > div { display: contents; }
.k-daten dt, .k-daten dd { padding: 7px 0; border-bottom: 1px solid rgba(49, 49, 49, .18); }
.k-daten dt { color: var(--t2); padding-right: 12px; line-height: 1.7; }
.k-daten dd { text-align: right; font-variant-numeric: tabular-nums; font-size: .9375rem; }
.k-status { justify-self: start; display: inline-flex; align-items: center; gap: 8px; font-size: .8125rem; font-weight: 600; }
.k-status::before { content: ''; width: 8px; height: 8px; border-radius: 50%; background: #3f7d4a; }
.k-status[data-status='reserviert']::before { background: none; box-shadow: inset 0 0 0 1.5px #b36b12; }
.k-status[data-status='verkauft']::before { background: var(--l); }
.k-folie .i-cta { justify-self: start; }
.k-pfeil { width: 44px; height: 44px; border-radius: 50%; border: 1px solid rgba(49, 49, 49, .32); background: transparent; display: grid; place-items: center; cursor: pointer; transition: background-color 200ms var(--kurve), color 200ms var(--kurve), border-color 200ms var(--kurve), opacity 200ms var(--kurve); }
.k-pfeil:hover:not(:disabled), .k-pfeil.druck { background: var(--marke); border-color: var(--marke); color: #fff; }
.k-pfeil:disabled { opacity: .3; cursor: default; }
.k-adresse { display: flex; align-items: center; gap: 10px 14px; flex-wrap: wrap; padding: 10px 14px; border: 1px solid var(--ln); background: #fff; }
.k-adresse-label { color: var(--l); }
.k-url { font: 500 .8125rem/1.3 'Geist Mono', ui-monospace, monospace; color: var(--t); overflow-wrap: anywhere; flex: 1; min-width: 12em; }
.k-url mark { background: none; color: var(--marke); box-shadow: inset 0 -2px 0 rgba(237, 127, 26, .7); }
.k-kopieren { min-height: 36px; padding: 0 14px; border: 1px solid var(--marke); background: transparent; color: var(--marke); font-weight: 600; font-size: .8125rem; cursor: pointer; transition: background-color 200ms var(--kurve), color 200ms var(--kurve); }
.k-kopieren:hover { background: var(--marke); color: #fff; }
.k-meldung { font-size: .8125rem; color: #2f6a3b; min-width: 7em; }
/* ============ Vorschau, Telefon, Touch, reduziert ============ */
.vorschau .z-kopf nav { visibility: hidden; }
.vorschau .vorschau .z-lauf, .vorschau .f-seitlich p, .vorschau .f-kopf nav, .vorschau .i-kopf nav, .vorschau .d-fein, .vorschau .k-lauf, .vorschau .a-kopf nav { display: none; }
@media (max-width: 1100px) {
.f-kopf nav, .i-kopf nav, .a-kopf nav { display: none; }
.i-cta, .f-paket, .a-tickets { margin-left: auto; }
.d-main { grid-template-columns: minmax(0, 1fr); }
.d-daten { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); align-items: start; }
.d-daten > :is(.i-label, h2) { grid-column: 1 / -1; }
}
@media (max-width: 860px) {
.k-karussell { grid-template-columns: minmax(0, 1fr); }
.k-folie { grid-template-columns: minmax(0, 1fr); }
.k-plan svg { max-height: none; }
}
@media (max-width: 760px) {
.z-kopf { grid-template-columns: minmax(0, 1fr) auto; }
.z-kopf nav { display: none; }
.z-marke { justify-self: start; }
.z-main { padding-left: 0; }
.z-karussell { grid-template-columns: minmax(0, 1fr); }
.z-text { padding-inline: var(--rand); }
.w-z { --z-breite: 82vw; }
.z-band { padding: 0 calc(100% - var(--z-breite) - var(--rand)) 6px var(--rand); scroll-padding-inline: var(--rand); }
.f-marke span { display: none; }
.f-titel { flex-direction: column; align-items: flex-start; }
.f-seitlich { justify-items: start; text-align: left; }
.w-f { --f-breite: 72vw; }
.i-marke span { display: none; }
.d-daten { grid-template-columns: minmax(0, 1fr); }
.d-band { aspect-ratio: 4 / 3; }
.d-band::scroll-marker-group, .d-daumen { grid-template-columns: repeat(4, minmax(0, 1fr)); }
.d-band::scroll-marker-group { height: calc(2 * var(--daumen-h, 58px) + 8px); }
.d-zeile .d-titel { display: none; }
.a-karussell { grid-template-columns: minmax(0, 1fr); grid-template-areas: 'titel' 'steuer' 'band' 'wahl'; }
.a-band { height: auto; }
.a-folie { grid-template-columns: minmax(0, 1fr); grid-template-rows: auto 48vw; }
.a-karussell > h2 span { display: block; margin: 8px 0 0; vertical-align: 0; }
.a-steuer { flex-wrap: wrap; }
.a-block h3 { font-size: clamp(1.9rem, 9vw, 2.6rem); }
.a-wahl button .t { display: none; }
.werte { text-align: left; }
}
@media (max-width: 420px) {
.a-wahl button .s { display: none; }
.z-buchen, .i-cta, .f-paket, .a-tickets { font-size: .6875rem; padding: 0 10px; }
.z-zaehler { min-width: 0; }
}
@media (max-width: 360px) {
.z-steuer { gap: 10px; }
.z-jetzt { font-size: 38px; }
.d-zeile { flex-wrap: wrap; }
}
@media (hover: none) { .zeiger { display: none !important; } }
.reduziert .z-folie img, .reduziert :is(.z-pfeil, .f-pfeil, .d-pfeil, .k-pfeil, .a-pfeil, .k-whg, .a-wahl button, .d-daumen button, .d-daumen img), .reduziert .d-band > li::scroll-marker, .reduziert .zeiger { transition: none; }
/* Kontrastmodus: Knöpfe mit Rahmen */
@media (forced-colors: active) {
button, [role=button], [role=tab], [role=switch] { border: 1px solid ButtonText !important; }
.f-fuellung { background: Highlight; }
.k-whg[aria-current='true'], .d-daumen button[aria-current='true'] { outline: 3px solid Highlight; }
}
</style>
</head>
<body>
<!-- ===================== Zähler: Haus Wiek, Seehotel in Zarrentin am Schaalsee ===================== -->
<div class="welt w-z" data-welt="zaehler">
<header class="z-kopf">
<nav aria-label="Hauptnavigation"><a href="#" aria-current="page">Zimmer</a><a href="#">Essen</a><a href="#">Sauna & Steg</a></nav>
<a class="z-marke" href="#">Haus <i>Wiek</i></a>
<a class="z-buchen" href="#">Verfügbarkeit prüfen</a>
</header>
<main class="z-main">
<section class="z-karussell" aria-roledescription="Karussell" aria-label="Zimmer und Suiten" data-z>
<div class="z-text">
<p class="z-label">Zimmer & Suiten · Zarrentin am Schaalsee</p>
<h2>Acht Zimmer, <em>alle</em> zum Wasser</h2>
<p class="z-lauf">Backsteinhaus von 1908 am Ostufer. Jedes Zimmer hat ein Fenster zum See, Frühstück gibt es bis zwölf.</p>
<div class="z-steuer">
<button type="button" class="z-pfeil pfeil" data-schritt="-1" aria-label="Vorheriges Zimmer" aria-controls="z-band"><svg viewBox="0 0 18 18" aria-hidden="true"><path d="M15 9H3m5-5L3 9l5 5" fill="none" stroke="currentColor" stroke-width="1.3"/></svg></button>
<p class="z-zaehler" aria-hidden="true"><span class="z-jetzt" data-z-jetzt><span>01</span></span><span class="z-trenn"></span><span class="z-ges">08</span></p>
<button type="button" class="z-pfeil pfeil" data-schritt="1" aria-label="Nächstes Zimmer" aria-controls="z-band"><svg viewBox="0 0 18 18" aria-hidden="true"><path d="M3 9h12m-5-5 5 5-5 5" fill="none" stroke="currentColor" stroke-width="1.3"/></svg></button>
</div>
<p class="sr" aria-live="polite" aria-atomic="true" data-status></p>
</div>
<ul class="band z-band" id="z-band" data-band tabindex="0" aria-label="Zimmer, mit Pfeiltasten blättern">
<li class="z-folie" role="group" aria-roledescription="Folie" aria-label="1 von 8: Seezimmer"><figure><img src="bilder/hotel-zimmer.webp" width="1200" height="800" alt="Helles Zimmer mit Doppelbett, Sessel und gelb gepolsterter Fensterbank, Sonnenflecken auf dem Holzboden" fetchpriority="high"></figure><div class="z-info"><h3>See<i>zimmer</i></h3><p class="z-preis">ab <b>168 €</b></p><p class="z-daten">24 m² · 2 Personen · 1. OG Seeseite</p></div></li>
<li class="z-folie" role="group" aria-roledescription="Folie" aria-label="2 von 8: Fensterplatz"><figure><img src="bilder/hotel-zimmer-fenster.webp" width="1200" height="800" alt="Breite Fensterbank mit gelbem Polster und Tasse, daneben eine Vase mit Zweigen, draußen der See" loading="lazy"></figure><div class="z-info"><h3>Fenster<i>platz</i></h3><p class="z-preis">ab <b>152 €</b></p><p class="z-daten">22 m² · 2 Personen · Leseplatz im Erker</p></div></li>
<li class="z-folie" role="group" aria-roledescription="Folie" aria-label="3 von 8: Kaminstube"><figure><img src="bilder/atmosphaere-partikel-kamin.jpg" width="1440" height="900" alt="Langes, niedriges Kaminfeuer in einer dunklen Putzwand, warmer Schein auf dem Boden" loading="lazy"></figure><div class="z-info"><h3>Kamin<i>stube</i></h3><p class="z-preis">ab <b>238 €</b></p><p class="z-daten">38 m² · Suite · eigener Kamin</p></div></li>
<li class="z-folie" role="group" aria-roledescription="Folie" aria-label="4 von 8: Bootshaus am Steg"><figure><img src="bilder/hotel-sauna.webp" width="1200" height="800" alt="Kleines Holzhaus mit zwei erleuchteten Fenstern auf dem Steg in der Dämmerung, Schilf, Spiegelung im See" loading="lazy"></figure><div class="z-info"><h3>Boots<i>haus</i></h3><p class="z-preis">ab <b>214 €</b></p><p class="z-daten">30 m² · 2 Personen · direkt am Steg</p></div></li>
<li class="z-folie" role="group" aria-roledescription="Folie" aria-label="5 von 8: Turmzimmer"><figure><img src="bilder/dialog-modal-nativ-turm.jpg" width="1600" height="1080" alt="Schmaler hoher Holzbau mit drei warm erleuchteten Fenstern am Ufer, dahinter Berge in violetter Dämmerung" loading="lazy"></figure><div class="z-info"><h3>Turm<i>zimmer</i></h3><p class="z-preis">ab <b>256 €</b></p><p class="z-daten">41 m² · drei Ebenen · Blick über den See</p></div></li>
<li class="z-folie" role="group" aria-roledescription="Folie" aria-label="6 von 8: Giebelsuite"><figure><img src="bilder/hotel-haus-am-see.webp" width="1920" height="1200" alt="Backsteinhaus mit erleuchteten Fenstern am See in der Abenddämmerung, alles spiegelt sich im Wasser" loading="lazy"></figure><div class="z-info"><h3>Giebel<i>suite</i></h3><p class="z-preis">ab <b>284 €</b></p><p class="z-daten">46 m² · bis 4 Personen · Dachgeschoss</p></div></li>
<li class="z-folie" role="group" aria-roledescription="Folie" aria-label="7 von 8: Uferblick"><figure><img src="bilder/buchungsleiste-datumsbereich-see.webp" width="1600" height="1000" alt="See in der Abenddämmerung vor flachen Hügeln, ein Steg mit kleinen Lichtern" loading="lazy"></figure><div class="z-info"><h3>Ufer<i>blick</i></h3><p class="z-preis">ab <b>112 €</b></p><p class="z-daten">16 m² · Einzelzimmer · 2. OG</p></div></li>
<li class="z-folie" role="group" aria-roledescription="Folie" aria-label="8 von 8: Winterzimmer"><figure><img src="bilder/split-screen-sticky-seiten-archiv-7.jpg" width="800" height="1000" alt="Heller, winterlicher See mit Eisflächen, am fernen Ufer ein Schilfgürtel" loading="lazy"></figure><div class="z-info"><h3>Winter<i>zimmer</i></h3><p class="z-preis">ab <b>138 €</b></p><p class="z-daten">20 m² · 2 Personen · Nov. bis März</p></div></li>
</ul>
</section>
</main>
</div>
<!-- ===================== Fortschrittsleiste: Weingut Kalkofen, Sommerach am Main ===================== -->
<div class="welt w-f" data-welt="fortschritt" hidden>
<header class="f-kopf">
<a class="f-marke" href="#"><b>Weingut <i>Kalkofen</i></b><span>Sommerach am Main</span></a>
<nav aria-label="Hauptnavigation"><a href="#">Weine</a><a href="#" aria-current="page">Flurstücke</a><a href="#">Hofladen</a><a href="#">Besuch</a></nav>
<a class="f-paket" href="#">Probierpaket</a>
</header>
<main class="f-main">
<section class="f-karussell" aria-roledescription="Karussell" aria-label="Flurstücke des Hofs" data-f>
<div class="f-titel">
<div><span class="f-label">Lagen & Flurstücke · 12</span><h2>Zwölf Flurstücke, <i>ein Hof</i></h2></div>
<div class="f-seitlich">
<p>Neun Weinlagen am Main, dazu Garten, Weide und Hofstelle. Was im Hofladen liegt, wächst hier.</p>
<div class="f-steuer">
<button type="button" class="f-pfeil pfeil" data-schritt="-1" aria-label="Vorheriges Flurstück" aria-controls="f-band"><svg viewBox="0 0 18 18" aria-hidden="true"><path d="M15 9H3m5-5L3 9l5 5" fill="none" stroke="currentColor" stroke-width="1.3"/></svg></button>
<button type="button" class="f-pfeil pfeil" data-schritt="1" aria-label="Nächstes Flurstück" aria-controls="f-band"><svg viewBox="0 0 18 18" aria-hidden="true"><path d="M3 9h12m-5-5 5 5-5 5" fill="none" stroke="currentColor" stroke-width="1.3"/></svg></button>
</div>
</div>
</div>
<ul class="band f-band" id="f-band" data-band tabindex="0" aria-label="Flurstücke, mit Pfeiltasten blättern" data-f-band></ul>
<div class="f-fussleiste"><div class="f-leiste" aria-hidden="true"><span class="f-fuellung" data-f-fuellung></span></div><p class="f-anzahl" aria-hidden="true" data-f-anzahl>01–04 / 12</p></div>
<p class="sr" aria-live="polite" aria-atomic="true" data-status></p>
</section>
</main>
</div>
<!-- ===================== Daumen: Vonderau Immobilien, Holzhaus in Ingelheim ===================== -->
<div class="welt w-d" data-welt="daumen" hidden>
<header class="i-kopf">
<a class="i-marke" href="#"><b>Vonderau</b><span>Immobilien · Mainz</span></a>
<nav aria-label="Hauptnavigation"><a href="#" aria-current="page">Kaufen</a><a href="#">Neubau</a><a href="#">Bewerten</a><a href="#">Büro</a></nav>
<a class="i-cta" href="#">Besichtigung anfragen</a>
</header>
<main class="d-main">
<section class="d-galerie" aria-roledescription="Karussell" aria-label="Bilder Holzhaus mit Weitblick" data-d>
<div class="d-zeile">
<p><b data-d-nr>01</b> / 08<span class="d-titel"> · <span data-d-titel>Ansicht von Süden</span></span></p>
<div class="d-steuer">
<button type="button" class="d-pfeil pfeil" data-schritt="-1" aria-label="Vorheriges Bild" aria-controls="d-band"><svg viewBox="0 0 18 18" aria-hidden="true"><path d="M15 9H3m5-5L3 9l5 5" fill="none" stroke="currentColor" stroke-width="1.3"/></svg></button>
<button type="button" class="d-pfeil pfeil" data-schritt="1" aria-label="Nächstes Bild" aria-controls="d-band"><svg viewBox="0 0 18 18" aria-hidden="true"><path d="M3 9h12m-5-5 5 5-5 5" fill="none" stroke="currentColor" stroke-width="1.3"/></svg></button>
</div>
</div>
<ul class="band d-band" id="d-band" data-band tabindex="0" aria-label="Bilder, mit Pfeiltasten blättern" data-marker="nativ" data-d-band></ul>
<div class="d-daumen" role="group" aria-label="Vorschaubilder" data-d-daumen hidden></div>
<p class="sr" aria-live="polite" aria-atomic="true" data-status></p>
</section>
<div class="d-daten">
<p class="i-label">Exposé 24-117 · Ingelheim am Rhein</p>
<h2>Holzhaus mit Weitblick</h2>
<dl>
<div><dt>Wohnfläche</dt><dd>168 m²</dd></div>
<div><dt>Zimmer</dt><dd>5</dd></div>
<div><dt>Grundstück</dt><dd>940 m²</dd></div>
<div><dt>Baujahr</dt><dd>2019</dd></div>
<div><dt>Energie</dt><dd>Bedarf 24 kWh/(m²·a) · A+</dd></div>
<div><dt>Kaufpreis</dt><dd class="d-preis">895.000 €</dd></div>
</dl>
<p class="d-fein">Energieausweis, Provision und Pflichtangaben sind Beispielwerte und vom Kunden prüfen zu lassen.</p>
</div>
</main>
</div>
<!-- ===================== Selbstlauf mit Pause: Kulturhaus Ziegelei, Rendsburg ===================== -->
<div class="welt w-a" data-welt="autoplay" hidden>
<header class="a-kopf">
<a class="a-marke" href="#">Kulturhaus Ziegelei</a>
<nav aria-label="Hauptnavigation"><a href="#" aria-current="page">Programm</a><a href="#">Ausstellungen</a><a href="#">Kurse</a><a href="#">Besuch</a></nav>
<a class="a-tickets" href="#">Tickets</a>
</header>
<main class="a-main">
<section class="a-karussell" aria-roledescription="Karussell" aria-label="Programm-Höhepunkte im November" data-a>
<h2>Im November <span>Rendsburg · Am Kanal 4</span></h2>
<div class="a-steuer">
<button type="button" class="a-pause" aria-pressed="false" data-a-pause>
<span class="a-ring" aria-hidden="true"><svg viewBox="0 0 44 44"><circle class="a-spur" cx="22" cy="22" r="20"/><circle class="a-rest" cx="22" cy="22" r="20" data-a-rest/></svg><span class="a-symbol"></span></span>
Wechsel anhalten
</button>
<span class="a-status" aria-hidden="true" data-a-status>läuft</span>
<button type="button" class="a-pfeil pfeil" data-schritt="-1" aria-label="Vorherige Veranstaltung" aria-controls="a-band"><svg viewBox="0 0 18 18" aria-hidden="true"><path d="M15 9H3m5-5L3 9l5 5" fill="none" stroke="currentColor" stroke-width="1.6"/></svg></button>
<button type="button" class="a-pfeil pfeil" data-schritt="1" aria-label="Nächste Veranstaltung" aria-controls="a-band"><svg viewBox="0 0 18 18" aria-hidden="true"><path d="M3 9h12m-5-5 5 5-5 5" fill="none" stroke="currentColor" stroke-width="1.6"/></svg></button>
</div>
<ul class="band a-band" id="a-band" data-band aria-live="off" data-a-band>
<li role="group" aria-roledescription="Folie" aria-label="1 von 5: Ton, Feuer, Form"><article class="a-folie" style="--sparte: var(--kunst)"><div class="a-block"><p class="a-sparte">Ausstellung · Halle 2</p><div><h3>Ton, Feuer, Form</h3></div><div class="a-unten"><div><p>Keramik aus vier Werkstätten an der Eider, mit offenen Brennterminen.</p><p class="a-datum">bis 17.01.2027</p></div><a href="#">Zur Ausstellung</a></div></div><figure><img src="bilder/keramik-stillleben.jpg" width="1200" height="800" alt="Helle Flaschenvase, schwarzes Kugelgefäß und kleine Schalen auf Gipssockeln vor Terrakotta" loading="lazy"></figure></article></li>
<li role="group" aria-roledescription="Folie" aria-label="2 von 5: Lichter über dem Kanal"><article class="a-folie" style="--sparte: var(--musik)"><div class="a-block"><p class="a-sparte">Konzert · Ringofen</p><div><h3>Lichter über dem Kanal</h3></div><div class="a-unten"><div><p>Ensemble Nordlicht spielt Pärt und Satie im alten Brennofen.</p><p class="a-datum">Sa 14.11. · 20 Uhr</p></div><a href="#">Tickets ab 18 €</a></div></div><figure><img src="bilder/atmosphaere-partikel-see.jpg" width="1440" height="900" alt="Unscharfe Lichterkette und Turmsilhouette über dunklem, stillem Wasser bei Nacht" loading="lazy"></figure></article></li>
<li role="group" aria-roledescription="Folie" aria-label="3 von 5: Die Ruhe der Dinge"><article class="a-folie" style="--sparte: var(--theater)"><div class="a-block"><p class="a-sparte">Lesung · Trockenboden</p><div><h3>Die Ruhe der Dinge</h3></div><div class="a-unten"><div><p>Lesung und Gespräch über Gegenstände, die bleiben.</p><p class="a-datum">Do 19.11. · 19:30 Uhr</p></div><a href="#">Platz reservieren</a></div></div><figure><img src="bilder/magazin-doppelseite.jpg" width="1200" height="800" alt="Aufgeschlagenes Magazin mit Keramik-Foto und einem Artikel auf hellem Tisch" loading="lazy"></figure></article></li>
<li role="group" aria-roledescription="Folie" aria-label="4 von 5: Nachtfahrt"><article class="a-folie" style="--sparte: var(--film)"><div class="a-block"><p class="a-sparte">Kino · Saal am Kanal</p><div><h3>Nachtfahrt</h3></div><div class="a-unten"><div><p>Kurzfilme vom Wasser, live vertont von der Musikschule.</p><p class="a-datum">Fr 27.11. · 21 Uhr</p></div><a href="#">Tickets ab 9 €</a></div></div><figure><img src="bilder/player-einbettung-zwei-klick-poster.webp" width="1280" height="720" alt="Dunkle Silhouette einer Seebrücke über leuchtend blauem Wasser bei Nacht, ein rot-weißer Pegel" loading="lazy"></figure></article></li>
<li role="group" aria-roledescription="Folie" aria-label="5 von 5: Plakate setzen"><article class="a-folie" style="--sparte: #0b0b0b"><div class="a-block"><p class="a-sparte">Werkstatt · Druckerei</p><div><h3>Plakate setzen</h3></div><div class="a-unten"><div><p>Bleisatz und Linolschnitt für alle ab 12 Jahren.</p><p class="a-datum">So 6.12. · 11–16 Uhr</p></div><a href="#">Anmelden</a></div></div><figure><img src="bilder/typo-plakat.jpg" width="1200" height="800" alt="Plakat mit dem Wort LICHT in schmaler Grotesk, einer orangen Scheibe und einer kursiven Zeile auf Limettengrün" loading="lazy"></figure></article></li>
</ul>
<div class="a-wahl" role="group" aria-label="Veranstaltung wählen" data-a-wahl>
<button type="button" aria-current="true" aria-controls="a-band" aria-label="01 Kunst: Ton, Feuer, Form"><b>01</b><span class="s"> Kunst</span><span class="t"> · Ton, Feuer, Form</span></button>
<button type="button" aria-controls="a-band" aria-label="02 Musik: Lichter über dem Kanal"><b>02</b><span class="s"> Musik</span><span class="t"> · Lichter über dem Kanal</span></button>
<button type="button" aria-controls="a-band" aria-label="03 Lesung: Die Ruhe der Dinge"><b>03</b><span class="s"> Lesung</span><span class="t"> · Die Ruhe der Dinge</span></button>
<button type="button" aria-controls="a-band" aria-label="04 Film: Nachtfahrt"><b>04</b><span class="s"> Film</span><span class="t"> · Nachtfahrt</span></button>
<button type="button" aria-controls="a-band" aria-label="05 Werkstatt: Plakate setzen"><b>05</b><span class="s"> Werkstatt</span><span class="t"> · Plakate setzen</span></button>
</div>
</section>
</main>
</div>
<!-- ===================== Anker-Links: Vonderau Immobilien, Neubau Am Kornhaus ===================== -->
<div class="welt w-k" data-welt="ankerlinks" hidden>
<header class="i-kopf">
<a class="i-marke" href="#"><b>Vonderau</b><span>Immobilien · Mainz</span></a>
<nav aria-label="Hauptnavigation"><a href="#">Kaufen</a><a href="#" aria-current="page">Neubau</a><a href="#">Bewerten</a><a href="#">Büro</a></nav>
<a class="i-cta" href="#">Beratung vereinbaren</a>
</header>
<main class="k-main">
<section class="k-karussell" aria-roledescription="Karussell" aria-label="Wohnungen Am Kornhaus" data-k>
<div class="k-links">
<p class="i-label">Neubau · Mainz-Gonsenheim · Bezug Sommer 2027</p>
<h2>Am Kornhaus</h2>
<p class="k-lauf">Sieben Eigentumswohnungen, Effizienzhaus 40, Wärmepumpe und Aufzug. Jede Wohnung hat eine eigene Adresse zum Weitergeben.</p>
<nav class="k-haus" aria-label="Wohnungen im Haus">
<ol class="k-etagen" data-k-haus></ol>
<div class="k-boden" aria-hidden="true"></div>
</nav>
<p class="k-legende" aria-hidden="true"><span>verfügbar</span><span class="r">reserviert</span><span class="v">verkauft</span></p>
</div>
<div class="k-rechts">
<div class="k-zeile">
<p class="i-label"><b data-k-nr>W1</b> · Grundriss und Daten</p>
<div class="d-steuer">
<button type="button" class="k-pfeil pfeil" data-schritt="-1" aria-label="Vorherige Wohnung" aria-controls="k-band"><svg viewBox="0 0 18 18" aria-hidden="true"><path d="M15 9H3m5-5L3 9l5 5" fill="none" stroke="currentColor" stroke-width="1.3"/></svg></button>
<button type="button" class="k-pfeil pfeil" data-schritt="1" aria-label="Nächste Wohnung" aria-controls="k-band"><svg viewBox="0 0 18 18" aria-hidden="true"><path d="M3 9h12m-5-5 5 5-5 5" fill="none" stroke="currentColor" stroke-width="1.3"/></svg></button>
</div>
</div>
<ul class="band k-band" id="k-band" data-band tabindex="0" aria-label="Wohnungen, mit Pfeiltasten blättern" data-k-band></ul>
<div class="k-adresse">
<span class="k-adresse-label">Direktlink</span>
<output class="k-url" data-k-url>vonderau-immobilien.de/am-kornhaus<mark>#wohnung-1</mark></output>
<button type="button" class="k-kopieren" data-k-kopieren>Link kopieren</button>
<span class="k-meldung" role="status" data-k-meldung></span>
</div>
<p class="sr" aria-live="polite" aria-atomic="true" data-status></p>
</div>
</section>
</main>
</div>
<footer class="fuss mono">
<div class="pultspalte">
<div class="modi" role="group" aria-label="Variante">
<span>Variante</span>
<button type="button" data-v-wahl="zaehler" aria-pressed="true">Zähler</button>
<button type="button" data-v-wahl="fortschritt" aria-pressed="false">Fortschrittsleiste</button>
<button type="button" data-v-wahl="daumen" aria-pressed="false">Daumen</button>
<button type="button" data-v-wahl="autoplay" aria-pressed="false">Selbstlauf + Pause</button>
<button type="button" data-v-wahl="ankerlinks" aria-pressed="false">Anker-Links</button>
</div>
<div class="modi" role="group" aria-label="Marker" data-marker-modi hidden>
<span>Daumen</span>
<button type="button" data-marker-knopf="nativ" aria-pressed="true">::scroll-marker</button>
<button type="button" data-marker-knopf="eigene" aria-pressed="false">Rückfall: Buttons</button>
</div>
</div>
<p class="werte" data-werte></p>
</footer>
<svg class="zeiger" data-zeiger viewBox="0 0 22 22" aria-hidden="true"><path d="M3 2 18.5 11l-7 1.5-3.4 6.5Z" fill="#222" stroke="#fff" stroke-width="1.4" stroke-linejoin="round"/></svg>
<script>
/* ============ Gemeinsam: Kurve, Band, Vorführung ============ */
var STEUER = {};
const html = document.documentElement;
const reduziert = () => html.classList.contains('reduziert');
const werteEl = document.querySelector('[data-werte]');
const zeiger = document.querySelector('[data-zeiger]');
const zwei = i => String(i + 1).padStart(2, '0');
// cubic-bezier als Funktion (für das Gleiten des Scrollers mit der Kurve des Rezepts)
function bezier(x1, y1, x2, y2) {
const cx = 3 * x1, bx = 3 * (x2 - x1) - cx, ax = 1 - cx - bx, cy = 3 * y1, by = 3 * (y2 - y1) - cy, ay = 1 - cy - by;
const X = t => ((ax * t + bx) * t + cx) * t, Y = t => ((ay * t + by) * t + cy) * t, dX = t => (3 * ax * t + 2 * bx) * t + cx;
return x => {
if (x <= 0) return 0; if (x >= 1) return 1;
let t = x;
for (let i = 0; i < 8; i++) { const e = X(t) - x; if (Math.abs(e) < 1e-5) return Y(t); const d = dX(t); if (Math.abs(d) < 1e-6) break; t -= e / d; }
let lo = 0, hi = 1; t = x;
for (let i = 0; i < 24; i++) { if (X(t) < x) lo = t; else hi = t; t = (lo + hi) / 2; }
return Y(t);
};
}
// Band: echter Scroller mit Scroll-Snap. Aktive Folie = nächster Einrastpunkt. Pfeile/Marker gleiten per JS.
function Band(band, opt) {
const folien = () => [...band.children];
let aktiv = -1, anim = 0, gleitet = false, ruheUhr = 0, leise = false;
const pad = () => parseFloat(getComputedStyle(band).scrollPaddingInlineStart) || 0;
const max = () => band.scrollWidth - band.clientWidth;
const pos = i => Math.max(0, Math.min(folien()[i].offsetLeft - pad(), max()));
function index() {
const x = band.scrollLeft; let b = 0, d = Infinity;
folien().forEach((s, i) => { const e = Math.abs(pos(i) - x); if (e < d - .5) { d = e; b = i; } });
return b;
}
function setze(i, quelle) { if (i === aktiv) return; const alt = aktiv; aktiv = i; if (opt.wechsel) opt.wechsel(i, alt, quelle); }
function ruhe() { if (!leise && opt.ruhe) opt.ruhe(aktiv); }
function abbrechen() { if (!gleitet) return; cancelAnimationFrame(anim); gleitet = false; band.style.scrollSnapType = ''; }
function gleite(ziel, ms) {
abbrechen();
const start = band.scrollLeft, d = ziel - start;
if (reduziert() || !ms || Math.abs(d) < 1) {
band.style.scrollSnapType = 'none'; band.scrollLeft = ziel; band.style.scrollSnapType = '';
clearTimeout(ruheUhr); ruheUhr = setTimeout(ruhe, 60); return;
}
band.style.scrollSnapType = 'none'; gleitet = true; // Snap aus, sonst zieht der Browser während der Kurve
const t0 = performance.now(), k = opt.kurve;
const schritt = jetzt => {
const p = Math.min(1, (jetzt - t0) / ms);
band.scrollLeft = start + d * k(p);
if (p < 1) anim = requestAnimationFrame(schritt);
else { gleitet = false; band.style.scrollSnapType = ''; ruhe(); }
};
anim = requestAnimationFrame(schritt);
}
function zu(i, ms) {
const n = folien().length;
i = Math.max(0, Math.min(n - 1, i));
setze(i, 'knopf');
gleite(pos(i), ms === undefined ? opt.dauer : ms);
}
band.addEventListener('scroll', () => {
if (gleitet) return;
setze(index(), 'scroll');
clearTimeout(ruheUhr); ruheUhr = setTimeout(ruhe, 140);
}, { passive: true });
['wheel', 'touchstart', 'pointerdown'].forEach(t => band.addEventListener(t, abbrechen, { passive: true }));
band.addEventListener('keydown', e => {
if (e.target !== band) return;
const n = folien().length;
const ziel = { ArrowRight: aktiv + 1, ArrowLeft: aktiv - 1, Home: 0, End: n - 1 }[e.key];
if (ziel === undefined) return;
e.preventDefault(); zu(opt.schleife ? (ziel + n) % n : ziel);
});
return {
zu, gleite, pos, max,
weiter: r => { const n = folien().length; zu(opt.schleife ? (aktiv + r + n) % n : aktiv + r); },
aktiv: () => aktiv, anzahl: () => folien().length,
amEnde: () => pos(Math.max(aktiv, 0)) >= max() - 1,
sofort(i) { leise = true; abbrechen(); aktiv = -1; band.scrollLeft = pos(i); setze(i, 'start'); setTimeout(() => { leise = false; }, 300); },
neu() { setze(index(), 'scroll'); }
};
}
// Pfeile: eine Folie weiter; am Anfang/Ende gesperrt (außer Schleife)
function pfeileVerbinden(knoepfe, B, schleife) {
knoepfe.forEach(k => k.addEventListener('click', () => B.weiter(+k.dataset.schritt)));
return () => {
if (schleife) return;
knoepfe.forEach(k => { k.disabled = +k.dataset.schritt < 0 ? B.aktiv() <= 0 : B.amEnde(); });
};
}
// Vorführung: Schleife ohne Nutzer, abbrechbar
function Vorfuehrung(schritt) {
let id = 0, uhr = 0;
const warte = ms => new Promise(r => { uhr = setTimeout(r, ms); });
return {
start() {
const mein = ++id; html.classList.add('vorfuehrung');
(async () => { while (mein === id) await schritt(() => mein === id, warte); })();
},
stopp() { id++; clearTimeout(uhr); }
};
}
function zeigerZu(el, dx = 0, dy = 0) {
const r = el.getBoundingClientRect();
if (!r.width || r.bottom < 0 || r.top > innerHeight) return false;
zeiger.style.transform = `translate(${r.left + r.width / 2 + dx - 4}px, ${r.top + r.height / 2 + dy - 2}px)`;
return true;
}
function zeigerStart(el) {
const r = el.getBoundingClientRect();
zeiger.style.transition = 'none';
zeiger.style.transform = `translate(${r.left - 80}px, ${r.bottom + 80}px)`;
zeiger.getBoundingClientRect(); zeiger.style.transition = '';
}
function druck(el) { el.classList.add('druck'); setTimeout(() => el.classList.remove('druck'), 170); }
/* ============ Zähler: Haus Wiek ============ */
(() => {
const welt = document.querySelector('[data-welt=zaehler]');
const band = welt.querySelector('[data-band]'), knoepfe = [...welt.querySelectorAll('[data-schritt]')];
const jetzt = welt.querySelector('[data-z-jetzt]'), status = welt.querySelector('[data-status]');
const namen = [...band.children].map(s => s.getAttribute('aria-label').split(': ')[1]);
const KURVE = 'cubic-bezier(0.37, 0, 0.63, 1)';
// Die aktuelle Zahl rollt in Laufrichtung durch eine Maske von 1lh
function rolle(i, alt) {
const neu = document.createElement('span'); neu.textContent = zwei(i);
const altEl = jetzt.lastElementChild;
if (reduziert() || alt < 0 || !altEl) { jetzt.replaceChildren(neu); return; }
const r = i > alt ? 1 : -1;
[...jetzt.children].slice(0, -1).forEach(x => x.remove());
jetzt.append(neu);
altEl.animate([{ transform: 'translateY(0)' }, { transform: `translateY(${-105 * r}%)` }], { duration: 600, easing: KURVE, fill: 'forwards' }).finished.then(() => altEl.remove(), () => {});
neu.animate([{ transform: `translateY(${105 * r}%)` }, { transform: 'translateY(0)' }], { duration: 600, easing: KURVE });
}
let pfeile = () => {};
const B = Band(band, {
dauer: 900, kurve: bezier(0.37, 0, 0.63, 1),
wechsel(i, alt) { rolle(i, alt); pfeile(); },
ruhe(i) { status.textContent = `Zimmer ${i + 1} von 8: ${namen[i]}`; }
});
pfeile = pfeileVerbinden(knoepfe, B);
const V = Vorfuehrung(async (ok, warte) => {
const r = B.amEnde() ? -1 : B.aktiv() <= 0 ? 1 : (V.r || 1); V.r = r;
const k = knoepfe[r > 0 ? 1 : 0];
const sicht = zeigerZu(k); await warte(sicht ? 760 : 300); if (!ok()) return;
druck(k); B.weiter(r);
await warte(1700);
});
STEUER.zaehler = {
werte: 'Zähler <b>„03 / 08“ tabular-nums</b> · Ziffer rollt <b>translateY ±105 % in 1lh · 600 ms</b> · Gleiten <b>900 ms cubic-bezier(0.37, 0, 0.63, 1)</b> · snap <b>start</b>, Endabstand <b>100 % − Folienbreite</b> · Live-Region <b>„Zimmer 3 von 8“ nach dem Einrasten</b>',
start() { B.sofort(0); pfeile(); },
stopp() { V.stopp(); },
neustart() { V.stopp(); html.classList.remove('vorfuehrung'); B.sofort(0); pfeile(); },
vorfuehren() { V.stopp(); zeigerStart(knoepfe[1]); V.start(); },
pause() { V.stopp(); },
weiter() { if (html.classList.contains('vorfuehrung')) V.start(); }
};
})();
/* ============ Fortschrittsleiste: Weingut Kalkofen ============ */
(() => {
const welt = document.querySelector('[data-welt=fortschritt]');
const band = welt.querySelector('[data-band]'), knoepfe = [...welt.querySelectorAll('[data-schritt]')];
const fuellung = welt.querySelector('[data-f-fuellung]'), anzahl = welt.querySelector('[data-f-anzahl]'), status = welt.querySelector('[data-status]');
const FLUR = [
['kreuzleite', 'Kreuzleite', 'Weinlage', 'Muschelkalk', '1,4 ha', 'Silvaner', 'Steiler Rebhang über einem Hof, Parzellen in Zeilenrichtung'],
['sonnwendberg', 'Sonnwendberg', 'Weinlage', 'Muschelkalk', '2,1 ha', 'Riesling, Silvaner', 'Breiter Südhang mit Rebparzellen bis zum Horizont'],
['grauer-buehl', 'Grauer Bühl', 'Weinlage', 'Keuper', '1,8 ha', 'Müller-Thurgau', 'Weite, flache Kuppe mit Rebparzellen in Gelb und Grün'],
['birnbaumleite', 'Birnbaumleite', 'Weinlage', 'Löss', '0,9 ha', 'Bacchus', 'Sanfter Hang mit Rebparzellen, einzelnen Birnbäumen und Waldkante'],
['kirschrain', 'Kirschrain', 'Weinlage', 'Löss', '0,6 ha', 'Domina', 'Grashang mit einer Reihe Kirschbäume'],
['muehlwinkel', 'Mühlwinkel', 'Weinlage', 'Muschelkalk', '1,1 ha', 'Silvaner', 'Rebhang am Fluss, kleines weißes Gebäude am Ufer'],
['lettenacker', 'Lettenacker', 'Weinlage', 'Lettenkeuper', '0,8 ha', 'Scheurebe', 'Dorf mit Kirchturm und roten Dächern vor Rebhängen'],
['zwetschgengrund', 'Zwetschgengrund', 'Weinlage', 'Muschelkalk', '0,7 ha', 'Spätburgunder', 'Taleinschnitt mit Obstbäumen und steilen grünen Hängen'],
['hohe-wiese', 'Hohe Wiese', 'Weinlage', 'Keuper', '1,2 ha', 'Riesling', 'Wiesenhochfläche mit Feldweg, bewaldete Kuppen am Horizont'],
['hofstelle', 'Hofstelle', 'Hof', 'Auelehm', '0,4 ha', 'Hofladen, Kelter', 'Hofgebäude mit roten Dächern zwischen Fluss und Rebhang'],
['gemuesegarten', 'Gemüsegarten', 'Garten', 'Auelehm', '0,3 ha', 'Kürbis, Bohnen, Kräuter', 'Hang über dem Fluss, Ortsrand mit roten Dächern, Gartenbeete'],
['huehnerweide', 'Hühnerweide', 'Weide', 'Auelehm', '0,5 ha', 'Eier für den Hofladen', 'Grünfläche mit Hühnermobil am Fluss, Rebhang dahinter']
];
band.innerHTML = FLUR.map(([bild, name, art, boden, flaeche, was, alt], i) => `<li class="f-karte" role="group" aria-roledescription="Folie" aria-label="${i + 1} von 12: ${name}">
<figure><img src="bilder/lagen-steckbrief-karte-${bild}.webp" width="900" height="600" alt="Geländemodell: ${alt}" loading="lazy"></figure>
<p class="f-nr">${zwei(i)} · ${art}</p><h3>${name}</h3>
<dl><dt>Boden</dt><dd>${boden}</dd><dt>Fläche</dt><dd>${flaeche}</dd><dt>${art === 'Weinlage' ? 'Reben' : 'Ertrag'}</dt><dd>${was}</dd></dl></li>`).join('');
const zeitleiste = CSS.supports('animation-timeline: scroll()');
// Anteil, der ohne Scrollen schon sichtbar ist: Startwert der Füllung
function messen() {
const anteil = Math.min(1, band.clientWidth / band.scrollWidth);
fuellung.style.setProperty('--anteil', anteil.toFixed(4));
if (!zeitleiste) rueckfall();
bereich();
}
function rueckfall() { // JS-Rückfall ohne scroll-timeline
const a = parseFloat(fuellung.style.getPropertyValue('--anteil')) || .3, m = band.scrollWidth - band.clientWidth;
fuellung.style.scale = `${a + (1 - a) * (m > 0 ? band.scrollLeft / m : 1)} 1`;
}
if (!zeitleiste) band.addEventListener('scroll', rueckfall, { passive: true });
function bereich() {
const links = band.scrollLeft, rechts = links + band.clientWidth, karten = [...band.children];
const sichtbar = karten.map((k, i) => [i, k.offsetLeft >= links - 2 && k.offsetLeft + k.offsetWidth <= rechts + 2]).filter(x => x[1]).map(x => x[0]);
if (!sichtbar.length) return;
const a = sichtbar[0], z = sichtbar[sichtbar.length - 1];
anzahl.textContent = `${zwei(a)}${z > a ? '–' + zwei(z) : ''} / 12`;
return [a, z];
}
let pfeile = () => {};
const B = Band(band, {
dauer: 700, kurve: bezier(0.37, 0, 0.63, 1),
wechsel() { pfeile(); },
ruhe() { const b = bereich(); if (b) status.textContent = `Flurstück ${b[0] + 1} bis ${b[1] + 1} von 12`; pfeile(); }
});
pfeile = pfeileVerbinden(knoepfe, B);
new ResizeObserver(messen).observe(band);
const V = Vorfuehrung(async (ok, warte) => {
const r = B.amEnde() ? -1 : B.aktiv() <= 0 ? 1 : (V.r || 1); V.r = r;
const k = knoepfe[r > 0 ? 1 : 0];
const sicht = zeigerZu(k); await warte(sicht ? 700 : 300); if (!ok()) return;
druck(k); B.weiter(r);
await warte(1300);
});
STEUER.fortschritt = {
werte: () => `Leiste <b>2 px</b>, Füllung <b>scale ${zeitleiste ? 'per scroll-timeline + timeline-scope' : 'per Scroll-Handler (Rückfall)'}</b> von <b>sichtbarem Anteil → 1</b> · Gleiten <b>700 ms cubic-bezier(0.37, 0, 0.63, 1)</b> · Karte <b>clamp(232px, 21.5vw, 330px)</b> · Leiste <b>aria-hidden</b>, Bereich als Text`,
start() { B.sofort(0); messen(); pfeile(); },
stopp() { V.stopp(); },
neustart() { V.stopp(); html.classList.remove('vorfuehrung'); B.sofort(0); messen(); pfeile(); },
vorfuehren() { V.stopp(); zeigerStart(knoepfe[1]); V.start(); },
pause() { V.stopp(); },
weiter() { if (html.classList.contains('vorfuehrung')) V.start(); }
};
})();
/* ============ Daumen: Vonderau, Holzhaus in Ingelheim ============ */
(() => {
const welt = document.querySelector('[data-welt=daumen]');
const band = welt.querySelector('[data-band]'), knoepfe = [...welt.querySelectorAll('[data-schritt]')];
const leiste = welt.querySelector('[data-d-daumen]'), status = welt.querySelector('[data-status]');
const nrEl = welt.querySelector('[data-d-nr]'), titelEl = welt.querySelector('[data-d-titel]');
const BILDER = [
['dialog-modal-nativ-haus.jpg', 1600, 1080, 'Ansicht von Süden', 'Flaches Holzhaus mit grauer Lattenfassade und zwei großen Fensterflächen auf einer Wiese vor Hügeln'],
['fenster-licht.jpg', 1200, 800, 'Diele mit Südlicht', 'Ruhiger Raum mit salbeigrüner Kalkputzwand, Sonnenfleck eines Sprossenfensters und Eichenbank mit Vase'],
['beton-treppe.jpg', 1200, 800, 'Treppe ins Obergeschoss', 'Freitragende Betonstufen an einer hellen Sichtbetonwand im Streiflicht'],
['zitronen-tisch.jpg', 1200, 800, 'Küche, Schiefertisch', 'Heller Teller mit Zitronen auf einem dunklen Schiefertisch im Gegenlicht'],
['handwerk-bad.webp', 1500, 1000, 'Bad mit bodengleicher Dusche', 'Bad mit bodengleicher Dusche, Wandnische und Holzhocker, Waschtisch vor salbeigrüner Wand'],
['handwerk-heizraum-hoch.webp', 800, 1000, 'Technikraum', 'Aufgeräumter Technikraum mit Warmwasserspeicher und Heizkreisverteiler'],
['handwerk-waermepumpe.webp', 1200, 800, 'Wärmepumpe', 'Außengerät einer Wärmepumpe auf einem Betonsockel an der Hauswand'],
['huegel-nebel.jpg', 1200, 800, 'Blick vom Garten', 'Gestaffelte bewaldete Hügelketten im Morgennebel']
];
band.innerHTML = BILDER.map(([d, w, h, titel, alt], i) => `<li role="group" aria-roledescription="Folie" aria-label="${i + 1} von 8: ${titel}" data-titel="Bild ${i + 1}: ${titel}" style="--daumen: url('bilder/${d}')"><img src="bilder/${d}" width="${w}" height="${h}" alt="${alt}" ${i ? 'loading="lazy"' : ''}></li>`).join('');
leiste.innerHTML = BILDER.map(([d, w, h, titel], i) => `<button type="button" aria-label="Bild ${i + 1}: ${titel}" aria-controls="d-band"${i ? '' : ' aria-current="true"'}><img src="bilder/${d}" alt="" loading="lazy"></button>`).join('');
const daumen = [...leiste.children];
const markerOk = CSS.supports('selector(::scroll-marker)');
let marker = markerOk ? 'nativ' : 'eigene';
let pfeile = () => {};
const B = Band(band, {
dauer: 900, kurve: bezier(0.16, 1, 0.3, 1),
wechsel(i) {
nrEl.textContent = zwei(i); titelEl.textContent = BILDER[i][3];
daumen.forEach((b, j) => b.setAttribute('aria-current', String(i === j)));
pfeile();
},
ruhe(i) { status.textContent = `Bild ${i + 1} von 8: ${BILDER[i][3]}`; }
});
pfeile = pfeileVerbinden(knoepfe, B);
daumen.forEach((b, i) => b.addEventListener('click', () => B.zu(i)));
function setzeMarker(m) {
marker = m === 'nativ' && !markerOk ? 'eigene' : m;
band.dataset.marker = marker;
leiste.hidden = marker === 'nativ';
document.querySelectorAll('[data-marker-knopf]').forEach(b => b.setAttribute('aria-pressed', String(b.dataset.markerKnopf === marker)));
werteEl.innerHTML = STEUER.daumen.werte();
}
document.querySelectorAll('[data-marker-knopf]').forEach(b => {
if (b.dataset.markerKnopf === 'nativ' && !markerOk) { b.disabled = true; b.title = 'Dieser Browser kennt ::scroll-marker noch nicht'; }
b.addEventListener('click', () => setzeMarker(b.dataset.markerKnopf));
});
// Vorführung: Zeiger tippt Daumen an (nativ: Lage des Markers aus der Band-Geometrie)
const FOLGE = [2, 5, 1, 7, 4, 0, 6, 3];
let schritt = 0;
const V = Vorfuehrung(async (ok, warte) => {
const i = FOLGE[schritt++ % FOLGE.length];
let sicht;
if (marker === 'eigene') sicht = zeigerZu(daumen[i]);
else {
const r = band.getBoundingClientRect(), spalten = innerWidth <= 760 ? 4 : 8, h = parseFloat(getComputedStyle(welt).getPropertyValue('--daumen-h')) || 58;
const b = (r.width - 8 * (spalten - 1)) / spalten, zeile = Math.floor(i / spalten), sp = i % spalten;
zeiger.style.transform = `translate(${r.left + sp * (b + 8) + b / 2 - 4}px, ${r.bottom + 12 + zeile * (h + 8) + h / 2 - 2}px)`; sicht = true;
}
await warte(sicht ? 760 : 300); if (!ok()) return;
B.zu(i);
await warte(1600);
});
STEUER.daumen = {
werte: () => marker === 'nativ'
? 'Daumen <b>::scroll-marker</b>, Bild per <b>background: var(--daumen)</b>, Name <b>content: \'\' / attr(data-titel)</b> · Gruppe <b>grid 8 Spalten</b> (Telefon 4 × 2, nie abgeschnitten) · aktiv <b>:target-current, Rahmen 2 px</b> · Gleiten <b>900 ms cubic-bezier(0.16, 1, 0.3, 1)</b>'
: 'Rückfall <b>8 Buttons mit aria-current</b> und <b>aria-label „Bild 3: …“</b> · Raster <b>8 Spalten</b>, Telefon <b>4 × 2</b> · Gleiten <b>900 ms cubic-bezier(0.16, 1, 0.3, 1)</b> · Zähler <b>„02 / 08“</b> über dem Bild',
start() { setzeMarker(marker); B.sofort(0); pfeile(); },
stopp() { V.stopp(); },
neustart() { V.stopp(); html.classList.remove('vorfuehrung'); schritt = 0; B.sofort(0); pfeile(); },
vorfuehren() { V.stopp(); zeigerStart(band); V.start(); },
pause() { V.stopp(); },
weiter() { if (html.classList.contains('vorfuehrung')) V.start(); }
};
})();
/* ============ Selbstlauf mit Pause: Kulturhaus Ziegelei ============ */
(() => {
const welt = document.querySelector('[data-welt=autoplay]');
const region = welt.querySelector('[data-a]'), band = welt.querySelector('[data-band]');
const knoepfe = [...welt.querySelectorAll('[data-schritt]')], wahl = [...welt.querySelectorAll('[data-a-wahl] button')];
const knopf = welt.querySelector('[data-a-pause]'), rest = welt.querySelector('[data-a-rest]'), statusEl = welt.querySelector('[data-a-status]');
const folien = [...band.children], N = folien.length, UMFANG = 2 * Math.PI * 20;
rest.style.strokeDasharray = UMFANG.toFixed(2);
const INTERVALL = 6000, VORF = 3200;
let intervall = INTERVALL, dreht = false, perKnopf = false, schwebt = false, bleibt = INTERVALL, zuletzt = 0, raf = 0;
const B = Band(band, {
dauer: 400, kurve: bezier(0.25, 1, 0.5, 1), schleife: true,
wechsel(i) {
folien.forEach((f, j) => { f.inert = i !== j; }); // nur die sichtbare Folie ist erreichbar
wahl.forEach((b, j) => b.setAttribute('aria-current', String(i === j)));
bleibt = intervall; ring();
}
});
knoepfe.forEach(k => k.addEventListener('click', () => B.weiter(+k.dataset.schritt)));
wahl.forEach((b, i) => b.addEventListener('click', () => B.zu(i)));
const naechste = () => { const i = (B.aktiv() + 1) % N; B.zu(i, i === 0 ? 800 : 400); }; // Rücklauf 800 ms wie Glide rewindDuration
function ring() { rest.style.strokeDashoffset = (UMFANG * (1 - (dreht ? bleibt / intervall : 1))).toFixed(2); }
function statusText() { statusEl.textContent = !dreht ? 'angehalten' : (schwebt || document.hidden) ? 'pausiert' : `läuft · ${Math.round(intervall / 1000)} s`; }
function takt(jetzt) {
if (!dreht) return;
const dt = Math.min(100, jetzt - zuletzt); zuletzt = jetzt;
if (!schwebt && !document.hidden) { bleibt -= dt; if (bleibt <= 0) { bleibt = intervall; naechste(); } }
ring(); raf = requestAnimationFrame(takt);
}
function starte(knopfGedrueckt) {
if (dreht) return;
dreht = true; perKnopf = knopfGedrueckt; bleibt = intervall; zuletzt = performance.now();
band.setAttribute('aria-live', 'off'); knopf.setAttribute('aria-pressed', 'false');
statusText(); cancelAnimationFrame(raf); raf = requestAnimationFrame(takt);
}
function stoppe() {
dreht = false; cancelAnimationFrame(raf);
band.setAttribute('aria-live', 'polite'); knopf.setAttribute('aria-pressed', 'true');
ring(); statusText();
}
knopf.addEventListener('click', () => (dreht ? stoppe() : starte(true)));
// Hover hält an und läuft danach weiter; Fokus beendet (kein Neustart), außer auf dem selbst gedrückten Startknopf
region.addEventListener('mouseenter', () => { schwebt = true; statusText(); });
region.addEventListener('mouseleave', () => { schwebt = false; statusText(); });
region.addEventListener('focusin', e => { if (dreht && !(perKnopf && e.target === knopf)) stoppe(); });
document.addEventListener('visibilitychange', statusText);
const selbstStart = () => !reduziert() && matchMedia('(hover: hover) and (pointer: fine) and (min-width: 761px)').matches;
const V = Vorfuehrung(async (ok, warte) => { // reduziert: kein Selbstlauf, der Zeiger blättert von Hand
const sicht = zeigerZu(knoepfe[1]); await warte(sicht ? 760 : 300); if (!ok()) return;
druck(knoepfe[1]); B.weiter(1); await warte(2400);
});
STEUER.autoplay = {
werte: () => `Intervall <b>${intervall} ms</b>${intervall !== INTERVALL ? ' (Vorführung, sonst 6000)' : ''} · Restzeit <b>Ring 44 px, stroke-dashoffset</b> · Wechsel <b>400 ms cubic-bezier(0.25, 1, 0.5, 1)</b>, Rücklauf <b>800 ms</b> · Pause-Knopf <b>zuerst, aria-pressed</b> · Hover <b>pausiert</b>, Fokus <b>stoppt</b> · aria-live <b>off ↔ polite</b> · Telefon/reduziert <b>kein Start</b>`,
start() { intervall = INTERVALL; B.sofort(0); schwebt = region.matches(':hover'); if (selbstStart()) starte(false); else stoppe(); werteEl.innerHTML = this.werte(); },
stopp() { V.stopp(); stoppe(); },
neustart() { V.stopp(); html.classList.remove('vorfuehrung'); stoppe(); this.start(); },
vorfuehren() {
V.stopp(); stoppe(); B.sofort(0);
if (reduziert()) { zeigerStart(knoepfe[1]); V.start(); }
else { html.classList.add('vorfuehrung'); intervall = VORF; schwebt = false; starte(false); }
werteEl.innerHTML = this.werte();
},
pause() { V.stopp(); stoppe(); },
weiter() { if (!html.classList.contains('vorfuehrung')) return; if (reduziert()) V.start(); else { schwebt = false; starte(false); } }
};
})();
/* ============ Anker-Links: Vonderau, Neubau Am Kornhaus ============ */
(() => {
const welt = document.querySelector('[data-welt=ankerlinks]');
const band = welt.querySelector('[data-band]'), knoepfe = [...welt.querySelectorAll('[data-schritt]')];
const haus = welt.querySelector('[data-k-haus]'), status = welt.querySelector('[data-status]');
const nrEl = welt.querySelector('[data-k-nr]'), urlEl = welt.querySelector('[data-k-url]');
const kopieren = welt.querySelector('[data-k-kopieren]'), meldung = welt.querySelector('[data-k-meldung]');
// Grundrisse als SVG: Räume [x, y, b, h, Name, m²], Freifläche, Türen [x, y, Drehung], Fenster [x1, y1, x2, y2]
const PLAENE = {
A: { aussen: [40, 30, 300, 200], raeume: [[160, 30, 180, 200, 'Wohnen · Kochen', '31,4'], [40, 30, 120, 110, 'Schlafen', '14,2'], [40, 140, 70, 90, 'Bad', '6,1'], [110, 140, 50, 90, 'Flur', '5,9']],
frei: [190, 230, 150, 44, 'Balkon'], tueren: [[160, 176, 90], [126, 140, 0], [110, 206, 270], [114, 230, 0]],
fenster: [[60, 30, 130, 30], [190, 30, 250, 30], [272, 30, 322, 30], [340, 60, 340, 130], [40, 58, 40, 112], [205, 230, 325, 230]] },
B: { aussen: [30, 30, 340, 200], raeume: [[200, 30, 170, 200, 'Wohnen · Essen', '36,8'], [30, 30, 100, 110, 'Schlafen', '14,6'], [30, 140, 100, 90, 'Kind', '11,9'], [130, 30, 70, 70, 'Bad', '7,2'], [130, 100, 70, 130, 'Flur', '9,4']],
frei: [220, 230, 150, 44, 'Balkon'], tueren: [[200, 176, 90], [130, 136, 270], [130, 206, 270], [146, 100, 0], [150, 230, 0]],
fenster: [[50, 30, 110, 30], [150, 30, 180, 30], [222, 30, 290, 30], [310, 30, 355, 30], [30, 160, 30, 210], [370, 60, 370, 200], [235, 230, 355, 230]] },
C: { aussen: [20, 25, 360, 210], raeume: [[200, 25, 180, 140, 'Wohnen · Essen', '34,0'], [290, 165, 90, 70, 'Küche', '9,8'], [200, 165, 90, 70, 'Arbeiten', '9,1'], [20, 25, 100, 105, 'Schlafen', '15,2'], [20, 130, 100, 105, 'Kind', '13,4'], [120, 25, 50, 70, 'Bad', '5,8'], [170, 25, 30, 70, 'WC', ''], [120, 95, 80, 140, 'Flur', '11,6']],
frei: [290, 235, 90, 40, 'Loggia'], tueren: [[200, 120, 90], [200, 190, 90], [120, 126, 270], [120, 206, 270], [130, 95, 0], [172, 95, 0], [354, 165, 180], [140, 235, 0]],
fenster: [[40, 25, 100, 25], [130, 25, 160, 25], [220, 25, 290, 25], [310, 25, 365, 25], [20, 160, 20, 215], [380, 50, 380, 140], [380, 180, 380, 222], [215, 235, 275, 235], [305, 235, 365, 235]] },
D: { aussen: [120, 30, 260, 210], raeume: [[230, 30, 150, 130, 'Wohnen · Kamin', '38,5'], [230, 160, 150, 80, 'Kochen · Essen', '22,6'], [120, 30, 110, 90, 'Schlafen', '18,9'], [120, 120, 50, 50, 'Ankl.', ''], [170, 120, 60, 50, 'Bad', '8,6'], [120, 170, 110, 70, 'Gast · Büro', '13,1']],
frei: [20, 30, 100, 210, 'Dachterrasse'], tueren: [[148, 120, 180], [170, 130, 90], [230, 104, 270], [230, 186, 90]],
fenster: [[140, 30, 210, 30], [250, 30, 360, 30], [380, 50, 380, 220], [120, 45, 120, 105], [120, 185, 120, 225], [250, 240, 360, 240]] }
};
function plan(art, spiegel) {
const P = PLAENE[art], X = x => (spiegel ? 400 - x : x);
const g = [];
g.push(`<defs><pattern id="k-schraffur-${art}${spiegel ? 's' : ''}" width="6" height="6" patternUnits="userSpaceOnUse" patternTransform="rotate(45)"><path d="M0 0V6" stroke="#B5B2AF" stroke-width="1"/></pattern></defs>`);
const [fx, fy, fw, fh] = P.frei;
g.push(`<g ${spiegel ? 'transform="matrix(-1 0 0 1 400 0)"' : ''}>`);
g.push(`<rect x="${fx}" y="${fy}" width="${fw}" height="${fh}" fill="url(#k-schraffur-${art}${spiegel ? 's' : ''})" stroke="#9c9894" stroke-width="1.2"/>`);
P.raeume.forEach(([x, y, w, h]) => g.push(`<rect x="${x}" y="${y}" width="${w}" height="${h}" fill="#FCFAF5" stroke="#313131" stroke-width="2.6"/>`));
const [ax, ay, aw, ah] = P.aussen;
g.push(`<rect x="${ax}" y="${ay}" width="${aw}" height="${ah}" fill="none" stroke="#313131" stroke-width="7"/>`);
P.fenster.forEach(([x1, y1, x2, y2]) => {
const waag = y1 === y2;
g.push(waag ? `<rect x="${x1}" y="${y1 - 4}" width="${x2 - x1}" height="8" fill="#fff"/><path d="M${x1} ${y1 - 3.5}H${x2}M${x1} ${y1}H${x2}M${x1} ${y1 + 3.5}H${x2}" stroke="#313131" stroke-width=".9"/>`
: `<rect x="${x1 - 4}" y="${y1}" width="8" height="${y2 - y1}" fill="#fff"/><path d="M${x1 - 3.5} ${y1}V${y2}M${x1} ${y1}V${y2}M${x1 + 3.5} ${y1}V${y2}" stroke="#313131" stroke-width=".9"/>`);
});
P.tueren.forEach(([x, y, r]) => g.push(`<g transform="translate(${x} ${y}) rotate(${r})"><rect x="0" y="-4" width="24" height="8" fill="#FCFAF5"/><path d="M0 0V-24A24 24 0 0 1 24 0" fill="none" stroke="#313131" stroke-width=".9"/></g>`));
g.push('</g>');
P.raeume.forEach(([x, y, w, h, name, m2]) => {
const cx = X(x + w / 2), cy = y + h / 2, klein = w < 56;
g.push(`<text x="${cx}" y="${cy - (m2 ? 3 : -3)}" text-anchor="middle" font-family="Hanken Grotesk, sans-serif" font-size="${klein ? 8 : 9.5}" font-weight="600" letter-spacing=".6" fill="#313131">${name.toUpperCase()}</text>`);
if (m2) g.push(`<text x="${cx}" y="${cy + 11}" text-anchor="middle" font-family="Hanken Grotesk, sans-serif" font-size="9.5" fill="#5d5b59">${m2} m²</text>`);
});
g.push(`<text x="${X(fx + fw / 2)}" y="${fy + fh / 2 + 3.5}" text-anchor="middle" font-family="Hanken Grotesk, sans-serif" font-size="9" font-weight="600" letter-spacing=".6" fill="#5d5b59">${P.frei[4].toUpperCase()}</text>`);
return `<svg viewBox="0 0 400 285" role="img" aria-label="Grundriss: ${P.raeume.map(r => r[4].replace('Ankl.', 'Ankleide') + (r[5] ? ' ' + r[5] + ' m²' : '')).join(', ')}, ${P.frei[4]}">${g.join('')}</svg>`;
}
const WHG = [
{ nr: 1, lage: 'EG links', zi: 2, m2: '61,8', frei: 'Terrasse 14,0 m²', preis: '368.000 €', status: 'verfügbar', plan: 'A', sp: false },
{ nr: 2, lage: 'EG rechts', zi: 3, m2: '84,2', frei: 'Terrasse 12,5 m²', preis: '476.000 €', status: 'reserviert', plan: 'B', sp: true },
{ nr: 3, lage: '1. OG links', zi: 3, m2: '86,4', frei: 'Balkon 9,2 m²', preis: '498.000 €', status: 'verfügbar', plan: 'B', sp: false },
{ nr: 4, lage: '1. OG rechts', zi: 4, m2: '112,6', frei: 'Loggia 8,4 m²', preis: '628.000 €', status: 'verfügbar', plan: 'C', sp: true },
{ nr: 5, lage: '2. OG links', zi: 3, m2: '86,4', frei: 'Balkon 9,2 m²', preis: '512.000 €', status: 'verkauft', plan: 'B', sp: false },
{ nr: 6, lage: '2. OG rechts', zi: 2, m2: '63,1', frei: 'Balkon 7,8 m²', preis: '389.000 €', status: 'verfügbar', plan: 'A', sp: true },
{ nr: 7, lage: 'Dachgeschoss', zi: 3, m2: '124,0', frei: 'Dachterrasse 38,0 m²', preis: '845.000 €', status: 'verfügbar', plan: 'D', sp: false }
];
band.innerHTML = WHG.map((w, i) => `<li id="wohnung-${w.nr}" role="group" aria-roledescription="Folie" aria-label="${i + 1} von 7: Wohnung ${w.nr}, ${w.lage}"><div class="k-folie">
<figure class="k-plan">${plan(w.plan, w.sp)}<figcaption><span>W${w.nr} · ${w.lage}</span><span>Maßstab frei · Norden oben</span></figcaption></figure>
<div class="k-daten"><p class="k-lage">Wohnung ${w.nr} · ${w.lage}</p><h3>${w.zi} Zimmer, ${w.m2} m²</h3>
<dl><div><dt>Wohnfläche</dt><dd>${w.m2} m²</dd></div><div><dt>Freifläche</dt><dd>${w.frei}</dd></div><div><dt>Kaufpreis</dt><dd>${w.preis}</dd></div><div><dt>Energie</dt><dd>A+ · 28 kWh/(m²·a)</dd></div></dl>
<p class="k-status" data-status="${w.status}">${w.status[0].toUpperCase() + w.status.slice(1)}</p>
<a class="i-cta" href="#">${w.status === 'verkauft' ? 'Ähnliche Wohnung anfragen' : 'Exposé anfordern'}</a></div></div></li>`).join('');
const ETAGEN = [['DG', [6], 'k-etage--dg'], ['2. OG', [4, 5]], ['1. OG', [2, 3]], ['EG', [0, 1]]];
haus.innerHTML = ETAGEN.map(([name, ids, kl]) => `<li class="k-etage ${kl || ''}"><span class="k-etage-name" aria-hidden="true">${name}</span><div class="k-whgs">${ids.map(i => { const w = WHG[i]; return `<a class="k-whg" href="#wohnung-${w.nr}" data-i="${i}" data-status="${w.status}"><b>W${w.nr}</b><span>${w.zi} Zi · ${w.m2} m²</span><span class="sr">, ${w.lage}, ${w.status}</span></a>`; }).join('')}</div></li>`).join('');
const links = [...haus.querySelectorAll('a')], folien = [...band.children];
const zeigeUrl = i => { urlEl.innerHTML = `vonderau-immobilien.de/am-kornhaus<mark>#wohnung-${i + 1}</mark>`; };
let aktivWelt = false, pfeile = () => {};
const B = Band(band, {
dauer: 900, kurve: bezier(0.16, 1, 0.3, 1),
wechsel(i) {
links.forEach(a => a.setAttribute('aria-current', String(+a.dataset.i === i)));
folien.forEach((f, j) => { f.inert = i !== j; });
nrEl.textContent = 'W' + (i + 1); zeigeUrl(i); pfeile();
},
ruhe(i) {
if (!aktivWelt) return;
// replaceState: die Adresse folgt der Folie, ohne für jede Folie einen Verlaufseintrag anzulegen
history.replaceState(null, '', location.pathname + location.search + '#wohnung-' + (i + 1));
status.textContent = `Wohnung ${i + 1} von 7, ${WHG[i].lage}`;
}
});
pfeile = pfeileVerbinden(knoepfe, B);
links.forEach(a => a.addEventListener('click', e => { e.preventDefault(); B.zu(+a.dataset.i); }));
const hashIndex = () => { const m = location.hash.match(/^#wohnung-(\d+)$/); return m && +m[1] >= 1 && +m[1] <= 7 ? +m[1] - 1 : -1; };
addEventListener('hashchange', () => { const i = hashIndex(); if (aktivWelt && i >= 0) B.zu(i); });
let meldeUhr = 0;
kopieren.addEventListener('click', async () => {
clearTimeout(meldeUhr);
try { await navigator.clipboard.writeText(location.href); meldung.textContent = 'Link kopiert'; }
catch (e) { const r = document.createRange(); r.selectNodeContents(urlEl); const s = getSelection(); s.removeAllRanges(); s.addRange(r); meldung.textContent = 'Markiert: mit Strg+C kopieren'; }
meldeUhr = setTimeout(() => { meldung.textContent = ''; }, 3000);
});
const FOLGE = [2, 6, 3, 0, 5, 1, 4];
let schritt = 0;
const V = Vorfuehrung(async (ok, warte) => {
const i = FOLGE[schritt++ % FOLGE.length], a = links.find(x => +x.dataset.i === i);
const sicht = zeigerZu(a); await warte(sicht ? 760 : 300); if (!ok()) return;
B.zu(i); await warte(1900);
});
STEUER.ankerlinks = {
werte: 'Folie <b>id="wohnung-3"</b> · Marker <b>a href="#wohnung-3"</b> im Gebäudeschnitt, <b>aria-current</b> · nach dem Einrasten <b>history.replaceState</b> (kein Verlaufseintrag je Folie) · Laden und <b>hashchange</b> springen zur Folie · Gleiten <b>900 ms cubic-bezier(0.16, 1, 0.3, 1)</b> · Folien außerhalb <b>inert</b>',
start() { aktivWelt = true; const i = hashIndex(); B.sofort(i >= 0 ? i : 0); pfeile(); },
stopp() { V.stopp(); aktivWelt = false; if (location.hash) history.replaceState(null, '', location.pathname + location.search); },
neustart() { V.stopp(); html.classList.remove('vorfuehrung'); schritt = 0; B.sofort(0); pfeile(); },
vorfuehren() { V.stopp(); zeigerStart(haus); V.start(); },
pause() { V.stopp(); },
weiter() { if (html.classList.contains('vorfuehrung')) V.start(); }
};
})();
/* ============ Varianten-Umschalter (Regel 13): ?v=zaehler | fortschritt | daumen | autoplay | ankerlinks ============ */
(() => {
const IDS = ['zaehler', 'fortschritt', 'daumen', 'autoplay', 'ankerlinks'];
let v = new URLSearchParams(location.search).get('v');
if (!IDS.includes(v)) v = /^#wohnung-\d+$/.test(location.hash) ? 'ankerlinks' : IDS[0];
let vorf = false;
const knoepfe = [...document.querySelectorAll('[data-v-wahl]')], markerModi = document.querySelector('[data-marker-modi]');
function setze(neu, url) {
const alt = html.dataset.v;
if (alt && STEUER[alt]) STEUER[alt].stopp();
v = neu; html.dataset.v = neu;
document.querySelectorAll('[data-welt]').forEach(w => { w.hidden = w.dataset.welt !== neu; });
knoepfe.forEach(b => b.setAttribute('aria-pressed', String(b.dataset.vWahl === neu)));
markerModi.hidden = neu !== 'daumen';
if (url) {
const q = new URLSearchParams(location.search); q.set('v', neu);
history.replaceState(null, '', location.pathname + '?' + q.toString().replace(/=(?=&|$)/g, '') + (neu === 'ankerlinks' ? location.hash : ''));
}
const S = STEUER[neu];
werteEl.innerHTML = typeof S.werte === 'function' ? S.werte() : S.werte;
S.start();
}
knoepfe.forEach(b => b.addEventListener('click', () => { setze(b.dataset.vWahl, true); if (vorf) STEUER[v].vorfuehren(); }));
setze(v, false);
// Echte Eingaben beenden die Vorführung
const ende = e => {
if (!e.isTrusted || !html.classList.contains('vorfuehrung') || (e.target.closest && e.target.closest('[data-fundus-pult], fundus-pult-knopf'))) return;
vorf = false; STEUER[v].pause(); html.classList.remove('vorfuehrung');
};
['pointerdown', 'wheel', 'touchstart', 'keydown'].forEach(t => document.addEventListener(t, ende, { capture: true, passive: true }));
})();
</script>
</body>
</html>