Zum Inhalt
← Navigation

Theme- und Sprachumschalter mit View-Transition-Kreis-Reveal

Hell, Dunkel oder Sprache wechseln, und der neue Zustand breitet sich als Kreis vom Klickpunkt über die Seite aus.

Seiten mit eigener Bildwelt aus Inline-SVG oder CSS, deren Farben komplett an Tokens hängen: Hotel, Gastronomie, Portfolio. Läuft in allen Browsern mit View Transitions, sonst Sofortwechsel.

Ein Klick auf den Theme-Schalter oder den Sprachlink merkt sich die Klickposition. document.startViewTransition() friert den alten Zustand als Bild ein, dann wird umgeschaltet. Der neue Zustand wird per clip-path: circle() vom Klickpunkt aus aufgezogen, bis der Kreis die entfernteste Ecke erreicht. Der alte Zustand bleibt darunter stehen. Die Wahl wird gespeichert und vor dem ersten Paint angewendet, damit nichts aufblitzt.

Ein banaler Schalter wird zu einem kleinen Moment. Die neue Farbe kommt sichtbar vom Knopf, Ursache und Wirkung sind sofort klar.

Wann einsetzen

Portfolio- und Produktseiten mit gepflegtem Hell- und Dunkel-Design. Mehrsprachige Seiten, auf denen der Sprachwechsel ohne Neuladen passiert. Andere Modus-Wechsel, etwa ein Persona- oder Farbschema-Umschalter.

Wann nicht

Ohne sauberes Token-System für beide Themes, sonst entstehen Mischzustände. Wenn kein echtes Dark Theme gepflegt wird. Wenn der Wechsel sehr oft passiert, der Effekt nutzt sich ab. Bei sehr großen Seiten mit vielen Bildern kostet der Schnappschuss spürbar Zeit.

Varianten

  • Kreis vom Klickpunkt (Standard, 420 ms ease-out)
  • Wipe von oben oder links über inset() statt circle()
  • Kurze Überblendung (ca. 200 ms) für ruhige Seiten
  • Drei Zustände System, Hell, Dunkel als Auswahl, etwa im Footer (Vercel)
  • Sprachumschalter als dezenter Textlink in der Zielsprache statt Flaggen (Locomotive, Your Majesty)
  • Light und Dark pro Komponente statt für die ganze Seite
  • light-dark() mit Bildern (Chrome 150): ein Satz Farben und Bilder, umgeschaltet wird nur color-scheme

Umsetzung

Beim Klick x und y aus clientX/clientY nehmen, bei Tastatur (event.detail 0) die Mitte des Knopfs. Radius r = Math.hypot(Math.max(x, innerWidth - x), Math.max(y, innerHeight - y)). Ohne document.startViewTransition oder bei reduzierter Bewegung direkt umschalten. Sonst const vt = document.startViewTransition(umschalten); vt.ready.then(() => document.documentElement.animate({ clipPath: [`circle(0px at ${x}px ${y}px)`, `circle(${r}px at ${x}px ${y}px)`] }, { duration: 420, easing: 'cubic-bezier(0, 0, 0.58, 1)', pseudoElement: '::view-transition-new(root)' })). CSS: ::view-transition-old(root), ::view-transition-new(root) { animation: none; mix-blend-mode: normal; }. Themes nur über Tokens wechseln (data-theme am html) und Farben nicht transitionieren, sonst fotografiert die Transition einen Zwischenstand. Wahl in localStorage speichern (try/catch), color-scheme setzen und per Inline-Script im head vor dem ersten Paint anwenden. Das storage-Event gleicht andere Tabs ab. Auf einer echten mehrsprachigen Seite mit eigenen URLs braucht der Sprachwechsel Cross-document View Transitions (@view-transition { navigation: auto; }); der Kreis lässt sich dann über pageswap und pagereveal mit der gespeicherten Klickposition nachbauen.

Robust umsetzen

Telefon. Funktioniert identisch, der Kreis startet am Tap-Punkt. Ziele mindestens 44 px groß.

Ohne Bewegung. Sofortwechsel ohne View Transition. Zusätzlich alle ::view-transition-* per prefers-reduced-motion auf animation: none setzen.

Barrierefrei. Theme-Knopf mit Namen, der das Ziel nennt („Zu dunklem Design wechseln“). Beide Icons im Markup, das inaktive mit display: none. Immer eine Rückkehr zu „System“ anbieten, am besten als Radiogruppe. Sprachlink mit lang und hreflang, Sprachname in der eigenen Sprache („English“, „Deutsch“). Bei Tastaturbedienung startet der Kreis in der Mitte des Bedienelements.

Code

<!DOCTYPE html>
<!--
  Theme- und Sprachumschalter mit View-Transition-Kreis-Reveal
  Fundus · Navigation · https://cecodesign.de/lab/fundus/navigation/theme-umschalter-view-transition/
  Eigenständige Datei. Bilder liegen hier unter bilder/ – eigene einsetzen.
-->
<html lang="de" data-theme="hell" data-wahl="system">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="color-scheme" content="light dark">
<title>Theme- und Sprachumschalter mit View-Transition-Kreis-Reveal · Fundus-Demo</title>
<script>document.documentElement.classList.add('js');if(matchMedia('(prefers-reduced-motion: reduce)').matches)document.documentElement.classList.add('reduziert')</script>
<script>/* Vor dem ersten Paint: gespeicherte Wahl anwenden, damit nichts aufblitzt */
(function () {
  var h = document.documentElement, w = 'system';
  try { w = localStorage.getItem('seeweide-darstellung') || 'system'; } catch (e) {}
  var dunkel = w === 'dunkel' || (w === 'system' && matchMedia('(prefers-color-scheme: dark)').matches);
  h.dataset.wahl = w; h.dataset.theme = dunkel ? 'dunkel' : 'hell';
  if (new URLSearchParams(location.search).get('v') === 'light-dark') h.dataset.welt = 'produkt';
})();
</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: 'Geist'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/geist:vf@latest/latin-wght-normal.woff2') format('woff2-variations'); font-weight: 100 900; font-display: swap; }
@font-face { font-family: 'Geist Mono'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/geist-mono:vf@latest/latin-wght-normal.woff2') format('woff2-variations'); font-weight: 100 900; font-display: swap; }

/* Zwei vollständige Token-Sätze: Tag (Seenebel am Morgen) und Nacht (Lichter im Hafen).
   Nur Tokens wechseln, keine Mischzustände, keine Farb-Transitions
   (sonst fotografiert die View Transition einen halben Übergang). */
:root, :root[data-theme='hell'] {
  color-scheme: light;
  --grund: #e3e8e5; --karte: #fbf6ee; --text: #3e3a37; --leise: rgba(62, 58, 55, .76); --linie: rgba(62, 58, 55, .15);
  --akzent: #2e5b57; --auf-akzent: #fbf6ee;
  /* Szene */
  --s-himmel: #d3dfe0; --s-sonne: #f6f1e6; --s-berg-fern: #bccacd; --s-berg: #a4b5b8; --s-schnee: #e9eeee;
  --s-see: #a9bfc1; --s-welle: #c9d8d8; --s-ufer: #8ea3a3; --s-baum: #5d7563; --s-baum-hell: #7a917c;
  --s-fassade: #f6f0e5; --s-dach: #6e5a47; --s-fenster: #8ea3a6; --s-fenster-aus: #8ea3a6; --s-holz: #6e5a47; --s-streifen: #2e5b57;
  --s-spiegel: .32; --nacht: 0; --tag: 1;
}
:root[data-theme='dunkel'] {
  color-scheme: dark;
  --grund: #0c1317; --karte: #152028; --text: #ece5d7; --leise: rgba(236, 229, 215, .62); --linie: rgba(236, 229, 215, .13);
  --akzent: #9cc6bc; --auf-akzent: #0c1317;
  --s-himmel: #0f1b24; --s-sonne: #ece4cf; --s-berg-fern: #17252f; --s-berg: #1d2e39; --s-schnee: #2a3c48;
  --s-see: #0a151c; --s-welle: #1b2e3a; --s-ufer: #0e1a21; --s-baum: #12201b; --s-baum-hell: #182a23;
  --s-fassade: #27303a; --s-dach: #171d23; --s-fenster: #f1c98c; --s-fenster-aus: #1f2830; --s-holz: #1e262c; --s-streifen: #3b5b5d;
  --s-spiegel: .55; --nacht: 1; --tag: 0;
}
:root {
  --vt-kurve: cubic-bezier(0, 0, 0.58, 1);   /* ease-out, gemessen: 420 ms */
  --ui: cubic-bezier(0.16, 1, 0.3, 1);       /* expo.out für den Vorführ-Zeiger */
  --serif: 'Newsreader', Georgia, serif;
  --sans: 'Mona Sans', system-ui, sans-serif;
}
* { box-sizing: border-box; margin: 0; }
html { background: var(--grund); color: var(--text); font-family: var(--sans); font-stretch: 100%; -webkit-font-smoothing: antialiased; }
body { min-height: 100svh; }
.seite[hidden] { display: none; }
:root[data-welt='produkt'] .seite:not(.produkt) { display: none; }
a { color: inherit; }
button { font: inherit; color: inherit; }
/* Labels nach Rezept: Mona Sans breit, versal */
.label { font: 560 11px/1.3 var(--sans); font-stretch: 120%; letter-spacing: .08em; text-transform: uppercase; color: var(--leise); }
/* Demo-Steuerung: Geist Mono wie bei den Piloten */
.mono { font: 500 11px/1.3 'Geist Mono', ui-monospace, monospace; letter-spacing: .14em; text-transform: uppercase; color: var(--leise); }

.seite { min-height: 100svh; display: grid; grid-template-rows: auto minmax(0, 1fr) auto auto auto; padding: clamp(16px, 2.2vw, 30px); padding-bottom: clamp(14px, 1.6vw, 20px); gap: clamp(14px, 1.8vw, 24px); }

/* Kopf: Navigation links, Wortmarke Mitte, Sprache und Tag/Nacht rechts */
.kopf { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 20px; }
.nav { display: flex; gap: clamp(16px, 2vw, 30px); list-style: none; padding: 0; }
.nav a { text-decoration: none; color: var(--leise); font-size: 11px; font-weight: 560; font-stretch: 120%; letter-spacing: .08em; text-transform: uppercase; }
.nav a:hover, .nav a[aria-current] { color: var(--text); }
.marke { justify-self: center; display: grid; justify-items: center; gap: 3px; text-decoration: none; line-height: 1; }
.marke b { font: 400 25px/1 var(--serif); font-variation-settings: 'opsz' 72; letter-spacing: .26em; margin-right: -.26em; text-transform: uppercase; }
.marke small { font-size: 9px; font-weight: 600; font-stretch: 125%; letter-spacing: .3em; margin-right: -.3em; text-transform: uppercase; color: var(--leise); }
.schalter { justify-self: end; display: flex; align-items: center; gap: 6px; }
.sprache { display: inline-flex; align-items: center; height: 40px; padding: 0 14px; border-radius: 999px; font-size: 14px; text-decoration: none; }
.sprache:hover { background: var(--karte); }
.thema { width: 40px; height: 40px; border-radius: 50%; border: 1px solid var(--linie); background: none; display: grid; place-items: center; cursor: pointer; }
.thema:hover { background: var(--karte); }
.thema svg { width: 18px; height: 18px; }
:root[data-theme='hell'] .thema .sonne, :root[data-theme='dunkel'] .thema .mond { display: none; }
:is(.sprache, .thema, .nav a, .buchen, .feld, .marke, .wahl label):focus-visible, .wahl input:focus-visible + span { outline: 2px solid var(--akzent); outline-offset: 2px; }
.gedrueckt { scale: .92; transition: scale .15s var(--ui); }

/* Bühne: Seepanorama als SVG, alle Farben aus den Tokens */
.held { position: relative; min-height: 300px; border-radius: 6px; overflow: hidden; display: flex; flex-direction: column; justify-content: space-between; padding: clamp(20px, 2.8vw, 40px); background: var(--s-himmel); }
.szene { position: absolute; inset: 0; width: 100%; height: 100%; display: block; }
.nur-nacht { opacity: var(--nacht); }
.nur-tag { opacity: var(--tag); }
.spiegel { opacity: var(--s-spiegel); }
.held-text { position: relative; display: grid; gap: 12px; max-width: 36rem; }
.titel { font: 340 clamp(36px, 5.6vw, 70px)/1.02 var(--serif); font-variation-settings: 'opsz' 72; letter-spacing: -.018em; text-wrap: balance; }
.titel em { font-style: italic; font-weight: 300; color: var(--akzent); }
/* Buchungsleiste direkt im Fuß des Hero */
.buchung-box { position: relative; background: var(--karte); border-radius: 4px; max-width: 760px; }
.buchung { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)) auto; align-items: stretch; padding: 6px; gap: 4px; }
.feld { display: grid; gap: 3px; align-content: center; padding: 8px 14px; border-radius: 3px; background: none; border: 0; text-align: left; cursor: pointer; }
.feld + .feld { box-shadow: -1px 0 0 var(--linie); }
.feld .label { font-size: 9.5px; }
.feld b { font-weight: 500; font-size: 14px; letter-spacing: -.005em; white-space: nowrap; }
.buchen { display: inline-flex; align-items: center; gap: 10px; padding: 0 20px; min-height: 48px; border-radius: 3px; border: 0; background: var(--akzent); color: var(--auf-akzent); font-size: 11px; font-weight: 620; font-stretch: 118%; letter-spacing: .08em; text-transform: uppercase; cursor: pointer; text-decoration: none; }
.hinweis { padding: 9px 20px 10px; border-top: 1px solid var(--linie); font-size: 12px; color: var(--leise); }
.hinweis b { font-weight: 560; color: var(--text); }

/* Drei Kacheln: Zimmer, Restaurant, Anreise – eigene Bildwelten */
.haus { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: clamp(12px, 1.6vw, 22px); list-style: none; padding: 0; }
.kachel figure { aspect-ratio: 2.5 / 1; border-radius: 4px; overflow: hidden; background: var(--karte); position: relative; }
.kachel figure svg { position: absolute; inset: 0; width: 100%; height: 100%; display: block; }
.kachel p { display: flex; justify-content: space-between; align-items: baseline; gap: 12px; padding-top: 9px; }
.kachel b { font: 400 18px/1.15 var(--serif); font-variation-settings: 'opsz' 24; letter-spacing: -.005em; }
.kachel small { display: block; font-size: 12.5px; color: var(--leise); margin-top: 2px; font-family: var(--sans); }
.kachel .label { white-space: nowrap; }
/* Speisekarte als Typo-Kachel */
.karte-typo { position: absolute; inset: 0; display: grid; align-content: center; gap: 6px; padding: 0 9%; }
.karte-typo .label { font-size: 9px; color: var(--akzent); }
.karte-typo .gericht { display: flex; align-items: baseline; gap: 8px; padding: 0; font: 400 15px/1.2 var(--serif); font-variation-settings: 'opsz' 18; }
.karte-typo .gericht i { font-style: italic; color: var(--leise); }
.karte-typo .gericht::after { content: ''; order: 1; flex: 1; border-bottom: 1px dotted var(--linie); transform: translateY(-3px); }
.karte-typo .gericht b { font: inherit; letter-spacing: 0; white-space: nowrap; }
.karte-typo .gericht span { order: 2; font-family: var(--sans); font-size: 12px; font-variant-numeric: tabular-nums; }
.linie-svg { stroke: var(--text); fill: none; }

/* Fuß mit Drei-Wege-Wahl */
.fuss { display: flex; justify-content: space-between; align-items: center; gap: 12px 24px; flex-wrap: wrap; padding-top: 12px; border-top: 1px solid var(--linie); }
.wahl { border: 0; padding: 0; display: flex; align-items: center; gap: 14px; }
.wahl legend { float: left; }
.wahl-spur { display: flex; padding: 3px; border-radius: 999px; border: 1px solid var(--linie); }
.wahl label { position: relative; cursor: pointer; }
.wahl input { position: absolute; opacity: 0; inset: 0; margin: 0; cursor: pointer; }
.wahl span { display: block; padding: 6px 12px; border-radius: 999px; color: var(--leise); }
.wahl input:checked + span { background: var(--text); color: var(--grund); }
.adresse { font-size: 12.5px; color: var(--leise); }

/* Demo-Steuerung: dezent, Geist Mono */
.steuerung { display: flex; justify-content: space-between; align-items: center; gap: 10px 24px; flex-wrap: wrap; padding-top: 12px; border-top: 1px dashed var(--linie); }
.modi { display: flex; align-items: center; gap: 4px; }
.modi > span { margin-right: 8px; }
.modi button { font: inherit; letter-spacing: inherit; text-transform: inherit; color: var(--leise); background: none; border: 1px solid var(--linie); border-radius: 999px; height: 28px; padding: 0 11px; cursor: pointer; }
.modi button:hover { color: var(--text); }
.modi button[aria-pressed='true'] { background: var(--text); color: var(--grund); border-color: var(--text); }
.werte { display: flex; flex-wrap: wrap; gap: 4px 16px; }
.werte b { color: var(--text); font-weight: 500; }

/* ── Variante light-dark(): ein Satz Farben, Bilder je Farbschema ──────────
   Welt: Produktseite der erfundenen Software „Werkbuch“ (Rezept saas-produkt-praezise, Geist).
   Umgeschaltet wird nur color-scheme am <html>. Farben stehen einmal als light-dark(hell, dunkel) da;
   seit Chrome 150 nimmt light-dark() auch Bilder (url(), image-set(), none). */
:root[data-welt='produkt'][data-wahl='system'] { color-scheme: light dark; }   /* System: light-dark() folgt direkt dem Betriebssystem */
:root[data-welt='produkt'] { background: light-dark(#f6f7f9, #08090b); }
.produkt {
  --p-grund: light-dark(#f6f7f9, #08090b); --p-flaeche: light-dark(#ffffff, #121418);
  --p-text: light-dark(#14161a, #eceef2); --p-leise: light-dark(#5d626c, #8a8f98);
  --p-linie: light-dark(#e2e5ea, #23262c); --p-akzent: light-dark(#3346d3, #8f9cff); --p-auf-akzent: light-dark(#ffffff, #08090b);
  --p-kurve: cubic-bezier(0.25, 0.46, 0.45, 0.94);   /* UI 150 ms nach Rezept */
  /* Steuerung und Fuß der Grundvariante lesen diese Tokens mit */
  --grund: var(--p-grund); --karte: var(--p-flaeche); --text: var(--p-text); --leise: var(--p-leise); --linie: var(--p-linie); --akzent: var(--p-akzent);
  background: var(--p-grund); color: var(--p-text); font-family: 'Geist', system-ui, sans-serif;
  grid-template-rows: auto auto auto auto auto auto; gap: 0; max-width: 1240px; margin: 0 auto;
}
.p-kopf { display: flex; align-items: center; gap: 28px; height: 52px; }
.p-marke { display: flex; align-items: center; gap: 9px; text-decoration: none; font-weight: 600; font-size: 16px; letter-spacing: -.02em; }
.p-marke svg { width: 24px; height: 24px; color: var(--p-akzent); }
.p-nav { display: flex; gap: 22px; list-style: none; padding: 0; font-size: 14px; color: var(--p-leise); }
.p-nav a { text-decoration: none; transition: color 150ms var(--p-kurve); }
.p-nav a:hover { color: var(--p-text); }
.p-rechts { margin-left: auto; display: flex; align-items: center; gap: 10px; font-size: 14px; }
.p-rechts > a:first-child { text-decoration: none; color: var(--p-leise); padding: 0 8px; }
.produkt .thema { width: 36px; height: 36px; border-radius: 8px; border-color: var(--p-linie); }
.produkt .thema:hover { background: var(--p-flaeche); }
.p-knopf { display: inline-flex; align-items: center; gap: 8px; height: 36px; padding: 0 14px; border-radius: 8px; font-size: 14px; font-weight: 500; text-decoration: none; background: var(--p-akzent); color: var(--p-auf-akzent); transition: filter 150ms var(--p-kurve); }
.p-knopf:hover { filter: brightness(1.08); }
.p-knopf.zweit { background: var(--p-flaeche); color: var(--p-text); box-shadow: inset 0 0 0 1px var(--p-linie); }
.p-held { display: grid; justify-items: center; text-align: center; gap: 18px; padding: clamp(40px, 7vh, 76px) 0 clamp(30px, 5vh, 52px); }
.p-label { display: inline-flex; gap: 10px; align-items: center; height: 28px; padding: 0 12px; border-radius: 999px; box-shadow: inset 0 0 0 1px var(--p-linie); }
.p-label b { color: var(--p-akzent); font-weight: 500; }
.p-titel { font-size: clamp(2.5rem, 1.6rem + 3.6vw, 4.5rem); font-weight: 600; letter-spacing: -.022em; line-height: 1; text-wrap: balance; max-width: 15ch; }
.p-unter { font-size: 17px; line-height: 1.6; letter-spacing: -.006em; color: var(--p-leise); max-width: 52ch; text-wrap: pretty; }
.p-ctas { display: flex; gap: 10px; margin-top: 6px; }
/* Bilder: Fallback zuerst (Selektor am Theme), darüber die native Weiche */
.p-bild { background: var(--p-flaeche) center / cover no-repeat; }
.p-app { aspect-ratio: 1600 / 1000; border-radius: 14px; box-shadow: 0 0 0 1px var(--p-linie), 0 30px 80px -40px rgba(0, 0, 0, .45); background-image: url('bilder/theme-umschalter-view-transition-app-hell.svg'); }
.p-bericht { background-image: url('bilder/theme-umschalter-view-transition-bericht-hell.svg'); }
.p-telefon { background-image: url('bilder/theme-umschalter-view-transition-telefon-hell.svg'); }
:root[data-theme='dunkel'] .p-app { background-image: url('bilder/theme-umschalter-view-transition-app-dunkel.svg'); }
:root[data-theme='dunkel'] .p-bericht { background-image: url('bilder/theme-umschalter-view-transition-bericht-dunkel.svg'); }
:root[data-theme='dunkel'] .p-telefon { background-image: url('bilder/theme-umschalter-view-transition-telefon-dunkel.svg'); }
@supports (background-image: light-dark(url("data:,a"), url("data:,b"))) {
  .p-app, :root[data-theme] .p-app { background-image: light-dark(url('bilder/theme-umschalter-view-transition-app-hell.svg'), url('bilder/theme-umschalter-view-transition-app-dunkel.svg')); }
  .p-bericht, :root[data-theme] .p-bericht { background-image: light-dark(url('bilder/theme-umschalter-view-transition-bericht-hell.svg'), url('bilder/theme-umschalter-view-transition-bericht-dunkel.svg')); }
  .p-telefon, :root[data-theme] .p-telefon { background-image: light-dark(url('bilder/theme-umschalter-view-transition-telefon-hell.svg'), url('bilder/theme-umschalter-view-transition-telefon-dunkel.svg')); }
}
.p-kacheln { list-style: none; padding: 0; display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 16px; margin-top: 56px; }
.p-kachel { border-radius: 12px; background: var(--p-flaeche); border: 1px solid var(--p-linie); overflow: hidden; display: grid; grid-template-rows: auto 1fr; }
.p-kachel .p-bild { aspect-ratio: 800 / 520; border-bottom: 1px solid var(--p-linie); }
.p-kachel div { padding: 16px 18px 18px; display: grid; gap: 6px; }
.p-kachel h2 { font-size: 16px; font-weight: 600; letter-spacing: -.01em; }
.p-kachel p { font-size: 14px; line-height: 1.55; color: var(--p-leise); }
.p-kachel code { font: 500 12px/1.5 'Geist Mono', ui-monospace, monospace; color: var(--p-text); }
.p-weiche { align-content: center; }
.p-weiche .p-status { display: inline-flex; align-items: center; gap: 8px; font: 500 11px/1.3 'Geist Mono', ui-monospace, monospace; letter-spacing: .1em; text-transform: uppercase; }
.p-weiche .p-status i { width: 8px; height: 8px; border-radius: 50%; background: var(--p-akzent); }
.produkt .fuss { margin-top: 48px; }
.produkt .steuerung { margin-top: 12px; }

/* ── Der Effekt ──────────────────────────────────────────────
   Kreis/Wipe: Standard-Überblendung aus, alter Schnappschuss bleibt stehen,
   der neue wird per clip-path aufgezogen (WAAPI, siehe Skript). */
:root[data-vt='kreis']::view-transition-old(root), :root[data-vt='kreis']::view-transition-new(root),
:root[data-vt='wipe']::view-transition-old(root), :root[data-vt='wipe']::view-transition-new(root) { animation: none; mix-blend-mode: normal; }
/* Blende: die eingebaute Überblendung, nur kürzer */
:root[data-vt='blende']::view-transition-old(root), :root[data-vt='blende']::view-transition-new(root) { animation-duration: 200ms; animation-timing-function: var(--vt-kurve); }
/* Der Vorführ-Zeiger liegt über allem und wird nicht mit beschnitten */
.zeiger { view-transition-name: zeiger; }
::view-transition-group(zeiger) { animation: none; }
::view-transition-old(zeiger) { display: none; }
::view-transition-new(zeiger) { animation: none; }
@media (prefers-reduced-motion: reduce) {
  ::view-transition-group(*), ::view-transition-old(*), ::view-transition-new(*) { animation: none !important; }
}

/* Vorführung: gezeichneter Zeiger */
.zeiger { position: fixed; left: 0; top: 0; z-index: 20; width: 20px; height: 20px; pointer-events: none; display: none; transition: transform .7s var(--ui); }
.zeiger svg { width: 20px; height: 20px; display: block; }
.zeiger i { position: absolute; left: -14px; top: -14px; width: 30px; height: 30px; border-radius: 50%; border: 1.5px solid #6fa89d; opacity: 0; }
.zeiger.klick i { animation: klick .5s var(--ui); }
@keyframes klick { from { opacity: 1; scale: .3; } to { opacity: 0; scale: 1.4; } }
.vorfuehrung .zeiger { display: block; }

/* Galerie-Vorschau: nur die Bühne */
.vorschau body { overflow: hidden; }
.vorschau .steuerung { display: none; }

/* Reduzierte Bewegung */
:root.reduziert * { transition-duration: .01ms !important; animation-duration: .01ms !important; }

@media (hover: none) {
  .sprache:hover, .thema:hover { background: none; }
  .thema { width: 44px; height: 44px; }
  .sprache { height: 44px; }
  .wahl span { padding: 9px 13px; }
}
@media (max-width: 980px) {
  .feld { padding: 8px 10px; }
  .buchen { padding: 0 16px; }
}
@media (max-width: 860px) {
  .p-nav { display: none; }
  .kopf { grid-template-columns: auto 1fr; }
  .kopf > nav { display: none; }
  .marke { justify-self: start; justify-items: start; }
  .marke b { font-size: 21px; letter-spacing: .2em; margin-right: 0; }
  .marke small { letter-spacing: .26em; margin-right: 0; }
}
@media (max-width: 640px) {
  .held { min-height: 520px; padding: 20px 16px 16px; }
  .buchung { grid-template-columns: 1fr 1fr; }
  .feld:nth-child(3) { grid-column: 1 / -1; box-shadow: 0 -1px 0 var(--linie); }
  .buchen { grid-column: 1 / -1; justify-content: center; }
  .hinweis { font-size: 11.5px; padding: 9px 12px 10px; }
  .haus { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .haus li:nth-child(3) { display: none; }
  .kachel figure { aspect-ratio: 1.35 / 1; }
  .kachel p { flex-direction: column; gap: 4px; }
  .karte-typo { padding: 0 8%; gap: 5px; }
  .karte-typo .gericht { font-size: 13px; }
  .karte-typo .gericht i { display: none; }
  .karte-typo .label { font-size: 8.5px; letter-spacing: .06em; }
  .karte-typo .gericht:nth-of-type(3) { display: none; }
  .wahl legend { display: none; }
  .p-nav, .p-rechts > a:first-child { display: none; }
  .p-kopf { gap: 12px; }
  .p-titel { font-size: 40px; }
  .p-unter { font-size: 15.5px; }
  .p-app { border-radius: 8px; }
  .p-kacheln { grid-template-columns: 1fr; margin-top: 32px; }
  .sprache { padding: 0 10px; }
  .adresse { font-size: 11.5px; }
}
/* Sehr schmal (320 px): Speisekarte in der Kachel umbrechend statt abgeschnitten */
@media (max-width: 360px) {
  .karte-typo { padding: 0 6%; }
  .karte-typo .label { white-space: normal; }
  .karte-typo .gericht { gap: 6px; font-size: 12.5px; }
  .karte-typo .gericht b { white-space: normal; }
}
/* Kontrastmodus: Knöpfe mit Rahmen */
@media (forced-colors: active) { button, [role=button], [role=tab], [role=switch] { border: 1px solid ButtonText !important; } }
</style>
</head>
<body>
<div class="seite">
  <header class="kopf">
    <nav aria-label="Hauptnavigation" data-t-aria="nav">
      <ul class="nav">
        <li><a href="#zimmer" aria-current="page" data-t="n1">Zimmer</a></li>
        <li><a href="#restaurant" data-t="n2">Restaurant</a></li>
        <li><a href="#anreise" data-t="n3">Anreise</a></li>
      </ul>
    </nav>
    <a class="marke" href="#start" aria-label="Hotel Seeweide, Lindau, Startseite">
      <b>Seeweide</b><small data-t="unter">Hotel · Lindau</small>
    </a>
    <div class="schalter">
      <a class="sprache" href="#en" hreflang="en" lang="en" data-sprache>English</a>
      <button class="thema" type="button" data-thema aria-label="Zur Nachtansicht wechseln">
        <svg class="mond" viewBox="0 0 24 24" aria-hidden="true"><path d="M20 14.6A8.2 8.2 0 0 1 9.4 4a8.2 8.2 0 1 0 10.6 10.6Z" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round"/></svg>
        <svg class="sonne" viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="4.2" fill="none" stroke="currentColor" stroke-width="1.6"/><path d="M12 2.5v2.2M12 19.3v2.2M2.5 12h2.2M19.3 12h2.2M5.3 5.3l1.55 1.55M17.15 17.15l1.55 1.55M5.3 18.7l1.55-1.55M17.15 6.85l1.55-1.55" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg>
      </button>
    </div>
  </header>

  <section class="held" id="start">
    <!-- Eigene Illustration: Bodensee mit Alpen, Hotel am Ufer, Steg mit Badehaus. Farben nur über Tokens. -->
    <svg class="szene" viewBox="0 0 1600 600" preserveAspectRatio="xMidYMax slice" role="img" aria-label="Illustration: das Hotel am Seeufer, davor ein Steg mit gestreiftem Badehaus, dahinter die Alpen" data-t-aria-szene="szene">
      <rect width="1600" height="600" fill="var(--s-himmel)"/>
      <!-- Sterne und Mond (Nacht), Sonne im Dunst (Tag) -->
      <g class="nur-nacht" fill="var(--s-sonne)">
        <circle cx="1060" cy="70" r="1.6"/><circle cx="1180" cy="120" r="1.2"/><circle cx="1420" cy="60" r="1.8"/><circle cx="1500" cy="150" r="1.2"/>
        <circle cx="980" cy="160" r="1.1"/><circle cx="1340" cy="210" r="1.3"/><circle cx="1550" cy="90" r="1"/><circle cx="1240" cy="40" r="1.2"/>
        <circle cx="900" cy="90" r="1"/><circle cx="1110" cy="230" r="1"/>
        <circle cx="1310" cy="140" r="26"/><circle cx="1321" cy="132" r="22" fill="var(--s-himmel)"/>
      </g>
      <circle class="nur-tag" cx="1310" cy="190" r="40" fill="var(--s-sonne)"/>
      <!-- Alpen: fern und nah -->
      <path d="M0 360 L90 330 L170 345 L260 300 L330 322 L420 286 L500 318 L590 296 L690 330 L780 300 L880 316 L960 280 L1050 312 L1140 290 L1230 322 L1320 284 L1420 310 L1510 292 L1600 316 V400 H0Z" fill="var(--s-berg-fern)"/>
      <path d="M0 384 L120 352 L210 366 L320 330 L400 352 L470 340 L560 362 L660 346 L760 368 L860 340 L940 356 L1040 332 L1120 352 L1210 340 L1300 360 L1400 326 L1480 348 L1600 338 V400 H0Z" fill="var(--s-berg)"/>
      <path d="M960 280 L985 292 L975 296 L960 290 L948 297 L940 291Z M420 286 L442 297 L430 300 L420 296 L408 301 L402 296Z M1320 284 L1344 297 L1332 299 L1320 294 L1308 300 L1302 295Z" fill="var(--s-schnee)"/>
      <!-- See -->
      <rect y="392" width="1600" height="208" fill="var(--s-see)"/>
      <rect y="390" width="1600" height="5" fill="var(--s-ufer)"/>
      <g stroke="var(--s-welle)" stroke-width="2" stroke-linecap="round">
        <path d="M60 430h120M260 452h70M1240 428h150M1460 446h90M120 500h90M1300 520h120M700 560h160M980 470h60M420 530h60M1500 560h60"/>
      </g>
      <!-- Leuchtturm auf der Hafenmole -->
      <g transform="translate(1330 0)">
        <rect x="90" y="384" width="190" height="10" fill="var(--s-ufer)"/>
        <path d="M168 384 L174 312 L190 312 L196 384Z" fill="var(--s-fassade)"/>
        <rect x="170" y="300" width="24" height="14" fill="var(--s-dach)"/>
        <path d="M168 300 L182 286 L196 300Z" fill="var(--s-dach)"/>
        <rect x="176" y="302" width="12" height="8" fill="var(--s-fenster)"/>
        <circle class="nur-nacht" cx="182" cy="306" r="22" fill="var(--s-fenster)" fill-opacity=".18"/>
        <path class="nur-nacht" d="M168 306 L-60 282 L-60 330Z" fill="var(--s-fenster)" fill-opacity=".08"/>
      </g>
      <!-- Garten hinter dem Hotel -->
      <g fill="var(--s-baum)">
        <circle cx="742" cy="350" r="34"/><circle cx="700" cy="366" r="26"/><circle cx="1120" cy="344" r="38"/><circle cx="1162" cy="364" r="28"/>
        <rect x="672" y="364" width="520" height="34"/>
      </g>
      <g fill="var(--s-baum-hell)"><circle cx="1088" cy="362" r="24"/><circle cx="772" cy="370" r="22"/></g>
      <!-- Hotel: Villa mit Mittelgiebel und zwei Flügeln -->
      <g>
        <rect x="780" y="300" width="300" height="100" fill="var(--s-fassade)"/>
        <path d="M770 302 L800 270 L1060 270 L1090 302Z" fill="var(--s-dach)"/>
        <rect x="880" y="256" width="100" height="46" fill="var(--s-fassade)"/>
        <path d="M870 258 L930 214 L990 258Z" fill="var(--s-dach)"/>
        <circle cx="930" cy="240" r="8" fill="var(--s-fenster)"/>
        <rect x="826" y="262" width="6" height="12" fill="var(--s-dach)"/><rect x="1028" y="262" width="6" height="12" fill="var(--s-dach)"/>
        <g fill="var(--s-fenster)">
          <rect x="800" y="314" width="16" height="24"/><rect x="832" y="314" width="16" height="24"/><rect x="864" y="314" width="16" height="24"/>
          <rect x="898" y="314" width="16" height="24"/><rect x="946" y="314" width="16" height="24"/>
          <rect x="980" y="314" width="16" height="24"/><rect x="1044" y="314" width="16" height="24"/>
          <rect x="800" y="352" width="16" height="24"/><rect x="864" y="352" width="16" height="24"/>
          <rect x="980" y="352" width="16" height="24"/><rect x="1012" y="352" width="16" height="24"/><rect x="1044" y="352" width="16" height="24"/>
          <rect x="898" y="268" width="14" height="22"/><rect x="948" y="268" width="14" height="22"/>
          <rect x="912" y="352" width="36" height="48"/>
        </g>
        <g fill="var(--s-fenster-aus)">
          <rect x="1012" y="314" width="16" height="24"/><rect x="832" y="352" width="16" height="24"/><rect x="922" y="268" width="14" height="22"/>
        </g>
        <rect x="770" y="342" width="320" height="4" fill="var(--s-dach)"/>
      </g>
      <!-- Uferterrasse mit Sonnenschirmen -->
      <rect x="640" y="396" width="600" height="10" fill="var(--s-ufer)"/>
      <g fill="var(--s-streifen)">
        <path d="M676 382 Q700 368 724 382Z"/><path d="M1146 382 Q1170 368 1194 382Z"/><path d="M1206 384 Q1226 372 1246 384Z"/>
      </g>
      <g stroke="var(--s-dach)" stroke-width="2"><path d="M700 382v14M1170 382v14M1226 384v12"/></g>
      <!-- Spiegelung des Hotels -->
      <g class="spiegel" transform="translate(0 806) scale(1 -1)">
        <rect x="780" y="300" width="300" height="100" fill="var(--s-fassade)" opacity=".5"/>
        <g fill="var(--s-fenster)"><rect x="800" y="314" width="16" height="24"/><rect x="864" y="314" width="16" height="24"/><rect x="946" y="314" width="16" height="24"/><rect x="1044" y="314" width="16" height="24"/><rect x="912" y="352" width="36" height="48"/><rect x="980" y="352" width="16" height="24"/></g>
      </g>
      <g class="nur-nacht" stroke="var(--s-fenster)" stroke-width="3" stroke-linecap="round" stroke-opacity=".55" stroke-dasharray="10 9">
        <path d="M808 420v80M872 420v110M930 418v150M954 420v90M988 420v120M1052 420v96"/>
      </g>
      <!-- Mondspur -->
      <g class="nur-nacht" stroke="var(--s-sonne)" stroke-width="2" stroke-linecap="round" stroke-opacity=".5">
        <path d="M1290 420h40M1280 446h60M1296 474h30M1270 506h80M1290 540h44"/>
      </g>
      <!-- Segelboot -->
      <g transform="translate(-230 90)">
        <path d="M1376 446 L1470 446 L1458 460 L1388 460Z" fill="var(--s-dach)"/>
        <path d="M1420 444 L1420 352 L1462 440Z" fill="var(--s-fassade)"/>
        <path d="M1416 444 L1416 368 L1384 440Z" fill="var(--s-fassade)" opacity=".8"/>
        <path class="spiegel" d="M1388 464 L1458 464 L1420 520Z" fill="var(--s-fassade)" fill-opacity=".5"/>
        <circle class="nur-nacht" cx="1420" cy="352" r="4" fill="var(--s-fenster)"/>
      </g>
      <!-- Steg mit Badehaus -->
      <g>
        <path d="M1290 600 L1370 492 L1600 492 L1600 506 L1392 506 L1330 600Z" fill="var(--s-holz)"/>
        <g stroke="var(--s-holz)" stroke-width="5"><path d="M1420 506v36M1490 506v32M1560 506v30M1330 560v40"/></g>
        <rect x="1452" y="436" width="96" height="56" fill="var(--s-fassade)"/>
        <rect x="1488" y="456" width="24" height="36" fill="var(--s-holz)"/>
        <path d="M1442 438 L1500 412 L1558 438Z" fill="var(--s-dach)"/>
        <g fill="var(--s-streifen)"><rect x="1452" y="436" width="12" height="12"/><rect x="1476" y="436" width="12" height="12"/><rect x="1500" y="436" width="12" height="12"/><rect x="1524" y="436" width="12" height="12"/></g>
        <path d="M1392 492 V452" stroke="var(--s-dach)" stroke-width="3"/>
        <circle cx="1392" cy="450" r="5" fill="var(--s-fenster)"/>
        <circle class="nur-nacht" cx="1392" cy="450" r="20" fill="var(--s-fenster)" fill-opacity=".2"/>
        <path class="spiegel" d="M1452 508h96v30h-96Z" fill="var(--s-fassade)" fill-opacity=".35"/>
      </g>
    </svg>

    <div class="held-text">
      <p class="label" data-t="ort">Hotel am See · Lindau, Bodensee · 24 Zimmer</p>
      <h1 class="titel" data-t="titel">Morgens Nebel über dem See, <em>abends</em> Licht im Hafen.</h1>
    </div>

    <div class="buchung-box">
      <form class="buchung" action="#" aria-label="Verfügbarkeit prüfen" data-t-aria-buchung="buchung">
        <button class="feld" type="button"><span class="label" data-t="b1">Anreise</span><b data-t="b1w">Fr, 13. Nov.</b></button>
        <button class="feld" type="button"><span class="label" data-t="b2">Abreise</span><b data-t="b2w">So, 15. Nov.</b></button>
        <button class="feld" type="button"><span class="label" data-t="b3">Gäste</span><b data-t="b3w">2 Erwachsene</b></button>
        <button class="buchen" type="submit"><span data-t="buchen">Verfügbarkeit prüfen</span><span aria-hidden="true">→</span></button>
      </form>
      <p class="hinweis" data-t="hinweis">Direkt gebucht: <b>ab 168 € pro Nacht</b>, Frühstück am See inklusive. Am Wochenende mindestens zwei Nächte.</p>
    </div>
  </section>

  <section id="zimmer" aria-label="Das Haus" data-t-aria-haus="haus">
    <ul class="haus">
      <li class="kachel">
        <figure>
          <!-- Grundriss Seezimmer -->
          <svg viewBox="96 12 270 148" preserveAspectRatio="xMidYMid meet" role="img" aria-label="Grundriss eines Seezimmers mit Balkon" data-t-aria-k="g1">
            <g class="linie-svg" stroke-width="2.2">
              <path d="M118 26 H282 V118 H118 Z"/>
              <path d="M118 118 V134 H282 V118" stroke-dasharray="4 4" stroke-width="1.4"/>
              <path d="M200 26 V60 M200 74 V80 H240"/>
              <path d="M240 80 V118" stroke-width="1"/>
            </g>
            <g class="linie-svg" stroke-width="1.3">
              <rect x="134" y="46" width="44" height="54" rx="2"/><path d="M134 56 H178"/>
              <rect x="252" y="40" width="20" height="22"/><circle cx="262" cy="94" r="7"/>
              <path d="M150 118 A26 26 0 0 1 176 92" stroke-dasharray="3 3"/>
            </g>
            <g fill="var(--leise)" font-family="Geist Mono, monospace" font-size="9" letter-spacing="1.2">
              <text x="200" y="152" text-anchor="middle" data-t-svg="g1t">BALKON · BLICK NACH SÜDEN</text>
              <text x="300" y="76">28 M²</text>
            </g>
            <path d="M330 120 l8 -20 l8 20 l-8 -6Z" fill="var(--akzent)"/>
            <text x="338" y="134" text-anchor="middle" fill="var(--akzent)" font-family="Mona Sans, sans-serif" font-size="9" font-weight="600" letter-spacing="1.4" data-t-svg="g1s">SEE</text>
          </svg>
        </figure>
        <p><b><span data-t="k1">Seezimmer mit Balkon</span><small data-t="k1u">28 m² · Doppelbett 180 · Südblick</small></b><span class="label" data-t="k1p">ab 168 €</span></p>
      </li>
      <li class="kachel" id="restaurant">
        <figure>
          <div class="karte-typo">
            <span class="label" data-t="m0">Heute Abend im Restaurant</span>
            <div class="gericht"><span>29</span><b data-t="m1">Felchen aus dem See</b><i data-t="m1i">Beurre blanc</i></div>
            <div class="gericht"><span>24</span><b data-t="m2">Kalbsbäckle</b><i data-t="m2i">Wurzelgemüse</i></div>
            <div class="gericht"><span>11</span><b data-t="m3">Apfelküchle</b><i data-t="m3i">Zimt</i></div>
          </div>
        </figure>
        <p><b><span data-t="k2">Restaurant am Ufer</span><small data-t="k2u">Bodensee-Küche, Terrasse bis 22 Uhr</small></b><span class="label" data-t="k2p">Di bis Sa</span></p>
      </li>
      <li class="kachel" id="anreise">
        <figure>
          <!-- Anreise: Inselumriss mit Fußweg vom Bahnhof -->
          <svg viewBox="28 20 300 124" preserveAspectRatio="xMidYMid meet" role="img" aria-label="Skizze: sechs Minuten zu Fuß vom Bahnhof zum Hotel" data-t-aria-k="g3">
            <path d="M96 92 C92 54 150 30 214 34 C280 38 318 62 312 96 C306 128 248 140 190 136 C130 132 100 120 96 92Z" fill="var(--grund)"/>
            <path d="M40 72 H100" stroke="var(--leise)" stroke-width="1.4" stroke-dasharray="2 4"/>
            <path d="M40 66 H100 M40 78 H100" stroke="var(--linie)" stroke-width="1"/>
            <path d="M128 76 C160 74 172 98 206 100 C236 102 250 82 276 84" fill="none" stroke="var(--akzent)" stroke-width="2.2" stroke-dasharray="1 6" stroke-linecap="round"/>
            <circle cx="128" cy="76" r="5" fill="var(--text)"/>
            <circle cx="276" cy="84" r="7" fill="var(--akzent)"/><circle cx="276" cy="84" r="13" fill="none" stroke="var(--akzent)" stroke-width="1"/>
            <g font-family="Geist Mono, monospace" font-size="9" letter-spacing="1.2" fill="var(--leise)">
              <text x="128" y="62" text-anchor="middle" data-t-svg="g3a">BAHNHOF</text>
              <text x="276" y="112" text-anchor="middle" fill="var(--text)">SEEWEIDE</text>
              <text x="206" y="124" text-anchor="middle" data-t-svg="g3b">6 MIN. ZU FUSS</text>
              <text x="40" y="58" data-t-svg="g3c">DAMM</text>
            </g>
          </svg>
        </figure>
        <p><b><span data-t="k3">Anreise mit der Bahn</span><small data-t="k3u">Lindau-Insel, Gepäck holen wir ab</small></b><span class="label" data-t="k3p">6 Min.</span></p>
      </li>
    </ul>
  </section>

  <footer class="fuss">
    <fieldset class="wahl label" data-wahl>
      <legend data-t="darstellung">Ansicht</legend>
      <div class="wahl-spur">
        <label><input type="radio" name="darstellung" value="system"><span>System</span></label>
        <label><input type="radio" name="darstellung" value="hell"><span data-t="hell">Tag</span></label>
        <label><input type="radio" name="darstellung" value="dunkel"><span data-t="dunkel">Nacht</span></label>
      </div>
    </fieldset>
    <p class="adresse" data-t="adresse">Uferweg 12 · 88131 Lindau · Rezeption 7 bis 23 Uhr</p>
  </footer>

  <div class="steuerung mono" lang="de">
    <div class="modi" role="group" aria-label="Variante">
      <span>Variante</span>
      <button type="button" data-welt-wahl="tokens" aria-pressed="true">Tokens</button>
      <button type="button" data-welt-wahl="light-dark" aria-pressed="false">light-dark() + Bilder</button>
    </div>
    <div class="modi" role="group" aria-label="Übergang">
      <span>Übergang</span>
      <button type="button" data-variante="kreis" aria-pressed="true">Kreis</button>
      <button type="button" data-variante="wipe" aria-pressed="false">Wipe</button>
      <button type="button" data-variante="blende" aria-pressed="false">Blende</button>
    </div>
    <p class="werte" data-werte aria-live="polite"></p>
  </div>
</div>

<!-- Variante light-dark(): Produktseite, Farben und Bilder je Farbschema aus light-dark() -->
<div class="seite produkt" data-welt="produkt" lang="de" hidden>
  <header class="p-kopf">
    <a class="p-marke" href="#start"><svg viewBox="0 0 24 24" aria-hidden="true"><rect width="24" height="24" rx="6" fill="currentColor"/><path d="M7 16.5 12 7l5 9.5" fill="none" stroke="var(--p-grund)" stroke-width="2.2" stroke-linejoin="round"/></svg>Werkbuch</a>
    <nav aria-label="Produktnavigation"><ul class="p-nav"><li><a href="#start">Funktionen</a></li><li><a href="#start">Preise</a></li><li><a href="#start">Kunden</a></li><li><a href="#start">Changelog</a></li></ul></nav>
    <div class="p-rechts">
      <a href="#start">Anmelden</a>
      <button class="thema" type="button" data-thema aria-label="Zu dunklem Design wechseln">
        <svg class="mond" viewBox="0 0 24 24" aria-hidden="true"><path d="M20 14.6A8.2 8.2 0 0 1 9.4 4a8.2 8.2 0 1 0 10.6 10.6Z" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round"/></svg>
        <svg class="sonne" viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="4.2" fill="none" stroke="currentColor" stroke-width="1.6"/><path d="M12 2.5v2.2M12 19.3v2.2M2.5 12h2.2M19.3 12h2.2M5.3 5.3l1.55 1.55M17.15 17.15l1.55 1.55M5.3 18.7l1.55-1.55M17.15 6.85l1.55-1.55" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg>
      </button>
      <a class="p-knopf" href="#start">Kostenlos testen</a>
    </div>
  </header>

  <section class="p-held" aria-labelledby="p-titel">
    <p class="p-label mono"><b>Neu</b> Wochenbericht als PDF</p>
    <h1 class="p-titel" id="p-titel">Projektzeiten, die sich selbst erfassen.</h1>
    <p class="p-unter">Werkbuch ordnet Stunden aus Kalender und Telefon den Projekten zu. Für Planungsbüros mit 5 bis 80 Leuten, Daten in Frankfurt.</p>
    <p class="p-ctas"><a class="p-knopf" href="#start">Kostenlos testen</a><a class="p-knopf zweit" href="#start">Demo in 20 Minuten</a></p>
  </section>

  <div class="p-bild p-app" role="img" aria-label="Werkbuch-Ansicht: Wochenplan mit Projektzeiten je Person, drei Kennzahlen und Projektanteilen als Ring"></div>

  <ul class="p-kacheln">
    <li class="p-kachel"><div class="p-bild p-bericht" role="img" aria-label="Wochenbericht als Seite mit Balkendiagramm und Tabelle"></div><div><h2>Bericht am Freitag</h2><p>Stunden je Projekt und Leistungsphase, als PDF für die Bauherrschaft.</p></div></li>
    <li class="p-kachel"><div class="p-bild p-telefon" role="img" aria-label="Telefon mit laufender Zeiterfassung als Ring und drei Projekten"></div><div><h2>Auf der Baustelle</h2><p>Ein Tipp startet die Uhr, der Standort schlägt das Projekt vor.</p></div></li>
    <li class="p-kachel p-weiche"><div><p class="p-status"><i></i><span data-weiche>Weiche wird geprüft</span></p><h2>Ein Bild je Farbschema</h2><p><code>background-image: light-dark(url(…-hell.svg), url(…-dunkel.svg))</code></p><p data-weiche-text></p></div></li>
  </ul>

  <footer class="fuss">
    <fieldset class="wahl label" data-wahl>
      <legend>Darstellung</legend>
      <div class="wahl-spur">
        <label><input type="radio" name="darstellung-p" value="system"><span>System</span></label>
        <label><input type="radio" name="darstellung-p" value="hell"><span>Hell</span></label>
        <label><input type="radio" name="darstellung-p" value="dunkel"><span>Dunkel</span></label>
      </div>
    </fieldset>
    <p class="adresse">Werkbuch Software · Hanauer Landstraße 160 · Frankfurt am Main</p>
  </footer>
</div>
<div class="zeiger" data-zeiger aria-hidden="true">
  <svg viewBox="0 0 20 20"><path d="M2.5 1.8 17 10.2l-6.4 1.4-3.2 6Z" fill="#1c2a30" stroke="#fbf6ee" stroke-width="1.3" stroke-linejoin="round"/></svg><i></i>
</div>

<script>
(() => {
  const html = document.documentElement;
  const reduziert = () => html.classList.contains('reduziert');
  const $ = s => document.querySelector(s);
  const themaBtns = [...document.querySelectorAll('[data-thema]')], spracheLink = $('[data-sprache]'), werte = $('[data-werte]'), zeiger = $('[data-zeiger]');
  const themaBtn = themaBtns[0], themaBtnP = themaBtns[1];
  const radios = [...document.querySelectorAll('[name^="darstellung"]')];
  const welten = { tokens: $('.seite:not(.produkt)'), 'light-dark': $('.produkt') };
  const steuerung = $('.steuerung'), weltBtns = [...document.querySelectorAll('[data-welt-wahl]')];
  // Kann light-dark() hier Bilder? (Chrome 150+, Firefox; sonst greift der Selektor am data-theme)
  const BILDER_NATIV = !!(window.CSS && CSS.supports('background-image', 'light-dark(url("data:,a"), url("data:,b"))'));
  const varBtns = [...document.querySelectorAll('[data-variante]')];

  const DAUER = 420, BLENDE = 200, KURVE = 'cubic-bezier(0, 0, 0.58, 1)', SPEICHER = 'seeweide-darstellung';
  let variante = 'kreis';
  // Variante aus der URL (?v=tokens|light-dark), unbekannt oder leer: erste Variante
  const ausUrl = new URLSearchParams(location.search).get('v');
  let welt = ausUrl in welten ? ausUrl : 'tokens';

  const TEXTE = {
    de: { n1: 'Zimmer', n2: 'Restaurant', n3: 'Anreise', unter: 'Hotel · Lindau', ort: 'Hotel am See · Lindau, Bodensee · 24 Zimmer',
      titel: 'Morgens Nebel über dem See, <em>abends</em> Licht im Hafen.',
      b1: 'Anreise', b1w: 'Fr, 13. Nov.', b2: 'Abreise', b2w: 'So, 15. Nov.', b3: 'Gäste', b3w: '2 Erwachsene', buchen: 'Verfügbarkeit prüfen',
      hinweis: 'Direkt gebucht: <b>ab 168 € pro Nacht</b>, Frühstück am See inklusive. Am Wochenende mindestens zwei Nächte.',
      k1: 'Seezimmer mit Balkon', k1u: '28 m² · Doppelbett 180 · Südblick', k1p: 'ab 168 €',
      m0: 'Heute Abend im Restaurant', m1: 'Felchen aus dem See', m1i: 'Beurre blanc', m2: 'Kalbsbäckle', m2i: 'Wurzelgemüse', m3: 'Apfelküchle', m3i: 'Zimt',
      k2: 'Restaurant am Ufer', k2u: 'Bodensee-Küche, Terrasse bis 22 Uhr', k2p: 'Di bis Sa',
      k3: 'Anreise mit der Bahn', k3u: 'Lindau-Insel, Gepäck holen wir ab', k3p: '6 Min.',
      g1t: 'BALKON · BLICK NACH SÜDEN', g1s: 'SEE', g3a: 'BAHNHOF', g3b: '6 MIN. ZU FUSS', g3c: 'DAMM',
      darstellung: 'Ansicht', hell: 'Tag', dunkel: 'Nacht', adresse: 'Uferweg 12 · 88131 Lindau · Rezeption 7 bis 23 Uhr',
      nav: 'Hauptnavigation', buchung: 'Verfügbarkeit prüfen', haus: 'Das Haus',
      szene: 'Illustration: das Hotel am Seeufer, davor ein Steg mit gestreiftem Badehaus, dahinter die Alpen',
      g1: 'Grundriss eines Seezimmers mit Balkon', g3: 'Skizze: sechs Minuten zu Fuß vom Bahnhof zum Hotel',
      zuDunkel: 'Zur Nachtansicht wechseln', zuHell: 'Zur Tagansicht wechseln', start: 'Hotel Seeweide, Lindau, Startseite' },
    en: { n1: 'Rooms', n2: 'Restaurant', n3: 'Getting here', unter: 'Hotel · Lindau', ort: 'Lakeside hotel · Lindau, Lake Constance · 24 rooms',
      titel: 'Mist on the lake at dawn, <em>harbour</em> lights at night.',
      b1: 'Check-in', b1w: 'Fri, 13 Nov', b2: 'Check-out', b2w: 'Sun, 15 Nov', b3: 'Guests', b3w: '2 adults', buchen: 'Check availability',
      hinweis: 'Book direct: <b>from €168 per night</b>, breakfast by the lake included. Two nights minimum at weekends.',
      k1: 'Lake room with balcony', k1u: '28 m² · king bed · south view', k1p: 'from €168',
      m0: 'Tonight in the restaurant', m1: 'Whitefish from the lake', m1i: 'beurre blanc', m2: 'Braised veal cheek', m2i: 'root vegetables', m3: 'Apple fritters', m3i: 'cinnamon',
      k2: 'Restaurant by the shore', k2u: 'Lake Constance cooking, terrace until 10 pm', k2p: 'Tue to Sat',
      k3: 'Arriving by train', k3u: 'Lindau-Insel, we collect your luggage', k3p: '6 min',
      g1t: 'BALCONY · FACING SOUTH', g1s: 'LAKE', g3a: 'STATION', g3b: '6 MIN ON FOOT', g3c: 'CAUSEWAY',
      darstellung: 'View', hell: 'Day', dunkel: 'Night', adresse: 'Uferweg 12 · 88131 Lindau · Reception 7 am to 11 pm',
      nav: 'Main navigation', buchung: 'Check availability', haus: 'The house',
      szene: 'Illustration: the hotel on the lakeshore, a jetty with a striped bathing hut in front, the Alps behind',
      g1: 'Floor plan of a lake room with balcony', g3: 'Sketch: six minutes on foot from the station to the hotel',
      zuDunkel: 'Switch to night view', zuHell: 'Switch to day view', start: 'Hotel Seeweide, Lindau, home' }
  };

  /* ── Zustände (die eigentliche Änderung, ohne Animation) ── */
  const systemDunkel = matchMedia('(prefers-color-scheme: dark)');
  function setzeWahl(w, speichern) {
    html.dataset.wahl = w;
    html.dataset.theme = w === 'dunkel' || (w === 'system' && systemDunkel.matches) ? 'dunkel' : 'hell';
    if (speichern) { try { localStorage.setItem(SPEICHER, w); } catch (e) {} }
    beschriften();
  }
  function setzeSprache(l) {
    const t = TEXTE[l];
    html.lang = l;
    document.querySelectorAll('[data-t]').forEach(el => { el.innerHTML = t[el.dataset.t]; });
    document.querySelectorAll('[data-t-svg]').forEach(el => { el.textContent = t[el.dataset.tSvg]; });
    document.querySelectorAll('[data-t-aria-k]').forEach(el => { el.setAttribute('aria-label', t[el.dataset.tAriaK]); });
    $('[data-t-aria]').setAttribute('aria-label', t.nav);
    $('[data-t-aria-szene]').setAttribute('aria-label', t.szene);
    $('[data-t-aria-buchung]').setAttribute('aria-label', t.buchung);
    $('[data-t-aria-haus]').setAttribute('aria-label', t.haus);
    $('.marke').setAttribute('aria-label', t.start);
    // Link zeigt immer die andere Sprache, in ihrer eigenen Sprache
    const ziel = l === 'de' ? 'en' : 'de';
    spracheLink.textContent = ziel === 'en' ? 'English' : 'Deutsch';
    spracheLink.lang = spracheLink.hreflang = ziel;
    spracheLink.href = '#' + ziel;
    beschriften();
  }
  function beschriften() {
    const t = TEXTE[html.lang] || TEXTE.de;
    themaBtn.setAttribute('aria-label', html.dataset.theme === 'dunkel' ? t.zuHell : t.zuDunkel);
    themaBtnP.setAttribute('aria-label', html.dataset.theme === 'dunkel' ? 'Zu hellem Design wechseln' : 'Zu dunklem Design wechseln');
    radios.forEach(r => { r.checked = r.value === html.dataset.wahl; });
  }

  /* ── Der Übergang ── */
  let zeigerPos = null;   // letzte echte Zeigerposition (für Radios, die per Label ausgelöst werden)
  addEventListener('pointerdown', e => { zeigerPos = { x: e.clientX, y: e.clientY, t: performance.now() }; }, true);

  function ursprung(e, el) {
    // Maus/Touch: Klickpunkt. Tastatur (detail 0, clientX 0): Mitte des Bedienelements.
    if (e && e.detail !== 0 && (e.clientX || e.clientY)) return { x: e.clientX, y: e.clientY };
    if (zeigerPos && performance.now() - zeigerPos.t < 800 && e && e.type === 'change') return zeigerPos;
    const r = el.getBoundingClientRect();
    return { x: r.left + r.width / 2, y: r.top + r.height / 2 };
  }

  function wechsel(e, el, aenderung, was) {
    const { x, y } = ursprung(e, el);
    const radius = Math.hypot(Math.max(x, innerWidth - x), Math.max(y, innerHeight - y));
    if (reduziert() || typeof document.startViewTransition !== 'function') {
      aenderung();
      zeigeWerte(was, reduziert() ? 'reduziert' : 'ohne');
      return;
    }
    html.dataset.vt = variante;
    const vt = document.startViewTransition(aenderung);
    vt.ready.then(() => {
      if (variante === 'blende') return;
      const clipPath = variante === 'kreis'
        ? [`circle(0px at ${x}px ${y}px)`, `circle(${radius}px at ${x}px ${y}px)`]
        : ['inset(0 0 100% 0)', 'inset(0 0 0 0)'];
      html.animate({ clipPath }, { duration: DAUER, easing: KURVE, pseudoElement: '::view-transition-new(root)' });
    }).catch(() => {});
    const art = variante;
    vt.updateCallbackDone.then(() => zeigeWerte(was, art, x, y, radius)).catch(() => {});
    vt.finished.catch(() => {}).then(() => { if (html.dataset.vt === art) delete html.dataset.vt; });
  }

  function zeigeWerte(was, art, x, y, r) {
    const ziel = (was === 'sprache' ? `Sprache → <b>${html.lang.toUpperCase()}</b>` : `Theme → <b>${html.dataset.theme === 'dunkel' ? (welt === 'tokens' ? 'Nacht' : 'Dunkel') : (welt === 'tokens' ? 'Tag' : 'Hell')}</b>`)
      + (welt === 'light-dark' ? `<span>color-scheme <b>${getComputedStyle(html).colorScheme}</b></span><span>Bilder <b>${BILDER_NATIV ? 'light-dark(url(), url())' : 'Weiche: :root[data-theme] …'}</b></span>` : '');
    const zeilen = {
      kreis: `<span>${ziel}</span><span>clip-path <b>circle(0 → ${Math.round(r)} px at ${Math.round(x)} ${Math.round(y)})</b></span><span><b>${DAUER} ms</b></span><span><b>${KURVE}</b></span>`,
      wipe: `<span>${ziel}</span><span>clip-path <b>inset(0 0 100% 0) → inset(0)</b></span><span><b>${DAUER} ms</b></span><span><b>${KURVE}</b></span>`,
      blende: `<span>${ziel}</span><span>Überblendung <b>${BLENDE} ms</b></span><span><b>${KURVE}</b></span>`,
      reduziert: `<span>${ziel}</span><span>Reduzierte Bewegung: <b>Sofortwechsel</b>, keine View Transition</span>`,
      ohne: `<span>${ziel}</span><span>Kein startViewTransition: <b>direkter Wechsel</b></span>`
    };
    werte.innerHTML = zeilen[art];
  }
  function werteStart() {
    if (welt === 'light-dark') {
      werte.innerHTML = `<span>Umschalten <b>nur color-scheme</b> am html</span><span>Farben <b>light-dark(#f6f7f9, #08090b)</b> …</span><span>Bilder <b>${BILDER_NATIV ? 'light-dark(url(), url()) nativ' : 'Weiche aktiv: Selektor :root[data-theme]'}</b></span><span>${reduziert() ? '<b>Sofortwechsel</b>' : `Übergang <b>${variante}</b> · ${variante === 'blende' ? BLENDE : DAUER} ms · <b>${KURVE}</b>`}</span>`;
      return;
    }
    werte.innerHTML = reduziert()
      ? '<span>Reduzierte Bewegung: <b>Sofortwechsel</b></span>'
      : variante === 'blende'
        ? `<span>startViewTransition · Überblendung <b>${BLENDE} ms</b> · <b>${KURVE}</b></span>`
        : `<span>startViewTransition · clip-path <b>${variante === 'kreis' ? 'circle()' : 'inset()'}</b> · <b>${DAUER} ms</b> · <b>${KURVE}</b></span>`;
  }

  /* ── Bedienung ── */
  const themaUm = () => setzeWahl(html.dataset.theme === 'dunkel' ? 'hell' : 'dunkel', true);
  const spracheUm = () => setzeSprache(html.lang === 'de' ? 'en' : 'de');
  themaBtns.forEach(b => b.addEventListener('click', e => { stoppVorfuehrung(); wechsel(e, b, themaUm, 'thema'); }));
  spracheLink.addEventListener('click', e => { e.preventDefault(); stoppVorfuehrung(); wechsel(e, spracheLink, spracheUm, 'sprache'); });
  radios.forEach(r => r.addEventListener('change', e => {
    stoppVorfuehrung();
    const w = r.value; beschriften();   // alter Zustand für den Schnappschuss
    wechsel(e, r.parentElement, () => setzeWahl(w, true), 'thema');
  }));
  // Systemwechsel bei Wahl „System“ (ohne Effekt) und Wahl aus anderen Tabs
  systemDunkel.addEventListener('change', () => { if (html.dataset.wahl === 'system') setzeWahl('system', false); });
  addEventListener('storage', e => { if (e.key === SPEICHER) setzeWahl(e.newValue || 'system', false); });
  document.querySelectorAll('.nav a, .marke, .produkt a').forEach(a => a.addEventListener('click', e => e.preventDefault()));
  $('.buchung').addEventListener('submit', e => e.preventDefault());
  varBtns.forEach(b => b.addEventListener('click', () => {
    variante = b.dataset.variante;
    varBtns.forEach(o => o.setAttribute('aria-pressed', String(o === b)));
    werteStart();
  }));

  /* ── Varianten: Tokens (Hotel) oder light-dark() mit Bildern (Produktseite) ── */
  const BILDER = ['app', 'bericht', 'telefon'].flatMap(n => ['hell', 'dunkel'].map(t => `bilder/theme-umschalter-view-transition-${n}-${t}.svg`));
  let vorgeladen = false;
  function setzeWelt(w) {
    welt = w;
    Object.entries(welten).forEach(([k, el]) => { el.hidden = k !== w; });
    if (w === 'light-dark') html.dataset.welt = 'produkt'; else delete html.dataset.welt;
    welten[w].appendChild(steuerung);   // die Steuerung wandert mit
    weltBtns.forEach(b => b.setAttribute('aria-pressed', String(b.dataset.weltWahl === w)));
    // Beide Bildsätze vorladen, damit der Schnappschuss der View Transition nie ein leeres Bild zeigt
    if (w === 'light-dark' && !vorgeladen) { vorgeladen = true; BILDER.forEach(src => { const i = new Image(); i.src = src; }); }
    $('[data-weiche]').textContent = BILDER_NATIV ? 'Nativ: light-dark() mit Bildern' : 'Weiche aktiv';
    $('[data-weiche-text]').textContent = BILDER_NATIV
      ? 'Dieser Browser wählt das Bild selbst nach color-scheme, auch bei „System“ ohne Skript.'
      : 'Dieser Browser kennt light-dark() nur für Farben. Ein Selektor am data-theme tauscht das Bild, sichtbar gleich.';
    beschriften(); werteStart();
  }
  weltBtns.forEach(b => b.addEventListener('click', () => {
    stoppVorfuehrung();
    setzeWelt(b.dataset.weltWahl);
    const u = new URL(location.href);
    u.searchParams.set('v', welt);   // andere Parameter (reduziert, vorschau) bleiben stehen
    history.replaceState(history.state, '', u);
  }));

  /* ── Vorführung: ein gedachter Zeiger klickt Theme, Sprache, Fuß-Wahl, Sprache ── */
  let uhren = [], erste = true;
  function mitte(el) { const r = el.getBoundingClientRect(); return { x: r.left + r.width / 2, y: r.top + r.height / 2 }; }
  function fahre(el) { const p = mitte(el); zeiger.style.transform = `translate(${p.x - 3}px, ${p.y - 2}px)`; }
  function klicke(el, aenderung, was) {
    const p = mitte(el);
    zeiger.classList.remove('klick'); void zeiger.offsetWidth; zeiger.classList.add('klick');
    el.classList.add('gedrueckt'); setTimeout(() => el.classList.remove('gedrueckt'), 160);
    wechsel({ clientX: p.x, clientY: p.y, detail: 1 }, el, aenderung, was);
  }
  function plan(ms, fn) { uhren.push(setTimeout(fn, ms)); }
  function runde() {
    uhren.forEach(clearTimeout); uhren = [];
    const v = erste ? 1300 : 0;      // erste Runde: kurz ruhig stehen, dann klicken
    erste = false;
    if (welt === 'light-dark') {
      // Produktseite: Theme-Knopf, Blick aufs Produktbild, zurück
      const bild = $('.p-app');
      plan(v + 300, () => fahre(themaBtnP));
      plan(v + 1080, () => klicke(themaBtnP, themaUm, 'thema'));
      plan(v + 2300, () => { const r = bild.getBoundingClientRect(); zeiger.style.transform = `translate(${r.left + r.width * .62}px, ${Math.min(innerHeight - 60, r.top + r.height * .3)}px)`; });
      plan(v + 4000, () => fahre(themaBtnP));
      plan(v + 4780, () => klicke(themaBtnP, themaUm, 'thema'));
      plan(v + 6000, () => { const r = bild.getBoundingClientRect(); zeiger.style.transform = `translate(${r.left + r.width * .3}px, ${Math.min(innerHeight - 60, r.top + r.height * .35)}px)`; });
      plan(v + 7400, runde);
      return;
    }
    const hellRadio = radios.find(r => r.value === 'hell').parentElement;
    plan(v + 300, () => fahre(themaBtn));
    plan(v + 1080, () => klicke(themaBtn, themaUm, 'thema'));
    plan(v + 2700, () => fahre(spracheLink));
    plan(v + 3480, () => klicke(spracheLink, spracheUm, 'sprache'));
    plan(v + 5100, () => fahre(hellRadio));
    plan(v + 5880, () => klicke(hellRadio, () => setzeWahl('hell', false), 'thema'));
    plan(v + 7500, () => fahre(spracheLink));
    plan(v + 8280, () => klicke(spracheLink, spracheUm, 'sprache'));
    plan(v + 9900, runde);
  }
  function grundzustand() {
    setzeSprache('de');
    setzeWahl('hell', false);
    delete html.dataset.vt;
  }
  function vorfuehren() {
    pause();
    grundzustand();
    html.classList.add('vorfuehrung');
    zeiger.style.transition = 'none';
    zeiger.style.transform = `translate(${innerWidth * 0.58}px, ${innerHeight * 0.62}px)`;
    void zeiger.offsetWidth; zeiger.style.transition = '';
    runde();
  }
  function pause() { uhren.forEach(clearTimeout); uhren = []; }
  function weiter() { if (!uhren.length && html.classList.contains('vorfuehrung')) runde(); }
  function stoppVorfuehrung() { if (html.classList.contains('vorfuehrung')) { pause(); html.classList.remove('vorfuehrung'); } }
  function neustart() {
    pause(); html.classList.remove('vorfuehrung'); erste = true;
    setzeSprache('de');
    let w = 'system'; try { w = localStorage.getItem(SPEICHER) || 'system'; } catch (e) {}
    setzeWahl(w, false);
    werteStart();
  }

  setzeSprache('de'); setzeWelt(welt);

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