Zum Inhalt
← Galerien & Slider

Scroll-Snap-Ereignisse

Zimmer-Karussell, Bildergalerie und Nächte-Rad eines Seehotels melden über scrollsnapchanging schon während der Geste, wohin sie wollen, und über scrollsnapchange, wo sie liegen; ohne die Ereignisse rechnet scrollend nach.

Hotel, Ferienwohnung, Immobilien: Karten mit Bildtext und Preis, Zähler und Status neben dem Band.

Ein natives Scroll-Snap-Band meldet selbst, wo es einrastet. scrollsnapchanging feuert schon während der Geste, sobald der Browser das neue Ziel vorhersieht, scrollsnapchange erst, wenn es liegt. Daraus werden ohne IntersectionObserver ein Zähler „03 / 06“, eine Bildunterschrift, die schon beim Wischen wechselt, ein Kapitelstatus oder ein Auswahlrad, dessen Wert erst beim Einrasten gilt. Browser ohne die Ereignisse rechnen das Ziel bei scrollend selbst aus.

Zähler und Bildunterschrift, die erst nach dem Loslassen springen, wirken träge; solche, die bei jedem Pixel flackern, wirken nervös. Die zwei Ereignisse trennen genau das: Vorschau während der Geste, Festlegung beim Einrasten. Der Code wird kurz, weil der Browser das Ziel kennt.

Wann einsetzen

Zimmer- und Galerie-Karussells mit Zähler und Bildtext (Hotel, Fotograf, Immobilien, Weingut), Bildergalerien mit Daumenleiste, Auswahlräder für Nächte, Personen oder Uhrzeit in einer Buchungsleiste.

Wann nicht

Nicht als einzige Steuerung: Pfeile, Tastatur und sichtbare Position gehören dazu (karussell-steuerung). Nicht für Seiten, die als ganze Folien einrasten (fullpage-snap-slides). Nicht für Werte, die man exakt eintippt; ein Auswahlrad ersetzt kein Zahlenfeld bei großen Bereichen.

Varianten

  • Zimmer-Karussell: Zähler und Bildunterschrift wechseln schon während der Geste, Status „wechselt“ und „eingerastet“, Pfeile, Tastatur
  • Galerie mit Daumenleiste: ein Bild je Ansicht, die Daumenleiste folgt schon in der Geste, festgelegt beim Einrasten
  • Auswahlrad: Nächte als senkrechtes Snap-Rad, Preis rechnet beim Wischen vor (≈) und gilt beim Einrasten

Umsetzung

  1. Band mit scroll-snap-type: x mandatory (Kapitel: y mandatory), Kinder mit scroll-snap-align: center bzw. start.
  2. Erkennung: const kannSnap = 'onscrollsnapchange' in window.
  3. Mit Unterstützung: band.addEventListener('scrollsnapchanging', e => vorschau(e.snapTargetInline)) setzt nur die leichte Vorschau (Zähler, Bildtext, Preis vorläufig); band.addEventListener('scrollsnapchange', e => festlegen(e.snapTargetInline)) setzt den gültigen Zustand (aria-current, Ansage, Wert). Bei senkrechten Bändern snapTargetBlock.
  4. Rückfall: scrollend (Chrome 114, Firefox 109, Safari 26.2) ruft festlegen(naechstes()), naechstes() vergleicht die Mitte jedes Kindes mit der Mitte des Bandes. Für die Vorschau im Rückfall ein scroll-Handler mit requestAnimationFrame, der dasselbe rechnet. Ohne scrollend ein Timer 120 ms nach dem letzten scroll.
  5. Pfeile und Tastatur rufen scrollIntoView({ inline: 'center', block: 'nearest' }) bzw. scrollBy; die Ereignisse kommen dann von selbst.
  6. Zähler mit tabellarischen Ziffern, Wechsel per kurzem Versatz; Ansage nur bei scrollsnapchange, nie bei scrollsnapchanging.

Robust umsetzen

Telefon. Wischen ist hier die Hauptbedienung; Pfeile bleiben trotzdem als 44-px-Knöpfe. Das Auswahlrad nutzt die native Trägheit; der Wert gilt erst beim Einrasten.

Ohne Bewegung. Pfeile springen ohne Gleiten (behavior: auto). Zähler und Bildtext wechseln ohne Versatz. Wischen selbst bleibt, das ist Bedienung.

Barrierefrei. Band als Region mit Namen, Folien als Gruppe mit „3 von 6“. Pfeile sind echte Knöpfe, Pfeiltasten im fokussierten Band. Ansage per aria-live nur beim Einrasten. Das Auswahlrad ist role="spinbutton"-artig mit Pfeiltasten und aria-valuenow, plus sichtbare Knöpfe Minus und Plus.

Code

<!DOCTYPE html>
<!--
  Scroll-Snap-Ereignisse
  Fundus · Galerien & Slider · https://cecodesign.de/lab/fundus/galerien-slider/scroll-snap-ereignisse/
  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>Scroll-Snap-Ereignisse · 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: '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; }

/* Rezept boutique-hotel-gastgeber: Sand, Creme, warmes Dunkelbraun-Grau, Holz als Hauston */
:root {
  --grund: #F3EEE7; --creme: #FEF2E3; --text: #3E3A37; --linie: #D5D1C8; --leise: #6A6C67; --haus: #6E5A47;
  --kurve: cubic-bezier(0.37, 0, 0.63, 1);      /* token-luxus-weich: Bilder, Text */
  --ui-ein: cubic-bezier(0.16, 1, 0.3, 1);       /* token-toggle-asymmetrisch */
  --ui-aus: cubic-bezier(0.7, 0, 0.84, 0);
  --rand: clamp(18px, 5vw, 72px);
}
* { box-sizing: border-box; margin: 0; }
html { background: var(--grund); color: var(--text); font: 400 1.0625rem/1.6 'Mona Sans', system-ui, sans-serif; font-stretch: 100%; }
body { min-height: 100svh; }
:focus-visible { outline: 2px solid var(--haus); outline-offset: 3px; }
img { display: block; max-width: 100%; }
button { font: inherit; color: inherit; }
.label { font: 560 .75rem/1 'Mona Sans', sans-serif; font-stretch: 118%; letter-spacing: .06em; text-transform: uppercase; color: var(--leise); }
.mono { font: 500 11px/1 'Geist Mono', ui-monospace, monospace; letter-spacing: .14em; text-transform: uppercase; }

.kopf { display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 22px var(--rand); }
.marke { font: 400 1.5rem/1 'Newsreader', Georgia, serif; font-variation-settings: 'opsz' 36; letter-spacing: -.01em; }
.marke i { font-style: italic; }
.kopf .label { white-space: nowrap; }

/* Gemeinsame Teile: Zähler, Pfeile, Status-Punkt */
.zaehler { display: flex; align-items: baseline; gap: 10px; font-variant-numeric: tabular-nums lining-nums; }
.zaehler .jetzt { position: relative; display: inline-block; overflow: hidden; height: 1lh; font: 300 clamp(3.2rem, 2rem + 4vw, 5.5rem)/1 'Newsreader', serif; font-variation-settings: 'opsz' 72; letter-spacing: -.02em; }
.zaehler .jetzt span { display: block; }
.zaehler .jetzt span.raus { position: absolute; inset: 0; animation: z-raus 160ms var(--ui-aus) both; }
.zaehler .jetzt span.rein { animation: z-rein 220ms var(--ui-ein) both; }
@keyframes z-raus { to { transform: translateY(-.6em); opacity: 0; } }
@keyframes z-rein { from { transform: translateY(.6em); opacity: 0; } }
.zaehler .ges { color: var(--leise); font-size: 1.05rem; }
.stand { display: inline-flex; align-items: center; gap: 8px; color: var(--leise); }
.stand i { width: 8px; height: 8px; border-radius: 50%; border: 1.5px solid var(--haus); background: var(--haus); transition: background-color 150ms var(--ui-ein); }
.stand[data-zustand="unterwegs"] i { background: transparent; }
.pfeile { display: flex; gap: 8px; }
.pfeil { width: 48px; height: 48px; border-radius: 50%; border: 1px solid var(--text); background: transparent; display: grid; place-items: center; cursor: pointer; transition: background-color 150ms var(--ui-ein), color 150ms var(--ui-ein); }
.pfeil:hover:not(:disabled) { background: var(--text); color: var(--grund); }
.pfeil:disabled { opacity: .4; cursor: default; }
.pfeil svg { width: 18px; height: 18px; }
.unsichtbar { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }

/* Text, der während der Geste schon wechselt */
.wechsel { display: grid; }
.wechsel > * { grid-area: 1 / 1; }
.wechsel .alt { animation: t-raus 140ms var(--kurve) both; }
.wechsel .neu { animation: t-rein 300ms var(--kurve) 80ms both; }
@keyframes t-raus { to { opacity: 0; } }
@keyframes t-rein { from { opacity: 0; transform: translateY(6px); } }

/* ============ Variante zimmer: Karussell mit Bildtext ============ */
.zimmer { display: grid; grid-template-columns: minmax(0, 24rem) minmax(0, 1fr); gap: clamp(24px, 4vw, 64px); padding: clamp(16px, 4vw, 48px) 0 clamp(40px, 6vw, 80px) var(--rand); align-items: end; }
.zimmer .links { display: grid; gap: 22px; padding-bottom: 8px; }
.zimmer h1 { font: 350 clamp(2.4rem, 1.5rem + 3.4vw, 4.4rem)/1.04 'Newsreader', serif; font-variation-settings: 'opsz' 72; letter-spacing: -.015em; text-wrap: balance; }
.zimmer h1 em { font-style: italic; }
.zimmer .bildtext h2 { font: 400 1.7rem/1.15 'Newsreader', serif; font-style: italic; font-variation-settings: 'opsz' 36; }
.zimmer .bildtext p { color: var(--leise); margin-top: 6px; max-width: 34ch; }
.zimmer .bildtext .preis { color: var(--text); font-weight: 560; margin-top: 10px; font-variant-numeric: tabular-nums; }
.zimmer .steuer { display: flex; align-items: center; justify-content: space-between; gap: 16px; flex-wrap: wrap; border-top: 1px solid var(--linie); padding-top: 18px; }
.band { display: flex; gap: clamp(12px, 1.6vw, 20px); overflow-x: auto; overscroll-behavior-x: contain; scroll-snap-type: x mandatory; scrollbar-width: none; padding: 4px var(--rand) 4px 0; scroll-padding-inline: 0 var(--rand); }
.band::-webkit-scrollbar { display: none; }
.band:focus-visible { outline-offset: -2px; }
.karte { flex: 0 0 clamp(240px, 30vw, 380px); scroll-snap-align: start; margin: 0; }
.karte .rahmen { aspect-ratio: 4 / 5; overflow: hidden; border-radius: 4px; background: #E3DCD1; }
.karte img { width: 100%; height: 100%; object-fit: cover; transform: scale(1.06); transition: transform 900ms var(--kurve); }
.karte[aria-current="true"] img { transform: scale(1); }
.karte figcaption { display: flex; justify-content: space-between; gap: 12px; margin-top: 10px; font-size: .92rem; }
.karte figcaption span:last-child { color: var(--leise); font-variant-numeric: tabular-nums; }

/* ============ Variante galerie: Vollbild mit Daumenleiste ============ */
.galerie { padding: clamp(8px, 2vw, 24px) var(--rand) clamp(40px, 6vw, 80px); display: grid; gap: 18px; max-width: 1280px; margin: 0 auto; }
.galerie .oben { display: flex; align-items: end; justify-content: space-between; gap: 18px; flex-wrap: wrap; }
.galerie h1 { font: 350 clamp(2rem, 1.3rem + 2.4vw, 3.4rem)/1.05 'Newsreader', serif; font-variation-settings: 'opsz' 72; letter-spacing: -.015em; }
.galerie h1 em { font-style: italic; }
.buehne-g { display: flex; overflow-x: auto; scroll-snap-type: x mandatory; overscroll-behavior-x: contain; scrollbar-width: none; border-radius: 4px; background: #2B2826; }
.buehne-g::-webkit-scrollbar { display: none; }
.buehne-g figure { flex: 0 0 100%; scroll-snap-align: center; scroll-snap-stop: always; margin: 0; aspect-ratio: 16 / 9; max-height: min(62svh, 640px); }
.buehne-g img { width: 100%; height: 100%; object-fit: cover; }
.galerie .unten { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 18px; align-items: center; }
.galerie .bildtext p { font: 400 1.25rem/1.3 'Newsreader', serif; font-style: italic; }
.galerie .bildtext small { display: block; color: var(--leise); font-size: .88rem; margin-top: 4px; font-style: normal; font-family: 'Mona Sans', sans-serif; }
.daumen { display: flex; gap: 8px; overflow-x: auto; scrollbar-width: none; padding: 4px 2px; list-style: none; }
.daumen::-webkit-scrollbar { display: none; }
.daumen button { width: 76px; height: 52px; padding: 0; border: 0; border-radius: 3px; overflow: hidden; cursor: pointer; position: relative; background: #E3DCD1; flex: none; }
.daumen img { width: 100%; height: 100%; object-fit: cover; filter: saturate(.6); transition: filter 300ms var(--kurve); }
.daumen button::after { content: ''; position: absolute; inset: 0; border-radius: 3px; box-shadow: inset 0 0 0 2px var(--text); opacity: 0; transition: opacity 150ms var(--ui-ein); }
.daumen button[aria-current="true"] img { filter: none; }
.daumen button[aria-current="true"]::after { opacity: 1; }
.daumen button[data-vorschau]::after { opacity: .5; }

/* ============ Variante rad: Nächte als Auswahlrad ============ */
.rad-welt { position: relative; min-height: calc(100svh - 80px); display: grid; place-items: center; padding: 24px var(--rand) 64px; }
.rad-welt .hinter { position: absolute; inset: 0 var(--rand) 40px; border-radius: 4px; overflow: hidden; }
.rad-welt .hinter img { width: 100%; height: 100%; object-fit: cover; }
.rad-welt .hinter::after { content: ''; position: absolute; inset: 0; background: linear-gradient(90deg, rgba(43, 40, 38, .55), rgba(43, 40, 38, .05) 70%); }
.leiste { position: relative; z-index: 1; justify-self: start; margin-left: clamp(0px, 4vw, 56px); width: min(100%, 25rem); background: var(--creme); border-radius: 6px; padding: 22px; display: grid; gap: 16px; box-shadow: 0 24px 60px rgba(30, 24, 20, .28); }
.leiste h1 { font: 400 1.9rem/1.1 'Newsreader', serif; font-variation-settings: 'opsz' 36; }
.leiste h1 em { font-style: italic; }
.leiste .felder { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 1px; background: var(--linie); border: 1px solid var(--linie); border-radius: 4px; overflow: hidden; }
.leiste .felder div { background: var(--creme); padding: 10px 12px; }
.leiste .felder b { display: block; font-weight: 560; font-size: .98rem; }
.radzeile { display: grid; grid-template-columns: 48px minmax(0, 1fr) 48px; gap: 10px; align-items: center; }
.rad { position: relative; height: 168px; overflow-y: auto; scroll-snap-type: y mandatory; overscroll-behavior: contain; scrollbar-width: none; border-radius: 4px; background: #fff; touch-action: pan-y;
  -webkit-mask-image: linear-gradient(transparent, #000 32%, #000 68%, transparent); mask-image: linear-gradient(transparent, #000 32%, #000 68%, transparent); }
.rad::-webkit-scrollbar { display: none; }
.rad:focus-visible { outline: 2px solid var(--haus); outline-offset: 3px; }
.rad ol { list-style: none; padding: 56px 0; margin: 0; }
.rad li { height: 56px; display: flex; align-items: baseline; justify-content: center; padding-top: 14px; scroll-snap-align: center; font: 400 1.6rem/1 'Newsreader', serif; font-variant-numeric: tabular-nums lining-nums; color: var(--leise); transition: color 150ms var(--ui-ein), font-size 150ms var(--ui-ein); }
.rad li[data-vorschau], .rad li[aria-selected="true"] { color: var(--text); }
.rad li[aria-selected="true"] { font-weight: 500; }
.rad li small { font: 400 .8rem/1 'Mona Sans', sans-serif; margin-left: 8px; color: var(--leise); }
.radzeile .mitte { position: relative; }
.radzeile .mitte::before { content: ''; position: absolute; left: 10px; right: 10px; top: 56px; height: 56px; border-top: 1px solid var(--linie); border-bottom: 1px solid var(--linie); pointer-events: none; z-index: 1; }
.summe { display: flex; justify-content: space-between; align-items: baseline; gap: 12px; border-top: 1px solid var(--linie); padding-top: 14px; font-variant-numeric: tabular-nums; }
.summe b { font: 400 1.7rem/1 'Newsreader', serif; }
.summe[data-zustand="unterwegs"] b { color: var(--leise); }
.summe small { color: var(--leise); font-size: .85rem; }
.leiste .los { min-height: 50px; border: 0; border-radius: 4px; background: var(--haus); color: #fff; font-weight: 600; font-stretch: 112%; letter-spacing: .04em; text-transform: uppercase; font-size: .85rem; cursor: pointer; transition: background-color 150ms var(--ui-ein); }
.leiste .los:hover { background: #57473A; }
.leiste .fuss { font-size: .82rem; color: var(--leise); }

/* Variante wählen */
html[data-v="zimmer"] .w-galerie, html[data-v="zimmer"] .w-rad { display: none; }
html[data-v="galerie"] .w-zimmer, html[data-v="galerie"] .w-rad { display: none; }
html[data-v="rad"] .w-zimmer, html[data-v="rad"] .w-galerie { display: none; }

/* Pult */
.pult { position: fixed; right: 16px; bottom: 16px; z-index: 20; display: flex; flex-wrap: wrap; align-items: center; gap: 10px; background: rgba(40, 36, 33, .94); color: #fff; padding: 8px 10px 8px 14px; border-radius: 10px; box-shadow: 0 10px 28px rgba(0, 0, 0, .2); max-width: calc(100vw - 32px); }
.pult .mono { font-size: 10px; color: rgba(255, 255, 255, .7); }
.pult .gruppe { display: flex; flex-wrap: wrap; background: rgba(255, 255, 255, .08); border-radius: 6px; padding: 2px; }
.pult button { background: none; border: 0; color: rgba(255, 255, 255, .8); font: 500 10.5px/1 'Geist Mono', monospace; letter-spacing: .06em; text-transform: uppercase; padding: 8px 10px; border-radius: 4px; cursor: pointer; }
.pult button[aria-pressed="true"] { background: #fff; color: var(--text); }
.pult button:focus-visible { outline: 2px solid #fff; outline-offset: 2px; }
.pult .werte { flex-basis: 100%; font: 400 10.5px/1.55 'Geist Mono', monospace; color: rgba(255, 255, 255, .72); max-width: 48ch; }
.pult .werte b { color: #fff; font-weight: 500; }

.reduziert .zaehler .jetzt span, .reduziert .wechsel > *, .reduziert .karte img { animation: none !important; transition: none !important; }
.reduziert .karte img { transform: none; }
.vorschau body { overflow: hidden; }

@media (max-width: 860px) {
  .zimmer { grid-template-columns: minmax(0, 1fr); padding-right: 0; }
  .zimmer .links { padding-right: var(--rand); }
  .zimmer .band { order: 2; }
  .karte { flex-basis: 78%; }
  .galerie .unten { grid-template-columns: minmax(0, 1fr); }
  .rad-welt { place-items: end center; padding-top: 140px; }
  .rad-welt .hinter { inset: 0 0 40px; border-radius: 0; }
  .leiste { justify-self: center; margin-left: 0; }
  .pult .werte { display: none; }
}
@media (max-width: 480px) { .kopf .lang { display: none; } }
@media (max-width: 360px) {
  .pfeil { width: 44px; height: 44px; }
  .daumen button { width: 60px; height: 42px; }
  .leiste { padding: 16px; }
}
@media (hover: none) { .pfeil:hover:not(:disabled) { background: transparent; color: inherit; } }
@media (forced-colors: active) {
  button, [role=button], [role=tab], [role=switch], input, select, textarea { border: 1px solid ButtonText !important; }
  .daumen button[aria-current="true"] { outline: 3px solid Highlight; }
  .rad li[aria-selected="true"] { outline: 2px solid Highlight; }
}
</style>
</head>
<body>
<header class="kopf">
  <span class="marke">Haus <i>Kranichsee</i></span>
  <span class="label">Neustrelitz<span class="lang"> · am Zierker See</span></span>
</header>

<main>
  <!-- ============ zimmer ============ -->
  <section class="zimmer w-zimmer" aria-labelledby="z-titel">
    <div class="links">
      <h1 id="z-titel">Elf Zimmer, <em>sechs</em> davon zum See.</h1>
      <div class="bildtext wechsel" data-z-text aria-hidden="true"></div>
      <div class="steuer">
        <div>
          <p class="zaehler" aria-hidden="true"><span class="jetzt" data-z-zahl><span>01</span></span><span class="ges">/ 06</span></p>
          <p class="stand label" data-z-stand><i></i><span>eingerastet</span></p>
        </div>
        <div class="pfeile">
          <button class="pfeil" type="button" aria-label="Vorheriges Zimmer" data-z-zurueck><svg viewBox="0 0 18 18" aria-hidden="true"><path d="M11 4 6 9l5 5" fill="none" stroke="currentColor" stroke-width="1.5"/></svg></button>
          <button class="pfeil" type="button" aria-label="Nächstes Zimmer" data-z-vor><svg viewBox="0 0 18 18" aria-hidden="true"><path d="m7 4 5 5-5 5" fill="none" stroke="currentColor" stroke-width="1.5"/></svg></button>
        </div>
      </div>
      <p class="unsichtbar" aria-live="polite" data-z-ansage></p>
    </div>
    <div class="band" tabindex="0" role="region" aria-roledescription="Karussell" aria-label="Zimmer, mit Pfeiltasten blättern" data-z-band>
      <figure class="karte" role="group" aria-roledescription="Folie" aria-label="1 von 6: Seezimmer" data-name="Seezimmer" data-text="Doppelbett, Sessel am Fenster, Blick über den Steg aufs Wasser." data-preis="ab 148 € pro Nacht"><div class="rahmen"><img src="bilder/hotel-zimmer.webp" alt="Helles Zimmer mit Doppelbett, Sessel und Fensterbank mit gelbem Polster, Sonnenlicht auf dem Holzboden" width="1200" height="800"></div><figcaption><span>Seezimmer</span><span>24 m²</span></figcaption></figure>
      <figure class="karte" role="group" aria-roledescription="Folie" aria-label="2 von 6: Fensterbank" data-name="Fensterbank" data-text="Die breite Fensterbank ist das Sofa. Morgens fällt die Sonne direkt aufs Polster." data-preis="ab 162 € pro Nacht"><div class="rahmen"><img src="bilder/hotel-zimmer-fenster.webp" alt="Breite Fensterbank mit gelbem Polster und Tasse, Vase mit Zweigen, draußen der See" width="1200" height="800" loading="lazy"></div><figcaption><span>Fensterbank</span><span>27 m²</span></figcaption></figure>
      <figure class="karte" role="group" aria-roledescription="Folie" aria-label="3 von 6: Kammer" data-name="Kammer" data-text="Unter den Dachbalken, klein und still. Für alle, die nur schlafen und schwimmen wollen." data-preis="ab 118 € pro Nacht"><div class="rahmen"><img src="bilder/bild-zoom-in-maske-kammer.svg" alt="Gezeichnete Schlafkammer unter schrägen Dachbalken mit salbeigrüner Decke" width="1200" height="800" loading="lazy"></div><figcaption><span>Kammer</span><span>16 m²</span></figcaption></figure>
      <figure class="karte" role="group" aria-roledescription="Folie" aria-label="4 von 6: Turmzimmer" data-name="Turmzimmer" data-text="Drei Seiten Fenster, eine Wendeltreppe. Das Zimmer mit dem weitesten Blick." data-preis="ab 196 € pro Nacht"><div class="rahmen"><img src="bilder/bild-zoom-in-maske-turmzimmer.svg" alt="Gezeichnetes helles Zimmer mit Holzsprossenfenster und Blick auf blaue Hügel" width="1200" height="800" loading="lazy"></div><figcaption><span>Turmzimmer</span><span>31 m²</span></figcaption></figure>
      <figure class="karte" role="group" aria-roledescription="Folie" aria-label="5 von 6: Suite mit Balkon" data-name="Suite mit Balkon" data-text="Wohnraum, Schlafraum und ein Balkon, auf dem zwei Stühle und ein Tisch Platz haben." data-preis="ab 238 € pro Nacht"><div class="rahmen"><img src="bilder/bild-zoom-in-maske-suite.svg" alt="Gezeichneter Balkon mit Holzgeländer, Tisch und Stuhl vor gestaffelten Hügeln" width="1200" height="800" loading="lazy"></div><figcaption><span>Suite mit Balkon</span><span>44 m²</span></figcaption></figure>
      <figure class="karte" role="group" aria-roledescription="Folie" aria-label="6 von 6: Gartenzimmer" data-name="Gartenzimmer" data-text="Ebenerdig, mit eigener Tür zum Garten. Hunde sind hier willkommen." data-preis="ab 138 € pro Nacht"><div class="rahmen"><img src="bilder/fenster-licht-hoch.jpg" alt="Ruhiger Raum mit salbeigrüner Wand, auf die die Abendsonne ein Fensterbild wirft" width="800" height="1000" loading="lazy"></div><figcaption><span>Gartenzimmer</span><span>22 m²</span></figcaption></figure>
    </div>
  </section>

  <!-- ============ galerie ============ -->
  <section class="galerie w-galerie" aria-labelledby="g-titel">
    <div class="oben">
      <h1 id="g-titel">Das Haus <em>in Bildern</em></h1>
      <div class="pfeile">
        <p class="zaehler" aria-hidden="true" style="margin-right:12px"><span class="jetzt" data-g-zahl style="font-size:2.4rem"><span>01</span></span><span class="ges">/ 06</span></p>
        <button class="pfeil" type="button" aria-label="Vorheriges Bild" data-g-zurueck><svg viewBox="0 0 18 18" aria-hidden="true"><path d="M11 4 6 9l5 5" fill="none" stroke="currentColor" stroke-width="1.5"/></svg></button>
        <button class="pfeil" type="button" aria-label="Nächstes Bild" data-g-vor><svg viewBox="0 0 18 18" aria-hidden="true"><path d="m7 4 5 5-5 5" fill="none" stroke="currentColor" stroke-width="1.5"/></svg></button>
      </div>
    </div>
    <div class="buehne-g" tabindex="0" role="region" aria-roledescription="Bildergalerie" aria-label="Das Haus in Bildern, mit Pfeiltasten blättern" data-g-band>
      <figure role="group" aria-roledescription="Bild" aria-label="1 von 6" data-text="Das Haus am Abend, vom Steg aus." data-meta="Backstein, 1911, seit 2019 Hotel"><img src="bilder/hotel-haus-am-see.webp" alt="Backsteinhaus mit erleuchteten Fenstern am See in der Abenddämmerung, Steg und kleine Sauna" width="1920" height="1200"></figure>
      <figure role="group" aria-roledescription="Bild" aria-label="2 von 6" data-text="Die Sauna steht auf dem Wasser." data-meta="Holzofen, bis 22 Uhr"><img src="bilder/hotel-sauna.webp" alt="Kleine Holzsauna auf einem Steg in der Dämmerung, zwei erleuchtete Fenster, Schilf" width="1200" height="800" loading="lazy"></figure>
      <figure role="group" aria-roledescription="Bild" aria-label="3 von 6" data-text="Frühstück auf der Terrasse." data-meta="7:30 bis 11 Uhr, am Wochenende bis 12"><img src="bilder/slideshow-innere-gegenparallaxe-fruehstueck.svg" alt="Gezeichneter Frühstückstisch auf der Terrasse mit Kaffee, Croissant und Brotkorb vor dem See" width="1600" height="1000" loading="lazy"></figure>
      <figure role="group" aria-roledescription="Bild" aria-label="4 von 6" data-text="Der Steg im Morgennebel." data-meta="Ruderboot zum Ausleihen"><img src="bilder/slideshow-innere-gegenparallaxe-steg.svg" alt="Gezeichneter Holzsteg, der im Nebel auf dem See verschwindet, Ruderboot" width="1600" height="1000" loading="lazy"></figure>
      <figure role="group" aria-roledescription="Bild" aria-label="5 von 6" data-text="Kaminzimmer, wenn es draußen nass ist." data-meta="Bibliothek mit Brettspielen"><img src="bilder/atmosphaere-partikel-kamin.jpg" alt="Breites, niedriges Kaminfeuer in einer dunklen Putzwand" width="1440" height="900" loading="lazy"></figure>
      <figure role="group" aria-roledescription="Bild" aria-label="6 von 6" data-text="Das Seezimmer am Nachmittag." data-meta="24 m², Blick nach Westen"><img src="bilder/hotel-zimmer.webp" alt="Helles Zimmer mit Doppelbett, Sessel und Fensterbank mit gelbem Polster" width="1200" height="800" loading="lazy"></figure>
    </div>
    <div class="unten">
      <div class="bildtext wechsel" data-g-text aria-hidden="true"></div>
      <ul class="daumen" aria-label="Bild wählen" data-daumen></ul>
    </div>
    <p class="unsichtbar" aria-live="polite" data-g-ansage></p>
  </section>

  <!-- ============ rad ============ -->
  <section class="rad-welt w-rad" aria-labelledby="r-titel">
    <div class="hinter" aria-hidden="true"><img src="bilder/buchungsleiste-datumsbereich-see.webp" alt="" width="1600" height="1000"></div>
    <form class="leiste" data-r-form>
      <h1 id="r-titel">Wie lange <em>bleiben</em> Sie?</h1>
      <div class="felder"><div><span class="label">Anreise</span><b>Fr., 14. Nov.</b></div><div><span class="label">Gäste</span><b>2 Erwachsene</b></div></div>
      <div>
        <span class="label" id="r-label">Nächte</span>
        <div class="radzeile">
          <button class="pfeil" type="button" aria-label="Eine Nacht weniger" data-r-minus><svg viewBox="0 0 18 18" aria-hidden="true"><path d="M4 9h10" stroke="currentColor" stroke-width="1.5"/></svg></button>
          <div class="mitte">
            <div class="rad" tabindex="0" role="spinbutton" aria-labelledby="r-label" aria-valuemin="1" aria-valuemax="14" aria-valuenow="2" aria-valuetext="2 Nächte" data-rad><ol data-r-liste></ol></div>
          </div>
          <button class="pfeil" type="button" aria-label="Eine Nacht mehr" data-r-plus><svg viewBox="0 0 18 18" aria-hidden="true"><path d="M4 9h10M9 4v10" stroke="currentColor" stroke-width="1.5"/></svg></button>
        </div>
      </div>
      <div class="summe" data-r-summe><span>Abreise <span data-r-ab>So., 16. Nov.</span><br><small data-r-hinweis>Seezimmer, Frühstück inklusive</small></span><b data-r-preis>296 €</b></div>
      <button class="los" type="submit">Verfügbarkeit prüfen</button>
      <p class="fuss">Beispielpreise. Endpreis inkl. MwSt. und Kurtaxe vom Kunden prüfen lassen.</p>
      <p class="unsichtbar" aria-live="polite" data-r-ansage></p>
    </form>
  </section>
</main>

<div class="pult">
  <span class="mono">Variante</span>
  <div class="gruppe" role="group" aria-label="Variante">
    <button type="button" data-v="zimmer" aria-pressed="true">Zimmer</button>
    <button type="button" data-v="galerie" aria-pressed="false">Galerie</button>
    <button type="button" data-v="rad" aria-pressed="false">Auswahlrad</button>
  </div>
  <p class="werte" data-werte></p>
</div>

<script>
(() => {
  const html = document.documentElement;
  const q0 = new URLSearchParams(location.search);
  const reduziert = () => html.classList.contains('reduziert');
  const $ = (s, r = document) => r.querySelector(s);
  const $$ = (s, r = document) => [...r.querySelectorAll(s)];
  // Erkennung laut Chrome-Doku; ?rueckfall erzwingt den Weg ohne Ereignisse
  const KANN_SNAP = !q0.has('rueckfall') && 'onscrollsnapchange' in window;
  const KANN_ENDE = 'onscrollend' in window;
  let letztes = '';
  const protokoll = t => { letztes = t; zeigeWerte(); };

  /* ---------- Der Kern: ein Band, zwei Rückmeldungen ----------
     vorschau(el): während der Geste (scrollsnapchanging) – leicht, keine Ansage
     fest(el):     eingerastet (scrollsnapchange) – Zustand, aria-current, Ansage */
  function beobachte(band, achse, vorschau, fest) {
    const kinder = () => [...band.querySelectorAll(':scope > figure, :scope > ol > li')];
    if (KANN_SNAP) {
      band.addEventListener('scrollsnapchanging', e => { const el = achse === 'x' ? e.snapTargetInline : e.snapTargetBlock; if (el) { protokoll('scrollsnapchanging'); vorschau(el); } });
      band.addEventListener('scrollsnapchange', e => { const el = achse === 'x' ? e.snapTargetInline : e.snapTargetBlock; if (el) { protokoll('scrollsnapchange'); fest(el); } });
      return;
    }
    // Rückfall: Ziel selbst ausrechnen (Mitte bzw. Anfang am nächsten)
    const naechstes = () => {
      const b = band.getBoundingClientRect(); let best = null, abst = Infinity;
      const ausrichtung = getComputedStyle(kinder()[0]).scrollSnapAlign;
      kinder().forEach(k => {
        const r = k.getBoundingClientRect();
        const d = achse === 'x'
          ? (ausrichtung.includes('start') ? Math.abs(r.left - b.left) : Math.abs(r.left + r.width / 2 - (b.left + b.width / 2)))
          : Math.abs(r.top + r.height / 2 - (b.top + b.height / 2));
        if (d < abst) { abst = d; best = k; }
      });
      // am Ende des Bandes: letztes Kind
      if (achse === 'x' && band.scrollLeft + band.clientWidth >= band.scrollWidth - 2) best = kinder().at(-1);
      return best;
    };
    let raf = 0, timer = 0;
    band.addEventListener('scroll', () => {
      cancelAnimationFrame(raf); raf = requestAnimationFrame(() => { protokoll('scroll (Rückfall)'); vorschau(naechstes()); });
      if (!KANN_ENDE) { clearTimeout(timer); timer = setTimeout(() => { protokoll('Timer 120 ms (Rückfall)'); fest(naechstes()); }, 120); }
    }, { passive: true });
    if (KANN_ENDE) band.addEventListener('scrollend', () => { protokoll('scrollend (Rückfall)'); fest(naechstes()); });
  }
  const verhalten = () => reduziert() ? 'auto' : 'smooth';
  function zeigeZahl(feld, n) {
    const t = String(n).padStart(2, '0'); const alt = feld.lastElementChild;
    if (alt && alt.textContent === t) return;
    if (reduziert() || !alt) { feld.innerHTML = `<span>${t}</span>`; return; }
    feld.querySelectorAll('.raus').forEach(s => s.remove());
    alt.className = 'raus'; const neu = document.createElement('span'); neu.className = 'rein'; neu.textContent = t; feld.appendChild(neu);
    setTimeout(() => alt.remove(), 170);
  }
  function wechsleText(box, inhalt) {
    if (box.dataset.jetzt === inhalt) return; box.dataset.jetzt = inhalt;
    const alt = box.querySelector('.neu, .start'); const neu = document.createElement('div'); neu.className = 'neu'; neu.innerHTML = inhalt;
    if (alt) { alt.className = 'alt'; setTimeout(() => alt.remove(), 150); }
    box.appendChild(neu);
  }

  // ================= zimmer =================
  const zBand = $('[data-z-band]'), zKarten = $$('.karte', zBand);
  const zText = $('[data-z-text]'), zZahl = $('[data-z-zahl]'), zStand = $('[data-z-stand]'), zAnsage = $('[data-z-ansage]');
  let zIndex = 0;
  const zInhalt = k => `<div><h2>${k.dataset.name}</h2><p>${k.dataset.text}</p><p class="preis">${k.dataset.preis}</p></div>`;
  function zVorschau(k) { const i = zKarten.indexOf(k); if (i < 0) return; if (i === zIndex) { zStand.dataset.zustand = 'fest'; zStand.lastChild.textContent = 'eingerastet'; zeigeZahl(zZahl, i + 1); wechsleText(zText, zInhalt(k)); return; } zStand.dataset.zustand = 'unterwegs'; zStand.lastChild.textContent = 'wechselt'; zeigeZahl(zZahl, i + 1); wechsleText(zText, zInhalt(k)); }
  function zFest(k) {
    const i = zKarten.indexOf(k); if (i < 0) return; zIndex = i;
    zKarten.forEach((x, j) => x.setAttribute('aria-current', String(j === i)));
    zStand.dataset.zustand = 'fest'; zStand.lastChild.textContent = 'eingerastet';
    zeigeZahl(zZahl, i + 1); wechsleText(zText, zInhalt(k));
    zAnsage.textContent = `${i + 1} von ${zKarten.length}: ${k.dataset.name}, ${k.dataset.preis}`;
    $('[data-z-zurueck]').disabled = i === 0; $('[data-z-vor]').disabled = i === zKarten.length - 1;
  }
  beobachte(zBand, 'x', zVorschau, zFest);
  function zGehe(i) { i = Math.max(0, Math.min(zKarten.length - 1, i)); zVorschau(zKarten[i]); zBand.scrollTo({ left: zKarten[i].offsetLeft - zBand.offsetLeft - 0, behavior: verhalten() }); }
  $('[data-z-vor]').addEventListener('click', () => zGehe(zIndex + 1));
  $('[data-z-zurueck]').addEventListener('click', () => zGehe(zIndex - 1));
  zBand.addEventListener('keydown', e => { if (e.key === 'ArrowRight') { e.preventDefault(); zGehe(zIndex + 1); } if (e.key === 'ArrowLeft') { e.preventDefault(); zGehe(zIndex - 1); } if (e.key === 'Home') { e.preventDefault(); zGehe(0); } if (e.key === 'End') { e.preventDefault(); zGehe(zKarten.length - 1); } });
  zText.innerHTML = `<div class="start">${zInhalt(zKarten[0]).slice(5, -6)}</div>`; zText.dataset.jetzt = zInhalt(zKarten[0]); zFest(zKarten[0]);

  // ================= galerie =================
  const gBand = $('[data-g-band]'), gBilder = $$('figure', gBand), gText = $('[data-g-text]'), gZahl = $('[data-g-zahl]'), gAnsage = $('[data-g-ansage]'), daumen = $('[data-daumen]');
  let gIndex = 0;
  gBilder.forEach((f, i) => {
    const li = document.createElement('li'); const b = document.createElement('button'); b.type = 'button';
    b.setAttribute('aria-label', `Bild ${i + 1}: ${f.dataset.text}`);
    b.innerHTML = `<img src="${f.querySelector('img').getAttribute('src')}" alt="" loading="lazy">`;
    b.addEventListener('click', () => gGehe(i)); li.appendChild(b); daumen.appendChild(li);
  });
  const dKnoepfe = $$('button', daumen);
  const gInhalt = f => `<p>${f.dataset.text}<small>${f.dataset.meta}</small></p>`;
  function gVorschau(f) { const i = gBilder.indexOf(f); if (i < 0) return; if (i === gIndex) { dKnoepfe.forEach(b => b.removeAttribute('data-vorschau')); zeigeZahl(gZahl, i + 1); wechsleText(gText, gInhalt(f)); return; } dKnoepfe.forEach((b, j) => b.toggleAttribute('data-vorschau', j === i && j !== gIndex)); zeigeZahl(gZahl, i + 1); wechsleText(gText, gInhalt(f)); }
  function gFest(f) {
    const i = gBilder.indexOf(f); if (i < 0) return; gIndex = i;
    dKnoepfe.forEach((b, j) => { b.removeAttribute('data-vorschau'); b.setAttribute('aria-current', String(j === i)); });
    { const b = dKnoepfe[i], l = b.offsetLeft - daumen.offsetLeft; if (l < daumen.scrollLeft || l + b.offsetWidth > daumen.scrollLeft + daumen.clientWidth) daumen.scrollTo({ left: l - 8, behavior: verhalten() }); } // nie scrollIntoView: das schöbe die ganze Seite
    zeigeZahl(gZahl, i + 1); wechsleText(gText, gInhalt(f));
    gAnsage.textContent = `Bild ${i + 1} von ${gBilder.length}: ${f.dataset.text}`;
    $('[data-g-zurueck]').disabled = i === 0; $('[data-g-vor]').disabled = i === gBilder.length - 1;
  }
  beobachte(gBand, 'x', gVorschau, gFest);
  function gGehe(i) { i = Math.max(0, Math.min(gBilder.length - 1, i)); gVorschau(gBilder[i]); gBand.scrollTo({ left: i * gBand.clientWidth, behavior: verhalten() }); }
  $('[data-g-vor]').addEventListener('click', () => gGehe(gIndex + 1));
  $('[data-g-zurueck]').addEventListener('click', () => gGehe(gIndex - 1));
  gBand.addEventListener('keydown', e => { if (e.key === 'ArrowRight') { e.preventDefault(); gGehe(gIndex + 1); } if (e.key === 'ArrowLeft') { e.preventDefault(); gGehe(gIndex - 1); } });
  gText.innerHTML = `<div class="start">${gInhalt(gBilder[0])}</div>`; gText.dataset.jetzt = gInhalt(gBilder[0]); gFest(gBilder[0]);

  // ================= rad =================
  const rad = $('[data-rad]'), rListe = $('[data-r-liste]'), rSumme = $('[data-r-summe]'), rPreis = $('[data-r-preis]'), rAb = $('[data-r-ab]'), rAnsage = $('[data-r-ansage]');
  const PREIS = 148, ANREISE = new Date(2026, 10, 14);
  const euro = new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR', maximumFractionDigits: 0 });
  const datum = new Intl.DateTimeFormat('de-DE', { weekday: 'short', day: 'numeric', month: 'short' });
  for (let n = 1; n <= 14; n++) { const li = document.createElement('li'); li.dataset.n = n; li.setAttribute('aria-hidden', 'true'); li.innerHTML = `${n}<small>${n === 1 ? 'Nacht' : 'Nächte'}</small>`; rListe.appendChild(li); }
  const rItems = $$('li', rListe);
  let rWert = 2;
  const rechne = n => { const ab = new Date(ANREISE); ab.setDate(ab.getDate() + n); return { preis: euro.format(n * PREIS), ab: datum.format(ab) }; };
  function rVorschau(li) { const n = +li.dataset.n; if (n === rWert) { rItems.forEach(x => x.removeAttribute('data-vorschau')); const r = rechne(n); rSumme.dataset.zustand = 'fest'; rPreis.textContent = r.preis; rAb.textContent = r.ab; return; } /* zurück zum gültigen Ziel: nichts in Schwebe, keine Ansage */ rItems.forEach(x => x.toggleAttribute('data-vorschau', x === li)); const r = rechne(n); rSumme.dataset.zustand = 'unterwegs'; rPreis.textContent = '≈ ' + r.preis; rAb.textContent = r.ab; }
  function rFest(li) {
    // Abgleich mit der Lage: beim ersten Layout nach display: none meldete Chromium einmal das Ziel von scrollTop 0
    const lage = Math.round(rad.scrollTop / 56) + 1; if (rad.offsetParent && lage !== +li.dataset.n) li = rItems[lage - 1] || li;
    const n = +li.dataset.n; rWert = n;
    rItems.forEach(x => { x.removeAttribute('data-vorschau'); x.setAttribute('aria-selected', String(x === li)); });
    const r = rechne(n); rSumme.dataset.zustand = 'fest'; rPreis.textContent = r.preis; rAb.textContent = r.ab;
    rad.setAttribute('aria-valuenow', n); rad.setAttribute('aria-valuetext', `${n} ${n === 1 ? 'Nacht' : 'Nächte'}, ${r.preis}`);
    rAnsage.textContent = `${n} ${n === 1 ? 'Nacht' : 'Nächte'}, Abreise ${r.ab}, ${r.preis}`;
    $('[data-r-minus]').disabled = n === 1; $('[data-r-plus]').disabled = n === 14;
  }
  // Rad ist ein Band in y: dieselbe Beobachtung, andere Achse
  beobachte(rad, 'y', rVorschau, rFest);
  function rGehe(n, glatt = true) { n = Math.max(1, Math.min(14, n)); rVorschau(rItems[n - 1]); rad.scrollTo({ top: (n - 1) * 56, behavior: glatt ? verhalten() : 'auto' }); }
  $('[data-r-minus]').addEventListener('click', () => rGehe(rWert - 1));
  $('[data-r-plus]').addEventListener('click', () => rGehe(rWert + 1));
  rad.addEventListener('keydown', e => {
    const k = { ArrowUp: -1, ArrowDown: 1, PageUp: -3, PageDown: 3 }[e.key];
    if (k) { e.preventDefault(); rGehe(rWert + k); }
    if (e.key === 'Home') { e.preventDefault(); rGehe(1); } if (e.key === 'End') { e.preventDefault(); rGehe(14); }
  });
  rItems.forEach(li => li.addEventListener('click', () => rGehe(+li.dataset.n)));
  $('[data-r-form]').addEventListener('submit', e => { e.preventDefault(); rAnsage.textContent = ''; setTimeout(() => rAnsage.textContent = `Anfrage für ${rWert} Nächte vorbereitet. In der echten Seite geht es hier zum Buchungssystem.`, 50); });

  // ================= Varianten =================
  function zeigeWerte() {
    const weg = KANN_SNAP ? '<b>scrollsnapchanging</b> → Vorschau · <b>scrollsnapchange</b> → fest' : `Rückfall: scroll + rAF → Vorschau · <b>${KANN_ENDE ? 'scrollend' : 'Timer 120 ms'}</b> → fest`;
    const zus = { zimmer: 'snap x mandatory, start · Zahl 220/160 ms (0.16,1,0.3,1 | 0.7,0,0.84,0) · Bildtext 300 ms (0.37,0,0.63,1)', galerie: 'snap x mandatory, center, snap-stop always · Daumen folgt schon in der Geste', rad: 'snap y mandatory, 56 px je Zeile · Preis ≈ in der Geste, gilt beim Einrasten · role=spinbutton' }[v];
    $('[data-werte]').innerHTML = `${weg} · ${zus}${letztes ? ' · zuletzt: <b>' + letztes + '</b>' : ''}`;
  }
  const IDS = ['zimmer', 'galerie', 'rad']; let v = IDS[0];
  function variante(neu) {
    v = IDS.includes(neu) ? neu : IDS[0]; html.dataset.v = v; letztes = '';
    $$('button[data-v]').forEach(b => b.setAttribute('aria-pressed', String(b.dataset.v === v)));
    zeigeWerte();
    if (v === 'rad') { rad.scrollTop = (rWert - 1) * 56; rFest(rItems[rWert - 1]); }
    const q = new URLSearchParams(location.search);
    if (q.get('v') !== v) { q.set('v', v); history.replaceState(null, '', '?' + q.toString().replace(/=(&|$)/g, '$1')); }
  }
  $$('button[data-v]').forEach(b => b.addEventListener('click', () => { pause(); variante(b.dataset.v); }));
  variante(q0.get('v'));

  // ================= Vorführung: Band schrittweise weiter, am Ende zurück =================
  let zeit = [], laeuft = false;
  const nach = (ms, f) => zeit.push(setTimeout(f, ms));
  function schritt() {
    if (!laeuft) return;
    if (v === 'zimmer') { const n = zIndex >= zKarten.length - 1 ? 0 : zIndex + 1; zGehe(n); }
    else if (v === 'galerie') { const n = gIndex >= gBilder.length - 1 ? 0 : gIndex + 1; gGehe(n); }
    else { const folge = [2, 5, 7, 3, 2]; const i = (folge.indexOf(rWert) + 1) % folge.length; rGehe(folge[i]); }
    nach(2200, schritt);
  }
  function neustart() { zeit.forEach(clearTimeout); zeit = []; zBand.scrollTo({ left: 0 }); gBand.scrollTo({ left: 0 }); rGehe(2, false); }
  function vorfuehren() { neustart(); laeuft = true; html.classList.add('vorfuehrung'); nach(1200, schritt); }
  function pause() { laeuft = false; zeit.forEach(clearTimeout); zeit = []; html.classList.remove('vorfuehrung'); }
  function weiter() { if (!laeuft) { laeuft = true; html.classList.add('vorfuehrung'); nach(600, schritt); } }

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