Zum Inhalt
← Scroll & Section-Animationen

Scroll-Hinweis, der verschwindet

Unter dem Weinberg-Hero eines Weinguts nennt ein kleiner Link das Ziel, ein Punkt wandert dreimal und steht dann, beim ersten Scrollen blendet der Hinweis aus und verlässt die Tab-Reihenfolge.

Hero mit vollem Bildschirm und ruhiger Typografie (weingut, hotel, fotograf, architektur).

Ein dezenter Hinweis unten im Hero sagt, dass es weitergeht, und nennt, wohin: „Weiter zu den Lagen“. Er ist ein echter Link zum nächsten Abschnitt. Eine kurze Bewegung läuft höchstens dreimal und steht dann still. Beim ersten Scrollen blendet der Hinweis aus, gesteuert über animation-timeline: scroll() mit Rückfall per JS, und ist danach nicht mehr fokussierbar. Varianten: Linie mit wanderndem Punkt, angeschnittener nächster Abschnitt nach NN/g, Hinweis passend zum Gerät (Mausrad oder Wischen).

Ein bildschirmfüllender Hero wirkt wie eine fertige Seite (Illusion of Completeness). Ein Hinweis mit Ziel beantwortet „gibt es mehr?“ und „was kommt?“ in einem. Weil er beim ersten Scrollen geht, stört er niemanden, der es schon weiß. Ein Ziel im Text ist hilfreicher als ein Pfeil allein.

Wann einsetzen

Bildstarke Startseiten mit vollem Hero: Weingut, Hotel, Eventlocation, Fotografie, Kreativstudio, Freizeit. Auch auf langen Projektseiten nach einem Titelbild.

Wann nicht

Wenn der Hero nicht den ganzen Bildschirm füllt und der nächste Abschnitt schon zu sehen ist: dann ist der angeschnittene Inhalt der Hinweis. Nicht auf Seiten, deren erster Bildschirm schon die Handlung enthält (Termin, Telefon). Kein dauernd hüpfender Pfeil.

Varianten

  • Linie: Label mit Ziel und senkrechter Linie, ein Punkt wandert dreimal
  • Angeschnitten: der nächste Abschnitt ragt 7.5rem in den ersten Bildschirm, der Hinweis nennt ihn
  • Gerät: Mausrad am Desktop, Wisch-Geste auf Touch

Umsetzung

Der Hinweis ist ein <a href="#lagen"> mit sichtbarem Text. Die kurze Bewegung läuft mit animation-iteration-count: 3 und steht dann. Ausblenden: @supports (animation-timeline: scroll()) { .hinweis { animation: weg linear both; animation-timeline: scroll(root); animation-range: 0 25vh } } mit Keyframes für opacity, translate und am Ende visibility: hidden (fällt aus der Tab-Reihenfolge). Ohne Unterstützung setzt ein einmaliger scroll-Listener (passiv) eine Klasse. Angeschnitten: Hero mit min-height: calc(100svh - 7rem), der nächste Abschnitt ragt hinein. Gerät: @media (hover: hover) and (pointer: fine) zeigt das Mausrad, (pointer: coarse) die Wisch-Geste.

Robust umsetzen

Telefon. Auf Touch heißt der Hinweis „Wischen“ bzw. zeigt eine Wisch-Geste; er sitzt über dem unteren Rand mit Abstand zu Browserleisten (svh, safe-area). Trefferfläche mindestens 44 px.

Ohne Bewegung. Keine Schleife: Punkt und Geste stehen still. Das Ausblenden beim Scrollen bleibt, weil es dem Scrollen folgt und keine eigene Bewegung ist.

Barrierefrei. Echter Link mit Ziel im Text, kein aria-hidden-Pfeil ohne Funktion. Bewegung unter 5 s (dreimal 1,6 s), danach still (WCAG 2.2.2). Nach dem Ausblenden visibility: hidden, damit der Fokus nicht ins Unsichtbare springt. Text mindestens 4,5 : 1 auf dem Foto, deshalb mit Fläche oder Verlauf dahinter.

Code

<!DOCTYPE html>
<!--
  Scroll-Hinweis, der verschwindet
  Fundus · Scroll & Section-Animationen · https://cecodesign.de/lab/fundus/scroll/scroll-hinweis/
  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-Hinweis, der verschwindet · 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: '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-400-normal.woff2') format('woff2'); font-weight: 400; 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; }

/* Welt: Rezept weingut-hofladen-herkunft · Weingut Hellmich, Klingenberg am Main (erfunden) */
:root {
  --kalk: #F1ECE2; --text: #22211F; --loess: #E6DCCB; --schiefer: #2F3236; --sandstein: #8A3A2B; --leise: #5A564F; --linie: #D8CFC0;
  --kalk-leise: rgba(241, 236, 226, .8);
  --kurve: cubic-bezier(0.37, 0, 0.63, 1);   /* token-luxus-weich */
  --t-puls: 1600ms; --t-auftritt: 600ms; --t-ui: 150ms;
  --serif: 'EB Garamond', Georgia, serif; --mono: 'DM Mono', ui-monospace, monospace;
}
* { box-sizing: border-box; margin: 0; }
html { background: var(--kalk); color: var(--text); font-family: var(--serif); -webkit-text-size-adjust: 100%; scroll-behavior: smooth; }
:root:has(:focus-visible) { scroll-behavior: auto; }
.reduziert { scroll-behavior: auto; }
:focus-visible { outline: 2px solid var(--text); outline-offset: 4px; }
.dunkel :focus-visible, .hero :focus-visible { outline-color: currentColor; }
a { color: inherit; }
.mono { font: 500 .75rem/1.3 var(--mono); letter-spacing: .1em; text-transform: uppercase; }
.nur-sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }

/* Kopf */
.kopf { position: absolute; z-index: 5; inset: 0 0 auto 0; display: flex; justify-content: space-between; align-items: center; gap: 16px; padding: 22px clamp(20px, 4vw, 52px); }
.marke { font: 500 1.5rem/1 var(--serif); letter-spacing: -.01em; text-decoration: none; }
.marke i { font-style: italic; }
.kopf nav { display: flex; gap: 26px; }
.kopf nav a { text-decoration: none; }

/* Hero: Text auf Kalk, Lage als Foto */
.hero { position: relative; display: grid; grid-template-columns: minmax(0, 5fr) minmax(0, 7fr); min-height: 100svh; }
.hero-text { position: relative; display: flex; flex-direction: column; justify-content: center; padding: 110px clamp(20px, 4vw, 52px) 150px; }
.hero h1 { font: 400 clamp(2.75rem, 1.6rem + 3.6vw, 5.25rem)/1 var(--serif); letter-spacing: -.015em; margin: 18px 0 22px; text-wrap: balance; }
.hero h1 em { font-style: italic; }
.hero-text > p:not(.mono) { font-size: 1.25rem; line-height: 1.55; max-width: 32ch; }
.handlungen { display: flex; flex-wrap: wrap; gap: 12px 18px; margin-top: 30px; align-items: center; }
.kaufen { display: inline-flex; align-items: center; height: 48px; padding: 0 22px; background: var(--sandstein); color: #fff; text-decoration: none; font: 500 .875rem/1 var(--mono); letter-spacing: .08em; text-transform: uppercase; transition: background-color var(--t-ui) linear; }
.kaufen:hover { background: #742F23; }
.ecken { position: relative; display: inline-flex; align-items: center; height: 48px; padding: 0 16px; text-decoration: none; font: 500 .875rem/1 var(--mono); letter-spacing: .08em; text-transform: uppercase; }
.ecken::before, .ecken::after { content: ''; position: absolute; width: 9px; height: 9px; border: 0 solid currentColor; }
.ecken::before { left: 0; top: 0; border-width: 1px 0 0 1px; } .ecken::after { right: 0; bottom: 0; border-width: 0 1px 1px 0; }
.foto { position: relative; overflow: hidden; background: var(--schiefer); }
.foto img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; object-position: 50% 60%; }
.foto figcaption { position: absolute; right: 16px; bottom: 14px; color: var(--kalk); font: 400 .75rem/1.3 var(--mono); letter-spacing: .06em; text-shadow: 0 1px 8px rgba(0, 0, 0, .5); }

/* ---------- Scroll-Hinweis: außen blendet er mit dem Scrollen aus, innen läuft der Puls höchstens dreimal ---------- */
.hinweis { position: absolute; z-index: 3; text-decoration: none; display: inline-flex; align-items: center; gap: 14px; min-height: 44px; }
.hinweis .wort { font: 500 .75rem/1.3 var(--mono); letter-spacing: .1em; text-transform: uppercase; }
.hinweis .wort b { font-weight: 500; display: block; color: var(--leise); }
.hinweis:hover .wort span { text-decoration: underline; text-underline-offset: 4px; }
.auftritt { display: contents; }
.hinweis > * { animation: auftritt var(--t-auftritt) var(--kurve) 600ms both; }
@keyframes auftritt { from { opacity: 0; transform: translateY(6px); } }
@supports (animation-timeline: scroll()) {
  .hinweis { animation: weg linear both; animation-timeline: scroll(root block); animation-range: 0 25vh; }
}
@keyframes weg { 0% { opacity: 1; translate: 0 0; } 99% { visibility: visible; } 100% { opacity: 0; translate: 0 -12px; visibility: hidden; } }
.hinweis.weg { opacity: 0; translate: 0 -12px; visibility: hidden; transition: opacity 300ms var(--kurve), translate 300ms var(--kurve), visibility 0s 300ms; }

/* Linie: senkrechte Linie, ein Punkt wandert 40 px, dreimal */
.v-linie { left: clamp(20px, 4vw, 52px); bottom: 34px; }
.v-linie .bahn { position: relative; width: 1px; height: 56px; background: var(--linie); background: rgba(34, 33, 31, .3); overflow: visible; }
.v-linie .bahn::after { content: ''; position: absolute; left: -2.5px; top: 0; width: 6px; height: 6px; border-radius: 50%; background: var(--sandstein); }
.laeuft .v-linie .bahn::after { animation: punkt var(--t-puls) var(--kurve) 1200ms 3 both; }
@keyframes punkt { 0% { transform: translateY(0); opacity: 0; } 15% { opacity: 1; } 70% { transform: translateY(40px); opacity: 1; } 100% { transform: translateY(50px); opacity: 0; } }

/* Angeschnitten: nächster Abschnitt ragt 7.5rem in den ersten Bildschirm */
.v-angeschnitten { display: none; }
[data-variante=angeschnitten] .hero { min-height: calc(100svh - 7.5rem); }
[data-variante=angeschnitten] .v-angeschnitten { display: inline-flex; left: clamp(20px, 4vw, 52px); top: calc(100svh - 7.5rem - 76px); flex-direction: row-reverse; }
[data-variante=angeschnitten] .lagen-kopf { padding-top: 28px; }
#lagen:focus { outline: none; }
[data-variante=angeschnitten] .v-linie, [data-variante=angeschnitten] .v-geraet { display: none; }
.v-angeschnitten .pfeil { width: 36px; height: 36px; display: grid; place-items: center; border: 1px solid rgba(34, 33, 31, .4); border-radius: 50%; }
.laeuft .v-angeschnitten .pfeil svg { animation: nicken var(--t-puls) var(--kurve) 1200ms 3 both; }
@keyframes nicken { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(5px); } }

/* Gerät: Mausrad am Desktop, Wisch-Geste auf Touch */
.v-geraet { display: none; }
[data-variante=geraet] .v-linie { display: none; }
[data-variante=geraet] .v-geraet { display: inline-flex; right: 28px; bottom: 28px; background: var(--kalk); padding: 10px 16px 10px 12px; }
.v-geraet .maus, .v-geraet .finger { flex: none; }
.v-geraet .finger { display: none; }
.v-geraet .rad { transform-box: fill-box; }
.laeuft .v-geraet .rad { animation: rad var(--t-puls) var(--kurve) 1200ms 3 both; }
@keyframes rad { 0% { transform: translateY(0); opacity: 1; } 60% { transform: translateY(7px); opacity: 0; } 100% { transform: translateY(0); opacity: 0; } }
.laeuft .v-geraet .wisch { animation: wisch var(--t-puls) var(--kurve) 1200ms 3 both; }
@keyframes wisch { 0% { transform: translateY(14px); opacity: 0; } 25% { opacity: 1; } 80% { transform: translateY(-10px); opacity: 1; } 100% { transform: translateY(-14px); opacity: 0; } }
.v-geraet .touch-wort { display: none; }
@media (hover: none), (pointer: coarse) {
  .v-geraet .maus, .v-geraet .maus-wort { display: none; }
  .v-geraet .finger { display: block; } .v-geraet .touch-wort { display: inline; }
}

/* Folgeabschnitte */
.lagen { background: var(--schiefer); color: var(--kalk); padding: 0 clamp(20px, 4vw, 52px) 96px; }
.lagen-kopf { display: grid; grid-template-columns: minmax(0, 5fr) minmax(0, 7fr); gap: 24px; align-items: end; padding: 36px 0 44px; border-bottom: 1px solid rgba(241, 236, 226, .2); }
.lagen-kopf h2 { font: 400 clamp(2rem, 1.5rem + 2vw, 3.25rem)/1.05 var(--serif); }
.lagen-kopf h2 em { font-style: italic; }
.lagen-kopf p { font-size: 1.25rem; line-height: 1.55; color: var(--kalk-leise); max-width: 46ch; }
.lagen .mono { color: var(--kalk-leise); }
.lagen-liste { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 24px; list-style: none; padding: 36px 0 0; }
.lagen-liste img { display: block; width: 100%; height: auto; aspect-ratio: 3 / 2; object-fit: cover; }
.lagen-liste h3 { font: italic 400 1.75rem/1.1 var(--serif); margin: 16px 0 6px; }
.lagen-liste p { font: 400 .875rem/1.55 var(--mono); color: var(--kalk-leise); }
.hofladen { padding: 80px clamp(20px, 4vw, 52px) 120px; display: grid; grid-template-columns: minmax(0, 5fr) minmax(0, 7fr); gap: 24px; }
.hofladen h2 { font: 400 clamp(2rem, 1.5rem + 2vw, 3.25rem)/1.05 var(--serif); }
.hofladen p { font-size: 1.25rem; line-height: 1.55; max-width: 50ch; }

/* Pult */
.pult { position: fixed; right: 16px; top: 76px; z-index: 50; display: flex; flex-wrap: wrap; align-items: center; gap: 4px; max-width: calc(100vw - 32px); padding: 8px 10px; background: rgba(34, 33, 31, .92); color: #f3f2ee; font: 500 10.5px/1 var(--mono); letter-spacing: .1em; text-transform: uppercase; }
.pult > span { color: rgba(243, 242, 238, .72); margin-right: 6px; }
.pult button { background: transparent; color: inherit; border: 1px solid rgba(243, 242, 238, .3); height: 28px; padding: 0 9px; font: inherit; letter-spacing: inherit; text-transform: inherit; cursor: pointer; }
.pult button[aria-pressed=true] { background: #f3f2ee; color: #22211F; border-color: #f3f2ee; }
.pult button:focus-visible { outline: 2px solid #f3f2ee; outline-offset: 2px; }
.vorschau body { overflow: hidden; }
.vorschau .kopf nav { display: none; }

@media (max-width: 820px) {
  .hero { grid-template-columns: minmax(0, 1fr); }
  .foto { position: absolute; inset: 0; }
  .foto::after { content: ''; position: absolute; inset: 0; background: linear-gradient(180deg, rgba(34, 33, 31, .55) 0%, rgba(34, 33, 31, 0) 30%, rgba(34, 33, 31, .1) 45%, rgba(34, 33, 31, .88) 100%); }
  .foto figcaption { display: none; }
  .hero-text { z-index: 2; color: var(--kalk); justify-content: flex-end; padding-bottom: 130px; }
  .kopf { color: var(--kalk); }
  .kopf nav { display: none; }
  .hinweis { color: var(--kalk); }
  .hinweis .wort b { color: var(--kalk-leise); }
  .v-linie .bahn { background: rgba(241, 236, 226, .45); }
  .v-linie .bahn::after { background: var(--kalk); }
  .v-angeschnitten .pfeil { border-color: rgba(241, 236, 226, .6); }
  .ecken { color: var(--kalk); }
  .hinweis.v-geraet { color: var(--text); }
  .v-geraet .wort b { color: var(--leise); }
  [data-variante=geraet] .v-geraet { right: auto; left: clamp(20px, 4vw, 52px); bottom: 34px; }
  .lagen-kopf, .hofladen { grid-template-columns: minmax(0, 1fr); }
  .lagen-liste { grid-template-columns: minmax(0, 1fr); }
}
:root.reduziert .hinweis > *, :root.reduziert .laeuft .hinweis * { animation: none !important; }
:root.reduziert .v-linie .bahn::after { animation: none !important; transform: translateY(20px); }
@media (forced-colors: active) {
  button, [role=button] { border: 1px solid ButtonText !important; }
  .kaufen { border: 1px solid ButtonText; }
  .v-linie .bahn, .v-linie .bahn::after { background: CanvasText; }
}
</style>
</head>
<body class="laeuft">
<header class="kopf">
  <a class="marke" href="#start">Weingut <i>Hellmich</i></a>
  <nav aria-label="Hauptmenü" class="mono"><a href="#weine">Weine</a><a href="#lagen">Lagen</a><a href="#hofladen">Hofladen</a><a href="#besuch">Besuchen</a></nav>
</header>
<main id="start">
  <section class="hero" aria-labelledby="h1">
    <div class="hero-text">
      <p class="mono">Klingenberg am Main · seit 1897</p>
      <h1 id="h1">Spätburgunder vom <em>roten Sandstein.</em></h1>
      <p>Drei Lagen über dem Main, Terrassen aus Buntsandstein, Lese von Hand. Probieren im Hof oder bestellen ab sechs Flaschen.</p>
      <div class="handlungen"><a class="kaufen" href="#weine">Weine ansehen</a><a class="ecken" href="#besuch">Hoffest 15. November</a></div>
    </div>
    <figure class="foto">
      <img src="bilder/foto-hero-weinberg-hoch.webp" alt="Steiler Weinberg im Herbst mit einem Weinbergshäuschen aus Sandstein in der Abendsonne" width="800" height="1000">
      <figcaption>Lage Kreuzleite, Oktober</figcaption>
    </figure>

    <a class="hinweis v-linie" href="#lagen"><span class="bahn" aria-hidden="true"></span><span class="wort"><b>Weiter</b><span>Zu den drei Lagen</span></span></a>
    <a class="hinweis v-angeschnitten" href="#lagen"><span class="wort"><b>Weiter</b><span>01 · Die Lagen</span></span><span class="pfeil" aria-hidden="true"><svg width="14" height="14" viewBox="0 0 14 14"><path d="M7 1v11M2.5 7.5L7 12l4.5-4.5" fill="none" stroke="currentColor" stroke-width="1.3"/></svg></span></a>
    <a class="hinweis v-geraet" href="#lagen">
      <svg class="maus" width="22" height="32" viewBox="0 0 22 32" aria-hidden="true"><rect x="1" y="1" width="20" height="30" rx="10" fill="none" stroke="currentColor" stroke-width="1.3"/><rect class="rad" x="10" y="7" width="2" height="6" rx="1" fill="#8A3A2B"/></svg>
      <svg class="finger" width="26" height="34" viewBox="0 0 26 34" aria-hidden="true"><g class="wisch"><circle cx="13" cy="17" r="6" fill="none" stroke="currentColor" stroke-width="1.3"/><circle cx="13" cy="17" r="2.2" fill="#8A3A2B"/></g></svg>
      <span class="wort"><b><span class="maus-wort">Scrollen</span><span class="touch-wort">Wischen</span></b><span>Zu den drei Lagen</span></span>
    </a>
  </section>

  <section class="lagen dunkel" id="lagen" aria-labelledby="lagen-titel" tabindex="-1">
    <div class="lagen-kopf">
      <div><p class="mono">01 · Die Lagen</p><h2 id="lagen-titel">Drei Hänge, <em>ein Boden.</em></h2></div>
      <p>Roter Buntsandstein speichert die Wärme des Tages und gibt sie nachts an die Reben zurück. Jede Lage hat ihren eigenen Hang, ihre eigene Sonne.</p>
    </div>
    <ul class="lagen-liste" role="list">
      <li><img src="bilder/lagen-steckbrief-karte-kreuzleite.webp" alt="" width="900" height="600" loading="lazy"><h3>Kreuzleite</h3><p>Süd · 42 % Hangneigung · 180–240 m · Spätburgunder</p></li>
      <li><img src="bilder/lagen-steckbrief-karte-sonnwendberg.webp" alt="" width="900" height="600" loading="lazy"><h3>Sonnwendberg</h3><p>Südwest · 28 % · 150–210 m · Frühburgunder, Silvaner</p></li>
      <li><img src="bilder/lagen-steckbrief-karte-muehlwinkel.webp" alt="" width="900" height="600" loading="lazy"><h3>Mühlwinkel</h3><p>Süd · 18 % · am Main · Riesling, Weißburgunder</p></li>
    </ul>
  </section>
  <section class="hofladen" id="hofladen" aria-labelledby="hl-titel">
    <h2 id="hl-titel">Hofladen und Probe</h2>
    <p>Freitag 14 bis 18 Uhr, Samstag 10 bis 16 Uhr. Probe für Gruppen ab sechs Personen nach Anmeldung.</p>
  </section>
</main>

<div class="pult" role="group" aria-label="Variante"><span>Variante</span>
  <button type="button" data-v="linie" aria-pressed="true">Linie</button>
  <button type="button" data-v="angeschnitten" aria-pressed="false">Angeschnitten</button>
  <button type="button" data-v="geraet" aria-pressed="false">Gerät</button>
</div>
<script>
(() => {
  const h = document.documentElement, $ = s => document.querySelector(s), $$ = s => [...document.querySelectorAll(s)];
  const reduziert = () => h.classList.contains('reduziert');
  const VARIANTEN = ['linie', 'angeschnitten', 'geraet'];
  const q = new URLSearchParams(location.search);
  let variante = VARIANTEN.includes(q.get('v')) ? q.get('v') : 'linie';

  /* Rückfall ohne scroll-getriebene Animationen: beim ersten Scrollen ausblenden, oben wieder zeigen */
  const nativ = CSS.supports('animation-timeline: scroll()');
  if (!nativ) {
    const pruefe = () => { const weg = scrollY > 40; $$('.hinweis').forEach(a => a.classList.toggle('weg', weg)); };
    addEventListener('scroll', pruefe, { passive: true }); pruefe();
  }
  /* Klick: zum Abschnitt, Fokus dorthin (Tastatur landet im Ziel, nicht wieder oben) */
  $$('.hinweis').forEach(a => a.addEventListener('click', e => {
    e.preventDefault();
    const ziel = $('#lagen');
    ziel.scrollIntoView({ behavior: reduziert() ? 'auto' : 'smooth' });
    ziel.focus({ preventScroll: true });
  }));

  function puls() { document.body.classList.remove('laeuft'); void document.body.offsetWidth; document.body.classList.add('laeuft'); }
  function setzeVariante(v, schreiben) {
    variante = v; document.body.dataset.variante = v;
    $$('[data-v]').forEach(b => b.setAttribute('aria-pressed', b.dataset.v === v ? 'true' : 'false'));
    if (schreiben) { const p = new URLSearchParams(location.search); p.set('v', v); history.replaceState(null, '', location.pathname + '?' + p + location.hash); }
  }
  $$('[data-v]').forEach(b => b.addEventListener('click', () => { pause(); setzeVariante(b.dataset.v, true); neustart(); }));

  function neustart() { scrollTo({ top: 0, behavior: 'instant' }); puls(); }

  /* Vorführung: Puls läuft, dann ein gedachtes Scrollen um 30 % der Höhe (der Hinweis geht), zurück nach oben */
  let lauf = 0, raf = 0;
  const warte = (ms, id) => new Promise((ok, nein) => setTimeout(() => (id === lauf ? ok() : nein('stopp')), ms));
  function rolle(nach, dauer, id) {
    return new Promise((ok, nein) => {
      const von = scrollY, t0 = performance.now(), k = t => t < .5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
      cancelAnimationFrame(raf);
      const schritt = jetzt => {
        if (id !== lauf) return nein('stopp');
        const t = Math.min(1, (jetzt - t0) / (reduziert() ? 1 : dauer));
        scrollTo({ top: von + (nach - von) * k(t), behavior: 'instant' });
        t < 1 ? (raf = requestAnimationFrame(schritt)) : ok();
      };
      raf = requestAnimationFrame(schritt);
    });
  }
  async function runde(id) {
    neustart();
    await warte(reduziert() ? 1800 : 1200 + 1600 * 2 + 400, id);
    await rolle(innerHeight * .32, 1400, id);
    await warte(1600, id);
    await rolle(0, 1000, id);
    await warte(400, id);
    runde(id).catch(() => {});
  }
  function vorfuehren() { const id = ++lauf; h.classList.add('vorfuehrung'); runde(id).catch(() => {}); }
  function pause() { lauf++; cancelAnimationFrame(raf); h.classList.remove('vorfuehrung'); }
  function weiter() { vorfuehren(); }

  setzeVariante(variante, false);

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