Foto-Hero mit Leiste, Karte oder Weiche
Am Foto hängt schon die erste Handlung: eine Buchungsleiste im Bild, eine Karte mit dem echten Öffnungsstatus oder zwei bis drei gleichwertige Wege zu den Filialen.
Hotel, Pension, Ferienwohnung, Eventlocation: Foto mit Rand auf Markengrund, Anreise, Abreise, Gäste und Code im unteren Bilddrittel, mobil im Dialog (Terreno).
Ein Foto-Hero, an dem die erste Handlung schon hängt. Leiste: Buchungs- oder Anfrageleiste im unteren Bilddrittel über 10 bis 12 Spalten, das Foto mit sichtbarem Rand auf Markengrund (Terreno). Karte: eine deckende Karte unten links im Bild mit dem echten Öffnungsstatus, „Jetzt geöffnet, Küche bis 22 Uhr“ (Le Petit Bleu). Weiche: zwei bis drei gleichwertige Wege zu Filialen oder Häusern mit Ortsname und Status, ohne Rangfolge und ohne Karussell (Union Boulangerie). Leiste und Karte fallen auf dem Telefon in den Fluss unter dem Bild; die Leiste öffnet sich dort als Dialog.
Gäste kommen mit einer Frage: Ist ein Zimmer frei? Hat das Lokal jetzt offen? Welche Filiale ist meine? Hängt die Antwort am Foto, ist sie im ersten Bildschirm, ohne dass das Bild an Wirkung verliert. Der Rand um das Foto, die deckende Karte und die gleich großen Wege halten Bild und Bedienung auseinander, darum bleibt beides lesbar.
Wann einsetzen
Leiste: Hotel, Pension, Ferienwohnung (Anreise, Abreise, Gäste) und Eventlocation (Wunschdatum, Anlass, Gäste). Karte: Restaurant, Café, Bar (Öffnungsstatus), Handwerk (aktuelles Projekt), Kultur (laufende Ausstellung), Praxis (Urlaub, Vertretung). Weiche: Bäckerei oder Café mit Filialen, Gastro mit zwei Häusern, Hotelgruppe, Praxis mit zwei Standorten.
Wann nicht
Leiste nicht für Tische (Datum, Uhrzeit, Personen gehören in termin-buchung-zwei-klick) und nicht ohne Buchungssystem oder gepflegte Belegung. Karte nie dekorativ: ohne echte Zeiten oder Daten keine Karte. Weiche nicht ab fünf Standorten (dann standortfinder-liste) und nie als Karussell oder mit hervorgehobenem Haupthaus. Auf dem Telefon keine zweite feste Leiste über einer unteren Navigation.
Varianten
- leiste: Foto mit 40 px Rand auf Markengrund, Buchungs- oder Anfrageleiste im unteren Bilddrittel über 10 bis 12 Spalten; unter 40rem ein Knopf, der die Leiste im nativen dialog öffnet (Terreno)
- karte: deckende Karte unten links im Bild, höchstens 22rem, mit echtem Öffnungsstatus nach oeffnungszeiten-status (Europe/Berlin); mobil unter dem Bild (Le Petit Bleu, Goodman Gallery)
- weiche: zwei bis drei gleichwertige Wege mit Ortsname, Adresse und Status als Band am Bildrand, keine Rangfolge, kein Karussell (Union Boulangerie, The Devonshire)
Umsetzung
Ein Foto-Hero (<picture>, LCP-Bild mit fetchpriority="high", feste aspect-ratio), daran ein eigenes Element. Leiste: <form> mit Anreise, Abreise (type=date mit min), Gästen (Erwachsene, Kinder) und Aktionscode; absolut im unteren Bilddrittel über 10 bis 12 Rasterspalten, das Foto mit 24 bis 40 px Rand auf dem Markengrund. Prüfen nach dem Absenden: Abreise nach Anreise, Mindestaufenthalt, Code; Fehler am Feld (aria-invalid, aria-describedby) und gesammelt in einer role="alert"-Zeile. Erst „Verfügbarkeit prüfen“ zeigt Nächte und Richtpreis, erst der zweite Klick übergibt per URL an das Buchungssystem. Den vollen Kalender mit Belegung liefert buchungsleiste-datumsbereich. Unter 40rem steht statt der Leiste ein Knopf mit der Zusammenfassung; er hängt dasselbe Formular in einen <dialog> (showModal) und beim Schließen zurück, die Eingaben bleiben erhalten. Karte: <aside> unten links im Bild, deckend, max-width 22rem; Status nach oeffnungszeiten-status mit Intl.DateTimeFormat('de-DE', { timeZone: 'Europe/Berlin', hourCycle: 'h23' }).formatToParts(), Wochenplan plus Sonderzeiten, Küchenschluss als eigene Zeit, Aktualisierung zur vollen Minute; mobil im Fluss unter dem Bild. Weiche: <nav aria-label> mit zwei bis drei gleich breiten Links (Raster mit gleichen Spalten), je Ortsname, Adresse und Status aus derselben Zeitlogik; Reihenfolge nach Lage oder Alphabet, nie nach Wichtigkeit.
Robust umsetzen
Telefon. Foto oben im Hochformat. Leiste: ein Knopf mit Zusammenfassung („Anreise wählen · 2 Gäste“) öffnet das Formular im Dialog von unten. Karte: im Fluss direkt unter dem Bild, volle Breite. Weiche: Wege als Zeilen untereinander, volle Breite, Status rechts.
Ohne Bewegung. Foto steht ab dem ersten Bild. Dialog und Fehlerzeilen erscheinen ohne Weg. Statuspunkt ohne Puls.
Barrierefrei. Leiste als <form> mit sichtbaren <label>, Fehler am Feld und gesammelt angesagt, Fokus nach dem Öffnen des Dialogs auf das erste Feld und nach dem Schließen zurück auf den Knopf. Status nie nur als Farbe: Punkt plus Wort („Jetzt geöffnet“). Die Karte ist deckend, damit der Text nicht vom Foto abhängt (4,5:1). Weiche als <nav> mit aria-label, Links mit Ortsnamen als Text; der Status steht im Linktext oder per aria-describedby. Bei 320 px brechen Leiste und Wege um.
Code
<!DOCTYPE html>
<!--
Foto-Hero mit Leiste, Karte oder Weiche
Fundus · Hero & Section-Layouts · https://cecodesign.de/lab/fundus/hero-layouts/foto-hero-mit-leiste/
Eigenständige Datei. Bilder liegen hier unter bilder/ – eigene einsetzen.
-->
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Foto-Hero mit Leiste, Karte oder Weiche · Fundus-Demo</title>
<script>document.documentElement.classList.add('js');if(matchMedia('(prefers-reduced-motion: reduce)').matches)document.documentElement.classList.add('reduziert')</script>
<style>
@font-face { font-family: 'Newsreader'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/newsreader:vf@latest/latin-opsz-normal.woff2') format('woff2-variations'); font-weight: 200 800; 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: 'Fraunces'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/fraunces:vf@latest/latin-full-normal.woff2') format('woff2-variations'); font-weight: 100 900; font-display: swap; }
@font-face { font-family: 'Commissioner'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/commissioner:vf@latest/latin-full-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; }
* { box-sizing: border-box; margin: 0; }
html { -webkit-text-size-adjust: 100%; }
:root:has(:focus-visible) { scroll-behavior: auto; }
body { min-height: 100svh; overflow-x: clip; }
img { display: block; max-width: 100%; }
a { color: inherit; }
button, input, select { font: inherit; color: inherit; }
.welt[hidden], [hidden] { display: none !important; }
.sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
/* =====================================================================
Variante leiste · Hotel Ufermann, Plau am See (Rezept boutique-hotel-gastgeber)
===================================================================== */
.hotel {
--grund: #6E5A47; --auf-grund: #FEF2E3; --sand: #F3EEE7; --creme: #FEF2E3; --text: #3E3A37; --leise: #6A6C67; --linie: #D5D1C8; --feld: #8f877c; --fehler: #B3261E;
--ease: cubic-bezier(0.37, 0, 0.63, 1); /* token-luxus-weich: Foto */
--ui-auf: cubic-bezier(0.16, 1, 0.3, 1); /* token-toggle-asymmetrisch */
--ui-zu: cubic-bezier(0.7, 0, 0.84, 0);
--rand: 40px; /* Terreno: 40 px Rand auf Markengrund */
background: var(--grund); color: var(--auf-grund);
font: 400 1.0625rem/1.6 'Mona Sans', system-ui, sans-serif;
min-height: 100svh; display: flex; flex-direction: column;
}
.hotel :focus-visible { outline: 2px solid currentColor; outline-offset: 3px; }
.h-kopf { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 16px; padding: 14px var(--rand); min-height: 76px; }
.h-nav { display: flex; gap: 22px; list-style: none; padding: 0; }
.lbl { font: 600 .75rem/1 'Mona Sans', system-ui, sans-serif; font-stretch: 118%; letter-spacing: .06em; text-transform: uppercase; }
.h-nav a, .h-rechts a { text-decoration: none; padding: 10px 0; display: inline-block; }
.h-nav a:hover, .h-rechts a:hover { text-decoration: underline; text-underline-offset: 5px; }
.h-marke { font: italic 300 2rem/1 'Newsreader', Georgia, serif; font-variation-settings: 'opsz' 48; letter-spacing: -.01em; text-decoration: none; }
.h-rechts { display: flex; gap: 18px; justify-content: flex-end; align-items: center; }
.h-rechts a[aria-current] { text-decoration: underline; text-underline-offset: 5px; }
.h-bild { position: relative; flex: 1; margin: 0 var(--rand) var(--rand); min-height: 520px; overflow: hidden; container-type: inline-size; }
.h-bild picture, .h-bild > picture img { position: absolute; inset: 0; width: 100%; height: 100%; }
.h-bild > picture img { object-fit: cover; object-position: 50% 0%; }
.h-bild::before { content: ""; position: absolute; inset: 0 0 auto; height: 22%; z-index: 1; pointer-events: none; background: linear-gradient(180deg, rgba(232, 222, 222, .5), rgba(232, 222, 222, 0)); }
.h-titel { position: absolute; z-index: 2; left: 0; right: 0; top: clamp(16px, 2.6cqi, 34px); padding: 0 24px; text-align: center; color: #2e2a28;
font: 300 clamp(2rem, 1rem + 2.6vw, 3.4rem)/1.05 'Newsreader', Georgia, serif; font-variation-settings: 'opsz' 72; letter-spacing: -.015em; text-wrap: balance; }
.h-titel em { font-style: italic; }
.h-titel small { display: block; margin-bottom: 18px; font: 600 .75rem/1 'Mona Sans', system-ui, sans-serif; font-stretch: 118%; letter-spacing: .06em; text-transform: uppercase; color: #3E3A37; }
/* Buchungsleiste im unteren Bilddrittel, 10 von 12 Spalten */
.leiste-platz { position: absolute; z-index: 3; left: 50%; bottom: clamp(24px, 7%, 64px); width: min(calc(100% - 48px), 1120px); transform: translateX(-50%); }
.leiste { background: var(--creme); color: var(--text); border-radius: 6px; }
.leiste__felder { display: grid; grid-template-columns: 1fr 1fr 1.3fr .9fr auto; align-items: stretch; }
.feld { position: relative; display: grid; align-content: center; gap: 4px; padding: 14px 18px; border-right: 1px solid var(--linie); min-width: 0; }
.feld label, .feld .feld__titel { font: 600 .6875rem/1 'Mona Sans', system-ui, sans-serif; font-stretch: 118%; letter-spacing: .07em; text-transform: uppercase; color: var(--leise); }
.feld input, .feld__knopf { width: 100%; min-width: 0; min-height: 36px; padding: 2px 0; border: 0; border-bottom: 1px solid transparent; background: transparent; font: 400 1.25rem/1.2 'Newsreader', Georgia, serif; font-variation-settings: 'opsz' 24; color: var(--text); }
.feld input::placeholder { color: var(--leise); opacity: 1; }
.feld input:hover, .feld__knopf:hover { border-bottom-color: var(--feld); }
.feld input:focus-visible, .feld__knopf:focus-visible { outline: 2px solid var(--text); outline-offset: 4px; border-radius: 2px; }
.feld input[type=date] { cursor: pointer; }
/* Datum: deutsches Kurzformat als Anzeige über dem nativen Feld; beim Tippen (Fokus) erscheint das native Feld */
.datum { grid-template-areas: "l" "f"; }
.datum label { grid-area: l; }
.datum input, .datum__text { grid-area: f; }
.datum__text { align-self: center; pointer-events: none; font: 400 1.25rem/1.2 'Newsreader', Georgia, serif; font-variation-settings: 'opsz' 24; color: var(--text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; padding-right: 26px; }
.datum__text.leer { color: var(--leise); }
.datum input:not(:focus) { color: transparent; }
.datum input:focus + .datum__text { visibility: hidden; }
.feld input[type=date]::-webkit-calendar-picker-indicator { opacity: .6; cursor: pointer; }
.feld:focus-within { background: #fffaf3; box-shadow: inset 0 -2px 0 var(--text); }
.feld[data-fehler] label, .feld[data-fehler] .feld__titel { color: var(--fehler); }
.feld[data-fehler] input, .feld[data-fehler] .feld__knopf { border-bottom: 2px solid var(--fehler); }
.feld__knopf { text-align: left; cursor: pointer; display: flex; justify-content: space-between; align-items: center; gap: 8px; }
.feld__knopf span { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.feld__knopf svg { width: 12px; height: 12px; flex: none; transition: transform 220ms var(--ui-auf); }
.feld__knopf[aria-expanded="true"] svg { transform: rotate(180deg); }
.leiste__los { display: flex; align-items: center; padding: 12px; }
.knopf-haus { display: inline-flex; align-items: center; justify-content: center; gap: 10px; min-height: 52px; padding: 0 24px; border: 0; border-radius: 999px; background: #6E5A47; color: #fff; cursor: pointer; white-space: nowrap; text-decoration: none;
font: 600 .8125rem/1 'Mona Sans', system-ui, sans-serif; font-stretch: 112%; letter-spacing: .05em; text-transform: uppercase; transition: background-color 220ms var(--ui-auf); }
.knopf-haus:hover { background: #57473a; }
.knopf-haus:focus-visible { outline: 2px solid var(--text); outline-offset: 3px; }
.knopf-haus svg { width: 14px; height: 14px; }
/* Gäste-Auswahl als Popover direkt am Feld */
.gaeste { position: absolute; left: 0; bottom: calc(100% + 10px); width: 300px; padding: 18px; border-radius: 6px; background: #fffaf3; color: var(--text); box-shadow: 0 18px 50px rgba(40, 30, 20, .28);
opacity: 0; transform: translateY(8px); visibility: hidden; transition: opacity 200ms var(--ui-zu), transform 200ms var(--ui-zu), visibility 0s 200ms; }
.gaeste.offen { opacity: 1; transform: none; visibility: visible; transition: opacity 300ms var(--ui-auf), transform 300ms var(--ui-auf), visibility 0s; }
.zaehl { display: flex; justify-content: space-between; align-items: center; gap: 12px; padding: 8px 0; }
.zaehl + .zaehl { border-top: 1px solid var(--linie); }
.zaehl b { font-weight: 500; display: block; }
.zaehl small { color: var(--leise); font-size: .8125rem; }
.stepper { display: flex; align-items: center; gap: 10px; }
.stepper button { width: 36px; height: 36px; border-radius: 50%; border: 1px solid var(--feld); background: transparent; cursor: pointer; font-size: 1.1rem; line-height: 1; }
.stepper button:disabled { opacity: .45; cursor: not-allowed; }
.stepper output { min-width: 1.4em; text-align: center; font-variant-numeric: tabular-nums; }
.alter { display: grid; grid-template-columns: repeat(auto-fill, minmax(118px, 1fr)); gap: 8px; margin-top: 8px; }
.alter label { display: grid; gap: 4px; font-size: .8125rem; color: var(--leise); }
.alter select { min-height: 40px; border: 1px solid var(--feld); border-radius: 6px; background: #fff; padding: 0 8px; color: var(--text); }
.alter select[aria-invalid="true"] { border: 2px solid var(--fehler); }
.gaeste__fuss { display: flex; justify-content: space-between; align-items: center; gap: 12px; margin-top: 12px; font-size: .8125rem; color: var(--leise); }
.gaeste__fuss button { min-height: 40px; padding: 0 16px; border-radius: 999px; border: 1px solid var(--text); background: transparent; cursor: pointer; }
/* Meldungen und Ergebnis unter den Feldern, im selben Papier */
.leiste__meldung { border-top: 1px solid var(--linie); padding: 14px 18px; font-size: .9375rem; }
.leiste__meldung[data-art="fehler"] { color: var(--fehler); }
.leiste__meldung ul { margin: 6px 0 0; padding-left: 1.1em; }
.leiste__meldung a { color: inherit; }
.ergebnis { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 12px 24px; color: var(--text); }
.ergebnis__zahl { font: 400 1.375rem/1.2 'Newsreader', Georgia, serif; font-variation-settings: 'opsz' 24; }
.ergebnis__zahl small { display: block; margin-top: 4px; font: 400 .8125rem/1.45 'Mona Sans', system-ui, sans-serif; color: var(--leise); }
.uebergabe { margin-top: 10px; padding: 10px 12px; border-radius: 6px; background: var(--sand); font: 400 .75rem/1.5 'Geist Mono', ui-monospace, monospace; color: var(--text); overflow-wrap: anywhere; }
.leiste__meldung { animation: rein 300ms var(--ui-auf); }
@keyframes rein { from { opacity: 0; transform: translateY(-4px); } }
/* Telefon: Knopf statt Leiste, Formular im Dialog */
.leiste-knopf { display: none; }
.leiste-dialog { width: 100%; max-width: 100%; max-height: 92svh; margin: auto 0 0; padding: 0; border: 0; border-radius: 14px 14px 0 0; background: var(--creme); color: var(--text); overflow: auto; }
.leiste-dialog::backdrop { background: rgba(30, 28, 26, .5); }
.leiste-dialog[open] { animation: blatt 300ms var(--ui-auf); }
@keyframes blatt { from { transform: translateY(100%); } }
.leiste-dialog__kopf { display: flex; justify-content: space-between; align-items: center; padding: 14px 18px; border-bottom: 1px solid var(--linie); position: sticky; top: 0; background: var(--creme); z-index: 2; }
.leiste-dialog__kopf h2 { font: 400 1.375rem/1.2 'Newsreader', Georgia, serif; }
.leiste-dialog__kopf button { width: 44px; height: 44px; border: 0; background: transparent; border-radius: 50%; cursor: pointer; font-size: 1.4rem; }
.leiste-dialog .leiste { border-radius: 0; }
.leiste-dialog .leiste__felder { grid-template-columns: minmax(0, 1fr); }
.leiste-dialog .feld { border-right: 0; border-bottom: 1px solid var(--linie); }
.leiste-dialog .leiste__los { padding: 16px 18px; }
.leiste-dialog .knopf-haus { width: 100%; }
.leiste-dialog .gaeste { position: static; width: auto; margin-top: 8px; box-shadow: none; border: 1px solid var(--linie); }
.leiste-dialog .gaeste:not(.offen) { display: none; }
/* Auftritt */
.js .hotel.auftritt .h-bild > picture img { transform: scale(1.06); }
.js .hotel.auftritt .h-titel { opacity: 0; transform: translateY(16px); }
.js .hotel.auftritt .leiste-platz { opacity: 0; transform: translate(-50%, 24px); }
.hotel .h-bild > picture img { transition: transform 1200ms var(--ease); }
.hotel .h-titel { transition: opacity 1200ms var(--ease) 200ms, transform 1200ms var(--ease) 200ms; }
.hotel .leiste-platz { transition: opacity 450ms var(--ui-auf) 650ms, transform 450ms var(--ui-auf) 650ms; }
@media (max-width: 1000px) {
.h-nav li:nth-child(n+3) { display: none; }
.leiste__felder { grid-template-columns: 1fr 1fr 1fr 1fr; }
.leiste__los { grid-column: 1 / -1; border-top: 1px solid var(--linie); justify-content: flex-end; }
.feld:nth-child(4) { border-right: 0; }
}
@media (max-width: 40rem) {
.hotel { --rand: 14px; }
.h-kopf { grid-template-columns: auto 1fr; min-height: 60px; padding: 10px var(--rand); }
.h-nav { display: none; }
.h-marke { font-size: 1.75rem; }
.h-bild { flex: none; min-height: 0; aspect-ratio: 4 / 5; margin-bottom: 0; }
.h-titel { font-size: clamp(2rem, 8.6vw, 2.6rem); top: 22px; }
.leiste-platz { display: none; }
.leiste-knopf { display: flex; justify-content: space-between; align-items: center; gap: 12px; width: calc(100% - 28px); margin: 14px; min-height: 64px; padding: 10px 10px 10px 18px; border: 0; border-radius: 6px; background: var(--creme); color: var(--text); cursor: pointer; text-align: left; }
.leiste-knopf span { display: grid; gap: 3px; }
.leiste-knopf b { font: 400 1.125rem/1.2 'Newsreader', Georgia, serif; }
.leiste-knopf small { font: 600 .6875rem/1 'Mona Sans', system-ui, sans-serif; font-stretch: 118%; letter-spacing: .07em; text-transform: uppercase; color: var(--leise); }
.leiste-knopf i { display: inline-flex; align-items: center; min-height: 44px; padding: 0 16px; border-radius: 999px; background: #6E5A47; color: #fff; font: 600 .75rem/1 'Mona Sans', system-ui, sans-serif; font-style: normal; letter-spacing: .05em; text-transform: uppercase; }
.leiste-knopf:focus-visible { outline: 2px solid var(--auf-grund); outline-offset: 3px; }
.h-unter { padding: 4px var(--rand) 28px; }
}
.h-unter { display: flex; flex-wrap: wrap; justify-content: space-between; gap: 6px 20px; padding: 0 var(--rand) 18px; margin-top: -24px; }
@media (max-width: 40rem) { .h-unter { margin-top: 0; } }
/* =====================================================================
Variante karte · Wirtschaft Fenchel, Freiburg-Wiehre (Rezept gastro-warm-appetitlich)
===================================================================== */
.gast, .baeckerei {
--grund: #F0ECE0; --text: #353839; --leise: #5E605E; --flaeche: #7F937A; --dunkel: #293E3A; --senf: #EAC16E; --rose: #E1A894; --linie: rgba(53, 56, 57, .2); --papier: #FBF8F1;
--offen: #2C6E49; --bald: #9A6A12; --zu: #7A7C7B;
--ease: cubic-bezier(0.16, 1, 0.3, 1); /* token-editorial-langsam */
--ui-auf: cubic-bezier(0.16, 1, 0.3, 1); --ui-zu: cubic-bezier(0.7, 0, 0.84, 0);
--rand: clamp(16px, 4vw, 48px); --spalt: clamp(12px, 2vw, 24px);
background: var(--grund); color: var(--text);
font: 400 1.0625rem/1.55 'Commissioner', system-ui, sans-serif;
}
.gast :focus-visible, .baeckerei :focus-visible { outline: 2px solid var(--text); outline-offset: 3px; }
.ru { font: 600 .75rem/1 'Commissioner', system-ui, sans-serif; font-variation-settings: 'FLAR' 80; letter-spacing: .15em; text-transform: uppercase; }
.g-hero { display: grid; grid-template-columns: repeat(12, minmax(0, 1fr)); column-gap: var(--spalt); min-height: 100svh; padding-right: var(--rand); }
.g-bild { grid-column: 1 / 8; position: relative; min-height: 100svh; overflow: hidden; border-radius: 0 12px 12px 0; }
.g-bild img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; object-position: 58% 50%; }
.g-text { grid-column: 8 / 13; display: flex; flex-direction: column; justify-content: space-between; padding: clamp(18px, 3vw, 32px) 0 clamp(24px, 4vw, 48px); gap: 32px; }
.g-kopf { display: flex; justify-content: space-between; align-items: center; gap: 12px; }
.g-marke { font: 600 1.375rem/1 'Fraunces', Georgia, serif; font-variation-settings: 'SOFT' 100, 'opsz' 48; text-decoration: none; }
.g-kopf nav { display: flex; gap: 18px; }
.g-kopf nav a { text-decoration: none; padding: 12px 0; }
.g-kopf nav a:hover { text-decoration: underline; text-underline-offset: 5px; }
.g-h1 { font: 500 clamp(2.5rem, 1.6rem + 2.6vw, 4.25rem)/1.02 'Fraunces', Georgia, serif; font-variation-settings: 'SOFT' 80; letter-spacing: -.01em; text-wrap: balance; margin: 14px 0 22px; }
.g-wege { display: flex; flex-wrap: wrap; gap: 12px; }
.g-knopf { display: inline-flex; align-items: center; gap: 10px; min-height: 48px; padding: 0 22px; border-radius: 999px; text-decoration: none; font-weight: 600; font-size: .9375rem; transition: background-color 200ms var(--ui-auf), color 200ms var(--ui-auf); }
.g-knopf--voll { background: var(--dunkel); color: #fff; }
.g-knopf--voll:hover { background: #1c2c29; }
.g-knopf--linie { box-shadow: inset 0 0 0 1.5px var(--text); }
.g-knopf--linie:hover { background: var(--text); color: var(--grund); }
.g-adresse { margin-top: 22px; color: var(--leise); font-size: .9375rem; }
.g-adresse a { color: var(--text); }
/* Karte: deckend, unten links im Bild, höchstens 22rem */
.karte { position: absolute; left: clamp(16px, 2.4vw, 32px); bottom: clamp(16px, 2.4vw, 32px); width: min(22rem, calc(100% - 32px)); padding: 20px 22px 18px; border-radius: 12px; background: var(--papier); color: var(--text); }
.status { display: flex; align-items: center; gap: 10px; }
.punkt { width: 10px; height: 10px; border-radius: 50%; flex: none; background: var(--zu); }
.status[data-art="offen"] .punkt { background: var(--offen); box-shadow: 0 0 0 5px rgba(44, 110, 73, .16); }
.status[data-art="kueche"] .punkt, .status[data-art="bald"] .punkt { background: var(--senf); box-shadow: 0 0 0 5px rgba(234, 193, 110, .3); }
.status[data-art="pause"] .punkt { background: transparent; box-shadow: inset 0 0 0 2px var(--zu); }
.status__kopf { font: 500 1.625rem/1.1 'Fraunces', Georgia, serif; font-variation-settings: 'SOFT' 100; }
.status__zeile { margin-top: 6px; font-size: 1rem; min-height: 1.55em; }
.status__zeile b { font-weight: 600; }
.karte dl { display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 6px 16px; margin-top: 14px; padding-top: 14px; border-top: 1px solid var(--linie); font-size: .9375rem; font-variant-numeric: tabular-nums; }
.karte dt { color: var(--leise); }
.karte dd { margin: 0; text-align: right; }
.karte__sonder { margin-top: 12px; padding: 8px 10px; border-radius: 6px; background: rgba(234, 193, 110, .3); font-size: .875rem; }
.karte__fuss { display: flex; justify-content: space-between; gap: 12px; margin-top: 14px; font-size: .9375rem; }
.karte__fuss a { text-underline-offset: 3px; }
.karte .wechsel { transition: opacity 200ms var(--ui-auf); }
.karte.tickt .wechsel { opacity: 0; transition-duration: 1ms; }
/* Auftritt (Rezept: nur Bilder bekommen eine Einblendung, h1 ein Masken-Reveal) */
.js .gast.auftritt .g-bild img, .js .baeckerei.auftritt .w-bild img { transform: scale(.96); opacity: 0; }
.js .gast.auftritt .g-h1 .z > span, .js .baeckerei.auftritt .w-h1 .z > span { transform: translateY(105%); }
.gast .g-bild img, .baeckerei .w-bild img { transition: transform 700ms var(--ease), opacity 700ms var(--ease); }
.z { display: block; overflow: clip; padding-bottom: .06em; }
.z > span { display: block; transition: transform 1000ms var(--ease) 150ms; }
@media (max-width: 860px) {
.g-hero { grid-template-columns: minmax(0, 1fr); padding: 0; min-height: 0; }
.g-bild { grid-column: 1; min-height: 0; border-radius: 0; overflow: visible; }
.g-bild { aspect-ratio: auto; display: flex; flex-direction: column; }
.g-bild picture { position: relative; inset: auto; display: block; aspect-ratio: 4 / 5; }
.g-text { display: contents; }
.g-kopf { order: -1; padding: 12px var(--rand); }
.g-text > div { order: 1; padding: 6px var(--rand) 32px; }
.g-kopf nav { display: none; }
.karte { position: relative; left: auto; bottom: auto; width: auto; margin: -48px var(--rand) 0; box-shadow: 0 10px 30px rgba(40, 30, 20, .12); }
}
/* =====================================================================
Variante weiche · Bäckerei Kornhaus, Münster (Rezept gastro-warm-appetitlich)
===================================================================== */
.w-hero { position: relative; min-height: 100svh; display: grid; grid-template-rows: auto 1fr auto; }
.w-bild { position: absolute; inset: 0; overflow: hidden; }
.w-bild img { width: 100%; height: 100%; object-fit: cover; object-position: 50% 60%; }
.w-bild::after { content: ""; position: absolute; inset: 0; pointer-events: none; background: radial-gradient(120% 70% at 0% 0%, rgba(240, 236, 224, .94) 0%, rgba(240, 236, 224, .82) 38%, rgba(240, 236, 224, 0) 70%); }
.w-kopf { position: relative; display: flex; justify-content: space-between; align-items: center; gap: 12px; padding: clamp(14px, 2vw, 24px) var(--rand); }
.w-marke { font: 600 1.375rem/1 'Fraunces', Georgia, serif; font-variation-settings: 'SOFT' 100, 'opsz' 48; text-decoration: none; }
.w-kopf a.ru { text-decoration: none; padding: 12px 0; }
.w-text { position: relative; padding: 0 var(--rand); max-width: 760px; }
.w-h1 { font: 500 clamp(2.5rem, 1.6rem + 3.4vw, 5rem)/1.02 'Fraunces', Georgia, serif; font-variation-settings: 'SOFT' 80; letter-spacing: -.01em; margin-top: 6px; text-wrap: balance; }
.w-text p { margin-top: 14px; max-width: 34ch; }
/* Die Weiche: gleich breite Wege, keine Rangfolge */
.weiche { position: relative; margin: 0 var(--rand) var(--rand); }
.weiche__titel { display: flex; padding: 0 0 10px; }
.weiche__titel span { display: inline-block; padding: 9px 12px; border-radius: 6px; background: var(--papier); }
.weiche ul { list-style: none; padding: 0; display: grid; grid-template-columns: repeat(var(--n, 3), minmax(0, 1fr)); gap: 1px; background: var(--linie); border-radius: 12px; overflow: hidden; }
.weg { display: grid; grid-template-rows: auto auto 1fr auto; gap: 4px; height: 100%; padding: 20px 22px 18px; background: var(--papier); text-decoration: none; transition: background-color 200ms var(--ui-auf); }
.weg:hover, .weg.vorgefuehrt { background: #fff; }
.weg__ort { display: flex; justify-content: space-between; align-items: baseline; gap: 12px; font: 500 clamp(1.625rem, 1.2rem + 1vw, 2.125rem)/1.05 'Fraunces', Georgia, serif; font-variation-settings: 'SOFT' 100; }
.weg__ort svg { width: 20px; height: 20px; flex: none; transition: transform 200ms var(--ui-auf); }
.weg:hover .weg__ort svg, .weg.vorgefuehrt .weg__ort svg { transform: translateX(4px); }
.weg__adr { color: var(--leise); font-size: .9375rem; }
.weg__extra { font-size: .9375rem; }
.weg .status { margin-top: 14px; padding-top: 12px; border-top: 1px solid var(--linie); font-size: .9375rem; }
.weg .status b { font-weight: 600; }
.w-unter { position: relative; padding: 0 var(--rand) 20px; color: var(--leise); font-size: .875rem; }
@media (max-width: 860px) {
.w-h1, .g-h1 { text-wrap: balance; }
.w-hero { min-height: 0; grid-template-rows: auto auto auto; }
.w-bild { position: relative; inset: auto; grid-row: 3; aspect-ratio: 4 / 3; margin: 18px 0 0; }
.w-text { grid-row: 2; }
.weiche { grid-row: 4; margin: 0; transform: none; }
.weiche ul { grid-template-columns: minmax(0, 1fr); border-radius: 0; }
.weg { grid-template-columns: minmax(0, 1fr) auto; grid-template-rows: auto auto; padding: 16px var(--rand); column-gap: 16px; }
.weg__ort { grid-column: 1; }
.weg__ort svg { display: none; }
.weg__adr { grid-column: 1; }
.weg__extra { display: none; }
.weg .status { grid-column: 2; grid-row: 1 / 3; margin: 0; padding: 0; border: 0; align-self: center; text-align: right; display: flex; justify-content: flex-end; gap: 8px; max-width: 12em; }
.weg .status .punkt { margin-top: .45em; align-self: flex-start; }
.weiche__titel { padding: 18px var(--rand) 10px; }
.weiche__titel span { padding: 0; background: none; }
.w-unter { padding-top: 14px; }
}
/* ---------- Pult ---------- */
.pult { position: fixed; left: 12px; bottom: 12px; z-index: 60; display: grid; gap: 8px; max-width: min(460px, calc(100vw - 24px)); padding: 10px 12px; border-radius: 12px;
background: rgba(18, 18, 20, .86); color: #f3f2ee; -webkit-backdrop-filter: blur(10px); backdrop-filter: blur(10px); border: 1px solid rgba(243, 242, 238, .16);
font: 500 10.5px/1.45 'Geist Mono', ui-monospace, monospace; letter-spacing: .06em; }
.pult__reihe { display: flex; gap: 6px; flex-wrap: wrap; align-items: center; }
.pult__titel { color: rgba(243, 242, 238, .7); text-transform: uppercase; letter-spacing: .14em; margin-right: 4px; }
.pult button, .pult select { font: inherit; letter-spacing: .1em; text-transform: uppercase; color: inherit; background: transparent; border: 1px solid rgba(243, 242, 238, .28); border-radius: 999px; padding: 6px 10px; cursor: pointer; }
.pult select { text-transform: none; letter-spacing: .04em; max-width: 100%; }
.pult select option { color: #121214; }
.pult button[aria-pressed="true"] { background: #f3f2ee; color: #121214; border-color: #f3f2ee; }
.pult button:focus-visible, .pult select:focus-visible { outline: 2px solid #34a7b9; outline-offset: 2px; }
.pult__werte { color: rgba(243, 242, 238, .74); }
.pult__werte b { color: #f3f2ee; font-weight: 500; }
.vorschau body { overflow: hidden; }
.vorschau .h-unter, .vorschau .w-unter { display: none; }
:root.reduziert * { transition-duration: .01ms !important; animation-duration: .01ms !important; transition-delay: 0s !important; }
@media (prefers-reduced-motion: reduce) { .punkt { box-shadow: none !important; } }
@media (max-width: 360px) {
.leiste-knopf i { padding: 0 12px; }
.g-wege .g-knopf { width: 100%; justify-content: center; }
}
@media (forced-colors: active) {
button, [role=button], [role=tab], [role=switch], input, select, textarea, .g-knopf, .knopf-haus { border: 1px solid ButtonText !important; }
.punkt { forced-color-adjust: none; }
}
</style>
</head>
<body>
<!-- ============ Variante leiste ============ -->
<div class="welt hotel" data-welt="leiste">
<header class="h-kopf">
<ul class="h-nav lbl">
<li><a href="#zimmer">Zimmer</a></li>
<li><a href="#sauna">Sauna am Steg</a></li>
<li><a href="#fruehstueck">Frühstück</a></li>
<li><a href="#anreise">Anreise</a></li>
</ul>
<a class="h-marke" href="#">Ufermann</a>
<div class="h-rechts lbl"><a href="#" aria-current="true" lang="de">DE</a><a href="#" lang="en">EN</a><a href="tel:+49387352240">038735 22 40</a></div>
</header>
<main class="h-bild">
<picture>
<source media="(max-width: 40rem)" srcset="bilder/hotel-haus-am-see-hoch.webp" width="900" height="1200">
<img src="bilder/hotel-haus-am-see.webp" width="1920" height="1200" fetchpriority="high" alt="Backsteinhaus mit erleuchteten Fenstern am See in der Abenddämmerung, daneben eine kleine Holzsauna am Steg; Haus und Bäume spiegeln sich im Wasser">
</picture>
<h1 class="h-titel">Zwölf Zimmer am <em>Plauer See</em></h1>
<div class="leiste-platz" data-platz>
<form class="leiste" data-leiste novalidate aria-label="Verfügbarkeit prüfen">
<div class="leiste__felder">
<div class="feld datum" data-feld="anreise">
<label for="anreise">Anreise</label>
<input id="anreise" name="anreise" type="date" aria-describedby="leiste-meldung">
<span class="datum__text" aria-hidden="true" data-datum-text="anreise">Datum wählen</span>
</div>
<div class="feld datum" data-feld="abreise">
<label for="abreise">Abreise</label>
<input id="abreise" name="abreise" type="date" aria-describedby="leiste-meldung">
<span class="datum__text" aria-hidden="true" data-datum-text="abreise">Datum wählen</span>
</div>
<div class="feld" data-feld="gaeste">
<span class="feld__titel" id="gaeste-titel">Gäste</span>
<button type="button" class="feld__knopf" data-gaeste-knopf aria-expanded="false" aria-controls="gaeste-wahl" aria-labelledby="gaeste-titel gaeste-text"><span id="gaeste-text">2 Erwachsene</span><svg viewBox="0 0 12 12" aria-hidden="true"><path d="M2 4l4 4 4-4" fill="none" stroke="currentColor" stroke-width="1.4"/></svg></button>
<div class="gaeste" id="gaeste-wahl" data-gaeste role="group" aria-label="Gäste wählen" inert>
<div class="zaehl"><span><b>Erwachsene</b><small>ab 14 Jahren</small></span><span class="stepper"><button type="button" data-schritt="erw" data-d="-1" aria-label="Erwachsene weniger">−</button><output data-zahl="erw" aria-live="polite">2</output><button type="button" data-schritt="erw" data-d="1" aria-label="Erwachsene mehr">+</button></span></div>
<div class="zaehl"><span><b>Kinder</b><small>0 bis 13 Jahre</small></span><span class="stepper"><button type="button" data-schritt="kind" data-d="-1" aria-label="Kinder weniger">−</button><output data-zahl="kind" aria-live="polite">0</output><button type="button" data-schritt="kind" data-d="1" aria-label="Kinder mehr">+</button></span></div>
<div class="alter" data-alter></div>
<div class="gaeste__fuss"><span>Höchstens 4 Gäste je Zimmer</span><button type="button" data-gaeste-fertig>Fertig</button></div>
</div>
</div>
<div class="feld" data-feld="code">
<label for="code">Aktionscode</label>
<input id="code" name="code" type="text" placeholder="optional" autocomplete="off" spellcheck="false" autocapitalize="characters" aria-describedby="leiste-meldung">
</div>
<div class="leiste__los"><button class="knopf-haus" type="submit" data-pruefen>Verfügbarkeit prüfen</button></div>
</div>
<div class="leiste__meldung" id="leiste-meldung" data-meldung role="status" hidden></div>
</form>
</div>
</main>
<button type="button" class="leiste-knopf" data-leiste-knopf aria-haspopup="dialog"><span><small>Anreise · Abreise · Gäste</small><b data-zusammen>Datum wählen · 2 Gäste</b></span><i>Prüfen</i></button>
<p class="h-unter lbl"><span>Seestraße 21 · 19395 Plau am See</span><span>Sauna am Steg · Frühstück bis 11 Uhr inklusive</span></p>
<dialog class="leiste-dialog" data-dialog aria-labelledby="dialog-titel">
<div class="leiste-dialog__kopf"><h2 id="dialog-titel">Verfügbarkeit prüfen</h2><button type="button" data-schliessen aria-label="Schließen">×</button></div>
<div data-dialog-platz></div>
</dialog>
</div>
<!-- ============ Variante karte ============ -->
<div class="welt gast" data-welt="karte" hidden>
<div class="g-hero">
<div class="g-bild">
<picture><img src="bilder/zitronen-tisch.jpg" width="1200" height="800" alt="Zitronen und eine halbierte Zitrone auf einem hellen Steingutteller, dazu Blätter auf dunklem Schiefer im Gegenlicht"></picture>
<aside class="karte" data-karte aria-labelledby="karte-kopf">
<div class="wechsel">
<div class="status" data-status data-art="zu"><span class="punkt" aria-hidden="true"></span><h2 class="status__kopf" id="karte-kopf" data-kopf>Öffnungszeiten</h2></div>
<p class="status__zeile" data-zeile>Dienstag bis Samstag ab 17.30 Uhr, Sonntag auch mittags.</p>
<dl data-tage></dl>
<p class="karte__sonder" data-sonder hidden></p>
</div>
<div class="karte__fuss"><a href="#zeiten">Alle Zeiten</a><a href="tel:+497617071880">0761 707 18 80</a></div>
</aside>
</div>
<div class="g-text">
<header class="g-kopf">
<a class="g-marke" href="#">Fenchel</a>
<nav class="ru" aria-label="Hauptmenü"><a href="#karte-essen">Karte</a><a href="#gruppen">Gruppen</a><a href="#anfahrt">Anfahrt</a></nav>
</header>
<div>
<p class="ru">Wirtschaft in der Wiehre · Freiburg</p>
<h1 class="g-h1"><span class="z"><span>Badische Küche mit dem, was die Höfe im Dreisamtal liefern.</span></span></h1>
<div class="g-wege">
<a class="g-knopf g-knopf--voll" href="#reservieren">Tisch reservieren</a>
<a class="g-knopf g-knopf--linie" href="#karte-essen">Karte ansehen</a>
</div>
<p class="g-adresse">Lorettostraße 18, 79100 Freiburg · <a href="#route">Route</a></p>
</div>
</div>
</div>
</div>
<!-- ============ Variante weiche ============ -->
<div class="welt baeckerei" data-welt="weiche" hidden>
<div class="w-hero">
<div class="w-bild"><img src="bilder/foto-hero-mit-leiste-backstube.webp" width="1600" height="1000" alt="Runde und lange Sauerteiglaibe mit eingeschnittener, bemehlter Kruste auf Leinen und Holzbrett, dahinter ein Regal voller Brote im Morgenlicht"></div>
<header class="w-kopf">
<a class="w-marke" href="#">Kornhaus</a>
<a class="ru" href="#bestellen">Brot vorbestellen</a>
</header>
<div class="w-text">
<p class="ru">Bäckerei seit 1952 · Münster</p>
<h1 class="w-h1"><span class="z"><span>Sauerteig, drei Tage geführt. Dreimal in Münster.</span></span></h1>
</div>
<nav class="weiche" aria-labelledby="weiche-titel">
<p class="weiche__titel"><span class="ru" id="weiche-titel">Unsere drei Filialen</span></p>
<ul data-wege></ul>
</nav>
</div>
<p class="w-unter">Alle Zeiten, Feiertage und Anfahrt stehen unter „Filialen“ (standortfinder-liste).</p>
</div>
<div class="pult" role="group" aria-label="Steuerung der Demo">
<div class="pult__reihe">
<span class="pult__titel">Variante</span>
<button type="button" data-v="leiste" aria-pressed="true">Leiste</button>
<button type="button" data-v="karte" aria-pressed="false">Karte</button>
<button type="button" data-v="weiche" aria-pressed="false">Weiche</button>
</div>
<div class="pult__reihe" data-nur="karte weiche">
<label class="pult__titel" for="uhr">Uhr</label>
<select id="uhr" data-uhr></select>
</div>
<div class="pult__reihe" data-nur="leiste">
<button type="button" data-leeren>Leeren</button>
<button type="button" data-beispiel>Beispiel ausfüllen</button>
</div>
<div class="pult__werte" data-werte></div>
</div>
<script>
(() => {
const html = document.documentElement;
const reduziert = () => html.classList.contains('reduziert');
const $ = (s, w = document) => w.querySelector(s), $$ = (s, w = document) => [...w.querySelectorAll(s)];
const VARIANTEN = ['leiste', 'karte', 'weiche'];
const q0 = new URLSearchParams(location.search).get('v');
let variante = VARIANTEN.includes(q0) ? q0 : 'leiste';
const ZONE = 'Europe/Berlin';
/* ---------- Ortszeit (Europe/Berlin), unabhängig von der Zeitzone des Besuchers ---------- */
const teileFmt = new Intl.DateTimeFormat('de-DE', { timeZone: ZONE, hourCycle: 'h23', year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', weekday: 'short' });
const WT = { So: 0, Mo: 1, Di: 2, Mi: 3, Do: 4, Fr: 5, Sa: 6 };
function ortszeit(date) {
const p = Object.fromEntries(teileFmt.formatToParts(date).map(x => [x.type, x.value]));
return { datum: `${p.year}-${p.month}-${p.day}`, min: (+p.hour % 24) * 60 + +p.minute, wt: WT[p.weekday.replace('.', '')] };
}
const tagDatum = s => new Date(s + 'T12:00:00Z');
const plusTage = (s, n) => { const d = tagDatum(s); d.setUTCDate(d.getUTCDate() + n); return d.toISOString().slice(0, 10); };
const wochentag = s => tagDatum(s).getUTCDay();
const fmtWT = new Intl.DateTimeFormat('de-DE', { timeZone: 'UTC', weekday: 'long' });
const fmtWTk = new Intl.DateTimeFormat('de-DE', { timeZone: 'UTC', weekday: 'short' });
const fmtKurz = new Intl.DateTimeFormat('de-DE', { timeZone: 'UTC', weekday: 'short', day: 'numeric', month: 'numeric' });
const fmtLang = new Intl.DateTimeFormat('de-DE', { timeZone: 'UTC', day: 'numeric', month: 'short' });
const fmtLangWT = new Intl.DateTimeFormat('de-DE', { timeZone: 'UTC', weekday: 'short', day: 'numeric', month: 'short' });
const zuMin = s => { const [h, m] = s.split(':'); return +h * 60 + +m; };
const uhr = m => { m = ((m % 1440) + 1440) % 1440; return m % 60 ? `${Math.floor(m / 60)}.${String(m % 60).padStart(2, '0')}` : String(Math.floor(m / 60)); };
const dauer = m => m < 60 ? `${m} Min.` : `${Math.floor(m / 60)} Std.${m % 60 ? ' ' + (m % 60) + ' Min.' : ''}`;
const spanne = ([a, b]) => `${uhr(zuMin(a))}–${uhr(zuMin(b))}`;
// Intervalle (Minuten ab heute 0 Uhr) aus Woche + Sonderzeiten; Ende ≤ Beginn = über Mitternacht
function zeitenFuer(D, datum) { const s = D.S.find(x => datum >= x.von && datum <= (x.bis || x.von)); return { zeiten: s ? s.zeiten : D.W[wochentag(datum)] || [], sonder: s }; }
function intervalle(D, heute) {
const liste = [];
for (let k = -1; k <= 14; k++) {
const datum = plusTage(heute, k);
zeitenFuer(D, datum).zeiten.forEach(([a, b, kue]) => {
const s = zuMin(a), e0 = zuMin(b), e = e0 <= s ? e0 + 1440 : e0;
const ku = kue ? (zuMin(kue) <= s ? zuMin(kue) + 1440 : zuMin(kue)) : null;
liste.push({ von: k * 1440 + s, bis: k * 1440 + e, kueche: ku != null ? k * 1440 + ku : null, tag: k, datum });
});
}
return liste.sort((x, y) => x.von - y.von);
}
function wann(i) {
if (!i) return 'bald wieder';
const z = `${uhr(i.von - i.tag * 1440)} Uhr`;
if (i.tag === 0) return `heute um ${z}`;
if (i.tag === 1) return `morgen um ${z}`;
if (i.tag < 7) return `${fmtWT.format(tagDatum(i.datum))} um ${z}`;
return `am ${fmtLang.format(tagDatum(i.datum))} um ${z}`;
}
function lage(D, jetzt) {
const iv = intervalle(D, jetzt.datum), m = jetzt.min;
return { iv, m, offen: iv.find(i => i.von <= m && m < i.bis), naechstes: iv.find(i => i.von > m), vorher: [...iv].reverse().find(i => i.bis <= m), heute: zeitenFuer(D, jetzt.datum) };
}
/* ---------- Simulierte Uhr (Pult) ---------- */
const PRESETS = {
karte: [['echt', 'Echtzeit (Berlin)'], ['2026-11-03T19:10', 'Di 19.10 · offen'], ['2026-11-03T22:15', 'Di 22.15 · Küche zu'], ['2026-11-03T22:40', 'Di 22.40 · schließt bald'], ['2026-11-08T15:40', 'So 15.40 · Pause'], ['2026-11-03T16:50', 'Di 16.50 · öffnet gleich'], ['2026-11-09T12:00', 'Mo 12.00 · Ruhetag'], ['2026-11-24T19:00', 'Di 24.11. · Sonderzeit']],
weiche: [['echt', 'Echtzeit (Berlin)'], ['2026-11-03T06:15', 'Di 6.15'], ['2026-11-03T07:05', 'Di 7.05'], ['2026-11-03T18:10', 'Di 18.10'], ['2026-11-07T13:40', 'Sa 13.40'], ['2026-11-08T10:30', 'So 10.30'], ['2026-11-08T13:00', 'So 13.00']],
};
let uhrWahl = 'echt';
function jetztHolen() {
if (uhrWahl === 'echt') return ortszeit(new Date());
const [d, t] = uhrWahl.split('T');
return { datum: d, min: zuMin(t), wt: wochentag(d) };
}
const uhrSel = $('[data-uhr]');
function uhrFuellen() {
const p = PRESETS[variante]; if (!p) return;
uhrSel.innerHTML = p.map(([v, t]) => `<option value="${v}">${t}</option>`).join('');
uhrSel.value = p.some(x => x[0] === uhrWahl) ? uhrWahl : 'echt'; uhrWahl = uhrSel.value;
}
uhrSel.addEventListener('change', () => { stopp(); uhrWahl = uhrSel.value; zeichneZeit(); });
/* =================== Karte: Wirtschaft Fenchel =================== */
const FENCHEL = {
// [auf, zu, Küche bis]; 0 = Sonntag
W: { 0: [['12:00', '15:00', '14:30'], ['17:30', '22:00', '21:00']], 1: [], 2: [['17:30', '23:00', '22:00']], 3: [['17:30', '23:00', '22:00']], 4: [['17:30', '23:00', '22:00']], 5: [['17:30', '00:00', '22:30']], 6: [['17:30', '00:00', '22:30']] },
S: [
{ von: '2026-11-24', grund: 'Geschlossene Gesellschaft, ab 25.11. wieder wie gewohnt', zeiten: [] },
{ von: '2026-12-24', bis: '2026-12-26', grund: 'Weihnachten', zeiten: [] },
{ von: '2026-12-31', grund: 'Silvestermenü, nur mit Reservierung', zeiten: [['18:30', '01:00', '23:00']] },
],
};
const karte = $('[data-karte]'), stKopf = $('[data-kopf]'), stZeile = $('[data-zeile]'), stBox = $('[data-status]'), tageDl = $('[data-tage]'), sonderP = $('[data-sonder]');
function karteZustand(jetzt) {
const { m, offen, naechstes, vorher, heute } = lage(FENCHEL, jetzt);
if (offen) {
const rest = offen.bis - m, zu = uhr(offen.bis);
if (rest <= 30) return { art: 'bald', kopf: 'Schließt bald', zeile: `Letzte Runde, um <b>${zu} Uhr</b> machen wir zu.` };
if (offen.kueche != null && m >= offen.kueche) return { art: 'kueche', kopf: 'Noch geöffnet', zeile: `Die Küche hat zu, die Bar ist offen bis <b>${zu} Uhr</b>.` };
const kue = offen.kueche != null ? offen.kueche - m : null;
return { art: 'offen', kopf: 'Jetzt geöffnet', zeile: kue != null && kue <= 45 ? `Küche noch <b>${dauer(kue)}</b>, Bar bis ${zu} Uhr.` : `Küche bis <b>${uhr(offen.kueche ?? offen.bis)} Uhr</b>, Bar bis ${zu} Uhr.` };
}
if (vorher && vorher.tag === 0 && naechstes && naechstes.tag === 0) return { art: 'pause', kopf: `Pause bis ${uhr(naechstes.von)} Uhr`, zeile: `Mittags war bis ${uhr(vorher.bis)} Uhr offen, abends geht es um <b>${uhr(naechstes.von)} Uhr</b> weiter.` };
if (naechstes && naechstes.tag === 0 && naechstes.von - m <= 60) return { art: 'zu', kopf: `Öffnet um ${uhr(naechstes.von)} Uhr`, zeile: `In <b>${dauer(naechstes.von - m)}</b> geht die Tür auf.` };
const kopf = heute.sonder && !heute.zeiten.length ? 'Heute geschlossen' : !heute.zeiten.length ? 'Heute Ruhetag' : 'Geschlossen';
return { art: 'zu', kopf, zeile: `Wir öffnen ${wann(naechstes)}.` };
}
function zeichneKarte() {
const jetzt = jetztHolen(), z = karteZustand(jetzt);
karte.classList.add('tickt'); void karte.offsetWidth;
stBox.dataset.art = z.art; stKopf.textContent = z.kopf; stZeile.innerHTML = z.zeile;
const zeile = (titel, datum) => { const { zeiten, sonder } = zeitenFuer(FENCHEL, datum); const t = zeiten.length ? zeiten.map(spanne).join(', ') : (sonder ? 'geschlossen' : 'Ruhetag'); return `<dt>${titel}</dt><dd>${t}${sonder ? ' *' : ''}</dd>`; };
tageDl.innerHTML = zeile(`Heute, ${fmtWT.format(tagDatum(jetzt.datum))}`, jetzt.datum) + zeile(`Morgen, ${fmtWT.format(tagDatum(plusTage(jetzt.datum, 1)))}`, plusTage(jetzt.datum, 1));
// nächste Sonderzeit in den kommenden 21 Tagen (heute eingeschlossen)
const bald = FENCHEL.S.find(s => (s.bis || s.von) >= jetzt.datum && s.von <= plusTage(jetzt.datum, 21));
sonderP.hidden = !bald;
if (bald) sonderP.textContent = `${bald.von <= jetzt.datum ? 'Heute' : fmtKurz.format(tagDatum(bald.von))}: ${bald.grund}${bald.zeiten.length ? ' (' + bald.zeiten.map(spanne).join(', ') + ')' : ''}`;
requestAnimationFrame(() => karte.classList.remove('tickt'));
werteZeigen(`Ortszeit <b>${fmtWTk.format(tagDatum(jetzt.datum))} ${uhr(jetzt.min)}</b> · Zustand <b>${z.art}</b> · Zone <b>Europe/Berlin</b>`);
}
/* =================== Weiche: Bäckerei Kornhaus =================== */
const FILIALEN = [
{ id: 'hafen', ort: 'Hafen', adr: 'Am Mittelhafen 8', extra: 'Mit Café und Frühstück bis 13 Uhr', W: { 0: [['08:00', '17:00']], 1: [['07:30', '18:00']], 2: [['07:30', '18:00']], 3: [['07:30', '18:00']], 4: [['07:30', '18:00']], 5: [['07:30', '18:00']], 6: [['07:30', '18:00']] }, S: [{ von: '2026-12-25', bis: '2026-12-26', grund: 'Weihnachten', zeiten: [] }] },
{ id: 'hiltrup', ort: 'Hiltrup', adr: 'Marktallee 41', extra: 'Parkplätze am Haus', W: { 0: [], 1: [['06:00', '18:00']], 2: [['06:00', '18:00']], 3: [['06:00', '18:00']], 4: [['06:00', '18:00']], 5: [['06:00', '18:00']], 6: [['06:00', '13:00']] }, S: [{ von: '2026-12-25', bis: '2026-12-26', grund: 'Weihnachten', zeiten: [] }] },
{ id: 'kreuzviertel', ort: 'Kreuzviertel', adr: 'Nordstraße 12', extra: 'Backstube mit Blick in den Ofen', W: { 0: [['08:00', '12:00']], 1: [['06:30', '18:30']], 2: [['06:30', '18:30']], 3: [['06:30', '18:30']], 4: [['06:30', '18:30']], 5: [['06:30', '18:30']], 6: [['07:00', '14:00']] }, S: [{ von: '2026-12-25', bis: '2026-12-26', grund: 'Weihnachten', zeiten: [] }] },
];
const wegeUl = $('[data-wege]');
wegeUl.style.setProperty('--n', FILIALEN.length);
wegeUl.innerHTML = FILIALEN.map(f => `<li><a class="weg" href="#filiale-${f.id}" aria-describedby="st-${f.id}">
<span class="weg__ort">${f.ort}<svg viewBox="0 0 20 20" aria-hidden="true"><path d="M3 10h13M11 5l5 5-5 5" fill="none" stroke="currentColor" stroke-width="1.5"/></svg></span>
<span class="weg__adr">${f.adr}</span>
<span class="weg__extra">${f.extra}</span>
<span class="status" id="st-${f.id}" data-art="zu"><span class="punkt" aria-hidden="true"></span><span data-st></span></span></a></li>`).join('');
function filialeZustand(f, jetzt) {
const { m, offen, naechstes, heute } = lage(f, jetzt);
if (offen) { const rest = offen.bis - m; return rest <= 60 ? { art: 'bald', text: `<b>Offen</b>, schließt in ${dauer(rest)}` } : { art: 'offen', text: `<b>Offen</b> bis ${uhr(offen.bis)} Uhr` }; }
if (naechstes && naechstes.tag === 0) return { art: 'zu', text: `<b>Öffnet</b> um ${uhr(naechstes.von)} Uhr` };
if (heute.sonder && !heute.zeiten.length) return { art: 'zu', text: `<b>Heute zu</b> · ${heute.sonder.grund}` };
return { art: 'zu', text: `<b>Zu</b> · öffnet ${wann(naechstes).replace(' um ', ' ')}` };
}
function zeichneWeiche() {
const jetzt = jetztHolen(); const arten = [];
FILIALEN.forEach(f => { const z = filialeZustand(f, jetzt), el = $(`#st-${f.id}`); el.dataset.art = z.art; $('[data-st]', el).innerHTML = z.text; arten.push(`${f.ort} ${z.art}`); });
werteZeigen(`Ortszeit <b>${fmtWTk.format(tagDatum(jetzt.datum))} ${uhr(jetzt.min)}</b> · ${arten.map(a => `<b>${a}</b>`).join(' · ')} · Reihenfolge <b>Alphabet</b>, gleich breit`);
}
function zeichneZeit() { if (variante === 'karte') zeichneKarte(); if (variante === 'weiche') zeichneWeiche(); }
// Echtzeit: zur nächsten vollen Minute und bei Rückkehr in den Tab neu rechnen
(function takt() { setTimeout(() => { if (uhrWahl === 'echt') zeichneZeit(); takt(); }, 60000 - Date.now() % 60000 + 50); })();
document.addEventListener('visibilitychange', () => { if (!document.hidden && uhrWahl === 'echt') zeichneZeit(); });
/* =================== Leiste: Hotel Ufermann =================== */
const form = $('[data-leiste]'), platz = $('[data-platz]'), dialog = $('[data-dialog]'), dPlatz = $('[data-dialog-platz]'), lKnopf = $('[data-leiste-knopf]');
const an = $('#anreise'), ab = $('#abreise'), code = $('#code'), meldung = $('[data-meldung]');
const gKnopf = $('[data-gaeste-knopf]'), gWahl = $('[data-gaeste]'), gText = $('#gaeste-text'), alterBox = $('[data-alter]');
const gast = { erw: 2, kind: 0, alter: [] };
const CODES = { SEESAUNA: 'Sauna-Abend am Steg für zwei inklusive' };
const PREIS = { woche: 128, wochenende: 158, person: 35 }; // Beispielwerte je Nacht, inkl. Frühstück
const KURTAXE = 2.0; // Beispielwert je Person und Nacht
const eur = new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR', maximumFractionDigits: 0 });
const eur2 = new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' });
const heuteBerlin = () => ortszeit(new Date()).datum;
an.min = heuteBerlin(); ab.min = plusTage(heuteBerlin(), 1);
function datumText() { [an, ab].forEach(i => { const t = $(`[data-datum-text="${i.id}"]`); t.textContent = i.value ? fmtLangWT.format(tagDatum(i.value)) : 'Datum wählen'; t.classList.toggle('leer', !i.value); }); }
an.addEventListener('change', () => { if (an.value) { ab.min = plusTage(an.value, 1); if (!ab.value || ab.value <= an.value) ab.value = plusTage(an.value, wochentag(an.value) >= 5 ? 2 : 1); } zusammenfassen(); feldOk(an); feldOk(ab); meldungWeg(); });
ab.addEventListener('change', () => { zusammenfassen(); feldOk(ab); meldungWeg(); });
code.addEventListener('input', () => { feldOk(code); });
function gaesteText() { return `${gast.erw === 1 ? '1 Erwachsener' : gast.erw + ' Erwachsene'}${gast.kind ? `, ${gast.kind} Kind${gast.kind > 1 ? 'er' : ''}` : ''}`; }
function gaesteZeichnen() {
$('[data-zahl="erw"]').textContent = gast.erw; $('[data-zahl="kind"]').textContent = gast.kind;
const summe = gast.erw + gast.kind;
$$('[data-schritt]').forEach(b => { const art = b.dataset.schritt, d = +b.dataset.d; b.disabled = d < 0 ? gast[art] <= (art === 'erw' ? 1 : 0) : summe >= 4 || (art === 'kind' && gast.kind >= 3); });
gast.alter.length = gast.kind;
alterBox.innerHTML = gast.alter.map((a, i) => `<label>Alter Kind ${i + 1}<select data-alter-i="${i}" ${a == null ? 'aria-invalid="false"' : ''}><option value="">wählen</option>${Array.from({ length: 14 }, (_, k) => `<option value="${k}" ${a === k ? 'selected' : ''}>${k} Jahre</option>`).join('')}</select></label>`).join('');
gText.textContent = gaesteText(); zusammenfassen();
}
alterBox.addEventListener('change', e => { const s = e.target.closest('select'); if (!s) return; gast.alter[+s.dataset.alterI] = s.value === '' ? null : +s.value; s.setAttribute('aria-invalid', 'false'); });
$$('[data-schritt]').forEach(b => b.addEventListener('click', () => { gast[b.dataset.schritt] += +b.dataset.d; if (b.dataset.schritt === 'kind' && +b.dataset.d > 0) gast.alter.push(null); gaesteZeichnen(); feldOk($('[data-feld="gaeste"]')); }));
function gaesteAuf(auf) {
gWahl.classList.toggle('offen', auf); gKnopf.setAttribute('aria-expanded', String(auf)); gWahl.inert = !auf;
if (auf) setTimeout(() => $('[data-schritt]:not(:disabled)', gWahl)?.focus({ preventScroll: true }), 20);
}
gKnopf.addEventListener('click', () => gaesteAuf(!gWahl.classList.contains('offen')));
$('[data-gaeste-fertig]').addEventListener('click', () => { gaesteAuf(false); gKnopf.focus(); });
gWahl.addEventListener('keydown', e => { if (e.key === 'Escape') { e.stopPropagation(); e.preventDefault(); gaesteAuf(false); gKnopf.focus(); } });
document.addEventListener('pointerdown', e => { if (gWahl.classList.contains('offen') && !e.target.closest('[data-feld="gaeste"]')) gaesteAuf(false); });
const fmtDatum = s => fmtKurz.format(tagDatum(s)).replace(',', '');
function zusammenfassen() {
datumText();
const g = gast.erw + gast.kind;
$('[data-zusammen]').textContent = `${an.value ? fmtDatum(an.value) + (ab.value ? ' – ' + fmtDatum(ab.value) : '') : 'Datum wählen'} · ${g} Gäste`;
}
const naechte = () => (tagDatum(ab.value) - tagDatum(an.value)) / 86400000;
function feldFehler(el, an) { const f = el.closest('.feld') || el; f.toggleAttribute('data-fehler', an); const i = f.querySelector('input, .feld__knopf'); if (i) i.setAttribute('aria-invalid', String(an)); }
function feldOk(el) { feldFehler(el, false); }
function meldungWeg() { meldung.hidden = true; meldung.innerHTML = ''; }
function pruefen() {
const fehler = [];
[an, ab, code, gKnopf].forEach(feldOk);
const heute = heuteBerlin();
if (!an.value) fehler.push([an, 'Bitte die Anreise wählen.']);
else if (an.value < heute) fehler.push([an, 'Die Anreise liegt in der Vergangenheit.']);
if (!ab.value) fehler.push([ab, 'Bitte die Abreise wählen.']);
else if (an.value && ab.value <= an.value) fehler.push([ab, 'Die Abreise muss nach der Anreise liegen.']);
if (an.value && ab.value > an.value) {
const n = naechte(), w = wochentag(an.value);
if ((w === 5 || w === 6) && n < 2) fehler.push([ab, 'Bei Anreise am Freitag oder Samstag bleiben Sie bitte mindestens 2 Nächte.']);
if (n > 21) fehler.push([ab, 'Für mehr als 21 Nächte schreiben Sie uns bitte eine Anfrage.']);
}
if (gast.alter.some(a => a == null)) { fehler.push([gKnopf, 'Bitte das Alter jedes Kindes angeben (für Bett und Frühstückspreis).']); $$('[data-alter-i]').forEach(s => { if (s.value === '') s.setAttribute('aria-invalid', 'true'); }); }
const c = code.value.trim().toUpperCase();
if (c && !CODES[c]) fehler.push([code, `Den Code „${code.value.trim()}“ kennen wir nicht. Bitte prüfen oder leer lassen.`]);
return fehler;
}
let geprueft = null;
form.addEventListener('submit', e => {
e.preventDefault(); gaesteAuf(false);
if (geprueft && e.submitter?.hasAttribute('data-weiter')) return uebergeben();
const fehler = pruefen();
meldung.hidden = false;
if (fehler.length) {
geprueft = null; meldung.dataset.art = 'fehler'; meldung.setAttribute('role', 'alert');
fehler.forEach(([el]) => feldFehler(el, true));
meldung.innerHTML = `<strong>${fehler.length === 1 ? 'Ein Feld' : fehler.length + ' Felder'} bitte prüfen:</strong><ul>${fehler.map(([el, t]) => `<li><a href="#${el.id || 'gaeste-titel'}" data-zu="${el.id || ''}">${t}</a></li>`).join('')}</ul>`;
return;
}
const n = naechte(); let summe = 0;
for (let k = 0; k < n; k++) { const w = wochentag(plusTage(an.value, k)); summe += (w === 5 || w === 6 ? PREIS.wochenende : PREIS.woche) + Math.max(0, gast.erw + gast.kind - 2) * PREIS.person; }
const kt = KURTAXE * gast.erw * n, c = code.value.trim().toUpperCase();
geprueft = { n, summe, kt, c };
meldung.dataset.art = 'ok'; meldung.setAttribute('role', 'status');
meldung.innerHTML = `<div class="ergebnis"><p class="ergebnis__zahl">${n} ${n === 1 ? 'Nacht' : 'Nächte'} · ${gaesteText()} · ab ${eur.format(summe)} gesamt
<small>inkl. Frühstück und MwSt., zzgl. Kurabgabe ${eur2.format(kt)} (${eur2.format(KURTAXE)} je Erwachsenem und Nacht)${c ? ' · ' + CODES[c] : ''} · Beispielpreise</small></p>
<button class="knopf-haus" type="submit" data-weiter>Weiter zur Buchung <svg viewBox="0 0 14 14" aria-hidden="true"><path d="M1 7h11M8 3l4 4-4 4" fill="none" stroke="currentColor" stroke-width="1.5"/></svg></button></div>`;
});
meldung.addEventListener('click', e => { const a = e.target.closest('[data-zu]'); if (!a) return; e.preventDefault(); const el = a.dataset.zu ? $('#' + a.dataset.zu) : gKnopf; el.focus(); });
function uebergeben() {
const p = new URLSearchParams({ arrival: an.value, departure: ab.value, adults: gast.erw, children: gast.kind, lang: 'de' });
if (gast.kind) p.set('childAges', gast.alter.join(','));
if (geprueft.c) p.set('promo', geprueft.c);
const box = document.createElement('p'); box.className = 'uebergabe';
box.textContent = `Übergabe an das Buchungssystem (Demo, kein Abruf): https://buchen.example/ufermann?${p}`;
meldung.querySelector('.uebergabe')?.remove(); meldung.append(box);
}
function leeren() { form.reset(); gast.erw = 2; gast.kind = 0; gast.alter = []; gaesteZeichnen(); [an, ab, code, gKnopf].forEach(feldOk); meldungWeg(); geprueft = null; an.min = heuteBerlin(); ab.min = plusTage(heuteBerlin(), 1); }
$('[data-leeren]').addEventListener('click', () => { stopp(); leeren(); });
$('[data-beispiel]').addEventListener('click', () => { stopp(); beispiel(4); });
function naechsterWT(w, abTag = 3) { let d = plusTage(heuteBerlin(), abTag); while (wochentag(d) !== w) d = plusTage(d, 1); return d; }
function beispiel(wt, n = 3) { const a = naechsterWT(wt); an.value = a; ab.value = plusTage(a, n); zusammenfassen(); meldungWeg(); [an, ab].forEach(feldOk); }
// Telefon: Formular im Dialog
const schmal = matchMedia('(max-width: 40rem)');
lKnopf.addEventListener('click', () => { dPlatz.append(form); dialog.showModal(); setTimeout(() => an.focus(), 30); });
$('[data-schliessen]').addEventListener('click', () => dialog.close());
dialog.addEventListener('close', () => { gaesteAuf(false); platz.append(form); lKnopf.focus({ preventScroll: true }); });
dialog.addEventListener('click', e => { if (e.target === dialog) dialog.close(); });
schmal.addEventListener?.('change', () => { if (!schmal.matches && dialog.open) dialog.close(); });
/* ---------- Varianten ---------- */
const welten = { leiste: $('.hotel'), karte: $('.gast'), weiche: $('.baeckerei') };
function setzeVariante(v, url = true) {
variante = v;
Object.entries(welten).forEach(([k, el]) => { el.hidden = k !== v; });
$$('[data-v]').forEach(b => b.setAttribute('aria-pressed', String(b.dataset.v === v)));
$$('[data-nur]').forEach(el => { el.hidden = !el.dataset.nur.split(' ').includes(v); });
if (url) { const q = new URLSearchParams(location.search); q.set('v', v); history.replaceState(null, '', '?' + q.toString() + location.hash); }
uhrFuellen(); zeichneZeit();
if (v === 'leiste') werteZeigen(`Rand <b>40 px</b> auf Holz #6E5A47 · Leiste <b>10 von 12 Spalten</b> · UI <b>300 ms auf, 200 ms zu</b> · unter <b>40rem</b> Dialog`);
auftritt();
}
$$('[data-v]').forEach(b => b.addEventListener('click', () => { stopp(); setzeVariante(b.dataset.v); }));
const werte = $('[data-werte]');
function werteZeigen(t) { werte.innerHTML = t; }
function auftritt() {
const w = welten[variante];
if (reduziert()) { w.classList.remove('auftritt'); return; }
w.classList.add('auftritt'); void w.offsetWidth;
requestAnimationFrame(() => requestAnimationFrame(() => w.classList.remove('auftritt')));
}
/* ---------- Vorführung ---------- */
let schritte = [], ti = 0, timer = 0, pausiert = false;
const warte = ms => new Promise(r => { timer = setTimeout(r, ms); });
function stopp() { clearTimeout(timer); ti++; $$('.weg.vorgefuehrt').forEach(w => w.classList.remove('vorgefuehrt')); }
async function folge(id) {
if (variante === 'leiste') {
leeren(); auftritt(); await warte(1400); if (id !== ti) return;
beispiel(5, 1); await warte(1100); if (id !== ti) return; // Freitag, eine Nacht
form.requestSubmit($('[data-pruefen]')); await warte(2300); if (id !== ti) return; // Fehler: Mindestaufenthalt
ab.value = plusTage(an.value, 2); feldOk(ab); meldungWeg(); zusammenfassen();
gaesteAuf(true); await warte(500); if (id !== ti) return;
gast.kind = 1; gast.alter = [6]; gaesteZeichnen(); await warte(900); if (id !== ti) return;
gaesteAuf(false); code.value = 'SEESAUNA'; await warte(700); if (id !== ti) return;
form.requestSubmit($('[data-pruefen]')); await warte(2200); if (id !== ti) return;
const w = $('[data-weiter]'); if (w) form.requestSubmit(w); await warte(2600); if (id !== ti) return;
} else {
const p = PRESETS[variante].slice(1);
auftritt(); await warte(900);
for (let k = 0; k < p.length; k++) {
if (id !== ti) return;
uhrWahl = p[k][0]; uhrSel.value = uhrWahl; zeichneZeit();
if (variante === 'weiche') { $$('.weg').forEach((w, i) => w.classList.toggle('vorgefuehrt', i === k % FILIALEN.length)); }
await warte(1900);
}
$$('.weg.vorgefuehrt').forEach(w => w.classList.remove('vorgefuehrt'));
}
if (id === ti && !pausiert) folge(id);
}
function vorfuehren() { stopp(); pausiert = false; folge(ti); }
function neustart() { stopp(); if (variante === 'leiste') leeren(); else { uhrWahl = 'echt'; uhrSel.value = 'echt'; zeichneZeit(); } auftritt(); }
function pause() { pausiert = true; stopp(); }
function weiter() { pausiert = false; folge(ti); }
gaesteZeichnen();
setzeVariante(variante, false);
})();
</script>
</body>
</html>