Zum Inhalt
← Hero & Section-Layouts

Termin-Buchung per 2-Klick-Einbettung

Buchungs-Platzhalter in Widget-Höhe: Leistungen, Anbieter und Datenfluss stehen sofort da, der Kalender des Drittanbieters lädt erst nach Klick.

Standard für Praxis, Friseur, Fahrschule, Restaurant: Leistungen und Datenfluss sofort, Widget erst nach Klick.

Statt das Buchungs-Widget eines Drittanbieters sofort zu laden, steht an seiner Stelle ein eigener Platzhalter. Er zeigt, was gebucht werden kann, nennt den Anbieter und sagt, welche Daten beim Laden an ihn gehen. Erst ein Klick auf „Online-Termin laden“ oder eine bestehende Einwilligung für genau diesen Dienst setzt iframe oder Script ein. Telefon oder E-Mail stehen immer daneben. Auf Wunsch merkt sich die Seite die Wahl für die Sitzung.

Online-Buchung ist für Praxen, Restaurants, Hotels und Fahrschulen der wichtigste CTA. Drittanbieter-Widgets setzen aber Cookies und kosten Ladezeit. Der Platzhalter erfüllt die Einwilligungspflicht und hält den ersten Bildschirm schnell. Wer nicht einwilligen will, hat trotzdem einen Weg: anrufen oder schreiben.

Wann einsetzen

Arztpraxen, Therapie, Friseur und Beauty, Restaurants mit Tischreservierung, Hotels, Fahrschulen, Beratung mit Kalender-Tool.

Wann nicht

Wenn die Buchung selbst gehostet ist und keine Daten an Dritte fließen. Dann direkt einbinden. Nie den Platzhalter ohne Alternative lassen: Telefon oder E-Mail gehören immer dazu.

Varianten

  • Platzhalter mit Leistungen, Hinweis zum Datenfluss und Button „Online-Termin laden“, daneben Telefon
  • Eigene Vorauswahl (Leistung, Klasse, Woche) vor dem Laden, Übergabe per URL-Parameter an das Widget
  • Link-out: Die Buchung öffnet beim Anbieter in einem neuen Tab, die eigene Seite lädt nichts nach (mobil oft besser)
  • Höhe passt sich an: Das iframe wächst ab Chrome 154 mit dem Inhalt (frame-sizing: content-height), wenn der Anbieter das Opt-in-Meta setzt; sonst feste Höhe wie oben

Umsetzung

Container mit fester Höhe in Widget-Höhe (hier 540 px, mobil 580 px), darin der Platzhalter; die Anbieter-Adresse steht in data-src. Der Klick erzeugt per JS das <iframe> mit title und loading="lazy" oder lädt das Script nach. Das iframe liegt absolut auf derselben Fläche, deshalb springt nichts. Platzhalter und iframe überblenden in 200 ms. Ein Status-Chip mit aria-live meldet „Nicht geladen“, „Verbinde …“ und „Geladen“. Die Freigabe mit der Kategorie im Consent-Banner abgleichen: Ist der Dienst dort erlaubt, lädt das Widget sofort. „Für diesen Besuch merken“ speichert die Wahl in sessionStorage. Ohne JS führt ein normaler Link zum Anbieter. Höhe, die sich anpasst (ab Chrome 154): Setzt der Anbieter im Widget <meta name="responsive-embedded-sizing" content="allow-origins=https://ihre-domain.example"> (oder allow-origins=*), kann das iframe seiner Inhaltshöhe folgen. Dazu der feste Wert als Rückfall und eine Weiche: .widget { height: 540px } @supports (frame-sizing: content-height) { .widget { height: auto; frame-sizing: content-height } }. Der Browser misst beim Laden; ändert sich das Widget danach (nächster Schritt, mehr Termine), ruft der Anbieter window.requestResize() auf. Das Meta lässt sich nicht nachträglich per Skript einsetzen, es muss im ausgelieferten Dokument stehen. Ohne Opt-in oder in Firefox und Safari bleibt die feste Höhe, längere Schritte scrollen im Widget. Den Platzhalter in der üblichen Starthöhe des Widgets reservieren, damit beim Laden nichts springt.

Robust umsetzen

Telefon. Platzhalter einspaltig und etwas höher. Unten eine feste Leiste mit Anmelden, Anrufen und WhatsApp. Auf dem Handy ist Link-out in einen neuen Tab oft besser als ein eingebettetes iframe.

Ohne Bewegung. Kein Effekt nötig. Platzhalter und Widget wechseln ohne Überblendung.

Barrierefrei. Echter <button>, klarer Text zum Datenfluss, iframe mit title. Nach dem Laden geht der Fokus in das Widget. Der Status-Chip meldet den Zustand per aria-live. Link-out mit Hinweis „öffnet in neuem Tab“ für Screenreader. Ein Button „Kalender wieder entfernen“ nimmt die Einwilligung zurück.

Code

<!DOCTYPE html>
<!--
  Termin-Buchung per 2-Klick-Einbettung
  Fundus · Hero & Section-Layouts · https://cecodesign.de/lab/fundus/hero-layouts/termin-buchung-zwei-klick/
  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>Termin-Buchung per 2-Klick-Einbettung · 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: 'Bricolage Grotesque'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/bricolage-grotesque:vf@latest/latin-opsz-normal.woff2') format('woff2-variations'); font-weight: 200 800; font-display: swap; }
@font-face { font-family: 'Atkinson Hyperlegible Next'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/atkinson-hyperlegible-next:vf@latest/latin-wght-normal.woff2') format('woff2-variations'); font-weight: 200 800; 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; }

/* Rezept „Fahrschule: freundlich und buchbar“: warmer heller Grund, Navy für Vertrauen,
   Himmelblau als Zweitton, Kennzeichen-Gelb nur als Fläche hinter dunklem Text */
:root {
  --grund: #f8f8ec; --tief: #eef0d8; --berg: #e3e6c6;
  --navy: #142b50; --nacht: #0e1d38; --himmel: #3fbadf; --gelb: #ffd60a;
  --text: #142b50; --leise: #566175; --linie: rgba(20, 43, 80, .14); --karte: #fffffa;
  --feder: cubic-bezier(0.34, 1.56, 0.64, 1);   /* nur Transform kleiner Elemente (Buttons), 350 ms */
  --ein: cubic-bezier(0.16, 1, 0.3, 1);         /* Einblendungen: Marker 800 ms, Straße 1800 ms */
  --ui: cubic-bezier(0.4, 0, 0.2, 1);           /* Buchung und Formulare: 200 ms, ohne Federung */
  --widget-h: 540px;                            /* Platzhalter = Höhe des Widgets: nichts springt */
}
* { box-sizing: border-box; margin: 0; }
html { background: var(--grund); color: var(--text); font: 400 17px/1.6 'Atkinson Hyperlegible Next', system-ui, sans-serif; -webkit-font-smoothing: antialiased; }
body { min-height: 100svh; display: flex; flex-direction: column; overflow-x: hidden; }
button, input, select { font: inherit; color: inherit; }
a { color: inherit; }
:focus-visible { outline: 3px solid var(--navy); outline-offset: 3px; border-radius: 6px; }
.mono { font: 500 11px/1.35 'Geist Mono', ui-monospace, monospace; letter-spacing: .14em; text-transform: uppercase; color: var(--leise); }
.zahl { font-variant-numeric: tabular-nums; }
.unsichtbar { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }

/* ── Kopf ───────────────────────────────────────────── */
.kopf { position: relative; z-index: 3; display: flex; align-items: center; gap: 28px; padding: 16px clamp(16px, 4.5vw, 64px); }
.marke { display: flex; align-items: center; gap: 11px; text-decoration: none; margin-right: auto; }
.marke svg { width: 40px; height: 40px; flex: none; }
.marke b { display: block; font: 800 23px/1 'Bricolage Grotesque', sans-serif; font-variation-settings: 'opsz' 36; letter-spacing: -.02em; }
.marke small { display: block; margin-top: 3px; font-size: 13px; line-height: 1; color: var(--leise); }
.kopf nav { display: flex; gap: 26px; font-size: 15.5px; }
.kopf nav a { text-decoration: none; padding: 6px 0; }
.kopf nav a:hover { text-decoration: underline; text-decoration-thickness: 2px; text-underline-offset: 5px; }
.kopf .tel { display: flex; align-items: center; gap: 8px; font-size: 15.5px; font-weight: 600; text-decoration: none; }
.kopf .tel svg { width: 18px; height: 18px; }

/* Federnder Button im TOFU-Prinzip: Gelb als Fläche, Navy-Kante, harter Schatten */
.knopf {
  --schatten: 4px;
  display: inline-flex; align-items: center; justify-content: center; gap: 10px;
  min-height: 48px; padding: 0 22px; border: 2px solid var(--navy); border-radius: 12px;
  background: var(--gelb); color: var(--navy); font-weight: 700; font-size: 16px; text-decoration: none; cursor: pointer;
  box-shadow: 0 var(--schatten) 0 var(--navy);
  transition: transform 350ms var(--feder), box-shadow 350ms var(--feder);
}
.knopf svg { width: 20px; height: 20px; flex: none; }
.knopf:hover { transform: translateY(-2px); --schatten: 6px; }
.knopf:active, .knopf.gedrueckt { transform: translateY(3px); --schatten: 1px; transition-duration: 150ms; }
.knopf.klein { min-height: 42px; padding: 0 18px; font-size: 15px; }
.knopf.leise { background: var(--karte); }

/* ── Hero ───────────────────────────────────────────── */
.held {
  --leiste-h: 74px;
  position: relative; flex: 1; display: grid; grid-template-columns: minmax(0, 1fr) minmax(340px, 440px);
  align-items: center; gap: clamp(32px, 6vw, 104px);
  padding: clamp(12px, 3vh, 40px) clamp(16px, 4.5vw, 64px) clamp(110px, 15vh, 160px);
}
.held-text { position: relative; z-index: 2; max-width: 640px; }
.held h1 {
  margin: 18px 0 22px; font: 700 clamp(2.5rem, 0.5rem + 5vw, 5.5rem)/.98 'Bricolage Grotesque', sans-serif;
  font-variation-settings: 'opsz' 96; letter-spacing: -.02em; hyphens: auto; text-wrap: balance;
}
.marker { position: relative; white-space: nowrap; }
.marker::before {
  content: ''; position: absolute; z-index: -1; left: -.06em; right: -.08em; bottom: .04em; height: .42em;
  background: var(--gelb); border-radius: .08em; transform-origin: 0 50%; rotate: -1.2deg;
}
.js .marker::before { transform: scaleX(0); }
.start .marker::before { transform: scaleX(1); transition: transform 800ms var(--ein) 300ms; }
.lead { font-size: 1.125rem; max-width: 34em; }
.lead strong { font-weight: 700; }
.fakten { display: flex; flex-wrap: wrap; gap: 10px 28px; margin-top: 28px; padding-top: 20px; border-top: 1px solid var(--linie); }
.fakten div { display: grid; gap: 4px; }
.fakten dd { margin: 0; font-weight: 600; font-size: 15.5px; }

/* ── Buchung: Kopfzeile, eingebettete Fläche, Fuß ───── */
.buchung { position: relative; z-index: 2; display: grid; gap: 12px; }
.buchung-kopf { display: flex; flex-wrap: wrap; justify-content: space-between; align-items: center; gap: 12px; }
.chip { display: inline-flex; align-items: center; gap: 8px; padding: 5px 11px 5px 9px; border-radius: 99px; background: var(--karte); border: 1px solid var(--linie); font: 500 10.5px/1.2 'Geist Mono', monospace; letter-spacing: .08em; text-transform: uppercase; color: var(--leise); }
.chip i { width: 8px; height: 8px; border-radius: 50%; background: #9aa3b2; flex: none; }
[data-zustand='laedt'] .chip i { background: var(--himmel); animation: puls 900ms var(--ui) infinite alternate; }
[data-zustand='geladen'] .chip i { background: #1f8a4c; }
@keyframes puls { to { opacity: .35; } }

.embed {
  position: relative; height: var(--widget-h); border-radius: 20px; overflow: hidden;
  background: var(--karte); border: 2px solid var(--navy); box-shadow: 0 8px 0 var(--navy);
}
.platzhalter {
  position: absolute; inset: 0; display: flex; flex-direction: column; padding: 26px 26px 22px;
  transition: opacity 200ms var(--ui);
}
.platzhalter h2 { font: 700 27px/1.05 'Bricolage Grotesque', sans-serif; font-variation-settings: 'opsz' 48; letter-spacing: -.015em; margin: 10px 0 6px; }
.platzhalter > p { font-size: 15.5px; color: var(--leise); line-height: 1.45; }
.p-kopf { display: flex; justify-content: space-between; align-items: flex-start; }
.kalender-bild { width: 40px; height: 40px; }
.preise { list-style: none; padding: 0; margin: 16px 0 0; border-top: 1px solid var(--linie); }
.preise li { display: flex; justify-content: space-between; align-items: baseline; gap: 12px; padding: 10px 0; border-bottom: 1px solid var(--linie); font-size: 15.5px; }
.preise small { color: var(--leise); font-size: 13.5px; }
.preise b { font-weight: 700; white-space: nowrap; }
.hinweis { display: grid; grid-template-columns: 22px 1fr; gap: 10px; margin: auto 0 14px; padding: 12px 14px; border-radius: 12px; background: var(--tief); font-size: 13.5px; line-height: 1.45; }
.hinweis svg { width: 20px; height: 20px; margin-top: 1px; }
.hinweis strong { font-weight: 700; }
.platzhalter .knopf { width: 100%; min-height: 54px; font-size: 17px; }
.merken { display: flex; align-items: center; gap: 10px; margin-top: 14px; font-size: 14px; color: var(--leise); cursor: pointer; }
.merken input { width: 20px; height: 20px; margin: 0; accent-color: var(--navy); }
.dreher { width: 18px; height: 18px; border: 2.5px solid currentColor; border-right-color: transparent; border-radius: 50%; display: none; animation: dreh 700ms linear infinite; }
[data-zustand='laedt'] .dreher { display: block; }
[data-zustand='laedt'] [data-laden] svg, [data-zustand='laedt'] .laden-text, .lade-text { display: none; }
[data-zustand='laedt'] .lade-text { display: inline; }
@keyframes dreh { to { rotate: 1turn; } }

/* Vorauswahl: eigene Felder, Übergabe per URL-Parameter */
.feld { margin-top: 14px; }
.feld > .mono { display: block; margin-bottom: 7px; }
.segment { display: flex; gap: 6px; border: 0; padding: 0; }
.segment label { flex: 1; }
.segment input { position: absolute; opacity: 0; pointer-events: none; }
.segment span { -webkit-tap-highlight-color: transparent;
  display: grid; place-items: center; min-height: 42px; padding: 0 8px; border-radius: 10px; border: 1.5px solid var(--linie);
  font-weight: 600; font-size: 15px; cursor: pointer; background: var(--grund);
  transition: background-color 200ms var(--ui), border-color 200ms var(--ui), color 200ms var(--ui);
}
.segment input:checked + span { background: var(--navy); border-color: var(--navy); color: var(--grund); }
.segment input:focus-visible + span { outline: 3px solid var(--navy); outline-offset: 2px; }
.uebergabe { margin: 14px 0 0; padding: 9px 12px; border-radius: 10px; border: 1px dashed var(--linie); font: 400 11.5px/1.45 'Geist Mono', monospace; color: var(--leise); word-break: break-all; }
.uebergabe b { color: var(--navy); font-weight: 600; }
.vorauswahl .hinweis { margin-top: auto; }

/* Varianten blenden nur ihre Teile ein */
[data-variante]:not([data-variante='vorauswahl']) .nur-vorauswahl, [data-variante]:not([data-variante='linkout']) .nur-linkout,
[data-variante='vorauswahl'] .ohne-vorauswahl, [data-variante='linkout'] .ohne-linkout { display: none; }
[data-variante='vorauswahl'] .p-kopf { display: none; }
[data-variante='vorauswahl'] .platzhalter h2 { margin-top: 0; }
[data-variante]:not([data-variante='hoehe']) .nur-hoehe, [data-variante='hoehe'] .ohne-hoehe { display: none; }

/* Das nachgeladene iframe liegt exakt auf der Fläche des Platzhalters */
.widget { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; background: #fff; opacity: 0; transition: opacity 200ms var(--ui); }
[data-zustand='geladen'] .widget { opacity: 1; }
[data-zustand='geladen'] .platzhalter { opacity: 0; pointer-events: none; }

/* Variante „Höhe passt sich an“: Rückfall wie oben (feste Höhe, das Widget scrollt innen).
   Ab Chrome 154 wächst das iframe mit dem Inhalt, wenn der Anbieter im Widget
   <meta name="responsive-embedded-sizing" content="allow-origins=…"> setzt. */
@supports (frame-sizing: content-height) {
  [data-variante='hoehe'][data-zustand='geladen'] .embed { height: auto; min-height: 0; }
  [data-variante='hoehe'][data-zustand='geladen'] .widget { position: relative; inset: auto; display: block; height: auto; frame-sizing: content-height; }
}
.hoehe-hinweis { display: grid; grid-template-columns: 10px 1fr; gap: 9px; align-items: baseline; text-transform: none; letter-spacing: .02em; font-size: 11.5px; }
.hoehe-hinweis i { width: 8px; height: 8px; border-radius: 2px; background: var(--leise); translate: 0 -1px; }
.hoehe-hinweis.aktiv i { background: #1f8a4c; }
.hoehe-hinweis code { font: inherit; color: var(--navy); font-weight: 600; }

.buchung-fuss { display: flex; justify-content: space-between; align-items: center; gap: 8px 16px; flex-wrap: wrap; font-size: 14px; color: var(--leise); }
.buchung-fuss a { font-weight: 700; color: var(--navy); text-decoration-thickness: 1.5px; text-underline-offset: 3px; }
.entfernen { border: 0; background: none; padding: 4px 0; font-size: 14px; color: var(--navy); text-decoration: underline; text-underline-offset: 3px; cursor: pointer; display: none; }
[data-zustand='geladen'] .entfernen { display: inline; }

/* Maßlinie: zeigt, dass die Höhe fest bleibt */
.mass { position: absolute; top: 0; bottom: 0; right: -30px; width: 12px; display: flex; flex-direction: column; align-items: center; }
.mass::before, .mass::after { content: ''; width: 9px; height: 1px; background: var(--leise); }
.mass i { flex: 1; width: 1px; background: var(--leise); opacity: .6; }
.mass span { position: absolute; top: 50%; left: 50%; translate: -50% -50%; rotate: 90deg; white-space: nowrap; background: var(--grund); padding: 2px 6px; }
.embed-rahmen { position: relative; }

/* ── Landschaft: Nordkette und Serpentine ───────────── */
.landschaft { position: absolute; left: 0; right: 0; bottom: var(--leiste-h, 0px); z-index: 0; width: 100%; height: clamp(150px, 27vh, 250px); pointer-events: none; }
.js .strasse-maske { stroke-dasharray: 1; stroke-dashoffset: 1; }
.start .strasse-maske { stroke-dashoffset: 0; transition: stroke-dashoffset 1800ms var(--ein) 150ms; }

/* ── Link-out: nachgestellter neuer Tab ─────────────── */
.tab {
  position: fixed; z-index: 20; inset: auto; width: min(520px, calc(100vw - 32px)); height: min(600px, calc(100svh - 48px));
  display: flex; flex-direction: column; border-radius: 14px; overflow: hidden; background: #fff;
  box-shadow: 0 30px 80px rgba(14, 29, 56, .35), 0 0 0 1px rgba(14, 29, 56, .2);
  opacity: 0; translate: 0 14px; visibility: hidden;
  transition: opacity 200ms var(--ui), translate 200ms var(--ui), visibility 0s 200ms;
}
.tab.offen { opacity: 1; translate: 0 0; visibility: visible; transition: opacity 200ms var(--ui), translate 200ms var(--ui); }
.tab-leiste { display: flex; align-items: center; gap: 10px; padding: 8px 10px 0; background: #dfe3ea; }
.tab-reiter { display: flex; align-items: center; gap: 8px; padding: 8px 12px; border-radius: 9px 9px 0 0; background: #fff; font: 500 12.5px/1 system-ui, sans-serif; color: #333; max-width: 70%; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.tab-reiter i { width: 12px; height: 12px; border-radius: 3px; background: #0f766e; flex: none; }
.tab-zu { margin-left: auto; width: 32px; height: 32px; border: 0; border-radius: 8px; background: none; font-size: 20px; line-height: 1; cursor: pointer; color: #333; }
.tab-zu:hover { background: rgba(0, 0, 0, .08); }
.tab-adresse { padding: 8px 12px; background: #fff; border-bottom: 1px solid #e5e7eb; }
.tab-adresse span { display: block; padding: 7px 12px; border-radius: 99px; background: #f1f3f6; font: 400 12.5px/1.2 system-ui, sans-serif; color: #444; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.tab iframe { flex: 1; width: 100%; border: 0; }
.schleier { position: fixed; inset: 0; z-index: 19; background: rgba(14, 29, 56, .28); opacity: 0; visibility: hidden; transition: opacity 200ms var(--ui), visibility 0s 200ms; }
.schleier.offen { opacity: 1; visibility: visible; transition: opacity 200ms var(--ui); }

/* ── Werkzeugleiste der Demo (dezent) ───────────────── */
.werkzeug { position: fixed; z-index: 10; left: 0; right: 0; bottom: 0; display: flex; justify-content: space-between; align-items: center; gap: 12px 24px; flex-wrap: wrap; padding: 10px clamp(16px, 4.5vw, 64px); background: rgba(248, 248, 236, .9); backdrop-filter: blur(8px); border-top: 1px solid var(--linie); }
.varianten { display: flex; flex-wrap: wrap; align-items: center; gap: 4px; }
.varianten .mono { margin-right: 8px; }
.varianten button { padding: 6px 10px; border: 1px solid transparent; border-radius: 99px; background: none; cursor: pointer; font: 500 11px/1 'Geist Mono', monospace; letter-spacing: .1em; text-transform: uppercase; color: var(--leise); transition: background-color 150ms var(--ui), color 150ms var(--ui); }
.varianten button[aria-pressed='true'] { background: var(--navy); color: var(--grund); }
.werte { text-align: right; overflow-wrap: anywhere; min-width: 0; }
.werte b { color: var(--navy); font-weight: 600; }

/* Vorführung: gezeichneter Zeiger */
.zeiger { position: fixed; left: 0; top: 0; z-index: 30; width: 22px; height: 22px; pointer-events: none; display: none; }
.vorfuehrung .zeiger { display: block; }
.vorfuehrung :focus-visible, .vorfuehrung .segment input:focus-visible + span { outline: none; }
.zeiger .ring { transform-origin: 3px 2px; opacity: 0; }
.zeiger.klickt .ring { animation: ring 450ms var(--ein); }
@keyframes ring { from { opacity: .9; transform: scale(.3); } to { opacity: 0; transform: scale(1.6); } }

/* Mobile Leiste: Anmelden, Anrufen, WhatsApp */
.leiste { display: none; }

/* ── Mittlere Breiten ───────────────────────────────── */
@media (max-width: 1100px) { .kopf nav { display: none; } }
@media (max-width: 860px) {
  .held { grid-template-columns: 1fr; gap: 32px; padding-bottom: 150px; }
  .held-text { max-width: none; }
  .buchung { max-width: 520px; }
  .mass { display: none; }
}
@media (max-width: 560px) {
  :root { --widget-h: 580px; }
  html { font-size: 16px; }
  .kopf { padding-top: 12px; padding-bottom: 12px; gap: 12px; }
  .kopf .tel span, .kopf .knopf { display: none; }
  .kopf .tel { width: 44px; height: 44px; justify-content: center; border-radius: 12px; border: 2px solid var(--navy); }
  .marke b { font-size: 21px; }
  .held h1 { margin: 12px 0 16px; }
  .fakten { gap: 12px 22px; }
  .platzhalter { padding: 22px 18px 18px; }
  .platzhalter h2 { font-size: 24px; }
  .held { --leiste-h: 0px; }
  .werkzeug { position: static; flex-direction: column; align-items: flex-start; padding-bottom: 96px; background: var(--tief); }
  .werte { text-align: left; }
  .varianten button { padding: 6px 8px; letter-spacing: .06em; }
  .varianten .mono { margin-right: 4px; }
  .leiste { position: fixed; z-index: 9; left: 0; right: 0; bottom: 0; display: grid; grid-template-columns: 1.3fr 1fr 1fr; gap: 8px; padding: 10px 12px calc(10px + env(safe-area-inset-bottom)); background: var(--navy); }
  .leiste a { display: flex; align-items: center; justify-content: center; gap: 8px; min-height: 48px; border-radius: 12px; color: var(--grund); font-weight: 700; font-size: 15px; text-decoration: none; border: 1.5px solid rgba(248, 248, 236, .35); }
  .leiste a:first-child { background: var(--gelb); color: var(--navy); border-color: var(--gelb); }
  .leiste svg { width: 18px; height: 18px; }
  .tab { left: 8px !important; top: 8px !important; width: calc(100vw - 16px); height: calc(100svh - 16px); }
}

/* Touch: kein Hover-Heben, große Ziele */
@media (hover: none) {
  .knopf:hover { transform: none; --schatten: 4px; }
  .merken { min-height: 44px; margin-top: 8px; }
}

/* Vorschau 960 × 720: nur die Bühne */
.vorschau body { overflow: hidden; height: 100svh; }
.vorschau .werkzeug { display: none; }
.vorschau .held { --leiste-h: 0px; padding-top: 4px; padding-bottom: 12px; }
.vorschau .kopf { padding-top: 12px; padding-bottom: 12px; }
.vorschau .entfernen { visibility: hidden; }

/* Reduzierte Bewegung: Austausch ohne Animation, nichts zeichnet sich */
:root.reduziert *, :root.reduziert *::before { transition-duration: .01ms !important; transition-delay: 0s !important; animation-duration: .01ms !important; animation-iteration-count: 1 !important; }
.reduziert .marker::before { transform: scaleX(1); }
.reduziert .strasse-maske { stroke-dashoffset: 0; }
.reduziert .knopf:hover { transform: none; }
@media (forced-colors: active) {
  button, [role=button], [role=tab], [role=switch] { border: 1px solid ButtonText !important; }
}
</style>
</head>
<body>
<header class="kopf">
  <a class="marke" href="#" aria-label="Fahrschule Spitzkehre, Startseite">
    <svg viewBox="0 0 40 40" aria-hidden="true"><rect width="40" height="40" rx="11" fill="#ffd60a"/><rect x="1" y="1" width="38" height="38" rx="10" fill="none" stroke="#142b50" stroke-width="2"/><path d="M9 31h14a5.5 5.5 0 0 0 0-11h-6a5.5 5.5 0 0 1 0-11h14" fill="none" stroke="#142b50" stroke-width="4.2" stroke-linecap="round"/><path d="M9 31h14a5.5 5.5 0 0 0 0-11h-6a5.5 5.5 0 0 1 0-11h14" fill="none" stroke="#ffd60a" stroke-width="1.1" stroke-dasharray="2 2.6"/></svg>
    <span><b>Spitzkehre</b><small>Fahrschule · Innsbruck</small></span>
  </a>
  <nav aria-label="Hauptnavigation"><a href="#">Klassen</a><a href="#">Preise</a><a href="#">Ablauf</a><a href="#">Team</a></nav>
  <a class="tel" href="tel:+43512394720" aria-label="Anrufen: 0512 39 47 20"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 3h3.5l1.8 4.6-2.4 1.6a11 11 0 0 0 6 6l1.6-2.4L20 14.6V18a2.5 2.5 0 0 1-2.7 2.5A17 17 0 0 1 3.5 5.7 2.5 2.5 0 0 1 5 3Z"/></svg><span class="zahl">0512 39 47 20</span></a>
  <a class="knopf klein" href="#">Anmelden</a>
</header>

<main class="held">
  <div class="held-text">
    <p class="mono">Innsbruck-Pradl · seit 2009</p>
    <h1>Fahren lernen, <span class="marker">Kehre</span> für Kehre.</h1>
    <p class="lead">Führerschein B, A und L17 mitten in Innsbruck. <strong>Deine erste Fahrstunde buchst du gleich hier</strong>, freie Termine gibt es schon ab Montag.</p>
    <dl class="fakten">
      <div><dt class="mono">Erste Fahrstunde</dt><dd class="zahl">50 Min. · € 72</dd></div>
      <div><dt class="mono">Theorie</dt><dd>Mo + Mi, 18 Uhr</dd></div>
      <div><dt class="mono">Wir sprechen</dt><dd>Deutsch, Englisch, Türkisch, BKS</dd></div>
    </dl>
  </div>

  <section class="buchung" data-buchung data-variante="platzhalter" data-zustand="leer" aria-labelledby="buchung-titel">
    <div class="buchung-kopf">
      <p class="mono">Online-Termin</p>
      <p class="chip" role="status" aria-live="polite"><i></i><span data-chip>Nicht geladen · keine Daten an Dritte</span></p>
    </div>
    <div class="embed-rahmen">
      <div class="embed" data-embed>
        <div class="platzhalter" data-platzhalter>
          <div class="p-kopf">
            <svg class="kalender-bild" viewBox="0 0 46 46" aria-hidden="true"><rect x="3" y="7" width="40" height="36" rx="8" fill="#eef0d8" stroke="#142b50" stroke-width="2"/><path d="M3 17h40" stroke="#142b50" stroke-width="2"/><path d="M14 3v8M32 3v8" stroke="#142b50" stroke-width="2.5" stroke-linecap="round"/><rect x="25" y="23" width="12" height="11" rx="3" fill="#ffd60a" stroke="#142b50" stroke-width="2"/><path d="M10 25h8M10 31h8" stroke="#142b50" stroke-width="2" stroke-linecap="round" opacity=".45"/></svg>
            <p class="mono ohne-vorauswahl ohne-linkout">Keine Cookies bis zum Klick</p><p class="mono nur-linkout">Öffnet einen neuen Tab</p>
            <p class="mono nur-vorauswahl">Vorauswahl</p>
          </div>
          <h2 id="buchung-titel"><span class="ohne-vorauswahl">Erste Fahrstunde buchen</span><span class="nur-vorauswahl">Freie Fahrstunden finden</span></h2>
          <p class="ohne-vorauswahl">Freien Termin wählen, Daten eintragen, fertig. Dauert keine zwei Minuten.</p>

          <ul class="preise ohne-vorauswahl">
            <li><span>Erste Fahrstunde B <small>· 50 Min.</small></span><b class="zahl">€ 72</b></li>
            <li><span>Probefahrt Motorrad A <small>· 50 Min.</small></span><b class="zahl">€ 78</b></li>
            <li><span>Beratung zur Anmeldung <small>· 20 Min.</small></span><b>kostenlos</b></li>
          </ul>

          <div class="vorauswahl nur-vorauswahl" data-vorauswahl>
            <fieldset class="feld segment-feld" style="border:0;padding:0"><legend class="mono" style="margin-bottom:7px">Klasse</legend>
              <div class="segment">
                <label><input type="radio" name="klasse" value="B" checked><span>B</span></label>
                <label><input type="radio" name="klasse" value="A"><span>A</span></label>
                <label><input type="radio" name="klasse" value="L17"><span>L17</span></label>
                <label><input type="radio" name="klasse" value="BE"><span>BE</span></label>
              </div>
            </fieldset>
            <fieldset class="feld" style="border:0;padding:0"><legend class="mono" style="margin-bottom:7px">Ab wann</legend>
              <div class="segment">
                <label><input type="radio" name="ab" value="2026-11-02" checked><span>Diese Woche</span></label>
                <label><input type="radio" name="ab" value="2026-11-09"><span>Nächste Woche</span></label>
              </div>
            </fieldset>
            <p class="uebergabe" aria-label="Übergabe an den Kalender">termine.terminwerk.example/spitzkehre<b data-url>?klasse=B&amp;leistung=erste-fahrstunde&amp;ab=2026-11-02</b></p>
          </div>

          <div class="hinweis">
            <svg viewBox="0 0 20 20" fill="none" stroke="#142b50" stroke-width="1.8" stroke-linejoin="round" aria-hidden="true"><path d="M10 1.8 3 4.6v4.8c0 4.2 2.9 7.6 7 8.8 4.1-1.2 7-4.6 7-8.8V4.6Z"/><path d="m6.8 10 2.2 2.2 4.2-4.4" stroke-linecap="round"/></svg>
            <p class="ohne-linkout">Der Kalender kommt von <strong>Terminwerk</strong>. Erst beim Laden gehen deine IP-Adresse und Browserdaten an Terminwerk. Dauerhaft erlauben: Cookie-Einstellungen.</p>
            <p class="nur-linkout">Die Buchung öffnet sich bei <strong>Terminwerk</strong> in einem neuen Tab. Erst dort gehen Daten an Terminwerk, diese Seite lädt nichts nach.</p>
          </div>

          <button class="knopf ohne-linkout" type="button" data-laden><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3.5" y="5" width="17" height="15.5" rx="3"/><path d="M3.5 10h17M8 2.8v4M16 2.8v4"/></svg><span class="dreher" aria-hidden="true"></span><span class="laden-text"><span class="ohne-vorauswahl">Online-Termin laden</span><span class="nur-vorauswahl">Freie Termine laden</span></span><span class="lade-text">Kalender wird geladen …</span></button>
          <a class="knopf nur-linkout" href="#terminwerk" target="_blank" rel="noopener" data-linkout>Bei Terminwerk buchen <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M14 4h6v6M20 4l-9 9M18 14v4.5A1.5 1.5 0 0 1 16.5 20h-11A1.5 1.5 0 0 1 4 18.5v-11A1.5 1.5 0 0 1 5.5 6H10"/></svg><span class="unsichtbar"> (öffnet in neuem Tab)</span></a>
          <label class="merken ohne-linkout"><input type="checkbox" data-merken> Für diesen Besuch merken</label>
        </div>
      </div>
      <div class="mass" aria-hidden="true"><i></i><span class="mono" data-mass-text>Höhe fest · 540 px</span><i></i></div>
    </div>
    <p class="mono hoehe-hinweis nur-hoehe" data-hoehe-hinweis><i aria-hidden="true"></i><span></span></p>
    <div class="buchung-fuss">
      <span>Lieber reden? <a href="tel:+43512394720" class="zahl">0512 39 47 20</a> · Mo bis Fr 9 bis 18 Uhr</span>
      <button class="entfernen" type="button" data-entfernen>Kalender wieder entfernen</button>
    </div>
  </section>

  <svg class="landschaft" viewBox="0 0 1440 240" preserveAspectRatio="xMinYMax slice" aria-hidden="true">
    <defs>
      <mask id="strasse-zeigen" maskUnits="userSpaceOnUse" x="-40" y="0" width="1520" height="260">
        <path class="strasse-maske" pathLength="1" d="M-30 166H382c22 0 22 30 0 30H176c-22 0-22 30 0 30H1480" fill="none" stroke="#fff" stroke-width="28"/>
      </mask>
    </defs>
    <path d="M0 150 70 118l46 12 74-58 52 30 66-60 50 34 40-16 62 44 70-38 64 40 72-30 62 36 70-42 58 30 72-44 60 34 74-28 64 36 78-46 54 26 70-20 70 26 82-30V240H0Z" fill="#e3e6c6" opacity=".55"/>
    <path d="M0 196c160-26 300-6 470-20s320-34 480-10 300-6 490-20V240H0Z" fill="#eef0d8"/>
    <g mask="url(#strasse-zeigen)">
      <path d="M-30 166H382c22 0 22 30 0 30H176c-22 0-22 30 0 30H1480" fill="none" stroke="#142b50" stroke-width="15" stroke-linejoin="round"/>
      <path d="M-30 166H382c22 0 22 30 0 30H176c-22 0-22 30 0 30H1480" fill="none" stroke="#f8f8ec" stroke-width="1.6" stroke-dasharray="7 9"/>
    </g>
  </svg>
</main>

<div class="werkzeug">
  <div class="varianten" role="group" aria-label="Variante">
    <span class="mono">Variante</span>
    <button type="button" data-v="platzhalter" aria-pressed="true">Platzhalter</button>
    <button type="button" data-v="vorauswahl" aria-pressed="false">Vorauswahl</button>
    <button type="button" data-v="linkout" aria-pressed="false">Link-out</button>
    <button type="button" data-v="hoehe" aria-pressed="false">Höhe wächst mit</button>
  </div>
  <p class="mono werte" data-werte></p>
</div>

<nav class="leiste" aria-label="Schnellzugriff">
  <a href="#buchung-titel">Anmelden</a>
  <a href="tel:+43512394720"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 3h3.5l1.8 4.6-2.4 1.6a11 11 0 0 0 6 6l1.6-2.4L20 14.6V18a2.5 2.5 0 0 1-2.7 2.5A17 17 0 0 1 3.5 5.7 2.5 2.5 0 0 1 5 3Z"/></svg>Anrufen</a>
  <a href="#"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 20l1.3-3.9A8 8 0 1 1 8 18.8Z"/></svg>WhatsApp</a>
</nav>

<div class="schleier" data-schleier></div>
<div class="tab" data-tab role="dialog" aria-modal="true" aria-label="Neuer Tab: Terminwerk">
  <div class="tab-leiste"><span class="tab-reiter"><i></i>Spitzkehre · Termin buchen | Terminwerk</span><button class="tab-zu" type="button" data-tab-zu aria-label="Tab schließen">×</button></div>
  <div class="tab-adresse"><span data-tab-url>termine.terminwerk.example/spitzkehre</span></div>
</div>

<svg class="zeiger" data-zeiger viewBox="0 0 22 22" aria-hidden="true"><circle class="ring" cx="3" cy="2" r="9" fill="none" stroke="#142b50" stroke-width="2"/><path d="M2.5 1.5 18 10.4l-6.9 1.5-3.4 6.5Z" fill="#142b50" stroke="#f8f8ec" stroke-width="1.4" stroke-linejoin="round"/></svg>

<script>
(() => {
  const html = document.documentElement;
  const reduziert = () => html.classList.contains('reduziert');
  const $ = s => document.querySelector(s);
  const buchung = $('[data-buchung]'), embed = $('[data-embed]'), platzhalter = $('[data-platzhalter]');
  const chip = $('[data-chip]'), werteZeile = $('[data-werte]'), urlZeile = $('[data-url]');
  const ladenKnopf = $('[data-laden]'), linkout = $('[data-linkout]'), merken = $('[data-merken]');
  const tab = $('[data-tab]'), schleier = $('[data-schleier]'), zeiger = $('[data-zeiger]');
  const MERK = 'spitzkehre-terminwerk';
  const LADEZEIT = 650;   // nachgestellte Ladezeit des Widgets (echt: Netz des Anbieters)

  const LEISTUNG = { B: ['Erste Fahrstunde', 'Klasse B', 72], A: ['Probefahrt Motorrad', 'Klasse A', 78], L17: ['Erste Fahrstunde L17', 'mit Begleitperson', 72], BE: ['Anhänger-Fahrstunde', 'Klasse BE', 84] };
  // Nur die Variante „Vorauswahl“ übergibt eigene Felder, sonst öffnet der Kalender mit Standardwerten
  const auswahl = () => variante !== 'vorauswahl' ? { klasse: 'B', ab: '2026-11-02' } : {
    klasse: (document.querySelector('[name=klasse]:checked') || {}).value || 'B',
    ab: (document.querySelector('[name=ab]:checked') || {}).value || '2026-11-02'
  };
  const parameter = a => `?klasse=${a.klasse}&leistung=${a.klasse === 'A' ? 'probefahrt' : 'erste-fahrstunde'}&ab=${a.ab}`;

  /* Das Widget des (erfundenen) Anbieters. In echt: iframe.src = Anbieter-URL aus data-src plus Parameter.
     Hier als srcdoc, damit die Demo eine Datei bleibt. Eigene Optik, eigene Schrift: es ist fremder Inhalt. */
  // natuerlich = Variante „Höhe passt sich an“: Widget ohne feste Höhe, mit Opt-in-Meta und requestResize()
  function widgetHTML(a, natuerlich = false) {
    const [titel, unter, preis] = LEISTUNG[a.klasse];
    const zweite = a.ab === '2026-11-09';
    const tage = zweite ? [['Mo', 9], ['Di', 10], ['Mi', 11], ['Do', 12], ['Fr', 13]] : [['Mo', 2], ['Di', 3], ['Mi', 4], ['Do', 5], ['Fr', 6]];
    const zeiten = zweite
      ? [['07:30', '15:20'], ['10:00', '13:00', '17:30'], ['08:20', '16:40'], ['14:30', '18:20'], ['09:10', '12:00', '15:20']]
      : [['13:00', '16:40'], ['09:10', '14:30', '17:30'], ['15:20'], ['07:30', '10:00', '16:40'], ['12:00', '14:30']];
    const spalten = tage.map((t, i) => `<div class="tag"><p><b>${t[0]}</b> ${t[1]}.</p>${zeiten[i].map(z => `<button type="button" class="slot" data-t="${t[0]}, ${t[1]}. Nov., ${z}">${z}</button>`).join('')}</div>`).join('');
    return `<!DOCTYPE html><html lang="de"><head><meta charset="utf-8">${natuerlich ? '<meta name="responsive-embedded-sizing" content="allow-origins=*">' : ''}<style>
*{box-sizing:border-box;margin:0}html{font:14px/1.4 system-ui,-apple-system,"Segoe UI",sans-serif;color:#1d2733;background:#fff}
body{${natuerlich ? '' : 'height:100vh;'}display:flex;flex-direction:column}${natuerlich ? '.n{margin:14px 0 12px}textarea{padding:9px 10px;border:1px solid #d5dae1;border-radius:7px;font:14px system-ui,sans-serif;resize:vertical}.c{display:flex!important;grid-template-columns:none;align-items:center;gap:8px!important}.c input{width:16px;height:16px;padding:0}.n2{padding:10px 12px;border-radius:8px;background:#f5f7f9;font-size:12px;color:#5b6573}' : ''}
.k{display:flex;justify-content:space-between;align-items:center;padding:12px 16px;border-bottom:1px solid #e6e9ee;font-size:12px;color:#5b6573}
.l{display:flex;align-items:center;gap:7px;font-weight:700;color:#0f766e;letter-spacing:-.01em;font-size:14px}.l i{width:14px;height:14px;border-radius:4px;background:#0f766e;display:block}
main{flex:1;padding:14px 16px 0;display:flex;flex-direction:column;min-height:0}
h1{font-size:17px;line-height:1.25}.u{color:#5b6573;font-size:12.5px;margin-top:2px}
.w{display:flex;justify-content:space-between;align-items:center;margin:12px 0 10px;font-weight:600;font-size:13px}
.w button{width:28px;height:28px;border:1px solid #d5dae1;border-radius:7px;background:#fff;color:#1d2733;cursor:pointer}
.g{display:grid;grid-template-columns:repeat(5,1fr);gap:6px;align-content:start}
.n{margin:auto 0 12px;padding:10px 12px;border-radius:8px;background:#f5f7f9;font-size:12px;color:#5b6573}
.tag p{font-size:12px;color:#5b6573;text-align:center;margin-bottom:6px}.tag p b{color:#1d2733}
.slot{display:block;width:100%;margin-bottom:6px;padding:7px 0;border:1px solid #b9d7d3;border-radius:7px;background:#f2faf9;color:#0b5d57;font:600 12.5px/1.2 system-ui,sans-serif;font-variant-numeric:tabular-nums;cursor:pointer;transition:background-color .15s,color .15s}
.slot:hover{background:#dff1ee}.slot[aria-pressed=true]{background:#0f766e;border-color:#0f766e;color:#fff}
.slot:focus-visible,.go:focus-visible,input:focus-visible{outline:2px solid #0f766e;outline-offset:2px}
.f{display:flex;align-items:center;justify-content:space-between;gap:10px;padding:12px 0;border-top:1px solid #e6e9ee;font-size:12.5px;color:#5b6573}
.f b{color:#1d2733}.go{padding:9px 16px;border:0;border-radius:8px;background:#0f766e;color:#fff;font:600 13px system-ui,sans-serif;cursor:pointer}.go:disabled{background:#c5ccd5;cursor:default}
.s2{display:none;flex-direction:column;gap:10px;flex:1}.s2 label{font-size:12.5px;color:#5b6573;display:grid;gap:4px}.s2 input{padding:9px 10px;border:1px solid #d5dae1;border-radius:7px;font:14px system-ui,sans-serif}
.zwei .g,.zwei .w,.zwei .n{display:none}.zwei .s2{display:flex}
.p{padding:8px 16px;border-top:1px solid #e6e9ee;font-size:11px;color:#8a93a0;display:flex;gap:12px}.p span:last-child{margin-left:auto}
</style></head><body>
<div class="k"><span class="l"><i></i>terminwerk</span><span>Fahrschule Spitzkehre · Innsbruck</span></div>
<main id="m"><h1>${titel} · ${unter}</h1><p class="u">50 Min. · € ${preis} · Treffpunkt Amraser Straße 41</p>
<div class="w"><button type="button" aria-label="Vorige Woche">‹</button><span>${zweite ? '9.–13. November' : '2.–6. November'} 2026</span><button type="button" aria-label="Nächste Woche">›</button></div>
<div class="g">${spalten}</div>
<p class="n">Bitte Ausweis und Brille (falls nötig) mitbringen. Kostenlos stornierbar bis 24 Stunden vorher.</p>
<div class="s2"><p style="font-weight:600">Schritt 2 von 3 · Kontakt</p><label>Vor- und Nachname<input autocomplete="name"></label><label>Telefon<input type="tel" autocomplete="tel"></label>${natuerlich ? '<label>E-Mail<input type="email" autocomplete="email"></label><label>Geburtsdatum<input autocomplete="bday" placeholder="TT.MM.JJJJ"></label><label>Anmerkung für den Fahrlehrer<textarea rows="3"></textarea></label><label class="c"><input type="checkbox"> Ich habe schon einen Führerschein (z. B. AM)</label><p class="n2">Wir melden uns innerhalb eines Werktags und bestätigen den Termin per SMS.</p>' : ''}</div>
<div class="f"><span id="wahl">Bitte Uhrzeit wählen</span><button class="go" type="button" disabled id="go">Weiter</button></div></main>
<div class="p"><span>Datenschutz</span><span>Impressum</span><span>Bereitgestellt von terminwerk</span></div>
<script>
const m=document.getElementById('m'),go=document.getElementById('go'),wahl=document.getElementById('wahl');
document.querySelectorAll('.slot').forEach(b=>b.addEventListener('click',()=>{document.querySelectorAll('.slot').forEach(x=>x.setAttribute('aria-pressed',x===b));wahl.innerHTML='<b>'+b.dataset.t+'</b>';go.disabled=false;}));
go.addEventListener('click',()=>{m.classList.add('zwei');go.textContent='Termin anfragen';${natuerlich ? "if('requestResize' in window)window.requestResize();" : ''}});
<\/script></body></html>`;
  }

  /* ── Zustand ── */
  let variante = 'platzhalter', zustand = 'leer', ladeUhr = null;

  function setzeZustand(z) {
    zustand = z; buchung.dataset.zustand = z;
    chip.textContent = z === 'leer' ? 'Nicht geladen · keine Daten an Dritte'
      : z === 'laedt' ? 'Verbinde mit Terminwerk …' : 'Geladen · Terminwerk aktiv';
    ladenKnopf.setAttribute('aria-busy', z === 'laedt');
    werte();
  }

  function laden({ nutzer = false } = {}) {
    if (zustand !== 'leer') return;
    setzeZustand('laedt');
    if (nutzer && merken.checked) { try { sessionStorage.setItem(MERK, '1'); } catch (e) {} }
    clearTimeout(ladeUhr);
    ladeUhr = setTimeout(() => {
      const f = document.createElement('iframe');
      f.className = 'widget';
      f.title = 'Online-Terminbuchung der Fahrschule Spitzkehre bei Terminwerk';
      f.srcdoc = widgetHTML(auswahl(), variante === 'hoehe');
      f.addEventListener('load', () => {
        if (zustand !== 'laedt') return;
        requestAnimationFrame(() => {
          setzeZustand('geladen');
          platzhalter.setAttribute('inert', '');
          if (nutzer) f.focus();
        });
      }, { once: true });
      embed.append(f);
    }, reduziert() ? 120 : LADEZEIT);
  }

  function entfernen() {
    clearTimeout(ladeUhr);
    embed.querySelectorAll('.widget').forEach(f => f.remove());
    platzhalter.removeAttribute('inert');
    try { sessionStorage.removeItem(MERK); } catch (e) {}
    setzeZustand('leer');
  }

  // Link-out: in echt öffnet der Link den Anbieter in einem neuen Tab. Hier ein nachgestellter Tab.
  let tabFrame = null;
  function tabOeffnen(nutzer) {
    const a = auswahl();
    $('[data-tab-url]').textContent = 'termine.terminwerk.example/spitzkehre' + parameter(a);
    tabFrame && tabFrame.remove();
    tabFrame = document.createElement('iframe');
    tabFrame.title = 'Terminwerk: Online-Terminbuchung';
    tabFrame.srcdoc = widgetHTML(a);
    tab.append(tabFrame);
    const r = embed.getBoundingClientRect();
    if (innerWidth > 560) {
      const w = Math.min(520, innerWidth - 32), h = Math.min(600, innerHeight - 48);
      tab.style.left = Math.max(16, Math.min(innerWidth - w - 16, r.left + r.width / 2 - w / 2 - 40)) + 'px';
      tab.style.top = Math.max(24, Math.min(innerHeight - h - 24, r.top - 30)) + 'px';
    }
    tab.classList.add('offen'); schleier.classList.add('offen');
    if (nutzer) tabFrame.addEventListener('load', () => $('[data-tab-zu]').focus(), { once: true });
    werte();
  }
  function tabSchliessen() {
    tab.classList.remove('offen'); schleier.classList.remove('offen');
    werte();
  }

  function setzeVariante(v) {
    if (zustand !== 'leer') entfernen();
    tabSchliessen();
    variante = v; buchung.dataset.variante = v;
    document.querySelectorAll('[data-v]').forEach(b => b.setAttribute('aria-pressed', b.dataset.v === v));
    werte();
  }

  const FRAME_SIZING = window.CSS && CSS.supports('frame-sizing', 'content-height');
  function werte() {
    const hoehe = embed.offsetHeight;
    const waechst = variante === 'hoehe' && FRAME_SIZING;
    const dritt = variante === 'linkout'
      ? (tab.classList.contains('offen') ? 'neuer Tab offen' : 'kein iframe auf der Seite')
      : (zustand === 'geladen' ? '<b>iframe geladen</b>' : zustand === 'laedt' ? 'lädt …' : '<b>0 Anfragen</b>');
    const teile = [
      variante !== 'hoehe' ? `Höhe <b>${hoehe} px</b> fest`
        : waechst ? `Höhe <b>${hoehe} px</b> · frame-sizing: content-height` : `Höhe <b>${hoehe} px</b> fest (Rückfall ohne frame-sizing)`,
      `Austausch <b>200 ms</b> cubic-bezier(0.4, 0, 0.2, 1)`,
      `Button <b>350 ms</b> cubic-bezier(0.34, 1.56, 0.64, 1)`,
      `Dritte: ${dritt}`
    ];
    if (variante === 'vorauswahl') teile.push(`<b>+ URL-Parameter</b>`);
    werteZeile.innerHTML = teile.join(' · ');
    $('[data-mass-text]').textContent = waechst && zustand === 'geladen' ? `Höhe passt sich an · ${hoehe} px` : `Höhe fest · ${hoehe} px`;
    const hh = $('[data-hoehe-hinweis]');
    hh.classList.toggle('aktiv', !!FRAME_SIZING);
    hh.lastElementChild.innerHTML = FRAME_SIZING
      ? 'Dieser Browser kann <code>frame-sizing: content-height</code>: Nach dem Laden wächst das iframe mit dem Widget, ohne Scrollen im Rahmen.'
      : 'Rückfall: Dieser Browser kennt <code>frame-sizing</code> nicht (erst ab Chrome 154). Die Höhe bleibt fest bei ' + hoehe + ' px, längere Schritte scrollen im Widget.';
  }
  if (window.ResizeObserver) new ResizeObserver(() => werte()).observe(embed);

  function vorauswahlAktualisieren() { urlZeile.textContent = parameter(auswahl()); werte(); }

  /* ── Bedienung ── */
  ladenKnopf.addEventListener('click', () => laden({ nutzer: true }));
  $('[data-entfernen]').addEventListener('click', () => { entfernen(); ladenKnopf.focus(); });
  linkout.addEventListener('click', e => { e.preventDefault(); tabOeffnen(true); });
  $('[data-tab-zu]').addEventListener('click', () => { tabSchliessen(); linkout.focus(); });
  schleier.addEventListener('click', tabSchliessen);
  addEventListener('keydown', e => { if (e.key === 'Escape' && tab.classList.contains('offen')) { tabSchliessen(); linkout.focus(); } });
  document.querySelectorAll('[name=klasse], [name=ab]').forEach(i => i.addEventListener('change', vorauswahlAktualisieren));
  document.querySelectorAll('[data-v]').forEach(b => b.addEventListener('click', () => {
    neustartVorfuehrung(); setzeVariante(b.dataset.v);
    const q = new URLSearchParams(location.search); q.set('v', b.dataset.v);   // Variante in der URL, andere Parameter bleiben
    history.replaceState(null, '', location.pathname + '?' + q + location.hash);
  }));
  document.querySelectorAll('a[href="#"]').forEach(a => a.addEventListener('click', e => e.preventDefault()));
  addEventListener('resize', werte);


  /* ── Einstieg: Marker und Straße zeichnen sich einmal ── */
  function intro() {
    html.classList.remove('start'); void html.offsetWidth;
    requestAnimationFrame(() => html.classList.add('start'));
  }

  /* ── Vorführung: gedachter Zeiger, pausierbar ── */
  let lauf = 0, pausiert = false, zx = 0, zy = 0;
  const abbruch = t => { if (t !== lauf) throw 'abbruch'; };
  function setzeZeiger(x, y) { zx = x; zy = y; zeiger.style.transform = `translate(${x - 3}px, ${y - 2}px)`; }
  function warte(ms, t) {
    return new Promise((ok, nein) => {
      let rest = ms, zuletzt = performance.now();
      const tick = () => {
        if (t !== lauf) return nein('abbruch');
        const jetzt = performance.now(); if (!pausiert) rest -= jetzt - zuletzt; zuletzt = jetzt;
        rest <= 0 ? ok() : setTimeout(tick, 30);
      };
      setTimeout(tick, 30);
    });
  }
  function fahre(x, y, ms, t) {
    if (reduziert()) { setzeZeiger(x, y); return warte(120, t); }
    const x0 = zx, y0 = zy; let verg = 0, zuletzt = performance.now();
    return new Promise((ok, nein) => {
      const f = jetzt => {
        if (t !== lauf) return nein('abbruch');
        if (!pausiert) verg += jetzt - zuletzt; zuletzt = jetzt;
        const p = Math.min(1, verg / ms), e = p < .5 ? 4 * p * p * p : 1 - Math.pow(-2 * p + 2, 3) / 2;
        setzeZeiger(x0 + (x - x0) * e, y0 + (y - y0) * e);
        p < 1 ? requestAnimationFrame(f) : ok();
      };
      requestAnimationFrame(f);
    });
  }
  const mitte = (el, frame) => {
    const r = el.getBoundingClientRect(), o = frame ? frame.getBoundingClientRect() : { left: 0, top: 0 };
    return [o.left + r.left + r.width * .55, o.top + r.top + r.height * .6];
  };
  async function klick(el, t, frame) {
    const [x, y] = mitte(el, frame);
    await fahre(x, y, 650, t);
    zeiger.classList.remove('klickt'); void zeiger.getBoundingClientRect(); zeiger.classList.add('klickt');
    el.classList.add('gedrueckt'); await warte(150, t); el.classList.remove('gedrueckt');
    el.click();
    if (document.activeElement && document.activeElement !== document.body) document.activeElement.blur();
  }
  async function szene(v, t) {
    setzeVariante(v);
    if (v === 'vorauswahl') { document.querySelector('[name=klasse][value=B]').checked = true; document.querySelector('[name=ab][value="2026-11-02"]').checked = true; vorauswahlAktualisieren(); }
    const r = embed.getBoundingClientRect();
    setzeZeiger(Math.min(innerWidth - 30, r.right + 40), Math.min(innerHeight - 30, r.bottom + 30));
    await warte(1100, t);
    if (v === 'vorauswahl') {
      await klick(document.querySelector('[name=klasse][value=L17]').nextElementSibling, t);
      await warte(350, t);
      await klick(document.querySelector('[name=ab][value="2026-11-09"]').nextElementSibling, t);
      await warte(450, t);
    }
    await klick(v === 'linkout' ? linkout : ladenKnopf, t);
    if (v === 'linkout') await warte(700, t);
    else { while (zustand !== 'geladen') await warte(60, t); await warte(500, t); }
    const frame = v === 'linkout' ? tabFrame : embed.querySelector('.widget');
    const doc = frame && frame.contentDocument;
    const slot = doc && doc.querySelectorAll('.slot')[v === 'vorauswahl' ? 2 : 3];
    if (slot) {
      await klick(slot, t, frame);
      await warte(450, t);
      const go = doc.getElementById('go');
      if (go && v === 'hoehe') { await klick(go, t, frame); await warte(300, t); }   // Schritt 2 ist länger: Rahmen wächst oder scrollt innen
      else if (go) { const [x, y] = mitte(go, frame); await fahre(x, y, 600, t); }
    }
    await warte(1700, t);
    if (v === 'linkout') { await klick($('[data-tab-zu]'), t); await warte(500, t); }
  }
  async function runde(t) {
    const folge = variante === 'hoehe' ? ['hoehe'] : ['platzhalter', 'vorauswahl', 'linkout'];
    let i = Math.max(0, folge.indexOf(variante));
    try { for (;;) { await szene(folge[i++ % folge.length], t); } } catch (e) { if (e !== 'abbruch') console.warn(e); }
  }
  function neustartVorfuehrung() { lauf++; pausiert = false; html.classList.remove('vorfuehrung'); }

  function neustart() {
    neustartVorfuehrung();
    entfernen(); tabSchliessen();
    intro();
  }
  function vorfuehren() {
    neustart();
    html.classList.add('vorfuehrung');
    const t = ++lauf;
    runde(t);
  }
  function pause() { pausiert = true; }
  function weiter() { if (html.classList.contains('vorfuehrung')) pausiert = false; else vorfuehren(); }

  const vStart = new URLSearchParams(location.search).get('v');
  setzeVariante(['platzhalter', 'vorauswahl', 'linkout', 'hoehe'].includes(vStart) ? vStart : 'platzhalter');
  // Bestehende Einwilligung für genau diesen Dienst (hier: für die Sitzung gemerkt) lädt sofort
  try { if (!new URLSearchParams(location.search).has('vorschau') && sessionStorage.getItem(MERK) === '1') { merken.checked = true; laden(); } } catch (e) {}
  intro();

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