Zum Inhalt
← Seitenübergänge

Wortmarke wird Kopfmarke

Der Name im Hero wird zur kleinen Marke im Kopf: beim Scrollen schrumpft er an seinen Platz, beim Seitenwechsel fliegt er als gemeinsames Element in den Kopf der Unterseite und beim Zurück wieder groß.

Lange Startseiten mit Foto-Hero und großem Namen (Hotel, Restaurant, Weingut): der Name wird beim Scrollen zur Kopfmarke, ohne Seitenübergang.

Die große Wortmarke im Hero (der Name über die ganze Breite) wird zur kleinen Marke im Kopf. Beim Scrollen schrumpft sie gekoppelt an den Scroll in ihren Platz im Kopf. Beim Seitenwechsel fliegt sie als gemeinsames Element (view-transition-name) von der Startseite in den Kopf der Unterseite, beim Zurück wächst sie wieder. Es ist ein Element mit einem Namen für beide Wege. Dadurch wirkt der Seitenwechsel nicht wie ein flüchtiger Wischer: Man sieht, wohin die Marke geht.

Ein gemeinsames Element verbindet zwei Zustände: Das Auge folgt der Marke und weiß danach, dass es noch beim selben Haus ist. Die große Marke ist der stärkste Moment des ersten Bildschirms; statt sie wegzuscrollen, wird sie zur Kopfmarke und bleibt präsent. Die Bewegung hat einen klaren Weg (groß nach klein, Mitte nach oben links) und eine Dauer, die man wahrnimmt, statt in 150 ms vorbei zu sein.

Wann einsetzen

Wenn der Hero eine große Wortmarke hat (foto-hero-wortmarke, schrift-hero-viewportbreit) und die Seite mehrere echte Unterseiten hat. Hotel, Manufaktur, Bäckerei, Weingut, Region, Kultur, Studio. scroll für lange Startseiten, seitenwechsel für Seiten mit wenigen Hauptpunkten, beides für Marken, bei denen der Name das Bild ist.

Wann nicht

Nicht bei Bildmarken ohne Schriftzug oder sehr langen Namen, die im Kopf auf unter 14 px schrumpfen müssten. Nicht zusammen mit einem zweiten großen gemeinsamen Element im selben Wechsel (Bild-Morph), sonst konkurrieren zwei Wege. Nicht bei hide-on-scroll-header: die Marke braucht einen festen Platz. Gegen vt-shared-element-morph abgegrenzt: dort fliegt ein Inhaltsbild aus dem Raster in den Hero, hier fliegt die Marke aus dem Hero in den Kopf. Gegen sticky-shrink-header abgegrenzt: dort schrumpft der Kopf, hier wandert die Hero-Wortmarke selbst in den Kopf.

Varianten

  • scroll: die Wortmarke schrumpft per Scroll-Zeitleiste in ihren Platz im Kopf; der Kopf bekommt seine Fläche und die dunkle Schrift erst, wenn sie fast angekommen ist
  • seitenwechsel: Cross-Document View Transition zwischen zwei Seiten, die große Marke fliegt in den Kopf der Unterseite, beim Zurück umgekehrt
  • beides: dieselbe Marke schrumpft beim Scrollen und fliegt beim Seitenwechsel aus jedem Zwischenstand in den Kopf

Umsetzung

  1. Beide Seiten: <style>@view-transition { navigation: auto; }</style> im Kopf.
  2. Startseite: die Wortmarke ist ein Link im Kopf (<a class="marke" href="/">Name</a>), der oben in Hero-Größe steht. Ihre Größe kommt aus dem Einpassen (wie foto-hero-wortmarke), ihr Zielplatz aus einem leeren Platzhalter im Kopf.
  3. scroll: @property --p (<number>) läuft per animation: marke-p linear both; animation-timeline: scroll(root); animation-range-start: 0; animation-range-end: var(--weg), wobei --weg der Abstand der Hero-Marke zu ihrem Platz im Kopf ist (so fährt die Marke genau mit der Seite hoch). Langform nach der Kurzschreibweise, die animation auf html sitzt, --p erbt an Marke und Kopf. Die Marke ist position: fixed, transform-origin: 0 0, und rechnet translate(calc(var(--dx) * var(--p)), calc(var(--dy) * var(--p))) scale(calc(1 + (var(--s) - 1) * var(--p))). --dx, --dy und --s misst JS bei Laden, Schriftladen und ResizeObserver. Rückfall: Scroll-Listener setzt --p.
  4. seitenwechsel: Startseite und Unterseite geben ihrer sichtbaren Marke denselben Namen (view-transition-name: marke); dieselbe Schrift, dieselbe Laufweite in em und dieselbe Zeilenhöhe, damit das Seitenverhältnis stimmt. ::view-transition-group(marke) mit 900 ms und einer In-out-Kurve, damit der Weg sichtbar bleibt; die übrige Seite blendet in 300 ms aus und ab 350 ms ein. pageswap setzt Typen (hin/zurueck) aus navigation.activation bzw. e.activation.
  5. beides: der Name sitzt immer auf der festen Marke, sie fliegt aus ihrem aktuellen Zwischenstand.
  6. Reduziert: kein Morph, kein Schrumpfen; die Marke steht klein im Kopf, der Hero zeigt den Namen als statischen Schriftzug, Seiten wechseln ohne Übergang.

Robust umsetzen

Telefon. Die Wortmarke passt sich der Breite an (am Telefon um 80 bis 100 px hoch) und landet links im Kopf neben dem Menüknopf. Morph 750 ms statt 900 ms. Der Kopf bleibt 56 px hoch, die Marke nie unter 20 px.

Ohne Bewegung. Kein Schrumpfen und kein Flug: Die Marke steht klein im Kopf, im Hero steht der Name als ruhiger Schriftzug. Seitenwechsel ohne View Transition (::view-transition-group(*) { animation: none }).

Barrierefrei. Die Marke ist ein Link mit Text (Name, Ziel Startseite) im Kopf (banner). Wandert sie (scroll, beides), ist der Hero-Schriftzug dieselbe Marke, kein zweites Element. Steht sie im Fluss (seitenwechsel), ist die Kopfmarke inert und unsichtbar, bis die große Marke oben aus dem Bild ist. Das h1 der Seite bleibt eigener Text. Hero-Fläche mit dunklem Grund unter dem Foto, damit helle Schrift auch ohne Bild lesbar bleibt. Einpassen per ResizeObserver neu (Textabstände 1.4.12). Kontrastmodus: Marke in CanvasText.

Code

<!DOCTYPE html>
<!--
  Wortmarke wird Kopfmarke
  Fundus · Seitenübergänge · https://cecodesign.de/lab/fundus/seitenuebergaenge/wortmarke-wird-kopfmarke/
  Eigenständige Datei. Bilder liegen hier unter bilder/ – eigene einsetzen.
-->
<html lang="de" data-v="scroll">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Wortmarke wird Kopfmarke · Fundus-Demo</title>
<script>document.documentElement.classList.add('js');if(matchMedia('(prefers-reduced-motion: reduce)').matches)document.documentElement.classList.add('reduziert')</script>
<!-- Cross-Document View Transition: beide Seiten brauchen dieses Opt-in (hier dieselbe Datei mit ?seite=unter) -->
<style>@view-transition { navigation: auto; }</style>
<!-- Erst zeichnen, wenn #ende geparst ist: dann stehen Marke und Namen schon, bevor pagereveal den Schnappschuss nimmt -->
<link rel="expect" blocking="render" href="#ende">
<script>(function(){var v=new URLSearchParams(location.search).get('v'),f={seitenwechsel:'bodoni-moda-latin-opsz-normal.woff2',beides:'big-shoulders-latin-wght-normal.woff2'}[v]||'newsreader-latin-opsz-normal.woff2',l=document.createElement('link');l.rel='preload';l.as='font';l.type='font/woff2';l.crossOrigin='';l.href='../../../../assets/fonts/'+f;document.head.appendChild(l)})()</script>
<style>
@font-face { font-family: 'Newsreader'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/newsreader:vf@latest/latin-opsz-normal.woff2') format('woff2-variations'); font-weight: 200 800; font-style: normal; font-display: swap; }
@font-face { font-family: 'Newsreader'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/newsreader:vf@latest/latin-opsz-italic.woff2') format('woff2-variations'); font-weight: 200 800; font-style: italic; font-display: swap; }
@font-face { font-family: 'Mona Sans'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/mona-sans:vf@latest/latin-wdth-normal.woff2') format('woff2-variations'); font-weight: 200 900; font-stretch: 75% 125%; font-display: swap; }
@font-face { font-family: 'Bodoni Moda'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/bodoni-moda:vf@latest/latin-opsz-normal.woff2') format('woff2-variations'); font-weight: 400 900; font-style: normal; font-display: swap; }
@font-face { font-family: 'Instrument Sans'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/instrument-sans: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: 'Host Grotesk'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/host-grotesk:vf@latest/latin-wght-normal.woff2') format('woff2-variations'); font-weight: 300 800; font-display: swap; }
@font-face { font-family: 'IBM Plex Mono'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/ibm-plex-mono@latest/latin-500-normal.woff2') format('woff2'); font-weight: 500; 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 1: Fortschritt --p der Marke (0 = Hero, 1 = Kopf) ============
   Die Marke ist EIN Element: position: fixed, Grundgröße = Hero-Größe, transform-origin oben links.
   Sie rechnet nur aus --p: translate(--dx, --dy) und scale(--s). JS misst --x0, --y0, --dx, --dy, --s, --weg. */
@property --p { syntax: '<number>'; inherits: true; initial-value: 0; }
@keyframes marke-p { from { --p: 0; } to { --p: 1; } }
/* Kurzschreibweise zuerst, dann Zeitleiste und Bereich als Langform (animation setzt beide sonst zurück).
   --weg = Hero-Oberkante der Marke minus Platz im Kopf: so fährt die Marke genau mit der Seite hoch und schrumpft dabei. */
@supports (animation-timeline: scroll()) {
  html[data-mod="fest"]:not(.reduziert):not(.rueckfall) {
    animation: marke-p linear both;
    animation-timeline: scroll(root block);
    animation-range-start: 0px;
    animation-range-end: var(--weg, 400px);
  }
}
html.reduziert { --p: 1; }

:root {
  --rand: clamp(20px, 4vw, 56px);
  --kopf-h: 68px;
  --vt-marke: 900ms;                         /* sichtbar langer Weg der Marke, nicht 200 ms */
  --vt-kurve: cubic-bezier(0.76, 0, 0.24, 1); /* In-out: der Flug ist über die ganze Dauer zu sehen */
  --ui: 150ms cubic-bezier(0.16, 1, 0.3, 1);
}
@media (max-width: 640px) { :root { --kopf-h: 56px; --vt-marke: 750ms; } }
* { box-sizing: border-box; margin: 0; }
html { -webkit-text-size-adjust: 100%; background: var(--grund); }
body { 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); outline-offset: 3px; }
:root:has(:focus-visible) { scroll-behavior: auto; }
a { color: inherit; }
img { display: block; max-width: 100%; height: auto; }
.label { font: 500 11px/1.3 'Geist Mono', ui-monospace, monospace; letter-spacing: .14em; text-transform: uppercase; }

/* ---------- Kopf ---------- */
.kopf { position: fixed; inset: 0 0 auto 0; z-index: 30; height: var(--kopf-h); padding: 0 var(--rand); display: flex; align-items: center; gap: 24px;
  color: var(--kopf-farbe, var(--text)); background: var(--kopf-grund, var(--grund)); view-transition-name: kopf; }
.kopf::after { content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 1px; background: var(--linie); opacity: var(--kopf-linie, 1); }
.kopf__platz { flex: none; height: 1px; } /* Zielplatz der Marke; Breite setzt das Skript */
.kopf nav { display: flex; gap: 6px 26px; margin-left: auto; font: var(--nav-font); letter-spacing: var(--nav-sperr, 0); text-transform: var(--nav-case, none); }
.kopf nav a { text-decoration: none; padding: 8px 0; background: linear-gradient(currentColor 0 0) 0 calc(100% - 4px) / 0 1px no-repeat; transition: background-size var(--ui); }
.kopf nav a:hover, .kopf nav a[aria-current="page"] { background-size: 100% 1px; }
.knopf { display: inline-flex; align-items: center; gap: .5em; min-height: 44px; padding: 0 20px; border-radius: var(--radius); background: var(--signal); color: var(--auf-signal); font: var(--knopf-font); letter-spacing: var(--nav-sperr, 0); text-transform: var(--nav-case, none); text-decoration: none; white-space: nowrap; transition: filter var(--ui); }
.knopf:hover { filter: brightness(.92); }
.kopf .knopf { min-height: 40px; }

/* ---------- Die Marke ---------- */
.marke { position: absolute; z-index: 31; left: var(--rand); top: 50%; translate: 0 -50%; font-size: var(--f); white-space: nowrap; text-decoration: none; display: inline-block;
  font-family: var(--f-marke); font-weight: var(--w-marke); font-variation-settings: var(--fvs-marke, normal); letter-spacing: var(--sperr-marke); line-height: var(--lh-marke); color: var(--marke-dunkel); padding: 0; }
/* Seitenwechsel: dieselbe Schrift, dieselbe Laufweite in em und dieselbe Zeilenhöhe groß und klein, damit das Seitenverhältnis der Schnappschüsse gleich bleibt */
html[data-mod="fest"] .marke { position: fixed; left: var(--x0); top: var(--y0); translate: none; font-size: var(--F); transform-origin: 0 0;
  transform: translate(calc(var(--dx) * var(--p)), calc(var(--dy) * var(--p))) scale(calc(1 + (var(--s) - 1) * var(--p)));
  color: color-mix(in oklab, var(--marke-hell) calc((1 - var(--a)) * 100%), var(--marke-dunkel)); will-change: transform; }
/* Kopf bekommt seine Fläche erst, wenn die Marke fast angekommen ist (ab --p 0,8); die Marke wechselt die Farbe im selben Moment,
   über dem Foto bleibt sie hell und lesbar */
html[data-mod="fest"] { --a: clamp(0, (var(--p) - .8) * 5, 1); }
html[data-mod="fest"] .kopf {
  background: color-mix(in oklab, var(--grund) calc(var(--a) * 100%), transparent); --kopf-linie: var(--a);
  color: color-mix(in oklab, var(--marke-hell) calc((1 - var(--a)) * 100%), var(--text)); }
html[data-mod="fest"] .kopf .knopf { background: color-mix(in oklab, var(--signal) calc(var(--a) * 100%), rgba(255,255,255,.16)); color: color-mix(in oklab, #fff calc((1 - var(--a)) * 100%), var(--auf-signal)); -webkit-backdrop-filter: blur(8px); backdrop-filter: blur(8px); }
html.reduziert[data-mod="fest"] .kopf .knopf { -webkit-backdrop-filter: none; backdrop-filter: none; }

/* ---------- Startseite: Hero ---------- */
.held { position: relative; isolation: isolate; min-height: min(94svh, 900px); display: grid; align-content: end; padding: 0 var(--rand) clamp(20px, 3vw, 40px); color: #fff; overflow: clip; background: var(--held-grund, #2a2826); } /* dunkler Grund unter dem Foto: helle Schrift bleibt lesbar, falls das Bild fehlt */
.held__bild { position: absolute; inset: 0; z-index: -1; }
.held__bild img { width: 100%; height: 100%; object-fit: cover; }
.held__bild::after { content: ''; position: absolute; inset: 0; background: linear-gradient(to bottom, rgba(10, 12, 14, .42), rgba(10, 12, 14, 0) 26%, rgba(10, 12, 14, 0) 52%, rgba(10, 12, 14, .5)); }
.held__zeile { display: flex; flex-wrap: wrap; justify-content: space-between; gap: 8px 24px; margin-bottom: calc(var(--F, 18vw) * .1 + 8px); } /* Platz für Oberlängen über der Zeilenbox */
.held__platz { position: relative; height: var(--Fh, 20vw); }
/* Ruhiger Schriftzug im Hero, nur wenn die Marke nicht wandert (reduziert, ohne JS) */
.held__abbild { position: absolute; left: 0; top: 0; white-space: nowrap; font-family: var(--f-marke); font-weight: var(--w-marke); font-variation-settings: var(--fvs-marke, normal); letter-spacing: var(--sperr-marke); line-height: var(--lh-marke); font-size: var(--F, 18vw); visibility: hidden; }
html.reduziert .held__abbild, html:not(.js) .held__abbild { visibility: visible; }
/* Seitenwechsel ohne Scroll-Schrumpfen: die große Marke steht im Fluss über dem Foto */
.held--fluss { min-height: 0; display: block; padding: calc(var(--kopf-h) + clamp(28px, 5vw, 64px)) var(--rand) 0; color: var(--text); overflow-x: clip; background: none; }
.held--fluss .held__gross { display: inline-block; font-size: var(--F, 18vw); white-space: nowrap; text-decoration: none; font-family: var(--f-marke); font-weight: var(--w-marke); font-variation-settings: var(--fvs-marke, normal); letter-spacing: var(--sperr-marke); line-height: var(--lh-marke); color: var(--marke-dunkel); }
.held--fluss .held__band { margin: clamp(20px, 3vw, 36px) calc(var(--rand) * -1) 0; aspect-ratio: 21 / 8; overflow: hidden; }
.held--fluss .held__band img { width: 100%; height: 100%; object-fit: cover; }
html[data-mod="fluss"] .kopf .marke { opacity: 0; visibility: hidden; transition: opacity 200ms cubic-bezier(0.7, 0, 0.84, 0), visibility 0s 200ms; }
html[data-mod="fluss"].marke-oben .kopf .marke { opacity: 1; visibility: visible; transition: opacity 300ms cubic-bezier(0.16, 1, 0.3, 1), visibility 0s; }
html.reduziert[data-mod="fluss"] .kopf .marke { transition: opacity 150ms linear, visibility 0s 150ms; }
html.reduziert[data-mod="fluss"].marke-oben .kopf .marke { transition: opacity 150ms linear, visibility 0s; }

/* ---------- Kapitel ---------- */
.inhalt { padding: clamp(48px, 8vw, 112px) var(--rand); }
.unter .inhalt { padding-top: calc(var(--kopf-h) + clamp(40px, 7vw, 96px)); }
.inhalt > * { max-width: 1240px; margin-inline: auto; }
.einstieg { display: grid; grid-template-columns: minmax(0, 7fr) minmax(0, 5fr); gap: 20px clamp(32px, 6vw, 96px); align-items: end; padding-bottom: clamp(40px, 6vw, 80px); border-bottom: 1px solid var(--linie); }
.einstieg h1, .unter h1 { font-family: var(--f-kopf); font-weight: var(--w-h1); font-variation-settings: var(--fvs-h1, normal); font-size: var(--g-h1); line-height: var(--lh-h1, 1.04); letter-spacing: -.015em; text-wrap: balance; }
.einstieg p { max-width: 44ch; text-wrap: pretty; }
.reihe { display: grid; grid-template-columns: minmax(0, 6fr) minmax(0, 5fr); gap: 24px clamp(32px, 6vw, 96px); align-items: center; padding-block: clamp(40px, 6vw, 88px); border-bottom: 1px solid var(--linie); }
.reihe:nth-child(odd) > figure { order: 2; }
.reihe figure img { width: 100%; aspect-ratio: 3 / 2; object-fit: cover; border-radius: var(--radius-bild, 0); }
.reihe h2 { font-family: var(--f-kopf); font-weight: var(--w-h2); font-variation-settings: var(--fvs-h2, normal); font-size: var(--g-h2); line-height: 1.08; letter-spacing: -.01em; text-wrap: balance; margin: 10px 0 14px; }
.reihe p { max-width: 48ch; text-wrap: pretty; }
.leise { color: var(--leise); }
.weiter { display: inline-flex; gap: .5em; margin-top: 18px; font-weight: 600; text-underline-offset: .25em; }
.unter__kopf { display: grid; grid-template-columns: minmax(0, 7fr) minmax(0, 5fr); gap: 24px clamp(32px, 6vw, 96px); align-items: end; }
.unter__bild { margin-top: clamp(32px, 5vw, 64px); }
.unter__bild img { width: 100%; aspect-ratio: 21 / 9; object-fit: cover; border-radius: var(--radius-bild, 0); }
.liste { margin-top: clamp(32px, 5vw, 56px); border-top: 1px solid var(--text); padding: 0; list-style: none; }
.liste li { display: grid; grid-template-columns: minmax(0, 3fr) minmax(0, 5fr) auto; gap: 6px 24px; align-items: baseline; padding: 18px 0; border-bottom: 1px solid var(--linie); }
.liste b { font-family: var(--f-kopf); font-weight: var(--w-h2); font-variation-settings: var(--fvs-h2, normal); font-size: clamp(1.25rem, 1rem + .9vw, 1.75rem); line-height: 1.1; }
.liste .zahl { font-variant-numeric: tabular-nums; white-space: nowrap; font-weight: 600; }
.hinweis { margin-top: 14px; font-size: .875rem; color: var(--leise); }
.zurueck { display: inline-flex; gap: .5em; margin-top: clamp(32px, 5vw, 56px); font-weight: 600; text-underline-offset: .25em; }
.fusszeile { padding: 28px var(--rand) 96px; border-top: 1px solid var(--linie); font-size: .875rem; color: var(--leise); }
.fusszeile > * { max-width: 1240px; margin-inline: auto; }
@media (max-width: 760px) {
  .einstieg, .reihe, .unter__kopf { grid-template-columns: minmax(0, 1fr); }
  .reihe:nth-child(odd) > figure { order: 0; }
  .kopf nav { display: none; }
  .kopf .knopf { margin-left: auto; padding: 0 14px; }
  .liste li { grid-template-columns: minmax(0, 1fr) auto; }
  .liste li span:not(.zahl) { grid-column: 1 / -1; grid-row: 2; }
  .unter__bild img { aspect-ratio: 4 / 3; }
  .held--fluss .held__band { aspect-ratio: 4 / 3; }
}

/* ============ Kern 2: Seitenwechsel ============
   Startseite: die sichtbare Marke trägt view-transition-name: marke; Unterseite: die Kopfmarke trägt ihn.
   Die Gruppe fliegt 900 ms mit In-out-Kurve; die übrige Seite geht in 300 ms und kommt ab 350 ms. */
html[data-vt="an"] .marke { view-transition-name: marke; }
html[data-vt="an"][data-mod="fluss"]:not(.marke-oben) .held__gross { view-transition-name: marke; }
html[data-vt="an"][data-mod="fluss"]:not(.marke-oben) .kopf .marke { view-transition-name: none; }
::view-transition-group(marke) { animation-duration: var(--vt-marke); animation-timing-function: var(--vt-kurve); z-index: 5; }
::view-transition-old(marke), ::view-transition-new(marke) { height: 100%; animation-duration: var(--vt-marke); animation-timing-function: var(--vt-kurve); }
::view-transition-group(kopf) { animation-duration: 450ms; animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1); z-index: 4; }
::view-transition-old(root) { animation: vt-raus 300ms cubic-bezier(0.7, 0, 0.84, 0) both; }
::view-transition-new(root) { animation: vt-rein 450ms cubic-bezier(0.16, 1, 0.3, 1) 350ms both; }
html:active-view-transition-type(zurueck)::view-transition-old(root) { animation-name: vt-raus-unten; }
html:active-view-transition-type(zurueck)::view-transition-new(root) { animation-name: vt-rein-oben; }
@keyframes vt-raus { to { opacity: 0; translate: 0 -28px; } }
@keyframes vt-rein { from { opacity: 0; translate: 0 40px; } }
@keyframes vt-raus-unten { to { opacity: 0; translate: 0 28px; } }
@keyframes vt-rein-oben { from { opacity: 0; translate: 0 -40px; } }
html.reduziert::view-transition-group(*), html.reduziert::view-transition-old(*), html.reduziert::view-transition-new(*) { animation: none !important; }
@media (prefers-reduced-motion: reduce) { @view-transition { navigation: none; } }

/* =====================================================================
   scroll · Haus Tessmer, Waren (Müritz) · boutique-hotel-gastgeber
   ===================================================================== */
html[data-v="scroll"] {
  --grund: #F3EEE7; --text: #3E3A37; --leise: #6A6C67; --linie: #D5D1C8; --signal: #6E5A47; --auf-signal: #fff; --fokus: #3E3A37; --radius: 999px; --radius-bild: 6px;
  --f-kopf: 'Newsreader', Georgia, serif; --f-text: 'Mona Sans', system-ui, sans-serif; --f-marke: 'Newsreader', Georgia, serif;
  --w-marke: 330; --fvs-marke: 'opsz' 72; --sperr-marke: -0.035em; --lh-marke: .92; --marke-hell: #FEF2E3; --marke-dunkel: #3E3A37;
  --w-h1: 330; --fvs-h1: 'opsz' 72; --g-h1: clamp(2.25rem, 1.4rem + 3.4vw, 4.5rem); --w-h2: 360; --fvs-h2: 'opsz' 48; --g-h2: clamp(1.75rem, 1.2rem + 2vw, 3rem);
  --nav-font: 500 .75rem/1 'Mona Sans', sans-serif; --nav-sperr: .06em; --nav-case: uppercase; --knopf-font: 600 .75rem/1 'Mona Sans', sans-serif;
}
html[data-v="scroll"] .kopf nav, html[data-v="scroll"] .knopf { font-stretch: 118%; }
/* =====================================================================
   seitenwechsel · Okelmann Keramik, Höhr-Grenzhausen · luxus-manufaktur-stille
   ===================================================================== */
html[data-v="seitenwechsel"] {
  --grund: #ffffff; --text: #23282f; --leise: #71757B; --linie: #d9dadb; --signal: #23282f; --auf-signal: #fff; --fokus: #a88a5a; --radius: 999px; --radius-bild: 0;
  --f-kopf: 'Bodoni Moda', Didot, serif; --f-text: 'Instrument Sans', system-ui, sans-serif; --f-marke: 'Bodoni Moda', Didot, serif;
  --w-marke: 400; --fvs-marke: normal; --sperr-marke: -0.02em; --lh-marke: .95; --marke-hell: #23282f; --marke-dunkel: #23282f;
  --w-h1: 400; --g-h1: clamp(2.25rem, 1.3rem + 3.6vw, 4.75rem); --w-h2: 400; --g-h2: clamp(1.75rem, 1.2rem + 2vw, 3rem); --lh-h1: 1.05;
  --nav-font: 500 .75rem/1 'Instrument Sans', sans-serif; --nav-sperr: .08em; --nav-case: uppercase; --knopf-font: 600 .75rem/1 'Instrument Sans', sans-serif;
}
html[data-v="seitenwechsel"] .einstieg h1, html[data-v="seitenwechsel"] .unter h1, html[data-v="seitenwechsel"] .reihe h2 { font-optical-sizing: auto; }
html[data-v="seitenwechsel"] .reihe figure { background: #f4f4f2; }
/* =====================================================================
   beides · Naturpark Hirschkamm, Schmiedefeld · region-tourismus-ausflugsziel
   ===================================================================== */
html[data-v="beides"] {
  --grund: #EEF0EC; --text: #1B2226; --leise: #565E60; --linie: #C9CCC4; --signal: #B1321C; --auf-signal: #fff; --fokus: #1B2226; --radius: 4px; --radius-bild: 16px;
  --f-kopf: 'Big Shoulders', 'Arial Narrow', sans-serif; --f-text: 'Host Grotesk', system-ui, sans-serif; --f-marke: 'Big Shoulders', 'Arial Narrow', sans-serif;
  --w-marke: 800; --sperr-marke: -0.01em; --lh-marke: .86; --marke-hell: #F4F6F2; --marke-dunkel: #1B2226;
  --w-h1: 800; --g-h1: clamp(2.75rem, 1.5rem + 5vw, 6rem); --lh-h1: .92; --w-h2: 700; --g-h2: clamp(2rem, 1.4rem + 2.4vw, 3.5rem);
  --nav-font: 600 .9375rem/1 'Host Grotesk', sans-serif; --knopf-font: 600 .9375rem/1 'Host Grotesk', sans-serif;
}
html[data-v="beides"] .liste .zahl, html[data-v="beides"] .tourdaten { font-family: 'IBM Plex Mono', monospace; font-weight: 500; font-size: .9375rem; }

/* ---------- Pult ---------- */
.pult { position: fixed; right: 14px; bottom: 14px; z-index: 60; width: min(320px, 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: flex; flex-wrap: wrap; gap: 6px; }
.pult button, .pult a { flex: 1 1 auto; display: inline-flex; align-items: center; justify-content: center; 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; text-decoration: none; cursor: pointer; transition: background-color var(--ui), border-color var(--ui); }
.pult button:hover, .pult a:hover { border-color: rgba(243, 242, 238, .5); }
.pult [aria-pressed="true"], .pult [aria-current="page"] { 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 body { overflow: hidden; }
@media (hover: none) { .kopf nav a:hover { background-size: 0 1px; } }
@media (forced-colors: active) {
  .marke, .held__gross, .held__abbild { forced-color-adjust: none; color: CanvasText !important; background: Canvas; }
  html[data-mod="fest"] .kopf { background: Canvas; color: CanvasText; }
  button, [role=button], [role=tab], [role=switch], input, select, textarea { border: 1px solid ButtonText !important; }
}
</style>
</head>
<body>
<header class="kopf" id="kopf">
  <span class="kopf__platz" id="platz" aria-hidden="true"></span>
  <a class="marke" id="marke" href="#">Tessmer</a>
  <nav id="nav" aria-label="Hauptnavigation (Demo)"></nav>
  <a class="knopf" id="cta" href="#">Anfragen</a>
</header>
<main id="seite"></main>

<div class="pult">
  <p class="label" id="pult-v">Variante</p>
  <div class="pult__wahl" role="group" aria-labelledby="pult-v">
    <button type="button" data-wahl="scroll">Scroll</button>
    <button type="button" data-wahl="seitenwechsel">Seitenwechsel</button>
    <button type="button" data-wahl="beides">Beides</button>
  </div>
  <p class="label" id="pult-s">Seite (echter Seitenaufruf)</p>
  <div class="pult__wahl" role="group" aria-labelledby="pult-s"><a id="zu-start" href="#">Startseite</a><a id="zu-unter" href="#">Unterseite</a></div>
  <p class="pult__messung"><output id="messung">…</output></p>
</div>

<script>
(() => {
  const html = document.documentElement;
  const reduziert = () => html.classList.contains('reduziert');
  const q = new URLSearchParams(location.search);
  const kannTimeline = !!(window.CSS && CSS.supports && CSS.supports('animation-timeline: scroll()'));
  const kannCross = !!(window.CSS && CSS.supports && CSS.supports('selector(:active-view-transition-type(a))')) && 'onpagereveal' in window;
  if (!kannTimeline) html.classList.add('rueckfall');
  const $ = (id) => document.getElementById(id);
  const seiteEl = $('seite'), marke = $('marke'), platz = $('platz'), nav = $('nav'), cta = $('cta');

  // ---------- Welten ----------
  const M = 'bilder/';
  const W = {
    scroll: {
      mod: 'fest', vt: false, name: 'Tessmer', voll: 'Haus Tessmer', f: [26, 22],
      bild: { src: M + 'hotel-haus-am-see.webp', hoch: M + 'hotel-haus-am-see-hoch.webp', alt: 'Backsteinhaus mit erleuchteten Fenstern am See in der Abenddämmerung, daneben eine kleine Holzsauna und ein Steg' },
      zeile: ['Hotel am Tiefwarensee', 'Waren (Müritz)'], nav: [['Zimmer', 'unter'], ['Sauna', '#sauna'], ['Anreise', '#anreise']], cta: 'Zimmer anfragen',
      h1: 'Zwölf Zimmer in einem Backsteinhaus am Wasser.', satz: 'Frühstück bis elf, Sauna auf dem Steg, und abends nur das Klappern der Boote. Scrollen Sie: Der Name wandert in den Kopf und bleibt dort.',
      reihen: [
        { id: 'zimmer', label: 'Zimmer', h2: 'Jedes Zimmer schaut auf den See.', p: 'Holzböden, Leinen, ein Sessel am Fenster. Kein Fernseher, dafür ein Bücherregal auf jedem Flur.', bild: [M + 'hotel-zimmer.webp', 'Hotelzimmer mit Doppelbett, Sessel und gelbem Polster auf der Fensterbank, Sonnenflecken auf dem Holzboden'], link: 'Alle zwölf Zimmer' },
        { id: 'sauna', label: 'Sauna', h2: 'Vom Steg in den See.', p: 'Die Holzsauna steht am Wasser und ist bis 22 Uhr warm. Bademäntel liegen im Zimmer.', bild: [M + 'hotel-sauna.webp', 'Holzsauna auf dem Steg in der Dämmerung mit zwei erleuchteten Fenstern, daneben Schilf'] },
        { id: 'anreise', label: 'Anreise', h2: 'Mit der Bahn bis Waren, den Rest holen wir.', p: 'Vom Bahnhof sind es acht Minuten mit dem Auto. Sagen Sie uns Ihren Zug, wir stehen am Gleis.' }
      ],
      unter: { label: 'Haus Tessmer · Zimmer', h1: 'Zwölf Zimmer, jedes zum Wasser.', satz: 'Alle Zimmer mit Frühstück und Sauna. Die Preise gelten pro Nacht für zwei Personen.', bild: [M + 'hotel-zimmer-fenster.webp', 'Fensterbank mit gelbem Polster, Tasse und Vase mit Zweigen, dahinter der See'],
        liste: [['Kammer', '18 m², Doppelbett, Blick über den Garten zum See', 'ab 128 €'], ['Seezimmer', '24 m², Sessel am Fenster, Balkon zum Wasser', 'ab 158 €'], ['Turmzimmer', '32 m², zwei Fenster, Badewanne', 'ab 196 €']],
        hinweis: 'Beispielpreise einer erfundenen Demo. Preisangaben vom Kunden prüfen lassen.' }
    },
    seitenwechsel: {
      mod: 'fluss', vt: true, name: 'Okelmann', voll: 'Okelmann Keramik', f: [24, 21],
      bild: { src: M + 'keramik-stillleben.jpg', hoch: M + 'keramik-stillleben-hoch.jpg', alt: 'Handgedrehte Keramik auf Gipssockeln vor einer terrakottafarbenen Hohlkehle' },
      nav: [['Stücke', '#stuecke'], ['Werkstatt', 'unter'], ['Besuch', '#besuch']], cta: 'Termin vereinbaren',
      h1: 'Steinzeug aus dem Westerwald, salzglasiert.', satz: 'Gedreht, getrocknet, bei 1.250 Grad gebrannt. Klicken Sie auf „Werkstatt“: Der Name fliegt in den Kopf der nächsten Seite.',
      reihen: [
        { id: 'stuecke', label: 'Stücke', h2: 'Becher, Krüge, Schalen.', p: 'Jedes Stück ist von Hand gedreht und trägt den Stempel der Werkstatt am Boden. Keines gleicht dem anderen.', bild: [M + 'keramik-stillleben-hoch.jpg', 'Bauchige Keramikgefäße auf hellen Sockeln vor warmem Hintergrund'] },
        { id: 'werkstatt', label: 'Werkstatt', h2: 'Zwei Drehscheiben, ein Salzofen.', p: 'Das Salz verdampft im Brand und legt sich als Glasur auf den Ton. So entsteht die feine Haut, die man am Westerwälder Steinzeug kennt.', link: 'Zur Werkstatt' },
        { id: 'besuch', label: 'Besuch', h2: 'Donnerstag bis Samstag offen.', p: 'Von 11 bis 17 Uhr steht die Tür zur Werkstatt offen. Gruppen bitte vorher anmelden.' }
      ],
      unter: { label: 'Okelmann Keramik · Werkstatt', h1: 'In der Werkstatt.', satz: 'Ein Brand dauert drei Tage: einen zum Aufheizen, einen im Feuer, einen zum Abkühlen.', bild: [M + 'keramik-stillleben.jpg', 'Handgedrehte Keramik auf Gipssockeln vor einer terrakottafarbenen Hohlkehle'],
        liste: [['Drehen', 'Ton aus der Region, auf der Scheibe hochgezogen', 'Tag 1'], ['Trocknen', 'langsam, unter Tüchern, damit nichts reißt', 'bis 3 Wochen'], ['Salzbrand', 'Salz in den heißen Ofen, die Glasur wächst im Feuer', '1.250 °C']],
        hinweis: 'Okelmann Keramik ist eine erfundene Werkstatt für diese Demo.' }
    },
    beides: {
      mod: 'fest', vt: true, name: 'Hirschkamm', voll: 'Naturpark Hirschkamm', f: [30, 26],
      bild: { src: M + 'huegel-nebel.jpg', hoch: M + 'huegel-nebel-hoch.jpg', alt: 'Gestaffelte bewaldete Hügelketten im Morgennebel bei tiefer Sonne' },
      zeile: ['Naturpark', 'Schmiedefeld · 780 m'], nav: [['Touren', 'unter'], ['Anreise', '#anreise'], ['Wetter', '#wetter']], cta: 'Tour planen',
      h1: 'Drei Kämme, ein Nebelmorgen.', satz: 'Wandern, wo der Wald in Stufen hintereinander steht. Scrollen Sie halb nach unten und öffnen Sie dann die Touren: Die Marke fliegt aus ihrem Zwischenstand.',
      reihen: [
        { id: 'touren', label: 'Touren', h2: 'Vom Kammweg bis zur kurzen Nebelrunde.', p: 'Acht markierte Touren zwischen sechs und 23 Kilometern. Alle beginnen an einer Haltestelle.', bild: [M + 'huegel-nebel-hoch.jpg', 'Bewaldete Hügelketten im Nebel, Hochformat'], link: 'Alle Touren' },
        { id: 'anreise', label: 'Anreise', h2: 'Mit dem Bus bis an den Wald.', p: 'Die Linie 14 fährt stündlich vom Bahnhof bis Schmiedefeld. Mit der Gästekarte fahren Sie kostenlos.' },
        { id: 'wetter', label: 'Wetter', h2: 'Auf dem Kamm ist es fünf Grad kälter.', p: 'Nebel hält sich oft bis zehn Uhr. Für die Kammtouren lohnt eine Jacke, auch im Sommer.' }
      ],
      unter: { label: 'Naturpark Hirschkamm · Touren', h1: 'Touren am Hirschkamm.', satz: 'Strecke, Höhenmeter, Dauer und Schwierigkeit auf einen Blick. Start und Ziel sind immer eine Haltestelle.', bild: [M + 'huegel-nebel.jpg', 'Gestaffelte bewaldete Hügelketten im Morgennebel bei tiefer Sonne'],
        liste: [['Kammweg', 'über alle drei Kämme, Aussicht am Hirschstein', '14,2 km · ↑ 420 m · 4:30 h'], ['Nebelrunde', 'kurz und flach, gut am frühen Morgen', '6,8 km · ↑ 160 m · 2:00 h'], ['Drei-Kämme-Tour', 'lang, mit zwei steilen Abstiegen', '22,5 km · ↑ 780 m · 7:00 h']],
        hinweis: 'Beispieldaten einer erfundenen Region. Wegdaten vom Kunden prüfen lassen.' }
    }
  };

  // ---------- Zustand aus der URL ----------
  let v = W[q.get('v')] ? q.get('v') : 'scroll';
  let seite = q.get('seite') === 'unter' ? 'unter' : 'start';
  const url = (s, extra) => { const u = new URL(location.href); u.searchParams.set('v', v); if (s === 'unter') u.searchParams.set('seite', 'unter'); else u.searchParams.delete('seite'); u.hash = extra || ''; return u.pathname + u.search + u.hash; };

  // ---------- Seiten bauen ----------
  function bauen() {
    const w = W[v];
    html.dataset.v = v;
    html.dataset.seite = seite;
    html.dataset.mod = seite === 'start' ? w.mod : 'kopf';
    html.dataset.vt = w.vt && !reduziert() ? 'an' : 'aus';
    html.classList.remove('marke-oben');
    marke.textContent = w.name;
    marke.href = url('start');
    marke.setAttribute('aria-label', w.voll + ', Startseite');
    nav.innerHTML = w.nav.map(([t, z]) => `<a href="${z === 'unter' ? url('unter') : url('start', z)}"${z === 'unter' && seite === 'unter' ? ' aria-current="page"' : ''}>${t}</a>`).join('');
    cta.textContent = w.cta; cta.href = url(seite, '#kontakt');
    const wort = `<span class="held__abbild" aria-hidden="true">${w.name}</span>`;
    if (seite === 'start') {
      const held = w.mod === 'fest'
        ? `<section class="held" aria-label="${w.voll}">
             <picture class="held__bild"><source media="(max-width: 640px)" srcset="${w.bild.hoch}"><img src="${w.bild.src}" alt="${w.bild.alt}" fetchpriority="high" width="1920" height="1200"></picture>
             <div class="held__zeile label"><span>${w.zeile[0]}</span><span>${w.zeile[1]}</span></div>
             <div class="held__platz" id="heldplatz">${wort}</div>
           </section>`
        : `<section class="held held--fluss" aria-label="${w.voll}">
             <a class="held__gross" id="heldgross" href="${url('start')}" aria-label="${w.voll}, Startseite">${w.name}</a>
             <figure class="held__band"><img src="${w.bild.src}" alt="${w.bild.alt}" fetchpriority="high" width="1200" height="800"></figure>
           </section>`;
      seiteEl.className = 'start';
      seiteEl.innerHTML = held + `<div class="inhalt">
          <div class="einstieg"><h1>${w.h1}</h1><p>${w.satz}</p></div>
          ${w.reihen.map(r => `<section class="reihe" id="${r.id}" aria-labelledby="t-${r.id}">
            ${r.bild ? `<figure><img src="${r.bild[0]}" alt="${r.bild[1]}" loading="lazy" width="1200" height="800"></figure>` : '<div aria-hidden="true"></div>'}
            <div><p class="label leise">${r.label}</p><h2 id="t-${r.id}">${r.h2}</h2><p>${r.p}</p>${r.link ? `<a class="weiter" href="${url('unter')}">${r.link} <span aria-hidden="true">→</span></a>` : ''}</div>
          </section>`).join('')}
        </div>`;
    } else {
      const u = w.unter;
      seiteEl.className = 'unter';
      seiteEl.innerHTML = `<div class="inhalt">
          <div class="unter__kopf"><div><p class="label leise">${u.label}</p><h1 style="margin-top:12px">${u.h1}</h1></div><p>${u.satz}</p></div>
          <figure class="unter__bild"><img src="${u.bild[0]}" alt="${u.bild[1]}" width="1200" height="800"></figure>
          <ul class="liste">${u.liste.map(([a, b, c]) => `<li><b>${a}</b><span>${b}</span><span class="zahl">${c}</span></li>`).join('')}</ul>
          <p class="hinweis">${u.hinweis}</p>
          <a class="zurueck" href="${url('start')}"><span aria-hidden="true">←</span> Zur Startseite</a>
        </div>`;
    }
    seiteEl.insertAdjacentHTML('beforeend', `<footer class="fusszeile" id="kontakt"><p>${w.voll} · erfundene Marke für diese Demo · <a href="${url(seite === 'start' ? 'unter' : 'start')}">${seite === 'start' ? w.unter.label.split(' · ')[1] : 'Startseite'}</a></p></footer>`);
    document.querySelectorAll('[data-wahl]').forEach(b => b.setAttribute('aria-pressed', String(b.dataset.wahl === v)));
    $('zu-start').href = url('start'); $('zu-unter').href = url('unter');
    $('zu-start').toggleAttribute('aria-current', seite === 'start'); $('zu-unter').toggleAttribute('aria-current', seite === 'unter');
    if (seite === 'start') $('zu-start').setAttribute('aria-current', 'page'); else $('zu-unter').setAttribute('aria-current', 'page');
    messen(); if (typeof beobachte === 'function') beobachte();
  }

  // ---------- Messen: Hero-Größe einpassen, Weg in den Kopf ----------
  function messen() {
    const w = W[v], s = matchMedia('(max-width: 640px)').matches ? w.f[1] : w.f[0];
    const rand = parseFloat(getComputedStyle(seiteEl.querySelector('.inhalt') || document.body).paddingLeft) || 20;
    html.style.setProperty('--f', s + 'px');
    // Breite der Marke bei 100 px: max-content, gleiche Laufweite
    marke.style.fontSize = '100px'; marke.style.transform = 'none';
    const b100 = marke.getBoundingClientRect().width || 1;
    marke.style.fontSize = ''; marke.style.transform = '';
    const platzBreite = b100 / 100 * s;
    platz.style.width = platzBreite + 'px';
    const F = Math.max(s, (innerWidth - 2 * rand) / b100 * 100 * .995);
    const lh = parseFloat(getComputedStyle(html).getPropertyValue('--lh-marke')) || .9;
    html.style.setProperty('--F', F + 'px');
    html.style.setProperty('--Fh', (F * lh) + 'px');
    if (html.dataset.mod === 'fest') {
      const hp = $('heldplatz');
      const y0 = hp.getBoundingClientRect().top + scrollY;       // Lage im Hero bei scrollY 0
      const kopfH = $('kopf').offsetHeight, ziel = (kopfH - s * lh) / 2; // Platz im Kopf, senkrecht mittig
      html.style.setProperty('--x0', rand + 'px');
      html.style.setProperty('--y0', y0 + 'px');
      html.style.setProperty('--dx', '0px');
      html.style.setProperty('--dy', (ziel - y0) + 'px');
      html.style.setProperty('--s', String(s / F));
      html.style.setProperty('--weg', Math.max(120, y0 - ziel) + 'px'); // Marke fährt genau mit der Seite hoch
    }
    takt();
  }

  // ---------- Rückfall ohne scroll(): --p per Skript; Seitenwechsel: Marke oben? ----------
  let wartet = false;
  function takt() {
    wartet = false;
    if (html.dataset.mod === 'fest' && html.classList.contains('rueckfall') && !reduziert()) {
      const weg = parseFloat(html.style.getPropertyValue('--weg')) || 400;
      html.style.setProperty('--p', Math.min(1, Math.max(0, scrollY / weg)).toFixed(4));
    }
    if (html.dataset.mod === 'fluss') {
      const g = $('heldgross');
      const oben = !!g && g.getBoundingClientRect().bottom < $('kopf').offsetHeight;
      html.classList.toggle('marke-oben', oben);
      g && g.toggleAttribute('inert', false);
      marke.toggleAttribute('inert', !oben);
    } else marke.removeAttribute('inert');
    zeige();
  }
  addEventListener('scroll', () => { if (!wartet) { wartet = true; requestAnimationFrame(takt); } }, { passive: true });
  let rz; addEventListener('resize', () => { clearTimeout(rz); rz = setTimeout(messen, 80); });
  document.fonts && document.fonts.ready.then(messen);
  // Einpassen neu messen, wenn sich die Schrift ändert (Textabstände 1.4.12, Zoom): beobachtet wird der ruhige Schriftzug, nicht die Marke selbst
  let roWert = '';
  const ro = window.ResizeObserver && new ResizeObserver(() => { const el = $('heldgross') || seiteEl.querySelector('.held__abbild'); const w = el ? Math.round(el.getBoundingClientRect().width) + '/' + getComputedStyle(el).letterSpacing : ''; if (w !== roWert) { roWert = w; requestAnimationFrame(messen); } });
  function beobachte() { if (!ro) return; ro.disconnect(); const el = $('heldgross') || seiteEl.querySelector('.held__abbild'); if (el) ro.observe(el); }

  // Pult: steht animation-range wirklich auf den gesetzten Werten?
  function zeige() {
    const o = $('messung');
    const vtText = W[v].vt ? (kannCross ? 'Cross-Document an' : 'Cross-Document hier nicht unterstützt, Link lädt ohne Übergang') : 'kein Seitenübergang (nur Scroll)';
    if (reduziert()) { o.textContent = 'reduziert: Marke steht im Kopf, kein Flug'; return; }
    if (html.dataset.mod === 'fest') {
      if (html.classList.contains('rueckfall')) { o.textContent = '--p per Skript (kein scroll()) · ' + vtText; return; }
      const cs = getComputedStyle(html);
      o.textContent = `animation-range ${cs.animationRangeStart} / ${cs.animationRangeEnd} · --p ${(+cs.getPropertyValue('--p')).toFixed(2)} · ${vtText}`;
    } else o.textContent = (seite === 'start' ? 'Marke im Hero, Kopfmarke erscheint beim Scrollen · ' : 'Unterseite · ') + vtText;
  }

  // ---------- Cross-Document: Richtung und Abschalten ----------
  function richtung(ziel) { try { return new URL(ziel, location.href).searchParams.get('seite') === 'unter' ? 'hin' : 'zurueck'; } catch (e) { return 'hin'; } }
  addEventListener('pageswap', (e) => {
    if (!e.viewTransition) return;
    if (!W[v].vt || reduziert()) { e.viewTransition.skipTransition(); return; }
    // Namen nur auf der Marke, die gerade zu sehen ist (CSS oben); hier nur die Richtung
    const ziel = e.activation && e.activation.entry && e.activation.entry.url;
    if (ziel) e.viewTransition.types.add(richtung(ziel));
  });
  addEventListener('pagereveal', (e) => {
    if (!e.viewTransition) return;
    if (!W[v].vt || reduziert()) { e.viewTransition.skipTransition(); return; }
    const a = window.navigation && navigation.activation;
    e.viewTransition.types.add(a && a.entry ? richtung(a.entry.url) : (seite === 'unter' ? 'hin' : 'zurueck'));
  });

  // ---------- Pult ----------
  document.querySelectorAll('[data-wahl]').forEach(b => b.addEventListener('click', () => {
    pause(); v = b.dataset.wahl; seite = 'start';
    history.replaceState(history.state, '', url('start')); scrollTo(0, 0); bauen();
  }));

  // ---------- Vorführung ----------
  // Scroll: Selbst-Scroll. Seitenwechsel: derselbe Übergang im Dokument (startViewTransition), die CSS-Regeln gelten 1:1.
  let rahmen = null, uhr = null;
  const sinus = (t) => 0.5 - 0.5 * Math.cos(Math.PI * t);
  function fahre(von, bis, ms) {
    return new Promise(res => {
      const t0 = performance.now();
      const lauf = (t) => { const k = Math.min(1, (t - t0) / ms); scrollTo(0, von + (bis - von) * sinus(k)); if (k < 1) rahmen = requestAnimationFrame(lauf); else res(); };
      rahmen = requestAnimationFrame(lauf);
    });
  }
  const warte = (ms) => new Promise(res => { uhr = setTimeout(res, ms); });
  function wechsel(neu) {
    const typ = neu === 'unter' ? 'hin' : 'zurueck';
    const update = () => { seite = neu; bauen(); scrollTo(0, 0); };
    if (!document.startViewTransition || reduziert() || !W[v].vt) { update(); return Promise.resolve(); }
    let vt; try { vt = document.startViewTransition({ update, types: [typ] }); } catch (e) { vt = document.startViewTransition(update); }
    return vt.finished.catch(() => {});
  }
  let lauf = 0;
  async function vorfuehren() {
    pause(); const id = ++lauf;
    const ok = () => id === lauf;
    if (seite !== 'start') { seite = 'start'; bauen(); }
    scrollTo(0, 0);
    const weg = () => parseFloat(html.style.getPropertyValue('--weg')) || 400;
    while (ok()) {
      if (v === 'scroll') {
        await warte(700); if (!ok()) return;
        if (reduziert()) { scrollTo(0, weg() * 1.6); await warte(1600); if (!ok()) return; scrollTo(0, 0); await warte(1400); continue; }
        await fahre(0, weg() * 1.25, 2600); if (!ok()) return;
        await warte(900); if (!ok()) return;
        await fahre(scrollY, 0, 1800); if (!ok()) return;
      } else {
        await warte(900); if (!ok()) return;
        if (v === 'beides' && !reduziert()) { await fahre(0, weg() * .5, 1400); if (!ok()) return; await warte(500); if (!ok()) return; }
        await wechsel('unter'); if (!ok()) return;
        await warte(1600); if (!ok()) return;
        await wechsel('start'); if (!ok()) return;
        await warte(600);
      }
    }
  }
  function pause() { lauf++; cancelAnimationFrame(rahmen); clearTimeout(uhr); }
  function neustart() { pause(); if (seite !== 'start') { seite = 'start'; bauen(); } scrollTo(0, 0); takt(); }
  function weiter() { vorfuehren(); }

  new MutationObserver(() => { html.dataset.vt = W[v].vt && !reduziert() ? 'an' : 'aus'; takt(); }).observe(html, { attributes: true, attributeFilter: ['class'] });
  bauen();

})();
</script>
<div id="ende" hidden></div>
</body>
</html>