Zum Inhalt
← Sektionen & Funktionen

Widerrufs- und Kündigungs-Button

Widerrufs- und Kündigungs-Button als zwei getrennte, zweistufige Abläufe aus der Fußzeile, mit Pflichtfeldern, Fehlerliste und Eingangsbestätigung, dazu zwei typische Fehler zum Vergleich.

Jede Kundenseite mit Online-Verträgen: Shop (Widerruf), Abo, Mitgliedschaft, Kurs, Fahrschule (Kündigung).

Zwei getrennte Abläufe für Online-Verträge. Der Widerrufs-Button „Vertrag widerrufen“ führt zu einer Seite, die nur Name, Vertragskennung und den Weg für die Eingangsbestätigung abfragt. Ein Grund wird nicht gefragt. Der zweite Schritt heißt „Widerruf bestätigen“. Danach kommt eine Eingangsbestätigung mit Datum und Uhrzeit. Der Kündigungs-Button „Verträge hier kündigen“ gilt für Abos und Mitgliedschaften. Er führt direkt zu einer Bestätigungsseite mit den erlaubten Feldern und dem Knopf „jetzt kündigen“, ohne Pause-, Rabatt- oder Halteangebote. Beide stehen ohne Anmeldung dauerhaft in der Fußzeile, mobil im Menü oder in der Fußzeile.

Kunden finden den Ausgang sofort und müssen nicht suchen, telefonieren oder sich anmelden. Das spart Anrufe und Beschwerden. Ein klarer, ruhiger Ablauf ohne Tricks wirkt seriös. Wer ohne Hürden gehen kann, kommt eher wieder. Für den Betrieb sinkt das Risiko einer Abmahnung und einer verlängerten Widerrufsfrist.

Wann einsetzen

Überall, wo Verbraucher online Verträge schließen: Shops (Widerruf), Abos, Mitgliedschaften, Kurse und Software (Kündigung), Fahrschulen mit Online-Anmeldung. Shops mit Abo brauchen beide Abläufe, sichtbar getrennt.

Wann nicht

Nicht bei reinen B2B-Verträgen und nicht bei Verträgen, die nur per Telefon, Brief oder vor Ort geschlossen werden. Den Widerrufs-Button nicht bei Verträgen ohne gesetzliches Widerrufsrecht. Nie als Ort für Rückgewinnung: Umfragen, Pausen und Rabatte gehören nicht auf diese Seiten. Ob und welche Pflicht im Einzelfall gilt, vom Kunden prüfen lassen.

Varianten

  • Richtig: beide Knöpfe in der Fußzeile, zweistufig, Eingangsbestätigung mit Datum und Uhrzeit
  • Zum Vergleich falsch: Halteangebot auf der Bestätigungsseite (Pause, Rabatt, großes Stimmungsbild) und Pflichtfeld „Grund“ beim Widerruf
  • Zum Vergleich falsch: versteckter Link (eingeklappt unter „Rechtliches“, nur nach Anmeldung)
  • Mobil: beide Einträge im Menü und in der Fußzeile, nie nur im Kundenkonto

Umsetzung

Zwei eigene Seiten (/widerruf, /kuendigung), keine Modale. Die Knöpfe „Vertrag widerrufen“ und „Verträge hier kündigen“ sind echte Links in der Fußzeile jeder Seite und im mobilen Menü, nebeneinander, gleich gewichtet, ohne Anmeldung. Jede Seite ist ein <form novalidate> mit genau den erlaubten Feldern; bedingte Felder (einzelne Artikel, wichtiger Grund, Datum) sind hidden und inert, bis sie gebraucht werden. Der Absende-Knopf heißt wörtlich „Widerruf bestätigen“ bzw. „jetzt kündigen“. Fehler erscheinen nach der ersten Eingabe und beim Absenden als Liste mit Links zu den Feldern. Danach eine Bestätigungsseite (role="status", Fokus auf die Überschrift) mit Inhalt, Datum und Uhrzeit, bei Kündigung mit Vertragsende, plus Knopf zum Speichern. Der Server schickt dieselbe Bestätigung sofort per E-Mail. In der Demo sind die Seiten Zustände einer Datei. Texte sind Platzhalter: vom Kunden rechtlich prüfen lassen.

Robust umsetzen

Telefon. Beide Einträge im Menü und in der Fußzeile, untereinander, volle Breite, mindestens 48 px hoch. Formularfelder einspaltig, 16 px Schrift gegen Zoom, passende autocomplete-Werte (name, email). Der Absende-Knopf steht unter dem letzten Feld, nicht fest am Rand.

Ohne Bewegung. Seitenwechsel ohne Versatz, nur Tausch. Die Markierungen der Fehlerbeispiele erscheinen ohne Puls.

Barrierefrei. Beschriftung der Knöpfe wörtlich und als echter Text, keine Icons allein. Jedes Feld mit sichtbarem <label>, Pflicht als Text. Fehler über aria-invalid und aria-describedby, Fehlerzusammenfassung mit Links zu den Feldern (WCAG 3.3.1, 3.3.3). Nach dem Absenden Fokus auf die Überschrift der Bestätigung, Inhalt in role="status" (4.1.3). Keine Zeitlimits, keine Anmeldung, kein CAPTCHA (3.3.8). Zielgröße mindestens 24 px, besser 44 px (2.5.8). Datum und Uhrzeit als <time datetime>. Das BFSG nennt die IHK ausdrücklich für den Widerrufs-Button, vom Kunden prüfen lassen.

Code

<!DOCTYPE html>
<!--
  Widerrufs- und Kündigungs-Button
  Fundus · Sektionen & Funktionen · https://cecodesign.de/lab/fundus/sektionen/widerruf-und-kuendigung-schaltflaeche/
  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>Widerrufs- und Kündigungs-Button · 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: 'Instrument Sans'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/instrument-sans:vf@latest/latin-wght-normal.woff2') format('woff2-variations'); font-weight: 400 700; font-display: swap; }
@font-face { font-family: 'Young Serif'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/young-serif@latest/latin-400-normal.woff2') format('woff2'); font-weight: 400; 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; }

:root {
  /* Rezept d2c-produkt-zuerst: warme Neutraltöne, fast schwarze Tinte, dunkler Primärknopf */
  --grund: #F5F4F1; --kachel: #ECE9E2; --weiss: #FFFFFF; --tinte: #212121; --leise: #5C5954;
  --linie: #D8D3C9; --gruen: #10714E; --kupfer: #A8481F; --rot: #A3160F; --mark: #C2261B;
  --auf: cubic-bezier(0, 0, 0.2, 1);      /* öffnen, Ausklang */
  --zu: cubic-bezier(0.4, 0, 1, 1);       /* schließen, Anlauf */
  --t-ui: 150ms; --t-auf: 250ms; --t-zu: 200ms;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
[hidden] { display: none !important; }
html { background: var(--grund); color: var(--tinte); font-family: 'Instrument Sans', system-ui, sans-serif; -webkit-font-smoothing: antialiased; }
body { min-height: 100svh; display: flex; flex-direction: column; }
button, input, select, textarea { font: inherit; color: inherit; }
a { color: inherit; }
.label { font-size: .75rem; font-weight: 600; letter-spacing: .06em; text-transform: uppercase; }
.mono { font: 500 10.5px/1.45 'Geist Mono', ui-monospace, monospace; letter-spacing: .14em; text-transform: uppercase; }
.ziffern { font-variant-numeric: tabular-nums; }
.nur-sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
:focus-visible { outline: 2px solid var(--tinte); outline-offset: 3px; }

/* ── Kopf des Shops ─────────────────────────────── */
.kopf { display: flex; align-items: center; gap: 28px; padding: 16px clamp(16px, 4vw, 48px); border-bottom: 1px solid var(--linie); background: var(--grund); }
.marke { display: flex; align-items: baseline; gap: 10px; text-decoration: none; }
.marke b { font: 400 24px/1 'Young Serif', Georgia, serif; letter-spacing: -.01em; }
.marke span { font-size: 12.5px; color: var(--leise); }
.kopf nav { display: flex; gap: 22px; font-size: 15px; margin-right: auto; }
.kopf nav a { text-decoration: none; padding: 6px 0; }
.kopf nav a:hover { text-decoration: underline; text-underline-offset: 4px; }
.kopf .werkzeug { display: flex; gap: 16px; font-size: 15px; align-items: center; }
.kopf .werkzeug a { text-decoration: none; }
.korb { display: inline-flex; align-items: center; gap: 6px; }
.korb i { font-style: normal; background: var(--tinte); color: var(--grund); font-size: 11px; font-weight: 600; min-width: 20px; height: 20px; border-radius: 10px; display: inline-grid; place-items: center; }
.menue-knopf { display: none; background: none; border: 1px solid var(--linie); border-radius: 999px; padding: 9px 16px; cursor: pointer; }
.menue { display: none; }

/* ── Seite: Bühne mit Panel und Randnotiz ─────────── */
.seite { flex: 1; width: 100%; max-width: 1200px; margin: 0 auto; padding: clamp(28px, 5vw, 64px) clamp(16px, 4vw, 48px) clamp(40px, 6vw, 80px); display: grid; grid-template-columns: minmax(0, 720px) 300px; justify-content: space-between; gap: clamp(28px, 5vw, 72px); align-items: start; }
.panel { min-width: 0; }
.panel form, .panel .intro, .panel .ok { max-width: 620px; }
.panel.raus { opacity: 0; transition: opacity var(--t-zu) var(--zu); }
.panel.rein { animation: rein var(--t-auf) var(--auf) both; }
@keyframes rein { from { opacity: 0; translate: 0 12px; } }
.reduziert .panel.rein { animation: none; }
.reduziert .panel.raus { transition: none; }

.pfad { font-size: 13.5px; color: var(--leise); margin-bottom: 14px; display: flex; gap: 8px; flex-wrap: wrap; }
.pfad a { color: var(--leise); text-underline-offset: 3px; }
.panel h1 { font-size: clamp(1.75rem, 1.3rem + 1.5vw, 2.4rem); font-weight: 600; letter-spacing: -.015em; line-height: 1.1; outline: none; }
.panel h1:focus-visible { outline: 2px solid var(--tinte); outline-offset: 6px; }
.intro { font-size: 16.5px; line-height: 1.55; margin-top: 12px; max-width: 56ch; text-wrap: pretty; }

/* Startseite: Röstpost-Teaser */
.teaser { display: grid; grid-template-columns: minmax(0, 1fr) 170px; gap: 32px; align-items: center; background: var(--kachel); border-radius: 6px; padding: clamp(22px, 3vw, 36px); }
.teaser h1 { font: 400 clamp(1.9rem, 1.3rem + 1.6vw, 2.75rem)/1.06 'Young Serif', Georgia, serif; letter-spacing: -.01em; margin-top: 10px; }
.teaser p { font-size: 15.5px; line-height: 1.55; margin-top: 12px; max-width: 44ch; }
.teaser .preis { font-weight: 600; }
.teaser .preis small { font-weight: 400; color: var(--leise); font-size: 13.5px; }
.teaser .label { color: var(--kupfer); }
.teaser svg { width: 100%; height: auto; display: block; }
.teaser .knopf { margin-top: 18px; }
.lieferung { margin-top: 18px; display: flex; gap: 10px 22px; flex-wrap: wrap; font-size: 13.5px; color: var(--leise); list-style: none; }
.lieferung li::before { content: '✓ '; color: var(--gruen); font-weight: 600; }

/* Formular */
form { margin-top: 26px; display: grid; gap: 20px; }
.feld { display: grid; gap: 7px; }
.feld > label, .gruppe legend { font-size: 15px; font-weight: 600; }
.feld .opt { font-weight: 400; color: var(--leise); }
.feld input[type=text], .feld input[type=email], .feld input[type=password], .feld input[type=date], .feld select, .feld textarea {
  width: 100%; font-size: 16px; padding: 12px 14px; background: var(--weiss); border: 1px solid #A9A398; border-radius: 4px;
  transition: border-color var(--t-ui) var(--auf), box-shadow var(--t-ui) var(--auf);
}
.feld textarea { min-height: 84px; resize: vertical; line-height: 1.45; }
.feld input:hover, .feld select:hover, .feld textarea:hover { border-color: var(--tinte); }
.feld input:focus-visible, .feld select:focus-visible, .feld textarea:focus-visible { outline: none; border-color: var(--tinte); box-shadow: 0 0 0 3px rgba(33, 33, 33, .18); }
.feld [aria-invalid="true"] { border-color: var(--rot); box-shadow: inset 4px 0 0 var(--rot); }
.hilfe { font-size: 13.5px; color: var(--leise); line-height: 1.4; }
.feldfehler { font-size: 13.5px; color: var(--rot); font-weight: 600; display: flex; gap: 6px; align-items: baseline; }
.feldfehler::before { content: '!'; display: inline-grid; place-items: center; width: 16px; height: 16px; border-radius: 50%; background: var(--rot); color: #fff; font-size: 11px; flex: none; translate: 0 2px; }
.gruppe { border: 0; display: grid; gap: 8px; }
.gruppe legend { margin-bottom: 8px; }
.wahl { display: flex; gap: 12px; align-items: flex-start; padding: 12px 14px; border: 1px solid var(--linie); border-radius: 4px; background: var(--weiss); cursor: pointer; font-size: 15px; line-height: 1.4; transition: border-color var(--t-ui) var(--auf); }
.wahl:hover { border-color: var(--tinte); }
.wahl:has(input:checked) { border-color: var(--tinte); box-shadow: inset 0 0 0 1px var(--tinte); }
.wahl input { width: 20px; height: 20px; accent-color: var(--tinte); margin: 0; flex: none; translate: 0 1px; }
.wahl small { display: block; color: var(--leise); font-size: 13.5px; }
.unterfeld { margin: 4px 0 0 32px; }

.fehlerliste { border: 2px solid var(--rot); border-radius: 4px; padding: 14px 16px; background: #FBEDEB; display: grid; gap: 6px; outline: none; }
.fehlerliste:focus-visible { box-shadow: 0 0 0 3px rgba(163, 22, 15, .3); }
.fehlerliste h2 { font-size: 16px; font-weight: 600; color: var(--rot); }
.fehlerliste ul { padding-left: 18px; font-size: 15px; line-height: 1.5; }
.fehlerliste a { color: var(--rot); text-underline-offset: 3px; }

.knopf { display: inline-flex; align-items: center; justify-content: center; gap: 10px; min-height: 52px; padding: 0 26px; border-radius: 4px; border: 1px solid var(--tinte); background: var(--tinte); color: var(--grund); font-size: 16px; font-weight: 600; text-decoration: none; cursor: pointer; transition: background-color var(--t-ui) var(--auf), color var(--t-ui) var(--auf); }
.knopf:hover { background: #000; }
.knopf.zweit { background: transparent; color: var(--tinte); }
.knopf.zweit:hover { background: rgba(33, 33, 33, .06); }
.knopf[aria-busy="true"] { opacity: .7; cursor: progress; }
.abschluss { display: grid; gap: 10px; justify-items: start; margin-top: 4px; }
.klein { font-size: 13.5px; color: var(--leise); line-height: 1.45; }

/* Bestätigung */
.ok { display: grid; gap: 18px; }
.ok-zeichen { width: 52px; height: 52px; border-radius: 50%; background: var(--gruen); display: grid; place-items: center; }
.ok-zeichen svg { width: 24px; height: 24px; }
.beleg { background: var(--weiss); border: 1px solid var(--linie); border-radius: 6px; padding: 6px 20px; }
.beleg div { display: grid; grid-template-columns: 150px minmax(0, 1fr); gap: 4px 18px; padding: 13px 0; border-bottom: 1px solid var(--kachel); font-size: 15px; line-height: 1.45; }
.beleg div:last-child { border-bottom: 0; }
.beleg dt { color: var(--leise); }
.beleg dd { font-weight: 500; overflow-wrap: anywhere; }
.knoepfe { display: flex; flex-wrap: wrap; gap: 10px; }
.ok p { font-size: 15.5px; line-height: 1.55; max-width: 58ch; }

/* Seiten der Fehler-Beispiele */
.bild-gefuehl { position: relative; height: 210px; border-radius: 6px; overflow: hidden; margin: 22px 0 4px; background: radial-gradient(120% 90% at 78% 30%, #F1C27D 0%, #C9733A 38%, #5B2A14 80%); color: #FFF8EE; display: flex; align-items: flex-end; padding: 22px 24px; }
.bild-gefuehl svg { position: absolute; right: 6%; bottom: -6px; width: 210px; height: auto; }
.bild-gefuehl p { position: relative; font: 400 clamp(1.4rem, 1rem + 1.6vw, 2.1rem)/1.1 'Young Serif', Georgia, serif; max-width: 15ch; text-shadow: 0 1px 12px rgba(40, 15, 5, .5); }
.angebot { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 8px 18px; align-items: center; border: 1px solid #E8A36A; background: #FFF3E6; border-radius: 6px; padding: 16px 18px; margin-top: 16px; }
.angebot b { font-size: 16px; }
.angebot p { font-size: 14.5px; line-height: 1.45; grid-column: 1; }
.angebot .knopf { background: #E86A1C; border-color: #E86A1C; color: #fff; grid-row: 1 / span 2; grid-column: 2; }
.knopf.blass { background: transparent; border-color: transparent; color: #6E6A63; font-weight: 400; min-height: 40px; padding: 0 4px; text-decoration: underline; }
.knopf.lockt { background: var(--kupfer); border-color: var(--kupfer); color: #fff; }
.belehrung { margin-top: 18px; display: grid; gap: 12px; font-size: 14.5px; line-height: 1.6; color: #3B3936; max-width: 62ch; }
.belehrung h2 { font-size: 15px; font-weight: 600; margin-top: 6px; }
.pdf { display: inline-flex; gap: 10px; align-items: center; font-size: 14.5px; }
.pdf span { font: 600 10px/1 'Geist Mono', monospace; border: 1px solid var(--leise); padding: 4px 5px; border-radius: 2px; }

/* Markierung der Fehlerstellen (nur Varianten „falsch“) */
.warnband { display: flex; gap: 10px; align-items: center; background: var(--mark); color: #fff; border-radius: 4px; padding: 9px 14px; margin-bottom: 18px; font-size: 14px; font-weight: 600; }
.warnband .mono { color: rgba(255, 255, 255, .85); letter-spacing: .1em; }
.fehlerstelle { position: relative; outline: 2px dashed var(--mark); outline-offset: 5px; border-radius: 4px; }
.fehlerstelle::after { content: attr(data-nr); position: absolute; top: -15px; left: -15px; width: 24px; height: 24px; border-radius: 50%; background: var(--mark); color: #fff; font: 700 12px/24px 'Instrument Sans', sans-serif; text-align: center; z-index: 2; animation: puls 1.6s var(--auf) 2; }
@keyframes puls { 50% { box-shadow: 0 0 0 7px rgba(194, 38, 27, .22); } }
.reduziert .fehlerstelle::after { animation: none; }
.bild-gefuehl.fehlerstelle::after { top: 12px; left: 12px; }

/* ── Randnotiz: was die Seite darf ────────────────── */
.notiz { position: sticky; top: 24px; border: 1px dashed #A9A398; border-radius: 6px; padding: 18px 18px 16px; display: grid; gap: 14px; font-size: 14px; line-height: 1.5; background: rgba(255, 255, 255, .45); }
.notiz .mono { color: var(--leise); }
.notiz h2 { font-size: 16px; font-weight: 600; line-height: 1.3; }
.schritte { list-style: none; display: grid; gap: 2px; counter-reset: s; }
.schritte li { counter-increment: s; display: grid; grid-template-columns: 26px minmax(0, 1fr); gap: 8px; padding: 6px 0; color: var(--leise); }
.schritte li::before { content: counter(s); width: 22px; height: 22px; border-radius: 50%; border: 1px solid #A9A398; display: grid; place-items: center; font-size: 12px; font-weight: 600; }
.schritte li.erledigt::before { content: '✓'; background: var(--gruen); border-color: var(--gruen); color: #fff; }
.schritte li[aria-current] { color: var(--tinte); font-weight: 600; }
.schritte li[aria-current]::before { background: var(--tinte); border-color: var(--tinte); color: var(--grund); }
.regeln { list-style: none; display: grid; gap: 6px; }
.regeln li { display: grid; grid-template-columns: 18px minmax(0, 1fr); gap: 6px; }
.regeln li::before { font-weight: 700; }
.regeln .ja::before { content: '✓'; color: var(--gruen); }
.regeln .nein::before { content: '✕'; color: var(--rot); }
.fehlerpunkte { list-style: none; display: grid; gap: 10px; }
.fehlerpunkte li { display: grid; grid-template-columns: 24px minmax(0, 1fr); gap: 10px; }
.fehlerpunkte li::before { content: attr(data-nr); width: 22px; height: 22px; border-radius: 50%; background: var(--mark); color: #fff; font-size: 12px; font-weight: 700; display: grid; place-items: center; }
.fehlerpunkte small { display: block; color: var(--leise); font-size: 12.5px; margin-top: 2px; }
.quelle { font-size: 12.5px; color: var(--leise); border-top: 1px solid var(--linie); padding-top: 10px; }

/* ── Fußzeile: hier stehen die beiden Knöpfe ──────── */
.fuss { background: var(--tinte); color: #F2EFE8; }
.fuss-innen { max-width: 1200px; margin: 0 auto; padding: 0 clamp(16px, 4vw, 48px); }
.vertraege { display: flex; align-items: center; flex-wrap: wrap; gap: 12px 14px; padding: 22px 0; border-bottom: 1px solid rgba(242, 239, 232, .18); }
.vertraege p { margin-right: auto; font-size: 14.5px; line-height: 1.4; color: rgba(242, 239, 232, .82); }
.vertraege p b { color: #F2EFE8; font-weight: 600; display: block; font-size: 15.5px; }
.vknopf { display: inline-flex; align-items: center; gap: 10px; min-height: 48px; padding: 0 20px; border: 1px solid rgba(242, 239, 232, .7); border-radius: 4px; color: #F2EFE8; text-decoration: none; font-size: 15.5px; font-weight: 600; transition: background-color var(--t-ui) var(--auf), color var(--t-ui) var(--auf); }
.vknopf:hover { background: #F2EFE8; color: var(--tinte); }
.vknopf svg { width: 15px; height: 15px; flex: none; }
.fuss :focus-visible { outline-color: #F2EFE8; }
.spalten { display: grid; grid-template-columns: 1.4fr repeat(3, 1fr); gap: 28px; padding: 30px 0 26px; }
.spalten h2 { font-size: 12px; font-weight: 600; letter-spacing: .06em; text-transform: uppercase; color: rgba(242, 239, 232, .72); margin-bottom: 12px; }
.spalten ul { list-style: none; display: grid; gap: 8px; font-size: 14.5px; }
.spalten a { text-decoration: none; color: #F2EFE8; }
.spalten a:hover { text-decoration: underline; text-underline-offset: 3px; }
.fuss .claim { font: 400 26px/1.1 'Young Serif', Georgia, serif; max-width: 13ch; }
.fuss .adresse { font-size: 13.5px; line-height: 1.55; color: rgba(242, 239, 232, .78); margin-top: 12px; }
.rechtlich summary { cursor: pointer; font-size: 12px; font-weight: 600; letter-spacing: .06em; text-transform: uppercase; color: rgba(242, 239, 232, .72); list-style: none; display: flex; gap: 6px; align-items: center; width: fit-content; }
.rechtlich summary::-webkit-details-marker { display: none; }
.rechtlich summary::after { content: '+'; font-size: 15px; }
.rechtlich[open] summary::after { content: '–'; }
.rechtlich ul { margin-top: 10px; }
.rechtlich .grau a { color: rgba(242, 239, 232, .66); font-size: 12px; }
.unterzeile { display: flex; flex-wrap: wrap; gap: 8px 22px; justify-content: space-between; padding: 14px 0 20px; border-top: 1px solid rgba(242, 239, 232, .18); font-size: 12.5px; color: rgba(242, 239, 232, .72); }
.fuss .fehlerstelle { --mark: #FF8F84; outline-color: #FF8F84; }
.fuss .fehlerstelle::after { color: var(--tinte); }

/* Gedachter Klick der Vorführung */
.tipp { position: relative; }
.tipp::before { content: ''; position: absolute; inset: -5px; border-radius: 8px; border: 2px solid var(--kupfer); animation: tipp 600ms var(--auf) forwards; pointer-events: none; }
.fuss .tipp::before { border-color: #F2EFE8; }
@keyframes tipp { from { opacity: 1; scale: .94; } to { opacity: 0; scale: 1.05; } }
.reduziert .tipp::before { animation: none; opacity: 0; }

/* ── Pult ──────────────────────────────────────────── */
.pult { position: fixed; right: 16px; bottom: 16px; z-index: 30; display: grid; gap: 8px; background: rgba(33, 33, 33, .95); color: #F2EFE8; padding: 12px 14px; border-radius: 6px; box-shadow: 0 12px 32px rgba(0, 0, 0, .22); max-width: calc(100vw - 32px); }
.pult .reihe { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; }
.pult .mono { color: rgba(242, 239, 232, .7); }
.pult .knopfreihe { display: flex; flex-wrap: wrap; border: 1px solid rgba(242, 239, 232, .3); border-radius: 3px; }
.pult button { background: none; border: 0; padding: 8px 10px; min-height: 30px; font: 500 10.5px/1 'Geist Mono', ui-monospace, monospace; letter-spacing: .1em; text-transform: uppercase; color: rgba(242, 239, 232, .8); cursor: pointer; }
.pult .knopfreihe button + button { border-left: 1px solid rgba(242, 239, 232, .3); }
.pult button[aria-pressed="true"] { background: #F2EFE8; color: var(--tinte); }
.pult button:focus-visible { outline: 2px solid #F2EFE8; outline-offset: 2px; }
.pult .neu { border: 1px solid rgba(242, 239, 232, .3); border-radius: 3px; }

@media (forced-colors: active) {
  button, [role=button], [role=tab], .knopf, .vknopf { border: 1px solid ButtonText; }
  .fehlerstelle { outline-color: Highlight; }
}

/* Touch: kein Hover-Zustand, größere Ziele */
@media (hover: none) {
  .knopf:hover { background: var(--tinte); }
  .knopf.zweit:hover, .vknopf:hover { background: transparent; color: inherit; }
  .wahl { padding: 14px; }
  .spalten ul { gap: 12px; }
}

@media (max-width: 980px) {
  .seite { grid-template-columns: minmax(0, 1fr); justify-content: stretch; }
  .notiz { position: static; }
  .spalten { grid-template-columns: 1fr 1fr; }
}
@media (max-width: 720px) {
  .kopf nav, .kopf .werkzeug .konto { display: none; }
  .kopf { gap: 12px; justify-content: space-between; }
  .menue-knopf { display: inline-flex; }
  .menue:not([hidden]) { display: grid; gap: 2px; padding: 8px clamp(16px, 4vw, 48px) 16px; border-bottom: 1px solid var(--linie); background: var(--grund); }
  .menue a { padding: 12px 0; font-size: 17px; text-decoration: none; border-bottom: 1px solid var(--kachel); }
  .menue .trenn { margin-top: 10px; font-size: 12px; color: var(--leise); padding: 6px 0; }
  .teaser { grid-template-columns: minmax(0, 1fr); }
  .teaser figure { max-width: 108px; grid-row: 1; }
  .vertraege { flex-direction: column; align-items: stretch; }
  .vknopf { justify-content: space-between; }
  .beleg div { grid-template-columns: minmax(0, 1fr); gap: 2px; }
  .angebot { grid-template-columns: minmax(0, 1fr); }
  .angebot .knopf { grid-row: auto; grid-column: 1; }
  .bild-gefuehl svg { width: 140px; }
  .knopf { width: 100%; }
  .knoepfe { display: grid; }
  .pult { left: 12px; right: 12px; bottom: 12px; }
}
@media (max-width: 420px) {
  .spalten { grid-template-columns: minmax(0, 1fr); }
  .marke span { display: none; }
  .korb .wort { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
  .korb::before { content: ''; width: 18px; height: 18px; background: currentColor; mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 18 18'%3E%3Cpath d='M3 6h12l-1 10H4z M6 6a3 3 0 0 1 6 0' fill='none' stroke='black' stroke-width='1.5'/%3E%3C/svg%3E") center / contain no-repeat; }
  .kopf .werkzeug { gap: 10px; }
  .unterfeld { margin-left: 0; }
}

/* Vorschau 960 × 720: nur die Bühne */
.vorschau body { overflow: hidden; }
.vorschau .kopf, .vorschau .lieferung, .vorschau .unterzeile, .vorschau .spalten .adresse { display: none; }
.vorschau .seite { padding-top: 28px; padding-bottom: 28px; grid-template-columns: minmax(0, 1fr) 280px; gap: 40px; justify-content: stretch; }
.vorschau .teaser { padding: 24px 28px; }
.vorschau .notiz { gap: 10px; padding: 14px 16px; font-size: 13px; }
.vorschau form { gap: 14px; margin-top: 18px; }
.vorschau .intro { font-size: 15px; }
.vorschau .spalten { grid-template-columns: 1.2fr repeat(3, 1fr); gap: 20px; }
</style>
</head>
<body>
<svg width="0" height="0" style="position:absolute" aria-hidden="true">
  <symbol id="pfeil" viewBox="0 0 16 16"><path d="M3 8h9M8.5 4.5 12 8l-3.5 3.5" fill="none" stroke="currentColor" stroke-width="1.6"/></symbol>
</svg>

<header class="kopf">
  <a class="marke" href="#start" data-geh="start"><b>Mangfall</b><span>Kaffeerösterei · Rosenheim</span></a>
  <nav aria-label="Hauptnavigation"><a href="#start" data-geh="start">Kaffee</a><a href="#start" data-geh="start">Röstpost-Abo</a><a href="#start" data-geh="start">Zubehör</a><a href="#start" data-geh="start">Rösterei</a></nav>
  <div class="werkzeug"><a class="konto" href="#start" data-geh="start">Konto</a><a class="korb" href="#start" data-geh="start"><span class="wort">Warenkorb</span> <i class="ziffern">2</i><span class="nur-sr"> Artikel</span></a>
    <button class="menue-knopf" type="button" aria-expanded="false" aria-controls="menue" data-menue>Menü</button></div>
</header>
<nav class="menue" id="menue" aria-label="Menü" hidden data-menue-liste></nav>

<main class="seite" id="inhalt">
  <div class="panel" data-panel></div>
  <aside class="notiz" aria-labelledby="notiz-titel" data-notiz></aside>
</main>

<footer class="fuss" data-fuss></footer>

<div class="pult">
  <div class="reihe"><span class="mono">Variante</span>
    <div class="knopfreihe" role="group" aria-label="Variante">
      <button type="button" data-v="richtig" aria-pressed="true">Richtig</button>
      <button type="button" data-v="halteangebot" aria-pressed="false">Falsch: Halteangebot</button>
      <button type="button" data-v="versteckt" aria-pressed="false">Falsch: versteckt</button>
    </div>
  </div>
  <div class="reihe"><span class="mono">Ablauf</span><button type="button" class="neu" data-vonvorn>Von vorn</button></div>
</div>

<script>
(() => {
  const html = document.documentElement;
  const reduziert = () => html.classList.contains('reduziert');
  const $ = (s, r = document) => r.querySelector(s);
  const panel = $('[data-panel]'), notiz = $('[data-notiz]'), fuss = $('[data-fuss]'), menueListe = $('[data-menue-liste]'), menueKnopf = $('[data-menue]');

  // Erfundene Rösterei. Texte sind Platzhalter des Betriebs: vom Kunden rechtlich prüfen lassen.
  const datum = new Intl.DateTimeFormat('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' });
  const uhr = new Intl.DateTimeFormat('de-DE', { hour: '2-digit', minute: '2-digit' });
  const PFEIL = '<svg aria-hidden="true"><use href="#pfeil"/></svg>';
  const esc = s => String(s).replace(/[&<>"]/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[c]));

  const z = { v: 'richtig', seite: 'start', beleg: null };

  /* ── Felder ── */
  function feld(f) {
    const hilfe = f.hilfe ? `<p class="hilfe" id="${f.id}-hilfe">${f.hilfe}</p>` : '';
    const besch = [f.hilfe ? f.id + '-hilfe' : '', f.id + '-fehler'].filter(Boolean).join(' ');
    const opt = f.pflicht ? '' : ' <span class="opt">(freiwillig)</span>';
    let eingabe;
    if (f.typ === 'select') eingabe = `<select id="${f.id}" name="${f.id}" ${f.pflicht ? 'data-pflicht required' : ''} aria-describedby="${besch}"><option value="">Bitte wählen</option>${f.optionen.map(o => `<option>${o}</option>`).join('')}</select>`;
    else if (f.typ === 'textarea') eingabe = `<textarea id="${f.id}" name="${f.id}" ${f.pflicht ? 'data-pflicht required' : ''} aria-describedby="${besch}"></textarea>`;
    else eingabe = `<input id="${f.id}" name="${f.id}" type="${f.typ || 'text'}" ${f.auto ? `autocomplete="${f.auto}"` : ''} ${f.inputmode ? `inputmode="${f.inputmode}"` : ''} ${f.pflicht ? 'data-pflicht required' : ''} aria-describedby="${besch}" ${f.platz ? `placeholder="${f.platz}"` : ''}>`;
    return `<div class="feld${f.klasse ? ' ' + f.klasse : ''}"${f.nr ? ` data-nr="${f.nr}"` : ''}><label for="${f.id}">${f.label}${opt}</label>${hilfe}${eingabe}<p class="feldfehler" id="${f.id}-fehler" hidden></p></div>`;
  }
  const MELDUNG = {
    name: 'Bitte geben Sie Ihren Namen ein.',
    bestellung: 'Bitte geben Sie die Bestellnummer ein, z. B. MK-2026-10482.',
    artikel: 'Bitte nennen Sie die Artikel, die Sie widerrufen.',
    email: 'Bitte geben Sie eine E-Mail-Adresse im Format name@beispiel.de ein.',
    vertrag: 'Bitte geben Sie die Abo- oder Kundennummer ein, z. B. RP-48213.',
    grund: 'Bitte nennen Sie den wichtigen Grund für die außerordentliche Kündigung.',
    zum: 'Bitte wählen Sie ein Datum ab morgen.',
    wgrund: 'Bitte wählen Sie einen Grund.'
  };
  function pruefeFeld(el) {
    if (!el || el.closest('[hidden]')) return null;
    const v = el.value.trim();
    let fehler = '';
    if (el.hasAttribute('data-pflicht') && !v) fehler = MELDUNG[el.id] || 'Bitte ausfüllen.';
    else if (el.type === 'email' && v && !/^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(v)) fehler = MELDUNG.email;
    else if (el.id === 'zum' && v && new Date(v) <= new Date()) fehler = MELDUNG.zum;
    const p = $('#' + el.id + '-fehler');
    el.setAttribute('aria-invalid', fehler ? 'true' : 'false');
    if (p) { p.textContent = fehler; p.hidden = !fehler; }
    return fehler ? { id: el.id, text: fehler } : null;
  }
  function bindeFormular(form, absenden) {
    const beruehrt = new Set();
    form.addEventListener('input', e => { if (!e.target.id) return; beruehrt.add(e.target.id); if (e.target.getAttribute('aria-invalid') === 'true') pruefeFeld(e.target); });
    form.addEventListener('focusout', e => { if (e.target.id && 'value' in e.target && beruehrt.has(e.target.id)) pruefeFeld(e.target); });
    form.addEventListener('change', e => {
      const r = e.target;
      if (r.name === 'umfang') { const u = $('[data-unter="artikel"]', form); u.hidden = r.value !== 'teile'; u.inert = u.hidden; }
      if (r.name === 'art') { const u = $('[data-unter="grund"]', form); u.hidden = r.value !== 'ausserordentlich'; u.inert = u.hidden; }
      if (r.name === 'ende') { const u = $('[data-unter="zum"]', form); u.hidden = r.value !== 'datum'; u.inert = u.hidden; }
    });
    form.addEventListener('submit', e => {
      e.preventDefault();
      const fehler = [...form.querySelectorAll('[data-pflicht], input[type=email], #zum')].map(pruefeFeld).filter(Boolean);
      const liste = $('.fehlerliste', form);
      if (fehler.length) {
        liste.innerHTML = `<h2>${fehler.length === 1 ? 'Eine Angabe fehlt noch' : fehler.length + ' Angaben fehlen noch'}</h2><ul>${fehler.map(f => `<li><a href="#${f.id}">${f.text}</a></li>`).join('')}</ul>`;
        liste.hidden = false; liste.focus();
        return;
      }
      liste.hidden = true;
      const k = $('button[type=submit]', form); k.setAttribute('aria-busy', 'true');
      nach(reduziert() ? 0 : 450, () => absenden(new FormData(form)));
    });
    form.addEventListener('click', e => { const a = e.target.closest('.fehlerliste a'); if (a) { e.preventDefault(); $(a.getAttribute('href'), form)?.focus(); } });
  }

  /* ── Seiten ── */
  const falsch = () => z.v !== 'richtig';
  const warnband = () => falsch() ? `<p class="warnband"><span class="mono">Beispiel</span> So bitte nicht. Die Markierungen erklärt die Notiz.</p>` : '';
  const pfad = t => `<nav class="pfad" aria-label="Brotkrumen"><a href="#start" data-geh="start">Startseite</a><span aria-hidden="true">/</span><span aria-current="page">${t}</span></nav>`;

  const TUETE = `<svg viewBox="0 0 200 250" aria-hidden="true"><defs><linearGradient id="kraft" x1="0" x2="1"><stop offset="0" stop-color="#C9A274"/><stop offset=".55" stop-color="#D8B68A"/><stop offset="1" stop-color="#B98F60"/></linearGradient></defs>
    <path d="M38 38 L162 38 L170 236 Q100 246 30 236 Z" fill="url(#kraft)"/><path d="M38 38 L50 12 L150 12 L162 38 Z" fill="#B38959"/><path d="M50 12 L150 12" stroke="#8C6640" stroke-width="3"/>
    <rect x="58" y="92" width="84" height="104" rx="3" fill="#F5F4F1"/><text x="100" y="118" text-anchor="middle" font-family="Young Serif, Georgia, serif" font-size="17" fill="#212121">Mangfall</text>
    <path d="M66 166 l14 -18 l10 10 l12 -20 l16 22 l16 -12" fill="none" stroke="#A8481F" stroke-width="2.2" stroke-linejoin="round"/>
    <text x="100" y="186" text-anchor="middle" font-family="Instrument Sans, sans-serif" font-size="7" letter-spacing=".6" fill="#212121">WENDELSTEIN 250 G</text></svg>`;

  const SEITEN = {
    start: () => `<section class="teaser" aria-labelledby="titel">
        <div><p class="label">Röstpost · das Kaffee-Abo</p>
          <h1 id="titel" tabindex="-1">Frisch geröstet, alle zwei Wochen im Briefkasten.</h1>
          <p>Espresso Wendelstein oder Filter Kampenwand, 250 oder 500 Gramm. Monatlich kündbar, ohne Mindestlaufzeit.</p>
          <p class="preis ziffern">ab 14,90 € je Lieferung <small>(5,96 € / 100 g)</small></p>
          <ul class="lieferung"><li>Versand am Rösttag</li><li>Passt in den Briefkasten</li></ul></div>
        <figure>${TUETE}</figure></section>`,

    widerruf: () => falsch() && z.v === 'versteckt' ? SEITEN.belehrung() : `${warnband()}${pfad('Vertrag widerrufen')}
      <h1 tabindex="-1">Vertrag widerrufen</h1>
      <p class="intro">Sie können eine Bestellung innerhalb der Widerrufsfrist ohne Angabe von Gründen widerrufen. Wir fragen nur, was wir brauchen, um die Bestellung zu finden und Ihnen den Eingang zu bestätigen.</p>
      <form novalidate data-form="widerruf" aria-label="Widerruf">
        <div class="fehlerliste" tabindex="-1" role="alert" hidden></div>
        ${feld({ id: 'name', label: 'Ihr Name', auto: 'name', pflicht: true })}
        ${feld({ id: 'bestellung', label: 'Bestellnummer', hilfe: 'Steht in der Bestellbestätigung, z. B. MK-2026-10482.', pflicht: true })}
        <fieldset class="gruppe"><legend>Was möchten Sie widerrufen?</legend>
          <label class="wahl"><input type="radio" name="umfang" value="ganz" checked><span>Die ganze Bestellung</span></label>
          <label class="wahl"><input type="radio" name="umfang" value="teile"><span>Einzelne Artikel</span></label>
          <div class="unterfeld" data-unter="artikel" hidden inert>${feld({ id: 'artikel', label: 'Welche Artikel?', typ: 'textarea', pflicht: true })}</div>
        </fieldset>
        ${z.v === 'halteangebot' ? `<div class="fehlerstelle" data-nr="1">${feld({ id: 'wgrund', label: 'Grund des Widerrufs', typ: 'select', pflicht: true, optionen: ['Geschmack passt nicht', 'Zu teuer', 'Falsch bestellt', 'Lieferung zu spät', 'Sonstiges'] })}</div>` : ''}
        ${feld({ id: 'email', label: 'E-Mail für die Eingangsbestätigung', typ: 'email', auto: 'email', inputmode: 'email', hilfe: 'Dorthin schicken wir sofort die Bestätigung mit Datum und Uhrzeit.', pflicht: true })}
        <div class="abschluss">
          ${z.v === 'halteangebot' ? '<button class="knopf fehlerstelle" data-nr="2" type="submit">Absenden</button>' : '<button class="knopf" type="submit">Widerruf bestätigen</button>'}
          <p class="klein">Ohne Anmeldung. Ihre Angaben nutzen wir nur für diesen Widerruf.</p>
        </div>
      </form>`,

    'w-ok': () => {
      const b = z.beleg;
      return `<div class="ok" role="status">
        <span class="ok-zeichen" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="m5 12.5 4.5 4.5L19 7.5" fill="none" stroke="#fff" stroke-width="2.4"/></svg></span>
        <h1 tabindex="-1">Ihr Widerruf ist bei uns eingegangen.</h1>
        <dl class="beleg">
          <div><dt>Eingegangen am</dt><dd><time class="ziffern" datetime="${b.iso}">${b.datum}, ${b.uhr} Uhr</time></dd></div>
          <div><dt>Ihre Erklärung</dt><dd>Hiermit widerrufe ich den Vertrag über die Bestellung ${esc(b.bestellung)}${b.artikel ? ` (Artikel: ${esc(b.artikel)})` : ' (ganze Bestellung)'}.</dd></div>
          <div><dt>Name</dt><dd>${esc(b.name)}</dd></div>
          <div><dt>Bestätigung an</dt><dd>${esc(b.email)}</dd></div>
        </dl>
        <p>Dieselbe Eingangsbestätigung ist unterwegs an ${esc(b.email)}. Sie hält fest, wann Ihre Erklärung bei uns angekommen ist. Das Rücksendeetikett schicken wir in einer eigenen E-Mail.</p>
        <div class="knoepfe"><button class="knopf" type="button" data-speichern>Bestätigung speichern</button><a class="knopf zweit" href="#start" data-geh="start">Zur Startseite</a></div>
      </div>`;
    },

    kuendigung: () => falsch() && z.v === 'versteckt' ? SEITEN.login() : `${warnband()}${pfad('Verträge kündigen')}
      <h1 tabindex="-1">Vertrag kündigen</h1>
      ${z.v === 'halteangebot' ? `<div class="bild-gefuehl fehlerstelle" data-nr="1" role="img" aria-label="Großes Stimmungsbild: dampfende Tasse vor Bergen"><svg viewBox="0 0 220 170" aria-hidden="true"><path d="M0 170 L60 70 L95 112 L140 40 L220 170 Z" fill="#3E1C0D" opacity=".55"/><path d="M58 92 h104 v44 a40 40 0 0 1 -40 34 h-24 a40 40 0 0 1 -40 -34 Z" fill="#FFF3E6"/><path d="M162 104 a18 18 0 0 1 0 28" fill="none" stroke="#FFF3E6" stroke-width="7"/><path d="M90 80 q-10 -16 0 -30 q10 -14 0 -30 M114 80 q-10 -16 0 -30 q10 -14 0 -30" fill="none" stroke="#FFF3E6" stroke-width="3" opacity=".7"/></svg><p>Schon gehen? Ihr Wendelstein vermisst Sie.</p></div>
        <div class="angebot fehlerstelle" data-nr="2"><b>Lieber erst einmal pausieren?</b><p>Drei Monate Pause, danach 20 % auf die nächsten drei Lieferungen.</p><button class="knopf" type="button">Abo pausieren</button></div>`
      : `<p class="intro">Hier kündigen Sie Ihr Röstpost-Abo. Wir fragen nur, was wir für die Kündigung brauchen. Die Bestätigung kommt sofort per E-Mail.</p>`}
      <form novalidate data-form="kuendigung" aria-label="Kündigung">
        <div class="fehlerliste" tabindex="-1" role="alert" hidden></div>
        <fieldset class="gruppe"><legend>Art der Kündigung</legend>
          <label class="wahl"><input type="radio" name="art" value="ordentlich" checked><span>Ordentliche Kündigung<small>zum Ende der Laufzeit oder Frist</small></span></label>
          <label class="wahl"><input type="radio" name="art" value="ausserordentlich"><span>Außerordentliche Kündigung<small>aus wichtigem Grund, ohne Frist</small></span></label>
          <div class="unterfeld" data-unter="grund" hidden inert>${feld({ id: 'grund', label: 'Wichtiger Grund', typ: 'textarea', pflicht: true })}</div>
        </fieldset>
        ${feld({ id: 'name', label: 'Ihr Name', auto: 'name', pflicht: true })}
        ${feld({ id: 'email', label: 'E-Mail-Adresse', typ: 'email', auto: 'email', inputmode: 'email', hilfe: 'Zur Zuordnung und für die Kündigungsbestätigung.', pflicht: true })}
        ${feld({ id: 'vertrag', label: 'Abo- oder Kundennummer', hilfe: 'Steht in jeder Versandmail, z. B. RP-48213.', pflicht: true })}
        <fieldset class="gruppe"><legend>Wann soll der Vertrag enden?</legend>
          <label class="wahl"><input type="radio" name="ende" value="naechst" checked><span>Zum nächstmöglichen Zeitpunkt</span></label>
          <label class="wahl"><input type="radio" name="ende" value="datum"><span>Zu einem bestimmten Datum</span></label>
          <div class="unterfeld" data-unter="zum" hidden inert>${feld({ id: 'zum', label: 'Datum', typ: 'date', pflicht: true })}</div>
        </fieldset>
        <div class="abschluss">
          ${z.v === 'halteangebot' ? `<div class="knoepfe"><button class="knopf lockt fehlerstelle" data-nr="3" type="button">Vertrag finden</button><button class="knopf blass" type="submit">jetzt kündigen</button></div>` : '<button class="knopf" type="submit">jetzt kündigen</button>'}
          <p class="klein">Ohne Anmeldung, ohne Passwort.</p>
        </div>
      </form>`,

    'k-ok': () => {
      const b = z.beleg;
      return `<div class="ok" role="status">
        <span class="ok-zeichen" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="m5 12.5 4.5 4.5L19 7.5" fill="none" stroke="#fff" stroke-width="2.4"/></svg></span>
        <h1 tabindex="-1">Ihre Kündigung ist bei uns eingegangen.</h1>
        <dl class="beleg">
          <div><dt>Eingegangen am</dt><dd><time class="ziffern" datetime="${b.iso}">${b.datum}, ${b.uhr} Uhr</time></dd></div>
          <div><dt>Gekündigt</dt><dd>Röstpost-Abo ${esc(b.vertrag)}, ${b.art}</dd></div>
          <div><dt>Vertrag endet</dt><dd class="ziffern">${b.ende}</dd></div>
          <div><dt>Bestätigung an</dt><dd>${esc(b.email)}</dd></div>
        </dl>
        <p>Diese Bestätigung haben wir Ihnen eben auch per E-Mail geschickt. Speichern Sie sie, wenn Sie mögen, mit Datum und Uhrzeit.</p>
        <div class="knoepfe"><button class="knopf" type="button" data-speichern>Bestätigung speichern</button><a class="knopf zweit" href="#start" data-geh="start">Zur Startseite</a></div>
      </div>`;
    },

    login: () => `${warnband()}${pfad('Anmelden')}
      <h1 tabindex="-1">Anmelden</h1>
      <p class="intro">Um Ihr Abo zu kündigen, melden Sie sich bitte in Ihrem Kundenkonto an.</p>
      <form novalidate data-form="login" aria-label="Anmeldung" class="fehlerstelle" data-nr="2">
        <div class="fehlerliste" tabindex="-1" role="alert" hidden></div>
        ${feld({ id: 'email', label: 'E-Mail-Adresse', typ: 'email', auto: 'email', pflicht: true })}
        ${feld({ id: 'passwort', label: 'Passwort', typ: 'password', auto: 'current-password', pflicht: true })}
        <div class="abschluss"><button class="knopf" type="submit">Anmelden</button><a class="klein" href="#start" data-geh="start">Passwort vergessen?</a></div>
      </form>`,

    belehrung: () => `${warnband()}${pfad('Widerrufsbelehrung')}
      <h1 tabindex="-1">Widerrufsbelehrung</h1>
      <div class="belehrung fehlerstelle" data-nr="2">
        <h2>Widerrufsrecht</h2>
        <p>Sie haben das Recht, binnen vierzehn Tagen ohne Angabe von Gründen diesen Vertrag zu widerrufen. Die Widerrufsfrist beträgt vierzehn Tage ab dem Tag, an dem Sie oder ein von Ihnen benannter Dritter die Waren in Besitz genommen haben.</p>
        <p>Um Ihr Widerrufsrecht auszuüben, müssen Sie uns mittels einer eindeutigen Erklärung (z. B. ein mit der Post versandter Brief oder E-Mail) über Ihren Entschluss informieren. Sie können dafür das beigefügte Muster-Widerrufsformular verwenden.</p>
        <a class="pdf" href="#start" data-geh="start"><span>PDF</span> Muster-Widerrufsformular (84 KB)</a>
      </div>`
  };

  /* ── Randnotiz ── */
  const STAND = '<p class="quelle">Stand 02.11.2026. Textbausteine und Felder vom Kunden rechtlich prüfen lassen.</p>';
  const SCHRITTE = {
    w: ['„Vertrag widerrufen“ in der Fußzeile', 'Angaben, dann „Widerruf bestätigen“', 'Eingangsbestätigung mit Datum und Uhrzeit'],
    k: ['„Verträge hier kündigen“ in der Fußzeile', 'Bestätigungsseite, dann „jetzt kündigen“', 'Bestätigung sofort per E-Mail']
  };
  const schritte = (art, nr) => `<ol class="schritte">${SCHRITTE[art].map((t, i) => `<li${i < nr ? ' class="erledigt"' : ''}${i === nr ? ' aria-current="step"' : ''}>${t}${i < nr ? '<span class="nur-sr"> (erledigt)</span>' : ''}</li>`).join('')}</ol>`;
  const regeln = (ja, nein) => `<ul class="regeln">${ja.map(t => `<li class="ja">${t}</li>`).join('')}${nein.map(t => `<li class="nein">${t}</li>`).join('')}</ul>`;
  const fehlerpunkte = p => `<ol class="fehlerpunkte">${p.map(([nr, t, q]) => `<li data-nr="${nr}"><span>${t}<small>${q}</small></span></li>`).join('')}</ol>`;
  const IHK = 'IHK Braunschweig zu § 356a BGB';
  const BGH = 'BGH, 16.07.2026, I ZR 200/25';

  function notizInhalt() {
    const s = z.seite, v = z.v;
    if (v === 'richtig') {
      if (s === 'start') return `<h2 id="notiz-titel">Zwei Ausgänge, immer sichtbar</h2>${regeln(['Beide Knöpfe in der Fußzeile jeder Seite, mobil auch im Menü', 'Ohne Anmeldung, ohne Kundenkonto', 'Getrennt: Widerruf für Bestellungen, Kündigung für das Abo'], [])}`;
      if (s === 'widerruf') return `<h2 id="notiz-titel">Widerruf: nur das Nötige</h2>${schritte('w', 1)}${regeln(['Name', 'Bestellnummer, auf Wunsch einzelne Artikel', 'Weg für die Eingangsbestätigung'], ['Kein Grund, kein Konto'])}<p class="quelle">Laut ${IHK}: seit 19.06.2026; fehlt die Funktion, kann sich die Widerrufsfrist auf 12 Monate und 14 Tage verlängern.</p>`;
      if (s === 'w-ok') return `<h2 id="notiz-titel">Eingangsbestätigung</h2>${schritte('w', 2)}${regeln(['Inhalt der Erklärung, Datum und Uhrzeit', 'Per E-Mail, also auf dauerhaftem Datenträger', 'Belegt den Eingang, nicht die Wirksamkeit'], [])}`;
      if (s === 'kuendigung') return `<h2 id="notiz-titel">Kündigung: Bestätigungsseite</h2>${schritte('k', 1)}${regeln(['Art der Kündigung, bei außerordentlicher der Grund', 'Name und E-Mail zur Zuordnung', 'Vertrag und gewünschtes Ende'], ['Keine Pause, kein Rabatt, keine Umfrage'])}<p class="quelle">§ 312k Abs. 2 BGB; ${BGH}: Angebote wie eine Vertragspause gehören nicht auf diese Seite.</p>`;
      if (s === 'k-ok') return `<h2 id="notiz-titel">Bestätigung in Textform</h2>${schritte('k', 2)}${regeln(['Inhalt, Datum und Uhrzeit des Eingangs', 'Zeitpunkt, zu dem der Vertrag endet', 'Sofort per E-Mail, zum Speichern'], [])}<p class="quelle">§ 312k Abs. 3 und 4 BGB, Zusammenfassung lxgesetze.de.</p>`;
    }
    if (v === 'halteangebot') {
      if (s === 'start') return `<h2 id="notiz-titel">Fehler: Halteangebot und Hürden</h2><p>Die Fußzeile stimmt hier. Die Fehler stecken auf den beiden Formularseiten. Öffnen Sie unten „Vertrag widerrufen“ oder „Verträge hier kündigen“.</p>`;
      if (s === 'widerruf') return `<h2 id="notiz-titel">Fehler in diesem Beispiel</h2>${fehlerpunkte([[1, 'Grund als Pflichtfeld', `Der Grund darf nicht abgefragt werden (${IHK}).`], [2, 'Knopf heißt „Absenden“', 'Vorgegeben ist „Widerruf bestätigen“ oder gleich eindeutig.']])}`;
      if (s === 'kuendigung') return `<h2 id="notiz-titel">Fehler in diesem Beispiel</h2>${fehlerpunkte([[1, 'Großes Stimmungsbild', BGH], [2, 'Pause und Rabatt als Alternative', `${BGH}: Die Angaben nach § 312k BGB sind abschließend.`], [3, 'Lauter Knopf „Vertrag finden“, „jetzt kündigen“ blass', BGH]])}`;
      return notizInhaltRichtigNach();
    }
    if (v === 'versteckt') {
      if (s === 'start') return `<h2 id="notiz-titel">Fehler: versteckter Link</h2>${fehlerpunkte([[1, 'Eingeklappt unter „Rechtliches“, klein und grau', `Der Knopf muss ohne Weiteres auffindbar sein (${IHK}).`]])}`;
      if (s === 'login' || s === 'kuendigung') return `<h2 id="notiz-titel">Fehler in diesem Beispiel</h2>${fehlerpunkte([[1, 'Link nur unter „Rechtliches“', 'nicht leicht erreichbar'], [2, 'Kündigung erst nach Anmeldung', '§ 312k Abs. 2 BGB: Schaltfläche und Bestätigungsseite ständig verfügbar sowie unmittelbar und leicht zugänglich. Eine Anmeldung davor ist eine Hürde; vom Kunden prüfen lassen.']])}`;
      if (s === 'belehrung' || s === 'widerruf') return `<h2 id="notiz-titel">Fehler in diesem Beispiel</h2>${fehlerpunkte([[1, 'Link nur unter „Rechtliches“', 'nicht ohne Weiteres auffindbar'], [2, 'Nur Belehrung und PDF-Formular', 'Kein Knopf „Vertrag widerrufen“, kein zweistufiger Ablauf.']])}`;
    }
    return notizInhaltRichtigNach();
  }
  function notizInhaltRichtigNach() { const v = z.v; z.v = 'richtig'; const t = notizInhalt(); z.v = v; return t; }
  function zeichneNotiz() { notiz.innerHTML = `<p class="mono">Notiz zur Demo</p>${notizInhalt()}${STAND}`; }

  /* ── Fußzeile und Menü je Variante ── */
  function zeichneFuss() {
    const offen = z.v !== 'versteckt';
    fuss.innerHTML = `<div class="fuss-innen">
      ${offen ? `<div class="vertraege"><p><b>Verträge</b>Widerruf und Kündigung, ohne Anmeldung.</p>
        <a class="vknopf" href="#widerruf" data-geh="widerruf">Vertrag widerrufen ${PFEIL}</a>
        <a class="vknopf" href="#kuendigung" data-geh="kuendigung">Verträge hier kündigen ${PFEIL}</a></div>` : ''}
      <div class="spalten">
        <div><p class="claim">Frisch aus Rosenheim.</p><p class="adresse">Mangfall Kaffeerösterei<br>Am Mangfallkanal 7, 83022 Rosenheim<br>Rösttage Dienstag und Freitag</p></div>
        <div><h2>Hilfe</h2><ul><li><a href="#start" data-geh="start">Versand</a></li><li><a href="#start" data-geh="start">Rücksendung</a></li><li><a href="#start" data-geh="start">Röstpost ändern</a></li><li><a href="#start" data-geh="start">Kontakt</a></li></ul></div>
        <div><h2>Rösterei</h2><ul><li><a href="#start" data-geh="start">Unsere Bohnen</a></li><li><a href="#start" data-geh="start">Röstkurse</a></li><li><a href="#start" data-geh="start">Für Cafés</a></li></ul></div>
        <div>${offen ? `<h2>Rechtliches</h2><ul><li><a href="#start" data-geh="start">AGB</a></li><li><a href="#start" data-geh="start">Widerrufsbelehrung</a></li><li><a href="#start" data-geh="start">Datenschutz</a></li><li><a href="#start" data-geh="start">Impressum</a></li></ul>`
          : `<details class="rechtlich fehlerstelle" data-nr="1"><summary>Rechtliches</summary><ul class="grau"><li><a href="#start" data-geh="start">AGB</a></li><li><a href="#widerruf" data-geh="widerruf">Widerrufsbelehrung</a></li><li><a href="#kuendigung" data-geh="kuendigung">Kündigung im Kundenkonto</a></li><li><a href="#start" data-geh="start">Impressum</a></li></ul></details>`}</div>
      </div>
      <div class="unterzeile"><span>© 2026 Mangfall Kaffeerösterei</span><span>Alle Preise inkl. MwSt., zzgl. Versand</span></div>
    </div>`;
    menueListe.innerHTML = `<a href="#start" data-geh="start">Kaffee</a><a href="#start" data-geh="start">Röstpost-Abo</a><a href="#start" data-geh="start">Zubehör</a><a href="#start" data-geh="start">Konto</a>
      ${offen ? '<p class="trenn label">Verträge</p><a href="#widerruf" data-geh="widerruf">Vertrag widerrufen</a><a href="#kuendigung" data-geh="kuendigung">Verträge hier kündigen</a>' : ''}`;
  }

  /* ── Seitenwechsel ── */
  let wechselUhr = null;
  function geh(seite, { fokus = !html.classList.contains('vorfuehrung') } = {}) {
    if (seite === 'w-ok' || seite === 'k-ok') { if (!z.beleg) seite = 'start'; }
    z.seite = seite;
    clearTimeout(wechselUhr);
    const zeige = () => {
      panel.classList.remove('raus', 'rein');
      panel.innerHTML = SEITEN[seite]();
      void panel.offsetWidth; panel.classList.add('rein');
      zeichneNotiz();
      const form = $('form', panel);
      if (form?.dataset.form === 'widerruf') bindeFormular(form, d => belege('w', d));
      if (form?.dataset.form === 'kuendigung') bindeFormular(form, d => belege('k', d));
      if (form?.dataset.form === 'login') bindeFormular(form, () => {});
      const obenSeite = $('#inhalt').getBoundingClientRect().top;
      if (seite !== 'start' && obenSeite < 0) scrollTo({ top: scrollY + obenSeite - 8, behavior: reduziert() ? 'auto' : 'smooth' });
      if (fokus) $('h1', panel)?.focus({ preventScroll: true });
    };
    if (reduziert() || !panel.innerHTML) return zeige();
    panel.classList.add('raus');
    wechselUhr = setTimeout(zeige, 200);
  }
  function belege(art, d) {
    const jetzt = new Date();
    const b = { iso: jetzt.toISOString(), datum: datum.format(jetzt), uhr: uhr.format(jetzt), name: d.get('name') || '', email: d.get('email') || '' };
    if (art === 'w') { b.bestellung = d.get('bestellung'); b.artikel = d.get('umfang') === 'teile' ? d.get('artikel') : ''; }
    else {
      b.vertrag = d.get('vertrag');
      b.art = d.get('art') === 'ausserordentlich' ? 'außerordentlich' : 'ordentlich';
      if (d.get('ende') === 'datum') b.ende = datum.format(new Date(d.get('zum') + 'T12:00'));
      else if (d.get('art') === 'ausserordentlich') b.ende = 'sofort mit Eingang';
      else { const e = new Date(jetzt.getFullYear(), jetzt.getMonth() + 1, 0); b.ende = `zum nächstmöglichen Zeitpunkt, laut Abo am ${datum.format(e)}`; }
    }
    z.beleg = b;
    geh(art === 'w' ? 'w-ok' : 'k-ok');
  }
  function speichern() {
    const b = z.beleg; if (!b) return;
    const text = z.seite === 'w-ok'
      ? `Eingangsbestätigung Widerruf\nMangfall Kaffeerösterei, Am Mangfallkanal 7, 83022 Rosenheim\n\nEingegangen am: ${b.datum}, ${b.uhr} Uhr\nErklärung: Hiermit widerrufe ich den Vertrag über die Bestellung ${b.bestellung}${b.artikel ? ' (Artikel: ' + b.artikel + ')' : ' (ganze Bestellung)'}.\nName: ${b.name}\nAbgegeben über: Widerrufsfunktion „Widerruf bestätigen“ auf der Website\nBestätigung an: ${b.email}\n`
      : `Kündigungsbestätigung\nMangfall Kaffeerösterei, Am Mangfallkanal 7, 83022 Rosenheim\n\nEingegangen am: ${b.datum}, ${b.uhr} Uhr\nGekündigt: Röstpost-Abo ${b.vertrag}, ${b.art}\nVertrag endet: ${b.ende}\nAbgegeben über: Schaltfläche „jetzt kündigen“ auf der Website\nBestätigung an: ${b.email}\n`;
    if (html.classList.contains('vorfuehrung')) return;
    const a = document.createElement('a');
    a.href = URL.createObjectURL(new Blob([text], { type: 'text/plain;charset=utf-8' }));
    a.download = (z.seite === 'w-ok' ? 'widerruf-' : 'kuendigung-') + b.iso.slice(0, 10) + '.txt';
    a.click(); setTimeout(() => URL.revokeObjectURL(a.href), 1000);
  }

  document.addEventListener('click', e => {
    const g = e.target.closest('[data-geh]');
    if (g) { e.preventDefault(); schliesseMenue(); geh(g.dataset.geh); return; }
    if (e.target.closest('[data-speichern]')) speichern();
  });
  function schliesseMenue() { menueListe.hidden = true; menueKnopf.setAttribute('aria-expanded', 'false'); }
  menueKnopf.addEventListener('click', () => { const auf = menueListe.hidden; menueListe.hidden = !auf; menueKnopf.setAttribute('aria-expanded', String(auf)); });
  document.addEventListener('keydown', e => { if (e.key === 'Escape' && !menueListe.hidden) { schliesseMenue(); menueKnopf.focus(); } });

  /* ── Variante (?v=) ── */
  const VARIANTEN = ['richtig', 'halteangebot', 'versteckt'];
  function variante(v, { seite = 'start' } = {}) {
    if (!VARIANTEN.includes(v)) v = VARIANTEN[0];
    z.v = v; z.beleg = null;
    document.querySelectorAll('[data-v]').forEach(b => b.setAttribute('aria-pressed', String(b.dataset.v === v)));
    const q = new URLSearchParams(location.search);
    if (q.get('v') !== v) { q.set('v', v); history.replaceState(null, '', '?' + q.toString().replace(/=(&|$)/g, '$1')); }
    zeichneFuss();
    panel.innerHTML = '';
    geh(seite, { fokus: false });
  }
  document.querySelectorAll('[data-v]').forEach(b => b.addEventListener('click', () => { if (html.classList.contains('vorfuehrung')) pause(); html.classList.remove('vorfuehrung'); variante(b.dataset.v); }));
  $('[data-vonvorn]').addEventListener('click', () => { pause(); html.classList.remove('vorfuehrung'); variante(z.v); });

  /* ── Vorführung ── */
  let zeitgeber = [], laeuft = false;
  function nach(ms, f) { zeitgeber.push(setTimeout(f, ms)); }
  function tippe(el) { if (!el) return; el.classList.remove('tipp'); void el.offsetWidth; el.classList.add('tipp'); setTimeout(() => el.classList.remove('tipp'), 650); }
  function klick(sel) { const el = typeof sel === 'string' ? $(sel) : sel; if (!el) return; tippe(el); el.click(); }
  function zeigeFuss() { const f = $('.vertraege, .rechtlich', fuss); if (!f) return; const r = f.getBoundingClientRect(); if (r.bottom > innerHeight) scrollTo({ top: scrollY + r.bottom - innerHeight + 24, behavior: reduziert() ? 'auto' : 'smooth' }); }
  function nachOben() { if (scrollY > 0) scrollTo({ top: 0, behavior: reduziert() ? 'auto' : 'smooth' }); }
  function insBild(el) { if (!el) return; const r = el.getBoundingClientRect(); if (r.top < 0 || r.bottom > innerHeight - 20) scrollTo({ top: scrollY + r.top - innerHeight * .35, behavior: reduziert() ? 'auto' : 'smooth' }); }
  function schreibe(id, text, start) {
    nach(start, () => { const el = $('#' + id, panel); if (!el) return; insBild(el.closest('.feld')); tippe(el.closest('.feld')); el.focus({ preventScroll: true }); el.value = ''; });
    const schritt = reduziert() ? 0 : 38;
    [...text].forEach((c, i) => nach(start + 120 + i * schritt, () => { const el = $('#' + id, panel); if (el) { el.value += c; el.dispatchEvent(new Event('input', { bubbles: true })); } }));
    return start + 120 + text.length * schritt;
  }
  function absenden(t) { nach(t - 350, () => insBild($('form button[type=submit]', panel))); nach(t, () => { const k = $('form button[type=submit]', panel); tippe(k); $('form', panel)?.requestSubmit(); }); }

  function rundeRichtig() {
    geh('start', { fokus: false }); nach(300, zeigeFuss);
    nach(1100, () => klick('.vertraege [data-geh="widerruf"]'));
    nach(1900, nachOben);
    absenden(2200);                                         // leer absenden: Fehlerliste
    let t = schreibe('name', 'Lena Hofbauer', 3600);
    t = schreibe('bestellung', 'MK-2026-10482', t + 250);
    t = schreibe('email', 'lena.hofbauer@example.de', t + 250);
    absenden(t + 500);
    nach(t + 4200, () => { geh('start', { fokus: false }); nach(300, zeigeFuss); });
    nach(t + 5300, () => klick('.vertraege [data-geh="kuendigung"]'));
    nach(t + 6100, nachOben);
    let u = schreibe('name', 'Lena Hofbauer', t + 6500);
    u = schreibe('email', 'lena.hofbauer@example.de', u + 250);
    u = schreibe('vertrag', 'RP-48213', u + 250);
    absenden(u + 500);
    nach(u + 4600, runde);
  }
  function rundeHalte() {
    geh('start', { fokus: false }); nach(300, zeigeFuss);
    nach(1100, () => klick('.vertraege [data-geh="kuendigung"]'));
    nach(1900, nachOben);
    nach(3700, () => insBild($('.knopf.lockt', panel)));
    nach(5600, () => { geh('start', { fokus: false }); nach(300, zeigeFuss); });
    nach(6600, () => klick('.vertraege [data-geh="widerruf"]'));
    nach(7400, nachOben);
    nach(8600, () => { const s = $('#wgrund', panel); if (s) { insBild(s); tippe(s.closest('.feld')); s.selectedIndex = 1; } });
    nach(11800, runde);
  }
  function rundeVersteckt() {
    geh('start', { fokus: false }); nach(300, zeigeFuss);
    nach(1300, () => { const d = $('.rechtlich', fuss); if (d) { tippe($('summary', d)); d.open = true; } });
    nach(2600, () => klick('.rechtlich [data-geh="kuendigung"]'));
    nach(3400, nachOben);
    nach(7000, () => { geh('start', { fokus: false }); nach(300, zeigeFuss); });
    nach(8000, () => { const d = $('.rechtlich', fuss); if (d) { tippe($('summary', d)); d.open = true; } });
    nach(9000, () => klick('.rechtlich [data-geh="widerruf"]'));
    nach(9800, nachOben);
    nach(13200, runde);
  }
  function runde() { z.beleg = null; ({ richtig: rundeRichtig, halteangebot: rundeHalte, versteckt: rundeVersteckt })[z.v](); }
  function pause() { zeitgeber.forEach(clearTimeout); zeitgeber = []; laeuft = false; }
  function neustart() { pause(); html.classList.remove('vorfuehrung'); schliesseMenue(); variante(z.v); scrollTo(0, 0); }
  function vorfuehren() { neustart(); html.classList.add('vorfuehrung'); laeuft = true; runde(); }

  variante(new URLSearchParams(location.search).get('v'));

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