Abschnittsweises Einrasten (Fullpage-Snap)
Fünf Bühnen eines Hotels in Meran: Jeder Scroll-Impuls führt zur nächsten Folie, erst im eingerasteten Zustand treten Text und Bild auf.
Jeder Abschnitt füllt den Viewport. Ein Scroll-Impuls führt zum nächsten Abschnitt, die Seite rastet ein. Das geht nativ mit scroll-snap-type oder gekapert: GSAP Observer fängt Wheel und Touch ab und spielt einen zeitbasierten Übergang. Erst im eingerasteten Zustand treten die Inhalte auf, in neuen Browsern per scroll-state(snapped) ganz ohne JavaScript.
Die Seite wird zur Präsentation. Jeder Abschnitt ist eine fertig komponierte Bühne, die nie halb angeschnitten erscheint.
Wann einsetzen
Kurze Kampagnen- und Launch-Seiten, Portfolio-Slideshows, Kiosk-Anwendungen, ein Haus in wenigen Räumen zeigen.
Wann nicht
Bei Inhalten unterschiedlicher Höhe, langen Seiten und Lesetexten. Viele Nutzer empfinden harte Snaps als bevormundend, besonders wenn sie Preise vergleichen wollen.
Varianten
- Native scroll-snap mit mandatory, weicher mit proximity
- ScrollTrigger snap: rastet nach dem Scroll-Ende weich ein
- GSAP Observer mit eigener Übergangsanimation (Scroll-Jacking)
- Lenis snap
Umsetzung
Nativ: html { scroll-snap-type: y mandatory } section { scroll-snap-align: start; min-height: 100svh }. Inhalte beim Einrasten: section { container-type: scroll-state } und darin @container scroll-state(snapped: y) { … }, sonst eine Klasse per IntersectionObserver. ScrollTrigger: snap: { snapTo: 1 / (n - 1), duration: { min: 0.2, max: 0.6 }, ease: 'power3' }. Observer: Observer.create({ type: 'wheel,touch', wheelSpeed: -1, onUp, onDown, tolerance: 10, preventDefault: true }) und eine Timeline pro Wechsel. Diese Variante nur mit vollständiger Tastatursteuerung (Pfeile, Bild ab, Leertaste, Pos1, Ende).
Robust umsetzen
Telefon. mandatory vermeiden, weil Adressleiste und hohe Inhalte springen. Besser proximity oder gar kein Snap.
Ohne Bewegung. Kein Snap. Übergänge ohne Bewegung, Inhalte nur kurz einblenden.
Barrierefrei. Scroll-Jacking bricht die Erwartung. Inhalte, die höher als der Viewport sind, werden mit mandatory unerreichbar. Tastatur und Bildschirmleser testen, Abschnitte mit aria-label benennen und eine sichtbare Abschnittsnavigation anbieten.
Code
<!DOCTYPE html>
<!--
Abschnittsweises Einrasten (Fullpage-Snap)
Fundus · Scroll & Section-Animationen · https://cecodesign.de/lab/fundus/scroll/fullpage-snap-slides/
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>Abschnittsweises Einrasten (Fullpage-Snap) · 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; }
:root {
/* Rezept boutique-hotel-gastgeber: Sand, Creme, warmes Dunkel, ein Naturton aus dem Haus */
--sand: #f3eee7; --creme: #fef2e3; --dunkel: #3e3a37; --linie: #d5d1c8; --meta: #5e605a; --holz: #6e5a47; --reb: #5e6b4e;
--kurve: cubic-bezier(0.25, 1, 0.5, 1); /* power3.out, GSAP-Snap-Standard */
--ui: cubic-bezier(0.4, 0, 0.2, 1);
--rand: clamp(20px, 4vw, 56px);
}
* { box-sizing: border-box; margin: 0; padding: 0; }
html { background: var(--sand); color: var(--dunkel); font-family: 'Mona Sans', system-ui, sans-serif; font-stretch: 100%; -webkit-font-smoothing: antialiased; }
body { font-size: 1.0625rem; line-height: 1.6; }
button { font: inherit; color: inherit; background: none; border: 0; cursor: pointer; }
.label { font-size: .75rem; font-stretch: 125%; font-weight: 500; letter-spacing: .06em; text-transform: uppercase; line-height: 1.3; }
/* ---------- Kopf, Zähler, Punkte ---------- */
.kopf { position: fixed; z-index: 20; inset: 0 0 auto; display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 22px var(--rand); color: var(--ton-text, var(--dunkel)); transition: color .4s var(--ui); pointer-events: none; }
.kopf > * { pointer-events: auto; }
.marke { display: flex; align-items: baseline; gap: 14px; text-decoration: none; color: inherit; }
.marke b { font: 400 1.5rem/1 'Newsreader', serif; font-variation-settings: 'opsz' 36; letter-spacing: -.01em; }
.marke b i { font-weight: 300; }
.kopf__rechts { display: flex; align-items: center; gap: 22px; }
.zaehler { font-variant-numeric: tabular-nums; }
.zaehler span { display: inline-block; min-width: 2ch; }
.sprachen { display: flex; gap: 10px; opacity: .8; }
.sprachen b { font-weight: 600; opacity: 1; }
.knopf { display: inline-flex; align-items: center; gap: 10px; height: 44px; padding: 0 20px; border-radius: 2px; background: var(--holz); color: var(--creme); transition: background-color .2s var(--ui); white-space: nowrap; }
.knopf:hover { background: #5a4939; }
.knopf:focus-visible, .punkte button:focus-visible { outline: 2px solid currentColor; outline-offset: 3px; }
.punkte { position: fixed; z-index: 20; right: calc(var(--rand) - 6px); top: 50%; transform: translateY(-50%); display: grid; gap: 6px; color: var(--ton-text, var(--dunkel)); transition: color .4s var(--ui); }
.punkte button { display: flex; align-items: center; justify-content: flex-end; gap: 10px; height: 26px; padding: 0 6px; opacity: .45; transition: opacity .2s var(--ui); }
.punkte button::after { content: ''; width: 14px; height: 1px; background: currentColor; transition: width .6s var(--kurve); }
.punkte button span { opacity: 0; transform: translateX(6px); transition: opacity .2s var(--ui), transform .6s var(--kurve); }
.punkte button:hover, .punkte button[aria-current='true'] { opacity: 1; }
.punkte button[aria-current='true']::after { width: 34px; }
.punkte button:hover span, .punkte button[aria-current='true'] span { opacity: 1; transform: none; }
/* ---------- Folien ---------- */
.folie { position: relative; min-height: 100svh; display: grid; align-items: center; overflow: hidden; background: var(--grund); color: var(--text); }
.folie[data-ton='sand'] { --grund: var(--sand); --text: var(--dunkel); }
.folie[data-ton='creme'] { --grund: var(--creme); --text: var(--dunkel); }
.folie[data-ton='reb'] { --grund: #4f5a42; --text: var(--creme); --meta-f: rgba(254, 242, 227, .8); --linie-f: rgba(254, 242, 227, .22); }
.folie[data-ton='stein'] { --grund: #e6e2db; --text: var(--dunkel); }
.folie[data-ton='nacht'] { --grund: var(--dunkel); --text: var(--creme); --meta-f: rgba(254, 242, 227, .68); --linie-f: rgba(254, 242, 227, .2); }
.innen { width: 100%; min-height: 100svh; display: grid; grid-template-columns: minmax(0, 1.05fr) minmax(0, .95fr); align-items: center; gap: clamp(32px, 6vw, 112px); padding: 104px calc(var(--rand) + 84px) 64px var(--rand); }
.folie:nth-child(even) .innen { grid-template-columns: minmax(0, .95fr) minmax(0, 1.05fr); }
.folie:nth-child(even) .bogen { order: -1; }
.text { display: grid; gap: 22px; max-width: 34rem; }
.folie:nth-child(even) .text { justify-self: start; }
.eyebrow { display: flex; gap: 14px; align-items: center; color: var(--meta-f, var(--meta)); }
.eyebrow::before { content: ''; width: 28px; height: 1px; background: currentColor; }
h1, h2 { font: 300 clamp(2.5rem, 1.5rem + 4vw, 5.5rem)/1.05 'Newsreader', serif; font-variation-settings: 'opsz' 72; letter-spacing: -.015em; text-wrap: balance; hyphens: manual; }
h1 em, h2 em { font-style: italic; font-weight: 300; }
.lead { max-width: 30rem; color: var(--text); opacity: .86; text-wrap: pretty; }
.fakten { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); border-top: 1px solid var(--linie-f, var(--linie)); max-width: 30rem; }
.fakten div { padding: 12px 0; border-bottom: 1px solid var(--linie-f, var(--linie)); }
.fakten div:nth-child(odd) { padding-right: 16px; }
.fakten dt { color: var(--meta-f, var(--meta)); }
.fakten dd { font-size: 1rem; font-variant-numeric: tabular-nums; }
/* Bogenfenster mit eigener Szene (SVG) */
.bogen { justify-self: center; width: min(100%, 31rem, calc((100svh - 220px) * .75)); display: grid; gap: 14px; }
.rahmen { position: relative; aspect-ratio: 3 / 4; overflow: hidden; border-radius: 50% 50% 2px 2px / 37.5% 37.5% 2px 2px; background: #d9c8b4; isolation: isolate; }
.szene { position: absolute; inset: -2px; transform-origin: 50% 70%; }
.szene svg { display: block; width: 100%; height: 100%; }
.bogen figcaption { display: flex; justify-content: space-between; gap: 12px; color: var(--meta-f, var(--meta)); }
/* Buchungsleiste in der ersten Folie */
.buchung { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); align-items: stretch; background: var(--creme); border: 1px solid var(--linie); max-width: 32rem; margin-top: 6px; }
.buchung button.feld:nth-child(3) { border-right: 0; }
.buchung button.feld { display: grid; gap: 2px; text-align: left; padding: 12px 16px; border-right: 1px solid var(--linie); transition: background-color .2s var(--ui); }
.buchung button.feld:hover { background: var(--sand); }
.buchung .feld small { color: var(--meta); }
.buchung .feld span { font-size: .9375rem; white-space: nowrap; }
.buchung .knopf { grid-column: 1 / -1; height: 50px; border-radius: 0; justify-content: space-between; }
.buchung .knopf::after, .folie[data-ton='nacht'] .knopf::after { content: '→'; font-stretch: 100%; letter-spacing: 0; font-size: 1rem; }
.angebot { display: grid; gap: 0; max-width: 32rem; border: 1px solid var(--linie-f); }
.angebot > div { display: flex; flex-wrap: wrap; align-items: baseline; justify-content: space-between; gap: 4px 16px; padding: 14px 18px; border-bottom: 1px solid var(--linie-f); font-variant-numeric: tabular-nums; }
.angebot > div:last-child { border-bottom: 0; }
.angebot small { color: var(--meta-f); }
.angebot .summe { font: 400 1.5rem/1.2 'Newsreader', serif; }
.vorteile { list-style: none; display: grid; gap: 6px; color: var(--meta-f); font-size: .9375rem; }
.vorteile li::before { content: '+ '; color: var(--text); }
.folie[data-ton='nacht'] .knopf { background: var(--creme); color: var(--dunkel); justify-self: start; }
.folie[data-ton='nacht'] .knopf:hover { background: #fff; }
/* ---------- Einrasten sichtbar machen ---------- */
/* Inhalte treten erst im eingerasteten Zustand auf: nativ per scroll-state(snapped), sonst per Klasse .aktiv */
.rein { opacity: 0; transform: translateY(28px); transition: opacity .6s var(--kurve), transform .6s var(--kurve); transition-delay: 0ms; }
.szene { transform: scale(1.06); transition: transform .9s var(--kurve); }
.zeigt-aktiv .folie.aktiv .rein, .alles-da .rein { opacity: 1; transform: none; transition-delay: calc(var(--i, 0) * 80ms); }
.zeigt-aktiv .folie.aktiv .szene, .alles-da .szene { transform: none; }
/* Tastatur: Inhalte einer Folie mit Fokus sofort zeigen, nicht erst nach dem Einrasten */
.folie:focus-within .rein { opacity: 1; transform: none; transition-delay: 0ms; }
@supports (container-type: scroll-state) {
.nativ .folie { container-type: scroll-state; }
@container scroll-state(snapped: y) {
.rein { opacity: 1; transform: none; transition-delay: calc(var(--i, 0) * 80ms); }
.szene { transform: none; }
}
}
/* A) CSS nativ: der Browser rastet ein */
html.nativ { scroll-snap-type: y mandatory; }
.nativ .folie { scroll-snap-align: start; scroll-snap-stop: always; }
html.selbstlauf { scroll-snap-type: none; }
/* C) Observer: Scrollen wird abgefangen, Folien liegen übereinander */
html.observer, .observer body { overflow: hidden; height: 100%; overscroll-behavior: none; }
.observer .spur { position: fixed; inset: 0; }
.observer .folie { position: absolute; inset: 0; height: 100svh; visibility: hidden; will-change: transform; }
.observer .folie.aktiv { visibility: visible; }
/* ---------- Demo-Pult ---------- */
.pult { position: fixed; z-index: 30; top: 18px; left: 50%; transform: translateX(-50%); display: grid; justify-items: center; gap: 8px; font: 500 10.5px/1.4 'Geist Mono', ui-monospace, monospace; letter-spacing: .14em; text-transform: uppercase; }
.pult__knoepfe { display: flex; gap: 2px; padding: 3px; border-radius: 999px; background: rgba(62, 58, 55, .86); -webkit-backdrop-filter: blur(10px); backdrop-filter: blur(10px); }
.pult button { height: 26px; padding: 0 12px; border-radius: 999px; color: rgba(254, 242, 227, .7); letter-spacing: inherit; text-transform: inherit; transition: background-color .15s var(--ui), color .15s var(--ui); }
.pult button:hover { color: var(--creme); }
.pult button[aria-pressed='true'] { background: var(--creme); color: var(--dunkel); }
.pult button:disabled { opacity: .4; cursor: not-allowed; }
.pult__werte { padding: 5px 12px; border-radius: 999px; background: rgba(254, 242, 227, .88); color: var(--dunkel); text-transform: none; letter-spacing: .04em; white-space: nowrap; }
/* ---------- Vorschau, Telefon, reduzierte Bewegung ---------- */
.vorschau body { overflow: hidden; }
.vorschau .punkte span { display: none; }
@media (max-width: 900px) {
.innen, .folie:nth-child(even) .innen { grid-template-columns: 1fr; align-content: center; gap: 20px; padding: 84px var(--rand) 28px; }
.folie:nth-child(even) .bogen { order: 0; }
.bogen { width: min(62vw, 300px, calc(34svh * .75)); }
.bogen figcaption { display: none; }
.text { gap: 14px; }
.punkte { display: none; }
.sprachen { display: none; }
}
@media (max-width: 640px) {
.kopf { padding: 14px var(--rand); }
.marke b { font-size: 1.25rem; }
.marke .label { display: none; }
.kopf .knopf { height: 36px; padding: 0 14px; }
.lead { font-size: 1rem; line-height: 1.5; }
.fakten div:nth-child(n+3) { display: none; }
.buchung { grid-template-columns: 1fr 1fr; }
.buchung button.feld:nth-child(3) { display: none; }
.buchung button.feld:nth-child(2) { border-right: 0; }
.buchung .knopf { height: 46px; }
.pult { top: auto; bottom: 14px; }
.pult__werte { white-space: normal; text-align: center; max-width: 92vw; }
.angebot > div { padding: 10px 14px; }
.vorteile li:nth-child(n+3) { display: none; }
}
/* Touch: weicher einrasten (mandatory springt mit der Adressleiste) */
@media (hover: none) { html.nativ { scroll-snap-type: y proximity; } .nativ .folie { scroll-snap-stop: normal; } }
/* Reduzierte Bewegung: kein Einrasten, nur kurzes Einblenden */
html.reduziert { scroll-snap-type: none !important; }
.reduziert .rein { transform: none !important; transition: opacity .2s linear !important; }
.reduziert .szene { transform: none !important; transition: none !important; }
.reduziert .punkte button::after, .reduziert .punkte button span { transition: none; }
@media (forced-colors: active) {
button, [role=button], [role=tab], [role=switch] { border: 1px solid ButtonText !important; }
}
</style>
</head>
<body>
<header class="kopf">
<a class="marke" href="#f1" aria-label="Villa Waalhang, Meran"><b>Villa <i>Waalhang</i></b><span class="label">Meran · Obermais</span></a>
<div class="kopf__rechts label">
<p class="zaehler" aria-live="polite"><span data-nr>01</span> / 05</p>
<p class="sprachen" aria-label="Sprache"><b>DE</b><span>EN</span><span>IT</span></p>
<button class="knopf label" type="button" data-zu="4">Buchen</button>
</div>
</header>
<nav class="punkte label" aria-label="Abschnitte">
<button type="button" data-zu="0"><span>Ankommen</span></button>
<button type="button" data-zu="1"><span>Kastanienstube</span></button>
<button type="button" data-zu="2"><span>Garten</span></button>
<button type="button" data-zu="3"><span>Frühstück</span></button>
<button type="button" data-zu="4"><span>Ihr Aufenthalt</span></button>
</nav>
<main class="spur">
<!-- 01 Ankommen -->
<section class="folie" id="f1" data-ton="sand" aria-label="01 Ankommen">
<div class="innen">
<div class="text">
<p class="eyebrow label rein" style="--i:0">01 · Seit 1908 am Hang</p>
<h1 class="rein" style="--i:1">Zwölf Zimmer <em>über Meran.</em></h1>
<p class="lead rein" style="--i:2">Eine Jugendstilvilla in Obermais, zwischen Weinbergen und dem Waalweg. Wir sind Vroni und Lukas Pircher und führen das Haus in dritter Generation.</p>
<div class="buchung rein" style="--i:3" role="group" aria-label="Verfügbarkeit">
<button class="feld" type="button"><small class="label">Anreise</small><span>Fr, 20. Nov.</span></button>
<button class="feld" type="button"><small class="label">Abreise</small><span>So, 22. Nov.</span></button>
<button class="feld" type="button"><small class="label">Gäste</small><span>2 Erwachsene</span></button>
<button class="knopf label" type="button" data-zu="4">Verfügbarkeit prüfen</button>
</div>
</div>
<figure class="bogen">
<div class="rahmen"><div class="szene" aria-hidden="true">
<svg viewBox="0 0 600 800" preserveAspectRatio="xMidYMid slice">
<defs><pattern id="w-reben1" width="22" height="22" patternUnits="userSpaceOnUse" patternTransform="rotate(-24)"><rect width="22" height="22" fill="#5e6b4e"/><rect y="9" width="22" height="4" fill="#4b573d"/></pattern></defs>
<rect width="600" height="800" fill="#ebcdb0"/>
<circle cx="392" cy="292" r="74" fill="#f6e3cb"/>
<path d="M0 430 L70 372 L128 398 L214 300 L262 338 L332 262 L404 336 L470 304 L540 352 L600 330 V800 H0Z" fill="#a3a49f"/>
<path d="M214 300 L242 324 L228 322 L214 334 L200 318Z M332 262 L364 296 L346 292 L332 304 L316 290Z M470 304 L492 322 L470 318 L456 326Z" fill="#f3eee7"/>
<path d="M0 470 C90 430 160 452 250 420 C340 390 430 420 600 396 V800 H0Z" fill="#8a9175"/>
<path d="M0 560 C120 520 240 548 340 506 C430 470 520 486 600 470 V800 H0Z" fill="url(#w-reben1)"/>
<!-- Villa -->
<g transform="translate(300 418)">
<rect x="0" y="40" width="150" height="92" fill="#fef2e3"/>
<rect x="104" y="6" width="46" height="126" fill="#f8e9d6"/>
<path d="M-8 44 L75 12 L158 44Z" fill="#8a5a44"/>
<path d="M98 10 L127 -16 L156 10Z" fill="#7a4e3b"/>
<g fill="#5e6b4e"><rect x="18" y="62" width="14" height="22"/><rect x="48" y="62" width="14" height="22"/><rect x="78" y="62" width="14" height="22"/><rect x="18" y="98" width="14" height="22"/><rect x="48" y="98" width="14" height="22"/><rect x="78" y="98" width="14" height="22"/><rect x="120" y="26" width="14" height="20"/></g>
<path d="M112 132 V100 a15 15 0 0 1 30 0 V132Z" fill="#c9a27e"/>
</g>
<!-- Zypressen und Palmen -->
<path d="M262 560 C248 520 250 456 262 404 C274 456 276 520 262 560Z" fill="#3f4a36"/>
<path d="M474 548 C462 512 464 460 474 420 C484 460 486 512 474 548Z" fill="#3f4a36"/>
<g stroke="#3e3a37" stroke-linecap="round" fill="none">
<path d="M150 640 C146 580 152 520 168 470" stroke-width="7"/>
<path d="M168 470 C140 452 112 456 92 474 M168 470 C150 440 124 430 100 434 M168 470 C176 438 200 424 226 428 M168 470 C196 456 222 462 238 482 M168 470 C166 446 172 428 186 414" stroke-width="5"/>
</g>
<path d="M0 640 C140 610 300 650 600 600 V800 H0Z" fill="#d9c3a5"/>
<path d="M0 668 C150 640 320 678 600 628" stroke="#a8bdb7" stroke-width="5" fill="none"/>
<rect x="0" y="700" width="600" height="100" fill="#cdb08c"/>
<g fill="#dcc4a3"><rect x="-20" y="712" width="45" height="24" rx="9"/><rect x="31" y="712" width="41" height="27" rx="9"/><rect x="78" y="712" width="50" height="25" rx="9"/><rect x="134" y="712" width="42" height="22" rx="9"/><rect x="182" y="712" width="40" height="22" rx="9"/><rect x="228" y="712" width="50" height="26" rx="9"/><rect x="284" y="712" width="47" height="28" rx="9"/><rect x="337" y="712" width="62" height="22" rx="9"/><rect x="405" y="712" width="45" height="26" rx="9"/><rect x="456" y="712" width="55" height="24" rx="9"/><rect x="517" y="712" width="46" height="28" rx="9"/><rect x="569" y="712" width="43" height="28" rx="9"/><rect x="618" y="712" width="41" height="24" rx="9"/><rect x="-6" y="742" width="44" height="22" rx="9"/><rect x="44" y="742" width="64" height="27" rx="9"/><rect x="114" y="742" width="63" height="24" rx="9"/><rect x="183" y="742" width="63" height="24" rx="9"/><rect x="252" y="742" width="44" height="23" rx="9"/><rect x="302" y="742" width="47" height="24" rx="9"/><rect x="355" y="742" width="58" height="28" rx="9"/><rect x="419" y="742" width="61" height="28" rx="9"/><rect x="486" y="742" width="49" height="22" rx="9"/><rect x="541" y="742" width="57" height="24" rx="9"/><rect x="604" y="742" width="59" height="25" rx="9"/><rect x="8" y="772" width="54" height="23" rx="9"/><rect x="68" y="772" width="43" height="23" rx="9"/><rect x="117" y="772" width="53" height="24" rx="9"/><rect x="176" y="772" width="40" height="28" rx="9"/><rect x="222" y="772" width="55" height="28" rx="9"/><rect x="283" y="772" width="47" height="22" rx="9"/><rect x="336" y="772" width="47" height="26" rx="9"/><rect x="389" y="772" width="60" height="24" rx="9"/><rect x="455" y="772" width="62" height="26" rx="9"/><rect x="523" y="772" width="44" height="25" rx="9"/><rect x="573" y="772" width="51" height="26" rx="9"/></g>
</svg>
</div></div>
<figcaption class="label rein" style="--i:4"><span>Blick vom Waalweg</span><span>17:40 Uhr</span></figcaption>
</figure>
</div>
</section>
<!-- 02 Kastanienstube -->
<section class="folie" id="f2" data-ton="creme" aria-label="02 Kastanienstube">
<div class="innen">
<div class="text">
<p class="eyebrow label rein" style="--i:0">02 · Zimmer & Suiten</p>
<h2 class="rein" style="--i:1">Die <em>Kastanienstube.</em></h2>
<p class="lead rein" style="--i:2">Unsere größte Suite: Lärchenboden, ein grüner Kachelofen und ein Balkon nach Süden. Morgens liegt die Texelgruppe im Fenster.</p>
<dl class="fakten rein" style="--i:3">
<div><dt class="label">Größe</dt><dd>38 m²</dd></div>
<div><dt class="label">Bett</dt><dd>1,80 × 2,00 m</dd></div>
<div><dt class="label">Ausblick</dt><dd>Texelgruppe, Süd</dd></div>
<div><dt class="label">Preis</dt><dd>ab 240 € pro Nacht</dd></div>
</dl>
</div>
<figure class="bogen">
<div class="rahmen"><div class="szene" aria-hidden="true">
<svg viewBox="0 0 600 800" preserveAspectRatio="xMidYMid slice">
<rect width="600" height="800" fill="#eadfcf"/>
<rect y="470" width="600" height="250" fill="#c9a27e"/>
<g fill="#b88f6a"><rect x="60" y="470" width="3" height="250"/><rect x="140" y="470" width="3" height="250"/><rect x="220" y="470" width="3" height="250"/><rect x="300" y="470" width="3" height="250"/><rect x="380" y="470" width="3" height="250"/><rect x="460" y="470" width="3" height="250"/><rect x="540" y="470" width="3" height="250"/></g>
<rect y="462" width="600" height="10" fill="#a9825f"/>
<!-- Fenster -->
<rect x="96" y="150" width="236" height="290" fill="#fef2e3"/>
<rect x="112" y="166" width="204" height="258" fill="#cfd8d6"/>
<path d="M112 330 L160 286 L196 304 L240 252 L276 284 L316 262 V424 H112Z" fill="#8f989b"/>
<path d="M240 252 L258 270 L246 268 L234 276 L226 266Z M160 286 L174 298 L160 296 L150 302Z" fill="#f3eee7"/>
<path d="M112 380 C170 360 240 372 316 352 V424 H112Z" fill="#6f7a5c"/>
<rect x="210" y="166" width="8" height="258" fill="#fef2e3"/><rect x="112" y="282" width="204" height="8" fill="#fef2e3"/>
<rect x="60" y="136" width="44" height="320" fill="#a86a4e"/><rect x="324" y="136" width="44" height="320" fill="#a86a4e"/>
<rect x="52" y="128" width="324" height="10" fill="#6e5a47"/>
<!-- Kachelofen -->
<g transform="translate(406 300)">
<rect x="0" y="0" width="160" height="380" fill="#6f7f68"/>
<g fill="#7f8f77"><rect x="8" y="8" width="44" height="44"/><rect x="58" y="8" width="44" height="44"/><rect x="108" y="8" width="44" height="44"/><rect x="8" y="58" width="44" height="44"/><rect x="58" y="58" width="44" height="44"/><rect x="108" y="58" width="44" height="44"/><rect x="8" y="108" width="44" height="44"/><rect x="58" y="108" width="44" height="44"/><rect x="108" y="108" width="44" height="44"/><rect x="8" y="158" width="44" height="44"/><rect x="58" y="158" width="44" height="44"/><rect x="108" y="158" width="44" height="44"/></g>
<g fill="#5d6b57"><circle cx="30" cy="30" r="8"/><circle cx="80" cy="80" r="8"/><circle cx="130" cy="30" r="8"/><circle cx="30" cy="130" r="8"/><circle cx="130" cy="130" r="8"/><circle cx="80" cy="180" r="8"/></g>
<rect x="-14" y="210" width="188" height="18" fill="#6e5a47"/>
<rect x="0" y="228" width="160" height="152" fill="#66755f"/>
</g>
<rect x="380" y="560" width="220" height="22" fill="#6e5a47"/>
<!-- Leuchte und Tisch -->
<rect x="214" y="0" width="2" height="96" fill="#3e3a37"/>
<path d="M186 124 L244 124 L230 96 L200 96Z" fill="#3e3a37"/>
<ellipse cx="215" cy="130" rx="40" ry="6" fill="#f6e3cb" opacity=".8"/>
<rect x="130" y="600" width="170" height="14" fill="#6e5a47"/><rect x="146" y="614" width="10" height="100" fill="#6e5a47"/><rect x="274" y="614" width="10" height="100" fill="#6e5a47"/>
<circle cx="232" cy="586" r="15" fill="#a8473a"/><rect x="231" y="566" width="3" height="8" fill="#3e3a37"/>
<rect x="0" y="720" width="600" height="80" fill="#b99678"/>
</svg>
</div></div>
<figcaption class="label rein" style="--i:4"><span>Suite 7 · zweiter Stock</span><span>08:10 Uhr</span></figcaption>
</figure>
</div>
</section>
<!-- 03 Garten -->
<section class="folie" id="f3" data-ton="reb" aria-label="03 Garten">
<div class="innen">
<div class="text">
<p class="eyebrow label rein" style="--i:0">03 · Garten & Becken</p>
<h2 class="rein" style="--i:1">Ein Garten, <em>der nach Süden fällt.</em></h2>
<p class="lead rein" style="--i:2">Drei Terrassen mit Pergola, Kakibaum und einem Becken, das wir bis Ende Oktober heizen. Im November sitzt man hier mittags noch in der Sonne.</p>
<dl class="fakten rein" style="--i:3">
<div><dt class="label">Becken</dt><dd>12 × 4 m, 26 °C</dd></div>
<div><dt class="label">Liegen</dt><dd>14, ohne Reservierung</dd></div>
<div><dt class="label">Sauna</dt><dd>täglich 15–20 Uhr</dd></div>
<div><dt class="label">Bademäntel</dt><dd>im Zimmer</dd></div>
</dl>
</div>
<figure class="bogen">
<div class="rahmen"><div class="szene" aria-hidden="true">
<svg viewBox="0 0 600 800" preserveAspectRatio="xMidYMid slice">
<rect width="600" height="800" fill="#e7dccb"/>
<path d="M0 330 L90 270 L150 300 L250 214 L330 270 L420 220 L500 268 L600 236 V800 H0Z" fill="#aeb0a7"/>
<path d="M250 214 L274 236 L262 234 L250 244 L238 232Z M420 220 L440 238 L428 236 L418 244 L408 234Z" fill="#f3eee7"/>
<path d="M0 400 H600 V800 H0Z" fill="#7d8669"/>
<rect y="400" width="600" height="14" fill="#d9c3a5"/>
<path d="M0 500 H600 V800 H0Z" fill="#6c7759"/>
<rect y="500" width="600" height="16" fill="#d9c3a5"/>
<!-- Kakibaum -->
<rect x="96" y="380" width="10" height="70" fill="#5a4535"/>
<circle cx="100" cy="350" r="56" fill="#5e6b4e"/>
<g fill="#d9813f"><circle cx="76" cy="334" r="7"/><circle cx="112" cy="320" r="7"/><circle cx="128" cy="358" r="7"/><circle cx="90" cy="372" r="7"/><circle cx="62" cy="362" r="6"/></g>
<!-- Zypressen -->
<path d="M470 500 C456 450 458 370 470 300 C482 370 484 450 470 500Z" fill="#3f4a36"/>
<path d="M528 500 C518 460 520 400 528 350 C536 400 538 460 528 500Z" fill="#3f4a36"/>
<!-- Pergola -->
<g fill="#6e5a47"><rect x="176" y="430" width="10" height="72"/><rect x="306" y="430" width="10" height="72"/><rect x="420" y="430" width="10" height="72"/><rect x="160" y="424" width="290" height="10"/></g>
<g fill="#5e6b4e"><circle cx="190" cy="420" r="16"/><circle cx="222" cy="414" r="12"/><circle cx="256" cy="420" r="15"/><circle cx="292" cy="414" r="12"/><circle cx="330" cy="420" r="16"/><circle cx="366" cy="414" r="12"/><circle cx="404" cy="420" r="15"/><circle cx="436" cy="416" r="11"/></g>
<!-- Becken -->
<rect x="60" y="560" width="480" height="120" fill="#e6dccb"/>
<rect x="80" y="576" width="440" height="88" fill="#8db0aa"/>
<g fill="#a9c6c0"><rect x="110" y="596" width="90" height="4"/><rect x="250" y="616" width="120" height="4"/><rect x="390" y="604" width="80" height="4"/><rect x="150" y="640" width="70" height="4"/></g>
<!-- Liegen -->
<g fill="#fef2e3"><rect x="90" y="712" width="120" height="16"/><rect x="250" y="712" width="120" height="16"/><rect x="410" y="712" width="120" height="16"/></g>
<g fill="#6e5a47"><rect x="96" y="728" width="6" height="18"/><rect x="200" y="728" width="6" height="18"/><rect x="256" y="728" width="6" height="18"/><rect x="360" y="728" width="6" height="18"/><rect x="416" y="728" width="6" height="18"/><rect x="520" y="728" width="6" height="18"/></g>
<g stroke="#3e3a37" stroke-linecap="round" fill="none">
<path d="M560 800 C556 720 560 640 548 560" stroke-width="7"/>
<path d="M548 560 C520 540 494 544 476 562 M548 560 C534 530 508 518 486 522 M548 560 C556 528 580 514 604 518 M548 560 C548 536 554 518 566 504" stroke-width="5"/>
</g>
</svg>
</div></div>
<figcaption class="label rein" style="--i:4"><span>Untere Terrasse</span><span>12:30 Uhr</span></figcaption>
</figure>
</div>
</section>
<!-- 04 Frühstück -->
<section class="folie" id="f4" data-ton="stein" aria-label="04 Frühstück">
<div class="innen">
<div class="text">
<p class="eyebrow label rein" style="--i:0">04 · Frühstück & Stube</p>
<h2 class="rein" style="--i:1">Frühstück <em>bis elf.</em></h2>
<p class="lead rein" style="--i:2">Vinschger Paarl vom Bäcker in Obermais, Apfelmus aus unseren Bäumen. Im November ist Törggelen: donnerstags ab 18 Uhr Keschtn, Nuier und Schlutzkrapfen in der Stube.</p>
<dl class="fakten rein" style="--i:3">
<div><dt class="label">Frühstück</dt><dd>7:30 bis 11 Uhr</dd></div>
<div><dt class="label">Törggelen</dt><dd>Do ab 18 Uhr</dd></div>
<div><dt class="label">Stube</dt><dd>22 Plätze</dd></div>
<div><dt class="label">Für Gäste</dt><dd>inklusive</dd></div>
</dl>
</div>
<figure class="bogen">
<div class="rahmen"><div class="szene" aria-hidden="true">
<svg viewBox="0 0 600 800" preserveAspectRatio="xMidYMid slice">
<rect width="600" height="800" fill="#b98a62"/>
<g fill="#ad7f58"><rect y="120" width="600" height="3"/><rect y="290" width="600" height="3"/><rect y="460" width="600" height="3"/><rect y="630" width="600" height="3"/></g>
<rect x="-40" y="330" width="380" height="250" fill="#f3eee7" transform="rotate(-8 150 455)"/>
<rect x="-40" y="330" width="380" height="6" fill="#d5d1c8" transform="rotate(-8 150 455)"/>
<!-- Teller mit Paarl -->
<circle cx="210" cy="440" r="118" fill="#fef2e3"/><circle cx="210" cy="440" r="92" fill="none" stroke="#e5dccd" stroke-width="3"/>
<circle cx="180" cy="430" r="46" fill="#a9764e"/><circle cx="246" cy="446" r="46" fill="#a3704a"/>
<g fill="#e8d6bd"><circle cx="168" cy="416" r="3"/><circle cx="190" cy="440" r="2.5"/><circle cx="240" cy="432" r="3"/><circle cx="258" cy="458" r="2.5"/><circle cx="176" cy="448" r="2"/></g>
<!-- Kaffee -->
<circle cx="444" cy="236" r="62" fill="#fef2e3"/><circle cx="444" cy="236" r="40" fill="#f3eee7"/><circle cx="444" cy="236" r="32" fill="#4a2f22"/>
<rect x="500" y="228" width="34" height="14" rx="7" fill="#fef2e3"/>
<!-- Kastanien -->
<g><path d="M412 520 c24-18 54-8 58 18 c4 24-28 40-52 30 c-18-8-22-34-6-48z" fill="#6b3e2a"/><path d="M416 556 c14 10 34 10 48 0 c-4 12-18 18-30 16 c-8-2-14-8-18-16z" fill="#c9a27e"/></g>
<g><path d="M488 470 c22-14 50-4 52 20 c2 22-26 34-48 24 c-16-8-18-30-4-44z" fill="#704430"/><path d="M490 504 c12 8 30 8 42 0 c-4 10-16 16-26 14 c-8-2-12-6-16-14z" fill="#c9a27e"/></g>
<g><path d="M470 600 c24-16 52-6 56 18 c2 22-26 36-50 26 c-16-8-20-30-6-44z" fill="#673b28"/><path d="M472 634 c12 8 32 8 44 0 c-4 10-16 16-28 14 c-8-2-12-6-16-14z" fill="#c9a27e"/></g>
<!-- Äpfel -->
<circle cx="132" cy="196" r="44" fill="#a8473a"/><circle cx="118" cy="182" r="10" fill="#bf6a58"/><rect x="130" y="146" width="4" height="14" fill="#3e3a37"/>
<circle cx="230" cy="150" r="38" fill="#b9543f"/><rect x="229" y="108" width="4" height="12" fill="#3e3a37"/>
<!-- Glas Most -->
<circle cx="330" cy="660" r="44" fill="#e7cf98" opacity=".9"/><circle cx="330" cy="660" r="44" fill="none" stroke="#fef2e3" stroke-width="5"/>
<rect x="80" y="610" width="10" height="150" rx="5" fill="#d5d1c8" transform="rotate(14 85 685)"/>
</svg>
</div></div>
<figcaption class="label rein" style="--i:4"><span>Tisch 3 · Stube</span><span>09:15 Uhr</span></figcaption>
</figure>
</div>
</section>
<!-- 05 Ihr Aufenthalt -->
<section class="folie" id="f5" data-ton="nacht" aria-label="05 Ihr Aufenthalt">
<div class="innen">
<div class="text">
<p class="eyebrow label rein" style="--i:0">05 · Ihr Aufenthalt</p>
<h2 class="rein" style="--i:1">Bis bald <em>in Obermais.</em></h2>
<div class="angebot rein" style="--i:2">
<div><span>Kastanienstube, 2 Erwachsene</span><small class="label">20.–22. Nov.</small></div>
<div><span>2 Nächte mit Frühstück</span><span class="summe">480 €</span></div>
</div>
<ul class="vorteile rein" style="--i:3">
<li>Direkt gebucht: Abreise bis 13 Uhr</li>
<li>Kostenlos stornieren bis 3 Tage vorher</li>
<li>Abholung am Bahnhof Meran, 12 Minuten</li>
</ul>
<button class="knopf label rein" style="--i:4" type="button">Jetzt verbindlich anfragen</button>
</div>
<figure class="bogen">
<div class="rahmen"><div class="szene" aria-hidden="true">
<svg viewBox="0 0 600 800" preserveAspectRatio="xMidYMid slice">
<defs><pattern id="w-reben5" width="22" height="22" patternUnits="userSpaceOnUse" patternTransform="rotate(-24)"><rect width="22" height="22" fill="#2f392b"/><rect y="9" width="22" height="4" fill="#283124"/></pattern></defs>
<rect width="600" height="800" fill="#2e3943"/>
<g fill="#f3eee7"><circle cx="80" cy="120" r="2"/><circle cx="170" cy="210" r="1.5"/><circle cx="260" cy="90" r="2"/><circle cx="520" cy="160" r="1.5"/><circle cx="460" cy="70" r="2"/><circle cx="120" cy="300" r="1.5"/><circle cx="350" cy="190" r="1.5"/></g>
<circle cx="410" cy="250" r="44" fill="#f3eee7"/><circle cx="430" cy="236" r="44" fill="#2e3943"/>
<path d="M0 430 L70 372 L128 398 L214 300 L262 338 L332 262 L404 336 L470 304 L540 352 L600 330 V800 H0Z" fill="#46525c"/>
<path d="M214 300 L242 324 L228 322 L214 334 L200 318Z M332 262 L364 296 L346 292 L332 304 L316 290Z" fill="#8c959b"/>
<path d="M0 470 C90 430 160 452 250 420 C340 390 430 420 600 396 V800 H0Z" fill="#3a4637"/>
<path d="M0 560 C120 520 240 548 340 506 C430 470 520 486 600 470 V800 H0Z" fill="url(#w-reben5)"/>
<g transform="translate(300 418)">
<rect x="0" y="40" width="150" height="92" fill="#cfc4b4"/>
<rect x="104" y="6" width="46" height="126" fill="#c7bba9"/>
<path d="M-8 44 L75 12 L158 44Z" fill="#5b4033"/>
<path d="M98 10 L127 -16 L156 10Z" fill="#523a2e"/>
<g fill="#f2c98a"><rect x="18" y="62" width="14" height="22"/><rect x="78" y="62" width="14" height="22"/><rect x="48" y="98" width="14" height="22"/><rect x="120" y="26" width="14" height="20"/></g>
<g fill="#4f5a42"><rect x="48" y="62" width="14" height="22"/><rect x="18" y="98" width="14" height="22"/><rect x="78" y="98" width="14" height="22"/></g>
<path d="M112 132 V100 a15 15 0 0 1 30 0 V132Z" fill="#f2c98a"/>
</g>
<path d="M262 560 C248 520 250 456 262 404 C274 456 276 520 262 560Z" fill="#1f261c"/>
<path d="M474 548 C462 512 464 460 474 420 C484 460 486 512 474 548Z" fill="#1f261c"/>
<g stroke="#1c1a19" stroke-linecap="round" fill="none">
<path d="M150 640 C146 580 152 520 168 470" stroke-width="7"/>
<path d="M168 470 C140 452 112 456 92 474 M168 470 C150 440 124 430 100 434 M168 470 C176 438 200 424 226 428 M168 470 C196 456 222 462 238 482 M168 470 C166 446 172 428 186 414" stroke-width="5"/>
</g>
<path d="M0 640 C140 610 300 650 600 600 V800 H0Z" fill="#5b5048"/>
<g fill="#f2c98a"><circle cx="120" cy="652" r="4"/><circle cx="300" cy="640" r="4"/><circle cx="480" cy="622" r="4"/></g>
</svg>
</div></div>
<figcaption class="label rein" style="--i:5"><span>Am Waalhang 7 · 39012 Meran</span><span>+39 0473 21 48 07</span></figcaption>
</figure>
</div>
</section>
</main>
<div class="pult">
<div class="pult__knoepfe" role="group" aria-label="Variante">
<button type="button" data-modus="nativ" aria-pressed="true">CSS-Snap</button>
<button type="button" data-modus="st" aria-pressed="false">ScrollTrigger</button>
<button type="button" data-modus="observer" aria-pressed="false">Observer</button>
</div>
<p class="pult__werte" data-werte>scroll-snap-type: y mandatory · scroll-state(snapped)</p>
</div>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.15.0/dist/gsap.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.15.0/dist/ScrollTrigger.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.15.0/dist/Observer.min.js"></script>
<script>
(() => {
gsap.registerPlugin(ScrollTrigger, Observer);
const html = document.documentElement;
const reduziert = () => html.classList.contains('reduziert');
const folien = [...document.querySelectorAll('.folie')];
const n = folien.length;
const nr = document.querySelector('[data-nr]');
const punkte = [...document.querySelectorAll('.punkte button')];
const werteFeld = document.querySelector('[data-werte]');
const KURVE = 'power3.out'; // = cubic-bezier(0.25, 1, 0.5, 1)
const SNAP = { min: 0.2, max: 0.6 }; // ScrollTrigger-Snap 200–600 ms
const WECHSEL = 1.0, GEGEN = 50, ALT = 30; // Observer: 1 s, Innenbild 50 % gegenläufig, alte Folie 30 %
const kannSnapState = CSS.supports('container-type: scroll-state');
const WERTE = {
nativ: 'scroll-snap-type: y mandatory · ' + (kannSnapState ? 'Einblenden per scroll-state(snapped)' : 'Einblenden per IntersectionObserver') + ' · 600 ms / 80 ms',
st: 'ScrollTrigger snap · 200–600 ms · power3.out · delay 0,1 s',
observer: 'Observer · Wechsel 1 s power3.out · Innenbild −50 % gegenläufig'
};
let modus = 'nativ', aktuell = 0, laeuft = false, obs = null, st = null, schleife = null, fahrt = null;
function setzeAktiv(i) {
aktuell = i;
folien.forEach((f, k) => f.classList.toggle('aktiv', k === i));
punkte.forEach((p, k) => p.setAttribute('aria-current', String(k === i)));
nr.textContent = String(i + 1).padStart(2, '0');
const ton = getComputedStyle(folien[i]).getPropertyValue('--text').trim();
html.style.setProperty('--ton-text', ton || '');
}
// Aktive Folie im Dokumentfluss: Mittellinie des Viewports
const io = new IntersectionObserver(es => {
if (modus === 'observer') return;
es.forEach(e => { if (e.isIntersecting) setzeAktiv(folien.indexOf(e.target)); });
}, { rootMargin: '-50% 0px -50% 0px' });
folien.forEach(f => io.observe(f));
// ---------- Observer: Scroll-Impuls abfangen, zeitbasiert wechseln ----------
function gehe(ziel, richtung) {
if (laeuft || ziel === aktuell) return;
ziel = (ziel + n) % n;
const dir = richtung || (ziel > aktuell ? 1 : -1);
const alt = folien[aktuell], neu = folien[ziel];
laeuft = true;
setzeAktiv(ziel);
if (reduziert()) {
gsap.set(alt, { autoAlpha: 0 });
gsap.fromTo(neu, { autoAlpha: 0 }, { autoAlpha: 1, duration: 0.2, ease: 'none', onComplete: () => { gsap.set(alt, { clearProps: 'all' }); laeuft = false; } });
return;
}
gsap.set(alt, { zIndex: 1, visibility: 'visible' });
gsap.set(neu, { zIndex: 2, yPercent: 100 * dir });
gsap.set(neu.firstElementChild, { yPercent: -GEGEN * dir });
gsap.timeline({ defaults: { duration: WECHSEL, ease: KURVE }, onComplete: () => {
gsap.set([alt, alt.firstElementChild], { clearProps: 'all' });
laeuft = false;
} })
.to(neu, { yPercent: 0 }, 0)
.to(neu.firstElementChild, { yPercent: 0 }, 0)
.to(alt, { yPercent: -ALT * dir }, 0);
}
function tasten(e) {
if (modus !== 'observer') return;
const weiter = ['ArrowDown', 'PageDown', ' '], zurueck = ['ArrowUp', 'PageUp'];
if (weiter.includes(e.key)) { e.preventDefault(); if (aktuell < n - 1) gehe(aktuell + 1, 1); }
else if (zurueck.includes(e.key)) { e.preventDefault(); if (aktuell > 0) gehe(aktuell - 1, -1); }
else if (e.key === 'Home') { e.preventDefault(); gehe(0, -1); }
else if (e.key === 'End') { e.preventDefault(); gehe(n - 1, 1); }
}
addEventListener('keydown', tasten);
// ---------- Modus wechseln ----------
function punkteST() {
const max = ScrollTrigger.maxScroll(window) || 1;
return folien.map(f => Math.min(1, f.offsetTop / max));
}
function aufraeumen() {
if (obs) { obs.kill(); obs = null; }
if (st) { st.kill(); st = null; }
if (fahrt) { fahrt.kill(); fahrt = null; }
gsap.killTweensOf(folien); gsap.killTweensOf(folien.map(f => f.firstElementChild));
gsap.set(folien, { clearProps: 'all' }); gsap.set(folien.map(f => f.firstElementChild), { clearProps: 'all' });
html.classList.remove('nativ', 'st', 'observer', 'selbstlauf');
laeuft = false;
}
function erstelleST() {
if (st || modus !== 'st' || reduziert()) return;
st = ScrollTrigger.create({
trigger: '.spur', start: 'top top', end: 'bottom bottom',
snap: { snapTo: v => gsap.utils.snap(punkteST(), v), duration: SNAP, delay: 0.1, ease: KURVE }
});
}
function setzeModus(m) {
const i = aktuell;
aufraeumen();
modus = reduziert() ? 'nativ' : m;
html.classList.add(modus);
html.classList.toggle('zeigt-aktiv', !(modus === 'nativ' && kannSnapState) || reduziert());
document.querySelectorAll('[data-modus]').forEach(b => b.setAttribute('aria-pressed', String(b.dataset.modus === modus)));
werteFeld.textContent = reduziert() ? 'Reduzierte Bewegung: kein Einrasten, nur Einblenden' : WERTE[modus];
if (modus === 'observer') {
scrollTo({ top: 0, behavior: 'instant' });
obs = Observer.create({
target: window, type: 'wheel,touch', wheelSpeed: -1, tolerance: 10, preventDefault: true,
onDown: () => { stopp(); if (aktuell > 0) gehe(aktuell - 1, -1); },
onUp: () => { stopp(); if (aktuell < n - 1) gehe(aktuell + 1, 1); }
});
} else {
scrollTo({ top: folien[i].offsetTop, behavior: 'instant' });
erstelleST();
}
setzeAktiv(i);
}
function springe(i) {
stopp();
if (modus === 'observer') return gehe(i);
scrollTo({ top: folien[i].offsetTop, behavior: reduziert() ? 'instant' : 'smooth' });
}
document.querySelectorAll('[data-zu]').forEach(b => b.addEventListener('click', () => springe(+b.dataset.zu)));
document.querySelectorAll('[data-modus]').forEach(b => b.addEventListener('click', () => { stopp(); setzeModus(b.dataset.modus); }));
// ---------- Vorführung: Scroll-Impuls, dann Einrasten ----------
function naechste() {
const ziel = (aktuell + 1) % n;
if (modus === 'observer') { gehe(ziel, ziel === 0 ? -1 : 1); return; }
if (st) { st.kill(); st = null; }
html.classList.add('selbstlauf');
const y = { v: scrollY }, zielY = folien[ziel].offsetTop;
if (reduziert()) { scrollTo(0, zielY); return; }
const impuls = ziel === 0 ? 0 : innerHeight * 0.26;
fahrt = gsap.timeline({ onUpdate: () => scrollTo(0, y.v), onComplete: () => { html.classList.remove('selbstlauf'); } })
.to(y, { v: scrollY + impuls, duration: impuls ? 0.38 : 0, ease: 'power1.in' })
.to(y, { v: zielY, duration: ziel === 0 ? 0.9 : 0.6, ease: KURVE }, '+=0.12');
}
function stopp() {
clearTimeout(schleife); schleife = null;
if (fahrt) { fahrt.kill(); fahrt = null; html.classList.remove('selbstlauf'); }
erstelleST();
}
function neustart() {
stopp();
if (modus === 'observer') { setzeModus('observer'); folien.forEach((f, k) => gsap.set(f, { clearProps: 'all' })); setzeAktiv(0); }
else { scrollTo({ top: 0, behavior: 'instant' }); setzeAktiv(0); }
}
function vorfuehren() {
neustart();
schleife = setTimeout(function takt() { naechste(); schleife = setTimeout(takt, 2600); }, 1200);
}
function pause() { stopp(); }
function weiter() { if (html.classList.contains('vorschau') && !schleife) vorfuehren(); }
['wheel', 'touchstart'].forEach(t => addEventListener(t, () => { if (schleife && !html.classList.contains('vorschau')) stopp(); }, { passive: true }));
document.addEventListener('DOMContentLoaded', () => {
if (reduziert()) document.querySelectorAll('[data-modus]').forEach(b => { b.disabled = true; b.title = 'Bei reduzierter Bewegung gibt es kein Einrasten'; });
setzeModus('nativ');
});
})();
</script>
</body>
</html>