Zum Inhalt
← Scroll & Section-Animationen

SVG-Pfad zeichnet sich beim Scrollen

Eine Straße zeichnet sich beim Scrollen durch fünf Etappen zum Führerschein, Wegschilder federn, sobald sie erreicht sind.

Fahrschule, Ablauf mit festen Etappen: eine breite Straße mit Schildern führt durch die Schritte, optional mit Auto oder Text am Pfad.

Eine geschwungene Linie läuft über mehrere Abschnitte, etwa als Leitlinie, Route oder Signatur. Ihre sichtbare Länge folgt dem Scroll: stroke-dasharray entspricht der Gesamtlänge, stroke-dashoffset läuft von der Länge auf 0. Optional fährt ein Objekt oder Text am Pfad mit.

Die Linie verbindet Abschnitte grafisch, macht den Fortschritt sichtbar und wirkt handgemacht.

Wann einsetzen

Prozess- und Timeline-Seiten, verspielte Marken, Signatur-Animationen.

Wann nicht

In nüchternen, datenorientierten Layouts und wenn die Linie Inhalte kreuzt.

Varianten

  • stroke-dashoffset an den Scroll gekoppelt
  • Text entlang des Pfads (textPath, startOffset)
  • Objekt entlang des Pfads (offset-path mit offset-distance oder getPointAtLength)
  • Canvas-Variante
  • Handlinie mit Ankern (Variante anker): Die Linie wird aus den Positionen echter Elemente errechnet (unterstreicht eine Headline, umkreist Zahlen und Preis, endet mit Pfeil am Knopf), mit Rauheit und Begleitstrich wie von Hand.

Umsetzung

Am Pfad pathLength="1" setzen, dann gilt stroke-dasharray: 1 und stroke-dashoffset von 1 auf 0, ohne getTotalLength. Nur CSS: animation: draw linear both; animation-timeline: view(); animation-range: entry 20% exit 60% mit @keyframes draw { to { stroke-dashoffset: 0 } }. Hat die Linie selbst ein Strichmuster (Mittellinie), zeichnet man stattdessen eine Maske mit diesem Trick. Soll die Spitze genau einer Höhe im Viewport folgen, den Pfad einmal abtasten und den Fortschritt per y-Wert suchen. Mitfahrendes Objekt: offset-path: path(…) mit offset-distance 0 % bis 100 % oder getPointAtLength.

Robust umsetzen

Telefon. Für Hochformat einen eigenen Pfad zeichnen, preserveAspectRatio beachten. Am robustesten: Pfad aus den echten Positionen der Inhalte bauen.

Ohne Bewegung. Linie vollständig gezeichnet zeigen.

Barrierefrei. Dekorative Linie mit aria-hidden ausblenden. Alle Informationen müssen im Text stehen, die Linie ergänzt nur.

Code

<!DOCTYPE html>
<!--
  SVG-Pfad zeichnet sich beim Scrollen
  Fundus · Scroll & Section-Animationen · https://cecodesign.de/lab/fundus/scroll/svg-pfad-zeichnen-scroll/
  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>SVG-Pfad zeichnet sich beim Scrollen · 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: '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: 'Onest'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/onest:vf@latest/latin-wght-normal.woff2') format('woff2-variations'); font-weight: 100 900; font-display: swap; }
@font-face { font-family: 'Shantell Sans'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/shantell-sans:vf@latest/latin-full-normal.woff2') format('woff2-variations'); font-weight: 300 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 {
  /* Rezept fahrschule-freundlich-buchbar */
  --grund: #f8f8ec; --grund-tief: #eef0d8; --navy: #142b50; --nacht: #0e1d38; --himmel: #3fbadf; --gelb: #ffd60a; --leise: #566175;
  --feder: cubic-bezier(0.34, 1.56, 0.64, 1);   /* nur kleine Elemente: Schilder, Knöpfe */
  --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(--grund); color: var(--navy); font-family: 'Atkinson Hyperlegible Next', system-ui, sans-serif; -webkit-font-smoothing: antialiased; }
body { font-size: 1.125rem; line-height: 1.6; overflow-x: hidden; }
button { font: inherit; color: inherit; background: none; border: 0; cursor: pointer; }
h1, h2, h3 { font-family: 'Bricolage Grotesque', sans-serif; font-weight: 700; letter-spacing: -.02em; }
.klein { font-size: .8125rem; font-weight: 600; letter-spacing: .08em; text-transform: uppercase; }

/* ---------- Kopf ---------- */
.kopf { position: sticky; top: 0; z-index: 10; display: flex; align-items: center; justify-content: space-between; gap: 20px; padding: 14px var(--rand); background: rgba(248, 248, 236, .9); -webkit-backdrop-filter: blur(8px); backdrop-filter: blur(8px); }
.marke { display: flex; align-items: center; gap: 10px; color: inherit; text-decoration: none; }
.marke svg { width: 38px; height: 38px; }
.marke b { font: 800 1.25rem/1 'Bricolage Grotesque', sans-serif; font-variation-settings: 'opsz' 36; letter-spacing: -.02em; }
.marke small { display: block; font-size: .6875rem; letter-spacing: .1em; text-transform: uppercase; color: var(--leise); font-weight: 600; margin-top: 3px; }
.kopf nav { display: flex; gap: 28px; font-weight: 600; font-size: 1rem; }
.kopf nav a { color: inherit; text-decoration: none; }
.kopf nav a[aria-current] { box-shadow: inset 0 -3px 0 var(--himmel); }
.knopf { display: inline-flex; align-items: center; gap: 10px; height: 48px; padding: 0 22px; border-radius: 999px; background: var(--gelb); color: var(--navy); font-weight: 700; font-size: 1rem; box-shadow: 0 2px 0 var(--navy); border: 2px solid var(--navy); transition: transform .35s var(--feder), box-shadow .35s var(--feder); text-decoration: none; white-space: nowrap; }
.knopf:hover { transform: translateY(-2px); box-shadow: 0 4px 0 var(--navy); }
.knopf:focus-visible, .klassen button:focus-visible { outline: 3px solid var(--himmel); outline-offset: 3px; }

/* ---------- Einstieg ---------- */
.hero { display: grid; grid-template-columns: minmax(0, 1.6fr) minmax(300px, .75fr); align-items: end; gap: 32px clamp(32px, 5vw, 80px); max-width: 1280px; margin: 0 auto; padding: clamp(40px, 8vh, 88px) var(--rand) 24px; }
.hero__text { display: grid; gap: 22px; }
.ort { color: var(--leise); }
.hero h1 { font-size: clamp(2.5rem, 1rem + 6.5vw, 6rem); line-height: .98; font-variation-settings: 'opsz' 96; hyphens: manual; }
.karte { display: grid; gap: 14px; padding: 22px; border: 2px solid var(--navy); border-radius: 20px; background: #fff; box-shadow: 6px 6px 0 var(--grund-tief); }
.karte .klein { color: var(--leise); }
.karte dl { display: grid; border-top: 1.5px solid var(--grund-tief); }
.karte dl div { display: flex; justify-content: space-between; gap: 12px; padding: 9px 0; border-bottom: 1.5px solid var(--grund-tief); font-size: 1rem; }
.karte dt { color: var(--leise); }
.karte dd { font-weight: 700; font-variant-numeric: tabular-nums; white-space: nowrap; }
.karte .knopf { justify-content: center; }
.hero h1 mark { color: inherit; background: var(--gelb); padding: 0 .12em; border-radius: .12em; box-decoration-break: clone; -webkit-box-decoration-break: clone; }
.hero .lead { max-width: 34rem; color: var(--leise); }
.hero .lead b { color: var(--navy); }
.klassen { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
.klassen button { height: 44px; min-width: 56px; padding: 0 16px; border-radius: 999px; border: 2px solid var(--navy); font-weight: 700; font-variant-numeric: tabular-nums; transition: background-color .2s var(--ui), color .2s var(--ui); }
.klassen button[aria-checked='true'] { background: var(--navy); color: var(--grund); }

/* ---------- Ablauf mit Straße ---------- */
.ablauf { position: relative; padding: 72px 0 0; }
.ablauf__kopf { max-width: 1280px; margin: 0 auto 64px; padding: 0 var(--rand); display: grid; gap: 10px; position: relative; z-index: 2; }
.ablauf__kopf > * { max-width: 40%; }
.ablauf__kopf h2 { font-size: clamp(2rem, 1rem + 4vw, 4rem); line-height: 1; font-variation-settings: 'opsz' 72; }
.ablauf__kopf p { color: var(--leise); max-width: 22rem; }
.strasse { position: absolute; inset: 0 0 auto 0; width: 100%; pointer-events: none; z-index: 1; overflow: visible; }
.strasse .geist { fill: none; stroke: var(--navy); stroke-opacity: .18; stroke-width: 2; stroke-dasharray: 2 9; stroke-linecap: round; }
.strasse .belag { fill: none; stroke: var(--navy); stroke-width: 34; stroke-linecap: butt; }
.strasse .rand { fill: none; stroke: var(--grund-tief); stroke-width: 46; }
.strasse .mittellinie { fill: none; stroke: var(--grund); stroke-width: 3; stroke-dasharray: 14 16; }
.strasse .maske { fill: none; stroke: #fff; stroke-width: 60; }
.strasse .pfadtext { font: 700 12.5px/1 'Atkinson Hyperlegible Next', sans-serif; letter-spacing: .14em; text-transform: uppercase; fill: var(--gelb); dominant-baseline: central; display: none; }
.strasse .auto { display: none; }
.m-auto .strasse .auto { display: inline; }
.m-text .strasse .pfadtext { display: inline; }
.m-text .strasse .mittellinie { display: none; }

.etappen { position: relative; z-index: 2; list-style: none; display: grid; row-gap: clamp(96px, 16vh, 168px); max-width: 1280px; margin: 0 auto; padding: 0 var(--rand); }
.etappe { display: grid; grid-template-columns: repeat(12, minmax(0, 1fr)); column-gap: 24px; min-height: 250px; }
.etappe__text { grid-column: 1 / 6; display: grid; gap: 12px; align-content: start; padding-top: 6px; }
.etappe__weg { grid-column: 6 / 13; position: relative; }
.etappe[data-seite='rechts'] .etappe__text { grid-column: 8 / 13; }
.etappe[data-seite='rechts'] .etappe__weg { grid-column: 1 / 8; grid-row: 1; }
.etappe__text .klein { color: var(--leise); }
.etappe h3 { font-size: clamp(1.75rem, 1rem + 2.2vw, 2.75rem); line-height: 1.02; font-variation-settings: 'opsz' 48; }
.etappe p { max-width: 30rem; }
.fakt { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 4px; }
.fakt span { font-size: .9375rem; font-weight: 600; padding: 4px 12px; border-radius: 999px; background: var(--grund-tief); font-variant-numeric: tabular-nums; }

/* Wegpunkt-Schild: federt, sobald die Straße es erreicht */
.schild { position: absolute; top: 0; left: 58%; width: 64px; height: 64px; margin-left: -32px; border-radius: 50%; display: grid; place-items: center; background: #fff; border: 3px solid var(--navy); font: 800 1.625rem/1 'Bricolage Grotesque', sans-serif; font-variation-settings: 'opsz' 36; transform: scale(.78); transition: transform .4s var(--feder), background-color .2s var(--ui); z-index: 3; }
.etappe[data-seite='rechts'] .schild { left: 42%; }
.schild.erreicht { transform: scale(1); background: var(--gelb); }
.deko { position: absolute; width: 150px; height: 150px; top: 60px; left: 10%; }
.etappe[data-seite='rechts'] .deko { left: auto; right: 6%; }
.deko * { vector-effect: non-scaling-stroke; }

.ziel { position: relative; z-index: 2; max-width: 1280px; margin: clamp(80px, 12vh, 140px) auto 0; padding: 0 var(--rand) 120px; display: grid; justify-items: center; text-align: center; gap: 18px; }
.ziel__fahne { width: 72px; height: 72px; }
.ziel h2 { font-size: clamp(2.5rem, 1rem + 6vw, 5.5rem); line-height: .98; font-variation-settings: 'opsz' 96; }
.ziel p { max-width: 32rem; color: var(--leise); }
.ziel .knoepfe { display: flex; flex-wrap: wrap; gap: 12px; justify-content: center; margin-top: 8px; }
.knopf--leise { background: transparent; }

/* ---------- Mobile Leiste (Teil des Rezepts) ---------- */
.leiste { display: none; }

/* ---------- Demo-Pult ---------- */
.pult { position: fixed; z-index: 30; right: 16px; bottom: 16px; display: grid; gap: 6px; justify-items: end; 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(14, 29, 56, .9); }
.pult button { height: 26px; padding: 0 12px; border-radius: 999px; color: rgba(248, 248, 236, .72); letter-spacing: inherit; text-transform: inherit; transition: background-color .15s var(--ui), color .15s var(--ui); }
.pult button:hover { color: #fff; }
.pult button[aria-pressed='true'] { background: var(--gelb); color: var(--navy); }
.pult__werte { padding: 5px 12px; border-radius: 999px; background: rgba(255, 255, 255, .92); color: var(--navy); text-transform: none; letter-spacing: .04em; font-variant-numeric: tabular-nums; box-shadow: 0 0 0 1px rgba(20, 43, 80, .1); }

.pult [hidden] { display: none !important; }

/* ---------- Variante anker: Handlinie verbindet echte Elemente (Rezept kurs-coaching-lernweg) ---------- */
.welt { display: contents; }
.welt[hidden] { display: none !important; }
html.v-anker { background: #f4f6fa; color: #141414; }
.wa { --wa-grund: #f4f6fa; --wa-tinte: #141414; --wa-leise: #545b69; --wa-marke: #3a50c8; --wa-marke-tief: #2f45b8; --wa-erfolg: #10bf7a; --wa-erfolg-tief: #0b3d2a; --wa-ui: cubic-bezier(0.16, 1, 0.3, 1); font-family: 'Onest', system-ui, sans-serif; font-size: 1.0625rem; line-height: 1.6; color: var(--wa-tinte); }
.wa h1, .wa h2, .wa h3 { font-family: 'Bricolage Grotesque', sans-serif; color: var(--wa-tinte); letter-spacing: -.02em; }
.wa h2 { font-size: clamp(1.75rem, 1.2rem + 2vw, 2.75rem); line-height: 1.05; font-weight: 650; font-variation-settings: 'opsz' 72; }
.wa h3 { font-size: 1.3125rem; line-height: 1.2; font-weight: 650; font-variation-settings: 'opsz' 36; }
.wa-hand { font-family: 'Shantell Sans', 'Onest', sans-serif; font-variation-settings: 'INFM' 40, 'BNCE' 0; }
.wa-kopf { position: sticky; top: 0; z-index: 10; display: flex; align-items: center; justify-content: space-between; gap: 20px; padding: 12px var(--rand); background: rgba(244, 246, 250, .92); -webkit-backdrop-filter: blur(8px); backdrop-filter: blur(8px); border-bottom: 1px solid rgba(20, 20, 20, .08); }
.wa-marke { display: flex; align-items: center; gap: 10px; color: inherit; text-decoration: none; }
.wa-marke svg { width: 36px; height: 36px; flex: none; }
.wa-marke b { font: 800 1.1875rem/1 'Bricolage Grotesque', sans-serif; letter-spacing: -.02em; }
.wa-marke small { display: block; margin-top: 3px; font-size: .75rem; color: var(--wa-leise); }
.wa-kopf nav { display: flex; gap: 26px; font-weight: 500; font-size: 1rem; }
.wa-kopf nav a { color: inherit; text-decoration: none; }
.wa-kopf nav a:hover { text-decoration: underline; text-underline-offset: 5px; }
.wa-knopf { display: inline-flex; align-items: center; justify-content: center; gap: 8px; min-height: 48px; padding: 0 22px; border-radius: 12px; border: 2px solid var(--wa-marke); background: var(--wa-marke); color: #fff; font-weight: 600; font-size: 1rem; text-decoration: none; white-space: nowrap; transition: background-color .15s var(--wa-ui), border-color .15s var(--wa-ui), transform .15s var(--wa-ui); }
.wa-knopf:hover { background: var(--wa-marke-tief); border-color: var(--wa-marke-tief); }
.wa-knopf:active { transform: translateY(1px); }
.wa-knopf--kontur { background: transparent; color: var(--wa-marke-tief); }
.wa-knopf--kontur:hover { background: rgba(58, 80, 200, .08); }
.wa a:focus-visible { outline: 3px solid var(--wa-tinte); outline-offset: 3px; border-radius: 6px; }
.wa-seite { position: relative; overflow-x: clip; }
.wa-linie { position: absolute; inset: 0 0 auto 0; width: 100%; z-index: 0; pointer-events: none; overflow: visible; }
.wa-strich, .wa-pfeil { fill: none; stroke: var(--wa-marke); stroke-width: 3.2; stroke-linecap: round; stroke-linejoin: round; }
.wa-strich--zwei { stroke-width: 1.4; stroke-opacity: .45; }
.wa-pfeil { opacity: 0; transition: opacity .2s var(--wa-ui); }
.wa-pfeil.da { opacity: 1; }
.wa-spitze { fill: var(--wa-marke); }
.wa-block { position: relative; z-index: 1; max-width: 1180px; margin: 0 auto; padding: 0 var(--rand); }
.wa-hero { padding-top: clamp(48px, 10vh, 104px); padding-bottom: clamp(88px, 14vh, 150px); }
.wa-label { font-size: .875rem; font-weight: 600; color: var(--wa-leise); }
.wa-hero h1 { font-size: clamp(2.5rem, 1.3rem + 4.8vw, 5rem); line-height: 1.03; font-weight: 700; font-variation-settings: 'opsz' 96; max-width: 12ch; margin: 14px 0 24px; }
.wa-lead { max-width: 35rem; font-size: 1.1875rem; color: var(--wa-leise); }
.wa-fakten { list-style: none; display: flex; flex-wrap: wrap; gap: 8px 28px; margin: 24px 0 30px; color: var(--wa-leise); }
.wa-fakten b { color: var(--wa-tinte); font-weight: 650; font-variant-numeric: tabular-nums; }
.wa-knoepfe { display: flex; flex-wrap: wrap; gap: 12px; }
.wa-notiz { position: absolute; z-index: 2; max-width: 12em; font-size: .9375rem; line-height: 1.3; color: var(--wa-marke-tief); opacity: 0; transform: translateY(6px) rotate(-3deg); transition: opacity .3s var(--wa-ui), transform .3s var(--wa-ui); }
.wa-notiz.da { opacity: 1; transform: rotate(-3deg); }
.wa-hero .wa-notiz { right: calc(var(--rand) + 4%); top: 46%; }
.wa-koennen { padding-bottom: clamp(96px, 15vh, 160px); }
.wa-koennen h2 { max-width: 16ch; margin-bottom: 36px; }
.wa-liste { list-style: none; display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 32px clamp(24px, 4vw, 56px); }
.wa-liste li { display: grid; gap: 8px; align-content: start; }
.wa-zahl { font: 600 .9375rem/1 'Shantell Sans', sans-serif; color: var(--wa-marke-tief); }
.wa-liste p { color: var(--wa-leise); }
.wa-liste b { font-weight: inherit; white-space: nowrap; }
.wa-koennen .wa-notiz { right: var(--rand); top: 40px; }
.wa-lernweg { padding-bottom: clamp(96px, 15vh, 160px); }
.wa-unter { max-width: 34rem; margin: 12px 0 48px; color: var(--wa-leise); }
.wa-module { list-style: none; display: grid; gap: clamp(48px, 9vh, 88px); }
.wa-modul { display: grid; grid-template-columns: 64px minmax(0, 1fr); gap: 22px; width: min(100%, 560px); }
.wa-modul:nth-child(even) { margin-left: auto; }
.wa-nr { display: grid; place-items: center; width: 56px; height: 56px; font: 700 1.5rem/1 'Bricolage Grotesque', sans-serif; font-variant-numeric: tabular-nums; }
.wa-modul .wa-hand { font-size: .9375rem; color: var(--wa-marke-tief); }
.wa-modul h3 { margin: 2px 0 6px; }
.wa-modul p { color: var(--wa-leise); }
.wa-ergebnis { margin-top: 8px; font-weight: 600; color: var(--wa-tinte) !important; }
.wa-ergebnis span { color: var(--wa-erfolg-tief); }
.wa-person { display: grid; grid-template-columns: 120px minmax(0, 1fr); gap: 32px; max-width: 920px; padding-bottom: clamp(96px, 15vh, 160px); }
.wa-person svg { width: 120px; height: 120px; }
.wa-person p { color: var(--wa-leise); margin: 8px 0 14px; max-width: 36rem; }
.wa-person blockquote { padding-left: 18px; border-left: 3px solid var(--wa-erfolg); font-size: 1.125rem; }
.wa-person cite { display: block; margin-top: 6px; font-size: .9375rem; font-style: normal; color: var(--wa-leise); }
.wa-preis { padding-bottom: 72px; }
.wa-preisbox { max-width: 620px; margin-left: auto; padding: clamp(24px, 4vw, 40px); border: 2px solid var(--wa-tinte); border-radius: 20px; }
.wa-preisbox .wa-hand { font-size: .9375rem; color: var(--wa-marke-tief); }
.wa-betrag { display: flex; flex-wrap: wrap; align-items: baseline; gap: 6px 16px; margin: 18px 0 16px; }
.wa-betrag > span { font: 700 clamp(2.5rem, 1.6rem + 3vw, 3.5rem)/1 'Bricolage Grotesque', sans-serif; font-variant-numeric: tabular-nums; letter-spacing: -.02em; }
.wa-betrag small { font-size: .9375rem; color: var(--wa-leise); }
.wa-drin { list-style: none; display: grid; gap: 6px; margin-bottom: 26px; }
.wa-drin li::before { content: '✓'; margin-right: 10px; font-weight: 700; color: var(--wa-erfolg-tief); }
.wa-cta { display: flex; flex-wrap: wrap; align-items: center; gap: 14px 18px; }
.wa-stempel { display: inline-block; padding: 6px 12px; border-radius: 8px; background: var(--wa-erfolg); color: var(--wa-erfolg-tief); font: 700 .9375rem/1.2 'Shantell Sans', sans-serif; opacity: 0; transform: rotate(-6deg) scale(.4); transition: transform .5s var(--feder), opacity .2s var(--wa-ui); }
.wa-stempel.da { opacity: 1; transform: rotate(-6deg) scale(1); }
.wa-fuss { display: flex; flex-wrap: wrap; justify-content: space-between; gap: 12px 24px; padding-top: 28px; padding-bottom: 160px; border-top: 1px solid rgba(20, 20, 20, .1); font-size: .9375rem; color: var(--wa-leise); }
.wa-fuss a { color: inherit; margin-left: 18px; }
.reduziert .wa-notiz, .reduziert .wa-stempel, .reduziert .wa-pfeil { transition: none; }
@media (max-width: 760px) {
  .wa-kopf nav, .wa-kopf .wa-knopf { display: none; }
  .wa-liste { grid-template-columns: minmax(0, 1fr); }
  .wa-modul { grid-template-columns: 56px minmax(0, 1fr); gap: 14px; }
  .wa-modul:nth-child(even) { margin-left: 0; }
  .wa-person { grid-template-columns: minmax(0, 1fr); gap: 16px; }
  .wa-person svg { width: 88px; height: 88px; }
  .wa-notiz { display: none; }
}
@media (forced-colors: active) { .wa-knopf { border: 2px solid ButtonText; } .wa-strich, .wa-pfeil { stroke: CanvasText; } .wa-spitze { fill: CanvasText; } }

/* ---------- Vorschau ---------- */
.vorschau body { overflow: hidden; }
.vorschau .kopf nav, .vorschau .wa-kopf nav { display: none; }

/* ---------- Telefon: eigener Pfad am linken Rand ---------- */
@media (max-width: 760px) {
  .kopf nav, .kopf .knopf { display: none; }
  .hero { grid-template-columns: 1fr; padding-top: 28px; }
  .hero h1 { font-size: clamp(2.5rem, 1rem + 8vw, 3.5rem); }
  .ablauf { padding-top: 40px; }
  .ablauf__kopf { margin-bottom: 40px; padding-left: calc(var(--rand) + 56px); }
  .ablauf__kopf > * { max-width: none; }
  .karte dl div:nth-child(n+2) { display: none; }
  .etappen { row-gap: 56px; padding-left: calc(var(--rand) + 56px); }
  .etappe, .etappe[data-seite] { display: block; min-height: 0; position: relative; }
  .etappe__weg { position: absolute; inset: 0 auto 0 -56px; width: 56px; }
  .schild, .etappe[data-seite='rechts'] .schild { left: 22px; width: 44px; height: 44px; margin-left: -22px; font-size: 1.25rem; border-width: 2.5px; }
  .deko { display: none; }
  .etappe h3 { font-size: 1.75rem; }
  .strasse .belag { stroke-width: 18; }
  .strasse .rand { stroke-width: 26; }
  .strasse .mittellinie { stroke-width: 2; stroke-dasharray: 8 10; }
  .strasse .maske { stroke-width: 34; }
  .strasse .pfadtext { font-size: 8.5px; letter-spacing: .1em; }
  .ziel { padding-bottom: 140px; }
  .leiste { position: fixed; z-index: 20; left: 0; right: 0; bottom: 0; display: grid; grid-template-columns: 1.4fr 1fr 1fr; gap: 8px; padding: 10px 12px calc(10px + env(safe-area-inset-bottom)); background: var(--nacht); }
  .leiste a { display: grid; place-items: center; height: 48px; border-radius: 12px; color: var(--grund); font-weight: 700; text-decoration: none; border: 1.5px solid rgba(248, 248, 236, .25); }
  .leiste a:first-child { background: var(--gelb); color: var(--navy); border-color: var(--gelb); }
  .pult { bottom: 84px; right: 12px; }
}
@media (hover: none) { .knopf:hover { transform: none; box-shadow: 0 2px 0 var(--navy); } }

/* ---------- Reduzierte Bewegung: Straße liegt fertig da ---------- */
.reduziert .schild { transition: none; transform: none; }
.reduziert .knopf { transition: none; }
.reduziert .strasse .auto { display: none !important; }
</style>
</head>
<body>
<div class="welt" data-welt="strasse">
<header class="kopf">
  <a class="marke" href="#" aria-label="Fahrschule Fuldaschleife, Startseite">
    <svg viewBox="0 0 40 40" aria-hidden="true"><rect x="1.5" y="1.5" width="37" height="37" rx="10" fill="#ffd60a" stroke="#142b50" stroke-width="3"/><path d="M9 29 C 14 29 14 12 20 12 S 26 24 31 24" fill="none" stroke="#142b50" stroke-width="5" stroke-linecap="round"/></svg>
    <span><b>Fuldaschleife</b><small>Fahrschule · Kassel</small></span>
  </a>
  <nav aria-label="Hauptnavigation"><a href="#">Klassen</a><a href="#">Preise</a><a href="#ablauf" aria-current="page">Ablauf</a><a href="#">Fahrlehrer:innen</a></nav>
  <a class="knopf" href="#ziel">Jetzt anmelden</a>
</header>

<section class="hero">
  <div class="hero__text">
    <p class="klein ort">Fahrschule in Kassel-Wehlheiden · seit 1994</p>
    <h1>Dein Weg zum <mark>Führerschein.</mark></h1>
    <p class="lead">So läuft Klasse B bei uns ab, von der Anmeldung bis zur Prüfung. Im Schnitt dauert es vier bis sechs Monate, und niemand muss das allein schaffen.</p>
  </div>
  <aside class="karte" aria-label="Schnellwahl">
    <p class="klein">Deine Klasse</p>
    <div class="klassen" role="radiogroup" aria-label="Führerscheinklasse">
      <button type="button" role="radio" aria-checked="true">B</button>
      <button type="button" role="radio" aria-checked="false">B197</button>
      <button type="button" role="radio" aria-checked="false">A</button>
      <button type="button" role="radio" aria-checked="false">BE</button>
    </div>
    <dl>
      <div><dt>Nächster Theorie-Einstieg</dt><dd>Mo, 2. Nov., 18:00</dd></div>
      <div><dt>Grundbetrag</dt><dd>390 €</dd></div>
      <div><dt>Fahrstunde, 45 Min.</dt><dd>68 €</dd></div>
    </dl>
    <a class="knopf" href="#ziel">Weiter zur Anmeldung <span aria-hidden="true">→</span></a>
  </aside>
</section>

<section class="ablauf" id="ablauf" aria-labelledby="ablauf-titel">
  <svg class="strasse" aria-hidden="true" focusable="false">
    <defs>
      <mask id="strasse-maske" maskUnits="userSpaceOnUse"><path class="maske" pathLength="1" stroke-dasharray="1 1" stroke-dashoffset="1"/></mask>
    </defs>
    <path class="geist"/>
    <g mask="url(#strasse-maske)">
      <path class="rand"/>
      <path class="belag"/>
      <path class="mittellinie"/>
    </g>
    <path id="strasse-bahn" fill="none" stroke="none"/>
    <text class="pfadtext"><textPath href="#strasse-bahn" startOffset="0">Anmelden · Sehtest · Erste Hilfe · Theorie · Praxis · Prüfung · Bestanden →</textPath></text>
    <g class="auto"><g class="auto__koerper">
      <rect x="-21" y="-12" width="42" height="24" rx="8" fill="#fff" stroke="#142b50" stroke-width="2.5"/>
      <path d="M6 -9.5 h7 a4 4 0 0 1 4 4 v11 a4 4 0 0 1 -4 4 h-7z" fill="#3fbadf"/>
      <rect x="-9" y="-6" width="12" height="12" rx="2" fill="#ffd60a" stroke="#142b50" stroke-width="1.5"/>
      <rect x="-17" y="-9" width="5" height="18" rx="2" fill="#3fbadf"/>
    </g></g>
  </svg>

  <div class="ablauf__kopf">
    <h2 id="ablauf-titel">Fünf Etappen, ein Ziel</h2>
    <p>Die Straße zeigt, wo du gerade stehst. Jede Etappe klappt auch ohne sie: Hier steht alles, was du wissen musst.</p>
  </div>

  <ol class="etappen">
    <li class="etappe" data-seite="links">
      <div class="etappe__text">
        <p class="klein">Etappe 1 · ein Termin</p>
        <h3>Anmelden</h3>
        <p>Klasse wählen, Vertrag unterschreiben, Zugang zur Lern-App bekommen. Online oder bei uns in der Friedrich-Ebert-Straße.</p>
        <p class="fakt"><span>Grundbetrag 390 €</span><span>ab 17 Jahren, B197 ab 16½</span></p>
      </div>
      <div class="etappe__weg">
        <span class="schild" aria-hidden="true">1</span>
        <svg class="deko" viewBox="0 0 150 150" aria-hidden="true"><g fill="none" stroke="#142b50" stroke-width="2.5" stroke-linejoin="round"><rect x="22" y="58" width="106" height="76" fill="#eef0d8"/><path d="M14 58 h122 l-10 -22 h-102z" fill="#fff"/><path d="M28 36 v22 M46 36 v22 M64 36 v22 M82 36 v22 M100 36 v22 M118 36 v22"/><rect x="34" y="78" width="44" height="34" fill="#3fbadf"/><rect x="90" y="78" width="26" height="56" fill="#fff"/><rect x="36" y="12" width="78" height="20" rx="4" fill="#ffd60a"/></g><text x="75" y="25.5" text-anchor="middle" font-family="Atkinson Hyperlegible Next, sans-serif" font-weight="700" font-size="9" letter-spacing=".6" fill="#142b50">FAHRSCHULE</text></svg>
      </div>
    </li>
    <li class="etappe" data-seite="rechts">
      <div class="etappe__text">
        <p class="klein">Etappe 2 · ein Samstag</p>
        <h3>Sehtest und Erste Hilfe</h3>
        <p>Sehtest beim Optiker, Erste-Hilfe-Kurs an einem Tag. Mit beiden Nachweisen und einem Passfoto stellst du den Antrag beim Straßenverkehrsamt Kassel.</p>
        <p class="fakt"><span>9 Unterrichtseinheiten</span><span>Antrag 4–6 Wochen</span></p>
      </div>
      <div class="etappe__weg">
        <span class="schild" aria-hidden="true">2</span>
        <svg class="deko" viewBox="0 0 150 150" aria-hidden="true"><g stroke="#142b50" stroke-width="2.5"><rect x="36" y="10" width="78" height="128" rx="4" fill="#fff"/></g><g font-family="Bricolage Grotesque, sans-serif" font-weight="800" fill="#142b50" text-anchor="middle"><text x="75" y="48" font-size="30">E</text><text x="75" y="76" font-size="19" letter-spacing="3">F P</text><text x="75" y="98" font-size="13" letter-spacing="3">T O Z</text><text x="75" y="115" font-size="9" letter-spacing="3">L P E D</text><text x="75" y="128" font-size="6" letter-spacing="2">P E C F D</text></g></svg>
      </div>
    </li>
    <li class="etappe" data-seite="links">
      <div class="etappe__text">
        <p class="klein">Etappe 3 · sieben Wochen</p>
        <h3>Theorie</h3>
        <p>14 Doppelstunden: 12 Grundstoff, 2 für Klasse B. Montags und mittwochs um 18 Uhr, Einstieg jederzeit. Die Prüfung machst du beim TÜV in Kassel.</p>
        <p class="fakt"><span>30 Fragen</span><span>höchstens 10 Fehlerpunkte</span></p>
      </div>
      <div class="etappe__weg">
        <span class="schild" aria-hidden="true">3</span>
        <svg class="deko" viewBox="0 0 150 150" aria-hidden="true"><rect x="72" y="80" width="6" height="68" fill="#566175"/><g transform="rotate(45 75 54)"><rect x="40" y="19" width="70" height="70" rx="4" fill="#fff" stroke="#142b50" stroke-width="2.5"/><rect x="54" y="33" width="42" height="42" fill="#ffd60a" stroke="#142b50" stroke-width="2"/></g></svg>
      </div>
    </li>
    <li class="etappe" data-seite="rechts">
      <div class="etappe__text">
        <p class="klein">Etappe 4 · nach deinem Tempo</p>
        <h3>Praxis</h3>
        <p>Übungsstunden, so viele du brauchst, dazu zwölf Sonderfahrten: fünf über Land, vier auf der A 7 und A 49, drei bei Dunkelheit. Schaltung oder Automatik.</p>
        <p class="fakt"><span>Fahrstunde 45 Min · 68 €</span><span>Sonderfahrt 82 €</span></p>
      </div>
      <div class="etappe__weg">
        <span class="schild" aria-hidden="true">4</span>
        <svg class="deko" viewBox="0 0 150 150" aria-hidden="true"><rect x="72" y="92" width="6" height="56" fill="#566175"/><rect x="30" y="16" width="90" height="80" rx="6" fill="#1f4f9a" stroke="#142b50" stroke-width="2.5"/><g fill="none" stroke="#fff" stroke-width="4" stroke-linecap="round"><path d="M44 70 h62"/><path d="M58 70 v-22 a17 17 0 0 1 34 0 v22"/><path d="M68 84 L64 92 M82 84 L86 92" stroke-width="3"/></g></svg>
      </div>
    </li>
    <li class="etappe" data-seite="links">
      <div class="etappe__text">
        <p class="klein">Etappe 5 · 55 Minuten</p>
        <h3>Prüfung</h3>
        <p>Praktische Prüfung mit Start an unserer Filiale. Deine Fahrlehrerin sitzt neben dir, der Prüfer hinten. Bestanden? Dann bekommst du den Führerschein direkt in die Hand.</p>
        <p class="fakt"><span>Start in Wehlheiden</span><span>Führerschein direkt dabei</span></p>
      </div>
      <div class="etappe__weg">
        <span class="schild" aria-hidden="true">5</span>
        <svg class="deko" viewBox="0 0 150 150" aria-hidden="true"><path d="M0 140 C 30 112 60 104 75 104 S 120 112 150 140z" fill="#eef0d8" stroke="#142b50" stroke-width="2.5"/><g fill="#fff" stroke="#142b50" stroke-width="2.5" stroke-linejoin="round"><path d="M48 106 v-26 h54 v26z"/><path d="M58 80 l17 -30 l17 30z"/><path d="M73 50 v-14 h4 v14z"/><circle cx="75" cy="31" r="5"/></g><g fill="#142b50"><rect x="56" y="88" width="6" height="12"/><rect x="72" y="88" width="6" height="12"/><rect x="88" y="88" width="6" height="12"/></g></svg>
      </div>
    </li>
  </ol>

  <div class="ziel" id="ziel">
    <svg class="ziel__fahne" viewBox="0 0 72 72" aria-hidden="true"><rect x="12" y="6" width="4" height="62" fill="#142b50"/><g transform="translate(16 8)"><rect width="44" height="30" fill="#fff" stroke="#142b50" stroke-width="2.5"/><g fill="#142b50"><rect x="0" y="0" width="11" height="10"/><rect x="22" y="0" width="11" height="10"/><rect x="11" y="10" width="11" height="10"/><rect x="33" y="10" width="11" height="10"/><rect x="0" y="20" width="11" height="10"/><rect x="22" y="20" width="11" height="10"/></g></g></svg>
    <h2>Bestanden. <br>Und los.</h2>
    <p>Anmeldung dauert zehn Minuten. Wir melden uns innerhalb eines Werktags mit deinem ersten Fahrtermin.</p>
    <div class="knoepfe"><a class="knopf" href="#">Klasse B anmelden</a><a class="knopf knopf--leise" href="#">0561 739 20 41</a></div>
  </div>
</section>

<nav class="leiste" aria-label="Schnellzugriff"><a href="#ziel">Anmelden</a><a href="#">Anrufen</a><a href="#">WhatsApp</a></nav>
</div>

<div class="welt wa" data-welt="anker" hidden>
<header class="wa-kopf">
  <a class="wa-marke" href="#" aria-label="Randskizze, Startseite">
    <svg viewBox="0 0 40 40" aria-hidden="true"><rect x="1.5" y="1.5" width="37" height="37" rx="11" fill="#3a50c8"/><path d="M9 27 C 13 17 18 30 22 19 S 29 13 31 15" fill="none" stroke="#fff" stroke-width="3.2" stroke-linecap="round"/><circle cx="31" cy="15" r="2.6" fill="#10bf7a"/></svg>
    <span><b>Randskizze</b><small>Sketchnote-Kurs · online</small></span>
  </a>
  <nav aria-label="Kursnavigation"><a href="#wa-lernweg">Lernweg</a><a href="#wa-person">Trainerin</a><a href="#wa-preis">Preis</a></nav>
  <a class="wa-knopf" href="#wa-preis">Platz sichern</a>
</header>
<main class="wa-seite">
  <svg class="wa-linie" aria-hidden="true" focusable="false"><path class="wa-strich wa-strich--zwei"/><path class="wa-strich"/><path class="wa-pfeil"/><circle class="wa-spitze" r="4.5" cx="-20" cy="-20"/></svg>

  <section class="wa-block wa-hero" aria-labelledby="wa-titel">
    <p class="wa-label">Online-Kurs · 4 Wochen · Start Mo, 16. November</p>
    <h1 id="wa-titel">Notizen, die du <span data-anker="strich">wieder liest.</span></h1>
    <p class="wa-lead">Sketchnotes für Meetings, Vorträge und den eigenen Kopf. Zwei Abende pro Woche, live mit Ines Wohlleben, für alle, die sagen: Ich kann nicht zeichnen.</p>
    <ul class="wa-fakten"><li><b>8</b> Live-Abende à 90 Min.</li><li>höchstens <b>24</b> Plätze</li><li><b>290 €</b> inkl. MwSt.</li></ul>
    <div class="wa-knoepfe"><a class="wa-knopf" href="#wa-preis">Platz sichern</a><a class="wa-knopf wa-knopf--kontur" href="#wa-lernweg">Lernweg ansehen</a></div>
    <p class="wa-notiz wa-hand" data-bei="0" aria-hidden="true">hier fängt's an, mit einem Strich</p>
  </section>

  <section class="wa-block wa-koennen" aria-labelledby="wa-k-titel">
    <h2 id="wa-k-titel">Was du nach vier Wochen kannst</h2>
    <ol class="wa-liste">
      <li><span class="wa-zahl">eins</span><h3>Ein Meeting auf einer Seite</h3><p>Du hörst zu, sortierst mit und hast am Ende ein Blatt, das auch die Kollegin versteht, die nicht dabei war.</p></li>
      <li><span class="wa-zahl">zwei</span><h3>Alles aus <b data-anker="kreis" data-aus="0.26">12 Grundformen</b></h3><p>Menschen, Pfeile, Rahmen, Symbole: Wir bauen sie aus Kreis, Linie und Dreieck. Talent brauchst du dafür nicht.</p></li>
      <li><span class="wa-zahl">drei</span><h3>Eine Bildsprache fürs Team</h3><p>Du legst ein eigenes Symbol-Set an, das dein Team auf Whiteboards und in Folien wiederverwendet.</p></li>
    </ol>
    <p class="wa-notiz wa-hand" data-bei="1" aria-hidden="true">mehr braucht's wirklich nicht</p>
  </section>

  <section class="wa-block wa-lernweg" id="wa-lernweg" aria-labelledby="wa-l-titel">
    <h2 id="wa-l-titel">Der Lernweg</h2>
    <p class="wa-unter">Dienstags und donnerstags, 19 bis 20:30 Uhr. Jeder Abend wird aufgezeichnet, falls du einmal nicht kannst.</p>
    <ol class="wa-module">
      <li class="wa-modul"><span class="wa-nr" data-anker="kreis">01</span><div><p class="wa-hand">Modul 01 · 2 Abende</p><h3>Strich, Form, Schrift</h3><p>Sichere Linien, die 12 Grundformen und eine Druckschrift, die auch in Eile lesbar bleibt.</p><p class="wa-ergebnis"><span aria-hidden="true">✓</span> Danach: dein erstes Symbol-Alphabet</p></div></li>
      <li class="wa-modul"><span class="wa-nr" data-anker="kreis">02</span><div><p class="wa-hand">Modul 02 · 2 Abende</p><h3>Menschen und Pfeile</h3><p>Figuren mit Haltung, Pfeile mit Richtung, Rahmen mit Gewicht. Wir üben an echten Gesprächen.</p><p class="wa-ergebnis"><span aria-hidden="true">✓</span> Danach: eine Person in fünf Sekunden</p></div></li>
      <li class="wa-modul"><span class="wa-nr" data-anker="kreis">03</span><div><p class="wa-hand">Modul 03 · 2 Abende</p><h3>Struktur auf dem Blatt</h3><p>Raster, Pfad oder Popcorn: drei Layouts für Vortrag, Meeting und Workshop.</p><p class="wa-ergebnis"><span aria-hidden="true">✓</span> Danach: ein ganzes Meeting auf einer Seite</p></div></li>
      <li class="wa-modul"><span class="wa-nr" data-anker="kreis">04</span><div><p class="wa-hand">Modul 04 · 2 Abende</p><h3>Dein Symbol-Set</h3><p>Gemeinsam baust du ein Set für deinen Arbeitsalltag. Ines gibt dir Rückmeldung zu deinem Abschlussblatt.</p><p class="wa-ergebnis"><span aria-hidden="true">✓</span> Danach: Vorlage als PDF für dein Team</p></div></li>
    </ol>
  </section>

  <section class="wa-block wa-person" id="wa-person" aria-labelledby="wa-p-titel">
    <svg viewBox="0 0 120 120" aria-hidden="true" data-anker="kreis"><circle cx="60" cy="60" r="58" fill="#e3e8f8"/><g fill="none" stroke="#141414" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round"><path d="M38 52 C 36 30 52 22 64 24 C 80 26 88 40 84 56"/><circle cx="61" cy="58" r="20" fill="#fff"/><path d="M53 56 h0.1 M68 56 h0.1" stroke-width="4"/><path d="M54 66 C 58 70 64 70 68 66"/><path d="M30 112 C 34 92 46 84 61 84 C 76 84 88 92 92 112"/><path d="M84 40 L 96 28" stroke="#3a50c8"/></g></svg>
    <div>
      <h2 id="wa-p-titel">Ines Wohlleben</h2>
      <p>Grafikerin aus Rinteln. Zeichnet seit 2014 Protokolle für Tagungen und Verwaltungen und unterrichtet seit 2019 online. Bisher 1.140 Teilnehmende in 38 Kursrunden.</p>
      <blockquote>„Früher habe ich mitgeschrieben, heute höre ich zu und zeichne mit. Unser Protokoll passt jetzt auf eine Seite.“<cite>Malte R., Projektleiter in einem Planungsbüro, Kurs im März 2026</cite></blockquote>
    </div>
  </section>

  <section class="wa-block wa-preis" id="wa-preis" aria-labelledby="wa-pr-titel">
    <div class="wa-preisbox">
      <p class="wa-hand">Nächste Runde · 16. November bis 10. Dezember</p>
      <h2 id="wa-pr-titel">Ein Preis, alles drin</h2>
      <p class="wa-betrag"><span data-anker="kreis">290 €</span><small>inkl. MwSt., auch in drei Raten</small></p>
      <ul class="wa-drin"><li>8 Live-Abende und alle Aufzeichnungen</li><li>Rückmeldung zu deinem Abschlussblatt</li><li>Symbol-Vorlage als PDF</li><li>Aufzeichnungen 12 Monate abrufbar</li></ul>
      <div class="wa-cta"><a class="wa-knopf" href="#" data-anker="pfeil">Platz sichern</a><span class="wa-stempel" aria-hidden="true">✓ Lernweg komplett</span></div>
    </div>
  </section>
  <footer class="wa-block wa-fuss"><p>Randskizze · Ines Wohlleben · Rinteln</p><p><a href="#">Impressum</a><a href="#">Datenschutz</a><a href="#">AGB</a></p></footer>
</main>
</div>

<div class="pult">
  <div class="pult__knoepfe" role="group" aria-label="Variante">
    <button type="button" data-variante="strasse" aria-pressed="true">Straße</button>
    <button type="button" data-variante="anker" aria-pressed="false">Handlinie, Anker</button>
  </div>
  <div class="pult__knoepfe" role="group" aria-label="Darstellung" data-nur="strasse">
    <button type="button" data-modus="linie" aria-pressed="true">Nur Linie</button>
    <button type="button" data-modus="auto" aria-pressed="false">Mit Auto</button>
    <button type="button" data-modus="text" aria-pressed="false">Text am Pfad</button>
  </div>
  <p class="pult__werte" data-werte>Fortschritt 0,00 · dashoffset 1,00 · linear</p>
</div>

<script>
(() => {
  const html = document.documentElement;
  const reduziert = () => html.classList.contains('reduziert');
  const ablauf = document.querySelector('.ablauf');
  const svg = ablauf.querySelector('.strasse');
  const pfade = [...svg.querySelectorAll('.geist, .rand, .belag, .mittellinie, .maske, #strasse-bahn')];
  const maske = svg.querySelector('.maske');
  const bahn = svg.querySelector('#strasse-bahn');
  const textPath = svg.querySelector('textPath');
  const auto = svg.querySelector('.auto');
  const schilder = [...ablauf.querySelectorAll('.schild')];
  const zielEl = ablauf.querySelector('.ziel__fahne');
  const werte = document.querySelector('[data-werte]');
  const schmal = matchMedia('(max-width: 760px)');
  const SPITZE = 0.7;             // Spitze der Straße folgt 70 % der Viewport-Höhe
  let L = 1, proben = [], wegpunkte = [], textLaenge = 0, modus = 'linie', geplant = false, uhr = null, variante = 'strasse';

  // ---------- Variante anker: Handlinie läuft von Anker zu Anker (echte Elemente mit data-anker) ----------
  const waSeite = document.querySelector('.wa-seite'), waSvg = waSeite.querySelector('.wa-linie');
  const waStriche = [...waSvg.querySelectorAll('.wa-strich')], waPfeil = waSvg.querySelector('.wa-pfeil'), waSpitze = waSvg.querySelector('.wa-spitze');
  const waAnker = [...waSeite.querySelectorAll('[data-anker]')], waNotizen = [...waSeite.querySelectorAll('[data-bei]')], waStempel = waSeite.querySelector('.wa-stempel');
  const RAU = [1.6, 2.4];                 // Rauheit in px: Hauptstrich, Begleitstrich
  const DREH = 1.12, SPIRALE = 0.07;      // Kreise: 1,12 Umdrehungen, Radius wächst um 7 %
  let aSeg = [], aPunkte = [], aKum = [], aL = 0;
  function bez(p0, p1, p2, p3) {
    const d = (a, b) => Math.hypot(b[0] - a[0], b[1] - a[1]);
    const n = Math.max(8, Math.ceil((d(p0, p1) + d(p1, p2) + d(p2, p3)) / 4)), out = [];
    for (let i = 1; i <= n; i++) {
      const t = i / n, u = 1 - t, a = u * u * u, b = 3 * u * u * t, c = 3 * u * t * t, e = t * t * t;
      out.push([a * p0[0] + b * p1[0] + c * p2[0] + e * p3[0], a * p0[1] + b * p1[1] + c * p2[1] + e * p3[1]]);
    }
    return out;
  }
  function baueAnker() {
    const box = waSeite.getBoundingClientRect(), W = waSeite.clientWidth, H = waSeite.offsetHeight, mobil = schmal.matches;
    waSvg.setAttribute('viewBox', `0 0 ${W} ${H}`); waSvg.style.height = H + 'px';
    const rel = el => { const r = el.getBoundingClientRect(); return { l: r.left - box.left, r: r.right - box.left, t: r.top - box.top, b: r.bottom - box.top, cx: r.left + r.width / 2 - box.left, cy: r.top + r.height / 2 - box.top, w: r.width, h: r.height }; };
    aSeg = []; let pos = null;
    const neu = (pts, ya, yb, i) => { aSeg.push({ pts, ya, yb, i }); pos = pts[pts.length - 1]; };
    // Übergang: Bézier von Anker zu Anker, schwingt abwechselnd seitlich aus
    // Übergang: fällt erst senkrecht ab und quert dann im Abstand zwischen den Blöcken.
    // data-aus am Ziel-Anker = seitlicher Schwung als Anteil der Breite (+ rechts, - links). Telefon: Spur am linken Rand.
    const verbinde = (ziel, yb, i = -1, el = null) => {
      const dy = Math.max(80, ziel[1] - pos[1]);
      if (mobil) {
        const spur = 6, a = [spur, pos[1] + 70], b = [spur, Math.max(a[1], ziel[1] - 70)], pts = bez(pos, [pos[0], pos[1] + 45], [spur, pos[1] + 25], a);
        for (let y = a[1] + 4; y < b[1]; y += 4) pts.push([spur, y]);
        neu(pts.concat(bez(b, [spur, b[1] + 45], [ziel[0] - 30, ziel[1] - 10], ziel)), pos[1], yb, i); return;
      }
      const aus = el && el.dataset.aus ? +el.dataset.aus * W : -Math.sign(ziel[0] - pos[0]) * 30;
      neu(bez(pos, [pos[0] + aus, pos[1] + dy * 0.62], [ziel[0] + aus * 0.4, ziel[1] - dy * 0.3], ziel), pos[1], yb, i);
    };
    waAnker.forEach((el, i) => {
      const r = rel(el), art = el.dataset.anker;
      if (art === 'strich') {            // Unterstreichung, leicht gewellt
        const a = [r.l - 8, r.b + 6], e = [r.r + 12, r.b + 2];
        if (pos) verbinde(a, a[1], -1, el); else pos = a;
        neu([a, ...bez(a, [r.l + r.w * 0.3, r.b + 12], [r.l + r.w * 0.7, r.b - 2], e)], r.b - innerHeight * 0.6, r.b, i);
      } else if (art === 'kreis') {      // Schlaufe um das Element, endet nicht dort, wo sie begann
        const pad = mobil ? 10 : 16, rx = r.w / 2 + pad, ry = r.h / 2 + pad * 0.8;
        const ref = mobil ? [6, r.cy - ry] : pos, w0 = Math.atan2((ref[1] - r.cy) / ry, (ref[0] - r.cx) / rx);   // beginnt auf der Seite, von der die Linie kommt
        const punkt = f => { const w = w0 + f * DREH * 2 * Math.PI, g = 1 + SPIRALE * f; return [r.cx + Math.cos(w) * rx * g, r.cy + Math.sin(w) * ry * g + 4 * f]; };
        verbinde(punkt(0), r.t - pad, -1, el);
        const n = Math.ceil(2 * Math.PI * Math.max(rx, ry) * DREH / 4), pts = [];
        for (let j = 1; j <= n; j++) pts.push(punkt(j / n));
        neu(pts, r.t - pad, r.b + pad, i);
      } else if (art === 'pfeil') {      // läuft auf den Knopf zu, Pfeilspitze erst am Ende
        verbinde(mobil ? [r.l + Math.min(40, r.w / 2), r.t - 12] : [r.l - 16, r.cy], r.cy, i, el);
      }
    });
    let vor = -Infinity;                 // Scroll-Fenster der Abschnitte streng aufsteigend
    aSeg.forEach(s => { s.ya = Math.max(s.ya, vor); s.yb = Math.max(s.yb, s.ya + 1); vor = s.yb; });
    aPunkte = []; aKum = []; let l = 0;
    aSeg.forEach(s => { s.l0 = l; s.pts.forEach(p => { const v = aPunkte[aPunkte.length - 1]; if (v) l += Math.hypot(p[0] - v[0], p[1] - v[1]); aPunkte.push(p); aKum.push(l); }); s.l1 = l; });
    aL = l;
    // Rauheit: Versatz quer zur Laufrichtung aus drei Sinuswellen (fester Seed, zwei Striche)
    waStriche.forEach((pfad, k) => {
      const ph = k ? [2.1, 4.7, 0.9] : [0.3, 1.9, 3.3];
      const d = aPunkte.map((p, j) => {
        const a = aPunkte[Math.max(0, j - 1)], b = aPunkte[Math.min(aPunkte.length - 1, j + 1)];
        let nx = a[1] - b[1], ny = b[0] - a[0]; const m = Math.hypot(nx, ny) || 1; nx /= m; ny /= m;
        const s = aKum[j], o = RAU[k] * (0.6 * Math.sin(s * 0.031 + ph[0]) + 0.3 * Math.sin(s * 0.083 + ph[1]) + 0.1 * Math.sin(s * 0.21 + ph[2]));
        return (j ? 'L' : 'M') + (p[0] + nx * o).toFixed(1) + ' ' + (p[1] + ny * o).toFixed(1);
      }).join('');
      pfad.setAttribute('d', d); pfad.setAttribute('pathLength', aL.toFixed(1)); pfad.style.strokeDasharray = `${aL.toFixed(1)} ${aL.toFixed(1)}`;
    });
    const e = aPunkte[aPunkte.length - 1], v = aPunkte[Math.max(0, aPunkte.length - 4)], w = Math.atan2(e[1] - v[1], e[0] - v[0]);
    const spitze = s => [e[0] - 15 * Math.cos(w + s), e[1] - 15 * Math.sin(w + s)];
    const [p1, p2] = [spitze(0.5), spitze(-0.5)];
    waPfeil.setAttribute('d', `M${p1[0].toFixed(1)} ${p1[1].toFixed(1)}L${e[0].toFixed(1)} ${e[1].toFixed(1)}L${p2[0].toFixed(1)} ${p2[1].toFixed(1)}`);
    zeichneAnker();
  }
  function zeichneAnker() {
    if (!aSeg.length) return;
    const max = document.documentElement.scrollHeight - innerHeight;
    const y = reduziert() || (max > 0 && scrollY >= max - 2) ? Infinity : innerHeight * SPITZE - waSeite.getBoundingClientRect().top;
    let l = 0;
    for (const s of aSeg) {
      if (y < s.ya) { l = s.l0; break; }
      if (y <= s.yb) { l = s.l0 + (s.l1 - s.l0) * (y - s.ya) / (s.yb - s.ya); break; }
      l = s.l1;
    }
    waStriche.forEach(p => { p.style.strokeDashoffset = (aL - l).toFixed(1); });
    const fertig = l >= aL - 0.5;
    waPfeil.classList.toggle('da', fertig); waStempel.classList.toggle('da', fertig);
    let j = 0, hi = aKum.length - 1;
    while (j < hi) { const m = (j + hi) >> 1; if (aKum[m] < l) j = m + 1; else hi = m; }
    const p = aPunkte[j];
    waSpitze.setAttribute('cx', p[0].toFixed(1)); waSpitze.setAttribute('cy', p[1].toFixed(1));
    waSpitze.style.opacity = l > 1 && !fertig ? 1 : 0;
    waNotizen.forEach(n => { const s = aSeg.find(x => x.i === +n.dataset.bei); n.classList.toggle('da', !!s && l >= s.l0 + (s.l1 - s.l0) * 0.5); });
    const erreicht = aSeg.filter(s => s.i >= 0 && l >= s.l1 - 1).length;
    werte.textContent = `Fortschritt ${zahl(aL ? l / aL : 0)} · Anker ${erreicht}/${waAnker.length} · Rauheit ${String(RAU[0]).replace('.', ',')} px`;
  }

  // Pfad aus den echten Positionen der Schilder bauen: senkrecht durch jede Etappe, im Abstand dazwischen eine S-Kurve
  function baue() {
    if (variante === 'anker') { baueAnker(); return; }
    const box = ablauf.getBoundingClientRect();
    const W = ablauf.clientWidth, H = ablauf.offsetHeight;
    const mobil = matchMedia('(max-width: 760px)').matches;
    svg.setAttribute('viewBox', `0 0 ${W} ${H}`);
    svg.style.height = H + 'px';
    const rel = el => { const r = el.getBoundingClientRect(); return { x: r.left + r.width / 2 - box.left, y: r.top + r.height / 2 - box.top, oben: r.top - box.top, unten: r.bottom - box.top }; };
    wegpunkte = schilder.map(s => ({ ...rel(s), zeile: rel(s.closest('.etappe')) }));
    const z = rel(zielEl);
    const r = n => Math.round(n * 10) / 10;
    const bogen = (x1, y1, x2, y2, aus) => {
      const m = (y1 + y2) / 2, k = (y2 - y1) * 0.5;
      return mobil
        ? ` C ${r(x1 + aus)} ${r(y1 + k * 0.6)} ${r(x2 + aus)} ${r(y2 - k * 0.6)} ${r(x2)} ${r(y2)}`
        : ` C ${r(x1)} ${r(m)} ${r(x2)} ${r(m)} ${r(x2)} ${r(y2)}`;
    };
    const s0 = wegpunkte[0];
    const karte = document.querySelector('.karte');
    const startX = mobil || !karte ? s0.x : rel(karte).x;
    let d = `M ${r(startX)} 0` + bogen(startX, 0, s0.x, s0.zeile.oben - 10, 0);
    wegpunkte.forEach((p, i) => {
      const unten = p.zeile.unten + 10;
      d += ` L ${r(p.x)} ${r(unten)}`;
      const n = wegpunkte[i + 1];
      if (n) d += bogen(p.x, unten, n.x, n.zeile.oben - 10, i % 2 ? -18 : 22);
    });
    const last = wegpunkte[wegpunkte.length - 1];
    const zielY = z.oben - 18;
    d += bogen(last.x, last.zeile.unten + 10, z.x, zielY, 18);
    pfade.forEach(p => p.setAttribute('d', d));
    // Länge und y-Werte entlang des Pfads abtasten (Pfad läuft monoton nach unten)
    L = bahn.getTotalLength();
    const n = Math.max(200, Math.ceil(L / 6));
    proben = [];
    for (let i = 0; i <= n; i++) { const l = L * i / n; proben.push([l, bahn.getPointAtLength(l).y]); }
    textLaenge = textPath.getComputedTextLength ? textPath.getComputedTextLength() : 0;
    zeichne();
  }
  function laengeBei(y) {
    if (y <= proben[0][1]) return 0;
    let lo = 0, hi = proben.length - 1;
    if (y >= proben[hi][1]) return L;
    while (hi - lo > 1) { const m = (lo + hi) >> 1; if (proben[m][1] < y) lo = m; else hi = m; }
    const [l0, y0] = proben[lo], [l1, y1] = proben[hi];
    return l0 + (l1 - l0) * (y1 === y0 ? 0 : (y - y0) / (y1 - y0));
  }
  const zahl = v => v.toFixed(2).replace('.', ',');
  function zeichne() {
    geplant = false;
    if (variante === 'anker') { zeichneAnker(); return; }
    if (!proben.length) return;
    const oben = ablauf.getBoundingClientRect().top;
    const y = reduziert() ? Infinity : innerHeight * SPITZE - oben;
    const l = laengeBei(y), p = L ? l / L : 0;
    maske.style.strokeDashoffset = String(1 - p);       // pathLength="1": Länge normiert, linear an den Scroll gekoppelt
    schilder.forEach((s, i) => s.classList.toggle('erreicht', y >= wegpunkte[i].y - 6));
    if (modus === 'auto' && !reduziert()) {
      const a = bahn.getPointAtLength(Math.max(0, Math.min(L, l))), b = bahn.getPointAtLength(Math.min(L, l + 2));
      const c = l >= L ? bahn.getPointAtLength(L - 2) : null;
      const winkel = c ? Math.atan2(a.y - c.y, a.x - c.x) : Math.atan2(b.y - a.y, b.x - a.x);
      auto.setAttribute('transform', `translate(${a.x.toFixed(1)} ${a.y.toFixed(1)}) rotate(${(winkel * 180 / Math.PI).toFixed(1)}) scale(${schmal.matches ? 0.6 : 1})`);
      auto.style.opacity = p > 0.002 ? 1 : 0;
    }
    if (modus === 'text') textPath.setAttribute('startOffset', (l - textLaenge - (reduziert() ? 0 : 36)).toFixed(1));
    werte.textContent = `Fortschritt ${zahl(p)} · dashoffset ${zahl(1 - p)} · linear`;
  }
  const planen = () => { if (!geplant) { geplant = true; requestAnimationFrame(zeichne); } };
  addEventListener('scroll', planen, { passive: true });
  let rt; addEventListener('resize', () => { clearTimeout(rt); rt = setTimeout(baue, 120); });

  function setzeModus(m) {
    modus = m;
    html.classList.remove('m-linie', 'm-auto', 'm-text'); html.classList.add('m-' + m);
    document.querySelectorAll('[data-modus]').forEach(b => b.setAttribute('aria-pressed', String(b.dataset.modus === m)));
    textLaenge = textPath.getComputedTextLength();
    zeichne();
  }
  document.querySelectorAll('[data-modus]').forEach(b => b.addEventListener('click', () => setzeModus(b.dataset.modus)));
  function setzeVariante(v, url) {
    variante = v === 'anker' ? 'anker' : 'strasse';
    document.querySelectorAll('[data-welt]').forEach(w => { w.hidden = w.dataset.welt !== variante; });
    document.querySelectorAll('[data-nur]').forEach(g => { g.hidden = g.dataset.nur !== variante; });
    document.querySelectorAll('[data-variante]').forEach(b => b.setAttribute('aria-pressed', String(b.dataset.variante === variante)));
    html.classList.toggle('v-anker', variante === 'anker');
    if (url) { const q = new URLSearchParams(location.search); q.set('v', variante); history.replaceState(null, '', '?' + q.toString().replace(/=(?=&|$)/g, '')); scrollTo(0, 0); }
    baue();
  }
  document.querySelectorAll('[data-variante]').forEach(b => b.addEventListener('click', () => setzeVariante(b.dataset.variante, true)));
  document.querySelectorAll('.klassen button').forEach(b => b.addEventListener('click', () => document.querySelectorAll('.klassen button').forEach(x => x.setAttribute('aria-checked', String(x === b)))));

  // Vorführung: Seite fährt langsam die ganze Strecke ab und springt zurück
  const RUNDE = 12000;
  function vorfuehren() {
    pause();
    scrollTo(0, 0);
    const t0 = performance.now();
    const schritt = t => {
      const q = ((t - t0) % RUNDE) / RUNDE;
      const max = document.documentElement.scrollHeight - innerHeight;
      const f = Math.min(1, Math.max(0, (q - 0.06) / 0.82));
      scrollTo(0, max * (0.5 - 0.5 * Math.cos(f * Math.PI)));
      uhr = requestAnimationFrame(schritt);
    };
    uhr = requestAnimationFrame(schritt);
  }
  function pause() { cancelAnimationFrame(uhr); uhr = null; }
  function weiter() { if (html.classList.contains('vorschau') && !uhr) vorfuehren(); }
  function neustart() { pause(); scrollTo(0, 0); zeichne(); }
  ['wheel', 'touchstart', 'keydown'].forEach(t => addEventListener(t, () => { if (uhr && !html.classList.contains('vorschau')) pause(); }, { passive: true }));

  document.addEventListener('DOMContentLoaded', () => { setzeModus('linie'); setzeVariante(new URLSearchParams(location.search).get('v')); });
  document.fonts && document.fonts.ready.then(baue);
  addEventListener('load', baue);

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