Zum Inhalt
← Sektionen & Funktionen

Gutschein-Konfigurator

Gutschein in drei Schritten: Betrag oder Erlebnis wählen, Karte mit Motiv und persönlichem Satz live gestalten, Zustellung festlegen, dann per URL zum Zahlungsdienst; darunter die Code-Prüfung mit Restwert.

Ein Wert- oder Leistungsgutschein in drei Schritten. Erstens Betrag oder Leistung („Eine Nacht für zwei“, „Menü für zwei“, „Haarschnitt und Pflege“). Zweitens Motiv, Namen und ein persönlicher Satz, während daneben die Karte live mitwächst. Drittens die Zustellung: PDF sofort per E-Mail oder als Karte per Post. Bezahlt wird beim Shop oder Zahlungsdienst des Betriebs, die Auswahl geht per URL dorthin. Gültigkeit und Einlösebedingungen stehen als markierte Platzhalter da, die der Betrieb ausfüllt. Unter dem Formular prüft „Gutschein-Code prüfen“ Restwert und Gültigkeit.

Ein Gutschein ist ein Geschenk, also kauft man ihn mit dem Blick auf die Karte, die ankommt. Die Live-Vorschau macht aus einem Formular ein Geschenk und beantwortet die Frage „Wie sieht das aus?“ vor dem Bezahlen. Drei kurze Schritte mit sichtbarer Summe halten die Strecke überschaubar, und die Code-Prüfung spart dem Betrieb Anrufe.

Wann einsetzen

Gastronomie, Hotel, Friseur und Kosmetik, Fitness, Kultur, Fotografie, Manufakturen und Fahrschulen, also überall, wo Erlebnisse oder Leistungen verschenkt werden. Besonders vor Weihnachten, Muttertag und Valentinstag.

Wann nicht

Wenn der Betrieb keinen Zahlungsdienst und keine Codeverwaltung hat; dann reicht „Gutschein telefonisch bestellen“ mit Bild. Nicht als Ersatz für einen Shop mit vielen Artikeln (dafür warenkorb-drawer). Keine Gültigkeit oder Einlöseregel erfinden: die Angaben kommen vom Betrieb und werden vom Kunden geprüft.

Varianten

  • Wertgutschein: feste Beträge plus freier Betrag mit Grenzen
  • Leistungsgutschein: Leistungen mit Preis als Karten (Übernachtung, Menü, Behandlung, Probestunde)
  • Zustellung als PDF sofort, per Post als Karte oder zur Abholung vor Ort
  • Einlöse-Prüfung mit Restwert für Wertgutscheine

Umsetzung

Ein <form> mit drei <fieldset>-Schritten und einer Schrittanzeige als <ol> mit aria-current="step". Schritt 1 als Radiogruppe (Art) und Radiokarten (Betrag oder Leistung), freier Betrag als <input type=number> mit min, max und step und Fehlermeldung erst nach dem Verlassen. Schritt 2 mit Motiv-Radios, Namen und <textarea maxlength> samt Zeichenzähler; jede Eingabe (input-Ereignis) schreibt in die Vorschau-Karte daneben, Texte nur über textContent. Schritt 3 mit Zustellung (PDF, Post, Abholung) und den dazu nötigen Feldern. Die Summe steht dauerhaft in einer Zeile mit Intl.NumberFormat('de-DE', {style: 'currency', currency: 'EUR'}). „Weiter zur Bezahlung“ baut eine URL mit URLSearchParams (art, wert oder leistung, motiv, zustellung, Texte) und leitet zum Shop oder Zahlungsdienst des Betriebs; den Code erzeugt erst der Server nach der Zahlung. Die PDF-Vorschau ist dieselbe Karte in einem <dialog> plus @media print mit @page und window.print(), ganz ohne Bibliothek. Die Code-Prüfung fragt einen Server-Endpunkt (mit Ratenbegrenzung) und zeigt Restwert, Gültigkeit oder einen klaren Fehler; Codes nie im Browser vorhalten. Gültigkeit, Einlösung, Versandkosten und Widerrufshinweis sind Platzhalter des Betriebs und vom Kunden prüfen lassen.

Robust umsetzen

Telefon. Einspaltig: oben eine kleine, mitlaufende Vorschau der Karte, darunter der aktuelle Schritt. Summe und „Weiter“ in einer festen Leiste unten. Betragskarten im Zweierraster, große Trefferflächen, Tastatur passend zum Feld (inputmode=numeric, type=email).

Ohne Bewegung. Schritte und Motive wechseln ohne Gleiten und Überblenden. Die Vorschau aktualisiert sich sofort.

Barrierefrei. Jeder Schritt ist ein fieldset mit legend, die Schrittanzeige nennt „Schritt 2 von 3“. Nach „Weiter“ geht der Fokus auf die Überschrift des neuen Schritts. Fehler stehen am Feld (aria-describedby, aria-invalid) und werden beim Absenden gesammelt angesagt. Die Vorschau-Karte ist ein Bild mit Textalternative, die Namen, Betrag und Satz wiederholt; Live-Ansage nur für die Summe. Ergebnis der Code-Prüfung in einer role="status"-Zeile. Kontrast mindestens 4,5 : 1 auch auf den Motiven.

Code

<!DOCTYPE html>
<!--
  Gutschein-Konfigurator
  Fundus · Sektionen & Funktionen · https://cecodesign.de/lab/fundus/sektionen/gutschein-konfigurator/
  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>Gutschein-Konfigurator · 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; }
@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 boutique-hotel-gastgeber: Sand, Creme, warmes Dunkel, Hauston Holz */
:root {
  --grund: #f3eee7; --creme: #fef2e3; --tisch: #e9e2d7; --text: #3e3a37; --leise: #5c5853; --meta: #66675f; --linie: #d5d1c8; --linie-stark: #a9a397;
  --haus: #6e5a47; --haus-tief: #58483a; --haus-text: #fef2e3;
  --gut: #3f6a43; --fehler: #a3321f; --platz: #8a4b12;
  --ui-kurve: cubic-bezier(0.4, 0, 0.2, 1);       /* Buchungsstrecke 150–250 ms */
  --auf: cubic-bezier(0.16, 1, 0.3, 1);           /* öffnen 300 ms */
  --zu: cubic-bezier(0.7, 0, 0.84, 0);            /* schließen 200 ms */
  --weich: cubic-bezier(0.37, 0, 0.63, 1);        /* Motivwechsel 600 ms (token-luxus-weich) */
  --ui: 180ms; --oeffnen: 300ms; --schliessen: 200ms; --motiv: 600ms;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
html { background: var(--grund); color: var(--text); font: 400 17px/1.6 'Mona Sans', system-ui, sans-serif; font-stretch: 100%; -webkit-font-smoothing: antialiased; }
body { min-height: 100svh; }
button, input, textarea, select { font: inherit; color: inherit; }
.ziffern { font-variant-numeric: tabular-nums lining-nums; }
[hidden] { display: none !important; }
.vh { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
.label { font: 600 11.5px/1.3 'Mona Sans', sans-serif; font-stretch: 118%; letter-spacing: .07em; text-transform: uppercase; color: var(--meta); }
.mono { font: 500 10.5px/1.5 'Geist Mono', ui-monospace, monospace; letter-spacing: .1em; text-transform: uppercase; }
:focus-visible { outline: 2px solid var(--text); outline-offset: 3px; }
h1, h2, h3 { font-family: 'Newsreader', Georgia, serif; font-weight: 340; letter-spacing: -.015em; line-height: 1.08; font-variation-settings: 'opsz' 72; text-wrap: balance; }
em { font-style: italic; }

/* Kopf */
.kopf { display: flex; align-items: center; gap: 28px; padding: 16px clamp(18px, 3.4vw, 44px); border-bottom: 1px solid var(--linie); }
.marke { display: flex; align-items: center; gap: 12px; margin-right: auto; text-decoration: none; color: inherit; }
.marke svg { width: 34px; height: 34px; flex: none; }
.marke b { display: block; font: 400 22px/1 'Newsreader', serif; letter-spacing: -.01em; }
.marke small { display: block; font: 600 10px/1 'Mona Sans', sans-serif; font-stretch: 120%; letter-spacing: .1em; text-transform: uppercase; color: var(--meta); margin-top: 5px; }
.kopf nav ul { display: flex; gap: 24px; list-style: none; }
.kopf nav a { color: var(--leise); text-decoration: none; font-size: 15px; padding: 4px 0; border-bottom: 1px solid transparent; transition: color var(--ui) var(--ui-kurve), border-color var(--ui) var(--ui-kurve); }
.kopf nav a:hover, .kopf nav a[aria-current] { color: var(--text); border-color: var(--text); }
.knopf { display: inline-flex; align-items: center; justify-content: center; gap: 10px; min-height: 48px; padding: 0 22px; border-radius: 999px; border: 1px solid var(--haus); background: var(--haus); color: var(--haus-text); font: 600 12.5px/1 'Mona Sans', sans-serif; font-stretch: 118%; letter-spacing: .07em; text-transform: uppercase; text-decoration: none; cursor: pointer; transition: background-color var(--ui) var(--ui-kurve), color var(--ui) var(--ui-kurve), border-color var(--ui) var(--ui-kurve); }
.knopf:hover { background: var(--haus-tief); border-color: var(--haus-tief); }
.knopf.leer { background: transparent; color: var(--text); border-color: var(--linie-stark); }
.knopf.leer:hover { border-color: var(--text); background: rgba(62, 58, 55, .04); }
.knopf.klein { min-height: 40px; padding: 0 16px; }
.knopf svg { width: 16px; height: 16px; }
.knopf .kurz { display: none; }
.knopf[aria-busy="true"] { pointer-events: none; opacity: .75; }

/* Bühne */
.buehne { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1.02fr); gap: clamp(24px, 4vw, 72px); max-width: 1280px; margin: 0 auto; padding: clamp(26px, 4vw, 54px) clamp(18px, 3.4vw, 44px) 40px; align-items: start; }
.einleitung h1 { font-size: clamp(2.3rem, 1.4rem + 2.8vw, 3.9rem); margin-top: 10px; }
.einleitung h1 em { font-weight: 300; }
.einleitung p { color: var(--leise); max-width: 46ch; margin-top: 12px; }

/* Schrittanzeige */
.schritte-nav { margin: 28px 0 0; }
.schritte-nav ol { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; list-style: none; counter-reset: s; }
.schritte-nav li { counter-increment: s; }
.schritte-nav button { width: 100%; display: grid; gap: 6px; text-align: left; background: none; border: 0; padding: 0 0 2px; cursor: pointer; color: var(--meta); }
.schritte-nav button:disabled { cursor: default; }
.schritte-nav .strich { height: 2px; background: var(--linie); position: relative; overflow: hidden; }
.schritte-nav .strich::after { content: ''; position: absolute; inset: 0; background: var(--text); transform: scaleX(0); transform-origin: left; transition: transform var(--oeffnen) var(--auf); }
.schritte-nav [data-stand="fertig"] .strich::after, .schritte-nav [aria-current="step"] .strich::after { transform: scaleX(1); }
.schritte-nav [data-stand="fertig"] .strich::after { background: var(--leise); }
.schritte-nav .txt { font: 600 11.5px/1.3 'Mona Sans', sans-serif; font-stretch: 112%; letter-spacing: .06em; text-transform: uppercase; }
.schritte-nav .txt::before { content: counter(s) '  '; font-family: 'Geist Mono', monospace; font-weight: 500; }
.schritte-nav [aria-current="step"] { color: var(--text); }
.schritte-nav [data-stand="fertig"]:hover .txt { color: var(--text); text-decoration: underline; text-underline-offset: 3px; }

/* Schritte */
.formular { margin-top: 22px; }
.schritt { animation: schritt-auf var(--oeffnen) var(--auf) both; }
.schritt[hidden] { display: none; }
@keyframes schritt-auf { from { opacity: 0; translate: 0 12px; } }
.schritt.geht { animation: schritt-zu var(--schliessen) var(--zu) both; }
@keyframes schritt-zu { to { opacity: 0; translate: 0 -8px; } }
.schritt h2 { font-size: clamp(1.55rem, 1.2rem + 1vw, 2rem); margin-bottom: 16px; }
/* Überschrift bekommt nach „Weiter“ den Fokus (tabindex -1, nicht in der Tab-Folge): kein Rahmen nötig */
.schritt h2:focus, .schritt h2:focus-visible { outline: none; }
fieldset { border: 0; min-width: 0; display: grid; gap: 10px; }
fieldset + fieldset { margin-top: 22px; }
legend { margin-bottom: 10px; padding: 0; }
legend.label { color: var(--text); }

.art { display: inline-grid; grid-template-columns: 1fr 1fr; padding: 4px; border-radius: 999px; background: var(--tisch); width: fit-content; }
.art label { position: relative; padding: 9px 18px; border-radius: 999px; font-size: 15px; cursor: pointer; transition: background-color var(--ui) var(--ui-kurve), color var(--ui) var(--ui-kurve), box-shadow var(--ui) var(--ui-kurve); }
.art input { position: absolute; inset: 0; opacity: 0; cursor: pointer; margin: 0; }
.art label:has(input:checked) { background: var(--creme); box-shadow: 0 1px 0 rgba(62, 58, 55, .12), 0 0 0 1px var(--linie); font-weight: 600; }
.art label:has(input:focus-visible) { outline: 2px solid var(--text); outline-offset: 2px; }

.betraege { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 8px; }
.wahl { position: relative; display: grid; align-content: center; gap: 2px; min-height: 64px; padding: 12px 14px; border: 1px solid var(--linie); border-radius: 10px; background: var(--creme); cursor: pointer; transition: border-color var(--ui) var(--ui-kurve), background-color var(--ui) var(--ui-kurve), box-shadow var(--ui) var(--ui-kurve); }
.wahl input { position: absolute; inset: 0; opacity: 0; margin: 0; cursor: pointer; }
.wahl:hover { border-color: var(--linie-stark); }
.wahl:has(input:checked) { border-color: var(--text); box-shadow: inset 0 0 0 1px var(--text); }
.wahl:has(input:focus-visible) { outline: 2px solid var(--text); outline-offset: 3px; }
.wahl .zahl { font: 400 26px/1 'Newsreader', serif; font-variation-settings: 'opsz' 48; }
.wahl .zahl small { font-size: .6em; margin-left: 2px; }
.eigener { display: grid; grid-template-columns: auto 1fr; gap: 6px 14px; align-items: center; margin-top: 4px; }
.eigener label { font-size: 15px; color: var(--leise); }
.feld { position: relative; }
.feld input, .feld textarea { width: 100%; min-height: 48px; padding: 11px 14px; border: 1px solid var(--linie-stark); border-radius: 10px; background: var(--creme); transition: border-color var(--ui) var(--ui-kurve), box-shadow var(--ui) var(--ui-kurve); }
.feld textarea { min-height: 96px; resize: vertical; line-height: 1.5; }
.feld input:focus, .feld textarea:focus { outline: none; border-color: var(--text); box-shadow: 0 0 0 1px var(--text); }
.feld input:focus-visible, .feld textarea:focus-visible { outline: 2px solid var(--text); outline-offset: 2px; }
.feld .einheit { position: absolute; right: 14px; top: 50%; translate: 0 -50%; color: var(--meta); pointer-events: none; }
.feld input[aria-invalid="true"], .feld textarea[aria-invalid="true"] { border-color: var(--fehler); box-shadow: 0 0 0 1px var(--fehler); }
.fehler { color: var(--fehler); font-size: 14.5px; line-height: 1.4; display: flex; gap: 6px; }
.fehler:empty { display: none; }
.fehler::before { content: '!'; flex: none; width: 18px; height: 18px; border-radius: 50%; background: var(--fehler); color: #fff; font: 700 12px/18px 'Mona Sans', sans-serif; text-align: center; margin-top: 1px; }
.eigener .fehler { grid-column: 1 / -1; }

.leistungen { display: grid; gap: 8px; }
.leistungen .wahl { grid-template-columns: 1fr auto; gap: 2px 16px; padding: 14px 16px; }
.leistungen b { font-weight: 600; font-size: 16.5px; }
.leistungen span { font-size: 14.5px; color: var(--leise); grid-column: 1; }
.leistungen .preis { grid-column: 2; grid-row: 1 / span 2; align-self: center; font: 400 24px/1 'Newsreader', serif; font-variation-settings: 'opsz' 48; }

.motive { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 8px; }
.motiv { position: relative; display: grid; gap: 6px; cursor: pointer; }
.motiv input { position: absolute; inset: 0; opacity: 0; margin: 0; cursor: pointer; }
.motiv .bild { aspect-ratio: 3 / 4; border-radius: 8px; overflow: hidden; box-shadow: 0 0 0 1px var(--linie); transition: box-shadow var(--ui) var(--ui-kurve), translate var(--ui) var(--ui-kurve); }
.motiv .bild svg { width: 100%; height: 100%; display: block; }
.motiv span.name { font-size: 13.5px; line-height: 1.3; color: var(--leise); }
.motiv:hover .bild { translate: 0 -2px; }
.motiv:has(input:checked) .bild { box-shadow: 0 0 0 2px var(--grund), 0 0 0 4px var(--text); }
.motiv:has(input:checked) span.name { color: var(--text); font-weight: 600; }
.motiv:has(input:focus-visible) .bild { outline: 2px solid var(--text); outline-offset: 5px; }
.zwei { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
.feldgruppe { display: grid; gap: 6px; }
.feldgruppe > label { font-size: 15px; font-weight: 500; }
.feldgruppe > label small { color: var(--meta); font-weight: 400; }
.zaehler { font-size: 13.5px; color: var(--meta); text-align: right; }

.zustellung { display: grid; gap: 8px; }
.zustellung .wahl { grid-template-columns: 22px 1fr auto; gap: 2px 12px; align-items: start; padding: 14px 16px; }
.zustellung .punkt { width: 20px; height: 20px; border-radius: 50%; border: 1.5px solid var(--linie-stark); grid-row: span 2; margin-top: 2px; position: relative; transition: border-color var(--ui) var(--ui-kurve); }
.zustellung .punkt::after { content: ''; position: absolute; inset: 4px; border-radius: 50%; background: var(--text); transform: scale(0); transition: transform var(--ui) var(--ui-kurve); }
.zustellung .wahl:has(input:checked) .punkt { border-color: var(--text); }
.zustellung .wahl:has(input:checked) .punkt::after { transform: scale(1); }
.zustellung b { font-weight: 600; }
.zustellung span { font-size: 14.5px; color: var(--leise); }
.zustellung .preis { grid-column: 3; grid-row: 1; font-size: 15px; font-weight: 600; white-space: nowrap; }
.zusatzfelder { display: grid; gap: 12px; padding: 4px 0 2px; }
.zusatzfelder[hidden] { display: none; }

.platzhalter { border: 1px dashed var(--platz); border-radius: 10px; padding: 12px 14px; display: grid; gap: 6px; background: rgba(254, 242, 227, .6); }
.platzhalter .mono { color: var(--platz); }
.platzhalter p { font-size: 14.5px; line-height: 1.5; color: var(--leise); }
.platzhalter mark { background: none; color: var(--platz); border-bottom: 1px dashed currentColor; font-weight: 600; }

.summe-zeile { display: flex; justify-content: space-between; align-items: baseline; gap: 16px; padding: 14px 0; border-top: 1px solid var(--linie); border-bottom: 1px solid var(--linie); margin-top: 22px; }
.summe-zeile dl { display: grid; gap: 2px; font-size: 14.5px; color: var(--leise); }
.summe-zeile .betrag { font: 400 34px/1 'Newsreader', serif; font-variation-settings: 'opsz' 60; color: var(--text); white-space: nowrap; }
.aktionen { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; margin-top: 18px; }
.aktionen .rechts { margin-left: auto; }
.klein-hinweis { font-size: 13.5px; color: var(--meta); margin-top: 10px; max-width: 56ch; }

/* Übergabe und Bestätigung */
.uebergabe, .danke { border: 1px solid var(--linie); border-radius: 14px; background: var(--creme); padding: clamp(18px, 2.4vw, 28px); display: grid; gap: 14px; }
.uebergabe h2, .danke h2 { margin: 0; }
.url { font: 400 12.5px/1.6 'Geist Mono', monospace; background: #2f2c2a; color: #efe7db; border-radius: 10px; padding: 14px 16px; overflow-wrap: anywhere; }
.url span { color: #e7b98a; }
.url b { color: #fff; font-weight: 500; }
.code-gross { font: 500 clamp(22px, 2.6vw, 30px)/1 'Geist Mono', monospace; letter-spacing: .08em; padding: 14px 16px; border: 1px solid var(--linie-stark); border-radius: 10px; background: var(--grund); width: fit-content; }
.danke .haken { width: 40px; height: 40px; border-radius: 50%; background: var(--gut); display: grid; place-items: center; }
.danke .haken svg { width: 20px; height: 20px; }

/* Vorschau rechts */
.vorschau-spalte { position: sticky; top: 18px; display: grid; gap: 14px; }
.tisch { position: relative; background: var(--tisch); border-radius: 18px; padding: clamp(26px, 4vw, 54px) clamp(18px, 3vw, 44px); display: grid; place-items: center; overflow: hidden; }
.tisch::before { content: ''; position: absolute; inset: 0; background: radial-gradient(120% 80% at 30% 10%, rgba(255, 255, 255, .5), transparent 60%); pointer-events: none; }
.umschlag { position: absolute; width: 74%; aspect-ratio: 1.6; background: #e2d5c2; border-radius: 6px; box-shadow: 0 2px 0 rgba(62, 58, 55, .06); translate: 9% 16%; rotate: 4deg; opacity: 0; transition: opacity var(--oeffnen) var(--auf), translate var(--oeffnen) var(--auf); }
.umschlag::before { content: ''; position: absolute; inset: 0; background: linear-gradient(160deg, transparent 49.6%, rgba(62, 58, 55, .12) 50%, transparent 50.6%), linear-gradient(20deg, transparent 49.6%, rgba(62, 58, 55, .12) 50%, transparent 50.6%); }
.umschlag .marke-p { position: absolute; right: 8%; top: 12%; width: 13%; aspect-ratio: .82; background: repeating-linear-gradient(90deg, #9e4a2f 0 2px, #b45c3c 2px 4px); outline: 2px dotted #e2d5c2; outline-offset: -1px; }
.mit-post .umschlag { opacity: 1; translate: 7% 12%; }
.karte { position: relative; width: 100%; max-width: 560px; aspect-ratio: 1.414; background: var(--creme); border-radius: 6px; box-shadow: 0 1px 0 rgba(62, 58, 55, .08), 0 18px 40px -14px rgba(62, 58, 55, .35); display: grid; grid-template-columns: 42% 1fr; overflow: hidden; transition: rotate var(--oeffnen) var(--auf), translate var(--oeffnen) var(--auf); }
.mit-post .karte { rotate: -2.2deg; translate: -3% -4%; }
.karte .motivflaeche { position: relative; }
.karte .motivflaeche svg { position: absolute; inset: 0; width: 100%; height: 100%; display: block; transition: opacity var(--motiv) var(--weich); }
.karte .motivflaeche svg.weg { opacity: 0; }
.karte .text { display: grid; grid-template-rows: auto auto 1fr auto; padding: 7% 7% 6%; min-width: 0; container-type: inline-size; }
.karte .kopfzeile { display: flex; justify-content: space-between; gap: 8px; font: 600 clamp(7px, 3.6cqi, 10.5px)/1.2 'Mona Sans', sans-serif; font-stretch: 122%; letter-spacing: .12em; text-transform: uppercase; color: var(--meta); }
.karte .fuer { font: italic 300 clamp(14px, 8cqi, 27px)/1.1 'Newsreader', serif; margin-top: 7%; min-height: 1.1em; overflow-wrap: anywhere; }
.karte .wert { font: 340 clamp(26px, 19cqi, 62px)/.95 'Newsreader', serif; font-variation-settings: 'opsz' 72; letter-spacing: -.02em; margin-top: 4%; }
.karte .wert.leistung { font-size: clamp(16px, 9.6cqi, 30px); line-height: 1.05; }
.karte .wert small { display: block; font: 500 clamp(8px, 4cqi, 12px)/1.3 'Mona Sans', sans-serif; letter-spacing: 0; color: var(--leise); margin-top: 6px; }
.karte .satz { font: italic 300 clamp(10px, 5.2cqi, 16px)/1.35 'Newsreader', serif; color: var(--leise); margin-top: 6%; align-self: start; overflow-wrap: anywhere; display: -webkit-box; -webkit-line-clamp: 4; -webkit-box-orient: vertical; overflow: hidden; }
.karte .satz.leer { color: var(--meta); }
.karte .fussz { display: flex; justify-content: space-between; align-items: end; gap: 8px; border-top: 1px solid var(--linie); padding-top: 5%; font: 500 clamp(7px, 3.4cqi, 10.5px)/1.35 'Geist Mono', monospace; letter-spacing: .06em; color: var(--meta); }
.karte .fussz .von { font: italic 400 clamp(10px, 5cqi, 15px)/1.2 'Newsreader', serif; letter-spacing: 0; color: var(--text); }
.karte .fussz .pl { color: var(--platz); }
.unter-karte { display: flex; flex-wrap: wrap; justify-content: space-between; gap: 12px; align-items: baseline; font-size: 14px; color: var(--meta); }
.unter-karte b { color: var(--text); font-weight: 600; }

/* Code prüfen */
.pruefen { max-width: 1280px; margin: 0 auto; padding: 0 clamp(18px, 3.4vw, 44px) 40px; }
.pruefen-innen { border-top: 1px solid var(--linie); padding-top: clamp(26px, 3.4vw, 44px); display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1.02fr); gap: clamp(24px, 4vw, 72px); }
.pruefen h2 { font-size: clamp(1.7rem, 1.25rem + 1.3vw, 2.4rem); }
.pruefen p { color: var(--leise); margin-top: 8px; max-width: 46ch; }
.pruefen form { display: grid; gap: 8px; align-content: start; }
.pruefen .reihe { display: grid; grid-template-columns: 1fr auto; gap: 8px; }
.pruefen input { font-family: 'Geist Mono', monospace; letter-spacing: .08em; text-transform: uppercase; }
.pruefen .demo-codes { font-size: 13.5px; color: var(--meta); }
.pruefen .demo-codes button { background: none; border: 0; padding: 0; font: 500 13px/1.3 'Geist Mono', monospace; color: var(--text); text-decoration: underline; text-underline-offset: 3px; cursor: pointer; }
.ergebnis { min-height: 24px; }
.ergebnis .karte-klein { display: grid; grid-template-columns: auto 1fr; gap: 4px 14px; padding: 14px 16px; border-radius: 10px; border: 1px solid var(--linie); background: var(--creme); animation: schritt-auf var(--oeffnen) var(--auf) both; }
.ergebnis .karte-klein.gut { border-color: rgba(63, 106, 67, .5); }
.ergebnis .karte-klein.nein { border-color: rgba(163, 50, 31, .5); }
.ergebnis .zeichen { width: 26px; height: 26px; border-radius: 50%; display: grid; place-items: center; color: #fff; font-weight: 700; font-size: 14px; grid-row: span 2; }
.gut .zeichen { background: var(--gut); } .nein .zeichen { background: var(--fehler); } .alt .zeichen { background: var(--meta); }
.ergebnis b { font-weight: 600; }
.ergebnis span { font-size: 14.5px; color: var(--leise); grid-column: 2; }
.lade { display: inline-block; width: 14px; height: 14px; border-radius: 50%; border: 2px solid currentColor; border-right-color: transparent; animation: dreh 700ms linear infinite; }
@keyframes dreh { to { rotate: 1turn; } }

.werte-zeile { max-width: 1280px; margin: 0 auto; padding: 0 clamp(18px, 3.4vw, 44px) 120px; color: var(--meta); }

/* PDF-Vorschau */
dialog.pdf { margin: auto; border: 0; padding: 0; background: transparent; max-width: calc(100vw - 24px); max-height: calc(100dvh - 24px); overflow: visible; }
dialog.pdf::backdrop { background: rgba(46, 42, 39, .55); }
dialog.pdf[open] { animation: schritt-auf var(--oeffnen) var(--auf) both; }
.pdf-rahmen { display: grid; gap: 12px; justify-items: center; }
.blatt { width: min(460px, calc(100vw - 24px), calc((100dvh - 110px) * .707)); aspect-ratio: 210 / 297; background: #fff; box-shadow: 0 24px 60px rgba(0, 0, 0, .3); padding: 7% 8%; display: grid; grid-template-rows: auto auto 1fr auto; gap: 4%; container-type: inline-size; overflow: hidden; }
.blatt .karte { box-shadow: 0 0 0 1px var(--linie); max-width: none; }
.blatt .falz { border-top: 1px dashed #b9b2a6; position: relative; font: 500 clamp(6px, 2cqi, 9px)/1 'Geist Mono', monospace; color: #8b857b; letter-spacing: .1em; text-transform: uppercase; padding-top: 6px; }
.blatt .einloesen { font-size: clamp(8px, 2.9cqi, 13px); line-height: 1.5; color: var(--leise); display: grid; gap: .8em; align-content: start; }
.blatt .einloesen h3 { font-size: clamp(12px, 4.6cqi, 20px); color: var(--text); }
.blatt .absender { font-size: clamp(7px, 2.4cqi, 11px); color: var(--meta); border-top: 1px solid var(--linie); padding-top: 3%; }
.pdf-knoepfe { display: flex; gap: 8px; flex-wrap: wrap; justify-content: center; }
.pdf-knoepfe .knopf.leer { background: var(--creme); }

/* Zeiger der Vorführung */
.zeiger { position: fixed; left: 0; top: 0; z-index: 30; width: 22px; height: 22px; margin: -4px 0 0 -4px; pointer-events: none; opacity: 0; transition: translate 600ms var(--ui-kurve), opacity 200ms var(--ui-kurve); }
.zeiger.an { opacity: 1; }
.zeiger.klick { scale: .86; }
.reduziert .zeiger { transition: opacity 150ms linear; }

/* mobile Leiste */
.leiste { display: none; }

:root.reduziert *, :root.reduziert *::before, :root.reduziert *::after { transition-duration: .01ms !important; animation-duration: .01ms !important; }
.vorschau body { overflow: hidden; }
.vorschau .leiste, .vorschau .werte-zeile, .vorschau .einleitung > p:not(.label) { display: none !important; }
.vorschau .buehne { padding-top: 22px; }
.vorschau .einleitung h1 { font-size: 2.5rem; }
.vorschau .schritte-nav { margin-top: 18px; }

@media (max-width: 1100px) { .kopf nav { display: none; } }
@media (max-width: 860px) {
  .buehne { grid-template-columns: minmax(0, 1fr); }
  .vorschau-spalte { position: relative; top: 0; order: -1; }
  .einleitung { order: -2; }
  .buehne > .formteil { display: contents; }
  .tisch { padding: 22px 18px 28px; }
  .karte { max-width: 420px; }
  .pruefen-innen { grid-template-columns: minmax(0, 1fr); }
}
@media (max-width: 600px) {
  .kopf { gap: 12px; }
  .kopf .knopf { padding: 0 14px; min-height: 40px; white-space: nowrap; }
  .marke small { display: none; }
  .marke { min-width: 0; }
  .kopf .knopf .lang { display: none; }
  .kopf .knopf .kurz { display: inline; }
  .marke b { font-size: 20px; }
  .betraege { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .art { width: 100%; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }
  .art label { padding: 9px 10px; text-align: center; font-size: 14.5px; line-height: 1.3; }
  .motive { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .motiv .bild { aspect-ratio: 4 / 3; }
  .zwei { grid-template-columns: 1fr; }
  .eigener { grid-template-columns: 1fr; }
  .schritte-nav .txt { font-size: 10.5px; letter-spacing: .04em; }
  .aktionen .rechts { margin-left: 0; }
  .leiste { display: flex; align-items: center; gap: 12px; position: fixed; left: 0; right: 0; bottom: 0; z-index: 15; padding: 10px 14px calc(10px + env(safe-area-inset-bottom)); background: rgba(243, 238, 231, .96); border-top: 1px solid var(--linie); -webkit-backdrop-filter: blur(8px); backdrop-filter: blur(8px); }
  .leiste .summe { display: grid; line-height: 1.15; }
  .leiste .summe small { font-size: 12.5px; color: var(--meta); }
  .leiste .summe b { font: 400 24px/1.1 'Newsreader', serif; }
  .leiste .knopf { margin-left: auto; min-height: 46px; }
  .formular .aktionen .weiter-knopf { display: none; }
  .werte-zeile { padding-bottom: 110px; }
}
@media (hover: none) {
  .motiv:hover .bild { translate: none; }
}
@media (forced-colors: active) {
  button, [role=button], [role=tab], .knopf { border: 1px solid ButtonText !important; }
  .schritte-nav button, .demo-codes button { padding: 4px 6px !important; }
  .wahl:has(input:checked), .motiv:has(input:checked) .bild { outline: 3px solid Highlight; }
  .art label:has(input:checked) { outline: 2px solid Highlight; }
}
@media print {
  @page { size: A4; margin: 0; }
  html, body { background: #fff; }
  body > *:not(dialog.pdf) { display: none !important; }
  dialog.pdf { position: static; display: block; max-width: none; max-height: none; }
  dialog.pdf::backdrop { display: none; }
  .pdf-knoepfe { display: none; }
  .blatt { width: 210mm; height: 297mm; box-shadow: none; }
}
</style>
</head>
<body>
<header class="kopf">
  <a class="marke" href="#">
    <svg viewBox="0 0 34 34" aria-hidden="true"><circle cx="17" cy="17" r="16" fill="none" stroke="#3e3a37" stroke-width="1.2"/><path d="M7 24 L13 14 L17 20 L21 12 L27 24 Z" fill="#6e5a47"/><circle cx="23.5" cy="9.5" r="2" fill="#3e3a37"/></svg>
    <span><b>Haus Annengrund</b><small>Hotel &amp; Gasthaus · Eisenach</small></span>
  </a>
  <nav aria-label="Hauptmenü"><ul>
    <li><a href="#">Zimmer</a></li><li><a href="#">Gasthaus</a></li><li><a href="#">Umgebung</a></li><li><a href="#" aria-current="page">Gutscheine</a></li>
  </ul></nav>
  <a class="knopf klein" href="#"><span class="lang">Zimmer buchen</span><span class="kurz">Buchen</span></a>
</header>

<main class="buehne">
  <div class="formteil">
    <header class="einleitung">
      <p class="label">Gutscheine · Haus Annengrund</p>
      <h1>Verschenken Sie eine Auszeit <em>unter der Wartburg</em></h1>
      <p>Wählen Sie Betrag oder Erlebnis, gestalten Sie die Karte und entscheiden Sie, wie sie ankommt. Die Vorschau zeigt, was später im Briefkasten oder Postfach liegt.</p>
    </header>

    <nav class="schritte-nav" aria-label="Fortschritt">
      <ol>
        <li><button type="button" data-zu="1"><span class="strich"></span><span class="txt">Auswahl</span></button></li>
        <li><button type="button" data-zu="2"><span class="strich"></span><span class="txt">Karte</span></button></li>
        <li><button type="button" data-zu="3"><span class="strich"></span><span class="txt">Zustellung</span></button></li>
      </ol>
    </nav>

    <form class="formular" novalidate>
      <!-- Schritt 1 -->
      <section class="schritt" data-schritt="1" aria-labelledby="s1-titel">
        <h2 id="s1-titel" tabindex="-1">Was möchten Sie verschenken?</h2>
        <fieldset>
          <legend class="vh">Art des Gutscheins</legend>
          <div class="art">
            <label><input type="radio" name="art" value="wert" checked> Wert&shy;gutschein</label>
            <label><input type="radio" name="art" value="leistung"> Leistungs&shy;gutschein</label>
          </div>
        </fieldset>
        <fieldset data-teil="wert">
          <legend class="label">Betrag</legend>
          <div class="betraege ziffern">
            <label class="wahl"><input type="radio" name="betrag" value="50"><span class="zahl">50<small>€</small></span></label>
            <label class="wahl"><input type="radio" name="betrag" value="100" checked><span class="zahl">100<small>€</small></span></label>
            <label class="wahl"><input type="radio" name="betrag" value="150"><span class="zahl">150<small>€</small></span></label>
            <label class="wahl"><input type="radio" name="betrag" value="250"><span class="zahl">250<small>€</small></span></label>
          </div>
          <div class="eigener">
            <label for="eigen">oder eigener Betrag</label>
            <div class="feld"><input id="eigen" name="eigen" type="number" inputmode="numeric" min="25" max="1000" step="5" placeholder="z. B. 75" aria-describedby="eigen-fehler eigen-hilfe"><span class="einheit" aria-hidden="true">€</span></div>
            <p class="vh" id="eigen-hilfe">25 bis 1.000 Euro in 5-Euro-Schritten</p>
            <p class="fehler" id="eigen-fehler" role="alert"></p>
          </div>
        </fieldset>
        <fieldset data-teil="leistung" hidden>
          <legend class="label">Erlebnis</legend>
          <div class="leistungen">
            <label class="wahl"><input type="radio" name="leistung" value="nacht" checked><b>Eine Nacht für zwei</b><span>Doppelzimmer zur Waldseite, Frühstück, Check-out bis 13 Uhr</span><span class="preis ziffern">189 €</span></label>
            <label class="wahl"><input type="radio" name="leistung" value="menue"><b>Abendessen im Gasthaus</b><span>Vier Gänge für zwei, Getränke separat</span><span class="preis ziffern">128 €</span></label>
            <label class="wahl"><input type="radio" name="leistung" value="wochenende"><b>Auszeit-Wochenende</b><span>Zwei Nächte für zwei, ein Abendessen, Sauna im Gartenhaus</span><span class="preis ziffern">448 €</span></label>
          </div>
        </fieldset>
        <div class="aktionen"><button type="button" class="knopf weiter-knopf rechts" data-weiter="2">Weiter: Karte gestalten <svg viewBox="0 0 16 16" aria-hidden="true"><path d="M2 8h11M9 4l4 4-4 4" fill="none" stroke="currentColor" stroke-width="1.6"/></svg></button></div>
      </section>

      <!-- Schritt 2 -->
      <section class="schritt" data-schritt="2" aria-labelledby="s2-titel" hidden>
        <h2 id="s2-titel" tabindex="-1">Wie soll die Karte aussehen?</h2>
        <fieldset>
          <legend class="label">Motiv</legend>
          <div class="motive" data-motive></div>
        </fieldset>
        <fieldset>
          <legend class="vh">Namen und persönlicher Satz</legend>
          <div class="zwei">
            <div class="feldgruppe"><label for="fuer">Für <small>(optional)</small></label><div class="feld"><input id="fuer" name="fuer" maxlength="32" autocomplete="off" placeholder="z. B. Anna"></div></div>
            <div class="feldgruppe"><label for="von">Von <small>(optional)</small></label><div class="feld"><input id="von" name="von" maxlength="32" autocomplete="name" placeholder="z. B. Jonas"></div></div>
          </div>
          <div class="feldgruppe">
            <label for="satz">Persönlicher Satz <small>(optional)</small></label>
            <div class="feld"><textarea id="satz" name="satz" maxlength="140" rows="3" aria-describedby="satz-zaehler" placeholder="Ein Satz, der auf die Karte gedruckt wird"></textarea></div>
            <p class="zaehler ziffern" id="satz-zaehler" aria-live="polite">noch 140 Zeichen</p>
          </div>
        </fieldset>
        <div class="aktionen">
          <button type="button" class="knopf leer" data-weiter="1">Zurück</button>
          <button type="button" class="knopf weiter-knopf rechts" data-weiter="3">Weiter: Zustellung <svg viewBox="0 0 16 16" aria-hidden="true"><path d="M2 8h11M9 4l4 4-4 4" fill="none" stroke="currentColor" stroke-width="1.6"/></svg></button>
        </div>
      </section>

      <!-- Schritt 3 -->
      <section class="schritt" data-schritt="3" aria-labelledby="s3-titel" hidden>
        <h2 id="s3-titel" tabindex="-1">Wie soll der Gutschein ankommen?</h2>
        <fieldset>
          <legend class="vh">Zustellung</legend>
          <div class="zustellung">
            <label class="wahl"><input type="radio" name="weg" value="pdf" checked><i class="punkt"></i><b>Sofort als PDF per E-Mail</b><span>Zum Ausdrucken mit Faltlinie, direkt nach der Zahlung</span><span class="preis">kostenlos</span></label>
            <div class="zusatzfelder" data-fuer="pdf">
              <div class="feldgruppe"><label for="mail">E-Mail für das PDF</label><div class="feld"><input id="mail" name="mail" type="email" inputmode="email" autocomplete="email" placeholder="name@beispiel.de" aria-describedby="mail-fehler"></div><p class="fehler" id="mail-fehler"></p></div>
            </div>
            <label class="wahl"><input type="radio" name="weg" value="post"><i class="punkt"></i><b>Als Karte im Umschlag per Post</b><span>Auf Naturkarton gedruckt, Versand in 1 bis 2 Werktagen</span><span class="preis ziffern">+ 3,50 €</span></label>
            <div class="zusatzfelder" data-fuer="post" hidden>
              <div class="feldgruppe"><label for="name">Name der Empfängerin oder des Empfängers</label><div class="feld"><input id="name" name="name" autocomplete="shipping name" aria-describedby="name-fehler"></div><p class="fehler" id="name-fehler"></p></div>
              <div class="feldgruppe"><label for="strasse">Straße und Hausnummer</label><div class="feld"><input id="strasse" name="strasse" autocomplete="shipping street-address" aria-describedby="strasse-fehler"></div><p class="fehler" id="strasse-fehler"></p></div>
              <div class="feldgruppe"><label for="ort">PLZ und Ort</label><div class="feld"><input id="ort" name="ort" autocomplete="shipping address-level2" placeholder="99817 Eisenach" aria-describedby="ort-fehler"></div><p class="fehler" id="ort-fehler"></p></div>
            </div>
            <label class="wahl"><input type="radio" name="weg" value="abholen"><i class="punkt"></i><b>An der Rezeption abholen</b><span>Im Umschlag, täglich 7 bis 22 Uhr, Am Annengrund 9</span><span class="preis">kostenlos</span></label>
          </div>
        </fieldset>

        <div class="platzhalter" style="margin-top: 18px">
          <span class="mono">Platzhalter des Betriebs · vom Kunden prüfen lassen</span>
          <p>Gültig bis <mark>31.12. des dritten Jahres nach dem Kauf</mark>. Einlösbar für <mark>Übernachtungen, Gasthaus und Sauna</mark>, auch in Teilbeträgen. <mark>Keine Barauszahlung.</mark> Widerrufsbelehrung und AGB zeigt der Zahlungsdienst vor dem Bezahlen.</p>
        </div>

        <div class="summe-zeile">
          <dl data-aufstellung></dl>
          <p class="betrag ziffern" data-summe aria-live="polite"></p>
        </div>
        <div class="aktionen">
          <button type="button" class="knopf leer" data-weiter="2">Zurück</button>
          <button type="button" class="knopf leer" data-pdf>PDF-Vorschau</button>
          <button type="submit" class="knopf weiter-knopf rechts" data-bezahlen>Weiter zur Bezahlung <svg viewBox="0 0 16 16" aria-hidden="true"><path d="M2 8h11M9 4l4 4-4 4" fill="none" stroke="currentColor" stroke-width="1.6"/></svg></button>
        </div>
        <p class="klein-hinweis">Sie bezahlen beim Zahlungsdienst des Hauses. Den Gutschein-Code erzeugen wir erst nach der Zahlung.</p>
      </section>

      <!-- Übergabe (simuliert) -->
      <section class="schritt uebergabe" data-schritt="4" aria-labelledby="s4-titel" hidden>
        <p class="label">Übergabe an den Zahlungsdienst · in der Demo simuliert</p>
        <h2 id="s4-titel" tabindex="-1">Gleich geht es weiter zur Kasse</h2>
        <p>Im Einsatz leitet der Knopf direkt dorthin weiter. Die Auswahl reist in der Adresse mit:</p>
        <div class="url" data-url></div>
        <div class="aktionen">
          <button type="button" class="knopf leer" data-weiter="3">Auswahl ändern</button>
          <button type="button" class="knopf rechts" data-zahlung>Zahlung simulieren</button>
        </div>
      </section>

      <!-- Bestätigung -->
      <section class="schritt danke" data-schritt="5" aria-labelledby="s5-titel" hidden>
        <span class="haken" aria-hidden="true"><svg viewBox="0 0 20 20"><path d="M4 10.5 8.2 14.5 16 6" fill="none" stroke="#fff" stroke-width="2.2"/></svg></span>
        <h2 id="s5-titel" tabindex="-1">Danke, der Gutschein ist bezahlt</h2>
        <p data-danke-text></p>
        <p class="code-gross" data-code></p>
        <div class="aktionen">
          <button type="button" class="knopf" data-pdf>PDF öffnen</button>
          <button type="button" class="knopf leer" data-neu>Noch einen Gutschein gestalten</button>
        </div>
      </section>
    </form>
  </div>

  <aside class="vorschau-spalte" aria-labelledby="vorschau-titel">
    <h2 class="vh" id="vorschau-titel">Vorschau der Karte</h2>
    <div class="tisch" data-tisch>
      <div class="umschlag" aria-hidden="true"><span class="marke-p"></span></div>
      <div class="karte" role="img" data-karte>
        <div class="motivflaeche" data-motivflaeche></div>
        <div class="text">
          <div class="kopfzeile"><span>Gutschein</span><span>Haus Annengrund</span></div>
          <div>
            <p class="fuer" data-k-fuer></p>
            <p class="wert ziffern" data-k-wert></p>
          </div>
          <p class="satz" data-k-satz></p>
          <div class="fussz"><span class="von" data-k-von></span><span><span data-k-code>Code nach Zahlung</span><br><span class="pl">gültig bis 31.12.2029*</span></span></div>
        </div>
      </div>
    </div>
    <div class="unter-karte"><span data-format></span><span>* Platzhalter</span></div>
  </aside>
</main>

<section class="pruefen" aria-labelledby="pruefen-titel">
  <div class="pruefen-innen">
    <div>
      <h2 id="pruefen-titel">Gutschein-Code prüfen</h2>
      <p>Sie haben einen Gutschein bekommen? Hier sehen Sie Restwert und Gültigkeit, bevor Sie buchen.</p>
    </div>
    <form data-pruef novalidate>
      <label for="code" class="label">Code auf der Karte</label>
      <div class="reihe">
        <div class="feld"><input id="code" name="code" autocomplete="off" spellcheck="false" autocapitalize="characters" placeholder="ANN-XXXX-XXXX" maxlength="15" aria-describedby="code-ergebnis"></div>
        <button type="submit" class="knopf" data-pruef-knopf>Prüfen</button>
      </div>
      <p class="demo-codes">Zum Ausprobieren: <button type="button" data-beispiel="ANN-7K4P-2QXM">ANN-7K4P-2QXM</button> (Restwert) · <button type="button" data-beispiel="ANN-3HWT-9BCE">ANN-3HWT-9BCE</button> (eingelöst)</p>
      <div class="ergebnis" id="code-ergebnis" role="status"></div>
    </form>
  </div>
</section>

<p class="werte-zeile mono">Schritt öffnen 300 ms cubic-bezier(0.16, 1, 0.3, 1) · schließen 200 ms cubic-bezier(0.7, 0, 0.84, 0) · UI 180 ms cubic-bezier(0.4, 0, 0.2, 1) · Motivwechsel 600 ms cubic-bezier(0.37, 0, 0.63, 1)</p>

<div class="leiste" aria-label="Summe und nächster Schritt" role="region">
  <p class="summe"><small>Summe</small><b class="ziffern" data-leiste-summe></b></p>
  <button type="button" class="knopf" data-leiste-knopf></button>
</div>

<dialog class="pdf" aria-labelledby="pdf-titel">
  <div class="pdf-rahmen">
    <div class="blatt" data-blatt>
      <div data-blatt-karte></div>
      <div class="falz">hier falten</div>
      <div class="einloesen">
        <h3 id="pdf-titel">So lösen Sie den Gutschein ein</h3>
        <p>Nennen Sie den Code bei der Buchung am Telefon oder geben Sie ihn im Buchungsfeld „Gutschein“ ein. Im Gasthaus legen Sie die Karte einfach beim Bezahlen vor.</p>
        <p class="pl" style="color: var(--platz)">Gültig bis 31.12.2029, keine Barauszahlung (Platzhalter, vom Kunden prüfen lassen).</p>
      </div>
      <div class="absender">Haus Annengrund · Am Annengrund 9 · 99817 Eisenach · Telefon 03691 74 28 10</div>
    </div>
    <div class="pdf-knoepfe">
      <button type="button" class="knopf" data-drucken>Drucken oder als PDF sichern</button>
      <button type="button" class="knopf leer" data-pdf-zu>Schließen</button>
    </div>
  </div>
</dialog>

<svg class="zeiger" viewBox="0 0 22 22" aria-hidden="true" data-zeiger><path d="M3 2 L3 18 L7.5 14 L10.5 20.5 L13.2 19.3 L10.3 13 L16.5 13 Z" fill="#2e2a27" stroke="#fff" stroke-width="1.4" stroke-linejoin="round"/></svg>

<script>
(() => {
  const html = document.documentElement;
  const reduziert = () => html.classList.contains('reduziert');
  const $ = (s, r = document) => r.querySelector(s);
  const $$ = (s, r = document) => [...r.querySelectorAll(s)];
  const form = $('form.formular');
  const geld = n => new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR', minimumFractionDigits: n % 1 ? 2 : 0 }).format(n);

  const LEISTUNG = {
    nacht: { name: 'Eine Nacht für zwei', kurz: 'Doppelzimmer, Frühstück', preis: 189 },
    menue: { name: 'Abendessen im Gasthaus', kurz: 'vier Gänge für zwei', preis: 128 },
    wochenende: { name: 'Auszeit-Wochenende', kurz: 'zwei Nächte für zwei', preis: 448 }
  };
  const WEG = {
    pdf: { name: 'PDF per E-Mail', preis: 0, format: 'PDF, A4 mit Faltlinie' },
    post: { name: 'Karte per Post', preis: 3.5, format: 'Klappkarte A6 im Umschlag' },
    abholen: { name: 'Abholung an der Rezeption', preis: 0, format: 'Karte A6 im Umschlag' }
  };

  // ------------------------------------------------------------------
  // Motive: eigene SVG-Zeichnungen in den Farben des Hauses
  // ------------------------------------------------------------------
  const MOTIVE = {
    wald: { name: 'Waldweg im Nebel', svg: () => `<svg viewBox="0 0 200 280" preserveAspectRatio="xMidYMid slice" aria-hidden="true">
      <defs><linearGradient id="wh" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#dfe0d6"/><stop offset="1" stop-color="#b9bcae"/></linearGradient>
      <linearGradient id="wn" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#f1efe8" stop-opacity="0"/><stop offset=".6" stop-color="#f1efe8" stop-opacity=".85"/><stop offset="1" stop-color="#f1efe8" stop-opacity="0"/></linearGradient></defs>
      <rect width="200" height="280" fill="url(#wh)"/>
      ${[[18, 120, '#a7ab9c'], [58, 104, '#9ea392'], [150, 112, '#a3a797'], [186, 128, '#a9ad9e']].map(([x, y, f]) => `<path d="M${x} ${y} l-16 ${200 - y + 60} h32 Z" fill="${f}"/>`).join('')}
      <rect y="150" width="200" height="60" fill="url(#wn)"/>
      ${[[-4, 70, '#6f7765'], [40, 46, '#5f6857'], [168, 52, '#5c6554'], [206, 80, '#6a725f']].map(([x, y, f]) => `<path d="M${x} ${y} l-26 ${260 - y} h52 Z" fill="${f}"/><rect x="${x - 3}" y="250" width="6" height="30" fill="#4a4237"/>`).join('')}
      <path d="M86 280 C92 236 104 214 100 186 C98 214 112 236 128 280 Z" fill="#cdbfa8"/>
      <rect y="232" width="200" height="48" fill="url(#wn)" opacity=".6"/></svg>` },
    burg: { name: 'Burg bei Nacht', svg: () => `<svg viewBox="0 0 200 280" preserveAspectRatio="xMidYMid slice" aria-hidden="true">
      <defs><linearGradient id="bh" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#2c3340"/><stop offset="1" stop-color="#55566a"/></linearGradient></defs>
      <rect width="200" height="280" fill="url(#bh)"/>
      <circle cx="146" cy="58" r="15" fill="#f3e6c8"/><circle cx="152" cy="54" r="13" fill="#3a4050" opacity=".9"/>
      ${[[30, 40], [70, 22], [110, 70], [20, 110], [176, 120], [90, 36]].map(([x, y]) => `<circle cx="${x}" cy="${y}" r="1" fill="#f3e6c8" opacity=".8"/>`).join('')}
      <path d="M0 200 C40 170 70 160 100 156 C130 152 160 166 200 186 V280 H0 Z" fill="#30352d"/>
      <path d="M58 160 V122 h8 v-6 h6 v6 h8 V100 l6 -10 l6 10 v34 h22 v-14 h5 v-5 h5 v5 h5 v14 h8 v40 Z" fill="#1f2228"/>
      <rect x="86" y="108" width="3" height="6" fill="#f2b766"/><rect x="66" y="132" width="3" height="5" fill="#f2b766"/><rect x="108" y="140" width="3" height="5" fill="#f2b766"/><rect x="120" y="140" width="3" height="5" fill="#f2b766" opacity=".7"/>
      <path d="M0 236 C50 214 120 220 200 232 V280 H0 Z" fill="#242820"/>
      ${[[24, 228], [44, 222], [170, 230], [188, 236]].map(([x, y]) => `<path d="M${x} ${y - 30} l-9 32 h18 Z" fill="#1c1f19"/>`).join('')}</svg>` },
    kamin: { name: 'Kaminzimmer', svg: () => `<svg viewBox="0 0 200 280" preserveAspectRatio="xMidYMid slice" aria-hidden="true">
      <defs><radialGradient id="kg" cx=".5" cy=".62" r=".6"><stop offset="0" stop-color="#8a5a37"/><stop offset="1" stop-color="#3b2c22"/></radialGradient></defs>
      <rect width="200" height="280" fill="url(#kg)"/>
      <rect x="40" y="96" width="120" height="132" fill="#d8c8b1"/><rect x="34" y="88" width="132" height="12" fill="#c7b69d"/>
      <path d="M58 228 V150 a42 42 0 0 1 84 0 V228 Z" fill="#241a14"/>
      <path d="M78 224 C76 200 92 192 88 172 C100 186 104 194 102 206 C108 198 110 190 108 180 C122 196 124 214 118 224 Z" fill="#e48a3c"/>
      <path d="M90 224 C90 210 98 206 98 196 C106 206 110 214 106 224 Z" fill="#f6c46f"/>
      <rect x="70" y="222" width="60" height="6" fill="#5b3a26"/>
      <rect y="228" width="200" height="52" fill="#4a3628"/><path d="M0 252 H200" stroke="#3b2c22" stroke-width="2"/>
      <path d="M150 280 v-40 q0 -22 22 -22 h28 v62 Z" fill="#6e5a47"/><path d="M146 246 h40" stroke="#5a4838" stroke-width="10" stroke-linecap="round"/></svg>` },
    fruehstueck: { name: 'Frühstück am Fenster', svg: () => `<svg viewBox="0 0 200 280" preserveAspectRatio="xMidYMid slice" aria-hidden="true">
      <rect width="200" height="280" fill="#efe4d2"/>
      <rect x="30" y="34" width="140" height="150" fill="#b9c3a6"/><path d="M30 120 C70 96 120 112 170 92 V184 H30 Z" fill="#8f9c7c"/><path d="M30 150 C80 132 130 150 170 136 V184 H30 Z" fill="#76845f"/>
      <path d="M100 34 V184 M30 109 H170" stroke="#f6efe3" stroke-width="6"/><rect x="30" y="34" width="140" height="150" fill="none" stroke="#f6efe3" stroke-width="8"/>
      <rect x="0" y="196" width="200" height="84" fill="#d8c3a5"/><path d="M0 196 H200" stroke="#c4ad8c" stroke-width="3"/>
      <ellipse cx="70" cy="226" rx="34" ry="8" fill="#f8f3ea"/><path d="M52 200 h34 l-4 24 h-26 Z" fill="#f8f3ea"/><path d="M86 206 q12 2 0 14" fill="none" stroke="#f8f3ea" stroke-width="4"/><ellipse cx="69" cy="201" rx="16" ry="3" fill="#7a5034"/>
      <ellipse cx="140" cy="232" rx="30" ry="9" fill="#f8f3ea"/><ellipse cx="134" cy="222" rx="15" ry="9" fill="#c98a4b"/><path d="M126 218 q8 -4 16 0" stroke="#a8692f" stroke-width="2" fill="none"/><ellipse cx="152" cy="226" rx="8" ry="5" fill="#f3c561"/></svg>` }
  };
  const motivBox = $('[data-motive]');
  Object.entries(MOTIVE).forEach(([id, m], i) => {
    const l = document.createElement('label'); l.className = 'motiv';
    l.innerHTML = `<input type="radio" name="motiv" value="${id}"${i === 0 ? ' checked' : ''}><span class="bild">${m.svg().replace(/id="(\w+)"/g, 'id="t-$1"').replace(/url\(#(\w+)\)/g, 'url(#t-$1)')}</span><span class="name">${m.name}</span>`;
    motivBox.append(l);
  });

  // ------------------------------------------------------------------
  // Zustand lesen, Vorschau und Summe schreiben
  // ------------------------------------------------------------------
  const wert = n => form.elements[n].value;
  function auswahl() {
    const art = wert('art');
    const eigen = form.elements.eigen;
    let betrag = 0, titel = '', zeile = '';
    if (art === 'wert') {
      const e = parseFloat(eigen.value);
      betrag = eigen.value !== '' && !isNaN(e) ? e : parseFloat(wert('betrag') || 0);
      titel = geld(betrag); zeile = 'Wertgutschein';
    } else {
      const L = LEISTUNG[wert('leistung')]; betrag = L.preis; titel = L.name; zeile = L.kurz;
    }
    const weg = WEG[wert('weg')];
    return { art, betrag, titel, zeile, weg, wegId: wert('weg'), motiv: wert('motiv'), fuer: form.elements.fuer.value.trim(), von: form.elements.von.value.trim(), satz: form.elements.satz.value.trim(), summe: betrag + weg.preis };
  }
  let letztesMotiv = null;
  function karteSchreiben(ziel, a, mitMotiv = true) {
    const k = ziel;
    if (mitMotiv) {
      const flaeche = $('[data-motivflaeche]', k);
      if (flaeche.dataset.motiv !== a.motiv) {
        const alt = flaeche.querySelector('svg:not(.weg)');
        const tmp = document.createElement('div'); tmp.innerHTML = MOTIVE[a.motiv].svg().replace(/id="(\w+)"/g, `id="${k.dataset.praefix || 'k'}-$1"`).replace(/url\(#(\w+)\)/g, `url(#${k.dataset.praefix || 'k'}-$1)`);
        const neu = tmp.firstElementChild;
        if (alt && !reduziert()) { neu.classList.add('weg'); flaeche.append(neu); requestAnimationFrame(() => requestAnimationFrame(() => { neu.classList.remove('weg'); alt.classList.add('weg'); })); setTimeout(() => alt.remove(), 650); }
        else { flaeche.replaceChildren(neu); }
        flaeche.dataset.motiv = a.motiv;
      }
    }
    $('[data-k-fuer]', k).textContent = a.fuer ? 'für ' + a.fuer : 'für …';
    const w = $('[data-k-wert]', k);
    w.classList.toggle('leistung', a.art === 'leistung');
    w.replaceChildren(a.titel, Object.assign(document.createElement('small'), { textContent: a.art === 'leistung' ? a.zeile : 'einlösbar im ganzen Haus' }));
    const s = $('[data-k-satz]', k);
    s.textContent = a.satz ? '„' + a.satz + '“' : 'Hier steht Ihr persönlicher Satz.';
    s.classList.toggle('leer', !a.satz);
    $('[data-k-von]', k).textContent = a.von ? 'von ' + a.von : '';
    $('[data-k-code]', k).textContent = code || 'Code nach Zahlung';
    k.setAttribute('aria-label', `Gutschein-Karte, Motiv ${MOTIVE[a.motiv].name}: ${a.fuer ? 'für ' + a.fuer + ', ' : ''}${a.titel}${a.art === 'leistung' ? ' (' + a.zeile + ')' : ''}${a.satz ? ', Satz: ' + a.satz : ''}${a.von ? ', von ' + a.von : ''}.`);
  }
  const karte = $('[data-karte]');
  function aktualisieren() {
    const a = auswahl();
    karteSchreiben(karte, a);
    $('[data-tisch]').classList.toggle('mit-post', a.wegId === 'post');
    $('[data-format]').replaceChildren('Vorschau · ', Object.assign(document.createElement('b'), { textContent: a.weg.format }));
    $('[data-aufstellung]').replaceChildren(
      Object.assign(document.createElement('div'), { textContent: (a.art === 'wert' ? 'Wertgutschein ' : a.titel + ' ') + geld(a.betrag) }),
      Object.assign(document.createElement('div'), { textContent: a.weg.name + (a.weg.preis ? ' ' + geld(a.weg.preis) : ', kostenlos') })
    );
    $('[data-summe]').textContent = geld(a.summe);
    $('[data-leiste-summe]').textContent = geld(a.summe);
    const satz = form.elements.satz; $('#satz-zaehler').textContent = 'noch ' + (140 - satz.value.length) + ' Zeichen';
    return a;
  }

  // ------------------------------------------------------------------
  // Schritte
  // ------------------------------------------------------------------
  let schritt = 1, erreicht = 1, code = '';
  const navKnoepfe = $$('.schritte-nav button');
  const LEISTE_TEXT = { 1: 'Weiter', 2: 'Weiter', 3: 'Zur Bezahlung', 4: 'Zahlung simulieren', 5: 'Neuer Gutschein' };
  let wechselUhr = 0;
  function zeigeSchritt(n, fokus = true) {
    const alt = $(`.schritt[data-schritt="${schritt}"]`), neu = $(`.schritt[data-schritt="${n}"]`);
    clearTimeout(wechselUhr);
    $$('.schritt').forEach(s => { s.classList.remove('geht'); if (s !== alt) s.hidden = true; });
    const oeffnen = () => {
      $$('.schritt').forEach(s => { s.hidden = s !== neu; s.classList.remove('geht'); });
      neu.style.animation = 'none'; neu.offsetHeight; neu.style.animation = '';
      if (fokus) { const h = neu.querySelector('h2'); h.focus({ preventScroll: true }); const r = neu.getBoundingClientRect(); if (r.top < 0 || r.top > innerHeight * .6) rolle(r.top + scrollY - 90, 500); }
    };
    // Schließen 200 ms mit Beschleunigung, dann öffnen 300 ms mit Ausklang
    if (alt && alt !== neu && !alt.hidden && fokus && !reduziert()) { alt.classList.add('geht'); wechselUhr = setTimeout(oeffnen, 200); }
    else oeffnen();
    schritt = n; erreicht = Math.max(erreicht, Math.min(n, 3));
    navKnoepfe.forEach((b, i) => {
      const s = i + 1;
      if (s === Math.min(n, 3) && n <= 3) b.setAttribute('aria-current', 'step'); else b.removeAttribute('aria-current');
      b.dataset.stand = (n > 3 || s < n) ? 'fertig' : (s === n ? 'jetzt' : 'offen');
      b.disabled = n === 5 || (s > erreicht);
      b.setAttribute('aria-label', `Schritt ${s} von 3: ${b.textContent.trim()}${b.dataset.stand === 'fertig' ? ', erledigt' : ''}`);
    });
    $('[data-leiste-knopf]').textContent = LEISTE_TEXT[n];
  }
  navKnoepfe.forEach((b, i) => b.addEventListener('click', () => { if (!b.disabled && i + 1 !== schritt) zeigeSchritt(i + 1); }));

  // Validierung erst nach dem Verlassen eines Feldes
  function fehler(feld, text) {
    const f = document.getElementById(feld.id + '-fehler');
    feld.setAttribute('aria-invalid', text ? 'true' : 'false');
    if (f) f.textContent = text || '';
    return !text;
  }
  function pruefeEigen() {
    const e = form.elements.eigen; if (e.value === '') return fehler(e, '');
    const n = Number(e.value);
    return fehler(e, (isNaN(n) || n < 25 || n > 1000 || n % 5) ? 'Bitte einen Betrag von 25 bis 1.000 € in 5-€-Schritten eingeben.' : '');
  }
  const pruefe = {
    mail: e => fehler(e, !e.value.trim() ? 'Bitte geben Sie eine E-Mail-Adresse für das PDF an.' : !/^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(e.value.trim()) ? 'Diese E-Mail-Adresse sieht unvollständig aus, z. B. name@beispiel.de.' : ''),
    name: e => fehler(e, e.value.trim() ? '' : 'Bitte den Namen für den Umschlag angeben.'),
    strasse: e => fehler(e, e.value.trim() ? '' : 'Bitte Straße und Hausnummer angeben.'),
    ort: e => fehler(e, /^\d{5}\s+\S/.test(e.value.trim()) ? '' : 'Bitte PLZ (5 Ziffern) und Ort angeben, z. B. 99817 Eisenach.')
  };
  form.elements.eigen.addEventListener('blur', pruefeEigen);
  form.elements.eigen.addEventListener('input', () => {
    if (form.elements.eigen.value !== '') $$('input[name="betrag"]').forEach(r => r.checked = false);
    if (form.elements.eigen.getAttribute('aria-invalid') === 'true') pruefeEigen();
  });
  $$('input[name="betrag"]').forEach(r => r.addEventListener('change', () => { form.elements.eigen.value = ''; fehler(form.elements.eigen, ''); aktualisieren(); }));
  Object.keys(pruefe).forEach(id => {
    const e = form.elements[id];
    e.addEventListener('blur', () => { if (e.value !== '' || e.dataset.beruehrt) pruefe[id](e); e.dataset.beruehrt = 1; });
    e.addEventListener('input', () => { if (e.getAttribute('aria-invalid') === 'true') pruefe[id](e); });
  });

  form.addEventListener('input', aktualisieren);
  form.addEventListener('change', e => {
    if (e.target.name === 'art') {
      const art = wert('art');
      $('[data-teil="wert"]').hidden = art !== 'wert';
      $('[data-teil="leistung"]').hidden = art !== 'leistung';
    }
    if (e.target.name === 'weg') $$('.zusatzfelder').forEach(z => z.hidden = z.dataset.fuer !== wert('weg'));
    aktualisieren();
  });

  function weiterVon(n) {
    if (n === 1 && wert('art') === 'wert') {
      if (!pruefeEigen()) { form.elements.eigen.focus(); return false; }
      if (!auswahl().betrag) { form.elements.eigen.focus(); fehler(form.elements.eigen, 'Bitte einen Betrag wählen oder eingeben.'); return false; }
    }
    if (n === 3) {
      const ids = wert('weg') === 'pdf' ? ['mail'] : wert('weg') === 'post' ? ['name', 'strasse', 'ort'] : [];
      const falsch = ids.filter(id => !pruefe[id](form.elements[id]));
      if (falsch.length) { form.elements[falsch[0]].focus(); return false; }
    }
    return true;
  }
  $$('[data-weiter]').forEach(b => b.addEventListener('click', () => {
    const ziel = +b.dataset.weiter;
    if (ziel > schritt && !weiterVon(schritt)) return;
    zeigeSchritt(ziel);
  }));

  // Übergabe per URL (simuliert)
  function uebergabeUrl(a) {
    const q = new URLSearchParams({ haus: 'annengrund', art: a.art });
    if (a.art === 'wert') q.set('betrag', a.betrag.toFixed(2)); else q.set('leistung', wert('leistung'));
    q.set('motiv', a.motiv); q.set('zustellung', a.wegId);
    if (a.fuer) q.set('fuer', a.fuer); if (a.von) q.set('von', a.von); if (a.satz) q.set('satz', a.satz);
    if (a.wegId === 'pdf') q.set('email', form.elements.mail.value.trim());
    q.set('summe', a.summe.toFixed(2)); q.set('zurueck', 'https://haus-annengrund.example/gutscheine/danke');
    return 'https://zahlung.example/kasse?' + q.toString();
  }
  form.addEventListener('submit', e => {
    e.preventDefault();
    if (schritt !== 3 || !weiterVon(3)) return;
    const knopf = $('[data-bezahlen]'); knopf.setAttribute('aria-busy', 'true');
    setTimeout(() => {
      knopf.removeAttribute('aria-busy');
      const url = uebergabeUrl(auswahl());
      const [basis, query] = url.split('?');
      const box = $('[data-url]'); box.replaceChildren(basis + '?');
      query.split('&').forEach((p, i) => { const [k, v] = p.split('='); box.append(document.createElement('br'), Object.assign(document.createElement('span'), { textContent: (i ? '&' : '') + k }), '=', Object.assign(document.createElement('b'), { textContent: v })); });
      zeigeSchritt(4);
    }, reduziert() ? 0 : 450);
  });
  const ZEICHEN = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
  const block = () => Array.from({ length: 4 }, () => ZEICHEN[Math.floor(Math.random() * ZEICHEN.length)]).join('');
  $('[data-zahlung]').addEventListener('click', () => {
    code = 'ANN-' + block() + '-' + block();
    const a = aktualisieren();
    $('[data-code]').textContent = code;
    $('[data-danke-text]').textContent = a.wegId === 'pdf' ? `Das PDF ist an ${form.elements.mail.value.trim()} unterwegs. Sie können es hier auch gleich öffnen.` : a.wegId === 'post' ? 'Die Karte geht in 1 bis 2 Werktagen in den Versand. Den Code haben Sie hier und per E-Mail.' : 'Die Karte liegt ab morgen an der Rezeption für Sie bereit.';
    zeigeSchritt(5);
  });
  function zuruecksetzen() {
    form.reset(); code = ''; erreicht = 1;
    $('[data-teil="wert"]').hidden = false; $('[data-teil="leistung"]').hidden = true;
    $$('.zusatzfelder').forEach(z => z.hidden = z.dataset.fuer !== 'pdf');
    $$('[aria-invalid]').forEach(e => fehler(e, '')); $$('[data-beruehrt]').forEach(e => delete e.dataset.beruehrt);
    aktualisieren();
  }
  $('[data-neu]').addEventListener('click', () => { zuruecksetzen(); zeigeSchritt(1); });
  $('[data-leiste-knopf]').addEventListener('click', () => {
    if (schritt < 3) $(`.schritt[data-schritt="${schritt}"] [data-weiter="${schritt + 1}"]`).click();
    else if (schritt === 3) form.requestSubmit();
    else if (schritt === 4) $('[data-zahlung]').click();
    else $('[data-neu]').click();
  });

  // PDF-Vorschau: dieselbe Karte auf A4, Drucken über print-CSS
  const dlg = $('dialog.pdf');
  $$('[data-pdf]').forEach(b => b.addEventListener('click', () => {
    const kopie = karte.cloneNode(true); kopie.dataset.praefix = 'p';
    $('[data-motivflaeche]', kopie).replaceChildren(); delete $('[data-motivflaeche]', kopie).dataset.motiv;
    kopie.removeAttribute('role');
    $('[data-blatt-karte]').replaceChildren(kopie);
    karteSchreiben(kopie, auswahl());
    kopie.setAttribute('role', 'img');
    dlg.showModal();
  }));
  $('[data-pdf-zu]').addEventListener('click', () => dlg.close());
  $('[data-drucken]').addEventListener('click', () => window.print());
  dlg.addEventListener('click', e => { if (e.target === dlg) dlg.close(); });

  // ------------------------------------------------------------------
  // Gutschein-Code prüfen (im Einsatz: Server-Endpunkt mit Ratenbegrenzung)
  // ------------------------------------------------------------------
  const DEMO_CODES = {
    'ANN-7K4P-2QXM': { art: 'gut', titel: 'Gültig · Restwert 62,00 €', text: 'Wertgutschein über 150 €, gekauft am 12.12.2025. Gültig bis 31.12.2028.' },
    'ANN-3HWT-9BCE': { art: 'alt', titel: 'Bereits eingelöst', text: 'Leistungsgutschein „Eine Nacht für zwei“, eingelöst am 14.09.2026. Fragen? Telefon 03691 74 28 10.' }
  };
  const pruefForm = $('[data-pruef]'), codeFeld = $('#code'), ergebnis = $('#code-ergebnis'), pruefKnopf = $('[data-pruef-knopf]');
  function normal(t) { const r = t.toUpperCase().replace(/[^A-Z0-9]/g, ''); const k = r.startsWith('ANN') ? r.slice(3) : r; return k.length ? 'ANN-' + k.slice(0, 4) + (k.length > 4 ? '-' + k.slice(4, 8) : '') : ''; }
  function zeigeErgebnis(art, titel, text) {
    const d = document.createElement('div'); d.className = 'karte-klein ' + art;
    d.append(Object.assign(document.createElement('span'), { className: 'zeichen', textContent: art === 'gut' ? '✓' : art === 'nein' ? '!' : '–', ariaHidden: 'true' }), Object.assign(document.createElement('b'), { textContent: titel }), Object.assign(document.createElement('span'), { textContent: text }));
    ergebnis.replaceChildren(d);
  }
  codeFeld.addEventListener('blur', () => { if (codeFeld.value) codeFeld.value = normal(codeFeld.value); });
  pruefForm.addEventListener('submit', e => {
    e.preventDefault();
    const c = normal(codeFeld.value); codeFeld.value = c;
    if (!c) { codeFeld.setAttribute('aria-invalid', 'true'); zeigeErgebnis('nein', 'Bitte einen Code eingeben', 'Er steht unten rechts auf der Karte, z. B. ANN-7K4P-2QXM.'); codeFeld.focus(); return; }
    if (!/^ANN-[A-Z0-9]{4}-[A-Z0-9]{4}$/.test(c)) { codeFeld.setAttribute('aria-invalid', 'true'); zeigeErgebnis('nein', 'Der Code ist unvollständig', 'Ein Code hat drei Teile: ANN, dann zweimal vier Zeichen.'); return; }
    codeFeld.setAttribute('aria-invalid', 'false');
    pruefKnopf.setAttribute('aria-busy', 'true'); pruefKnopf.innerHTML = '<span class="lade" aria-hidden="true"></span> Prüfe';
    ergebnis.textContent = 'Code wird geprüft …';
    setTimeout(() => {
      pruefKnopf.removeAttribute('aria-busy'); pruefKnopf.textContent = 'Prüfen';
      const t = DEMO_CODES[c] || (c === code ? { art: 'gut', titel: 'Gültig · neu ausgestellt', text: auswahl().titel + ', heute bezahlt. Gültig bis 31.12.2029 (Platzhalter).' } : null);
      if (t) zeigeErgebnis(t.art, t.titel, t.text);
      else zeigeErgebnis('nein', 'Diesen Code kennen wir nicht', 'Bitte prüfen Sie die Schreibweise (0 und O, 1 und I kommen nicht vor) oder rufen Sie uns an: 03691 74 28 10.');
    }, reduziert() ? 0 : 650);
  });
  $$('[data-beispiel]').forEach(b => b.addEventListener('click', () => { codeFeld.value = b.dataset.beispiel; pruefForm.requestSubmit(); }));

  // ------------------------------------------------------------------
  // Vorführung
  // ------------------------------------------------------------------
  const KURVE = t => 1 - Math.pow(1 - t, 3);
  let rollId = 0;
  function rolle(y, ms) {
    const s = document.scrollingElement, von = s.scrollTop, bis = Math.max(0, Math.min(y, s.scrollHeight - innerHeight)), id = ++rollId;
    if (reduziert() || ms <= 0) { s.scrollTop = bis; return; }
    const t0 = performance.now();
    const f = t => { if (id !== rollId) return; const p = Math.min(1, (t - t0) / ms); s.scrollTop = von + (bis - von) * KURVE(p); if (p < 1) requestAnimationFrame(f); };
    requestAnimationFrame(f);
  }
  const zeiger = $('[data-zeiger]');
  let uhr = [], laeuft = false;
  const nach = (ms, f) => uhr.push(setTimeout(f, ms));
  function zeigeAuf(n, dx = .5, dy = .5) { if (!n) return; const r = n.getBoundingClientRect(); zeiger.style.translate = `${r.left + r.width * dx}px ${r.top + r.height * dy}px`; zeiger.classList.add('an'); }
  function klick(sel, dx, dy) {
    const n = typeof sel === 'string' ? $(sel) : sel; zeigeAuf(n, dx, dy);
    nach(560, () => { zeiger.classList.add('klick'); n.click(); nach(140, () => zeiger.classList.remove('klick')); });
  }
  function tippen(t, feld, text, ms = 42) {
    nach(t, () => { zeigeAuf(feld, .2, .5); feld.focus({ preventScroll: true }); });
    [...text].forEach((z, i) => nach(t + 350 + i * ms, () => { feld.value += z; feld.dispatchEvent(new Event('input', { bubbles: true })); }));
    return t + 350 + text.length * ms;
  }
  function runde() {
    uhr.forEach(clearTimeout); uhr = [];
    if (dlg.open) dlg.close();
    zuruecksetzen(); zeigeSchritt(1, false); ergebnis.replaceChildren(); codeFeld.value = ''; rolle(0, 0);
    let t = 500;
    nach(t, () => klick('input[name="art"][value="leistung"]', .5, .5)); t += 1100;
    nach(t, () => klick($$('input[name="leistung"]')[0].closest('.wahl'), .3, .4)); t += 1100;
    nach(t, () => klick('[data-schritt="1"] [data-weiter="2"]', .4, .5)); t += 1300;
    nach(t, () => klick($$('input[name="motiv"]')[1].closest('.motiv'), .5, .4)); t += 1300;
    t = tippen(t, form.elements.fuer, 'Anna') + 200;
    t = tippen(t, form.elements.von, 'Jonas') + 200;
    t = tippen(t, form.elements.satz, 'Für ein Wochenende ohne Wecker. Ich freu mich mit dir drauf!', 36) + 700;
    nach(t, () => klick('[data-schritt="2"] [data-weiter="3"]', .4, .5)); t += 1300;
    nach(t, () => klick($$('input[name="weg"]')[1].closest('.wahl'), .3, .4)); t += 1400;
    nach(t, () => klick($$('input[name="weg"]')[0].closest('.wahl'), .3, .4)); t += 1100;
    t = tippen(t, form.elements.mail, 'anna.berger@beispiel.de', 34) + 400;
    nach(t, () => { form.elements.mail.blur(); klick('[data-bezahlen]', .4, .5); }); t += 2600;
    nach(t, () => klick('[data-zahlung]', .4, .5)); t += 2200;
    nach(t, () => klick('[data-schritt="5"] [data-pdf]', .5, .5)); t += 2900;
    nach(t, () => dlg.close()); t += 400;
    nach(t, () => { zeiger.classList.remove('an'); rolle($('.pruefen').getBoundingClientRect().top + scrollY - 40, 900); }); t += 1200;
    t = tippen(t, codeFeld, 'ann7k4p2qxm', 60) + 300;
    nach(t, () => klick('[data-pruef-knopf]', .5, .5)); t += 2600;
    nach(t, () => { zeiger.classList.remove('an'); rolle(0, 900); }); t += 1400;
    nach(t, runde);
  }
  function vorfuehren() { pause(); html.classList.add('vorfuehrung'); laeuft = true; runde(); }
  function pause() { uhr.forEach(clearTimeout); uhr = []; rollId++; laeuft = false; }
  function weiter() { if (!laeuft && html.classList.contains('vorfuehrung')) { laeuft = true; runde(); } }
  function neustart() { pause(); html.classList.remove('vorfuehrung'); zeiger.classList.remove('an'); if (dlg.open) dlg.close(); zuruecksetzen(); zeigeSchritt(1, false); ergebnis.replaceChildren(); codeFeld.value = ''; rolle(0, 0); }

  aktualisieren(); zeigeSchritt(1, false);

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