Zum Inhalt
← Scroll & Section-Animationen

Kapitel-Kante als Signatur

Die Kante zwischen zwei Kapiteln trägt die Leitidee: ein Bleistiftstrich, eine Wölbung wie ein Atemzug, eine Treppe mit Höhenkote oder eine Uhrzeit, die über Nacht weiterläuft, gezeichnet vom Scroll.

Handwerk, Tischlerei, Malerbetrieb, Gebäudedienste: ein Werkzeugstrich (Bleistift, Kreide, Abzieher) zieht die Kapitelgrenze, Kapitel auf gleichem Grund.

Die Kante zwischen zwei Kapiteln trägt die Leitidee der Marke über die ganze Seite. Bei der Tischlerei ist es ein Bleistiftstrich mit Anrissmarke, bei der Bäckerei eine Uhrzeit, die über Nacht weiterläuft, beim Bauunternehmen eine Treppe mit Höhenkote, beim Yogastudio eine Fläche, die sich wie ein Atemzug aufwölbt. Die Kante entsteht beim Scrollen (scroll-getrieben, mit Rückfall für ältere Browser), trägt auf Wunsch ein Kapitel-Wort oder eine Uhrzeit und sieht auf jeder Seite gleich aus. So wird aus einer Idee im Hero ein Ordnungsprinzip.

Eine Leitidee, die nur im Hero steht, ist nach dem ersten Bildschirm vergessen. Kehrt dieselbe Form an jeder Kapitelgrenze wieder, wird sie zur Handschrift: Man erkennt die Seite an der Kante wieder, nicht an einer Karte. Die Bewegung ist an den Scroll gebunden und damit an die Lesegeschwindigkeit. Sie zeigt den Übergang genau dann, wenn er passiert, und steht danach still. Trägt die Kante eine Station oder Uhrzeit, ordnet sie den Inhalt zusätzlich als Geschichte (erst das, dann das).

Wann einsetzen

Wenn das Rezept oder die Probe eine Leitidee hat, die eine Form hergibt: Strich, Wölbung, Stufe, Uhrzeit, Station. Für Startseiten und Kapitelseiten mit drei bis sechs Kapiteln. Handwerk, Bäckerei und Lebensmittelhandwerk, Bau und Immobilien, Praxis und Yoga, Weingut, Gebäudedienste. Die Variante zeitachse für Betriebe mit einem Tagesablauf oder Arbeitsgang (Backstube, Brauerei, Pflege, Hofladen).

Wann nicht

Nicht als Schmuck zwischen jeder Sektion einer langen Seite: höchstens eine Kante je Kapitelgrenze, auf Unterseiten sparsam. Nicht in Formularen, Preislisten und Rechtstexten. Nicht, wenn die Form nichts bedeutet; dann reicht sektionskante-shape als stille Kante ohne Bewegung. Die Kante schneidet nie Überschriften oder Bilder an: der Abschnitt davor braucht unten so viel Abstand, wie die Kante hoch ist. Keine erfundenen Uhrzeiten oder Arbeitsschritte, die Abläufe kommen vom Betrieb.

Varianten

  • linie: ein gezeichneter Strich (Bleistift, Kreide, Abzieher) läuft beim Scrollen über die Breite, an der Spitze ein kleines Werkzeug, mit Anrissmarke und Kapitel-Wort
  • woelbung: die Fläche des nächsten Kapitels wölbt sich auf (Atemzug, Laib, Kissen), mit kurzem Überschwingen und Kapitel-Wort auf dem Scheitel
  • stufe: die Kante tritt Stufe für Stufe heraus (Treppe, Raster, Terrasse), an der obersten Stufe eine Höhenkote
  • zeitachse: jede Kante trägt eine fortlaufende Uhrzeit oder Station, eine mitlaufende Uhr zählt zwischen den Kapiteln weiter, die Flächen wechseln von Nacht zu Morgen

Umsetzung

  1. Kante als eigenes Element am Anfang jedes Kapitels (<div class="kante" aria-hidden="true"> plus ein sichtbares Kapitel-Wort als echter Text). Sie ragt mit negativem Abstand in den Abschnitt davor; der Abschnitt davor hat unten mindestens Kantenhöhe plus Luft.
  2. Ein registrierter Fortschritt @property --kp (<number>, 0 bis 1). Er wird per animation: kante-kp linear both; animation-timeline: view(); animation-range-start: cover 5%; animation-range-end: cover 50% gesetzt. Wichtig: animation-range immer als Langform und nach der Kurzschreibweise animation, sonst setzt animation sie auf normal zurück.
  3. Alle Teile der Kante rechnen nur aus --kp: stroke-dashoffset (linie), transform: scaleY (woelbung), scaleY je Stufe mit clamp(0, --kp * n − i, 1) (stufe), Strichlänge und Uhrzeit (zeitachse). Nur transform, opacity und stroke, kein Layout.
  4. Rückfall ohne animation-timeline: ein passiver Scroll-Listener mit requestAnimationFrame rechnet denselben Fortschritt aus getBoundingClientRect und setzt --kp je Kante.
  5. Reduzierte Bewegung: --kp: 1, die Kante steht fertig da.
  6. zeitachse: die mitlaufende Uhr liest den Fortschritt der Kapitel und schreibt die Zeit per textContent, aria-hidden; die echte Uhrzeit steht als <time> in jeder Kante.

Robust umsetzen

Telefon. Gleiche Kante, flacher: Höhe per clamp() mit der Breite. Kapitel-Wort und Uhrzeit bleiben lesbar (mindestens 0,875 rem). Der Scroll-Bereich hängt an der Bildschirmhöhe, die Kante ist also auch am Telefon fertig, bevor sie die Mitte erreicht.

Ohne Bewegung. Die Kante steht fertig da (--kp: 1), ohne Zeichnen, Wölben oder Stufen. Die Uhr zeigt die Uhrzeit des Kapitels.

Barrierefrei. Die Form ist Dekoration (aria-hidden). Kapitel-Wort und Uhrzeit sind echter Text, der jederzeit vollständig im DOM steht und nach dem Bereich ganz sichtbar ist. Überschriften liegen nie unter der Kante. Kontrastmodus: Kante als 2 px CanvasText-Linie.

Code

<!DOCTYPE html>
<!--
  Kapitel-Kante als Signatur
  Fundus · Scroll & Section-Animationen · https://cecodesign.de/lab/fundus/scroll/kapitel-kante-signatur/
  Eigenständige Datei. Bilder liegen hier unter bilder/ – eigene einsetzen.
-->
<html lang="de" data-v="linie">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Kapitel-Kante als Signatur · Fundus-Demo</title>
<script>document.documentElement.classList.add('js');if(matchMedia('(prefers-reduced-motion: reduce)').matches)document.documentElement.classList.add('reduziert')</script>
<style>
/* Schriften der vier Welten (geladen wird nur, was die aktive Variante setzt) */
@font-face { font-family: 'Special Gothic'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/special-gothic:vf@latest/latin-standard-normal.woff2') format('woff2-variations'); font-weight: 400 700; font-stretch: 75% 125%; 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: 'Fraunces'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/fraunces:vf@latest/latin-full-normal.woff2') format('woff2-variations'); font-weight: 100 900; font-style: normal; font-display: swap; }
@font-face { font-family: 'Familjen Grotesk'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/familjen-grotesk:vf@latest/latin-wght-normal.woff2') format('woff2-variations'); font-weight: 400 700; font-display: swap; }
@font-face { font-family: 'Big Shoulders'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/big-shoulders:vf@latest/latin-wght-normal.woff2') format('woff2-variations'); font-weight: 100 900; font-display: swap; }
@font-face { font-family: 'Google Sans Flex'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/google-sans-flex:vf@latest/latin-wght-normal.woff2') format('woff2-variations'); font-weight: 100 1000; font-display: swap; }
@font-face { font-family: 'Shrikhand'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/shrikhand@latest/latin-400-normal.woff2') format('woff2'); font-weight: 400; 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: '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; }

/* ============ Kern: ein Fortschritt --kp je Kante, 0 bis 1 ============
   Alles an der Kante rechnet nur aus --kp (transform, opacity, stroke, clip-path).
   Die Zeitleiste ist die Kante selbst: view(), Bereich cover 5 % bis cover 50 %.
   Ohne animation-timeline setzt das Skript --kp aus getBoundingClientRect. */
@property --kp { syntax: '<number>'; inherits: true; initial-value: 0; }
@keyframes kante-kp { from { --kp: 0; } to { --kp: 1; } }
/* Wölbung: Atemzug mit Überschwingen 1,12 bei 70 % des Bereichs */
@keyframes kante-atem { 0% { --kp: 0; } 70% { --kp: 1.12; } 100% { --kp: 1; } }

:root {
  --kurve: linear;                         /* an den Scroll gekoppelt: keine eigene Dauer */
  --ui: 150ms cubic-bezier(0.16, 1, 0.3, 1);
  --rand: clamp(20px, 5vw, 72px);
  --max: 1240px;
}
* { box-sizing: border-box; margin: 0; }
html { scroll-behavior: auto; -webkit-text-size-adjust: 100%; }
body { background: var(--grund); color: var(--text); font-family: var(--f-text); font-size: 1.0625rem; line-height: 1.6; overflow-x: clip; }
:focus-visible { outline: 2px solid var(--fokus, currentColor); outline-offset: 3px; }
a { color: inherit; }
.nur-sr { position: absolute !important; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
.label { font: 500 11px/1.2 'Geist Mono', ui-monospace, monospace; letter-spacing: .14em; text-transform: uppercase; }

/* ---------- Seite ---------- */
.kopf { position: relative; z-index: 20; display: flex; flex-wrap: wrap; align-items: center; gap: 12px 28px; padding: 18px var(--rand); background: var(--fl0); color: var(--t0, var(--text)); }
.kopf__marke { font-family: var(--f-kopf); font-weight: var(--w-kopf); font-size: 1.25rem; letter-spacing: -.01em; text-decoration: none; margin-right: auto; line-height: 1.1; }
.kopf nav { display: flex; flex-wrap: wrap; gap: 6px 22px; font-size: .9375rem; }
.kopf nav a { text-decoration: none; padding: 6px 0; background: linear-gradient(currentColor 0 0) 0 100% / 0 1.5px no-repeat; transition: background-size var(--ui); }
.kopf nav a:hover { background-size: 100% 1.5px; }
.knopf { display: inline-flex; align-items: center; gap: .55em; min-height: 44px; padding: 0 20px; border-radius: var(--radius); background: var(--signal); color: var(--auf-signal); font-weight: 600; font-size: .9375rem; text-decoration: none; transition: filter var(--ui), translate var(--ui); }
.knopf:hover { filter: brightness(.92); }
.knopf:active { translate: 0 1px; }

.kapitel { position: relative; overflow-x: clip; background: var(--fl); color: var(--t, var(--text)); padding: var(--pad-oben) var(--rand) var(--pad-unten); }
.kapitel > .innen { max-width: var(--max); margin-inline: auto; }
.kapitel--auftakt { padding-top: clamp(3rem, 9vw, 7.5rem); min-height: min(80svh, 860px); display: grid; align-content: center; } /* erste Kante beginnt am unteren Bildrand */
.auftakt__ort { margin-bottom: 1.25rem; color: var(--leise0, var(--leise)); }
.auftakt h1 { font-family: var(--f-kopf); font-weight: var(--w-h1); font-size: var(--g-h1); line-height: var(--lh-h1, 1); letter-spacing: -.015em; max-width: 14ch; text-wrap: balance; font-variation-settings: var(--fvs-h1, normal); }
.auftakt__satz { margin-top: 1.5rem; max-width: 46ch; font-size: clamp(1.0625rem, 1rem + .35vw, 1.25rem); text-wrap: pretty; }
.zeile { display: grid; grid-template-columns: minmax(0, 5fr) minmax(0, 7fr); gap: 1.5rem clamp(2rem, 6vw, 6rem); align-items: start; }
.zeile h2 { font-family: var(--f-kopf); font-weight: var(--w-h2); font-size: var(--g-h2); line-height: 1.05; letter-spacing: -.01em; text-wrap: balance; font-variation-settings: var(--fvs-h2, normal); }
.zeile__text p { max-width: 58ch; text-wrap: pretty; }
.zeile__text p + p { margin-top: .9em; }
.fakten { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(150px, 100%), 1fr)); gap: 0 24px; margin-top: 1.75rem; border-top: 1px solid var(--linie); }
.fakten div { padding: 12px 0; border-bottom: 1px solid var(--linie); }
.fakten dt { color: var(--leise); }
.fakten dd { font-weight: 600; font-variant-numeric: tabular-nums; margin-top: 4px; }
.bild { margin-top: 2rem; }
.bild img { display: block; width: 100%; height: auto; aspect-ratio: 16 / 10; object-fit: cover; border-radius: var(--radius-bild, 0); }
.bild figcaption { margin-top: .6rem; font-size: .875rem; color: var(--leise); }
.fuss .zeile__text .knopf { margin-top: 1.5rem; }
.fuss__klein { margin-top: 2rem; font-size: .875rem; color: var(--leise); }
@media (max-width: 760px) { .zeile { grid-template-columns: minmax(0, 1fr); } .kopf nav { display: none; } }

/* ---------- Die Kante ----------
   Sitzt am Anfang des Kapitels und ragt in den Abschnitt davor (bottom: 100 %).
   Der Abschnitt davor hat unten --kh + Luft: keine Kante schneidet eine Überschrift an. */
.kante { position: absolute; left: 0; right: 0; bottom: 100%; height: var(--kh); pointer-events: none; container-type: inline-size; color: var(--t); }
.kante__form { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; }
.kante__wort { position: absolute; font-variant-numeric: tabular-nums; }

/* Bewegung: Kurzschreibweise ZUERST, danach Zeitleiste und Bereich als Langform.
   animation setzt animation-timeline und animation-range als reset-only-Werte auf auto bzw. normal zurück.
   Steht animation nach (oder spezifischer als) animation-range, läuft die Kante über den ganzen Bildschirm
   statt über den gesetzten Bereich (Fehler der Bäckerei-Probe 04.11.2026: gemessen normal / normal).
   Varianten ändern nur Langformen (animation-name, animation-timing-function), nie erneut animation. */
@supports (animation-timeline: view()) {
  html:not(.reduziert):not(.rueckfall) .kante {
    animation: kante-kp linear both;
    animation-timeline: view();
    animation-range-start: cover 5%;
    animation-range-end: cover 50%;
  }
}
@supports (animation-timeline: view()) {
  /* Wölbung: nur die Langformen tauschen, die Bereichswerte oben bleiben stehen */
  html[data-v="woelbung"]:not(.reduziert):not(.rueckfall) .kante { animation-name: kante-atem; animation-timing-function: cubic-bezier(0.37, 0, 0.63, 1); }
}
html.reduziert .kante { --kp: 1; }

/* =====================================================================
   Variante linie · Tischlerei Brümmer, Soltau (handwerk-werkstatt-klartext)
   Ein Bleistiftstrich zieht die Kapitelgrenze, an der Spitze der Stift, Krähenfuß als Anrissmarke.
   ===================================================================== */
html[data-v="linie"] {
  --grund: #EFEDEA; --text: #1A1A1A; --leise: #57524C; --linie: #D3CDC2; --signal: #FB6100; --auf-signal: #1A1A1A; --fokus: #1A1A1A;
  --f-kopf: 'Special Gothic', 'Arial Narrow', sans-serif; --f-text: 'Atkinson Hyperlegible Next', system-ui, sans-serif;
  --w-kopf: 700; --w-h1: 700; --w-h2: 700; --g-h1: clamp(2.5rem, 1.4rem + 4.6vw, 5.25rem); --g-h2: clamp(1.75rem, 1.2rem + 1.8vw, 2.5rem); --lh-h1: 1;
  --radius: 4px; --radius-bild: 0; --kh: clamp(64px, 7vw, 92px);
  --pad-oben: clamp(2.75rem, 6vw, 5rem); --pad-unten: calc(var(--kh) + clamp(2.75rem, 6vw, 5rem));
  --fl0: #EFEDEA;
}
[data-v="linie"] .kapitel { --fl: #EFEDEA; }
[data-v="linie"] .kapitel.fuss { --fl: #C3B4A5; --leise: #3F3A34; --linie: #A89A8B; }
[data-v="linie"] .graphit { color: #2E2C29; }
.v-linie .kante__form { inset: auto 0 0 0; height: 24px; }
/* Kein vector-effect: non-scaling-stroke am Strich: damit rechnet Chromium die Strichelung im Bildschirmraum, pathLength stimmt dann nicht mehr */
.v-linie .strich { fill: none; stroke: currentColor; stroke-width: 2.6; stroke-linecap: round;
  stroke-dasharray: 1 1; stroke-dashoffset: calc(1 - var(--kp)); filter: url(#graphit); }
.v-linie .kraehe { position: absolute; left: 7%; bottom: 11px; width: 18px; height: 16px; overflow: visible; }
.v-linie .kraehe path { fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; vector-effect: non-scaling-stroke;
  stroke-dasharray: 1 1; stroke-dashoffset: calc(1 - clamp(0, (var(--kp) - .05) * 9, 1)); }
.v-linie .stift { position: absolute; left: 0; bottom: 1px; width: 74px; height: 22px; transform-origin: 0 50%;
  translate: calc(var(--kp) * 100cqw) 0; rotate: -24deg;
  opacity: clamp(0, min(var(--kp) * 30, (1 - var(--kp)) * 30), 1); }
.v-linie .kante__wort { left: calc(7% + 30px); bottom: 22px; display: flex; gap: .6em; align-items: baseline;
  font-family: var(--f-kopf); font-stretch: 75%; font-weight: 700; font-size: .875rem; letter-spacing: .06em; text-transform: uppercase;
  clip-path: inset(-.4em calc((1 - clamp(0, (var(--kp) - .06) * 6, 1)) * 100%) -.4em 0); }
.v-linie .kante__wort b { font-weight: 700; color: var(--leise); }
.v-linie .kante__mass { position: absolute; right: 4%; bottom: 22px; font-family: var(--f-kopf); font-stretch: 75%; font-weight: 600; font-size: .875rem; letter-spacing: .06em; text-transform: uppercase;
  opacity: clamp(0, (var(--kp) - .82) * 8, 1); translate: 0 calc((1 - clamp(0, (var(--kp) - .82) * 8, 1)) * 6px); }
.v-linie .kante__mass::before { content: ''; display: inline-block; width: 1px; height: 14px; margin-right: 8px; vertical-align: -2px; background: currentColor; }
.skizze { width: 100%; height: auto; margin-top: 2rem; color: #2E2C29; }
.skizze path, .skizze line, .skizze rect { fill: none; stroke: currentColor; stroke-width: 1.4; vector-effect: non-scaling-stroke; stroke-linecap: round; }
.skizze text { font: 600 13px 'Special Gothic', sans-serif; font-stretch: 75%; letter-spacing: .04em; fill: currentColor; }

/* =====================================================================
   Variante woelbung · Atemraum, Bad Harzburg (yoga-studio-achtsamkeit)
   Die Fläche des nächsten Kapitels hebt sich wie ein Atemzug, die Atemlinie läuft voraus.
   ===================================================================== */
html[data-v="woelbung"] {
  --grund: #F2EEE8; --text: #2A2622; --leise: #655E57; --linie: #D7CFC4; --signal: #4C5B47; --auf-signal: #fff; --fokus: #4C5B47;
  --f-kopf: 'Fraunces', Georgia, serif; --f-text: 'Familjen Grotesk', system-ui, sans-serif;
  --w-kopf: 400; --w-h1: 300; --w-h2: 350; --g-h1: clamp(2.75rem, 1.5rem + 5vw, 5.75rem); --g-h2: clamp(1.875rem, 1.3rem + 2vw, 3rem); --lh-h1: 1.02;
  --fvs-h1: 'opsz' 144, 'SOFT' 100, 'WONK' 0; --fvs-h2: 'opsz' 72, 'SOFT' 100;
  --radius: 999px; --radius-bild: 24px; --kh: clamp(56px, 9vw, 132px);
  --pad-oben: clamp(2.5rem, 5vw, 4.5rem); --pad-unten: calc(var(--kh) + clamp(3rem, 7vw, 6rem));
  --fl0: #F2EEE8;
}
[data-v="woelbung"] .kapitel { --fl: #F2EEE8; }
[data-v="woelbung"] .kapitel:nth-of-type(even) { --fl: #E3D8CA; --leise: #5A534C; }
[data-v="woelbung"] .kapitel.abend { --fl: #3A3430; --t: #F2EEE8; --leise: #CFC6BB; --linie: #5A514A; --signal: #F2EEE8; --auf-signal: #2A2622; --fokus: #F2EEE8; }
.v-woelbung .kante__form { inset: 0 0 -2px 0; height: calc(100% + 2px); } /* 2 px Überlapp: keine Haarfuge zur Fläche darunter */
.v-woelbung .flaeche { fill: var(--fl); transform-box: fill-box; transform-origin: 50% 100%; transform: scaleY(var(--kp)); }
.v-woelbung .atemlinie { fill: none; stroke: #4C5B47; stroke-width: 1.5; vector-effect: non-scaling-stroke; transform-box: fill-box; transform-origin: 50% 100%;
  transform: scaleY(calc(var(--kp) * 1.18)); opacity: clamp(0, var(--kp) * 3, .9); }
.abend .v-woelbung .atemlinie, .v-woelbung.auf-hell .atemlinie { stroke: #8A9A84; }
.v-woelbung .kante__wort { left: 50%; bottom: calc(var(--kh) * .12); translate: -50% calc((1 - min(var(--kp), 1)) * var(--kh) * .9); text-align: center;
  opacity: clamp(0, (var(--kp) - .25) * 2.5, 1); color: var(--t, var(--text)); }
.v-woelbung .kante__wort span { display: block; }
.v-woelbung .kante__wort .atem { font-family: var(--f-kopf); font-style: normal; font-weight: 350; font-size: clamp(1.25rem, 1rem + 1vw, 1.75rem); font-variation-settings: 'opsz' 72, 'SOFT' 100; line-height: 1.1; }
.v-woelbung .kante__wort .takt { margin-top: 4px; font-size: .875rem; color: var(--t, #3F3A35); font-variant-numeric: tabular-nums; }

/* =====================================================================
   Variante stufe · Heitmüller Bau, Lippstadt (bau-unternehmen-projekt)
   Die Kante tritt Stufe für Stufe heraus; oben die Höhenkote des Geschosses.
   ===================================================================== */
html[data-v="stufe"] {
  --grund: #EDECE8; --text: #1A1A18; --leise: #5B5952; --linie: #CFCBC0; --signal: #B43C0A; --auf-signal: #fff; --fokus: #1A1A18;
  --f-kopf: 'Big Shoulders', 'Arial Narrow', sans-serif; --f-text: 'Google Sans Flex', system-ui, sans-serif;
  --w-kopf: 800; --w-h1: 800; --w-h2: 800; --g-h1: clamp(3rem, 1.6rem + 6vw, 7rem); --g-h2: clamp(2rem, 1.4rem + 2.4vw, 3.5rem); --lh-h1: .9;
  --radius: 0; --radius-bild: 0; --kh: clamp(60px, 10vw, 150px); --n: 6;
  --pad-oben: clamp(2.5rem, 5vw, 4.5rem); --pad-unten: calc(var(--kh) + clamp(5rem, 8vw, 6.5rem));
  --fl0: #EDECE8;
}
[data-v="stufe"] .kapitel { --fl: #EDECE8; }
[data-v="stufe"] .kapitel:nth-of-type(even) { --fl: #DCD4C3; --leise: #4F4D47; }
[data-v="stufe"] .kapitel.asphalt { --fl: #262624; --t: #EDECE8; --leise: #BDBAB2; --linie: #45443F; --signal: #EDECE8; --auf-signal: #1A1A18; --fokus: #EDECE8; }
.v-stufe .stufen { position: absolute; inset: 0 0 -2px 0; display: flex; align-items: flex-end; overflow: clip; }
/* Stufe i erscheint zwischen --kp = i/8 und (i+1)/8: die Treppe läuft von links nach rechts hoch */
.v-stufe .stufen i { flex: 1; height: calc((var(--i) + 1) / var(--n) * 100%); background: var(--fl); border-top: 2px solid var(--t, var(--text));
  translate: 0 calc((1 - clamp(0, var(--kp) * 8 - var(--i), 1)) * 104%); }
.v-stufe .kante__wort { right: var(--rand); bottom: calc(100% + 10px); text-align: right; display: grid; justify-items: end; gap: 2px;
  opacity: clamp(0, var(--kp) * 8 - 5.2, 1); translate: 0 calc((1 - clamp(0, var(--kp) * 8 - 5.2, 1)) * 10px); color: var(--t-vor, var(--text)); }
.v-stufe .kote { font-family: 'Geist Mono', monospace; font-size: .9375rem; font-weight: 600; letter-spacing: .02em; display: inline-flex; align-items: center; gap: 8px; }
.v-stufe .kote svg { width: 14px; height: 12px; }
.v-stufe .kote svg path { fill: none; stroke: currentColor; stroke-width: 1.6; }
.v-stufe .geschoss { font-size: .875rem; color: var(--leise-vor, var(--leise)); }

/* =====================================================================
   Variante zeitachse · Backstube Ohlendorf, Peine (lebensmittelhandwerk-theke)
   Jede Kante trägt eine Uhrzeit, die Uhr unten links läuft mit, die Flächen gehen von Nacht zu Morgen.
   ===================================================================== */
html[data-v="zeitachse"] {
  --grund: #241B14; --text: #F7F1E6; --leise: #D9CCBA; --linie: #4A3A2E; --signal: #8A3C12; --auf-signal: #fff; --fokus: #F7F1E6;
  --f-kopf: 'Shrikhand', Georgia, serif; --f-text: 'Onest', system-ui, sans-serif;
  --w-kopf: 400; --w-h1: 400; --w-h2: 400; --g-h1: clamp(3rem, 1.6rem + 6vw, 7rem); --g-h2: clamp(1.625rem, 1.2rem + 1.6vw, 2.5rem); --lh-h1: .95;
  --radius: 999px; --radius-bild: 16px; --kh: 0px; --verlauf: clamp(7rem, 14vw, 11rem);
  --pad-oben: clamp(1.5rem, 3vw, 2.5rem); --pad-unten: calc(var(--verlauf) + clamp(2.5rem, 5vw, 4rem));
  --fl0: #1C1511; --t0: #F7F1E6;
}
/* Nacht → Morgen: jede Fläche läuft unten in die Farbe des nächsten Kapitels aus */
[data-v="zeitachse"] .kapitel { background: linear-gradient(to bottom, var(--fl) calc(100% - var(--verlauf)), var(--fl-nach, var(--fl))); }
[data-v="zeitachse"] .kapitel.licht { --t: #241B14; --leise: #5E4F3F; --linie: #DDCDB4; --fokus: #241B14; }
[data-v="zeitachse"] .kapitel.licht .knopf { --signal: #8A3C12; }
.v-zeit { position: relative; bottom: auto; height: auto; margin: 0 calc(var(--rand) * -1) clamp(1.75rem, 4vw, 3rem); padding: 0 var(--rand); }
.v-zeit .skala { position: relative; height: 14px; max-width: var(--max); margin-inline: auto;
  background: linear-gradient(currentColor 0 0) 0 0 / 100% 1px no-repeat,
              repeating-linear-gradient(to right, currentColor 0 1px, transparent 1px calc(100% / 16)) 0 0 / 100% 8px no-repeat;
  clip-path: inset(-2px calc((1 - var(--kp)) * 100%) -2px 0); opacity: .7; }
.v-zeit .zeiger { position: absolute; top: -4px; width: 9px; height: 9px; border-radius: 50%; background: currentColor; left: calc(var(--kp) * 100% - 4px);
  opacity: clamp(0, min(var(--kp) * 30, (1 - var(--kp)) * 30), 1); }
.v-zeit .kante__wort { position: relative; max-width: var(--max); margin: 18px auto 0; display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 18px;
  clip-path: inset(-.3em calc((1 - clamp(0, var(--kp) * 2.4, 1)) * 100%) -.3em 0); }
.v-zeit time { font-family: 'Shrikhand', Georgia, serif; font-size: clamp(2.5rem, 1.6rem + 4vw, 5rem); line-height: 1; font-variant-numeric: tabular-nums; }
.v-zeit .station { font-weight: 600; font-size: clamp(1rem, .9rem + .4vw, 1.25rem); }
.uhr { position: fixed; right: 14px; top: 14px; z-index: 30; display: none; align-items: center; gap: 10px; height: 40px; padding: 0 16px 0 12px; border-radius: 999px;
  background: rgba(28, 21, 17, .9); color: #F7F1E6; font: 600 .9375rem/1 'Onest', system-ui, sans-serif; font-variant-numeric: tabular-nums; box-shadow: 0 6px 24px rgba(0,0,0,.2); }
html[data-v="zeitachse"] .uhr { display: inline-flex; opacity: 0; visibility: hidden; translate: 0 -6px; transition: opacity 200ms cubic-bezier(0.7, 0, 0.84, 0), translate 200ms cubic-bezier(0.7, 0, 0.84, 0), visibility 0s 200ms; }
/* Die Uhr erscheint erst unter dem Kopf, damit sie nie Marke oder Knopf verdeckt */
html[data-v="zeitachse"] .uhr.da { opacity: 1; visibility: visible; translate: 0 0; transition: opacity 300ms cubic-bezier(0.16, 1, 0.3, 1), translate 300ms cubic-bezier(0.16, 1, 0.3, 1), visibility 0s; }
.uhr__himmel { width: 18px; height: 18px; border-radius: 50%; background: #F7F1E6; box-shadow: inset -6px -2px 0 0 #1C1511; transition: box-shadow 400ms cubic-bezier(0.16, 1, 0.3, 1), background-color 400ms cubic-bezier(0.16, 1, 0.3, 1); }
.uhr.morgen .uhr__himmel { background: #E9A64B; box-shadow: inset 0 0 0 0 #1C1511; }
/* Reduzierte Bewegung: nur kurze Blende, kein Weg */
html.reduziert[data-v="zeitachse"] .uhr { translate: none; transition: opacity 150ms linear, visibility 0s 150ms; }
html.reduziert[data-v="zeitachse"] .uhr.da { translate: none; transition: opacity 150ms linear, visibility 0s; }
html.reduziert .uhr__himmel { transition-duration: 150ms; }
.uhr__zeit { min-width: 3.1em; font-size: 1.0625rem; }
.uhr__ort { font-weight: 500; opacity: .8; }

/* ---------- Pult ---------- */
.pult { position: fixed; right: 14px; bottom: 14px; z-index: 40; width: min(330px, calc(100vw - 28px)); padding: 14px; border-radius: 12px; background: rgba(18, 18, 20, .9); color: #f3f2ee;
  -webkit-backdrop-filter: blur(10px); backdrop-filter: blur(10px); display: grid; gap: 10px; font: 500 12px/1.35 'Geist Mono', ui-monospace, monospace; box-shadow: 0 10px 30px rgba(0,0,0,.25); }
.pult .label { color: rgba(243, 242, 238, .7); }
.pult__wahl { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 6px; }
.pult button { min-height: 34px; padding: 0 10px; border: 1px solid rgba(243, 242, 238, .22); border-radius: 8px; background: transparent; color: inherit; font: inherit; letter-spacing: .04em; cursor: pointer; transition: background-color var(--ui), border-color var(--ui); }
.pult button:hover { border-color: rgba(243, 242, 238, .5); }
.pult button[aria-pressed="true"] { background: #f3f2ee; color: #121214; border-color: #f3f2ee; }
.pult :focus-visible { outline: 2px solid #34a7b9; outline-offset: 2px; }
.pult__messung { color: rgba(243, 242, 238, .8); word-break: break-word; }
.pult__messung output { color: #f3f2ee; }

/* ---------- Vorschau, Touch, Kontrast ---------- */
.vorschau body { overflow: hidden; }
.vorschau .uhr { top: 18px; right: 18px; }
@media (max-width: 640px) { .uhr { top: auto; right: auto; left: 12px; bottom: 12px; height: 36px; } .uhr__ort { display: none; } }
@media (hover: none) { .kopf nav a:hover { background-size: 0 1.5px; } }
@media (max-width: 520px) {
  .v-linie .kante__mass { display: none; }
  .v-stufe .geschoss { max-width: 16ch; }
}
@media (forced-colors: active) {
  .kante__form, .stufen, .skala, .stift, .zeiger, .kraehe { display: none; }
  .kante { border-bottom: 2px solid CanvasText; }
  .v-zeit { border-top: 2px solid CanvasText; border-bottom: 0; }
  .kante__wort { clip-path: none !important; opacity: 1 !important; translate: none !important; }
  button, [role=button], [role=tab], [role=switch], input, select, textarea { border: 1px solid ButtonText !important; }
}
</style>
</head>
<body>
<svg width="0" height="0" style="position:absolute" aria-hidden="true" focusable="false">
  <!-- Graphitkorn: der Strich franst leicht aus wie Bleistift auf Holz -->
  <filter id="graphit" x="-2%" y="-60%" width="104%" height="220%">
    <feTurbulence type="fractalNoise" baseFrequency="0.9 0.35" numOctaves="1" seed="7" result="korn"/>
    <feDisplacementMap in="SourceGraphic" in2="korn" scale="2.2" xChannelSelector="R" yChannelSelector="G"/>
  </filter>
</svg>
<header class="kopf" id="kopf"></header>
<main id="welt"></main>
<div class="uhr" id="uhr" aria-hidden="true"><span class="uhr__himmel"></span><span class="uhr__zeit">21:30</span><span class="uhr__ort">Backstube</span></div>

<div class="pult">
  <p class="label" id="pult-titel">Kante</p>
  <div class="pult__wahl" role="group" aria-labelledby="pult-titel">
    <button type="button" data-wahl="linie">Linie</button>
    <button type="button" data-wahl="woelbung">Wölbung</button>
    <button type="button" data-wahl="stufe">Stufe</button>
    <button type="button" data-wahl="zeitachse">Zeitachse</button>
  </div>
  <button type="button" id="rueckfall" aria-pressed="false">JS-Rückfall zeigen</button>
  <p class="pult__messung">gemessen an Kante 1: <output id="messung">…</output></p>
</div>

<script>
(() => {
  const html = document.documentElement;
  const reduziert = () => html.classList.contains('reduziert');
  const kannTimeline = !!(window.CSS && CSS.supports && CSS.supports('animation-timeline: view()'));
  if (!kannTimeline) html.classList.add('rueckfall');
  const welt = document.getElementById('welt'), kopf = document.getElementById('kopf'), uhr = document.getElementById('uhr');
  const BEREICH = [0.05, 0.50]; // cover 5 % bis cover 50 %, wie animation-range-start/-end im CSS

  // ---------- Formen der Kanten ----------
  const STIFT = `<svg class="stift" viewBox="0 0 74 22" aria-hidden="true"><path d="M20 4 L9 8 L9 14 L20 18 Z" fill="#D9C2A0"/><path d="M0 11 L9 8 L9 14 Z" fill="#2E2C29"/><rect x="20" y="4" width="54" height="14" rx="1.5" fill="#2E2C29"/><rect x="20" y="4" width="54" height="4.5" fill="#55514C"/><rect x="66" y="4" width="8" height="14" fill="#C3B4A5"/></svg>`;
  const formen = {
    linie: (k) => `<div class="kante v-linie graphit">
        <svg class="kante__form" viewBox="0 0 1000 24" preserveAspectRatio="none" aria-hidden="true"><path class="strich" pathLength="1" d="M0 12.4 C 90 11.6, 180 13, 290 12.2 S 520 11.5, 640 12.6 S 860 11.7, 1000 12.3"/></svg>
        <svg class="kraehe" viewBox="0 0 18 16" aria-hidden="true"><path pathLength="1" d="M1 1 L9 15 L17 1"/></svg>
        ${STIFT}
        <p class="kante__wort"><b>${k.nr}</b><span>${k.wort}</span></p>
        <p class="kante__mass">${k.mass}</p>
      </div>`,
    woelbung: (k) => `<div class="kante v-woelbung">
        <svg class="kante__form" viewBox="0 0 100 10" preserveAspectRatio="none" aria-hidden="true"><path class="flaeche" d="M-20 10 Q50 -10 120 10 Z"/><path class="atemlinie" d="M-20 10 Q50 -10 120 10"/></svg>
        <p class="kante__wort"><span class="atem">${k.wort}</span><span class="takt">${k.takt}</span></p>
      </div>`,
    stufe: (k) => `<div class="kante v-stufe">
        <div class="stufen" aria-hidden="true">${Array.from({ length: 6 }, (_, i) => `<i style="--i:${i}"></i>`).join('')}</div>
        <p class="kante__wort"><span class="kote"><svg viewBox="0 0 14 12" aria-hidden="true"><path d="M1 1 H13 L7 11 Z"/></svg>${k.kote}</span><span class="geschoss">${k.wort}</span></p>
      </div>`,
    zeitachse: (k) => `<div class="kante v-zeit">
        <div class="skala" aria-hidden="true"><span class="zeiger"></span></div>
        <p class="kante__wort"><time datetime="${k.zeit}">${k.zeit}</time><span class="station">${k.wort}</span></p>
      </div>`
  };

  // ---------- Welten ----------
  const W = {
    linie: {
      marke: 'Tischlerei Brümmer', nav: ['Möbel', 'Innenausbau', 'Werkstatt'], cta: 'Aufmaß anfragen',
      auftakt: { ort: 'Tischlerei · Soltau', h1: 'Erst der Strich. Dann der Schnitt.', satz: 'Jedes Stück beginnt bei uns mit einem Bleistiftstrich auf dem Brett. Wo er sitzt, entscheidet über alles danach. Darum zeigen wir zuerst, wie wir arbeiten.' },
      kapitel: [
        { k: { nr: '01', wort: 'Aufmaß', mass: '1 Termin vor Ort' }, h2: 'Wir messen selbst.', p: ['Wände sind selten gerade. Wir kommen mit Zollstock, Laser und Skizzenblock und messen jede Nische dreimal.', 'Danach bekommen Sie eine Zeichnung mit allen Maßen, keinen Prospekt.'], extra: 'skizze' },
        { k: { nr: '02', wort: 'Zuschnitt', mass: 'Tag 2 bis 4' }, h2: 'Der Bleistift zeigt, wo die Äste sitzen.', p: ['Eiche und Esche lagern bei uns, bis sie trocken genug sind. Am Brett markieren wir mit Krähenfuß und Strich, welche Seite nach vorn kommt und wo gesägt wird.'], fakten: [['Holz', 'Eiche, Esche, Ahorn'], ['Platten', 'Multiplex, MDF lackiert']] },
        { k: { nr: '03', wort: 'Verleimen', mass: 'über Nacht' }, h2: 'Am Morgen steht der Korpus.', p: ['Zinken, Dübel, Leim und Zwingen. Was abends in die Presse kommt, ist am nächsten Morgen ein Möbel. Jetzt sieht man, ob der Strich gestimmt hat.'] },
        { k: { nr: '04', wort: 'Einbau', mass: '1 bis 2 Tage' }, h2: 'Wir bauen ein und fegen die Späne weg.', p: ['Wir tragen, setzen und richten aus, bis jede Fuge stimmt. Zum Schluss zeigen wir Ihnen, wie Sie die Oberfläche pflegen.'] }
      ],
      fuss: { k: { nr: '05', wort: 'Anfrage', mass: 'Antwort in 2 Werktagen' }, h2: 'Was sollen wir bauen?', p: ['Schicken Sie uns zwei Fotos und ungefähre Maße. Wir melden uns mit einem Termin fürs Aufmaß.'], cta: 'Aufmaß anfragen', klein: 'Tischlerei Brümmer · Soltau · erfundener Betrieb für diese Demo' }
    },
    woelbung: {
      marke: 'Atemraum', nav: ['Stundenplan', 'Neu hier', 'Preise'], cta: 'Probestunde buchen',
      auftakt: { ort: 'Yoga · Bad Harzburg', h1: 'Erst einatmen.', satz: 'Jede Stunde beginnt bei uns mit einem langen Atemzug. Diese Seite atmet mit: Jedes Kapitel hebt sich, bevor Sie weiterlesen.' },
      kapitel: [
        { k: { wort: 'Einatmen', takt: 'vier Zählzeiten' }, h2: 'Ankommen, bevor es losgeht.', p: ['Zehn Minuten vor jeder Stunde ist der Raum offen. Matten, Decken und Blöcke liegen bereit, Sie brauchen nur bequeme Kleidung.'], fakten: [['Raum', 'höchstens 12 Matten'], ['Stunde', '75 Minuten']] },
        { k: { wort: 'Halten', takt: 'zwei Zählzeiten' }, h2: 'Hatha am Morgen, Yin am Abend.', p: ['Montag bis Freitag um 7:15 Uhr eine ruhige Hatha-Stunde. Abends Yin und sanftes Vinyasa, samstags eine lange Stunde für alle.'], bild: { src: 'bilder/fenster-licht.jpg', alt: 'Salbeigrüne Kalkputzwand mit dem Sonnenfleck eines Sprossenfensters, davor eine Eichenbank mit Vase und Zweig', text: 'Der Übungsraum am Morgen (Bildbeispiel)' } },
        { k: { wort: 'Ausatmen', takt: 'sechs Zählzeiten' }, h2: 'Neu hier? Zuerst ein Gespräch.', p: ['Vor der ersten Stunde sprechen wir zehn Minuten über Rücken, Knie und das, was Sie mitbringen. Was Sie erzählen, bleibt im Raum.'], abend: true },
        { k: { wort: 'Nachspüren', takt: 'so lange es braucht' }, h2: 'Die erste Stunde ist zum Ausprobieren.', p: ['Buchen Sie eine Probestunde im Stundenplan. Passt es nicht, ist nichts weiter zu tun: kein Abo, keine Kündigung.'] }
      ],
      fuss: { k: { wort: 'Ankommen', takt: 'Bad Harzburg' }, h2: 'Bis bald auf der Matte.', p: ['Atemraum, Herzog-Wilhelm-Straße (erfundene Anschrift). Probestunden buchen Sie im Stundenplan oder telefonisch.'], cta: 'Probestunde buchen', klein: 'Atemraum · Bad Harzburg · erfundenes Studio für diese Demo' }
    },
    stufe: {
      marke: 'Heitmüller Bau', nav: ['Projekte', 'Leistungen', 'Karriere'], cta: 'Projekt anfragen',
      auftakt: { ort: 'Hoch- und Rohbau · Lippstadt', h1: 'Geschoss für Geschoss.', satz: 'Wir bauen Ein- und Mehrfamilienhäuser im Kreis Soest. Diese Seite erzählt einen Bau von der Baugrube bis zum Richtfest, Stufe für Stufe.' },
      kapitel: [
        { k: { kote: '−3,20', wort: 'Baugrube' }, h2: 'Erst nach unten.', p: ['Bodengutachten, Vermessung, Aushub. Bevor es aufwärts geht, wissen wir genau, worauf das Haus steht.'], fakten: [['Dauer', 'rund 2 Wochen'], ['Gerät', 'Bagger, Rüttelplatte']] },
        { k: { kote: '±0,00', wort: 'Bodenplatte' }, h2: 'Der Nullpunkt.', p: ['Auf diese Höhe beziehen sich alle Maße am Bau. Bewehrung, Beton, danach sieben Tage feucht halten.'], bild: { src: 'bilder/beton-treppe.jpg', alt: 'Freitragende Betonstufen an einer hellen Sichtbetonwand im tiefen Abendlicht', text: 'Sichtbeton in Brettschalung (Bildbeispiel)' } },
        { k: { kote: '+3,05', wort: 'Decke über EG' }, h2: 'Wände, Decke, nächste Etage.', p: ['Kalksandstein für die Wände, Filigranplatten für die Decke. Jede Etage bekommt ihre Höhenkote, auf die der nächste Trupp aufbaut.'] },
        { k: { kote: '+6,10', wort: 'Richtfest' }, h2: 'Der Kranz geht hoch.', p: ['Nach rund zwölf Wochen Rohbau steht der Dachstuhl. Dann laden wir alle ein, die mitgebaut haben, auch die Nachbarn.'], asphalt: true }
      ],
      fuss: { k: { kote: '+8,45', wort: 'First' }, h2: 'Ihr Projekt als Nächstes?', p: ['Schicken Sie uns Lageplan oder Entwurf. Sie bekommen eine erste Einschätzung zu Ablauf und Bauzeit.'], cta: 'Projekt anfragen', klein: 'Heitmüller Bau · Lippstadt · erfundener Betrieb für diese Demo' }
    },
    zeitachse: {
      marke: 'Backstube Ohlendorf', nav: ['Sortiment', 'Filialen', 'Backstube'], cta: 'Vorbestellen',
      auftakt: { ort: 'Bäckerei · Peine · 21:30 Uhr', h1: 'Über Nacht.', satz: 'Unser Brot wird nachts gemacht und liegt morgens um sechs in der Theke. Scrollen Sie durch eine Nacht in der Backstube, die Uhr oben rechts läuft mit.' },
      kapitel: [
        { k: { zeit: '22:00', wort: 'Teig ansetzen' }, h2: 'Sauerteig braucht Zeit.', p: ['Der Roggensauerteig wurde am Nachmittag angefrischt. Um zehn rühren wir den Brotteig an, dann ruht er mehrere Stunden in der kühlen Backstube.'], fl: '#2B211B' },
        { k: { zeit: '02:00', wort: 'Ofen an' }, h2: 'Der Steinofen heizt auf.', p: ['Um zwei kommt die erste Schicht. Der Ofen braucht gut eine Stunde, bis die Steinplatte durchgeheizt ist. In der Zeit werden die Laibe geformt.'], fl: '#3B2A1F' },
        { k: { zeit: '04:30', wort: 'Ausbacken' }, h2: 'Erst das Brot, dann die Brötchen.', p: ['Große Laibe brauchen die meiste Hitze und kommen zuerst. Danach Brötchen, Laugengebäck und zuletzt der Kuchen, wenn der Ofen schon ruhiger ist.'], fl: '#6E4429' },
        { k: { zeit: '06:00', wort: 'Laden auf' }, h2: 'Die Theke ist voll.', p: ['Um sechs öffnen beide Filialen. Was Sie bis 18 Uhr vorbestellen, liegt am nächsten Morgen mit Ihrem Namen bereit.'], fl: '#EBD8BB', licht: true, bild: { src: 'bilder/foto-hero-mit-leiste-backstube.webp', alt: 'Frisch gebackene, kreuzweise eingeschnittene Brötchen auf Leinen, dahinter eine Reihe auf dem Brett', text: 'Laugenbrötchen am Morgen (Bildbeispiel)' } }
      ],
      fuss: { k: { zeit: '06:15', wort: 'Erste Kundschaft' }, h2: 'Bis morgen früh.', p: ['Vorbestellen geht bis 18 Uhr für den nächsten Tag. Abholung in beiden Filialen ab 6 Uhr.'], cta: 'Vorbestellen', klein: 'Backstube Ohlendorf · Peine · erfundener Betrieb für diese Demo', fl: '#F7F1E6', licht: true }
    }
  };
  const ZEIT_START = 21 * 60 + 30; // 21:30, Auftakt

  const SKIZZE = `<svg class="skizze" viewBox="0 0 520 210" role="img" aria-label="Bleistiftskizze eines Sideboards mit Maßen: 1.800 mm breit, 820 mm hoch, vier Fächer">
      <rect x="40" y="40" width="420" height="120"/><line x1="40" y1="80" x2="460" y2="80"/><line x1="145" y1="80" x2="145" y2="160"/><line x1="250" y1="80" x2="250" y2="160"/><line x1="355" y1="80" x2="355" y2="160"/>
      <line x1="60" y1="160" x2="60" y2="178"/><line x1="440" y1="160" x2="440" y2="178"/>
      <path d="M40 195 H460 M40 189 V201 M460 189 V201"/><text x="250" y="190" text-anchor="middle">1.800</text>
      <path d="M485 40 V160 M479 40 H491 M479 160 H491"/><text x="497" y="104">820</text>
      <path d="M120 58 L128 70 L136 58"/></svg>`;

  function abschnitt(v, d, i, letzte, vorher) {
    const klasse = ['kapitel'];
    if (d.abend) klasse.push('abend');
    if (d.asphalt) klasse.push('asphalt');
    if (d.licht) klasse.push('licht');
    if (letzte) klasse.push('fuss');
    let stil = d.fl ? `--fl:${d.fl};${d.flNach ? `--fl-nach:${d.flNach};` : ''}` : '';
    // Beschriftung über der Kante liegt auf der Fläche davor: deren Schriftfarbe
    if (vorher && (vorher.asphalt || vorher.abend)) stil += '--t-vor:#EDECE8;--leise-vor:#BDBAB2;';
    stil = stil ? ` style="${stil}"` : '';
    const fakten = d.fakten ? `<dl class="fakten">${d.fakten.map(([a, b]) => `<div><dt>${a}</dt><dd>${b}</dd></div>`).join('')}</dl>` : '';
    const bild = d.bild ? `<figure class="bild"><img src="${d.bild.src}" alt="${d.bild.alt}" loading="lazy" decoding="async" width="1600" height="1000"><figcaption>${d.bild.text}</figcaption></figure>` : '';
    const extra = d.extra === 'skizze' ? SKIZZE : '';
    const knopf = d.cta ? `<a class="knopf" href="#kopf">${d.cta} <span aria-hidden="true">→</span></a>` : '';
    const klein = d.klein ? `<p class="fuss__klein">${d.klein}</p>` : '';
    return `<section class="${klasse.join(' ')}" aria-labelledby="h-${i}"${stil}>
      ${formen[v](d.k)}
      <div class="innen zeile">
        <h2 id="h-${i}">${d.h2}</h2>
        <div class="zeile__text">${d.p.map(t => `<p>${t}</p>`).join('')}${fakten}${extra}${bild}${knopf}${klein}</div>
      </div>
    </section>`;
  }

  let v = 'linie';
  function bauen() {
    const w = W[v];
    html.dataset.v = v;
    const liste = [...w.kapitel, w.fuss];
    // zeitachse: jede Fläche kennt die Farbe der nächsten (Verlauf Nacht → Morgen)
    if (v === 'zeitachse') liste.forEach((d, i) => { d.flNach = (liste[i + 1] || {}).fl || d.fl; });
    kopf.innerHTML = `<a class="kopf__marke" href="#kopf">${w.marke}</a><nav aria-label="Hauptnavigation (Demo)">${w.nav.map(n => `<a href="#welt">${n}</a>`).join('')}</nav><a class="knopf" href="#welt">${w.cta}</a>`;
    const auftaktStil = v === 'zeitachse' ? ` style="--fl:#1C1511;--fl-nach:${liste[0].fl}"` : '';
    welt.innerHTML = `<section class="kapitel kapitel--auftakt auftakt" aria-labelledby="h-auf"${auftaktStil}>
        <div class="innen"><p class="label auftakt__ort">${w.auftakt.ort}</p><h1 id="h-auf">${w.auftakt.h1}</h1><p class="auftakt__satz">${w.auftakt.satz}</p></div>
      </section>` + liste.map((d, i) => abschnitt(v, d, i, i === liste.length - 1, liste[i - 1])).join('');
    document.querySelectorAll('[data-wahl]').forEach(b => b.setAttribute('aria-pressed', String(b.dataset.wahl === v)));
    kanten = [...welt.querySelectorAll('.kante')];
    zeiten = v === 'zeitachse' ? [ZEIT_START, ...liste.map(d => minuten(d.k.zeit))] : [];
    takt();
    requestAnimationFrame(messen);
  }
  const minuten = (t) => { const [h, m] = t.split(':').map(Number); let x = h * 60 + m; if (x < ZEIT_START) x += 1440; return x; };
  const uhrText = (x) => { x = Math.round(x) % 1440; return String(Math.floor(x / 60)).padStart(2, '0') + ':' + String(x % 60).padStart(2, '0'); };

  // ---------- Fortschritt (Rückfall und Uhr) ----------
  // Dieselbe Rechnung wie view() mit cover: 0 = Oberkante am unteren Bildrand, 1 = Unterkante am oberen Bildrand
  let kanten = [], zeiten = [], wartet = false;
  const sinus = (t) => 0.5 - 0.5 * Math.cos(Math.PI * t);
  function fortschritt(el) {
    const r = el.getBoundingClientRect(), vh = innerHeight;
    const cover = (vh - r.top) / (vh + r.height);
    return Math.min(1, Math.max(0, (cover - BEREICH[0]) / (BEREICH[1] - BEREICH[0])));
  }
  function takt() {
    wartet = false;
    const rf = html.classList.contains('rueckfall') && !reduziert();
    const ps = kanten.map(fortschritt);
    if (rf) kanten.forEach((el, i) => {
      let p = ps[i];
      if (v === 'woelbung') p = p < .7 ? 1.12 * sinus(p / .7) : 1.12 - .12 * sinus((p - .7) / .3);
      el.style.setProperty('--kp', p.toFixed(4));
    });
    if (v === 'zeitachse') {
      let t = zeiten[0];
      if (reduziert()) { ps.forEach((p, i) => { if (p > 0) t = zeiten[i + 1]; }); }
      else ps.forEach((p, i) => { if (p > 0) t = zeiten[i] + (zeiten[i + 1] - zeiten[i]) * p; });
      uhr.querySelector('.uhr__zeit').textContent = uhrText(t);
      uhr.classList.toggle('morgen', t >= 1440 + 5 * 60 + 30);
      uhr.classList.toggle('da', scrollY > kopf.offsetHeight + 8);
    }
  }
  addEventListener('scroll', () => { if (!wartet) { wartet = true; requestAnimationFrame(takt); } }, { passive: true });
  addEventListener('resize', () => requestAnimationFrame(takt));

  // Messung im Pult: steht animation-range wirklich auf den gesetzten Werten?
  const messung = document.getElementById('messung');
  function messen() {
    const el = kanten[0];
    if (!el) return;
    if (reduziert()) { messung.textContent = 'reduziert: --kp 1, keine Animation'; return; }
    if (html.classList.contains('rueckfall')) { messung.textContent = 'Rückfall: --kp per Skript (' + (kannTimeline ? 'erzwungen' : 'kein animation-timeline') + ')'; return; }
    const cs = getComputedStyle(el);
    messung.textContent = `animation-range ${cs.animationRangeStart} / ${cs.animationRangeEnd} · Zeitleiste ${cs.animationTimeline}`;
  }

  // ---------- Pult ----------
  function setzeVariante(neu, url) {
    if (!W[neu]) neu = 'linie';
    v = neu; bauen(); scrollTo(0, 0);
    if (url) { const u = new URL(location.href); u.searchParams.set('v', neu); history.replaceState(history.state, '', u); }
  }
  document.querySelectorAll('[data-wahl]').forEach(b => b.addEventListener('click', () => { pause(); setzeVariante(b.dataset.wahl, true); }));
  const rfKnopf = document.getElementById('rueckfall');
  if (!kannTimeline) { rfKnopf.disabled = true; rfKnopf.textContent = 'Rückfall aktiv (kein view())'; }
  rfKnopf.addEventListener('click', () => {
    const an = !html.classList.contains('rueckfall');
    html.classList.toggle('rueckfall', an); rfKnopf.setAttribute('aria-pressed', String(an));
    if (!an) kanten.forEach(el => el.style.removeProperty('--kp'));
    takt(); messen();
  });
  new MutationObserver(() => { takt(); messen(); }).observe(html, { attributes: true, attributeFilter: ['class'] });

  // ---------- Vorführung: Selbst-Scroll durch die Seite ----------
  let rahmen = null, schleife = null;
  function neustart() { pause(); scrollTo(0, 0); takt(); }
  function vorfuehren() {
    pause(); scrollTo(0, 0);
    const max = () => document.documentElement.scrollHeight - innerHeight;
    if (reduziert()) { // in Stufen springen statt gleiten
      let i = 0; const ziele = () => [0, ...kanten.map(k => k.closest('.kapitel').offsetTop - innerHeight * .35)];
      const schritt = () => { const z = ziele(); scrollTo(0, Math.max(0, Math.min(max(), z[i % z.length]))); i++; schleife = setTimeout(schritt, 1800); };
      schritt(); return;
    }
    const DAUER = Math.max(9000, kanten.length * 2200), start = performance.now();
    const lauf = (jetzt) => {
      const q = Math.min(1, (jetzt - start) / DAUER);
      scrollTo(0, max() * sinus(q));
      if (q < 1) rahmen = requestAnimationFrame(lauf);
      else schleife = setTimeout(vorfuehren, 1200);
    };
    rahmen = requestAnimationFrame(lauf);
  }
  function pause() { cancelAnimationFrame(rahmen); clearTimeout(schleife); rahmen = schleife = null; }
  function weiter() { vorfuehren(); }

  setzeVariante(new URLSearchParams(location.search).get('v') || 'linie', false);
  document.fonts && document.fonts.ready.then(() => { takt(); messen(); });

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