Zum Inhalt
← Preloader & Intro

Intro nur beim ersten Besuch (Session-Gedächtnis)

Das volle Intro läuft nur beim ersten Besuch im Tab. Neuladen und Navigation zeigen bloß einen kurzen Reveal mit 25 ms Versatz.

Beim ersten Aufruf der Website in einem Tab läuft das volle Intro. Bei jeder weiteren Navigation, beim Neuladen und beim direkten Einstieg auf eine Unterseite läuft nur ein kurzer Seiten-Reveal oder gar nichts. Ein Klick oder eine Taste überspringt das Intro jederzeit.

Das Intro bleibt ein Ereignis statt eines Hindernisses. Die Marke bekommt ihren Auftritt einmal, am Anfang. Wer weiterklickt oder wiederkommt, ist sofort beim Inhalt.

Wann einsetzen

Bei jeder Seite mit einem Intro über etwa 1 Sekunde. Besonders dort, wo Besucher viel zwischen Seiten wechseln, etwa zwischen Preisen, Terminen und Projekten.

Wann nicht

Bei sehr kurzen Intros unter 800 ms, die niemanden aufhalten. Und wenn Besucher sofort Titel, Termin oder Preis brauchen: Dann besser gar kein Intro.

Varianten

  • sessionStorage-Flag: volles Intro einmal pro Tab, danach nur ein kurzer Reveal (Locomotive, Stewart & Partners).
  • Zusätzlich localStorage: Wiederkehrer bekommen in einem neuen Tab ein schnelleres Intro (OFF+BRAND).
  • Barba oder Taxi: eigener once-Hook bzw. First-Load-Transition, getrennt von den Navigations-Übergängen (Osmo).
  • Nativ mit Cross-document-View-Transitions: pagereveal ohne e.viewTransition ist ein Erstaufruf, mit e.viewTransition eine Navigation.
  • Überspringen per Klick, Esc oder Scrollen.

Umsetzung

Im <head>, parser-blockierend und vor dem ersten Paint: try { if (!sessionStorage.getItem('intro')) { document.documentElement.classList.add('has-intro'); sessionStorage.setItem('intro', '1'); } } catch (e) {}. Das Intro-CSS greift nur unter .has-intro. Fallstrick: Wird die Klasse erst später gesetzt, blitzt der Inhalt kurz auf, bevor das Intro ihn verdeckt. try/catch fängt private Modi ab. Mit MPA-View-Transitions (@view-transition { navigation: auto; }) zusätzlich im pagereveal-Handler: if (e.viewTransition) document.documentElement.classList.remove('has-intro'). Bei Barba oder Taxi gehört das Intro in den once-Hook.

Robust umsetzen

Telefon. Identisch. Auf Touch den Überspringen-Knopf ohne Tastenhinweis zeigen. Wischen über dem Intro überspringt es ebenfalls.

Ohne Bewegung. Intro immer überspringen. Seitenwechsel ohne Reveal.

Barrierefrei. Überspringen per Tastatur (Esc, Enter auf dem Knopf). Bei Intros über 2 Sekunden einen sichtbaren Überspringen-Knopf zeigen. Der Inhalt darunter ist schon gerendert, das Intro verdeckt ihn nur.

Code

<!DOCTYPE html>
<!--
  Intro nur beim ersten Besuch (Session-Gedächtnis)
  Fundus · Preloader & Intro · https://cecodesign.de/lab/fundus/preloader-intro/einmal-intro-session/
  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>Intro nur beim ersten Besuch (Session-Gedächtnis) · Fundus-Demo</title>
<script>document.documentElement.classList.add('js');if(matchMedia('(prefers-reduced-motion: reduce)').matches)document.documentElement.classList.add('reduziert')</script>
<script>
/* Das eigentliche Muster: parser-blockierend im <head>, also vor dem ersten Paint.
   Erster Aufruf in diesem Tab → Klasse mit-intro, Flag setzen. Reload, Navigation,
   Einstieg auf einer Unterseite → Flag ist da, kein Intro. Speicher gesperrt → kein Intro. */
(function () {
  var h = document.documentElement;
  if (h.classList.contains('reduziert') || /[?&]reduziert/.test(location.search)) return;
  try {
    if (!sessionStorage.getItem('kielwasser.intro')) {
      h.classList.add('mit-intro');
      sessionStorage.setItem('kielwasser.intro', '1');
    }
  } catch (e) {}
})();
</script>
<style>
@font-face { font-family: 'Bricolage Grotesque'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/bricolage-grotesque:vf@latest/latin-opsz-normal.woff2') format('woff2-variations'); font-weight: 200 800; font-display: swap; }
@font-face { font-family: 'Atkinson Hyperlegible Next'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/atkinson-hyperlegible-next:vf@latest/latin-wght-normal.woff2') format('woff2-variations'); font-weight: 200 800; font-display: swap; }
@font-face { font-family: '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 {
  --creme: #f8f8ec; --creme-tief: #eef0d8; --navy: #142b50; --nacht: #0e1d38;
  --himmel: #3fbadf; --gelb: #ffd60a; --tinte: #17243c; --leise: #566175;
  --linie: rgba(20, 43, 80, .14);
  --expo: cubic-bezier(0.16, 1, 0.3, 1);          /* Einblendungen, Intro */
  --feder: cubic-bezier(0.34, 1.56, 0.64, 1);     /* nur Buttons, Badges */
  --ui: cubic-bezier(0.4, 0, 0.2, 1);
}
* { box-sizing: border-box; margin: 0; }
html { background: var(--nacht); color: var(--tinte); font-family: 'Atkinson Hyperlegible Next', system-ui, sans-serif; -webkit-font-smoothing: antialiased; }
body { min-height: 100svh; display: grid; grid-template-rows: auto 1fr auto; grid-template-columns: minmax(0, 1fr); }
button, select { font: inherit; color: inherit; }
a { color: inherit; text-decoration: none; }
.mono { font: 500 11px/1.4 'Geist Mono', ui-monospace, monospace; letter-spacing: .14em; text-transform: uppercase; }
.vh { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }

/* ── Browser-Leiste: macht Tab, Reload und Adresse greifbar ── */
.leiste { display: flex; align-items: center; gap: 10px; padding: 8px clamp(12px, 2vw, 20px); color: rgba(248, 248, 236, .62); }
.punkte { display: flex; gap: 6px; margin-right: 6px; }
.punkte i { width: 10px; height: 10px; border-radius: 50%; background: rgba(248, 248, 236, .16); }
.knopf { display: inline-flex; align-items: center; gap: 8px; height: 30px; padding: 0 10px; border: 1px solid rgba(248, 248, 236, .14); border-radius: 8px; background: none; cursor: pointer; transition: background 150ms var(--ui), color 150ms var(--ui), border-color 150ms var(--ui); }
.knopf:hover { color: var(--creme); border-color: rgba(248, 248, 236, .36); }
.knopf svg { width: 14px; height: 14px; }
.adresse { flex: 1; min-width: 0; display: flex; align-items: center; gap: 8px; height: 30px; padding: 0 12px; border-radius: 8px; background: rgba(248, 248, 236, .07); font: 400 13px/1 'Geist Mono', monospace; letter-spacing: .01em; color: rgba(248, 248, 236, .8); overflow: hidden; white-space: nowrap; }
.adresse svg { width: 11px; height: 11px; flex: none; opacity: .6; }
.adresse b { font-weight: 400; color: var(--himmel); }
.knopf.tipp { animation: tipp 700ms var(--expo); }
@keyframes tipp { 0% { box-shadow: 0 0 0 0 rgba(255, 214, 10, .7); background: rgba(255, 214, 10, .2); } 100% { box-shadow: 0 0 0 10px rgba(255, 214, 10, 0); } }

/* ── Die Website der Fahrschule ── */
.fenster { position: relative; overflow: hidden; background: var(--creme); border-radius: 12px; margin: 0 clamp(6px, 1vw, 12px); display: grid; grid-template-rows: auto 1fr; min-height: 0; }
.nav { display: flex; align-items: center; gap: clamp(16px, 2.6vw, 40px); padding: 16px clamp(20px, 4vw, 56px); border-bottom: 1px solid var(--linie); }
.marke { display: flex; align-items: center; gap: 10px; font: 750 19px/1 'Bricolage Grotesque', sans-serif; letter-spacing: -.02em; color: var(--navy); background: none; border: 0; cursor: pointer; padding: 0; }
.marke svg { width: 34px; height: 34px; }
.nav ul { display: flex; gap: clamp(16px, 2.2vw, 32px); list-style: none; padding: 0; margin-left: auto; font-size: 15px; }
.nav ul button { background: none; border: 0; padding: 6px 0; cursor: pointer; position: relative; color: var(--tinte); }
.nav ul button::after { content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 2px; border-radius: 2px; background: var(--himmel); transform: scaleX(0); transform-origin: left; transition: transform 350ms var(--expo); }
.nav ul button:hover::after, .nav ul button[aria-current='page']::after { transform: scaleX(1); }
.tel { font-size: 15px; color: var(--leise); font-variant-numeric: tabular-nums; }
.cta { display: inline-flex; align-items: center; gap: 10px; border: 0; cursor: pointer; background: var(--gelb); color: var(--navy); font: 700 15px/1 'Atkinson Hyperlegible Next', sans-serif; padding: 13px 18px; border-radius: 999px; box-shadow: 0 2px 0 var(--navy); transition: transform 350ms var(--feder), box-shadow 350ms var(--feder); }
.cta:hover { transform: translateY(-2px); box-shadow: 0 4px 0 var(--navy); }
.cta.gross { font-size: 17px; padding: 17px 24px; }
.cta svg { width: 16px; height: 16px; }

.seite { display: grid; padding: clamp(24px, 4.5vh, 56px) clamp(20px, 4vw, 56px) clamp(24px, 4vh, 44px); min-height: 0; }
.seite[hidden] { display: none; }
.start { grid-template-columns: minmax(0, 1.1fr) minmax(0, .9fr); gap: clamp(28px, 4vw, 64px); align-items: center; }
.kicker { display: flex; align-items: center; gap: 10px; font-size: 14px; font-weight: 600; letter-spacing: .06em; text-transform: uppercase; color: var(--leise); }
.kicker i { width: 8px; height: 8px; border-radius: 50%; background: var(--himmel); }
.h1 { font: 700 clamp(2.5rem, .8rem + 4.6vw, 5.4rem)/.98 'Bricolage Grotesque', sans-serif; letter-spacing: -.02em; color: var(--navy); margin: 18px 0 20px; hyphens: auto; text-wrap: balance; }
.marker { background: linear-gradient(transparent 58%, var(--gelb) 58%, var(--gelb) 92%, transparent 92%); padding: 0 .06em; margin: 0 -.06em; }
.lead { font-size: clamp(16px, 1.25vw, 18px); line-height: 1.6; color: var(--leise); max-width: 46ch; }
.wahl { margin-top: clamp(20px, 3vh, 32px); display: grid; gap: 14px; max-width: 560px; }
.wahl legend { font-size: 13px; font-weight: 700; color: var(--navy); margin-bottom: 8px; padding: 0; }
.wahl fieldset { border: 0; padding: 0; }
.klassen { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 8px; }
.klassen label { position: relative; display: grid; gap: 2px; padding: 11px 12px 10px; border: 1.5px solid var(--linie); border-radius: 14px; background: #fff; cursor: pointer; transition: border-color 150ms var(--ui), transform 350ms var(--feder); }
.klassen label:hover { transform: translateY(-2px); border-color: rgba(20, 43, 80, .35); }
.klassen input { position: absolute; opacity: 0; }
.klassen b { font: 700 20px/1 'Bricolage Grotesque', sans-serif; color: var(--navy); }
.klassen span { font-size: 12.5px; color: var(--leise); white-space: nowrap; }
.klassen label:has(input:checked) { border-color: var(--navy); box-shadow: inset 0 0 0 1px var(--navy); background: #fff; }
.klassen label:has(input:checked)::after { content: ''; position: absolute; top: 10px; right: 10px; width: 9px; height: 9px; border-radius: 50%; background: var(--gelb); box-shadow: 0 0 0 2px var(--navy); }
.klassen label:has(input:focus-visible) { outline: 2px solid var(--himmel); outline-offset: 2px; }
.weiter { display: flex; gap: 10px; align-items: stretch; flex-wrap: wrap; }
.ort { flex: 1; min-width: 180px; display: flex; align-items: center; gap: 10px; padding: 0 14px; border: 1.5px solid var(--linie); border-radius: 999px; background: #fff; font-size: 15px; }
.ort select { flex: 1; border: 0; background: none; padding: 13px 0; cursor: pointer; color: var(--navy); font-weight: 600; }
.ort svg { width: 16px; height: 16px; color: var(--himmel); }
.fakten { display: flex; flex-wrap: wrap; gap: 6px 22px; margin-top: clamp(18px, 3vh, 28px); font-size: 14px; color: var(--leise); }
.fakten b { color: var(--navy); font-variant-numeric: tabular-nums; }

.bild { position: relative; align-self: stretch; min-height: 0; display: grid; }
.bild svg.welt { width: 100%; height: 100%; max-height: 560px; border-radius: 28px; display: block; align-self: center; }
.sticker { position: absolute; top: 6%; left: -4%; width: 118px; height: 118px; border-radius: 50%; background: var(--gelb); color: var(--navy); display: grid; place-content: center; text-align: center; transform: rotate(-9deg); box-shadow: 0 3px 0 var(--navy); font-size: 12.5px; line-height: 1.25; font-weight: 600; transition: transform 450ms var(--feder); }
.sticker b { display: block; font: 750 26px/1 'Bricolage Grotesque', sans-serif; font-variant-numeric: tabular-nums; margin: 3px 0 2px; }
.sticker:hover { transform: rotate(-3deg) scale(1.04); }

/* Seite 2: Klassen & Preise */
.preise { grid-template-columns: minmax(0, 1.25fr) minmax(0, .75fr); gap: clamp(28px, 4vw, 64px); align-content: start; }
.h2 { font: 700 clamp(2rem, .9rem + 3.2vw, 3.9rem)/1 'Bricolage Grotesque', sans-serif; letter-spacing: -.02em; color: var(--navy); margin: 14px 0 22px; text-wrap: balance; }
.reiter { display: inline-flex; gap: 4px; padding: 4px; border-radius: 999px; background: var(--creme-tief); margin-bottom: 18px; }
.reiter button { border: 0; background: none; padding: 8px 16px; border-radius: 999px; cursor: pointer; font-weight: 700; color: var(--leise); transition: background 150ms var(--ui), color 150ms var(--ui); }
.reiter button[aria-selected='true'] { background: var(--navy); color: var(--creme); }
.tabelle { width: 100%; border-collapse: collapse; font-size: 16px; }
.tabelle td { padding: 14px 0; border-top: 1px solid var(--linie); }
.tabelle td small { display: block; font-size: 13px; color: var(--leise); margin-top: 2px; }
.tabelle td:last-child { text-align: right; font: 700 20px/1 'Bricolage Grotesque', sans-serif; color: var(--navy); font-variant-numeric: tabular-nums; white-space: nowrap; padding-left: 16px; }
.hinweis { margin-top: 14px; font-size: 14px; color: var(--leise); max-width: 60ch; }
.karte { align-self: end; background: var(--navy); color: var(--creme); border-radius: 24px; padding: 26px; display: grid; gap: 14px; }
.karte .mono { color: var(--himmel); }
.karte p { font-size: 15.5px; line-height: 1.55; color: rgba(248, 248, 236, .82); }
.karte strong { font: 700 44px/1 'Bricolage Grotesque', sans-serif; color: var(--gelb); font-variant-numeric: tabular-nums; }
.karte ul { list-style: none; padding: 0; display: grid; gap: 6px; font-size: 14.5px; }
.karte li { display: flex; justify-content: space-between; border-top: 1px solid rgba(248, 248, 236, .14); padding-top: 6px; font-variant-numeric: tabular-nums; }

/* ── Das Intro: liegt über der Website, nur in diesem Fenster ── */
.intro { position: absolute; inset: 0; z-index: 10; display: none; background: var(--navy); color: var(--creme); grid-template-rows: auto 1fr auto; padding: clamp(18px, 3vh, 28px) clamp(20px, 4vw, 56px); overflow: hidden; }
.mit-intro .intro, .intro[data-an] { display: grid; }
.intro__kopf { display: flex; justify-content: space-between; align-items: center; gap: 16px; position: relative; z-index: 2; }
.intro__kopf .marke { color: var(--creme); }
.skip { display: inline-flex; align-items: center; gap: 10px; background: rgba(248, 248, 236, .08); border: 1px solid rgba(248, 248, 236, .22); color: var(--creme); border-radius: 999px; padding: 9px 10px 9px 16px; cursor: pointer; font-size: 14px; font-weight: 600; transition: background 150ms var(--ui); }
.skip:hover { background: rgba(248, 248, 236, .16); }
.skip kbd { font: 500 10px/1 'Geist Mono', monospace; letter-spacing: .1em; padding: 5px 7px; border-radius: 6px; background: rgba(248, 248, 236, .14); }
.intro__mitte { align-self: center; position: relative; z-index: 2; padding-bottom: 8vh; }
.moin { font: 800 clamp(6rem, 3rem + 13vw, 15rem)/.86 'Bricolage Grotesque', sans-serif; letter-spacing: -.045em; display: flex; overflow: hidden; padding: .04em .06em .08em 0; margin-left: -.04em; }
.moin span { display: block; }
.moin .punkt { color: var(--gelb); }
.gruss { font-size: clamp(17px, 1.5vw, 22px); color: rgba(248, 248, 236, .78); margin-top: 10px; }
.strasse { position: absolute; left: 0; right: 0; bottom: 0; width: 100%; height: 62%; z-index: 1; }
.intro__hinweis { position: absolute; z-index: 2; right: clamp(20px, 4vw, 56px); top: 38%; max-width: 25ch; font-size: 15px; line-height: 1.5; color: rgba(248, 248, 236, .62); padding-left: 16px; border-left: 2px solid var(--gelb); }
.intro__fuss { display: flex; justify-content: space-between; position: relative; z-index: 2; color: rgba(248, 248, 236, .66); }

/* ── Pult: Varianten, Gedächtnis, Protokoll, Werte ── */
.pult { display: grid; grid-template-columns: auto minmax(0, 1fr) auto; gap: 10px 28px; align-items: center; padding: 12px clamp(12px, 2vw, 22px) 14px; color: rgba(248, 248, 236, .6); }
.modi { display: flex; gap: 4px; flex-wrap: wrap; }
.modi button { font: inherit; letter-spacing: inherit; text-transform: inherit; color: rgba(248, 248, 236, .6); background: none; border: 1px solid rgba(248, 248, 236, .16); border-radius: 999px; height: 30px; padding: 0 12px; cursor: pointer; transition: background 150ms var(--ui), color 150ms var(--ui), border-color 150ms var(--ui); }
.modi button:hover { color: var(--creme); border-color: rgba(248, 248, 236, .4); }
.modi button[aria-pressed='true'] { background: var(--creme); color: var(--nacht); border-color: var(--creme); }
.modi button[data-v='ohne'] { border-style: dashed; }
.modi button[data-v='ohne'][aria-pressed='true'] { background: #e2574c; border-color: #e2574c; color: #fff; }
.protokoll { display: flex; gap: 8px; min-width: 0; overflow: hidden; list-style: none; padding: 0; }
.protokoll li { flex: none; display: flex; align-items: center; gap: 8px; height: 30px; padding: 0 12px; border-radius: 999px; background: rgba(248, 248, 236, .06); white-space: nowrap; font-size: 10.5px; letter-spacing: .1em; }
.protokoll li:first-child { background: rgba(248, 248, 236, .12); color: var(--creme); }
.protokoll li i { width: 7px; height: 7px; border-radius: 50%; background: rgba(248, 248, 236, .4); }
.protokoll li[data-art='voll'] i { background: var(--gelb); }
.protokoll li[data-art='kurz'] i { background: var(--himmel); }
.protokoll li[data-art='reveal'] i { background: #7fd49b; }
.protokoll li:not(:first-child) { opacity: .55; }
.speicher { text-align: right; white-space: nowrap; font-size: 10.5px; }
.speicher code { font: inherit; color: var(--creme); }
.speicher code.leer { color: rgba(248, 248, 236, .4); }
.werte { grid-column: 1 / -1; display: flex; flex-wrap: wrap; gap: 4px 18px; font-size: 10px; letter-spacing: .1em; color: rgba(248, 248, 236, .55); }
.werte b { color: rgba(248, 248, 236, .82); font-weight: 500; }

/* Reduzierte Bewegung: nie ein Intro, nichts gleitet */
.reduziert .intro { display: none !important; }
.reduziert *, .reduziert *::after { transition-duration: .01ms !important; animation-duration: .01ms !important; }

/* Vorschau 960 × 720: Bühne groß, Nebensachen weg */
.vorschau body { overflow: hidden; height: 100svh; }
.vorschau .werte, .vorschau .tel, .vorschau .fakten, .vorschau .speicher { display: none; }
.vorschau .pult { grid-template-columns: auto minmax(0, 1fr); }

@media (max-width: 1100px) {
  .tel { display: none; }
  .nav ul { gap: 18px; font-size: 14px; }
  .pult { grid-template-columns: auto minmax(0, 1fr); }
  .speicher { grid-column: 1 / -1; text-align: left; }
}
@media (max-width: 860px) {
  .start, .preise { grid-template-columns: minmax(0, 1fr); }
  .karte { align-self: start; }
  .bild { display: none; }
  .nav ul { display: none; }
  .nav .cta { margin-left: auto; }
}
/* Zugang: bei 200 % Zoom (bis 860 px) nur der neueste Protokolleintrag, darf umbrechen statt abgeschnitten zu werden */
@media (min-width: 561px) and (max-width: 860px) {
  .protokoll li:nth-child(n+2) { display: none; }
  .protokoll li { flex: 0 1 auto; min-width: 0; white-space: normal; height: auto; min-height: 30px; padding-block: 6px; }
}
@media (forced-colors: active) { button, [role=button], [role=tab], [role=switch] { border: 1px solid ButtonText !important; } }
@media (max-width: 560px) {
  .punkte, .leiste .knopf span { display: none; }
  .adresse { font-size: 12px; }
  .nav { padding: 12px 16px; }
  .nav .cta { padding: 11px 14px; font-size: 14px; }
  .seite { padding: 24px 16px 28px; }
  .h1 { font-size: clamp(2.3rem, 10.5vw, 3rem); margin: 14px 0 14px; }
  .klassen { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .weiter .cta { width: 100%; justify-content: center; }
  .pult { grid-template-columns: 1fr; gap: 10px; }
  .protokoll { flex-direction: column; }
  .protokoll li:nth-child(n+3) { display: none; }
  .protokoll li { flex: 0 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
  .speicher { white-space: normal; overflow-wrap: anywhere; }
  .intro { padding: 16px; }
  .moin { font-size: 31vw; }
  .intro__mitte { align-self: center; padding: 0 0 22vh; }
  .intro__hinweis { display: none; }
  .strasse { height: 45%; }
}
@media (hover: none) {
  /* Kein Hover-Heben auf Touch, Tippen reicht; Überspringen per Tipp oder Wischen */
  .klassen label:hover, .cta:hover, .sticker:hover { transform: none; }
  .cta:active { transform: translateY(1px); box-shadow: 0 1px 0 var(--navy); }
  .skip kbd { display: none; }
  .skip { padding: 9px 16px; }
}
</style>
</head>
<body>
<header class="leiste" aria-label="Simulierter Browser">
  <span class="punkte" aria-hidden="true"><i></i><i></i><i></i></span>
  <button class="knopf mono" id="reload" type="button" title="Seite neu laden (gleicher Tab)">
    <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" aria-hidden="true"><path d="M13.5 8a5.5 5.5 0 1 1-1.8-4.1"/><path d="M12.2 1.5v3h-3"/></svg><span>Neu laden</span>
  </button>
  <div class="adresse" aria-live="polite">
    <svg viewBox="0 0 12 12" fill="currentColor" aria-hidden="true"><path d="M3 5V3.6a3 3 0 0 1 6 0V5h.6c.5 0 .9.4.9.9v4.6c0 .5-.4.9-.9.9H2.4a.9.9 0 0 1-.9-.9V5.9c0-.5.4-.9.9-.9H3Zm1.4 0h3.2V3.6a1.6 1.6 0 0 0-3.2 0V5Z"/></svg>
    <span>fahrschule-kielwasser.de<b id="pfad"></b></span>
  </div>
  <button class="knopf mono" id="neuertab" type="button" title="Neuen Tab simulieren: sessionStorage ist leer">
    <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" aria-hidden="true"><path d="M8 3v10M3 8h10"/></svg><span>Neuer Tab</span>
  </button>
</header>

<div class="fenster" id="fenster">
  <nav class="nav" aria-label="Hauptnavigation">
    <button class="marke" type="button" data-geh="start" aria-label="Fahrschule Kielwasser, Startseite">
      <svg viewBox="0 0 34 34" aria-hidden="true"><rect width="34" height="34" rx="10" fill="#142b50"/><path d="M6 21c3.2 0 3.2-2.4 6.4-2.4s3.2 2.4 6.4 2.4 3.2-2.4 6.4-2.4 2.4 1.2 2.8 1.6" fill="none" stroke="#3fbadf" stroke-width="2.4" stroke-linecap="round"/><path d="M9 14.5 17 9l8 5.5" fill="none" stroke="#ffd60a" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round"/></svg>
      Kielwasser
    </button>
    <ul>
      <li><button type="button" data-geh="preise">Klassen &amp; Preise</button></li>
      <li><button type="button" data-geh="start">Ablauf</button></li>
      <li><button type="button" data-geh="start">Theorie</button></li>
      <li><button type="button" data-geh="start">Team</button></li>
    </ul>
    <a class="tel" href="tel:+494315550188">0431 555 01 88</a>
    <button class="cta" type="button" data-geh="preise">Anmelden</button>
  </nav>

  <main>
    <section class="seite start" data-seite="start" aria-labelledby="t-start">
      <div>
        <p class="kicker" data-r><i></i>Fahrschule in Kiel · Wik · Gaarden · Holtenau</p>
        <h1 class="h1" id="t-start" data-r>Führerschein an der Förde. Ohne <span class="marker">Bammel</span>.</h1>
        <p class="lead" data-r>Geduldige Fahrlehrer:innen, Theorie auch abends und die Anmeldung in drei Minuten. Wähl deine Klasse, wir zeigen dir Preis und freie Termine.</p>
        <form class="wahl" data-r onsubmit="return false">
          <fieldset>
            <legend>Welche Klasse?</legend>
            <div class="klassen">
              <label><input type="radio" name="klasse" value="B" checked><b>B</b><span>Auto</span></label>
              <label><input type="radio" name="klasse" value="B197"><b>B197</b><span>Automatik+</span></label>
              <label><input type="radio" name="klasse" value="A"><b>A</b><span>Motorrad</span></label>
              <label><input type="radio" name="klasse" value="BE"><b>BE</b><span>Anhänger</span></label>
            </div>
          </fieldset>
          <div class="weiter">
            <label class="ort">
              <svg viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"><path d="M8 1a5 5 0 0 0-5 5c0 3.6 5 9 5 9s5-5.4 5-9a5 5 0 0 0-5-5Zm0 7a2 2 0 1 1 0-4 2 2 0 0 1 0 4Z"/></svg>
              <span class="vh">Standort</span>
              <select aria-label="Standort"><option>Wik, Holtenauer Straße</option><option>Gaarden, Vinetaplatz</option><option>Holtenau, Kanalufer</option></select>
            </label>
            <button class="cta gross" type="button" data-geh="preise">Preise &amp; Termine
              <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 8h10M9 4l4 4-4 4"/></svg></button>
          </div>
        </form>
        <p class="fakten" data-r><span>Fahrstunde ab <b>68 €</b></span><span>Theorie <b>Mo &amp; Mi 18:00</b></span><span>Deutsch · Englisch · Türkisch · Arabisch</span></p>
      </div>
      <div class="bild" data-r aria-hidden="true">
        <svg class="welt" viewBox="0 0 520 560" preserveAspectRatio="xMidYMid slice">
          <rect width="520" height="560" fill="#cfeaf1"/>
          <circle cx="360" cy="170" r="92" fill="#ffd60a"/>
          <path d="M0 268c60-10 120 6 190 0s140-22 200-12 100 10 130 4v300H0Z" fill="#3fbadf"/>
          <g fill="none" stroke="#f8f8ec" stroke-width="3" stroke-linecap="round" opacity=".55">
            <path d="M40 310h46M130 330h70M300 305h54M400 336h60M70 372h52M230 360h40"/>
          </g>
          <path d="M0 262c40-4 70 0 104-4 22-2 30-14 46-14s20 12 40 12" fill="#eef0d8"/>
          <g transform="translate(112 182)">
            <path d="M8 74 12 8h12l4 66Z" fill="#f8f8ec"/>
            <path d="M10.6 30h14.6l.8 14H9.8Z" fill="#142b50"/>
            <path d="M12 8 18 0l6 8Z" fill="#142b50"/>
            <path d="M23 13 96 -14v58Z" fill="#ffd60a" opacity=".45"/>
            <rect x="12.5" y="9" width="11" height="8" rx="1.5" fill="#ffd60a"/>
          </g>
          <g transform="translate(300 262)">
            <path d="M0 40h64l-8 12H8Z" fill="#142b50"/>
            <path d="M30 36V-14l26 50Z" fill="#f8f8ec"/>
            <path d="M28 36V0L8 36Z" fill="#f8f8ec" opacity=".8"/>
          </g>
          <path d="M0 420c90-40 170-30 260-6s190 20 260-20v166H0Z" fill="#f8f8ec"/>
          <path d="M-20 560C40 500 120 470 210 476s150 34 230 0 90-60 110-74" fill="none" stroke="#142b50" stroke-width="40" stroke-linecap="round"/>
          <path d="M-20 560C40 500 120 470 210 476s150 34 230 0 90-60 110-74" fill="none" stroke="#f8f8ec" stroke-width="3" stroke-dasharray="16 14"/>
          <g transform="translate(226 440) rotate(3) scale(1.25)">
            <rect x="0" y="6" width="62" height="24" rx="9" fill="#ffd60a" stroke="#142b50" stroke-width="3"/>
            <path d="M12 7 20 -6h22l9 13" fill="#f8f8ec" stroke="#142b50" stroke-width="3" stroke-linejoin="round"/>
            <rect x="22" y="-14" width="20" height="8" rx="2" fill="#142b50"/>
            <path d="M23 -3h8v10h-8ZM34 -3h6l6 10h-12Z" fill="#cfeaf1"/>
            <circle cx="15" cy="31" r="6" fill="#142b50"/><circle cx="48" cy="31" r="6" fill="#142b50"/>
          </g>
        </svg>
        <p class="sticker">Nächste Theorie<b>Mo 18:00</b>Wik, Raum 2</p>
      </div>
    </section>

    <section class="seite preise" data-seite="preise" aria-labelledby="t-preise" hidden>
      <div>
        <p class="kicker" data-r><i></i>Klassen &amp; Preise · Stand November 2026</p>
        <h2 class="h2" id="t-preise" data-r>Was der Führerschein bei uns kostet.</h2>
        <div class="reiter" role="tablist" aria-label="Führerscheinklasse" data-r>
          <button role="tab" aria-selected="true" data-k="B">B</button>
          <button role="tab" aria-selected="false" data-k="B197">B197</button>
          <button role="tab" aria-selected="false" data-k="A">A</button>
          <button role="tab" aria-selected="false" data-k="BE">BE</button>
        </div>
        <table class="tabelle" data-r><tbody id="preisliste"></tbody></table>
        <p class="hinweis" data-r>Gebühren der Prüfstelle kommen dazu und werden direkt dort bezahlt. Zahlung in Raten ohne Aufpreis möglich.</p>
      </div>
      <aside class="karte" data-r>
        <p class="mono">Typisch in Kiel</p>
        <strong id="stunden">28–36</strong>
        <p>Übungsstunden brauchen die meisten bei uns bis zur Prüfung. Dazu kommen die Sonderfahrten, die Pflicht sind.</p>
        <ul id="sonder"></ul>
        <button class="cta" type="button" data-geh="start">Jetzt anmelden</button>
      </aside>
    </section>
  </main>

  <div class="intro" id="intro" role="dialog" aria-label="Begrüßung">
    <div class="intro__kopf">
      <span class="marke" aria-hidden="true"><svg viewBox="0 0 34 34"><rect width="34" height="34" rx="10" fill="#f8f8ec"/><path d="M6 21c3.2 0 3.2-2.4 6.4-2.4s3.2 2.4 6.4 2.4 3.2-2.4 6.4-2.4 2.4 1.2 2.8 1.6" fill="none" stroke="#3fbadf" stroke-width="2.4" stroke-linecap="round"/><path d="M9 14.5 17 9l8 5.5" fill="none" stroke="#142b50" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round"/></svg>Kielwasser</span>
      <button class="skip" id="skip" type="button"><span>Überspringen</span><kbd>Esc</kbd></button>
    </div>
    <div class="intro__mitte">
      <p class="moin" aria-label="Moin."><span>M</span><span>o</span><span>i</span><span>n</span><span class="punkt">.</span></p>
      <p class="gruss">Schön, dass du da bist. Gleich geht’s los.</p>
    </div>
    <p class="intro__hinweis" id="hinweis">Diese Begrüßung siehst du nur beim ersten Besuch in diesem Tab. Danach geht es direkt zur Seite.</p>
    <svg class="strasse" viewBox="0 0 1200 400" preserveAspectRatio="xMidYMax slice" aria-hidden="true">
      <defs>
        <mask id="aufzeichnen" maskUnits="userSpaceOnUse">
          <path id="maskpfad" class="spur" d="M-40 330C160 330 260 210 450 230S760 360 930 280 1140 140 1260 150" fill="none" stroke="#fff" stroke-width="80" pathLength="1" stroke-dasharray="1 1" stroke-dashoffset="0"/>
        </mask>
      </defs>
      <g mask="url(#aufzeichnen)">
        <path class="spur" d="M-40 330C160 330 260 210 450 230S760 360 930 280 1140 140 1260 150" fill="none" stroke="#3fbadf" stroke-width="66" stroke-linecap="round" opacity=".35"/>
        <path id="strassenpfad" class="spur" d="M-40 330C160 330 260 210 450 230S760 360 930 280 1140 140 1260 150" fill="none" stroke="#0e1d38" stroke-width="58" stroke-linecap="round"/>
        <path class="spur" d="M-40 330C160 330 260 210 450 230S760 360 930 280 1140 140 1260 150" fill="none" stroke="#f8f8ec" stroke-width="3" stroke-dasharray="18 16" opacity=".85"/>
      </g>
      <g id="ziel" transform="translate(1110 150)" opacity="0">
        <path d="M0 0V-62" stroke="#f8f8ec" stroke-width="3"/>
        <path d="M0-62h34l-8 10 8 10H0Z" fill="#ffd60a"/>
      </g>
      <g id="auto"><circle r="18" fill="#ffd60a" opacity=".25"/><circle r="10" fill="#ffd60a" stroke="#0e1d38" stroke-width="3"/></g>
    </svg>
    <div class="intro__fuss mono"><span>Erste Stunde</span><span>Bestanden</span></div>
  </div>
</div>

<section class="pult mono" aria-label="Demo-Steuerung">
  <div class="modi" role="group" aria-label="Variante">
    <button type="button" data-v="session" aria-pressed="true">Session-Flag</button>
    <button type="button" data-v="wiederkehr" aria-pressed="false">+ Wiederkehr kürzer</button>
    <button type="button" data-v="ohne" aria-pressed="false">Ohne Gedächtnis</button>
  </div>
  <ol class="protokoll" id="protokoll" aria-live="polite"></ol>
  <p class="speicher" id="speicher"></p>
  <p class="werte" id="werte"></p>
</section>

<script>
(() => {
  const h = document.documentElement;
  const reduziert = () => h.classList.contains('reduziert');
  const $ = (s, r = document) => r.querySelector(s);
  const $$ = (s, r = document) => [...r.querySelectorAll(s)];

  const EXPO = 'cubic-bezier(0.16, 1, 0.3, 1)';
  const T = {                      /* exakt diese Werte stehen in bau.werte */
    voll: { zeichen: 900, zeichenVersatz: 60, gruss: 700, grussAb: 380, strasse: 1400, strasseAb: 150, strasseKurve: 'cubic-bezier(0.65, 0, 0.35, 1)', wischenAb: 1750, wischen: 850, gesamt: 2600 },
    kurz: { zeichen: 600, zeichenVersatz: 30, wischenAb: 450, wischen: 550, gesamt: 1000 },
    reveal: { dauer: 500, versatz: 25, weg: 14 },
    skip: 150
  };
  const SKEY = 'kielwasser.intro', LKEY = 'kielwasser.besucht';

  /* Speicher mit Rückfall im Arbeitsspeicher (privater Modus, gesperrte Cookies) */
  const notfall = {};
  const speicher = (art) => ({
    get: k => { try { return window[art].getItem(k); } catch (e) { return notfall[art + k] ?? null; } },
    set: k => { try { window[art].setItem(k, '1'); } catch (e) { notfall[art + k] = '1'; } },
    del: k => { try { window[art].removeItem(k); } catch (e) { delete notfall[art + k]; } }
  });
  const ss = speicher('sessionStorage'), ls = speicher('localStorage');

  let variante = 'session', seite = 'start';
  let laeufe = [], schleife = [], autoRaf = 0, introAktiv = false, vorfuehrung = false;

  /* ── Preise (Klasse B ist Standard) ── */
  const PREISE = {
    B:    { zeilen: [['Grundbetrag', 'Theorieunterricht, Lernapp, Verwaltung', 390], ['Übungsstunde', '45 Minuten', 68], ['Sonderfahrt', 'Überland, Autobahn, Nacht · 45 Minuten', 82], ['Vorstellung Theorieprüfung', '', 120], ['Vorstellung praktische Prüfung', '', 220]], stunden: '28–36', sonder: [['Überland', '5 ×'], ['Autobahn', '4 ×'], ['Nacht', '3 ×']] },
    B197: { zeilen: [['Grundbetrag', 'Theorieunterricht, Lernapp, Verwaltung', 390], ['Übungsstunde Automatik', '45 Minuten', 68], ['Schaltstunden', '10 × 45 Minuten im Schaltwagen', 690], ['Sonderfahrt', '45 Minuten', 82], ['Vorstellung praktische Prüfung', 'auf Automatik', 220]], stunden: '24–32', sonder: [['Überland', '5 ×'], ['Autobahn', '4 ×'], ['Nacht', '3 ×']] },
    A:    { zeilen: [['Grundbetrag', 'mit Schutzkleidung zum Leihen', 340], ['Übungsstunde', '45 Minuten', 74], ['Sonderfahrt', '45 Minuten', 88], ['Vorstellung praktische Prüfung', '', 240]], stunden: '14–20', sonder: [['Überland', '5 ×'], ['Autobahn', '4 ×'], ['Nacht', '3 ×']] },
    BE:   { zeilen: [['Grundbetrag', 'ohne Theorieprüfung', 190], ['Übungsstunde mit Anhänger', '45 Minuten', 84], ['Sonderfahrt', '45 Minuten', 92], ['Vorstellung praktische Prüfung', '', 240]], stunden: '6–10', sonder: [['Überland', '3 ×'], ['Autobahn', '1 ×'], ['Nacht', '1 ×']] }
  };
  function zeigePreise(k) {
    const p = PREISE[k];
    $('#preisliste').innerHTML = p.zeilen.map(([a, b, c]) => `<tr><td>${a}${b ? `<small>${b}</small>` : ''}</td><td>${c} €</td></tr>`).join('');
    $('#stunden').textContent = p.stunden;
    $('#sonder').innerHTML = p.sonder.map(([a, b]) => `<li><span>${a}</span><span>${b}</span></li>`).join('');
    $$('.reiter button').forEach(b => b.setAttribute('aria-selected', b.dataset.k === k));
  }
  $$('.reiter button').forEach(b => b.addEventListener('click', () => zeigePreise(b.dataset.k)));
  zeigePreise('B');

  /* ── Aufräumen ── */
  function abbrechen() {
    laeufe.forEach(a => a.cancel()); laeufe = [];
    cancelAnimationFrame(autoRaf);
    introAktiv = false;
  }
  const anim = (el, kf, o) => { const a = el.animate(kf, { fill: 'both', easing: EXPO, ...o }); laeufe.push(a); return a; };

  function zeigeSeite(name) {
    seite = name;
    $$('.seite').forEach(s => s.hidden = s.dataset.seite !== name);
    const preisLink = $('.nav ul button');
    if (name === 'preise') preisLink.setAttribute('aria-current', 'page'); else preisLink.removeAttribute('aria-current');
    $('#pfad').textContent = name === 'preise' ? '/klassen-preise' : '/';
  }

  /* ── Folgebesuch: kurzer Seiten-Reveal, 25 ms Versatz (Locomotive) ── */
  function reveal(start = 0) {
    $$('[data-r]', $(`.seite[data-seite="${seite}"]`)).concat($$('.nav > *')).forEach((el, i) => {
      anim(el, [{ opacity: 0, transform: `translateY(${T.reveal.weg}px)` }, { opacity: 1, transform: 'none' }],
        { duration: T.reveal.dauer, delay: start + i * T.reveal.versatz });
    });
  }

  /* ── Volles bzw. kurzes Intro ── */
  const intro = $('#intro'), zeichen = $$('.moin span'), maske = $('#maskpfad'), pfad = $('#strassenpfad'), auto = $('#auto');
  /* Zugang: Solange das Intro liegt, ist die Seite darunter inert (Fokus landet nicht unter der Fläche) */
  const unterIntro = [$('#fenster > nav'), $('#fenster > main')];
  const introSperre = () => { const an = intro.hasAttribute('data-an') || h.classList.contains('mit-intro'); unterIntro.forEach(el => el && (el.inert = an)); };
  new MutationObserver(introSperre).observe(intro, { attributes: true, attributeFilter: ['data-an'] });
  new MutationObserver(introSperre).observe(h, { attributes: true, attributeFilter: ['class'] });
  introSperre();
  /* Straßenform je Breite: breit 1200 × 400, schmal (Telefon) 400 × 420. Das Auto hält an der Flagge. */
  const FORM = {
    breit:  { vb: '0 0 1200 400', d: 'M-40 330C160 330 260 210 450 230S760 360 930 280 1140 140 1260 150', zielX: 1096 },
    schmal: { vb: '0 0 400 420', d: 'M-30 360C70 360 120 280 200 290C270 300 280 350 320 330C350 315 360 240 440 220', zielX: 330 }
  };
  let zielLaenge = 0, flagge = [0, 0];
  function strasseEinrichten() {
    const f = FORM[matchMedia('(max-width: 560px)').matches ? 'schmal' : 'breit'];
    $('.strasse').setAttribute('viewBox', f.vb);
    $$('.strasse .spur').forEach(el => el.setAttribute('d', f.d));
    let lo = 0, hi = pfad.getTotalLength();
    for (let i = 0; i < 24; i++) { const m = (lo + hi) / 2; (pfad.getPointAtLength(m).x < f.zielX ? lo = m : hi = m); }
    zielLaenge = lo;
    const pt = pfad.getPointAtLength(lo);
    flagge = [pt.x + 14, pt.y];
  }

  function introSpielen(kurz) {
    const t = kurz ? T.kurz : T.voll;
    introAktiv = true;
    intro.setAttribute('data-an', '');
    h.classList.remove('mit-intro');
    zeichen.forEach((z, i) => anim(z, [{ transform: 'translateY(105%)' }, { transform: 'none' }], { duration: t.zeichen, delay: i * t.zeichenVersatz }));
    $('.strasse').style.display = kurz ? 'none' : '';
    $('.intro__fuss').style.visibility = kurz ? 'hidden' : '';
    $('.gruss').style.visibility = kurz ? 'hidden' : '';
    $('#hinweis').textContent = variante === 'ohne' ? 'Gegenprobe: Ohne Gedächtnis kommt diese Begrüßung bei jedem Klick wieder.'
      : kurz ? 'Du warst schon einmal hier. Darum heute nur die kurze Fassung.'
      : 'Diese Begrüßung siehst du nur beim ersten Besuch in diesem Tab. Danach geht es direkt zur Seite.';
    if (!kurz) {
      strasseEinrichten();
      anim($('.gruss'), [{ opacity: 0, transform: 'translateY(12px)' }, { opacity: 1, transform: 'none' }], { duration: t.gruss, delay: t.grussAb });
      anim($('.intro__fuss'), [{ opacity: 0 }, { opacity: 1 }], { duration: 600, delay: t.strasseAb });
      const zeichnen = anim(maske, [{ strokeDashoffset: '1' }, { strokeDashoffset: '0' }], { duration: t.strasse, delay: t.strasseAb, easing: t.strasseKurve });
      anim($('#ziel'), [{ opacity: 0, transform: `translate(${flagge[0]}px, ${flagge[1] + 16}px)` }, { opacity: 1, transform: `translate(${flagge[0]}px, ${flagge[1]}px)` }], { duration: 600, delay: t.strasseAb + t.strasse - 350, easing: 'cubic-bezier(0.34, 1.56, 0.64, 1)' });
      const fahren = () => {
        const p = zeichnen.effect.getComputedTiming().progress ?? 0;
        const pt = pfad.getPointAtLength(Math.max(0, Math.min(1, p)) * zielLaenge);
        auto.setAttribute('transform', `translate(${pt.x} ${pt.y})`);
        if (introAktiv) autoRaf = requestAnimationFrame(fahren);
      };
      fahren();
    }
    /* Ausgang: Fläche wischt nach oben, die Seite darunter erscheint im Reveal */
    const wischen = anim(intro, [{ clipPath: 'inset(0 0 0 0)' }, { clipPath: 'inset(0 0 100% 0)' }], { duration: t.wischen, delay: t.wischenAb, easing: 'cubic-bezier(0.76, 0, 0.24, 1)' });
    reveal(t.wischenAb + 120);
    /* Ende hängt an der Animation selbst: pause()/weiter() verschieben es mit */
    wischen.finished.then(introEnde, () => {});
  }
  function introEnde() {
    introAktiv = false;
    cancelAnimationFrame(autoRaf);
    intro.removeAttribute('data-an');
    h.classList.remove('mit-intro');
  }
  function ueberspringen() {
    if (!introAktiv && !h.classList.contains('mit-intro')) return;
    abbrechen();
    intro.setAttribute('data-an', '');
    const a = intro.animate([{ opacity: 1 }, { opacity: 0 }], { duration: T.skip, easing: 'linear', fill: 'both' });
    laeufe.push(a);
    a.onfinish = () => { introEnde(); a.cancel(); };
    protokoll('skip', 'Übersprungen', 'Intro beendet');
  }
  $('#skip').addEventListener('click', ueberspringen);
  addEventListener('keydown', e => { if (e.key === 'Escape') ueberspringen(); });
  intro.addEventListener('wheel', ueberspringen, { passive: true });
  intro.addEventListener('touchmove', ueberspringen, { passive: true });

  /* ── Entscheidung pro Aufruf ── */
  const NAMEN = { erst: 'Erstaufruf', neuertab: 'Neuer Tab', reload: 'Neu laden', navigation: 'Navigation' };
  function entscheide(art, kopfHatteIntro) {
    if (reduziert()) return 'keins';
    if (variante === 'ohne') return 'voll';
    if (art === 'navigation') return 'reveal';
    let erstmals;
    if (art === 'erst') erstmals = kopfHatteIntro;
    else { erstmals = !ss.get(SKEY); ss.set(SKEY); }
    if (!erstmals) return 'reveal';
    const kannte = ls.get(LKEY); ls.set(LKEY);
    return variante === 'wiederkehr' && kannte && art !== 'erst' ? 'kurz' : 'voll';
  }
  function besuch(art, ziel) {
    abbrechen();
    const kopf = h.classList.contains('mit-intro');
    h.classList.remove('mit-intro');
    intro.removeAttribute('data-an');
    if (ziel) zeigeSeite(ziel);
    const modus = entscheide(art, kopf);
    if (modus === 'voll' || modus === 'kurz') introSpielen(modus === 'kurz');
    else if (modus === 'reveal') reveal(art === 'reload' ? 80 : 0);
    const text = { voll: `Volles Intro · ${T.voll.gesamt} ms`, kurz: `Kurz-Intro · ${T.kurz.gesamt} ms`, reveal: `Reveal · ${T.reveal.versatz} ms Versatz`, keins: 'Reduziert · kein Intro' }[modus];
    protokoll(modus, NAMEN[art], text);
    zeigeSpeicher();
    if (art === 'reload') { const f = $('#fenster'); const a = f.animate([{ opacity: .25 }, { opacity: 1 }], { duration: 180, easing: 'linear' }); laeufe.push(a); }
  }

  function protokoll(art, wer, was) {
    const li = document.createElement('li');
    li.dataset.art = art;
    li.innerHTML = `<i></i>${wer} → ${was}`;
    const ol = $('#protokoll');
    ol.prepend(li);
    while (ol.children.length > 4) ol.lastChild.remove();
  }
  function zeigeSpeicher() {
    const s = ss.get(SKEY), l = ls.get(LKEY);
    const fmt = v => v ? `<code>"${v}"</code>` : '<code class="leer">null</code>';
    $('#speicher').innerHTML = `sessionStorage.${SKEY} = ${fmt(s)}` + (variante === 'wiederkehr' ? ` · localStorage.${LKEY} = ${fmt(l)}` : '');
  }
  function zeigeWerte() {
    const w = {
      session: [['Gedächtnis', 'sessionStorage pro Tab, gesetzt im <head>'], ['Intro', `${T.voll.gesamt} ms`], ['Buchstaben', `${T.voll.zeichen} ms · ${T.voll.zeichenVersatz} ms Versatz · expo.out`], ['Straße', `${T.voll.strasse} ms`], ['Wischen', `${T.voll.wischen} ms ab ${T.voll.wischenAb} ms · (0.76, 0, 0.24, 1)`], ['Folgeaufruf', `Reveal ${T.reveal.dauer} ms · ${T.reveal.versatz} ms Versatz · ${T.reveal.weg} px`]],
      wiederkehr: [['Neuer Tab, schon mal da', `Kurz-Intro ${T.kurz.gesamt} ms`], ['Buchstaben', `${T.kurz.zeichen} ms · ${T.kurz.zeichenVersatz} ms Versatz`], ['Wischen', `${T.kurz.wischen} ms ab ${T.kurz.wischenAb} ms`], ['Merker', 'localStorage, dauerhaft'], ['Folgeaufruf', `Reveal ${T.reveal.versatz} ms Versatz`]],
      ohne: [['Gegenprobe', 'Intro bei jedem Aufruf'], ['Kosten', `${T.voll.gesamt} ms Wartezeit pro Klick`], ['So nicht', 'Wiederkehrer warten jedes Mal']]
    }[variante];
    $('#werte').innerHTML = w.map(([k, v]) => `<span>${k} <b>${v.replace(/</g, '&lt;')}</b></span>`).join('');
  }

  /* ── Bedienung ── */
  $$('[data-geh]').forEach(b => b.addEventListener('click', () => besuch('navigation', b.dataset.geh)));
  $('#reload').addEventListener('click', () => besuch('reload'));
  $('#neuertab').addEventListener('click', () => { ss.del(SKEY); besuch('neuertab', 'start'); });
  $$('.modi button').forEach(b => b.addEventListener('click', () => {
    variante = b.dataset.v;
    $$('.modi button').forEach(x => x.setAttribute('aria-pressed', x === b));
    zeigeWerte(); zeigeSpeicher();
  }));

  /* ── API ── */
  function neustart() {
    schleife.forEach(clearTimeout); schleife = [];
    ss.del(SKEY);
    besuch('neuertab', 'start');
  }
  const tippe = (el) => { el.classList.remove('tipp'); void el.offsetWidth; el.classList.add('tipp'); };
  function vorfuehren() {
    schleife.forEach(clearTimeout); schleife = [];
    vorfuehrung = true;
    const plan = [
      [0,     () => besuch('navigation', 'start')],
      [1500,  () => { tippe($('#neuertab')); ss.del(SKEY); besuch('neuertab', 'start'); }],
      [5600,  () => { tippe($('.nav ul button')); besuch('navigation', 'preise'); }],
      [7800,  () => { tippe($('#reload')); besuch('reload'); }],
      [10000, () => vorfuehren()]
    ];
    plan.forEach(([ms, fn]) => schleife.push(setTimeout(fn, ms)));
  }
  function pause() {
    schleife.forEach(clearTimeout); schleife = [];
    laeufe.forEach(a => a.playState === 'running' && a.pause());
  }
  function weiter() {
    if (vorfuehrung) return vorfuehren();
    laeufe.forEach(a => a.playState === 'paused' && a.play());
  }


  /* Erster echter Aufruf: der Kopf hat schon entschieden (Klasse mit-intro) */
  zeigeWerte();
  zeigeSeite('start');
  const los = () => besuch('erst');
  if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', los); else queueMicrotask(los);
})();
</script>
</body>
</html>