Richtungsbewusste Übergänge (vorwärts, zurück, hierarchisch)
Die Bewegung zeigt, wohin es geht: hinein in ein Thema, zurück zur Übersicht oder seitlich zum nächsten.
Jede Navigation bekommt eine Klasse: tiefer (Übersicht zu Detail), höher (Detail zu Übersicht), seitlich (Projekt n zu n+1 oder zurück zu n−1) und zurück (Browser-Zurück). Je Klasse läuft eine eigene, gespiegelte Animation. Vorwärts schiebt nach links oder oben, zurück nach rechts oder unten. Tiefer zoomt hinein, höher zoomt heraus. Nativ übernehmen das View-Transition-Typen, in einem JS-Router ein Richtungsflag.
Die Bewegung bildet die Struktur der Seite ab. Man spürt, ob man tiefer geht, zur Übersicht zurückkehrt oder seitlich blättert. Der Zurück-Button fühlt sich wie Zurück an und nicht wie eine neue Seite.
Wann einsetzen
Immer, wenn ein gerichteter Übergang genutzt wird, etwa Schieben, Wipe, Spalten oder Zoom. Besonders bei Seiten mit klarer Hierarchie aus Übersicht, Detail und Unterseite.
Wann nicht
Bei Crossfades ohne Richtung nicht nötig. Bei flachen Seiten ohne erkennbare Ebenen bringt es wenig.
Varianten
- Hierarchisch: Zoom für tiefer und höher, Schieben für seitlich
- Nur vorwärts und zurück, wie Push und Pop in iOS-Apps
- Typen fest in CSS: @view-transition { navigation: auto; types: slide, forwards; }
- Typen in pagereveal aus navigation.activation ableiten (von- und nach-URL, navigationType)
- React: addTransitionType('navigation-forward') mit <ViewTransition default={{...}}>
- JS-Router: Ein popstate-Flag kehrt die Richtung um (inside-out.tech)
Umsetzung
Alle Seiten setzen @view-transition { navigation: auto; }. Die Typen vergibt ein Listener auf der neuen Seite: window.addEventListener('pagereveal', e => { if (!e.viewTransition || !navigation.activation.from) return; const from = new URL(navigation.activation.from.url).pathname; const to = new URL(navigation.activation.entry.url).pathname; e.viewTransition.types.add(depth(to) > depth(from) ? 'deeper' : 'shallower'); if (navigation.activation.navigationType === 'traverse') e.viewTransition.types.add('back'); }). Im CSS bekommt jeder Typ eigene Keyframes, z. B. html:active-view-transition-type(deeper)::view-transition-new(root). Innerhalb einer Seite übergibt man die Typen direkt: document.startViewTransition({ update, types: ['deeper'] }). Fallstricke: Firefox unterstützte Typen in der ersten View-Transition-Implementierung nicht (web.dev). React überspringt Animationen bei popstate, solange der Router nicht die Navigation API nutzt.
Robust umsetzen
Telefon. Entspricht dem gewohnten App-Verhalten (iOS-Push und -Pop) und ist auf dem Telefon besonders wertvoll.
Ohne Bewegung. Die Richtung entfällt, es bleibt ein kurzer Fade.
Barrierefrei. Keine Nachteile, die Richtung hilft bei der Orientierung. Sie ergänzt Brotkrumen und eindeutige Seitentitel, ersetzt sie aber nicht.
Code
<!DOCTYPE html>
<!--
Richtungsbewusste Übergänge (vorwärts, zurück, hierarchisch)
Fundus · Seitenübergänge · https://cecodesign.de/lab/fundus/seitenuebergaenge/richtungsbewusste-uebergaenge/
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>Richtungsbewusste Übergänge · 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: 'Hedvig Letters Serif'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/hedvig-letters-serif:vf@latest/latin-opsz-normal.woff2') format('woff2-variations'); font-weight: 400; 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: '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; }
/* Im echten Einsatz (mehrere HTML-Seiten, gleiche Origin) steht in allen Seiten:
@view-transition { navigation: auto; }
Ein pagereveal-Listener vergibt die Typen (tiefer, hoeher, vor, rueck, zurueck) aus
navigation.activation. Die Demo simuliert die Seiten als Zustände und übergibt dieselben
Typen an document.startViewTransition({ update, types }). Die CSS-Regeln unten gelten 1:1. */
:root {
/* Aueblick, Physiotherapie in Kassel: gedecktes Petrol statt Klinikblau, warmes Off-White, Apricot nur für Hinweise */
--grund: #f5f7f7; --flaeche: #e2e0de; --weiss: #fff; --marke: #005450; --marke-tief: #003d3a;
--text: #363b3b; --leise: #596666; --linie: rgba(54, 59, 59, .12); --akzent: #ffc776; --akzent-tief: #d98f2b;
--minz: #dce9e4; --rand: clamp(16px, 3.4vw, 48px); --kopf: 68px;
--ui: cubic-bezier(0.2, 0, 0.38, 0.9); /* Token tech-präzise aus dem Rezept */
/* Seitenübergang */
--vt-dauer: 400ms; --vt-kurve: cubic-bezier(0.2, 0, 0.38, 0.9);
--versatz: 25%; --zoom-rein: 1.06; --zoom-raus: .94;
--ux: 50vw; --uy: 50vh; /* Ursprung des Zooms, setzt das JS */
}
:root.reduziert { --vt-dauer: 200ms; --vt-kurve: linear; }
* { box-sizing: border-box; margin: 0; padding: 0; }
html { background: var(--grund); color: var(--text); font: 400 17px/1.6 'Google Sans Flex', system-ui, sans-serif; -webkit-font-smoothing: antialiased; text-rendering: optimizeLegibility; }
body { min-height: 100svh; }
a { color: inherit; text-decoration: none; }
button { font: inherit; color: inherit; background: none; border: 0; cursor: pointer; }
svg { display: block; }
h1[tabindex]:focus { outline: none; }
.serif { font-family: 'Hedvig Letters Serif', Georgia, serif; font-weight: 400; font-optical-sizing: auto; }
.klein { font-size: 14px; line-height: 1.5; }
.marker { font: 560 12.5px/1.3 'Google Sans Flex', system-ui, sans-serif; letter-spacing: .08em; text-transform: uppercase; }
.leise { color: var(--leise); }
:focus-visible { outline: 2px solid var(--marke); outline-offset: 3px; border-radius: 6px; }
.knopf { display: inline-flex; align-items: center; justify-content: center; gap: .55em; min-height: 46px; padding: 0 22px; border-radius: 999px;
background: var(--marke); color: #fff; font-weight: 520; font-size: 16px; white-space: nowrap; transition: background-color 200ms var(--ui); }
.knopf:hover { background: var(--marke-tief); }
.knopf--leer { background: transparent; color: var(--marke); box-shadow: inset 0 0 0 1.5px rgba(0, 84, 80, .35); }
.knopf--leer:hover { background: rgba(0, 84, 80, .07); }
.knopf .pfeil { transition: translate 200ms var(--ui); }
.knopf:hover .pfeil { translate: 3px 0; }
/* ---------- Kopf: bleibt über jeden Seitenwechsel stehen ---------- */
.kopf { position: sticky; top: 0; z-index: 10; height: var(--kopf); display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 20px;
padding: 0 var(--rand); background: rgba(245, 247, 247, .94); border-bottom: 1px solid var(--linie); view-transition-name: kopf; }
.marke { display: inline-flex; align-items: center; gap: 11px; white-space: nowrap; }
.marke svg { width: 34px; height: 34px; flex: none; }
.marke b { display: block; font-size: 23px; line-height: 1; letter-spacing: -.01em; color: var(--marke-tief); }
.marke small { display: block; margin-top: 4px; font-size: 12px; line-height: 1; letter-spacing: .04em; color: var(--leise); }
.menue { display: flex; gap: clamp(18px, 2.4vw, 34px); font-size: 15.5px; font-weight: 480; }
.menue a { position: relative; color: var(--leise); padding: 6px 0; transition: color 150ms var(--ui); }
.menue a:hover, .menue a[aria-current] { color: var(--text); }
.menue a[aria-current]::after { content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 2px; border-radius: 2px; background: var(--marke); }
.kopf__rechts { justify-self: end; display: flex; align-items: center; gap: 18px; }
.tel { display: inline-flex; align-items: center; gap: 8px; font-weight: 520; font-size: 15.5px; color: var(--marke-tief); white-space: nowrap; font-variant-numeric: tabular-nums; }
.tel svg { width: 18px; height: 18px; }
.kopf .knopf { min-height: 42px; padding: 0 18px; font-size: 15px; gap: 0; }
/* ---------- Seiten ---------- */
.seite { max-width: 1320px; margin: 0 auto; padding: 0 var(--rand) clamp(150px, 20vh, 190px); }
.pfadzeile { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 10px 24px; min-height: 64px; padding: 14px 0; }
.pfad { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 0; font-size: 14.5px; color: var(--leise); }
.pfad a { padding: 4px 2px; border-radius: 4px; transition: color 150ms var(--ui); }
.pfad a:hover { color: var(--marke); text-decoration: underline; text-underline-offset: .2em; }
.pfad span[aria-current] { color: var(--text); font-weight: 520; padding: 4px 2px; }
.pfad i { font-style: normal; padding: 0 9px; color: rgba(89, 102, 102, .5); }
.pfad i::before { content: '/'; } /* Trenner rein dekorativ (WCAG 1.4.3 nimmt Dekor aus): per CSS, nicht als Text */
.blaettern { display: flex; gap: 8px; }
.blaettern > a, .blaettern > span { display: inline-flex; align-items: center; gap: 8px; height: 38px; padding: 0 15px; border-radius: 999px; font-size: 14.5px; font-weight: 500;
background: var(--weiss); box-shadow: inset 0 0 0 1px var(--linie); transition: box-shadow 150ms var(--ui), color 150ms var(--ui); white-space: nowrap; }
.blaettern > a:hover { box-shadow: inset 0 0 0 1.5px rgba(0, 84, 80, .45); color: var(--marke); }
.blaettern > span { color: rgba(89, 102, 102, .45); background: transparent; box-shadow: inset 0 0 0 1px rgba(54, 59, 59, .07); }
.blaettern b { font-weight: 500; color: var(--leise); font-variant-numeric: tabular-nums; }
.wisch { display: none; }
/* Übersicht */
.einstieg { display: grid; grid-template-columns: minmax(0, 1.25fr) minmax(260px, 1fr); align-items: end; gap: 18px clamp(28px, 5vw, 80px); padding: clamp(10px, 2.4vh, 28px) 0 clamp(26px, 4.4vh, 48px); }
h1 { font-size: clamp(2.25rem, 1.4rem + 3.2vw, 4.5rem); line-height: 1.08; letter-spacing: -.01em; text-wrap: balance; color: var(--marke-tief); }
h1 .ton { color: var(--marke); background: linear-gradient(transparent 68%, rgba(255, 199, 118, .55) 68%, rgba(255, 199, 118, .55) 92%, transparent 92%); }
.einstieg p { max-width: 44ch; color: var(--leise); text-wrap: pretty; }
.hinweis { display: inline-flex; align-items: center; gap: 10px; margin-top: 14px; padding: 7px 14px 7px 10px; border-radius: 999px; background: rgba(255, 199, 118, .32); color: #6b4a14; font-size: 14.5px; font-weight: 500; }
.hinweis i { width: 8px; height: 8px; border-radius: 50%; background: var(--akzent-tief); }
.raster { list-style: none; display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: clamp(14px, 1.6vw, 24px); }
.karte { display: grid; gap: 14px; height: 100%; padding: 10px 10px 18px; border-radius: 26px; background: var(--weiss); box-shadow: 0 1px 0 rgba(54, 59, 59, .04), inset 0 0 0 1px var(--linie); transition: box-shadow 250ms var(--ui); }
.karte__bild { display: block; overflow: hidden; border-radius: 18px; aspect-ratio: 4 / 3; }
.karte__bild svg { width: 100%; height: 100%; transition: scale 600ms cubic-bezier(0.37, 0, 0.63, 1); }
.karte__text { display: grid; gap: 3px; padding: 0 8px; }
.karte__titel { font-size: clamp(20px, 1.6vw, 24px); line-height: 1.2; color: var(--marke-tief); }
.karte__fach { font-size: 14px; line-height: 1.45; color: var(--leise); }
.karte__kurz { margin-top: 6px; font-size: 15px; line-height: 1.45; }
@media (hover: hover) {
.karte:hover { box-shadow: 0 14px 34px -18px rgba(0, 61, 58, .35), inset 0 0 0 1.5px rgba(0, 84, 80, .3); }
.karte:hover .karte__bild svg { scale: 1.04; }
}
.fuss { display: grid; grid-template-columns: repeat(3, minmax(0, auto)); justify-content: space-between; gap: 14px 28px; margin-top: clamp(40px, 7vh, 80px); padding-top: 20px; border-top: 1px solid var(--linie); font-size: 14.5px; color: var(--leise); }
.fuss b { display: block; color: var(--text); font-weight: 560; }
/* Behandlung */
.behandlung { display: grid; grid-template-columns: minmax(0, 6fr) minmax(0, 5fr); gap: clamp(28px, 5vw, 80px); align-items: start; padding-top: clamp(4px, 1.6vh, 20px); }
.behandlung__nr { color: var(--marke); }
.behandlung h1 { margin-top: 12px; }
.behandlung__fach { margin-top: 10px; font-size: 16px; color: var(--leise); }
.behandlung__lauf { margin-top: 18px; max-width: 54ch; text-wrap: pretty; }
.tun { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 24px; }
.fakten { margin-top: clamp(24px, 4vh, 40px); max-width: 560px; font-size: 15.5px; }
.fakten div { display: grid; grid-template-columns: 7.5em 1fr; gap: 16px; padding: 11px 0; border-top: 1px solid var(--linie); }
.fakten dt { color: var(--leise); }
.held { overflow: hidden; border-radius: 30px; aspect-ratio: 4 / 3.4; }
.held svg { width: 100%; height: 100%; }
/* Übungen */
.uebungen__kopf { display: grid; grid-template-columns: minmax(0, 1.2fr) minmax(240px, 1fr); align-items: end; gap: 16px clamp(28px, 5vw, 80px); padding: clamp(4px, 1.6vh, 20px) 0 clamp(24px, 4vh, 40px); }
.uebungen__kopf p { max-width: 44ch; color: var(--leise); }
.uebungen { list-style: none; display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: clamp(14px, 1.6vw, 24px); counter-reset: u; }
.uebung { display: grid; grid-template-rows: auto auto 1fr auto; gap: 8px; padding: 24px 24px 22px; border-radius: 26px; background: var(--weiss); box-shadow: inset 0 0 0 1px var(--linie); }
.uebung__nr { font-size: 54px; line-height: 1; color: var(--marke); }
.uebung h2 { font-size: 24px; line-height: 1.2; color: var(--marke-tief); margin-top: 8px; }
.uebung p { font-size: 15.5px; line-height: 1.55; text-wrap: pretty; }
.uebung__dosis { justify-self: start; margin-top: 8px; padding: 5px 12px; border-radius: 999px; background: var(--minz); color: var(--marke-tief); font-size: 14px; font-weight: 560; font-variant-numeric: tabular-nums; }
.vorsicht { display: flex; gap: 14px; align-items: flex-start; margin-top: 20px; padding: 16px 20px; border-radius: 18px; background: rgba(255, 199, 118, .26); color: #5a4214; font-size: 15.5px; max-width: 760px; }
.vorsicht svg { flex: none; width: 22px; height: 22px; margin-top: 2px; }
.vorsicht a { font-weight: 600; text-decoration: underline; text-underline-offset: .18em; }
/* ---------- View Transitions (gelten genauso mit @view-transition zwischen echten Seiten) ---------- */
::view-transition-group(root),
::view-transition-old(root),
::view-transition-new(root) { animation-duration: var(--vt-dauer); animation-timing-function: var(--vt-kurve); }
::view-transition-old(root),
::view-transition-new(root) { transform-origin: var(--ux) var(--uy); }
/* tiefer: in das geklickte Element hineinzoomen */
html:active-view-transition-type(tiefer) {
&::view-transition-old(root) { animation-name: alt-rein; }
&::view-transition-new(root) { animation-name: neu-rein; }
}
/* höher: aus der Seite heraus auf die Stelle in der Übersicht zoomen */
html:active-view-transition-type(hoeher) {
&::view-transition-old(root) { animation-name: alt-raus; }
&::view-transition-new(root) { animation-name: neu-raus; }
}
/* seitlich und Variante „Vor/Zurück“: vorwärts nach links, zurück nach rechts */
html:active-view-transition-type(vor, vorwaerts) {
&::view-transition-old(root) { animation-name: alt-links; }
&::view-transition-new(root) { animation-name: neu-rechts; }
}
html:active-view-transition-type(rueck, rueckwaerts) {
&::view-transition-old(root) { animation-name: alt-rechts; }
&::view-transition-new(root) { animation-name: neu-links; }
}
/* Alt blendet bis 50 % aus, Neu erst ab 30 % ein: kein doppelt belichteter Text */
@keyframes alt-rein { 50% { opacity: 0; } to { opacity: 0; transform: scale(var(--zoom-rein)); } }
@keyframes neu-rein { from { opacity: 0; transform: scale(var(--zoom-raus)); } 30% { opacity: 0; } }
@keyframes alt-raus { 50% { opacity: 0; } to { opacity: 0; transform: scale(var(--zoom-raus)); } }
@keyframes neu-raus { from { opacity: 0; transform: scale(var(--zoom-rein)); } 30% { opacity: 0; } }
@keyframes alt-links { 50% { opacity: 0; } to { opacity: 0; transform: translateX(calc(var(--versatz) * -1)); } }
@keyframes neu-rechts { from { opacity: 0; transform: translateX(var(--versatz)); } 30% { opacity: 0; } }
@keyframes alt-rechts { 50% { opacity: 0; } to { opacity: 0; transform: translateX(var(--versatz)); } }
@keyframes neu-links { from { opacity: 0; transform: translateX(calc(var(--versatz) * -1)); } 30% { opacity: 0; } }
/* Kopf, Steuerung und Zeiger stehen still */
::view-transition-group(kopf), ::view-transition-group(dock), ::view-transition-group(zeiger) { animation: none; }
::view-transition-group(kopf) { z-index: 2; }
::view-transition-group(dock) { z-index: 3; }
::view-transition-group(zeiger) { z-index: 4; }
::view-transition-old(kopf), ::view-transition-old(dock), ::view-transition-old(zeiger) { display: none; }
::view-transition-new(kopf), ::view-transition-new(dock), ::view-transition-new(zeiger) { animation: none; }
/* Reduzierte Bewegung: das JS vergibt keine Typen, es bleibt der Root-Crossfade mit 200 ms */
/* ---------- Demo-Steuerung ---------- */
.dock { position: fixed; z-index: 40; left: 50%; bottom: 16px; translate: -50% 0; width: max-content; max-width: calc(100% - 24px);
display: grid; gap: 10px; padding: 12px 14px; border-radius: 18px; color: var(--text);
background: rgba(255, 255, 255, .9); -webkit-backdrop-filter: blur(16px) saturate(1.3); backdrop-filter: blur(16px) saturate(1.3);
box-shadow: 0 18px 50px -20px rgba(0, 45, 42, .35), inset 0 0 0 1px rgba(54, 59, 59, .1); view-transition-name: dock; }
.label { font: 500 10.5px/1.2 'Geist Mono', ui-monospace, monospace; letter-spacing: .14em; text-transform: uppercase; }
.dock__zeile { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.dock__zeile > span.label { color: var(--leise); }
.modi, .verlauf { display: flex; gap: 4px; }
.modi button, .verlauf button { height: 30px; padding: 0 12px; border-radius: 999px; color: var(--leise); box-shadow: inset 0 0 0 1px rgba(54, 59, 59, .16); transition: background-color 150ms var(--ui), color 150ms var(--ui); }
.modi button:hover, .verlauf button:hover:not(:disabled) { color: var(--text); background: rgba(0, 84, 80, .06); }
.modi button[aria-pressed='true'] { background: var(--marke); color: #fff; box-shadow: none; }
.verlauf { margin-left: auto; }
.verlauf button { padding: 0 11px; display: inline-flex; align-items: center; gap: 6px; }
.verlauf button:disabled { opacity: .38; cursor: default; }
.reduziert .modi { opacity: .55; }
.dock__unten { display: flex; align-items: center; gap: 14px; }
.karte-mini { flex: none; width: 152px; height: 56px; }
.werte { font: 400 11px/1.6 'Geist Mono', ui-monospace, monospace; color: var(--leise); font-variant-numeric: tabular-nums; min-width: 0; }
.werte > span { display: block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.werte .nw { white-space: nowrap; }
.werte b { color: var(--marke-tief); font-weight: 600; }
.werte .typ { display: inline-block; padding: 0 6px; border-radius: 4px; background: rgba(255, 199, 118, .45); color: #5a4214; font-weight: 600; }
/* Gedachter Zeiger für die Vorführung */
.zeiger { position: fixed; z-index: 50; left: 0; top: 0; width: 26px; height: 26px; margin: -13px 0 0 -13px; border-radius: 50%; border: 1.5px solid var(--marke-tief); background: rgba(0, 84, 80, .14); pointer-events: none; opacity: 0; transition: opacity 150ms linear; view-transition-name: zeiger; }
.zeiger.an { opacity: 1; }
:root.reduziert * { transition-duration: .01ms !important; }
@media (forced-colors: active) { button, [role=button], [role=tab], [role=switch] { border: 1px solid ButtonText !important; } }
/* Galerie-Vorschau: nur die Bühne */
.vorschau body { overflow: hidden; }
.vorschau .werte .lang { display: none; }
@media (hover: none) {
.modi button, .verlauf button { height: 34px; }
.wisch { display: inline; }
}
@media (max-width: 1180px) {
.tel span { display: none; }
.menue { gap: 20px; }
}
@media (max-width: 1020px) {
.raster { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.karte__kurz { display: none; }
}
@media (max-width: 900px) {
.menue { display: none; }
.kopf { grid-template-columns: 1fr auto; }
.einstieg, .uebungen__kopf { grid-template-columns: minmax(0, 1fr); }
.behandlung { grid-template-columns: minmax(0, 1fr); gap: 22px; }
.held { order: -1; aspect-ratio: 16 / 9; border-radius: 22px; }
.uebungen { grid-template-columns: minmax(0, 1fr); }
.fuss { grid-template-columns: minmax(0, 1fr); }
}
@media (max-width: 620px) {
:root { --kopf: 60px; }
.marke b { font-size: 20px; }
.marke svg { width: 30px; height: 30px; }
.kopf .knopf { min-height: 38px; padding: 0 14px; }
.kopf .knopf .lang-text { display: none; }
.raster { grid-template-columns: minmax(0, 1fr); gap: 10px; }
.karte { grid-template-columns: 112px minmax(0, 1fr); align-items: center; padding: 8px; border-radius: 20px; gap: 12px; }
.karte__bild { border-radius: 14px; aspect-ratio: 1; }
.karte__text { padding: 0; }
.karte__titel { font-size: 19px; }
.pfadzeile { min-height: 0; padding: 12px 0 14px; }
.blaettern { width: 100%; }
.blaettern > a, .blaettern > span { flex: 1 1 0; min-width: 0; justify-content: center; height: 36px; padding: 0 10px; font-size: 13.5px; }
.blaettern .name { overflow: hidden; text-overflow: ellipsis; }
.tun .knopf { flex: 1 1 auto; }
.uebung { padding: 20px; }
.uebung__nr { font-size: 42px; }
.dock { width: calc(100% - 20px); bottom: 10px; padding: 10px; gap: 8px; }
.dock__zeile > span.label { display: none; }
.dock__zeile { flex-wrap: nowrap; gap: 8px; }
@media (max-width: 360px) { .kopf, .kopf__rechts { gap: 10px; } .dock__zeile { flex-wrap: wrap; } }
.modi button { padding: 0 8px; letter-spacing: .06em; }
.modi .lang-text { display: none; }
.verlauf button { padding: 0 10px; }
.verlauf { margin-left: auto; }
.verlauf .lang-text { display: none; }
.karte-mini { width: 112px; height: 42px; }
.werte .lang { display: none; }
.werte > span { white-space: normal; }
}
</style>
</head>
<body>
<header class="kopf">
<a class="marke" href="#/behandlungen" data-ziel="/behandlungen" aria-label="Aueblick Physiotherapie, Behandlungen">
<svg viewBox="0 0 34 34" aria-hidden="true"><circle cx="17" cy="17" r="17" fill="#005450"/><path d="M8.5 17.5a8.5 8.5 0 0 1 17 0" fill="none" stroke="#ffc776" stroke-width="2.4" stroke-linecap="round"/><path d="M6 22.2c3.4-2.4 6.8-2.4 11 0s7.6 2.4 11 0M8.5 26.6c2.8-1.8 5.6-1.8 8.5 0s5.7 1.8 8.5 0" fill="none" stroke="#dce9e4" stroke-width="2" stroke-linecap="round"/></svg>
<span><b class="serif">Aueblick</b><small>Physiotherapie · Kassel</small></span>
</a>
<nav class="menue" aria-label="Hauptmenü">
<a href="#/behandlungen" data-ziel="/behandlungen" aria-current="page">Behandlungen</a>
<a href="#team" data-tot>Team</a>
<a href="#praxis" data-tot>Praxis</a>
<a href="#kosten" data-tot>Kosten und Kasse</a>
</nav>
<div class="kopf__rechts">
<a class="tel" href="tel:+495619204730" aria-label="Anrufen: 0561 920 47 30"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6.6 3.5h2.6l1.4 4.1-2 1.4a12.2 12.2 0 0 0 6.4 6.4l1.4-2 4.1 1.4v2.6A2.1 2.1 0 0 1 18.4 20 15.5 15.5 0 0 1 4 5.6a2.1 2.1 0 0 1 2.6-2.1Z"/></svg><span>0561 920 47 30</span></a>
<a class="knopf" href="#termin" data-tot>Termin<span class="lang-text"> vereinbaren</span></a>
</div>
</header>
<main id="seite"></main>
<aside class="dock" aria-label="Demo-Steuerung">
<div class="dock__zeile">
<span class="label">Übergang</span>
<div class="modi label" role="group" aria-label="Art des Übergangs">
<button type="button" data-variante="hierarchisch" aria-pressed="true">Hierarchisch</button>
<button type="button" data-variante="schieben" aria-pressed="false">Vor/Zurück</button>
<button type="button" data-variante="ohne" aria-pressed="false">Ohne<span class="lang-text"> Richtung</span></button>
</div>
<div class="verlauf label" role="group" aria-label="Browser-Verlauf (simuliert)">
<button type="button" data-verlauf="zurueck" aria-label="Browser zurück" disabled><span aria-hidden="true">←</span><span class="lang-text">Zurück</span></button>
<button type="button" data-verlauf="vor" aria-label="Browser vorwärts" disabled><span aria-hidden="true">→</span></button>
</div>
</div>
<div class="dock__unten">
<svg class="karte-mini" viewBox="0 0 152 56" aria-hidden="true" data-mini></svg>
<p class="werte" data-werte aria-live="polite"></p>
</div>
</aside>
<div class="zeiger" aria-hidden="true"></div>
<script>
(() => {
const html = document.documentElement;
const reduziert = () => html.classList.contains('reduziert');
const seite = document.getElementById('seite');
const zeiger = document.querySelector('.zeiger');
const werteFeld = document.querySelector('[data-werte]');
const mini = document.querySelector('[data-mini]');
const kannVT = typeof document.startViewTransition === 'function';
/* Werte: 400 ms, Kurve tech-präzise aus dem Rezept; Versatz 25 % (Messwert: typisch 20–30 % plus Fade) */
const W = { dauer: 400, kurve: 'cubic-bezier(0.2, 0, 0.38, 0.9)', versatz: '25 %', zoom: [0.94, 1.06] };
const WR = { dauer: 200, kurve: 'linear' };
let variante = 'hierarchisch';
/* ---------- Bildwelt: eigene SVG-Landschaften in den Praxisfarben ---------- */
function bild(id, alt) {
const a = alt ? `role="img" aria-label="${alt}"` : 'aria-hidden="true"';
const k = `<svg viewBox="0 0 400 300" preserveAspectRatio="xMidYMid slice" ${a}>`;
if (id === 'ruecken') {
// Wirbelsäule als Reihe weicher Kapseln entlang einer ruhigen S-Kurve
let wirbel = '';
for (let i = 0; i < 11; i++) {
const t = i / 10, y = 50 + t * 196, x = 200 + Math.sin(t * Math.PI * 1.5 - .4) * 18, w = 22 + t * 26, r = Math.cos(t * Math.PI * 1.5 - .4) * 9;
wirbel += `<rect x="${(x - w / 2).toFixed(1)}" y="${(y - 5 - t * 2.5).toFixed(1)}" width="${w.toFixed(1)}" height="${(10 + t * 5).toFixed(1)}" rx="${(5 + t * 2.5).toFixed(1)}" transform="rotate(${r.toFixed(1)} ${x.toFixed(1)} ${y.toFixed(1)})" fill="${i % 2 ? '#2f7a72' : '#005450'}"/>`;
}
return k + `<rect width="400" height="300" fill="#dce9e4"/>
<circle cx="304" cy="86" r="46" fill="#ffc776"/>
<path d="M0 236c70-34 130-30 200-6s132 24 200-8v78H0Z" fill="#bcd7cd"/>
<path d="M0 262c80-22 150-16 216 2s124 12 184-6v42H0Z" fill="#9dbfb3"/>
<path d="M68 70a132 132 0 0 1 132-48" fill="none" stroke="#005450" stroke-width="1.5" stroke-linecap="round" opacity=".35"/>
${wirbel}</svg>`;
}
if (id === 'knie') {
// Weg über zwei Hügel: Treppen und lange Wege
return k + `<rect width="400" height="300" fill="#e9e4da"/>
<path d="M40 300a170 170 0 0 1 340 0" fill="none" stroke="#005450" stroke-width="1.5" opacity=".3"/>
<circle cx="96" cy="84" r="30" fill="#ffc776"/>
<path d="M-10 214C60 150 120 140 178 176s92 34 132-4 74-40 100-24v152H-10Z" fill="#c9dcd3"/>
<path d="M-10 250c74-46 150-50 214-18s126 22 206-24v92H-10Z" fill="#8fb8ab"/>
<path d="M-10 276c90-26 170-24 240-4s120 10 180-6v34H-10Z" fill="#5e8c82"/>
<path d="M120 300c10-30 40-46 84-58s70-34 92-66" fill="none" stroke="#f5f7f7" stroke-width="10" stroke-linecap="round" stroke-dasharray="1 20"/>
<circle cx="296" cy="176" r="8" fill="#005450"/><path d="M296 176v-24" stroke="#005450" stroke-width="3" stroke-linecap="round"/><path d="M296 152h18l-5 6 5 6h-18" fill="#ffc776"/>
</svg>`;
}
if (id === 'nach-op') {
// Stufen, die langsam höher werden, die Sonne geht über der letzten auf
const stufe = (i, h, f) => `<rect x="${70 + i * 54}" y="${262 - h}" width="46" height="${h + 40}" rx="12" fill="${f}"/>`;
return k + `<rect width="400" height="300" fill="#f1e7d8"/>
<path d="M70 230C140 210 220 140 330 70" fill="none" stroke="#005450" stroke-width="1.5" stroke-dasharray="2 7" stroke-linecap="round" opacity=".5"/>
<circle cx="332" cy="68" r="28" fill="#ffc776"/>
${stufe(0, 34, '#cfe0d9')}${stufe(1, 62, '#9dbfb3')}${stufe(2, 96, '#5e8c82')}${stufe(3, 134, '#2f7a72')}${stufe(4, 176, '#005450')}
<rect x="0" y="262" width="400" height="38" fill="#e2d6c3"/>
</svg>`;
}
// Schwindel: Steinmännchen im Gleichgewicht
return k + `<rect width="400" height="300" fill="#dde7e8"/>
<circle cx="200" cy="150" r="112" fill="#eef3f3"/>
<path d="M40 244h320" stroke="#005450" stroke-width="1.5" opacity=".35" stroke-linecap="round"/>
<ellipse cx="200" cy="250" rx="120" ry="10" fill="#c9d7d6"/>
<ellipse cx="198" cy="222" rx="74" ry="26" fill="#5e8c82"/>
<ellipse cx="206" cy="178" rx="56" ry="21" fill="#9dbfb3"/>
<ellipse cx="196" cy="141" rx="40" ry="17" fill="#c9b79c"/>
<ellipse cx="203" cy="110" rx="25" ry="13" fill="#ffc776"/>
<path d="M156 214c10-8 30-12 50-11M176 172c8-6 22-8 36-7" fill="none" stroke="#fff" stroke-width="3" stroke-linecap="round" opacity=".35"/>
</svg>`;
}
/* ---------- Inhalte ---------- */
const THEMEN = [
{ id: 'ruecken', titel: 'Rücken und Nacken', fach: 'Krankengymnastik · Manuelle Therapie', kurz: 'Wenn Sitzen, Heben oder Schlafen wehtut.',
text: 'Im ersten Termin fragen wir, wann es wehtut und was Sie im Alltag einschränkt. Dann prüfen wir, wie beweglich Wirbelsäule und Schultern sind. Sie gehen mit zwei, drei Übungen nach Hause, die Sie wirklich schaffen.',
fakten: [['Dauer', '25 Minuten je Termin'], ['Rezept', 'Krankengymnastik (KG) oder Manuelle Therapie (MT)'], ['Kosten', 'Mit Rezept zahlt die Kasse. Ohne Rezept ab 38 €.']],
uebungen: [['Katzenbuckel', 'Im Vierfüßlerstand den Rücken langsam rund machen. Dann sanft durchhängen lassen.', '10 × ruhig'],
['Kinn zurück', 'Aufrecht sitzen und das Kinn waagerecht nach hinten schieben. Fünf Sekunden halten.', '3 × 8'],
['Becken kippen', 'Auf dem Rücken liegen, Beine aufgestellt. Den unteren Rücken in die Unterlage drücken und lösen.', '3 × 10']] },
{ id: 'knie', titel: 'Knie und Hüfte', fach: 'Krankengymnastik am Gerät (KGG)', kurz: 'Wenn Treppen und lange Wege schwerfallen.',
text: 'Oft fehlt den Muskeln rund ums Gelenk die Kraft, nicht dem Gelenk selbst. An unseren Geräten bauen wir diese Kraft Schritt für Schritt auf. Wie viel Gewicht Sie bewegen, bestimmen Sie mit.',
fakten: [['Dauer', '60 Minuten, höchstens drei Personen'], ['Rezept', 'Krankengymnastik am Gerät (KGG)'], ['Kosten', 'Mit Rezept zahlt die Kasse. Ohne Rezept ab 52 €.']],
uebungen: [['Aufstehen ohne Hände', 'Langsam vom Stuhl aufstehen und wieder setzen. Die Arme bleiben vor der Brust.', '3 × 8'],
['Brücke', 'Auf dem Rücken liegen, Füße aufgestellt. Das Becken heben, bis Knie, Hüfte und Schultern eine Linie bilden.', '3 × 10'],
['Fersen heben', 'An der Arbeitsplatte festhalten und langsam auf die Zehenspitzen gehen.', '3 × 12']] },
{ id: 'nach-op', titel: 'Nach einer Operation', fach: 'Reha nach Gelenkersatz, Kreuzbandriss oder Bruch', kurz: 'Wenn Sie nach der Klinik wieder sicher werden wollen.',
text: 'Wir lesen den OP-Bericht und sprechen bei Bedarf mit Ihrer Klinik. In den ersten Wochen geht es um Schwellung und Beweglichkeit, danach um Kraft. Das Ziel legen wir gemeinsam fest, zum Beispiel wieder allein einkaufen.',
fakten: [['Dauer', '25 oder 60 Minuten, je nach Rezept'], ['Rezept', 'KG, KGG oder Lymphdrainage (MLD)'], ['Hausbesuch', 'Möglich, solange Sie noch nicht mobil sind.']],
uebungen: [['Fußwippe', 'Im Liegen die Füße abwechselnd zu sich ziehen und strecken. Das hilft gegen Schwellung.', '2 Minuten'],
['Knie strecken', 'Eine Rolle unter das Knie legen. Den Unterschenkel heben, bis das Bein gerade ist.', '3 × 10'],
['Gehen mit Plan', 'Jeden Tag ein paar Minuten länger gehen als am Vortag. Schreiben Sie die Minuten auf.', 'täglich']] },
{ id: 'schwindel', titel: 'Schwindel und Gleichgewicht', fach: 'Vestibuläre Rehabilitation', kurz: 'Wenn sich der Boden unsicher anfühlt.',
text: 'Zuerst klären wir, woher der Schwindel kommt. Manche Formen lassen sich mit einem einfachen Lagerungsmanöver lösen. Bei anderen trainieren wir Augen, Kopf und Gleichgewicht, bis der Alltag wieder sicher ist.',
fakten: [['Dauer', '25 Minuten je Termin'], ['Rezept', 'Krankengymnastik (KG) mit Vermerk Schwindel'], ['Kosten', 'Mit Rezept zahlt die Kasse. Ohne Rezept ab 38 €.']],
uebungen: [['Blick halten', 'Einen Punkt an der Wand ansehen und den Kopf langsam drehen. Der Blick bleibt auf dem Punkt.', '30 Sekunden'],
['Fuß vor Fuß', 'An einer Wand entlanggehen und die Ferse direkt vor die Zehen setzen.', '2 × 10 Schritte'],
['Einbeinstand', 'Beim Zähneputzen auf einem Bein stehen. Eine Hand bleibt am Waschbecken.', '2 × 30 Sek.']] }
];
const UEBER = '/behandlungen';
const teile = p => p.split('/').filter(Boolean);
const tiefe = p => teile(p).length;
const themaVon = p => THEMEN.find(t => t.id === teile(p)[1]);
const index = p => THEMEN.indexOf(themaVon(p));
const pfadText = p => p.replace(UEBER, '/behandlungen');
const krumen = liste => `<nav class="pfad" aria-label="Brotkrumen">${liste.map(([t, z], i) => (i ? '<i aria-hidden="true"></i>' : '') + (z ? `<a href="#${z}" data-ziel="${z}">${t}</a>` : `<span aria-current="page">${t}</span>`)).join('')}</nav>`;
function seiteUebersicht() {
return `<div class="seite" data-seite="${UEBER}">
<div class="pfadzeile">${krumen([['Start', null], ['Behandlungen', null]]).replace('<span aria-current="page">Start</span>', '<a href="#start" data-tot>Start</a>')}<span class="klein leise">4 Schwerpunkte · alle Kassen und privat</span></div>
<section class="einstieg">
<h1 class="serif" tabindex="-1">Wobei wir Ihnen <span class="ton">helfen</span>.</h1>
<div><p>Sie brauchen kein Fachwissen. Wählen Sie, wo es wehtut. Auf jeder Seite steht, wie die Behandlung abläuft und was die Kasse zahlt.</p>
<span class="hinweis"><i aria-hidden="true"></i>Neue Termine meist innerhalb einer Woche</span></div>
</section>
<ul class="raster">${THEMEN.map(t => `
<li><a class="karte" href="#${UEBER}/${t.id}" data-ziel="${UEBER}/${t.id}" data-ursprung="${UEBER}/${t.id}">
<span class="karte__bild">${bild(t.id)}</span>
<span class="karte__text"><b class="karte__titel serif">${t.titel}</b><span class="karte__fach">${t.fach}</span><span class="karte__kurz">${t.kurz}</span></span>
</a></li>`).join('')}
</ul>
<footer class="fuss"><span><b>Sprechzeiten</b>Mo bis Do 7:30–19 Uhr · Fr 7:30–14 Uhr</span><span><b>Am Auedamm 14, 34121 Kassel</b>Ebenerdiger Eingang, Aufzug, Parkplätze am Haus</span><span><b>Im Notfall</b>Bereitschaftsdienst 116 117 · Notruf 112</span></footer>
</div>`;
}
function seiteThema(t) {
const i = THEMEN.indexOf(t), v = THEMEN[i - 1], n = THEMEN[i + 1];
const blatt = (x, r) => x ? `<a href="#${UEBER}/${x.id}" data-ziel="${UEBER}/${x.id}" data-seitlich="${r}">${r === 'rueck' ? '<span aria-hidden="true">←</span> ' : ''}<span class="name">${x.titel}</span>${r === 'vor' ? ' <span aria-hidden="true">→</span>' : ''}</a>`
: `<span aria-hidden="true">${r === 'rueck' ? '← Anfang' : 'Ende →'}</span>`;
return `<div class="seite" data-seite="${UEBER}/${t.id}">
<div class="pfadzeile">${krumen([['Behandlungen', UEBER], [t.titel, null]])}<div class="blaettern" aria-label="Weitere Behandlungen">${blatt(v, 'rueck')}${blatt(n, 'vor')}</div></div>
<article class="behandlung">
<div>
<p class="behandlung__nr marker">Behandlung ${i + 1} von ${THEMEN.length}<span class="wisch leise"> · wischen zum Blättern</span></p>
<h1 class="serif" tabindex="-1">${t.titel}</h1>
<p class="behandlung__fach">${t.fach}</p>
<p class="behandlung__lauf">${t.text}</p>
<div class="tun"><a class="knopf" href="#termin" data-tot>Termin vereinbaren</a><a class="knopf knopf--leer" href="#${UEBER}/${t.id}/uebungen" data-ziel="${UEBER}/${t.id}/uebungen" data-ursprung="${UEBER}/${t.id}/uebungen">Übungen für zu Hause <span class="pfeil" aria-hidden="true">→</span></a></div>
<dl class="fakten">${t.fakten.map(([a, b]) => `<div><dt>${a}</dt><dd>${b}</dd></div>`).join('')}</dl>
</div>
<figure class="held">${bild(t.id, 'Illustration zu ' + t.titel)}</figure>
</article>
</div>`;
}
function seiteUebungen(t) {
return `<div class="seite" data-seite="${UEBER}/${t.id}/uebungen">
<div class="pfadzeile">${krumen([['Behandlungen', UEBER], [t.titel, `${UEBER}/${t.id}`], ['Übungen', null]])}<span class="klein leise">Stand: Oktober 2026 · geprüft vom Praxisteam</span></div>
<section class="uebungen__kopf">
<h1 class="serif" tabindex="-1">Drei Übungen für zu Hause.</h1>
<p>Für die Zeit zwischen den Terminen: ${t.titel}. Üben Sie nur, solange es nicht wehtut. Lieber jeden Tag kurz als einmal lang.</p>
</section>
<ol class="uebungen">${t.uebungen.map(([titel, text, dosis], i) => `
<li class="uebung"><span class="uebung__nr serif" aria-hidden="true">${i + 1}</span><h2 class="serif">${titel}</h2><p>${text}</p><span class="uebung__dosis">${dosis}</span></li>`).join('')}
</ol>
<p class="vorsicht"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" aria-hidden="true"><circle cx="12" cy="12" r="9.5"/><path d="M12 7.5v5.5M12 16.5v.01"/></svg><span>Starke Schmerzen, Taubheit oder Schwindel beim Üben? Hören Sie auf und rufen Sie uns an: <a href="tel:+495619204730">0561 920 47 30</a>.</span></p>
</div>`;
}
function vorlage(p) {
const t = themaVon(p);
if (!t) return seiteUebersicht();
return tiefe(p) === 3 ? seiteUebungen(t) : seiteThema(t);
}
function titel(p) {
const t = themaVon(p);
return (!t ? 'Behandlungen' : tiefe(p) === 3 ? 'Übungen: ' + t.titel : t.titel) + ' · Aueblick Physiotherapie Kassel';
}
/* ---------- Klassifizieren: tiefer, höher, seitlich (vor/rueck), dazu zurück im Verlauf ---------- */
function klasse(von, nach) {
const dv = tiefe(von), dn = tiefe(nach);
if (dn > dv) return 'tiefer';
if (dn < dv) return 'hoeher';
return index(nach) > index(von) ? 'vor' : 'rueck';
}
function typenFuer(k, traverse) {
if (reduziert() || !kannVT || variante === 'ohne') return [];
if (variante === 'hierarchisch') return traverse === 'zurueck' ? [k, 'zurueck'] : [k];
// Vor/Zurück wie iOS-Push und -Pop: Browser-Zurück schiebt immer nach rechts
if (traverse === 'zurueck') return ['rueckwaerts', 'zurueck'];
if (traverse === 'vor') return ['vorwaerts'];
return (k === 'tiefer' || k === 'vor') ? ['vorwaerts'] : ['rueckwaerts'];
}
/* ---------- Karte der Seitenstruktur im Dock ---------- */
const KNOTEN = (() => {
const m = { [UEBER]: [76, 9] };
THEMEN.forEach((t, i) => { const x = 22 + i * 36; m[`${UEBER}/${t.id}`] = [x, 29]; m[`${UEBER}/${t.id}/uebungen`] = [x, 48]; });
return m;
})();
function zeichneMini(aktuell, vorher) {
const k = KNOTEN, s = [];
s.push(`<defs><marker id="rb-pfeil" viewBox="0 0 8 8" refX="6.5" refY="4" markerWidth="6" markerHeight="6" orient="auto-start-reverse"><path d="M0 0L8 4 0 8Z" fill="#d98f2b"/></marker></defs>`);
THEMEN.forEach(t => {
const [x, y] = k[`${UEBER}/${t.id}`];
s.push(`<path d="M76 9V19H${x}V${y}M${x} ${y}V48" fill="none" stroke="rgba(54,59,59,.18)" stroke-width="1"/>`);
});
Object.entries(k).forEach(([p, [x, y]]) => {
const an = p === aktuell, war = p === vorher;
s.push(`<circle cx="${x}" cy="${y}" r="${an ? 5 : 3}" fill="${an ? '#005450' : war ? '#fff' : '#dce9e4'}" stroke="${an || war ? '#005450' : 'rgba(0,84,80,.35)'}" stroke-width="${war ? 1.5 : 1}"/>`);
});
if (vorher && k[vorher] && vorher !== aktuell) {
const [x1, y1] = k[vorher], [x2, y2] = k[aktuell];
const dx = x2 - x1, dy = y2 - y1, l = Math.hypot(dx, dy), a = 6 / l, b = 8 / l;
const sx = x1 + dx * a, sy = y1 + dy * a, ex = x2 - dx * b, ey = y2 - dy * b;
const bogen = dy === 0 ? -8 : 0;
s.push(`<path d="M${sx.toFixed(1)} ${sy.toFixed(1)}Q${((sx + ex) / 2 + (dy ? 9 : 0)).toFixed(1)} ${((sy + ey) / 2 + bogen).toFixed(1)} ${ex.toFixed(1)} ${ey.toFixed(1)}" fill="none" stroke="#d98f2b" stroke-width="1.6" stroke-linecap="round" marker-end="url(#rb-pfeil)"/>`);
}
mini.innerHTML = s.join('');
}
/* ---------- Werte-Anzeige ---------- */
const BESCHREIBUNG = {
tiefer: `alt scale 1 → ${W.zoom[1]}, neu ${W.zoom[0]} → 1 · Ursprung: geklicktes Element`,
hoeher: `alt scale 1 → ${W.zoom[0]}, neu ${W.zoom[1]} → 1 · Ursprung: Elternkarte`,
vor: `alt translateX 0 → −${W.versatz}, neu ${W.versatz} → 0`,
rueck: `alt translateX 0 → ${W.versatz}, neu −${W.versatz} → 0`,
vorwaerts: `alt translateX 0 → −${W.versatz}, neu ${W.versatz} → 0`,
rueckwaerts: `alt translateX 0 → ${W.versatz}, neu −${W.versatz} → 0`
};
let letzte = null;
function zeigeWerte() {
const z = [];
if (reduziert()) {
z.push(`<span><b>reduziert</b> · keine Typen · Root-Crossfade ${WR.dauer} ms ${WR.kurve}</span>`);
} else if (!kannVT) {
z.push(`<span><b>ohne View Transitions</b> · neue Seite blendet mit Richtung ein (WAAPI)</span>`);
} else if (variante === 'ohne') {
z.push(`<span><b>Crossfade</b> · ${W.dauer} ms · <span class="nw">${W.kurve}</span></span><span class="lang">jede Navigation sieht gleich aus, die Struktur bleibt unsichtbar</span>`);
} else if (letzte && letzte.typen.length) {
z.push(`<span>${letzte.typen.map(t => `<span class="typ">${t}</span>`).join(' ')} · ${W.dauer} ms · <span class="nw">${W.kurve}</span></span><span class="lang">${BESCHREIBUNG[letzte.typen[0]]} · Fade 0–50 % / 30–100 %</span>`);
} else {
z.push(`<span><b>${variante === 'hierarchisch' ? 'tiefer · höher · seitlich' : 'vorwärts · rückwärts'}</b> · ${W.dauer} ms · <span class="nw">${W.kurve}</span></span><span class="lang">Versatz ${W.versatz} · Zoom ${W.zoom[0]} / ${W.zoom[1]} · Fade</span>`);
}
z.push(letzte ? `<span>${pfadText(letzte.von)} → ${pfadText(letzte.nach)}${letzte.traverse ? ' · Browser-' + (letzte.traverse === 'zurueck' ? 'Zurück' : 'Vor') : ''}</span>`
: `<span>Behandlung öffnen, blättern, Übungen, dann zurück</span>`);
werteFeld.innerHTML = z.join('');
}
function setzeVariante(name) {
variante = name;
document.querySelectorAll('[data-variante]').forEach(b => b.setAttribute('aria-pressed', String(b.dataset.variante === name)));
letzte = null;
zeigeWerte();
}
/* ---------- Verlauf (simuliert den Browser-Verlauf in der Demo) ---------- */
let verlauf = [UEBER], pos = 0, aktuell = UEBER, laeuft = false;
const scrollMerk = {};
const knopfZurueck = document.querySelector('[data-verlauf="zurueck"]'), knopfVor = document.querySelector('[data-verlauf="vor"]');
function verlaufKnoepfe() { knopfZurueck.disabled = pos <= 0; knopfVor.disabled = pos >= verlauf.length - 1; }
function mitte(el) { const r = el.getBoundingClientRect(); return [Math.round(r.left + r.width / 2), Math.round(r.top + r.height / 2)]; }
function setzeUrsprung(xy) {
const [x, y] = xy || [innerWidth / 2, innerHeight / 2];
html.style.setProperty('--ux', x + 'px'); html.style.setProperty('--uy', y + 'px');
}
function rendere(p, k, mitFokus) {
seite.innerHTML = vorlage(p);
document.title = titel(p);
// Höher und Browser-Zurück: Scroll-Position wiederherstellen, sonst oben beginnen
scrollTo(0, (k === 'hoeher' || k === 'zurueck') ? (scrollMerk[p] || 0) : 0);
aktuell = p;
if (mitFokus) seite.querySelector('h1').focus({ preventScroll: true });
}
const FALLBACK = {
tiefer: [{ opacity: 0, transform: `scale(${W.zoom[0]})` }, { opacity: 1, transform: 'none' }],
hoeher: [{ opacity: 0, transform: `scale(${W.zoom[1]})` }, { opacity: 1, transform: 'none' }],
vor: [{ opacity: 0, transform: 'translateX(25%)' }, { opacity: 1, transform: 'none' }],
rueck: [{ opacity: 0, transform: 'translateX(-25%)' }, { opacity: 1, transform: 'none' }]
};
FALLBACK.vorwaerts = FALLBACK.vor; FALLBACK.rueckwaerts = FALLBACK.rueck;
function navigiere(nach, { traverse = null, fokus = false } = {}) {
if (laeuft || nach === aktuell) return Promise.resolve();
const von = aktuell, k = klasse(von, nach), typen = typenFuer(k, traverse);
scrollMerk[von] = scrollY;
if (!traverse) { verlauf = verlauf.slice(0, pos + 1); verlauf.push(nach); pos = verlauf.length - 1; }
letzte = { von, nach, traverse, typen };
// Ursprung für „tiefer“: das Element auf der alten Seite, das zur neuen führt
const quelle = seite.querySelector(`[data-ursprung="${nach}"]`);
setzeUrsprung(k === 'tiefer' && quelle ? mitte(quelle) : null);
const update = () => {
rendere(nach, traverse === 'zurueck' ? 'zurueck' : k, fokus);
// Ursprung für „höher“: die Karte der Seite, von der wir kommen
if (k === 'hoeher') { const ziel = seite.querySelector(`[data-ursprung="${von}"]`); setzeUrsprung(ziel ? mitte(ziel) : null); }
zeichneMini(nach, von); zeigeWerte(); verlaufKnoepfe();
};
if (!kannVT) {
update();
const kf = !reduziert() && variante !== 'ohne' && FALLBACK[(typen[0] || '')];
if (kf) seite.animate(kf, { duration: W.dauer, easing: W.kurve });
return Promise.resolve();
}
laeuft = true;
let vt;
try { vt = document.startViewTransition({ update, types: typen }); }
catch (e) { vt = document.startViewTransition(update); }
vt.ready.catch(() => {});
return vt.finished.catch(() => {}).then(() => { laeuft = false; });
}
function geheVerlauf(r) {
if (laeuft) return Promise.resolve();
const neu = pos + (r === 'zurueck' ? -1 : 1);
if (neu < 0 || neu >= verlauf.length) return Promise.resolve();
pos = neu;
return navigiere(verlauf[pos], { traverse: r });
}
document.addEventListener('click', e => {
const a = e.target.closest('a');
if (!a) return;
if (a.getAttribute('href').startsWith('tel:')) return;
e.preventDefault();
if (a.hasAttribute('data-tot') || !a.dataset.ziel) return;
stoppeVorfuehrung();
navigiere(a.dataset.ziel, { fokus: true });
});
document.querySelectorAll('[data-variante]').forEach(b => b.addEventListener('click', () => { stoppeVorfuehrung(); setzeVariante(b.dataset.variante); }));
document.querySelectorAll('[data-verlauf]').forEach(b => b.addEventListener('click', () => { stoppeVorfuehrung(); geheVerlauf(b.dataset.verlauf); }));
addEventListener('keydown', e => {
if (!e.altKey) return;
if (e.key === 'ArrowLeft') { e.preventDefault(); stoppeVorfuehrung(); geheVerlauf('zurueck'); }
if (e.key === 'ArrowRight') { e.preventDefault(); stoppeVorfuehrung(); geheVerlauf('vor'); }
});
/* Touch: auf einer Behandlung seitlich wischen blättert, Richtung passt zur Geste */
let tx = 0, ty = 0;
seite.addEventListener('touchstart', e => { tx = e.touches[0].clientX; ty = e.touches[0].clientY; }, { passive: true });
seite.addEventListener('touchend', e => {
const dx = e.changedTouches[0].clientX - tx, dy = e.changedTouches[0].clientY - ty;
if (Math.abs(dx) < 60 || Math.abs(dy) > 40 || tiefe(aktuell) !== 2) return;
const ziel = seite.querySelector(`[data-seitlich="${dx < 0 ? 'vor' : 'rueck'}"]`);
if (ziel) { stoppeVorfuehrung(); navigiere(ziel.dataset.ziel, { fokus: true }); }
}, { passive: true });
function start() {
laeuft = false; verlauf = [UEBER]; pos = 0; letzte = null;
rendere(UEBER, null, false);
zeichneMini(UEBER, null); zeigeWerte(); verlaufKnoepfe();
}
/* ---------- Vorführung: gedachter Zeiger klickt sich durch die Struktur ---------- */
const REIHE = ['hierarchisch', 'schieben', 'ohne'];
const SCHRITTE = [
() => seite.querySelector(`.karte[data-ziel="${UEBER}/ruecken"]`),
() => seite.querySelector('[data-seitlich="vor"]'),
() => seite.querySelector('[data-ursprung$="/uebungen"]'),
() => knopfZurueck,
() => knopfZurueck,
() => seite.querySelector(`.pfad a[data-ziel="${UEBER}"]`)
];
let runde = 0, wecker = null, lief = false, zx = 0, zy = 0, durchgang = 0;
const warte = (ms, t) => new Promise(r => { wecker = setTimeout(() => r(t === runde), ms); });
function zeigerZu(el, t, fx = .5, fy = .5) {
const r = el.getBoundingClientRect(), x = Math.round(r.left + r.width * fx), y = Math.round(r.top + r.height * fy);
zeiger.animate({ transform: [`translate(${zx}px, ${zy}px)`, `translate(${x}px, ${y}px)`] }, { duration: reduziert() ? 1 : 700, easing: 'cubic-bezier(0.2, 0, 0.38, 0.9)', fill: 'forwards' });
zx = x; zy = y;
return warte(reduziert() ? 200 : 760, t);
}
const klickPuls = () => zeiger.animate({ scale: [1, .72, 1] }, { duration: 240, easing: 'cubic-bezier(0.2, 0, 0.38, 0.9)' });
async function spiel(t) {
if (!(await warte(700, t))) return;
while (t === runde) {
if (aktuell !== UEBER) { await navigiere(UEBER); if (t !== runde) return; }
// Variante wählen: Zeiger tippt auf den Umschalter
const v = REIHE[durchgang % REIHE.length], vk = document.querySelector(`[data-variante="${v}"]`);
if (variante !== v) {
if (!(await zeigerZu(vk, t))) return;
klickPuls(); setzeVariante(v);
if (!(await warte(500, t))) return;
}
verlauf = [UEBER]; pos = 0; verlaufKnoepfe();
for (let i = 0; i < SCHRITTE.length; i++) {
const el = SCHRITTE[i]();
if (!el) break;
if (!(await zeigerZu(el, t, el.classList.contains('karte') ? .5 : .5, el.classList.contains('karte') ? .35 : .5))) return;
klickPuls();
if (!(await warte(140, t))) return;
if (el === knopfZurueck) await geheVerlauf('zurueck');
else await navigiere(el.dataset.ziel);
if (t !== runde) return;
if (!(await warte(1100, t))) return;
}
durchgang++;
}
}
function stoppeVorfuehrung() { runde++; clearTimeout(wecker); lief = false; zeiger.classList.remove('an'); html.classList.remove('vorfuehrung'); }
function neustart() {
stoppeVorfuehrung();
start();
wecker = setTimeout(() => navigiere(`${UEBER}/ruecken`), 500);
}
function vorfuehren() {
stoppeVorfuehrung();
start();
durchgang = 0; lief = true;
setzeVariante('hierarchisch');
html.classList.add('vorfuehrung');
zx = Math.round(innerWidth * .6); zy = Math.round(innerHeight * .25);
zeiger.animate({ transform: [`translate(${zx}px, ${zy}px)`] }, { duration: 1, fill: 'forwards' });
zeiger.classList.add('an');
spiel(runde);
}
function pause() { const war = lief; stoppeVorfuehrung(); lief = war; }
function weiter() { if (lief) { const t = ++runde; html.classList.add('vorfuehrung'); zeiger.classList.add('an'); spiel(t); } }
setzeVariante('hierarchisch');
start();
addEventListener('load', zeigeWerte);
})();
</script>
</body>
</html>