Buchungsleiste mit Datumsbereich
Anreise und Abreise in einem Kalender mit Belegung und Mindestaufenthalt, Gäste mit Kinderalter, am Desktop als verankertes Popover, auf dem Telefon als Bottom-Sheet; Übergabe an die Engine erst nach zweitem Klick.
Startseite von Hotel, Pension oder Ferienwohnungen: die Leiste liegt halb über dem Hero, der Kalender öffnet nach oben, wenn unten kein Platz ist.
Die Leiste direkt unter dem Hero fragt nur, was die Booking-Engine braucht: Anreise und Abreise in einem Kalender, die Zahl der Nächte, Erwachsene und Kinder mit Alter. Belegte Tage und Mindestaufenthalt stehen schon im Kalender. Am Desktop öffnen Kalender und Gäste als Popover, per Anchor Positioning an der Leiste verankert. Auf dem Telefon wird die Leiste zur festen unteren Zeile, die ein Bottom-Sheet (dialog) öffnet. Die Daten gehen erst nach einem zweiten Klick mit Datenschutz-Hinweis an die Booking-Engine, als Parameter in der URL.
Die Website ist das Foyer, die Buchungsleiste die Rezeption. Wer schon weiß, wann er kommt, will Verfügbarkeit sehen, ohne erst die fremde Buchungsseite zu laden. Ein eigener Kalender passt zur Marke, zeigt Regeln früh und lädt keine fremden Skripte, bevor der Gast es will.
Wann einsetzen
Hotel, Pension, Ferienwohnung, Campingplatz; abgewandelt für Tischreservierung (ein Datum, Uhrzeit, Personen), Kursbuchung oder Verleih.
Wann nicht
Wenn die Booking-Engine ohne URL-Parameter arbeitet und alles im eigenen Widget abfragt: dann reicht ein klarer Knopf „Zimmer buchen“. Bei Häusern mit nur einer Wohnung und wenigen Anfragen ist ein Anfrageformular ehrlicher.
Varianten
- Leiste unter dem Hero, mobil feste untere Zeile mit Bottom-Sheet
- Buchungskarte auf der Zimmerseite, sticky neben Fotos und Ausstattung, mit Ab-Preis pro Nacht
- Tischreservierung: ein Datum, Uhrzeit in Slots, Personen
- Ohne JS: native date-Inputs und Selects im selben Formular
Umsetzung
Grundlage ist ein echtes form method=get mit action = Engine-URL und den Feldern arrival, departure (type=date, required), adults, children und childAges; ohne JS funktioniert es so. JS setzt noValidate, blendet die nativen Felder aus und zeigt Knöpfe, die einen eigenen Kalender öffnen. Kalender: zwei Tabellen mit role=grid, Zellen mit data-d und Roving tabindex; erster Klick Anreise, zweiter Abreise, beim Zeigen eine gestrichelte Bereichsvorschau. Regeln aus der Engine (belegte Nächte, Mindestaufenthalt je Anreisetag) sperren Tage mit Grund im aria-label. Desktop: popover mit position-anchor an der Leiste und position-try-fallbacks (--oben, --rechts, --rechts-oben) plus position-try-order: most-height; geöffnet per showPopover({ source }). Ohne Anchor Positioning rechnet JS die Lage aus getBoundingClientRect. Telefon: dieselben Knoten (Kalender, Gäste) wandern in ein dialog-Bottom-Sheet (showModal, closedby=any). Übergabe in zwei Schritten: Klick 1 prüft (Daten fehlen → Kalender öffnet mit Hinweis; Kinderalter fehlt → Gäste öffnen mit Fehler) und zeigt Zusammenfassung und Datenschutz-Hinweis; erst Klick 2 baut die URL und öffnet die Engine (window.open oder iFrame-src erst jetzt). Parameternamen hängen von der Engine ab, bei Mews z. B. mewsStart, mewsEnd, mewsAdultCount, mewsChildCount. In der Demo zeigt ein Dialog die URL statt sie zu öffnen.
Robust umsetzen
Telefon. Feste untere Zeile mit Zusammenfassung und „Prüfen“. Bottom-Sheet mit zwei Monaten untereinander, Tagen in 44 px, Gästen und festem Fuß mit Nächten und Weiter-Knopf.
Ohne Bewegung. Popover und Sheet erscheinen ohne Weg, nur mit kurzem Einblenden oder sofort; die Bereichsvorschau springt.
Barrierefrei. Kalender nach dem APG-Beispiel Date Picker Dialog: Pfeiltasten (Tag, Woche), Bild auf/ab (Monat), Pos1/Ende (Woche), Enter/Leertaste wählt; nach der Abreise springt der Fokus auf „Übernehmen“, Esc schließt und gibt den Fokus an das Feld zurück. Jeder Tag trägt den vollen Namen, gesperrte Tage nennen den Grund („nicht wählbar: Mindestaufenthalt 2 Nächte“, „ausgebuchte Nacht dazwischen“), der Hinweis über dem Kalender ist aria-live. Wochentage mit abbr. Stepper-Knöpfe mit Namen („Kinder: eines mehr“), Zahl als output mit aria-live, Plus gesperrt bei voller Belegung mit Erklärung. Fehlendes Kinderalter: Feld markiert, Fokus springt hin, Fehlertext. Datenschutz-Hinweis zur Weitergabe und Preisangaben vom Kunden prüfen lassen.
Code
<!DOCTYPE html>
<!--
Buchungsleiste mit Datumsbereich
Fundus · Sektionen & Funktionen · https://cecodesign.de/lab/fundus/sektionen/buchungsleiste-datumsbereich/
Eigenständige Datei. Bilder liegen hier unter bilder/ – eigene einsetzen.
-->
<html lang="de" data-v="leiste">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Buchungsleiste mit Datumsbereich · 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-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; }
:root {
/* Haus Seeweite · Rezept boutique-hotel-gastgeber: Sand, Creme, warmes Dunkel, Holz als Hauston */
--grund: #f3eee7; --creme: #fef2e3; --karte: #fffaf3; --text: #3e3a37; --leise: #6b6d68; --linie: #d5d1c8; --band: #e8ddcf; --haus: #6e5a47; --haus-tief: #5a4939;
/* token-toggle-asymmetrisch: auf 300 ms mit Ausklang, zu 200 ms; Auswahl 150 ms */
--auf: cubic-bezier(0.16, 1, 0.3, 1); --zu: cubic-bezier(0.4, 0, 0.2, 1);
--t-auf: 300ms; --t-zu: 200ms; --t-ui: 150ms;
--bild: cubic-bezier(0.37, 0, 0.63, 1); /* token-luxus-weich, nur Bilder */
--rand: clamp(18px, 3.6vw, 56px);
}
* { box-sizing: border-box; margin: 0; }
html { background: var(--grund); color: var(--text); font-family: 'Mona Sans', system-ui, sans-serif; font-stretch: 100%; -webkit-font-smoothing: antialiased; }
body { min-height: 100svh; font-size: 1.0625rem; line-height: 1.6; padding-bottom: 72px; }
button, input, select { font: inherit; color: inherit; }
a { color: inherit; }
.sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
.label { font-size: .75rem; font-weight: 560; font-stretch: 118%; letter-spacing: .06em; text-transform: uppercase; }
:focus-visible { outline: 2px solid var(--haus); outline-offset: 3px; }
[data-v="leiste"] [data-nur="zimmer"], [data-v="zimmer"] [data-nur="leiste"] { display: none !important; }
/* ---------- Kopf ---------- */
.kopf { position: absolute; inset: 0 0 auto; z-index: 4; display: flex; align-items: center; justify-content: space-between; gap: 24px; padding: 22px var(--rand); color: #fef2e3; }
[data-v="zimmer"] .kopf { position: relative; color: var(--text); border-bottom: 1px solid var(--linie); }
.marke { font: italic 400 1.75rem/1 'Newsreader', serif; font-variation-settings: "opsz" 72; letter-spacing: -.01em; text-decoration: none; }
.kopf nav ul { list-style: none; padding: 0; display: flex; gap: clamp(16px, 2.4vw, 36px); }
.kopf nav a { text-decoration: none; }
.kopf nav a[aria-current] { text-decoration: underline; text-underline-offset: 6px; text-decoration-thickness: 1px; }
.kopf__rechts { display: flex; gap: 18px; align-items: center; }
.sprache { display: flex; gap: 6px; }
.sprache a { text-decoration: none; opacity: .65; }
.sprache a[aria-current] { opacity: 1; }
/* ---------- Hero ---------- */
.held { position: relative; height: max(520px, 74svh); overflow: hidden; background: #5f6470; }
.held img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; object-position: 50% 62%; scale: 1.06; animation: zurueck 1200ms var(--bild) forwards; }
@keyframes zurueck { to { scale: 1; } }
.held::after { content: ""; position: absolute; inset: 0; background: linear-gradient(180deg, rgba(30, 28, 26, .38), transparent 26%, transparent 52%, rgba(30, 28, 26, .48)); }
.held__text { position: absolute; z-index: 2; left: var(--rand); right: var(--rand); bottom: clamp(84px, 13vh, 132px); color: #fef2e3; }
.held__text .label { opacity: .85; margin-bottom: 12px; }
.held h1 { font: 300 clamp(2.5rem, 1.5rem + 4vw, 5.5rem)/1.04 'Newsreader', serif; font-variation-settings: "opsz" 72; letter-spacing: -.015em; max-width: 13ch; text-wrap: balance; }
.held h1 em { font-weight: 300; }
/* ---------- Buchungsleiste ---------- */
.ort-leiste { position: relative; z-index: 3; margin: -46px auto 0; padding: 0 var(--rand); max-width: calc(1180px + 2 * var(--rand)); }
.leiste { display: grid; grid-template-columns: minmax(0, 2.2fr) minmax(0, 1.1fr) auto; align-items: stretch; min-height: 92px; background: var(--creme); border-radius: 6px; box-shadow: 0 1px 2px rgba(62, 58, 55, .08), 0 18px 50px rgba(62, 58, 55, .14); }
.feld { position: relative; display: flex; align-items: stretch; min-width: 0; }
.feld--datum { anchor-name: --datum; }
.feld--gaeste { anchor-name: --gaeste; border-left: 1px solid var(--linie); }
.feld__knopf { flex: 1; min-width: 0; display: grid; align-content: center; gap: 4px; padding: 14px 26px; border: 0; background: none; text-align: left; cursor: pointer; transition: background-color var(--t-ui) var(--zu); }
.feld__knopf:hover, .feld__knopf[aria-expanded="true"] { background: rgba(110, 90, 71, .07); }
.feld__knopf .label { color: var(--leise); }
.feld__wert { font: 400 1.375rem/1.2 'Newsreader', serif; font-variation-settings: "opsz" 24; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.feld__wert.leer { color: var(--leise); font-style: italic; }
.naechte { align-self: center; flex: none; display: grid; place-items: center; min-width: 74px; height: 34px; padding: 0 12px; border-radius: 999px; border: 1px solid var(--linie); font-size: .8125rem; font-weight: 520; font-variant-numeric: tabular-nums; color: var(--leise); transition: background-color var(--t-ui) var(--zu), color var(--t-ui) var(--zu); }
.naechte.voll { background: var(--text); border-color: var(--text); color: var(--creme); }
.los { anchor-name: --los; display: inline-flex; align-items: center; justify-content: center; gap: 10px; margin: 12px; padding: 0 30px; min-height: 56px; border: 0; border-radius: 4px; background: var(--haus); color: var(--creme); font-size: .8125rem; font-weight: 600; font-stretch: 118%; letter-spacing: .07em; text-transform: uppercase; cursor: pointer; white-space: nowrap; transition: background-color var(--t-ui) var(--zu); }
.los:hover { background: var(--haus-tief); }
.los svg { width: 16px; height: 16px; }
.mobil-zeile { display: none; }
.vorteile { display: flex; justify-content: center; flex-wrap: wrap; gap: 8px 32px; margin: 22px auto 0; padding: 0 var(--rand); font-size: .875rem; color: var(--leise); list-style: none; }
.vorteile li { display: flex; align-items: center; gap: 8px; }
.vorteile svg { width: 14px; height: 14px; color: var(--haus); flex: none; }
/* Ohne JS: native Felder im selben Formular (Pult „Ohne JS“ zeigt sie) */
.nativ { display: none; }
.ohne-js .feld, .ohne-js .mobil-zeile { display: none !important; }
.ohne-js .nativ { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 12px; padding: 14px 18px; align-items: end; }
.nativ label { display: grid; gap: 4px; font-size: .75rem; font-weight: 560; letter-spacing: .06em; text-transform: uppercase; color: var(--leise); }
.nativ input, .nativ select { height: 44px; padding: 0 10px; border: 1px solid var(--linie); border-radius: 4px; background: #fff; color: var(--text); font-size: 1rem; text-transform: none; letter-spacing: 0; }
.ohne-js .leiste { grid-template-columns: minmax(0, 1fr) auto; }
/* ---------- Popover (Desktop): verankert, asymmetrisches Tempo ---------- */
.pop { margin: 0; padding: 0; border: 1px solid var(--linie); border-radius: 8px; background: var(--karte); color: var(--text); box-shadow: 0 24px 70px rgba(40, 34, 28, .22); overflow: visible;
inset: auto; opacity: 0; translate: 0 8px;
transition: opacity var(--t-zu) var(--zu), translate var(--t-zu) var(--zu), overlay var(--t-zu) var(--zu) allow-discrete, display var(--t-zu) var(--zu) allow-discrete; }
.pop:popover-open { opacity: 1; translate: 0 0; transition-duration: var(--t-auf); transition-timing-function: var(--auf); }
@starting-style { .pop:popover-open { opacity: 0; translate: 0 8px; } }
.pop--kal { position-anchor: --datum; top: anchor(bottom); left: anchor(left); margin-top: 10px; width: 764px; max-width: calc(100vw - 24px); position-try-fallbacks: --oben, --rechts, --rechts-oben; position-try-order: most-height; }
.pop--gaeste { position-anchor: --gaeste; top: anchor(bottom); left: anchor(left); margin-top: 10px; width: 360px; position-try-fallbacks: --oben, --rechts, --rechts-oben; }
.pop--ueber { position-anchor: --los; top: anchor(bottom); right: anchor(right); margin-top: 10px; width: 400px; max-width: calc(100vw - 24px); position-try-fallbacks: --oben-r; }
@position-try --oben { top: auto; bottom: anchor(top); margin: 0 0 10px; }
@position-try --rechts { left: auto; right: anchor(right); }
@position-try --rechts-oben { left: auto; right: anchor(right); top: auto; bottom: anchor(top); margin: 0 0 10px; }
@position-try --oben-r { top: auto; bottom: anchor(top); margin: 0 0 10px; }
.pop.ohne-anker { position: fixed; }
/* ---------- Kalender ---------- */
.kal { display: grid; gap: 10px; padding: 16px 20px 14px; }
.kal__kopf { display: grid; grid-template-columns: 36px 1fr 36px; align-items: center; gap: 10px; }
.kal__nav { width: 36px; height: 36px; border-radius: 50%; border: 1px solid var(--linie); background: none; cursor: pointer; display: grid; place-items: center; }
.kal__nav:disabled { opacity: .3; cursor: default; }
.kal__nav svg { width: 14px; height: 14px; }
.kal__hinweis { text-align: center; font-size: .9375rem; min-height: 1.6em; }
.kal__hinweis b { font-weight: 600; }
.kal__hinweis.fehler { color: #8c3b21; }
.kal__monate { display: grid; grid-template-columns: 1fr 1fr; gap: 28px; }
.monat { width: 100%; border-collapse: separate; border-spacing: 0 3px; table-layout: fixed; }
.monat caption { font: 400 1.1875rem/1.2 'Newsreader', serif; font-variation-settings: "opsz" 24; padding-bottom: 4px; text-align: left; }
.monat th { font-size: .6875rem; font-weight: 560; font-stretch: 118%; letter-spacing: .06em; text-transform: uppercase; color: var(--leise); padding-bottom: 4px; }
.monat td { position: relative; height: 38px; padding: 0; text-align: center; font-size: .9375rem; font-variant-numeric: tabular-nums; cursor: pointer; outline: none; }
.monat td:empty { cursor: default; }
.monat td span { position: relative; z-index: 1; display: grid; place-items: center; width: 36px; height: 36px; margin: 0 auto; border-radius: 50%; transition: background-color var(--t-ui) var(--zu), color var(--t-ui) var(--zu); }
.monat td:not(:empty):not([aria-disabled="true"]):hover span { box-shadow: inset 0 0 0 1px var(--text); }
.monat td:focus-visible span { box-shadow: 0 0 0 2px var(--karte), 0 0 0 4px var(--haus); }
.monat td.heute span::after { content: ""; position: absolute; bottom: 6px; left: 50%; width: 4px; height: 4px; margin-left: -2px; border-radius: 50%; background: var(--haus); }
.monat td.vergangen { color: #b7b3ab; cursor: default; }
.monat td[aria-disabled="true"] { cursor: not-allowed; }
/* ausgebuchte Nacht: durchgestrichen und schraffiert (als Abreise trotzdem wählbar) */
.monat td.belegt span { color: #a19b92; text-decoration: line-through; background: repeating-linear-gradient(135deg, transparent 0 4px, rgba(130, 120, 108, .16) 4px 5px); }
/* Mindestaufenthalt oder belegte Nacht dazwischen: als Abreise gesperrt */
.monat td.gesperrt { cursor: not-allowed; }
.monat td.gesperrt span { color: #b7b3ab; }
.monat td.min span { text-decoration: underline dotted; text-underline-offset: 4px; }
/* Bereich: Band hinter den Tagen, Anfang und Ende gefüllt */
.monat td.band::before { content: ""; position: absolute; top: 1px; bottom: 1px; left: 0; right: 0; background: var(--band); }
.monat td.start::before { left: 50%; }
.monat td.ende::before { right: 50%; }
.monat td.vorschau::before { background: transparent; border-block: 1px dashed #b9ab98; }
.monat td.start span, .monat td.ende span { background: var(--text); color: var(--creme); text-decoration: none; }
.kal__fuss { display: flex; align-items: center; gap: 10px 18px; flex-wrap: wrap; padding-top: 12px; border-top: 1px solid var(--linie); }
.legende { list-style: none; padding: 0; display: flex; gap: 14px; font-size: .8125rem; color: var(--leise); margin-right: auto; }
.legende li { display: flex; align-items: center; gap: 7px; }
.legende i { width: 18px; height: 18px; border-radius: 50%; display: inline-block; }
.legende .l-belegt { background: repeating-linear-gradient(135deg, transparent 0 3px, rgba(130, 120, 108, .35) 3px 4px); border: 1px solid #d5d1c8; }
.legende .l-min { border: 1px dotted #8f8a82; }
.summe { font-size: .9375rem; font-variant-numeric: tabular-nums; }
.summe b { font: 400 1.25rem/1 'Newsreader', serif; }
.knopf-text { border: 0; background: none; padding: 8px 4px; text-decoration: underline; text-underline-offset: 4px; cursor: pointer; font-size: .9375rem; }
.knopf-voll { height: 44px; padding: 0 20px; border: 0; border-radius: 4px; background: var(--text); color: var(--creme); font-size: .8125rem; font-weight: 600; font-stretch: 118%; letter-spacing: .07em; text-transform: uppercase; cursor: pointer; }
.knopf-voll:disabled { opacity: .35; cursor: not-allowed; }
/* ---------- Gäste ---------- */
.gaeste { display: grid; gap: 4px; padding: 18px 22px 18px; }
.zaehler { display: grid; grid-template-columns: 1fr auto; align-items: center; gap: 12px; padding: 12px 0; border-bottom: 1px solid var(--linie); }
.zaehler b { display: block; font-weight: 560; }
.zaehler small { color: var(--leise); font-size: .8125rem; }
.stepper { display: flex; align-items: center; gap: 4px; }
.stepper button { width: 40px; height: 40px; border-radius: 50%; border: 1px solid var(--linie); background: none; cursor: pointer; font-size: 1.25rem; line-height: 1; display: grid; place-items: center; transition: border-color var(--t-ui) var(--zu); }
.stepper button:hover:not(:disabled) { border-color: var(--text); }
.stepper button:disabled { opacity: .3; cursor: not-allowed; }
.stepper output { min-width: 2ch; text-align: center; font: 400 1.375rem/1 'Newsreader', serif; font-variant-numeric: tabular-nums; }
.alter { display: grid; grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); gap: 8px; padding: 12px 0 4px; }
.alter:empty { display: none; }
.alter label { display: grid; gap: 3px; font-size: .75rem; color: var(--leise); }
.alter select { height: 40px; padding: 0 8px; border: 1px solid var(--linie); border-radius: 4px; background: #fff; font-size: .9375rem; color: var(--text); }
.alter select.fehlt { border-color: #8c3b21; box-shadow: 0 0 0 1px #8c3b21; }
.gaeste__hinweis { font-size: .8125rem; color: var(--leise); min-height: 1.5em; padding-top: 8px; }
.gaeste__hinweis.fehler { color: #8c3b21; }
.gaeste__fuss { display: flex; justify-content: flex-end; padding-top: 8px; }
/* ---------- Klick 1: Zusammenfassung mit Datenschutz-Hinweis ---------- */
.ueber { padding: 22px 24px 20px; display: grid; gap: 14px; }
.ueber h2 { font: 400 1.5rem/1.15 'Newsreader', serif; font-variation-settings: "opsz" 24; }
.ueber dl { display: grid; grid-template-columns: auto 1fr; gap: 6px 16px; font-size: .9375rem; padding: 12px 0; border-block: 1px solid var(--linie); }
.ueber dt { color: var(--leise); }
.ueber dd { font-variant-numeric: tabular-nums; }
.ueber__daten { font-size: .8125rem; color: var(--leise); line-height: 1.5; }
.ueber__daten a { color: var(--text); }
.ueber__knoepfe { display: flex; gap: 12px; align-items: center; justify-content: flex-end; flex-wrap: wrap; }
.ueber__knoepfe .los { margin: 0; min-height: 48px; padding: 0 22px; }
/* ---------- Klick 2: Übergabe an die Booking-Engine ---------- */
.engine { width: min(620px, calc(100vw - 24px)); margin: auto; padding: 0; border: 0; border-radius: 10px; background: var(--karte); color: var(--text); box-shadow: 0 30px 90px rgba(30, 26, 22, .35); opacity: 0; scale: .98;
transition: opacity var(--t-zu) var(--zu), scale var(--t-zu) var(--zu), overlay var(--t-zu) allow-discrete, display var(--t-zu) allow-discrete; }
.engine[open] { opacity: 1; scale: 1; transition-duration: var(--t-auf); transition-timing-function: var(--auf); }
@starting-style { .engine[open] { opacity: 0; scale: .98; } }
.engine::backdrop { background: rgba(30, 28, 26, .5); }
.engine__kopf { display: flex; justify-content: space-between; align-items: center; gap: 12px; padding: 14px 18px; border-bottom: 1px solid var(--linie); font-size: .875rem; }
.engine__kopf b { font-weight: 650; letter-spacing: .02em; }
.engine__inhalt { padding: 22px; display: grid; gap: 16px; }
.engine__balken { height: 3px; border-radius: 3px; background: var(--band); overflow: hidden; }
.engine__balken i { display: block; height: 100%; width: 0; background: var(--haus); transition: width 900ms var(--zu); }
.engine code { display: block; padding: 12px 14px; border-radius: 6px; background: #2d2a27; color: #f3eee7; font: 400 .8125rem/1.55 ui-monospace, SFMono-Regular, Menlo, monospace; word-break: break-all; }
.engine code mark { background: transparent; color: #f2c88d; }
.engine table { width: 100%; border-collapse: collapse; font-size: .875rem; }
.engine td { padding: 6px 0; border-bottom: 1px solid var(--linie); }
.engine td:first-child { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: .8125rem; color: var(--leise); width: 40%; }
.engine p { font-size: .9375rem; }
.engine__zu { width: 32px; height: 32px; border-radius: 50%; border: 1px solid var(--linie); background: none; cursor: pointer; }
/* ---------- Bottom-Sheet (Telefon) ---------- */
.blatt { margin: auto 0 0; width: 100%; max-width: 100%; max-height: 92svh; padding: 0; border: 0; border-radius: 20px 20px 0 0; background: var(--karte); color: var(--text); display: none; flex-direction: column;
translate: 0 100%; transition: translate var(--t-zu) var(--zu), overlay var(--t-zu) allow-discrete, display var(--t-zu) allow-discrete; }
.blatt[open] { display: flex; translate: 0 0; transition-duration: var(--t-auf); transition-timing-function: var(--auf); }
@starting-style { .blatt[open] { translate: 0 100%; } }
.blatt::backdrop { background: rgba(30, 28, 26, 0); transition: background-color var(--t-zu) var(--zu), overlay var(--t-zu) allow-discrete, display var(--t-zu) allow-discrete; }
.blatt[open]::backdrop { background: rgba(30, 28, 26, .45); }
@starting-style { .blatt[open]::backdrop { background: rgba(30, 28, 26, 0); } }
.blatt__kopf { display: flex; justify-content: space-between; align-items: center; padding: 10px 18px 10px; border-bottom: 1px solid var(--linie); }
.blatt__kopf::before { content: ""; position: absolute; top: 6px; left: 50%; width: 40px; height: 4px; margin-left: -20px; border-radius: 4px; background: var(--linie); }
.blatt__kopf h2 { font: 400 1.375rem/1.2 'Newsreader', serif; padding-top: 6px; }
.blatt__inhalt { overflow-y: auto; overscroll-behavior: contain; }
.blatt .kal__monate { grid-template-columns: 1fr; gap: 18px; }
.blatt .kal { padding: 14px 16px; }
.blatt .monat td { height: 46px; }
.blatt .monat td span { width: 44px; height: 44px; }
.blatt .kal__fuss { display: none; }
.blatt .gaeste { border-top: 8px solid var(--grund); padding: 14px 16px 18px; }
.blatt .gaeste__fuss { display: none; }
.blatt__fuss { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 12px 16px calc(12px + env(safe-area-inset-bottom)); border-top: 1px solid var(--linie); background: var(--karte); }
.blatt__fuss .los { margin: 0; min-height: 48px; padding: 0 18px; }
/* ---------- Variante Zimmerseite ---------- */
.zimmer { max-width: 1280px; margin: 0 auto; padding: clamp(24px, 4vh, 44px) var(--rand) 0; display: grid; grid-template-columns: minmax(0, 7fr) minmax(320px, 4.2fr); gap: clamp(28px, 4vw, 64px); align-items: start; }
.pfad { font-size: .8125rem; color: var(--leise); margin-bottom: 10px; }
.pfad a { text-decoration: none; }
.zimmer h1 { font: 300 clamp(2.25rem, 1.4rem + 2.6vw, 3.75rem)/1.05 'Newsreader', serif; font-variation-settings: "opsz" 72; letter-spacing: -.015em; }
.zimmer h1 em { font-weight: 300; }
.fenster { position: relative; margin-top: 22px; aspect-ratio: 16 / 9.5; border-radius: 6px; overflow: hidden; background: #5f6470; }
.fenster img { width: 100%; height: 100%; object-fit: cover; object-position: 50% 60%; }
.fenster::after { content: ""; position: absolute; inset: 0; border: 14px solid #efe7dc; border-radius: 6px; background: linear-gradient(90deg, transparent calc(50% - 5px), #efe7dc calc(50% - 5px) calc(50% + 5px), transparent calc(50% + 5px)); box-shadow: inset 0 0 0 1px rgba(0, 0, 0, .06); }
.fakten { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; margin-top: 22px; }
.fakten div { border-top: 1px solid var(--text); padding-top: 10px; }
.fakten dt { color: var(--leise); font-size: .75rem; font-weight: 560; font-stretch: 118%; letter-spacing: .06em; text-transform: uppercase; }
.fakten dd { font: 400 1.25rem/1.3 'Newsreader', serif; margin-top: 2px; }
.zimmer__text { margin-top: 20px; max-width: 58ch; color: var(--leise); }
.buchkarte { position: sticky; top: 24px; display: grid; gap: 14px; padding: 26px 24px 22px; background: var(--creme); border-radius: 6px; box-shadow: 0 1px 2px rgba(62, 58, 55, .08), 0 18px 50px rgba(62, 58, 55, .12); }
.buchkarte__preis { display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap; }
.buchkarte__preis b { font: 400 2.25rem/1 'Newsreader', serif; font-variant-numeric: tabular-nums; }
.buchkarte__preis span { color: var(--leise); font-size: .9375rem; }
.buchkarte__klein { font-size: .8125rem; color: var(--leise); }
.buchkarte .leiste { grid-template-columns: 1fr; box-shadow: none; background: var(--karte); border: 1px solid var(--linie); min-height: 0; }
.buchkarte .feld--gaeste { border-left: 0; border-top: 1px solid var(--linie); }
.buchkarte .feld__knopf { padding: 12px 16px; }
.buchkarte .los { margin: 10px; }
.buchkarte .naechte { display: none; }
.buchkarte .feld__wert { font-size: 1.1875rem; }
.buchkarte__summe { font-size: .9375rem; font-variant-numeric: tabular-nums; min-height: 1.6em; }
.buchkarte__summe b { font: 400 1.25rem/1 'Newsreader', serif; }
.buchkarte .ohne-js .nativ, .ohne-js .buchkarte .nativ { grid-template-columns: 1fr 1fr; }
.buchkarte .vorteile { display: grid; justify-content: start; gap: 6px; margin: 4px 0 0; padding: 0; }
/* ---------- Demo-Pult ---------- */
.pult { position: fixed; right: 14px; bottom: 14px; z-index: 30; display: flex; align-items: center; gap: 8px 12px; padding: 7px 8px 7px 14px; border-radius: 999px; background: rgba(46, 42, 39, .94); color: #f3eee7; box-shadow: 0 10px 34px rgba(0, 0, 0, .25); font-size: .75rem; font-stretch: 110%; }
.pult__titel { color: #c9bfb1; letter-spacing: .06em; text-transform: uppercase; font-weight: 560; }
.pult button { height: 28px; padding: 0 11px; border-radius: 999px; border: 1px solid rgba(243, 238, 231, .24); background: transparent; color: inherit; cursor: pointer; font-size: .75rem; }
.pult button[aria-pressed="true"] { background: #f3eee7; color: #2e2a27; border-color: #f3eee7; }
.pult__werte { color: #c9bfb1; font-variant-numeric: tabular-nums; }
.zeiger { position: fixed; left: 0; top: 0; z-index: 2147483001; width: 20px; height: 20px; pointer-events: none; display: none; transition: scale 150ms var(--zu); }
.vorfuehrung .zeiger { display: block; }
.zeiger.drueckt { scale: .8; }
.vorschau body { overflow: hidden; padding-bottom: 0; }
/* ---------- Reduzierte Bewegung ---------- */
.reduziert *, .reduziert *::before, .reduziert *::after, .reduziert *::backdrop { transition-duration: 1ms !important; animation: none !important; }
.reduziert .held img { scale: 1; }
.reduziert .pop, .reduziert .blatt { translate: 0 0 !important; }
/* ---------- Schmal: feste untere Zeile, Bottom-Sheet ---------- */
@media (max-width: 1100px) {
.kopf nav { display: none; } .pult__werte { display: none; }
.feld__knopf { padding: 12px 16px; } .feld__wert { font-size: 1.25rem; } .naechte { min-width: 0; } .los { padding: 0 20px; }
}
.nur-schmal { display: none; }
@media (max-width: 900px) {
.zimmer { grid-template-columns: minmax(0, 1fr); }
.fakten { grid-template-columns: 1fr 1fr; }
.leiste { grid-template-columns: minmax(0, 1fr) minmax(0, .8fr); }
.leiste .los { grid-column: 1 / -1; margin-top: 0; }
}
@media (max-width: 640px) {
body { padding-bottom: 96px; }
.kopf { padding-block: 16px; }
.marke { font-size: 1.5rem; }
.held { height: 72svh; min-height: 460px; }
.held__text { bottom: 36px; }
.vorteile { justify-content: start; display: grid; margin-top: 22px; }
.ort-leiste { margin-top: 0; }
/* Die Leiste wird zur festen unteren Zeile */
.leiste, .buchkarte .leiste { position: fixed; left: 0; right: 0; bottom: 0; z-index: 6; grid-template-columns: minmax(0, 1fr) auto; min-height: 0; border-radius: 0; border: 0; border-top: 1px solid var(--linie); background: var(--creme); box-shadow: 0 -10px 30px rgba(62, 58, 55, .1); padding-bottom: env(safe-area-inset-bottom); }
.leiste .feld { display: none; }
.leiste .mobil-zeile { display: grid; align-content: center; gap: 2px; min-width: 0; padding: 10px 16px; border: 0; background: none; text-align: left; cursor: pointer; }
.mobil-zeile .label { color: var(--leise); font-size: .6875rem; }
.mobil-zeile b { font: 400 1.1875rem/1.2 'Newsreader', serif; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.leiste .los { grid-column: auto; margin: 10px 12px 10px 0; min-height: 50px; padding: 0 18px; }
.leiste .los .nur-breit { display: none; } .leiste .los .nur-schmal { display: inline; }
.ohne-js .leiste { position: static; }
.ohne-js .nativ { grid-template-columns: 1fr 1fr; }
.pop--ueber { position-anchor: none; position-try-fallbacks: none; inset: auto 0 0 0; width: 100%; max-width: 100%; margin: 0; border-radius: 20px 20px 0 0; translate: 0 100%; }
.pop--ueber:popover-open { translate: 0 0; }
@starting-style { .pop--ueber:popover-open { translate: 0 100%; } }
.buchkarte { position: static; }
.pult { top: 10px; bottom: auto; }
}
</style>
</head>
<body>
<header class="kopf">
<a class="marke" href="#start">Haus Seeweite</a>
<nav aria-label="Hauptnavigation"><ul>
<li><a href="#zimmer">Zimmer & Suiten</a></li><li><a href="#restaurant">Restaurant</a></li><li><a href="#bregenz">Bregenz erleben</a></li><li><a href="#anreise">Anreise</a></li>
</ul></nav>
<div class="kopf__rechts"><div class="sprache" aria-label="Sprache"><a href="#de" aria-current="true" hreflang="de">DE</a><a href="#en" hreflang="en">EN</a></div></div>
</header>
<main>
<!-- Variante Leiste: Hero mit Buchungsleiste darunter -->
<section class="held" data-nur="leiste" aria-labelledby="held-titel">
<img src="bilder/buchungsleiste-datumsbereich-see.webp" alt="Blick über den Bodensee in der Dämmerung, rechts die beleuchtete Mole, dahinter der Pfänder" fetchpriority="high" width="1600" height="1000">
<div class="held__text"><p class="label">Boutique-Hotel am Bodensee · Bregenz</p><h1 id="held-titel">Ankommen, wo der <em>See</em> anfängt.</h1></div>
</section>
<div class="ort-leiste" data-ort="leiste" data-nur="leiste">
<!-- Das Formular funktioniert auch ohne JS: native Felder, Abschicken öffnet die Engine mit Parametern -->
<form class="leiste" id="buchen" action="https://buchen.quartierbuch.example/haus-seeweite" method="get" target="_blank" aria-label="Zimmer suchen">
<div class="feld feld--datum">
<button type="button" class="feld__knopf" data-oeffne="kalender" data-teil="anreise" aria-haspopup="dialog" aria-expanded="false" aria-controls="kal-pop"><span class="label">Anreise</span><span class="feld__wert leer" data-wert="anreise">Datum wählen</span></button>
<span class="naechte" data-naechte aria-hidden="true">– Nächte</span>
<button type="button" class="feld__knopf" data-oeffne="kalender" data-teil="abreise" aria-haspopup="dialog" aria-expanded="false" aria-controls="kal-pop"><span class="label">Abreise</span><span class="feld__wert leer" data-wert="abreise">Datum wählen</span></button>
</div>
<div class="feld feld--gaeste">
<button type="button" class="feld__knopf" data-oeffne="gaeste" aria-haspopup="dialog" aria-expanded="false" aria-controls="gaeste-pop"><span class="label">Gäste</span><span class="feld__wert" data-wert="gaeste">2 Erwachsene</span></button>
</div>
<button type="button" class="mobil-zeile" data-oeffne="blatt" aria-haspopup="dialog"><span class="label" data-mobil-label>Ihr Aufenthalt</span><b data-mobil-wert>Reisedaten wählen</b></button>
<div class="nativ">
<label>Anreise <input type="date" name="arrival" data-n="anreise" required></label>
<label>Abreise <input type="date" name="departure" data-n="abreise" required></label>
<label>Erwachsene <select name="adults" data-n="erwachsene"><option>1</option><option selected>2</option><option>3</option><option>4</option></select></label>
<label>Kinder <select name="children" data-n="kinder"><option selected>0</option><option>1</option><option>2</option></select></label>
<input type="hidden" name="childAges" data-n="alter">
<input type="hidden" name="lang" value="de">
</div>
<button type="submit" class="los"><span class="nur-breit">Verfügbarkeit prüfen</span><span class="nur-schmal">Prüfen</span><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true"><path d="M3 8h10M9 4l4 4-4 4"/></svg></button>
</form>
</div>
<ul class="vorteile" data-nur="leiste" data-vorteile>
<li><svg viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="1.6" aria-hidden="true"><path d="m2.5 7.2 3 3 6-6.4"/></svg>Direkt gebucht: bester Preis</li>
<li><svg viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="1.6" aria-hidden="true"><path d="m2.5 7.2 3 3 6-6.4"/></svg>Frühstück am See inklusive</li>
<li><svg viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="1.6" aria-hidden="true"><path d="m2.5 7.2 3 3 6-6.4"/></svg>Kostenfrei stornieren bis 3 Tage vor Anreise</li>
</ul>
<!-- Variante Zimmerseite: Buchungskarte neben dem Zimmer -->
<section class="zimmer" data-nur="zimmer" aria-labelledby="zimmer-titel">
<div>
<p class="pfad"><a href="#zimmer">Zimmer & Suiten</a> / Seeseite</p>
<h1 id="zimmer-titel">Zimmer Molo, <em>mit Balkon zum See</em></h1>
<figure class="fenster"><img src="bilder/buchungsleiste-datumsbereich-see-klein.webp" alt="Blick aus dem Zimmerfenster auf den Bodensee und die Mole am Abend" width="800" height="500" loading="lazy"></figure>
<dl class="fakten">
<div><dt>Größe</dt><dd>28 m²</dd></div>
<div><dt>Bett</dt><dd>180 × 200</dd></div>
<div><dt>Ausblick</dt><dd>See und Mole</dd></div>
<div><dt>Belegung</dt><dd>bis 3 Personen</dd></div>
</dl>
<p class="zimmer__text">Eichenboden, Leinen, ein Sessel am Fenster. Vom Balkon sehen Sie die Lichter der Mole angehen; morgens bringt der Bodensee das Licht ins Zimmer. Zustellbett für ein Kind auf Anfrage.</p>
</div>
<aside class="buchkarte" aria-label="Zimmer Molo buchen">
<p class="buchkarte__preis"><b>ab 168 €</b><span>pro Nacht · Zimmer für zwei</span></p>
<p class="buchkarte__klein">inkl. Frühstück und MwSt., zzgl. Gästetaxe</p>
<div data-ort="zimmer"></div>
<p class="buchkarte__summe" data-summe-karte aria-live="polite">Reisedaten wählen für den Gesamtpreis.</p>
<ul class="vorteile">
<li><svg viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="1.6" aria-hidden="true"><path d="m2.5 7.2 3 3 6-6.4"/></svg>Direkt gebucht: bester Preis</li>
<li><svg viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="1.6" aria-hidden="true"><path d="m2.5 7.2 3 3 6-6.4"/></svg>Kostenfrei stornieren bis 3 Tage vorher</li>
</ul>
</aside>
</section>
</main>
<!-- Kalender und Gäste: Inhalte wandern zwischen Popover (Desktop) und Bottom-Sheet (Telefon) -->
<div id="kal-pop" class="pop pop--kal" popover aria-label="Reisedaten wählen">
<div class="kal" data-kal>
<div class="kal__kopf">
<button type="button" class="kal__nav" data-monat="-1" aria-label="Vorheriger Monat"><svg viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="1.6" aria-hidden="true"><path d="M8.5 3 4.5 7l4 4"/></svg></button>
<p class="kal__hinweis" data-hinweis aria-live="polite">Wann reisen Sie an?</p>
<button type="button" class="kal__nav" data-monat="1" aria-label="Nächster Monat"><svg viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="1.6" aria-hidden="true"><path d="m5.5 3 4 4-4 4"/></svg></button>
</div>
<div class="kal__monate" data-monate></div>
<div class="kal__fuss">
<ul class="legende"><li><i class="l-belegt" aria-hidden="true"></i>ausgebucht</li><li><i class="l-min" aria-hidden="true"></i>Mindestaufenthalt</li></ul>
<p class="summe" data-summe></p>
<button type="button" class="knopf-text" data-zuruecksetzen>Zurücksetzen</button>
<button type="button" class="knopf-voll" data-uebernehmen>Übernehmen</button>
</div>
</div>
</div>
<div id="gaeste-pop" class="pop pop--gaeste" popover aria-label="Gäste wählen">
<div class="gaeste" data-gaeste>
<div class="zaehler"><div><b id="z-erw">Erwachsene</b><small>ab 14 Jahren</small></div><div class="stepper" role="group" aria-labelledby="z-erw"><button type="button" data-schritt="erwachsene" data-um="-1" aria-label="Erwachsene: eine Person weniger">−</button><output data-zahl="erwachsene" aria-live="polite">2</output><button type="button" data-schritt="erwachsene" data-um="1" aria-label="Erwachsene: eine Person mehr">+</button></div></div>
<div class="zaehler"><div><b id="z-kind">Kinder</b><small>0 bis 13 Jahre</small></div><div class="stepper" role="group" aria-labelledby="z-kind"><button type="button" data-schritt="kinder" data-um="-1" aria-label="Kinder: eines weniger">−</button><output data-zahl="kinder" aria-live="polite">0</output><button type="button" data-schritt="kinder" data-um="1" aria-label="Kinder: eines mehr">+</button></div></div>
<div class="alter" data-alter></div>
<p class="gaeste__hinweis" data-gaeste-hinweis aria-live="polite"></p>
<div class="gaeste__fuss"><button type="button" class="knopf-voll" data-gaeste-fertig>Fertig</button></div>
</div>
</div>
<dialog id="blatt" class="blatt" closedby="any" aria-labelledby="blatt-titel">
<div class="blatt__kopf"><h2 id="blatt-titel">Ihr Aufenthalt</h2><button type="button" class="engine__zu" data-blatt-zu aria-label="Schließen">×</button></div>
<div class="blatt__inhalt" data-blatt-inhalt></div>
<div class="blatt__fuss"><p class="summe" data-summe-blatt></p><button type="button" class="los" data-blatt-weiter>Verfügbarkeit prüfen</button></div>
</dialog>
<!-- Klick 1: erst zusammenfassen und auf die Weitergabe hinweisen -->
<div id="ueber-pop" class="pop pop--ueber" popover aria-labelledby="ueber-titel">
<div class="ueber">
<p class="label" style="color: var(--leise)">Fast geschafft</p>
<h2 id="ueber-titel">Weiter zu unserem Buchungssystem</h2>
<dl data-ueber-liste></dl>
<p class="ueber__daten">Beim Weiter übergeben wir Reisedaten und Personenzahl an <b>Quartierbuch</b>, unser Buchungssystem. Erst dort sehen Sie freie Zimmer und Preise und geben Name und Zahlungsdaten ein. Mehr dazu in der <a href="#datenschutz">Datenschutzerklärung</a>.</p>
<div class="ueber__knoepfe"><button type="button" class="knopf-text" data-ueber-zurueck>Zurück</button><button type="button" class="los" data-ueber-weiter>Weiter zur Buchung<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true"><path d="M5 3h8v8M13 3 4 12"/></svg></button></div>
</div>
</div>
<!-- Klick 2: Übergabe (in der Demo gezeigt statt geöffnet) -->
<dialog id="engine" class="engine" closedby="any" aria-labelledby="engine-titel">
<div class="engine__kopf"><span><b>Quartierbuch</b> · Übergabe</span><button type="button" class="engine__zu" data-engine-zu aria-label="Schließen">×</button></div>
<div class="engine__inhalt">
<div class="engine__balken"><i data-balken></i></div>
<h2 id="engine-titel" style="font: 400 1.5rem/1.2 'Newsreader', serif">Ihre Auswahl geht an die Buchungsstrecke.</h2>
<code data-url></code>
<table><tbody data-parameter></tbody></table>
<p>Auf der echten Seite öffnet sich jetzt die Booking-Engine in einem neuen Tab oder, nach diesem Klick, als eingebettetes iFrame. Vorher wurde nichts geladen und nichts übertragen.</p>
</div>
</dialog>
<div class="pult">
<span class="pult__titel">Ansicht</span>
<button type="button" data-variante="leiste" aria-pressed="true">Leiste unter dem Hero</button>
<button type="button" data-variante="zimmer" aria-pressed="false">Zimmerseite</button>
<button type="button" data-ohne-js aria-pressed="false">Ohne JS</button>
<span class="pult__werte">auf 300 ms · zu 200 ms</span>
</div>
<svg class="zeiger" data-zeiger viewBox="0 0 18 18" aria-hidden="true"><path d="M2 1.5 15.5 9.2l-6 1.3-2.9 5.6Z" fill="#3e3a37" stroke="#fef2e3" stroke-width="1.2" stroke-linejoin="round"/></svg>
<script>
(() => {
const html = document.documentElement;
const reduziert = () => html.classList.contains('reduziert');
const $ = s => document.querySelector(s), $$ = s => [...document.querySelectorAll(s)];
const ANKER = CSS.supports('position-anchor: --a');
const schmal = matchMedia('(max-width: 640px)');
/* ---------- Datum: alles als 'JJJJ-MM-TT', gerechnet auf UTC-Mittag ---------- */
const D = s => new Date(s + 'T12:00:00Z');
const plus = (s, n) => { const d = D(s); d.setUTCDate(d.getUTCDate() + n); return d.toISOString().slice(0, 10); };
const diff = (a, b) => Math.round((D(b) - D(a)) / 864e5);
const wt = s => D(s).getUTCDay();
const heute = (() => { const p = Object.fromEntries(new Intl.DateTimeFormat('de-DE', { timeZone: 'Europe/Vienna', year: 'numeric', month: '2-digit', day: '2-digit' }).formatToParts(new Date()).map(x => [x.type, x.value])); return `${p.year}-${p.month}-${p.day}`; })();
const fKurz = new Intl.DateTimeFormat('de-AT', { timeZone: 'UTC', weekday: 'short', day: 'numeric', month: 'short' });
const fLang = new Intl.DateTimeFormat('de-AT', { timeZone: 'UTC', weekday: 'long', day: 'numeric', month: 'long', year: 'numeric' });
const fMonat = new Intl.DateTimeFormat('de-AT', { timeZone: 'UTC', month: 'long', year: 'numeric' });
const kurz = s => fKurz.format(D(s)).replace(/\.,/, ',');
const euro = n => n.toLocaleString('de-AT') + ' €';
/* ---------- Haus-Regeln (kämen aus der Engine bzw. einem täglichen Export) ---------- */
// Ausgebuchte Nächte: hier aus dem Datum gewürfelt, damit die Demo in jedem Monat Belegung zeigt
const hash = s => { let h = 2166136261; for (const c of s) h = Math.imul(h ^ c.charCodeAt(0), 16777619); return (h >>> 0) % 100; };
const belegt = s => s >= heute && (hash(s) < (wt(s) === 5 || wt(s) === 6 ? 30 : 9));
const festtage = s => { const md = s.slice(5); return md >= '12-24' && md <= '12-31'; };
const mindest = s => festtage(s) ? 3 : (wt(s) === 5 || wt(s) === 6) ? 2 : 1;
const preisNacht = s => festtage(s) ? 228 : (wt(s) === 5 || wt(s) === 6) ? 198 : 168;
const MAX_NAECHTE = 21;
/* ---------- Zustand ---------- */
const st = { anreise: null, abreise: null, erwachsene: 2, kinder: 0, alter: [], monat: heute.slice(0, 7) + '-01', zeige: null };
let maxGaeste = 4;
function abreiseGrund(start, d) {
const n = diff(start, d);
if (n <= 0) return 'vor der Anreise';
if (n > MAX_NAECHTE) return `höchstens ${MAX_NAECHTE} Nächte online`;
for (let k = 0; k < n; k++) if (belegt(plus(start, k))) return 'ausgebuchte Nacht dazwischen';
if (n < mindest(start)) return `Mindestaufenthalt ${mindest(start)} Nächte`;
return '';
}
// Anreise nur, wenn der Mindestaufenthalt ab diesem Tag überhaupt frei ist
const anreiseGrund = d => belegt(d) ? 'ausgebucht' : abreiseGrund(d, plus(d, mindest(d))) ? `Mindestaufenthalt ${mindest(d)} Nächte nicht frei` : '';
const naechte = () => st.anreise && st.abreise ? diff(st.anreise, st.abreise) : 0;
const preis = () => { let s = 0; for (let k = 0; k < naechte(); k++) s += preisNacht(plus(st.anreise, k)); return s; };
const gaesteText = () => `${st.erwachsene} ${st.erwachsene === 1 ? 'Erwachsener' : 'Erwachsene'}${st.kinder ? `, ${st.kinder} ${st.kinder === 1 ? 'Kind' : 'Kinder'}` : ''}`;
const gaesteKurz = () => { const n = st.erwachsene + st.kinder; return `${n} ${n === 1 ? 'Gast' : 'Gäste'}`; };
/* ---------- Kalender zeichnen ---------- */
const monateEl = $('[data-monate]'), hinweis = $('[data-hinweis]');
const MONATE = () => 2;
function monatTabelle(erster) {
const t = document.createElement('table');
t.className = 'monat'; t.setAttribute('role', 'grid');
const id = 'm-' + erster.slice(0, 7);
t.setAttribute('aria-labelledby', id);
const tage = ['Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag', 'Sonntag'];
let h = `<caption id="${id}">${fMonat.format(D(erster))}</caption><thead><tr>${tage.map(t => `<th scope="col" abbr="${t}">${t.slice(0, 2)}</th>`).join('')}</tr></thead><tbody><tr>`;
const versatz = (wt(erster) + 6) % 7;
for (let i = 0; i < versatz; i++) h += '<td role="gridcell"></td>';
let d = erster, spalte = versatz;
while (d.slice(0, 7) === erster.slice(0, 7)) {
if (spalte === 7) { h += '</tr><tr>'; spalte = 0; }
h += `<td role="gridcell" data-d="${d}" tabindex="-1"><span>${+d.slice(8)}</span></td>`;
d = plus(d, 1); spalte++;
}
while (spalte < 7) { h += '<td role="gridcell"></td>'; spalte++; }
t.innerHTML = h + '</tr></tbody>';
return t;
}
function zeichneMonate() {
const erster = st.monat;
const zweiter = plus(erster, 40).slice(0, 7) + '-01';
monateEl.replaceChildren(monatTabelle(erster), monatTabelle(zweiter));
$('[data-monat="-1"]').disabled = erster.slice(0, 7) <= heute.slice(0, 7);
markieren();
}
// Klassen, Namen und Sperren aktualisieren, ohne neu zu zeichnen
function markieren() {
const vor = st.zeige;
let fokusDa = false;
monateEl.querySelectorAll('td[data-d]').forEach(td => {
const d = td.dataset.d;
const c = td.classList;
const vergangen = d < heute, bel = belegt(d);
let gesperrt = vergangen, grund = vergangen ? 'vergangen' : '';
// Als Anreise: belegte Nächte gesperrt. Mit gewählter Anreise: Abreise-Regeln
const waehltAbreise = st.anreise && !st.abreise;
if (!vergangen && waehltAbreise && d > st.anreise) { grund = abreiseGrund(st.anreise, d); gesperrt = !!grund; }
else if (!vergangen && anreiseGrund(d)) { grund = anreiseGrund(d); gesperrt = !(waehltAbreise && d > st.anreise); }
const start = d === st.anreise, ende = d === st.abreise;
const imBereich = st.anreise && st.abreise && d >= st.anreise && d <= st.abreise;
const vorschau = waehltAbreise && vor && !abreiseGrund(st.anreise, vor) && d >= st.anreise && d <= vor;
c.toggle('vergangen', vergangen);
c.toggle('belegt', bel && !vergangen);
c.toggle('gesperrt', !vergangen && gesperrt && waehltAbreise && d > st.anreise);
c.toggle('min', grund.startsWith('Mindest') && !vergangen);
c.toggle('heute', d === heute);
c.toggle('start', start); c.toggle('ende', ende || (vorschau && d === vor));
c.toggle('band', !!(imBereich || vorschau) && !(start && !st.abreise && !vorschau));
c.toggle('vorschau', !!vorschau && !imBereich);
td.setAttribute('aria-disabled', String(!!gesperrt));
td.setAttribute('aria-selected', String(start || ende));
td.setAttribute('aria-label', fLang.format(D(d)) + (d === heute ? ', heute' : '') + (start ? ', Anreise' : '') + (ende ? ', Abreise' : '') + (grund && gesperrt ? `, nicht wählbar: ${grund}` : bel && !vergangen ? ', Nacht ausgebucht, nur als Abreise' : ''));
if (td.tabIndex === 0) fokusDa = true;
});
if (!fokusDa) setzeFokusZelle(st.abreise || st.anreise || heute, false);
const n = naechte();
$('[data-summe-karte]').innerHTML = n ? `<b>${n}</b> ${n === 1 ? 'Nacht' : 'Nächte'} · ab ${euro(preis())} gesamt` : 'Reisedaten wählen für den Gesamtpreis.';
$$('[data-summe], [data-summe-blatt]').forEach(el => el.innerHTML = n ? `<b>${n}</b> ${n === 1 ? 'Nacht' : 'Nächte'} · ab ${euro(preis())}` : (st.anreise ? 'Abreise wählen' : ' '));
$('[data-uebernehmen]').disabled = !n;
}
function setzeFokusZelle(d, fokus) {
let td = monateEl.querySelector(`td[data-d="${d}"]`) || monateEl.querySelector('td[data-d]:not(.vergangen)') || monateEl.querySelector('td[data-d]');
monateEl.querySelectorAll('td[tabindex="0"]').forEach(x => x.tabIndex = -1);
if (td) { td.tabIndex = 0; if (fokus) td.focus(); }
}
/* ---------- Auswahl ---------- */
function waehle(d) {
const td = monateEl.querySelector(`td[data-d="${d}"]`);
if (!td || d < heute) return;
if (!st.anreise || st.abreise || d <= st.anreise) {
if (anreiseGrund(d)) { sage(`Anreise ${kurz(d)} geht nicht: ${anreiseGrund(d)}`, true); return; }
st.anreise = d; st.abreise = null;
sage(`Anreise <b>${kurz(d)}</b> · jetzt die Abreise wählen${mindest(d) > 1 ? `, mindestens ${mindest(d)} Nächte` : ''}`);
} else {
const g = abreiseGrund(st.anreise, d);
if (g) { sage(`Abreise ${kurz(d)} geht nicht: ${g}`, true); return; }
st.abreise = d;
sage(`<b>${kurz(st.anreise)} – ${kurz(d)}</b> · ${naechte()} ${naechte() === 1 ? 'Nacht' : 'Nächte'}`);
}
st.zeige = null;
markieren(); felder();
}
function sage(t, fehler) { hinweis.innerHTML = t; hinweis.classList.toggle('fehler', !!fehler); }
monateEl.addEventListener('click', e => { const td = e.target.closest('td[data-d]'); if (td) { setzeFokusZelle(td.dataset.d, false); waehle(td.dataset.d); } });
monateEl.addEventListener('pointerover', e => { const td = e.target.closest('td[data-d]'); const d = td ? td.dataset.d : null; if (d !== st.zeige) { st.zeige = d; markieren(); } });
monateEl.addEventListener('pointerleave', () => { st.zeige = null; markieren(); });
// Tastatur nach APG: Pfeile, Bild auf/ab (Monat), Pos1/Ende (Woche), Enter/Leertaste
monateEl.addEventListener('keydown', e => {
const td = e.target.closest('td[data-d]'); if (!td) return;
const d = td.dataset.d;
const ziel = { ArrowLeft: plus(d, -1), ArrowRight: plus(d, 1), ArrowUp: plus(d, -7), ArrowDown: plus(d, 7),
Home: plus(d, -((wt(d) + 6) % 7)), End: plus(d, 6 - (wt(d) + 6) % 7) }[e.key];
let neu = ziel;
if (e.key === 'PageUp' || e.key === 'PageDown') {
const [y, m, t] = d.split('-').map(Number), mm = m + (e.key === 'PageUp' ? -1 : 1);
const ersterNeu = new Date(Date.UTC(y, mm - 1, 1)).toISOString().slice(0, 10);
const letzter = plus(new Date(Date.UTC(y, mm, 1)).toISOString().slice(0, 10), -1);
neu = plus(ersterNeu, Math.min(t, +letzter.slice(8)) - 1);
}
if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); waehle(d); setzeFokusZelle(d, true); if (st.abreise) $('[data-uebernehmen]').focus(); return; }
if (!neu) return;
e.preventDefault();
if (neu < heute) neu = heute;
const sichtbar = monateEl.querySelector(`td[data-d="${neu}"]`);
if (!sichtbar) { st.monat = neu.slice(0, 7) + '-01'; if (neu > d) st.monat = plus(neu.slice(0, 7) + '-01', -1).slice(0, 7) + '-01'; if (st.monat.slice(0, 7) < heute.slice(0, 7)) st.monat = heute.slice(0, 7) + '-01'; zeichneMonate(); }
st.zeige = st.anreise && !st.abreise ? neu : null; markieren();
setzeFokusZelle(neu, true);
});
$$('[data-monat]').forEach(b => b.addEventListener('click', () => {
const [y, m] = st.monat.split('-').map(Number);
st.monat = new Date(Date.UTC(y, m - 1 + +b.dataset.monat, 1)).toISOString().slice(0, 10);
zeichneMonate();
}));
$('[data-zuruecksetzen]').addEventListener('click', () => { st.anreise = st.abreise = null; sage('Wann reisen Sie an?'); markieren(); felder(); });
/* ---------- Gäste ---------- */
const alterEl = $('[data-alter]'), gHinweis = $('[data-gaeste-hinweis]');
function gaeste() {
$('[data-zahl="erwachsene"]').textContent = st.erwachsene;
$('[data-zahl="kinder"]').textContent = st.kinder;
const voll = st.erwachsene + st.kinder >= maxGaeste;
$$('[data-schritt]').forEach(b => {
const k = b.dataset.schritt, um = +b.dataset.um;
b.disabled = um < 0 ? st[k] <= (k === 'erwachsene' ? 1 : 0) : voll;
});
while (st.alter.length < st.kinder) st.alter.push('');
st.alter.length = st.kinder;
if (alterEl.children.length !== st.kinder) {
alterEl.replaceChildren(...st.alter.map((a, i) => {
const l = document.createElement('label');
l.innerHTML = `Alter Kind ${i + 1}<select data-kind="${i}"><option value="">bitte wählen</option>${Array.from({ length: 14 }, (_, j) => `<option value="${j}">${j} ${j === 1 ? 'Jahr' : 'Jahre'}</option>`).join('')}</select>`;
l.querySelector('select').value = a;
return l;
}));
}
gHinweis.classList.remove('fehler');
gHinweis.textContent = voll ? `Höchstens ${maxGaeste} Personen pro Zimmer. Für Familien und Gruppen schreiben Sie uns gern.` : st.kinder ? 'Das Alter bei Anreise bestimmt Preis und Bett.' : '';
felder();
}
$$('[data-schritt]').forEach(b => b.addEventListener('click', () => { st[b.dataset.schritt] += +b.dataset.um; gaeste(); }));
alterEl.addEventListener('change', e => { const s = e.target.closest('select'); if (s) { st.alter[+s.dataset.kind] = s.value; s.classList.remove('fehlt'); felder(); } });
/* ---------- Felder der Leiste und native Werte synchron halten ---------- */
function felder() {
const wa = $('[data-wert="anreise"]'), wb = $('[data-wert="abreise"]');
wa.textContent = st.anreise ? kurz(st.anreise) : 'Datum wählen'; wa.classList.toggle('leer', !st.anreise);
wb.textContent = st.abreise ? kurz(st.abreise) : 'Datum wählen'; wb.classList.toggle('leer', !st.abreise);
const n = naechte(), nEl = $('[data-naechte]');
nEl.textContent = n ? `${n} ${n === 1 ? 'Nacht' : 'Nächte'}` : '– Nächte'; nEl.classList.toggle('voll', !!n);
$('[data-wert="gaeste"]').textContent = st.kinder ? `${st.erwachsene} Erw. · ${st.kinder} ${st.kinder === 1 ? 'Kind' : 'Kinder'}` : gaesteText();
$('[data-mobil-wert]').textContent = n ? `${kurz(st.anreise).replace(/^\S+ /, '')} – ${kurz(st.abreise).replace(/^\S+ /, '')} · ${gaesteKurz()}` : 'Reisedaten wählen';
$('[data-mobil-label]').textContent = n ? `${n} ${n === 1 ? 'Nacht' : 'Nächte'} · ab ${euro(preis())}` : 'Ihr Aufenthalt';
const f = $('#buchen');
f.querySelector('[data-n="anreise"]').value = st.anreise || '';
f.querySelector('[data-n="abreise"]').value = st.abreise || '';
f.querySelector('[data-n="erwachsene"]').value = st.erwachsene;
f.querySelector('[data-n="kinder"]').value = Math.min(st.kinder, 2);
f.querySelector('[data-n="alter"]').value = st.alter.join(',');
}
/* ---------- Öffnen: Popover am Desktop, Bottom-Sheet auf dem Telefon ---------- */
const kalPop = $('#kal-pop'), gPop = $('#gaeste-pop'), blatt = $('#blatt'), ueberPop = $('#ueber-pop'), engine = $('#engine');
const kal = $('[data-kal]'), gaesteEl = $('[data-gaeste]');
let ausloeser = null;
function lageOhneAnker(pop, anker) {
if (ANKER) return;
pop.classList.add('ohne-anker');
const r = anker.getBoundingClientRect(), w = pop.offsetWidth, h = pop.offsetHeight;
const unten = r.bottom + 10 + h <= innerHeight;
pop.style.left = Math.max(12, Math.min(innerWidth - w - 12, r.left)) + 'px';
pop.style.top = (unten ? r.bottom + 10 : Math.max(12, r.top - 10 - h)) + 'px';
}
function oeffneKalender(knopf, teil) {
ausloeser = knopf;
if (teil === 'abreise' && st.anreise && st.abreise) { st.abreise = null; sage(`Anreise <b>${kurz(st.anreise)}</b> · neue Abreise wählen`); }
else if (!st.anreise) sage('Wann reisen Sie an?');
const ziel = st.anreise || heute;
if (ziel.slice(0, 7) < st.monat.slice(0, 7) || ziel.slice(0, 7) > plus(st.monat, 40).slice(0, 7)) st.monat = ziel.slice(0, 7) + '-01';
if (schmal.matches) return oeffneBlatt(knopf);
if (kal.parentElement !== kalPop) kalPop.append(kal);
zeichneMonate();
if (gPop.matches(':popover-open')) gPop.hidePopover();
if (!kalPop.matches(':popover-open')) { try { kalPop.showPopover({ source: knopf }); } catch (e) { kalPop.showPopover(); } }
lageOhneAnker(kalPop, $('.feld--datum'));
$$('[data-oeffne="kalender"]').forEach(b => b.setAttribute('aria-expanded', 'true'));
setzeFokusZelle(st.abreise || st.anreise || heute, !html.classList.contains('vorfuehrung'));
}
function oeffneGaeste(knopf) {
ausloeser = knopf;
if (schmal.matches) return oeffneBlatt(knopf);
if (gaesteEl.parentElement !== gPop) gPop.append(gaesteEl);
if (kalPop.matches(':popover-open')) kalPop.hidePopover();
try { gPop.showPopover({ source: knopf }); } catch (e) { gPop.showPopover(); }
lageOhneAnker(gPop, $('.feld--gaeste'));
knopf.setAttribute('aria-expanded', 'true');
}
function oeffneBlatt(knopf) {
ausloeser = knopf;
const inhalt = $('[data-blatt-inhalt]');
inhalt.append(kal, gaesteEl);
zeichneMonate();
if (!blatt.open) blatt.showModal();
}
kalPop.addEventListener('toggle', e => { if (e.newState === 'closed') { $$('[data-oeffne="kalender"]').forEach(b => b.setAttribute('aria-expanded', 'false')); fokusZurueck(kalPop); } });
gPop.addEventListener('toggle', e => { if (e.newState === 'closed') { $('[data-oeffne="gaeste"]').setAttribute('aria-expanded', 'false'); fokusZurueck(gPop); } });
function fokusZurueck(von) { if (ausloeser && (von.contains(document.activeElement) || document.activeElement === document.body) && !html.classList.contains('vorfuehrung')) ausloeser.focus(); }
$$('[data-oeffne]').forEach(b => b.addEventListener('click', () => {
const was = b.dataset.oeffne;
if (was === 'kalender') oeffneKalender(b, b.dataset.teil);
else if (was === 'gaeste') oeffneGaeste(b);
else oeffneBlatt(b);
}));
$('[data-uebernehmen]').addEventListener('click', () => kalPop.hidePopover());
$('[data-gaeste-fertig]').addEventListener('click', () => gPop.hidePopover());
$('[data-blatt-zu]').addEventListener('click', () => blatt.close());
$('[data-blatt-weiter]').addEventListener('click', () => pruefen($('[data-blatt-weiter]')));
/* ---------- Klick 1: prüfen und zusammenfassen ---------- */
function pruefen(knopf) {
if (!st.anreise || !st.abreise) {
sage(st.anreise ? 'Bitte noch die <b>Abreise</b> wählen.' : 'Bitte zuerst <b>Anreise und Abreise</b> wählen.', true);
if (!blatt.open) oeffneKalender($('[data-teil="anreise"]'), 'anreise');
return;
}
const fehlt = st.alter.findIndex(a => a === '');
if (fehlt > -1) {
if (!blatt.open) oeffneGaeste($('[data-oeffne="gaeste"]'));
gHinweis.textContent = 'Bitte das Alter aller Kinder angeben, es bestimmt Preis und Bett.'; gHinweis.classList.add('fehler');
const s = alterEl.querySelector(`[data-kind="${fehlt}"]`); s.classList.add('fehlt'); s.focus();
return;
}
if (kalPop.matches(':popover-open')) kalPop.hidePopover();
if (gPop.matches(':popover-open')) gPop.hidePopover();
const n = naechte();
$('[data-ueber-liste]').innerHTML = `<dt>Aufenthalt</dt><dd>${kurz(st.anreise)} – ${kurz(st.abreise)} · ${n} ${n === 1 ? 'Nacht' : 'Nächte'}</dd>
<dt>Gäste</dt><dd>${gaesteText()}${st.kinder ? ` (${st.alter.map(a => a + ' J.').join(', ')})` : ''}</dd>
<dt>Richtpreis</dt><dd>ab ${euro(preis())} inkl. Frühstück</dd>`;
ausloeser = knopf;
try { ueberPop.showPopover({ source: knopf }); } catch (e) { ueberPop.showPopover(); }
lageOhneAnker(ueberPop, knopf);
if (!html.classList.contains('vorfuehrung')) $('[data-ueber-weiter]').focus();
}
$('#buchen').addEventListener('submit', e => { e.preventDefault(); pruefen(e.submitter || $('.los')); });
$('[data-ueber-zurueck]').addEventListener('click', () => ueberPop.hidePopover());
ueberPop.addEventListener('toggle', e => { if (e.newState === 'closed') fokusZurueck(ueberPop); });
/* ---------- Klick 2: URL bauen und übergeben ---------- */
function uebergeben() {
const f = $('#buchen'), p = new URLSearchParams();
p.set('arrival', st.anreise); p.set('departure', st.abreise); p.set('adults', st.erwachsene);
if (st.kinder) { p.set('children', st.kinder); p.set('childAges', st.alter.join(',')); }
p.set('lang', 'de');
const url = f.action + '?' + p.toString().replace(/%2C/g, ',');
$('[data-url]').innerHTML = url.replace(/([?&])([^=]+)=([^&]*)/g, '$1$2=<mark>$3</mark>');
$('[data-parameter]').innerHTML = [...p].map(([k, v]) => `<tr><td>${k}</td><td>${v}</td></tr>`).join('');
ueberPop.hidePopover();
if (blatt.open) blatt.close();
const balken = $('[data-balken]'); balken.style.transition = 'none'; balken.style.width = '0';
engine.showModal();
requestAnimationFrame(() => requestAnimationFrame(() => { balken.style.transition = ''; balken.style.width = '100%'; }));
// Echt: window.open(url, '_blank', 'noopener') oder iFrame mit src = url erst jetzt einsetzen
}
$('[data-ueber-weiter]').addEventListener('click', uebergeben);
$('[data-engine-zu]').addEventListener('click', () => engine.close());
/* ---------- Variante (?v=) und Ohne-JS-Ansicht ---------- */
const VARIANTEN = ['leiste', 'zimmer'];
const q = new URLSearchParams(location.search);
let variante = VARIANTEN.includes(q.get('v')) ? q.get('v') : 'leiste';
function setzeVariante(v, schreiben) {
variante = v; html.dataset.v = v;
$(`[data-ort="${v}"]`).append($('#buchen'));
maxGaeste = v === 'zimmer' ? 3 : 4;
if (st.erwachsene + st.kinder > maxGaeste) { st.kinder = Math.max(0, maxGaeste - st.erwachsene); }
gaeste();
$$('[data-variante]').forEach(b => b.setAttribute('aria-pressed', String(b.dataset.variante === v)));
[kalPop, gPop, ueberPop].forEach(p => { if (p.matches(':popover-open')) p.hidePopover(); });
if (schreiben) { const p = new URLSearchParams(location.search); p.set('v', v); history.replaceState(null, '', location.pathname + '?' + p.toString().replace(/=(?=&|$)/g, '')); }
}
$$('[data-variante]').forEach(b => b.addEventListener('click', () => setzeVariante(b.dataset.variante, true)));
$('[data-ohne-js]').addEventListener('click', e => { const an = !html.classList.contains('ohne-js'); html.classList.toggle('ohne-js', an); e.currentTarget.setAttribute('aria-pressed', String(an)); });
schmal.addEventListener('change', () => { [kalPop, gPop].forEach(p => { if (p.matches(':popover-open')) p.hidePopover(); }); if (blatt.open) blatt.close(); });
$('#buchen').noValidate = true; // JS prüft selbst; ohne JS greifen required und die nativen Felder
setzeVariante(variante, false);
zeichneMonate();
/* ---------- Vorführung: Zeiger wählt Daten, Gäste und prüft ---------- */
const zeiger = $('[data-zeiger]');
let uhr = null, t0 = 0, angehalten = 0, schritt = -1, pos = { x: 0, y: 0 }, von = { x: 0, y: 0 }, erledigt = new Set(), runde = -1, plan = [];
const mitte = el => { const r = el.getBoundingClientRect(); return { x: r.left + r.width * .5, y: r.top + r.height * .55 }; };
function ziele() {
// Anreise: erster freier Donnerstag im zweiten sichtbaren Monat, drei Nächte
const m2 = plus(st.monat, 40).slice(0, 7) + '-01';
let a = m2; while (wt(a) !== 4 || belegt(a) || abreiseGrund(a, plus(a, 3))) a = plus(a, 1);
return { a, b: plus(a, 3), h: plus(a, 2) };
}
let z = null;
const td = d => monateEl.querySelector(`td[data-d="${d}"]`);
const ruhe = () => ({ x: innerWidth * .7, y: innerHeight * .3 });
function baueplan() {
return [
[600, () => $('[data-teil="anreise"]'), 'klick'],
[1700, () => td(z.a), 'zeige'],
[2300, () => td(z.a), 'klick'],
[3000, () => td(z.h), 'zeige'],
[3600, () => td(z.b), 'zeige'],
[4200, () => td(z.b), 'klick'],
[5200, () => $('[data-uebernehmen]'), 'klick'],
[6100, () => $('[data-oeffne="gaeste"]'), 'klick'],
[7000, () => $('[data-schritt="kinder"][data-um="1"]'), 'klick'],
[7900, () => alterEl.querySelector('select'), 'alter'],
[8700, () => $('[data-gaeste-fertig]'), 'klick'],
[9600, () => $('#buchen .los'), 'klick'],
[11600, () => $('[data-ueber-weiter]'), 'klick'],
[14600, ruhe, 'zu'],
];
}
const RUNDE = 15400, WEG = 520;
const sanft = p => p < .5 ? 4 * p * p * p : 1 - Math.pow(-2 * p + 2, 3) / 2;
const punkt = f => { const v = f(); if (v instanceof Element) { const r = v.getBoundingClientRect(); return r.width ? mitte(v) : pos; } return v || ruhe(); };
function grundzustand() {
[kalPop, gPop, ueberPop].forEach(p => { if (p.matches(':popover-open')) p.hidePopover(); });
if (engine.open) engine.close();
st.anreise = st.abreise = null; st.kinder = 0; st.alter = []; st.erwachsene = 2; st.monat = heute.slice(0, 7) + '-01';
sage('Wann reisen Sie an?'); gaeste(); zeichneMonate(); felder();
}
function rahmen(t) {
const r = Math.floor((t - t0) / RUNDE), zeit = (t - t0) % RUNDE;
if (r !== runde) { runde = r; schritt = -1; erledigt.clear(); grundzustand(); z = ziele(); plan = baueplan(); }
let i = -1; plan.forEach((p, k) => { if (zeit >= p[0] - WEG) i = k; });
if (i >= 0) {
const [ankunft, ziel, aktion] = plan[i];
if (i !== schritt) { schritt = i; von = { ...pos }; }
const p = Math.min(1, Math.max(0, (zeit - (ankunft - WEG)) / WEG));
const zz = punkt(ziel);
pos = { x: von.x + (zz.x - von.x) * sanft(p), y: von.y + (zz.y - von.y) * sanft(p) };
if (aktion === 'zeige') { const el = ziel(); if (el && st.zeige !== el.dataset.d) { st.zeige = el.dataset.d; markieren(); } }
if (p >= 1 && !erledigt.has(i)) {
erledigt.add(i);
const el = ziel();
if (aktion === 'klick' || aktion === 'alter') { zeiger.classList.add('drueckt'); setTimeout(() => zeiger.classList.remove('drueckt'), 160); }
if (aktion === 'klick' && el) el.click();
if (aktion === 'alter' && el) { el.value = '6'; el.dispatchEvent(new Event('change', { bubbles: true })); }
if (aktion === 'zu') grundzustand();
}
}
zeiger.style.transform = `translate(${pos.x - 2}px, ${pos.y - 1}px)`;
uhr = requestAnimationFrame(rahmen);
}
function vorfuehren() {
pause(); html.classList.add('vorfuehrung');
pos = ruhe(); von = { ...pos }; runde = -1;
t0 = performance.now(); uhr = requestAnimationFrame(rahmen);
}
function pause() { if (uhr) angehalten = performance.now() - t0; cancelAnimationFrame(uhr); uhr = null; }
function weiter() { if (uhr || !html.classList.contains('vorfuehrung')) return; t0 = performance.now() - angehalten; uhr = requestAnimationFrame(rahmen); }
function neustart() { pause(); html.classList.remove('vorfuehrung'); grundzustand(); }
})();
</script>
</body>
</html>