Zum Inhalt
← Sektionen & Funktionen

Vorbestellung mit Abholtag und Zeitfenster

Vorbestellen ohne Online-Zahlung: Laden, Abholtag mit Ruhetagen und Bestellschluss, Zeitfenster mit Restkapazität, Ware mit Stepper, Allergenen und Vorlauf, mitlaufende Summe, Fehlerliste und eine Bestätigung mit Nummer und .ics.

Bäckerei, Konditorei, Rösterei, Mittagstisch: Stückware mit Bestellschluss am Vortag und Torten mit Vorlauf.

Vorbestellen für Bäckerei, Metzgerei oder Hofladen, bezahlt wird bei Abholung. Zuerst Laden und Abholtag: Ruhetage sind gesperrt, der Bestellschluss steht dabei („bis 18 Uhr am Vortag“). Dann ein Zeitfenster mit Restkapazität. Danach die Ware mit Mengen-Steppern, Allergenen und Vorlaufzeiten (Torten zwei Tage). Eine Zusammenfassung läuft mit und zeigt Abholung, Positionen und Summe. Nach dem Absenden wird das Formular zur Bestätigung mit Bestellnummer, Abholzeit, „In den Kalender“ und dem Weg zum Ändern. Ware nach Gewicht zeigt den Grundpreis je Kilogramm und einen ehrlichen Circa-Preis.

Die Theke ist morgens voll, und das Lieblingsbrot ist um zehn weg. Vorbestellen löst beides, wenn es so einfach ist wie ein Zettel an der Kasse: Tag, Uhrzeit, Menge, Name. Gesperrte Tage und volle Fenster mit Grund verhindern Bestellungen, die der Laden nicht erfüllen kann. Ohne Online-Zahlung braucht der Betrieb keinen Zahlungsdienst und kein Kundenkonto.

Wann einsetzen

Bäckerei, Konditorei, Metzgerei, Hofladen, Feinkost, Rösterei, Mittagstisch zum Mitnehmen. Besonders vor Feiertagen (Stollen, Braten, Platten) und für Torten mit Vorlauf.

Wann nicht

Nicht, wenn online bezahlt oder geliefert wird (dann warenkorb-drawer, Widerruf und Versandangaben). Nicht ohne jemanden, der Bestellungen rechtzeitig liest: Bestellschluss und Kapazität müssen im Laden ankommen. Keine Mengen ohne Obergrenze; große Mengen besser telefonisch.

Varianten

  • Backstube: Stückware mit Stepper, Bestellschluss am Vortag, Torten mit zwei Tagen Vorlauf, Zeitfenster je 30 Minuten
  • Theke nach Gewicht: Stepper in 250-g-Schritten, Grundpreis je kg, Circa-Preis mit Hinweis „Endpreis nach Wiegen“

Umsetzung

Ein <form> mit drei <fieldset>: Abholung, Ware, Kontakt. Laden als Radiogruppe mit Öffnungszeiten. Abholtag als Radiogruppe der nächsten 7 Tage (Intl.DateTimeFormat('de-DE', {weekday: 'short', day: 'numeric', month: 'numeric'})); Ruhetage und Tage nach dem Bestellschluss sind disabled mit sichtbarem Grund. Zeitfenster als Radiogruppe mit Restkapazität aus dem Server („noch 3“), volle Fenster disabled. Ware als Liste mit Stepper (Knöpfe − und + mit aria-label, <input type=number inputmode=numeric> mit min, max, step), Allergene als Abkürzung mit Legende, Vorlauf je Produkt („ab Freitag“) sperrt den Stepper für zu frühe Tage. Zusammenfassung als <aside> mit aria-live="polite" für die Summe; mobil als feste Leiste unten. Absenden: Fehler sammeln (formular-fehlerzusammenfassung), sonst POST an den Betrieb, Antwort mit Bestellnummer; das Formular wird zur Bestätigung (Fokus auf deren h2) mit Abholzeit, Positionen, „In den Kalender“ (.ics als Blob) und „Ändern bis Vortag 18 Uhr: anrufen“. Ware nach Gewicht: step 250, Grundpreis je kg neben dem Preis, Summe als „ca.“ und Satz zur Abweichung. Bezahlt wird im Laden. Allergenkennzeichnung, Preisangaben und Datenschutz vom Kunden prüfen lassen.

Robust umsetzen

Telefon. Einspaltig in derselben Reihenfolge. Abholtage als waagerechte Reihe, die umbricht statt zu scrollen. Stepper-Knöpfe 44 px. Die Zusammenfassung wird zur festen Leiste unten mit Summe, Zahl der Positionen und „Vorbestellen“; ein Tipp öffnet die volle Zusammenfassung.

Ohne Bewegung. Ziffern wechseln ohne Rollen, die Bestätigung erscheint ohne Versatz.

Barrierefrei. fieldset mit legend für jede Gruppe, gesperrte Tage und Fenster mit Grund im Text. Stepper-Knöpfe benannt („Bauernbrot: eins mehr“), die Zahl bleibt ein Eingabefeld. Summe in aria-live. Fehler am Feld und gesammelt oben, Fokus auf die Zusammenfassung der Fehler. Nach dem Absenden Fokus auf die Bestätigung. Allergene ausgeschrieben in der Legende und als title-freier Text.

Code

<!DOCTYPE html>
<!--
  Vorbestellung mit Abholtag und Zeitfenster
  Fundus · Sektionen & Funktionen · https://cecodesign.de/lab/fundus/sektionen/vorbestellung-abholung/
  Eigenständige Datei. Bilder liegen hier unter bilder/ – eigene einsetzen.
-->
<html lang="de" data-v="backstube">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Vorbestellung mit Abholtag und Zeitfenster · 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: 'Fraunces'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/fraunces:vf@latest/latin-full-normal.woff2') format('woff2-variations'); font-weight: 100 900; font-style: normal; font-display: swap; }
@font-face { font-family: 'Commissioner'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/commissioner:vf@latest/latin-full-normal.woff2') format('woff2-variations'); font-weight: 100 900; font-display: swap; }
@font-face { font-family: 'Geist Mono'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/geist-mono:vf@latest/latin-wght-normal.woff2') format('woff2-variations'); font-weight: 100 900; font-display: swap; }

/* Token aus gastro-warm-appetitlich, UI nach token-toggle-asymmetrisch */
:root {
  --grund: #f0ece0; --karte: #f8f5ec; --text: #353839; --leise: #5c5f5f; --linie: rgba(53, 56, 57, .2); --salbei: #7f937a; --salbei-hell: #dfe4d6; --dunkel: #293e3a; --senf: #eac16e; --rose: #e1a894; --fehler: #9b2c1f;
  --h: 'Fraunces', Georgia, serif; --t: 'Commissioner', system-ui, sans-serif;
  --auf: cubic-bezier(0.16, 1, 0.3, 1); --zu: cubic-bezier(0.7, 0, 0.84, 0); --bildkurve: cubic-bezier(0.16, 1, 0.3, 1);
  --ui: 200ms; --ziffer: 300ms; --wandel: 400ms; --bild: 700ms;
  --r: 6px; --R: 12px;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
html { background: var(--grund); color: var(--text); font-family: var(--t); -webkit-font-smoothing: antialiased; scroll-padding-top: 20px; }
:root:has(:focus-visible) { scroll-behavior: auto; }
body { font-size: 1.0625rem; line-height: 1.55; min-height: 100svh; }
a { color: inherit; text-underline-offset: .2em; }
button, input, textarea { font: inherit; color: inherit; }
button { cursor: pointer; background: none; border: 0; }
:focus-visible { outline: 2px solid var(--dunkel); outline-offset: 3px; }
.vh { position: absolute !important; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
.huelle { max-width: 1360px; margin: 0 auto; padding: 0 clamp(16px, 4vw, 48px); }
.ziffern { font-variant-numeric: tabular-nums lining-nums; }
.rubrik { font-size: .75rem; font-weight: 600; letter-spacing: .15em; text-transform: uppercase; font-variation-settings: 'FLAR' 80; color: var(--leise); }
h1, h2, h3 { font-family: var(--h); font-weight: 500; text-wrap: balance; font-variation-settings: 'SOFT' 80, 'WONK' 0; letter-spacing: -.01em; }
p { text-wrap: pretty; hyphens: auto; }

/* Kopf */
.kopf { border-bottom: 1px solid var(--linie); position: relative; z-index: 20; background: var(--grund); }
.kopf .huelle { display: flex; align-items: center; gap: 28px; min-height: 72px; }
.marke { display: grid; text-decoration: none; margin-right: auto; line-height: 1; }
.marke b { font: 600 1.875rem/1 var(--h); font-variation-settings: 'SOFT' 100, 'WONK' 1; letter-spacing: -.02em; }
.marke span { font-size: .75rem; color: var(--leise); margin-top: 4px; letter-spacing: .02em; }
.nav ul { display: flex; gap: 6px; list-style: none; }
.nav a { display: block; text-decoration: none; padding: 8px 14px; border-radius: 999px; font-weight: 500; font-size: .9375rem; transition: background-color var(--ui) var(--auf); }
.nav a:hover { background: var(--salbei-hell); }
.nav a[aria-current] { background: var(--dunkel); color: #fff; }
.offen { display: inline-flex; align-items: center; gap: 8px; font-size: .875rem; font-weight: 500; }
.offen i { width: 9px; height: 9px; border-radius: 50%; background: var(--salbei); box-shadow: 0 0 0 3px var(--salbei-hell); }
.menueknopf { display: none; font-weight: 600; padding: 10px 4px; }

/* Einstieg */
.einstieg { display: grid; grid-template-columns: minmax(0, 6fr) minmax(0, 5fr); gap: clamp(20px, 4vw, 56px); align-items: center; padding: clamp(28px, 4vw, 56px) 0; }
.einstieg h1 { font-size: clamp(2.5rem, 1.6rem + 4vw, 5rem); line-height: 1.02; font-weight: 500; margin-top: 12px; }
.einstieg h1 em { font-style: italic; font-weight: 400; color: var(--dunkel); }
.einstieg p.lead { font-size: 1.1875rem; margin-top: 18px; max-width: 34rem; }
.regeln { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 20px; list-style: none; }
.regeln li { background: var(--karte); border: 1px solid var(--linie); padding: 7px 14px; border-radius: 999px; font-size: .875rem; font-weight: 500; }
.regeln li b { font-weight: 700; }
.einstieg figure { border-radius: var(--R); overflow: hidden; aspect-ratio: 4 / 3; background: #c9b89b; position: relative; }
.einstieg figure img { width: 100%; height: 100%; object-fit: cover; display: block; transition: opacity var(--bild) var(--bildkurve), transform var(--bild) var(--bildkurve); }
.js .einstieg figure img[data-laed] { opacity: 0; transform: scale(.96); }
.einstieg figcaption { position: absolute; left: 12px; bottom: 12px; background: rgba(248, 245, 236, .92); padding: 5px 10px; border-radius: var(--r); font-size: .75rem; }
.einstieg .tafel { aspect-ratio: 4 / 3; border-radius: var(--R); background: var(--dunkel); color: var(--grund); padding: clamp(20px, 3vw, 40px); display: grid; align-content: space-between; }
.einstieg .tafel b { font: 500 clamp(2rem, 1.4rem + 2vw, 3.25rem)/1.05 var(--h); font-style: italic; font-variation-settings: 'SOFT' 100, 'WONK' 1; }
.einstieg .tafel ul { list-style: none; display: grid; gap: 6px; font-size: .9375rem; }
.einstieg .tafel li { display: flex; justify-content: space-between; gap: 12px; border-bottom: 1px solid rgba(240, 236, 224, .25); padding-bottom: 6px; }

/* Formular */
.bestellen { display: grid; grid-template-columns: minmax(0, 8fr) minmax(0, 4fr); gap: clamp(20px, 3vw, 40px); align-items: start; padding-bottom: clamp(56px, 7vw, 112px); }
fieldset { border: 0; min-width: 0; }
.schritt { border-top: 1px solid var(--text); padding: 22px 0 32px; display: grid; grid-template-columns: 3.5rem minmax(0, 1fr); gap: 0 16px; }
.schritt > legend { display: contents; }
.schritt .nr { grid-row: 1 / span 3; font: 400 2.75rem/1 var(--h); font-variation-settings: 'SOFT' 100, 'WONK' 1; color: #5f7359; font-style: italic; }
.schritt h2 { font-size: clamp(1.5rem, 1.2rem + 1vw, 2rem); line-height: 1.1; }
.schritt .unter { color: var(--leise); font-size: .9375rem; margin-top: 4px; }
.schritt .inhalt { grid-column: 2; margin-top: 18px; display: grid; gap: 20px; }
.gruppe { border: 0; display: grid; gap: 10px; }
.gruppe > legend { font-weight: 600; font-size: .9375rem; margin-bottom: 8px; }
.laeden { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; }
.wahl { position: relative; display: block; cursor: pointer; }
.wahl input { position: absolute; opacity: 0; inset: 0; cursor: pointer; margin: 0; }
.wahl .k { display: block; border: 1px solid var(--linie); background: var(--karte); border-radius: var(--R); padding: 14px 16px; transition: border-color var(--ui) var(--auf), background-color var(--ui) var(--auf); }
.wahl:hover .k { border-color: var(--text); }
.wahl input:checked + .k { border-color: var(--dunkel); background: #fff; box-shadow: inset 0 0 0 1px var(--dunkel); }
.wahl input:focus-visible + .k { outline: 2px solid var(--dunkel); outline-offset: 3px; }
.wahl input:disabled + .k { cursor: not-allowed; background: transparent; border-style: dashed; color: var(--leise); }
.wahl b { display: block; font-weight: 600; }
.wahl small { display: block; font-size: .8125rem; color: var(--leise); margin-top: 2px; }
.chips { display: flex; flex-wrap: wrap; gap: 8px; }
.chips .wahl .k { border-radius: 999px; padding: 9px 16px; text-align: center; min-width: 6.5rem; }
.chips .wahl input:checked + .k { background: var(--dunkel); color: #fff; box-shadow: none; }
.chips .wahl input:checked + .k small { color: #d4dcd8; }
.chips .wahl .k small { margin-top: 0; }
.chips.fenster .wahl .k { min-width: 7.5rem; }
.grund { font-size: .8125rem; color: var(--leise); }
.waren { display: grid; gap: 22px; }
.waren h3 { font-size: 1.25rem; display: flex; justify-content: space-between; align-items: baseline; gap: 12px; }
.waren h3 small { font: 500 .8125rem/1.2 var(--t); color: var(--leise); }
.waren ul { list-style: none; border-top: 1px solid var(--linie); }
.ware { display: grid; grid-template-columns: minmax(0, 1fr) auto auto; gap: 2px 18px; align-items: center; padding: 14px 0; border-bottom: 1px solid var(--linie); }
.ware b { font-weight: 600; font-size: 1.0625rem; }
.ware .preis { font-variant-numeric: tabular-nums; font-weight: 600; text-align: right; white-space: nowrap; }
.ware .preis small { display: block; font-weight: 400; font-size: .75rem; color: var(--leise); }
.ware .info { grid-column: 1; font-size: .8125rem; color: var(--leise); }
.ware .info em { font-style: normal; color: var(--text); }
.ware .sperre { grid-column: 1 / -1; font-size: .8125rem; color: var(--fehler); font-weight: 600; }
.ware[data-gesperrt] b, .ware[data-gesperrt] .preis { color: var(--leise); }
.ware .extra { grid-column: 1 / -1; font-size: .875rem; display: flex; align-items: center; gap: 8px; }
.ware .extra input { width: 18px; height: 18px; accent-color: var(--dunkel); }
.stepper { grid-column: 3; grid-row: 1 / span 2; display: inline-grid; grid-template-columns: 40px 3.2em 40px; align-items: center; border: 1px solid var(--text); border-radius: 999px; background: #fff; overflow: hidden; }
.stepper button { height: 40px; font-size: 1.25rem; font-weight: 500; transition: background-color var(--ui) var(--auf); }
.stepper button:hover:not(:disabled) { background: var(--salbei-hell); }
.stepper button:disabled { color: #a3a59f; cursor: not-allowed; }
.stepper .zahl { position: relative; overflow: hidden; height: 40px; }
.stepper input { width: 100%; height: 40px; border: 0; text-align: center; font-variant-numeric: tabular-nums; font-weight: 600; background: transparent; -moz-appearance: textfield; }
.stepper input::-webkit-outer-spin-button, .stepper input::-webkit-inner-spin-button { -webkit-appearance: none; }
.stepper input[data-rollt="hoch"] { animation: hoch var(--ziffer) var(--auf); }
.stepper input[data-rollt="runter"] { animation: runter var(--ziffer) var(--auf); }
@keyframes hoch { from { transform: translateY(.6em); opacity: 0; } }
@keyframes runter { from { transform: translateY(-.6em); opacity: 0; } }
.feld { display: grid; gap: 5px; }
.feld label { font-weight: 600; font-size: .9375rem; }
.feld label small { font-weight: 400; color: var(--leise); }
.feld input, .feld textarea { border: 1px solid var(--text); background: #fff; border-radius: var(--r); padding: 11px 12px; width: 100%; }
.feld textarea { min-height: 84px; resize: vertical; }
.feld input[aria-invalid="true"] { border-color: var(--fehler); box-shadow: inset 0 0 0 1px var(--fehler); }
.feld .fehler { color: var(--fehler); font-size: .875rem; font-weight: 600; }
.zwei { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 14px; }
.fehlerliste { border: 2px solid var(--fehler); border-radius: var(--R); background: #fff; padding: 16px 18px; margin-bottom: 18px; }
.fehlerliste h2 { font-size: 1.25rem; color: var(--fehler); }
.fehlerliste ul { margin-top: 8px; padding-left: 1.2em; }
.fehlerliste a { color: var(--fehler); font-weight: 600; }

/* Zusammenfassung */
.zusammen { position: sticky; top: 20px; background: var(--karte); border: 1px solid var(--linie); border-radius: var(--R); padding: 20px; display: grid; gap: 14px; }
.zusammen h2 { font-size: 1.5rem; }
.zusammen .abholung { background: var(--salbei-hell); border-radius: var(--r); padding: 10px 12px; font-size: .9375rem; }
.zusammen .abholung b { display: block; }
.posten { list-style: none; display: grid; gap: 6px; font-size: .9375rem; max-height: 260px; overflow: auto; }
.posten li { display: grid; grid-template-columns: 2.4em minmax(0, 1fr) auto; gap: 8px; }
.posten li span:first-child { font-variant-numeric: tabular-nums; color: var(--leise); }
.posten li span:last-child { font-variant-numeric: tabular-nums; }
.posten .leer { display: block; color: var(--leise); }
.summe { display: flex; justify-content: space-between; align-items: baseline; border-top: 1px solid var(--text); padding-top: 12px; font: 500 1.5rem/1.1 var(--h); }
.summe output { white-space: nowrap; }
.summe small { display: block; font: 400 .8125rem/1.3 var(--t); color: var(--leise); }
.abschicken { width: 100%; background: var(--dunkel); color: #fff; border-radius: 999px; padding: 15px 20px; font-weight: 600; font-size: 1.0625rem; display: flex; justify-content: center; gap: 10px; transition: background-color var(--ui) var(--auf), transform var(--ui) var(--auf); }
.abschicken:hover { background: #1c2c29; }
.abschicken:active { transform: scale(.98); }
.abschicken[aria-busy="true"] { opacity: .8; cursor: progress; }
.klein { font-size: .8125rem; color: var(--leise); }

/* Bestätigung */
.bestaetigung[hidden], .formular[hidden], .zusammen[hidden] { display: none; }
.bestaetigung { grid-column: 1 / -1; background: #fff; border-radius: var(--R); border: 1px solid var(--linie); padding: clamp(24px, 4vw, 56px); display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: clamp(20px, 4vw, 56px); animation: wandel var(--wandel) var(--auf) both; }
.bestaetigung:focus { outline: none; }
@keyframes wandel { from { opacity: 0; transform: translateY(16px); } }
.bestaetigung h2 { font-size: clamp(2.25rem, 1.6rem + 2.4vw, 3.75rem); line-height: 1; }
.bestaetigung h2 em { font-style: italic; color: var(--dunkel); }
.bestaetigung .nr { display: inline-block; margin-top: 16px; background: var(--senf); border-radius: 999px; padding: 6px 14px; font-weight: 700; font-variant-numeric: tabular-nums; letter-spacing: .04em; }
.bestaetigung dl { display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 8px 18px; }
.bestaetigung dt { color: var(--leise); font-size: .9375rem; }
.bestaetigung dd { font-weight: 600; }
.bestaetigung .knoepfe { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 22px; }
.bestaetigung .knoepfe button { border: 1px solid var(--text); border-radius: 999px; padding: 10px 16px; font-weight: 600; font-size: .9375rem; transition: background-color var(--ui) var(--auf); }
.bestaetigung .knoepfe button:hover { background: var(--salbei-hell); }
.bestaetigung .knoepfe button:first-child { background: var(--dunkel); color: #fff; border-color: var(--dunkel); }

/* Telefonleiste, Fuß, Pult */
.leiste { display: none; }
.fuss { background: var(--dunkel); color: #e8e4d8; padding: clamp(40px, 5vw, 72px) 0 28px; font-size: .9375rem; }
.fuss .reihe { display: grid; grid-template-columns: 1.4fr 1fr 1fr; gap: 28px; }
.fuss b.name { font: 600 2rem/1 var(--h); font-variation-settings: 'SOFT' 100, 'WONK' 1; color: #fff; display: block; margin-bottom: 10px; }
.fuss h2 { font: 600 .75rem/1.2 var(--t); letter-spacing: .15em; text-transform: uppercase; color: var(--senf); margin-bottom: 10px; font-variation-settings: 'FLAR' 80; }
.fuss ul { list-style: none; display: grid; gap: 4px; }
.fuss a { text-decoration: none; }
.fuss a:hover { text-decoration: underline; }
.fuss :focus-visible { outline-color: var(--senf); }
.fuss .unten { margin-top: 36px; padding-top: 14px; border-top: 1px solid rgba(232, 228, 216, .2); font-size: .8125rem; color: #c3c9c2; }
.pult { position: fixed; right: 16px; bottom: 16px; z-index: 40; width: 240px; background: #fff; border: 1px solid var(--linie); border-radius: var(--R); padding: 12px; display: grid; gap: 8px; font-size: 13px; }
.pult .m { font: 500 10.5px/1.3 'Geist Mono', monospace; letter-spacing: .14em; text-transform: uppercase; color: var(--leise); }
.pult .reihe { display: grid; grid-template-columns: 1fr 1fr; border: 1px solid var(--text); border-radius: 999px; overflow: hidden; }
.pult .reihe button { padding: 7px 4px; font-weight: 600; }
.pult .reihe button[aria-pressed="true"] { background: var(--dunkel); color: #fff; }
.pult .werte { font: 400 10.5px/1.5 'Geist Mono', monospace; color: var(--leise); }
.zeiger { position: fixed; left: 0; top: 0; width: 22px; height: 22px; z-index: 60; pointer-events: none; opacity: 0; transition: opacity 200ms var(--auf); }
.vorfuehrung .zeiger { opacity: 1; }
.zeiger.drueckt { scale: .86; transform-origin: 3px 2px; }
.vorschau body { overflow: hidden; }
.vorschau .pult, .vorschau .leiste { display: none !important; }

@media (max-width: 1080px) {
  .nav, .offen { display: none; } .menueknopf { display: inline-block; }
  .nav[data-offen] { display: block; position: absolute; top: 100%; left: 0; right: 0; background: var(--grund); border-bottom: 1px solid var(--linie); padding: 10px clamp(16px, 4vw, 48px) 16px; }
  .nav[data-offen] ul { flex-direction: column; }
}
@media (max-width: 900px) {
  .einstieg, .bestellen, .bestaetigung { grid-template-columns: minmax(0, 1fr); }
  .zusammen { position: static; }
  .leiste { display: flex; position: fixed; left: 0; right: 0; bottom: 0; z-index: 30; align-items: center; justify-content: space-between; gap: 12px; padding: 10px 16px calc(10px + env(safe-area-inset-bottom)); background: var(--karte); border-top: 1px solid var(--text); }
  .leiste p { font-size: .8125rem; line-height: 1.25; }
  .leiste p b { display: block; font: 500 1.25rem/1.1 var(--h); }
  .leiste a { background: var(--dunkel); color: #fff; text-decoration: none; border-radius: 999px; padding: 12px 18px; font-weight: 600; }
  body { padding-bottom: 76px; }
  .pult { bottom: 86px; }
}
@media (max-width: 600px) {
  .schritt { grid-template-columns: minmax(0, 1fr); }
  .schritt .nr { grid-row: auto; font-size: 2rem; }
  .schritt .inhalt { grid-column: 1; }
  .laeden, .zwei { grid-template-columns: minmax(0, 1fr); }
  .ware { grid-template-columns: minmax(0, 1fr) auto; }
  .ware .preis { grid-column: 2; grid-row: 1; }
  .stepper { grid-column: 2; grid-row: 2 / span 2; }
  .chips .wahl .k { min-width: 0; padding: 9px 13px; }
  .fuss .reihe { grid-template-columns: minmax(0, 1fr); }
}
@media (max-width: 360px) { .stepper { grid-template-columns: 36px 2.6em 36px; } .marke b { font-size: 1.5rem; } }
@media (hover: none) { .wahl:hover .k { border-color: var(--linie); } }
:root.reduziert * { transition-duration: .01ms !important; animation-duration: .01ms !important; }
@media (forced-colors: active) {
  button, [role=button], [role=tab], [role=switch], input, select, textarea { border: 1px solid ButtonText !important; }
  .wahl input:checked + .k { outline: 3px solid Highlight; }
  .wahl .k { border: 1px solid CanvasText; }
}
@media print { .kopf, .pult, .leiste, .zeiger, .einstieg, .fuss, .knoepfe { display: none !important; } .bestaetigung { border: 0; padding: 0; } }
</style>
</head>
<body>
<header class="kopf">
  <div class="huelle">
    <a class="marke" href="#" data-marke><b data-ref="marke">Ofenstein</b><span data-ref="marke-unter">Bäckerei seit 1961 · Deggendorf</span></a>
    <button class="menueknopf" type="button" aria-expanded="false" aria-controls="hauptmenue">Menü</button>
    <nav class="nav" id="hauptmenue" aria-label="Hauptmenü"><ul data-ref="nav"><li><a href="#">Sortiment</a></li><li><a href="#">Läden</a></li><li><a href="#" aria-current="page">Vorbestellen</a></li><li><a href="#">Über uns</a></li></ul></nav>
    <span class="offen" data-ref="offen"><i aria-hidden="true"></i>Stadtplatz heute bis 18 Uhr</span>
  </div>
</header>

<main id="inhalt">
  <div class="huelle">
    <section class="einstieg" aria-labelledby="h1">
      <div>
        <p class="rubrik">Vorbestellen und abholen</p>
        <h1 id="h1" data-ref="h1">Heute bestellen, <em>morgen früh</em> abholen.</h1>
        <p class="lead" data-ref="lead">Bis 18 Uhr bestellen, am nächsten Morgen liegt alles bereit. Bezahlt wird im Laden, bar oder mit Karte.</p>
        <ul class="regeln" data-ref="regeln"></ul>
      </div>
      <div data-ref="bildplatz"></div>
    </section>

    <div class="bestellen" data-bestellen>
      <form class="formular" data-form novalidate aria-labelledby="h1">
        <div data-fehlerliste></div>
        <fieldset class="schritt" aria-labelledby="s1">
          <legend><span class="nr" aria-hidden="true">1</span></legend>
          <div><h2 id="s1">Wo und wann abholen?</h2><p class="unter" data-ref="s1-unter">Bestellschluss ist 18 Uhr am Vortag. Abholung zwischen Öffnung und 12 Uhr.</p></div>
          <div class="inhalt">
            <fieldset class="gruppe"><legend>Laden</legend><div class="laeden" data-laeden></div></fieldset>
            <fieldset class="gruppe" id="g-tag"><legend>Abholtag</legend><div class="chips" data-tage></div><p class="grund" data-tag-grund></p></fieldset>
            <fieldset class="gruppe" id="g-fenster"><legend>Zeitfenster</legend><div class="chips fenster" data-fenster></div><p class="grund" data-fenster-grund></p></fieldset>
          </div>
        </fieldset>
        <fieldset class="schritt" aria-labelledby="s2">
          <legend><span class="nr" aria-hidden="true">2</span></legend>
          <div><h2 id="s2" data-ref="s2">Was dürfen wir einpacken?</h2><p class="unter" data-ref="s2-unter">Allergene stehen bei jedem Produkt. Größere Mengen bitte telefonisch.</p></div>
          <div class="inhalt"><div class="waren" data-waren id="g-ware"></div></div>
        </fieldset>
        <fieldset class="schritt" aria-labelledby="s3">
          <legend><span class="nr" aria-hidden="true">3</span></legend>
          <div><h2 id="s3">Auf welchen Namen?</h2><p class="unter">Wir rufen nur an, wenn etwas nicht klappt.</p></div>
          <div class="inhalt">
            <div class="zwei">
              <div class="feld"><label for="f-name">Name</label><input id="f-name" autocomplete="name" required aria-describedby="e-name"><span class="fehler" id="e-name" hidden>Bitte einen Namen angeben, damit wir die Tüte beschriften können.</span></div>
              <div class="feld"><label for="f-tel">Telefon</label><input id="f-tel" type="tel" inputmode="tel" autocomplete="tel" required aria-describedby="e-tel"><span class="fehler" id="e-tel" hidden>Bitte eine Telefonnummer angeben, z. B. 0991 123456.</span></div>
            </div>
            <div class="feld"><label for="f-mail">E-Mail <small>(freiwillig, für die Bestätigung)</small></label><input id="f-mail" type="email" autocomplete="email"></div>
            <div class="feld"><label for="f-notiz">Anmerkung <small>(freiwillig)</small></label><textarea id="f-notiz" placeholder="z. B. Semmeln bitte gut gebacken"></textarea></div>
          </div>
        </fieldset>
      </form>

      <aside class="zusammen" aria-labelledby="z-titel" id="zusammenfassung">
        <h2 id="z-titel">Ihre Vorbestellung</h2>
        <p class="abholung" data-z-abholung></p>
        <ul class="posten" data-posten></ul>
        <p class="summe"><span>Summe<small data-z-hinweis>Endpreis inkl. MwSt., zahlbar bei Abholung</small></span><output aria-live="polite" data-summe class="ziffern">0,00 €</output></p>
        <button type="button" class="abschicken" data-abschicken>Verbindlich vorbestellen</button>
        <p class="klein" data-z-klein>Ändern oder stornieren bis 18 Uhr am Vortag: 0991 000 00-0.</p>
      </aside>
      <section class="bestaetigung" tabindex="-1" data-ok hidden></section>
    </div>
  </div>
</main>

<div class="leiste" data-leiste><p><b data-leiste-summe>0,00 €</b><span data-leiste-text>noch nichts gewählt</span></p><a href="#zusammenfassung">Ansehen</a></div>

<footer class="fuss">
  <div class="huelle">
    <div class="reihe">
      <div><b class="name" data-ref="fuss-name">Ofenstein</b><p data-ref="fuss-adr">Bäckerei Ofenstein · Am Stadtplatz 9 · 94469 Deggendorf</p></div>
      <div><h2>Läden</h2><ul data-ref="fuss-laeden"></ul></div>
      <div><h2>Rechtliches</h2><ul><li><a href="#">Impressum</a></li><li><a href="#">Datenschutz</a></li><li><a href="#">Allergene und Zusatzstoffe</a></li></ul></div>
    </div>
    <p class="unten">Demo: Betriebe, Adressen, Preise und Telefonnummern sind erfunden.</p>
  </div>
</footer>

<div class="pult" role="group" aria-label="Steuerung der Demo">
  <span class="m">Variante</span>
  <div class="reihe"><button type="button" data-v="backstube" aria-pressed="true">Backstube</button><button type="button" data-v="theke" aria-pressed="false">Theke</button></div>
  <p class="werte" data-werte></p>
</div>
<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="#141413" 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), $$ = (s, r = document) => [...r.querySelectorAll(s)];
  const ref = n => $(`[data-ref="${n}"]`);
  const eur = new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' });
  const tagKurz = new Intl.DateTimeFormat('de-DE', { weekday: 'short', day: 'numeric', month: 'numeric' });
  const tagLang = new Intl.DateTimeFormat('de-DE', { weekday: 'long', day: 'numeric', month: 'long' });
  const kg = g => (g / 1000).toLocaleString('de-DE', { maximumFractionDigits: 2 });
  const SCHLUSS = 18; // Bestellschluss 18 Uhr am Vortag

  // Öffnungszeiten je Laden: [Tag 0 = Sonntag … 6] → [von, bis] in Minuten oder null
  const h = (a, b) => [a * 60, b * 60];
  const BETRIEBE = {
    backstube: {
      marke: 'Ofenstein', unter: 'Bäckerei seit 1961 · Deggendorf', offen: 'Stadtplatz heute bis 18 Uhr', fussAdr: 'Bäckerei Ofenstein · Am Stadtplatz 9 · 94469 Deggendorf',
      h1: 'Heute bestellen, <em>morgen früh</em> abholen.', lead: 'Bis 18 Uhr bestellen, am nächsten Morgen liegt alles bereit. Bezahlt wird im Laden, bar oder mit Karte.',
      regeln: ['<b>18 Uhr</b> Bestellschluss am Vortag', 'Torten <b>2 Tage</b> vorher', '<b>Keine</b> Online-Zahlung'],
      bild: '<figure><img src="bilder/foto-hero-mit-leiste-backstube.webp" width="1600" height="1000" alt="Sauerteiglaibe auf Leinen und Brett, dahinter ein Brotregal im Morgenlicht" data-laed><figcaption>Backstube Ofenstein, 4:30 Uhr (Platzhalter-Bild)</figcaption></figure>',
      s2: 'Was dürfen wir einpacken?', s2unter: 'Allergene stehen bei jedem Produkt. Größere Mengen bitte telefonisch.', hinweis: 'Endpreis inkl. MwSt., zahlbar bei Abholung',
      laeden: [
        { id: 'stadtplatz', name: 'Am Stadtplatz', adr: 'Am Stadtplatz 9', zeiten: [null, h(6.5, 18), h(6.5, 18), h(6.5, 18), h(6.5, 18), h(6.5, 18), h(6.5, 13)], text: 'Mo–Fr 6:30–18, Sa 6:30–13 Uhr' },
        { id: 'hafen', name: 'Backstube Hafenstraße', adr: 'Hafenstraße 31, mit Café', zeiten: [h(7.5, 11), null, h(6, 12), h(6, 12), h(6, 12), h(6, 12), h(6, 12)], text: 'Di–Sa 6–12, So 7:30–11 Uhr' }
      ],
      waren: [
        { gruppe: 'Brot', hinweis: 'Stückpreis, auf Wunsch geschnitten', liste: [
          { id: 'bauern', name: 'Bauernlaib', info: '1.000 g, Roggenmischbrot', allergene: 'Roggen, Weizen', preis: 5.2, max: 6, schnitt: true },
          { id: 'dinkel', name: 'Dinkel-Vollkorn', info: '750 g, mit Saaten', allergene: 'Dinkel, Sesam', preis: 5.8, max: 6, schnitt: true },
          { id: 'waldler', name: 'Waldler', info: '1.250 g, Roggen-Sauerteig, 24 Stunden geführt', allergene: 'Roggen', preis: 6.4, max: 4, schnitt: true } ] },
        { gruppe: 'Kleingebäck', hinweis: 'höchstens 30 je Sorte', liste: [
          { id: 'semmel', name: 'Semmel', info: 'Weizen, handgeschlagen', allergene: 'Weizen', preis: .55, max: 30 },
          { id: 'breze', name: 'Laugenbreze', info: '', allergene: 'Weizen', preis: .85, max: 30 },
          { id: 'kuerbis', name: 'Kürbiskernsemmel', info: '', allergene: 'Weizen, Roggen', preis: .9, max: 30 } ] },
        { gruppe: 'Torten', hinweis: '2 Tage Vorlauf, 12 Stück', liste: [
          { id: 'kaesesahne', name: 'Käsesahne', info: 'ganze Torte, 26 cm', allergene: 'Weizen, Milch, Ei', preis: 34, max: 2, vorlauf: 2 },
          { id: 'zwetschge', name: 'Zwetschgendatschi', info: 'halbes Blech, 12 Stück, im Winter mit Apfel', allergene: 'Weizen, Milch, Ei', preis: 26, max: 2, vorlauf: 2 } ] }
      ],
      werte: 'öffnen 0.16,1,0.3,1 · schließen 0.7,0,0.84,0 · UI 200 ms · Ziffer 300 ms, 0.6em · Bestätigung 400 ms, 16 px · Bild 700 ms · Fenster 30 Min., Kapazität 8 · Schluss 18 Uhr Vortag · Torten 2 Tage'
    },
    theke: {
      marke: 'Brandl', unter: 'Hofmetzgerei · Deggendorf-Natternberg', offen: 'Laden heute bis 18 Uhr', fussAdr: 'Hofmetzgerei Brandl · Natternberger Straße 2 · 94469 Deggendorf',
      h1: 'Braten und Platten <em>vorbestellen</em>.', lead: 'Sie bestellen das Gewicht, wir schneiden frisch zu. Der Endpreis steht nach dem Wiegen fest, meist innerhalb von 10 % um den Circa-Preis.',
      regeln: ['<b>18 Uhr</b> Bestellschluss am Vortag', 'Grundpreis <b>je kg</b> bei jeder Ware', '<b>Keine</b> Online-Zahlung'],
      bild: '<div class="tafel" aria-hidden="true"><b>Heute an der Theke</b><ul><li><span>Schweinebraten, Schulter</span><span>14,90 €/kg</span></li><li><span>Rinderbraten, Schaufel</span><span>24,90 €/kg</span></li><li><span>Leberkäse am Stück</span><span>13,90 €/kg</span></li></ul></div>',
      s2: 'Wie viel darf es sein?', s2unter: 'Gewicht in 250-g-Schritten. Grundpreis je Kilogramm steht am Preis.', hinweis: 'ca.-Preis inkl. MwSt., Endpreis nach Wiegen',
      laeden: [
        { id: 'hof', name: 'Hofladen Natternberg', adr: 'Natternberger Straße 2', zeiten: [null, null, h(7, 18), h(7, 18), h(7, 18), h(7, 18), h(7, 12)], text: 'Di–Fr 7–18, Sa 7–12 Uhr' },
        { id: 'markt', name: 'Wochenmarkt Stadtplatz', adr: 'Stand am Brunnen', zeiten: [null, null, null, h(7, 12), null, null, h(7, 12)], text: 'Mi und Sa 7–12 Uhr' }
      ],
      waren: [
        { gruppe: 'Braten', hinweis: 'nach Gewicht, frisch zugeschnitten', liste: [
          { id: 'schwein', name: 'Schweinebraten', info: 'Schulter mit Schwarte', allergene: 'keine', kg: 14.9, schritt: 250, max: 4000 },
          { id: 'rind', name: 'Rinderbraten', info: 'Schaufelstück, 21 Tage gereift', allergene: 'keine', kg: 24.9, schritt: 250, max: 3000 },
          { id: 'leberkaes', name: 'Leberkäse am Stück', info: 'zum Selberbacken', allergene: 'Senf', kg: 13.9, schritt: 250, max: 3000 } ] },
        { gruppe: 'Stückware', hinweis: 'Stückpreis', liste: [
          { id: 'weisswurst', name: 'Weißwürste', info: 'je ca. 70 g', allergene: 'Senf, Sellerie', preis: 1.1, max: 30 },
          { id: 'platte', name: 'Aufschnittplatte', info: 'für 6 Personen, 2 Tage Vorlauf', allergene: 'Senf, Sellerie, Milch', preis: 42, max: 3, vorlauf: 2 } ] }
      ],
      werte: 'wie Backstube · Gewicht step 250 g, max. 3–4 kg · Grundpreis je kg neben dem Preis · Summe „ca.“ · Endpreis nach Wiegen ±10 %'
    }
  };
  let v = 'backstube';
  const B = () => BETRIEBE[v];
  const auswahl = { laden: null, tag: null, fenster: null };
  const menge = {}, schnitt = {};
  let fertig = null;

  // Tage: die nächsten 7 Tage ab morgen
  const jetzt = new Date();
  const tage = Array.from({ length: 7 }, (_, i) => { const d = new Date(jetzt); d.setHours(12, 0, 0, 0); d.setDate(d.getDate() + i + 1); return d; });
  const isoTag = d => d.toISOString().slice(0, 10);
  const nachSchluss = jetzt.getHours() >= SCHLUSS;
  const vorlaufOk = (tagIdx, vorlauf) => tagIdx + 1 >= (vorlauf || 1) + (nachSchluss ? 1 : 0);

  function hash(s) { let x = 7; for (const c of s) x = (x * 31 + c.charCodeAt(0)) % 9973; return x; }
  function fensterFuer(laden, d) {
    const z = laden.zeiten[d.getDay()]; if (!z) return [];
    const liste = [];
    for (let m = z[0]; m + 30 <= Math.min(z[1], 12 * 60); m += 30) {
      const frei = Math.max(0, 8 - (hash(laden.id + isoTag(d) + m) % 10));
      liste.push({ m, frei, text: `${String(Math.floor(m / 60)).padStart(2, '0')}:${String(m % 60).padStart(2, '0')}` });
    }
    return liste;
  }
  const bis = m => { const e = m + 30; return `${Math.floor(e / 60)}:${String(e % 60).padStart(2, '0')}`; };

  function abholungBauen() {
    const b = B();
    if (!b.laeden.find(l => l.id === auswahl.laden)) auswahl.laden = b.laeden[0].id;
    $('[data-laeden]').innerHTML = b.laeden.map(l => `<label class="wahl"><input type="radio" name="laden" value="${l.id}" ${l.id === auswahl.laden ? 'checked' : ''}><span class="k"><b>${l.name}</b><small>${l.adr}</small><small>${l.text}</small></span></label>`).join('');
    const laden = b.laeden.find(l => l.id === auswahl.laden);
    let gruende = [];
    $('[data-tage]').innerHTML = tage.map((d, i) => {
      const zu = !laden.zeiten[d.getDay()], schluss = i === 0 && nachSchluss;
      const grund = zu ? 'Ruhetag' : schluss ? 'Bestellschluss vorbei' : '';
      if (grund && !gruende.includes(grund)) gruende.push(grund);
      return `<label class="wahl"><input type="radio" name="tag" value="${i}" ${grund ? 'disabled' : ''} ${String(auswahl.tag) === String(i) && !grund ? 'checked' : ''} aria-describedby="tag-grund"><span class="k">${tagKurz.format(d)}<small>${grund || (i === 0 ? 'morgen' : '&nbsp;')}</small></span></label>`;
    }).join('');
    if (auswahl.tag !== null && (!laden.zeiten[tage[auswahl.tag].getDay()] || (auswahl.tag === 0 && nachSchluss))) auswahl.tag = null;
    $('[data-tag-grund]').id = 'tag-grund';
    $('[data-tag-grund]').textContent = gruende.length ? `Nicht wählbar: ${gruende.join(', ')}.` : '';
    // Zeitfenster
    const fe = $('[data-fenster]'), fg = $('[data-fenster-grund]');
    if (auswahl.tag === null) { fe.innerHTML = ''; fg.textContent = 'Bitte zuerst einen Tag wählen.'; auswahl.fenster = null; }
    else {
      const liste = fensterFuer(laden, tage[auswahl.tag]);
      if (!liste.find(f => f.m === auswahl.fenster && f.frei > 0)) auswahl.fenster = null;
      fe.innerHTML = liste.map(f => `<label class="wahl"><input type="radio" name="fenster" value="${f.m}" ${f.frei ? '' : 'disabled'} ${f.m === auswahl.fenster ? 'checked' : ''}><span class="k">${f.text}–${bis(f.m)}<small>${f.frei ? `noch ${f.frei} frei` : 'voll'}</small></span></label>`).join('');
      fg.textContent = liste.some(f => !f.frei) ? 'Volle Fenster sind ausgegraut. Andere Zeiten gern telefonisch.' : '';
    }
    warenSperren(); zusammen();
  }

  function warenBauen() {
    const b = B();
    $('[data-waren]').innerHTML = b.waren.map(g => `<section aria-labelledby="w-${g.gruppe}"><h3 id="w-${g.gruppe}">${g.gruppe}<small>${g.hinweis}</small></h3><ul>${g.liste.map(w => {
      const gewicht = !!w.kg, einheit = gewicht ? 'g' : 'Stück';
      const preis = gewicht ? `${eur.format(w.kg * w.schritt / 1000)}<small>je ${w.schritt} g · Grundpreis ${eur.format(w.kg)} / kg</small>` : `${eur.format(w.preis)}<small>je Stück</small>`;
      return `<li class="ware" data-ware="${w.id}"><b id="n-${w.id}">${w.name}</b><span class="preis">${preis}</span>
        <span class="info">${w.info ? w.info + ' · ' : ''}enthält: <em>${w.allergene}</em></span>
        <span class="stepper" role="group" aria-labelledby="n-${w.id}">
          <button type="button" data-minus="${w.id}" aria-label="${w.name}: ${gewicht ? w.schritt + ' g' : 'eins'} weniger">−</button>
          <span class="zahl"><input type="number" inputmode="numeric" min="0" max="${w.max}" step="${gewicht ? w.schritt : 1}" value="${menge[w.id] || 0}" data-menge="${w.id}" aria-label="${w.name}, Menge in ${einheit}"></span>
          <button type="button" data-plus="${w.id}" aria-label="${w.name}: ${gewicht ? w.schritt + ' g' : 'eins'} mehr">+</button></span>
        ${w.schnitt ? `<label class="extra"><input type="checkbox" data-schnitt="${w.id}" ${schnitt[w.id] ? 'checked' : ''}> geschnitten</label>` : ''}
        <span class="sperre" data-sperre="${w.id}" hidden></span></li>`; }).join('')}</ul></section>`).join('');
    warenSperren();
  }
  const alleWaren = () => B().waren.flatMap(g => g.liste);

  function warenSperren() {
    alleWaren().forEach(w => {
      const li = $(`[data-ware="${w.id}"]`); if (!li) return;
      const ok = auswahl.tag === null || !w.vorlauf || vorlaufOk(auswahl.tag, w.vorlauf);
      const sp = $(`[data-sperre="${w.id}"]`);
      if (!ok) {
        const erster = tage.findIndex((d, i) => vorlaufOk(i, w.vorlauf));
        sp.hidden = false; sp.textContent = `Braucht ${w.vorlauf} Tage Vorlauf: frühestens ${tagLang.format(tage[erster])}.`;
        li.setAttribute('data-gesperrt', ''); menge[w.id] = 0;
      } else { sp.hidden = true; li.removeAttribute('data-gesperrt'); }
      const inp = $(`[data-menge="${w.id}"]`); inp.value = menge[w.id] || 0; inp.disabled = !ok;
      $(`[data-minus="${w.id}"]`).disabled = !ok || !(menge[w.id] > 0);
      $(`[data-plus="${w.id}"]`).disabled = !ok || (menge[w.id] || 0) >= w.max;
    });
  }

  function positionen() {
    return alleWaren().filter(w => menge[w.id] > 0).map(w => {
      const betrag = w.kg ? w.kg * menge[w.id] / 1000 : w.preis * menge[w.id];
      return { w, text: w.kg ? `${w.name}, ca. ${kg(menge[w.id])} kg` : `${w.name}${schnitt[w.id] ? ', geschnitten' : ''}`, anzahl: w.kg ? '' : menge[w.id] + ' ×', betrag };
    });
  }
  function zusammen() {
    const b = B(), laden = b.laeden.find(l => l.id === auswahl.laden), pos = positionen();
    const summe = pos.reduce((s, p) => s + p.betrag, 0), circa = pos.some(p => p.w.kg);
    $('[data-z-abholung]').innerHTML = auswahl.tag !== null && auswahl.fenster !== null ? `<b>${tagLang.format(tage[auswahl.tag])}, ${fensterText()}</b>${laden.name}, ${laden.adr}` : `<b>Abholung noch offen</b>${laden.name}: Tag und Zeitfenster wählen`;
    $('[data-posten]').innerHTML = pos.length ? pos.map(p => `<li><span>${p.anzahl}</span><span>${p.text}</span><span>${circa && p.w.kg ? 'ca. ' : ''}${eur.format(p.betrag)}</span></li>`).join('') : '<li class="leer">Noch nichts gewählt.</li>';
    $('[data-summe]').textContent = (circa ? 'ca. ' : '') + eur.format(summe);
    $('[data-z-hinweis]').textContent = circa ? 'ca.-Preis inkl. MwSt., Endpreis nach Wiegen' : b.hinweis;
    $('[data-leiste-summe]').textContent = (circa ? 'ca. ' : '') + eur.format(summe);
    $('[data-leiste-text]').textContent = pos.length ? `${pos.length} ${pos.length === 1 ? 'Position' : 'Positionen'}${auswahl.fenster !== null ? ' · ' + tagKurz.format(tage[auswahl.tag]) + ' ' + fensterText() : ''}` : 'noch nichts gewählt';
  }
  const fensterText = () => { const m = auswahl.fenster; return `${Math.floor(m / 60)}:${String(m % 60).padStart(2, '0')}–${bis(m)} Uhr`; };

  function aendern(id, richtung) {
    const w = alleWaren().find(x => x.id === id), s = w.kg ? w.schritt : 1;
    const alt = menge[id] || 0, neu = Math.max(0, Math.min(w.max, alt + richtung * s));
    if (neu === alt) return;
    menge[id] = neu;
    const inp = $(`[data-menge="${id}"]`); inp.value = neu;
    inp.removeAttribute('data-rollt'); void inp.offsetWidth; inp.setAttribute('data-rollt', richtung > 0 ? 'hoch' : 'runter');
    warenSperren(); zusammen(); fehlerWeg('ware');
  }

  // Ereignisse
  const form = $('[data-form]');
  form.addEventListener('change', e => {
    const t = e.target;
    if (t.name === 'laden') { auswahl.laden = t.value; abholungBauen(); }
    if (t.name === 'tag') { auswahl.tag = +t.value; abholungBauen(); fehlerWeg('tag'); }
    if (t.name === 'fenster') { auswahl.fenster = +t.value; zusammen(); fehlerWeg('fenster'); }
    if (t.dataset.schnitt) { schnitt[t.dataset.schnitt] = t.checked; zusammen(); }
    if (t.dataset.menge) { const w = alleWaren().find(x => x.id === t.dataset.menge), s = w.kg ? w.schritt : 1; let n = Math.round((parseFloat(t.value) || 0) / s) * s; n = Math.max(0, Math.min(w.max, n)); menge[w.id] = n; t.value = n; warenSperren(); zusammen(); }
  });
  form.addEventListener('click', e => { const p = e.target.closest('[data-plus]'), m = e.target.closest('[data-minus]'); if (p) aendern(p.dataset.plus, 1); if (m) aendern(m.dataset.minus, -1); });
  form.addEventListener('submit', e => { e.preventDefault(); absenden(); });
  $('[data-abschicken]').addEventListener('click', absenden);
  ['f-name', 'f-tel'].forEach(id => $('#' + id).addEventListener('blur', () => { if ($('#' + id).getAttribute('aria-invalid') === 'true') pruefeFeld(id); }));

  function pruefeFeld(id) {
    const i = $('#' + id), ok = id === 'f-tel' ? /[0-9]{5,}/.test(i.value.replace(/[\s\/-]/g, '')) : i.value.trim().length > 1;
    i.setAttribute('aria-invalid', !ok); $('#' + (id === 'f-tel' ? 'e-tel' : 'e-name')).hidden = ok; return ok;
  }
  function fehlerWeg(was) { const l = $(`[data-fehlerliste] [data-f="${was}"]`); if (l) l.parentElement.remove(); if ($('[data-fehlerliste] li') === null) $('[data-fehlerliste]').innerHTML = ''; }
  function absenden() {
    const f = [];
    if (auswahl.tag === null) f.push(['tag', 'Bitte einen Abholtag wählen.', '#g-tag']);
    else if (auswahl.fenster === null) f.push(['fenster', 'Bitte ein Zeitfenster wählen.', '#g-fenster']);
    if (!positionen().length) f.push(['ware', 'Bitte mindestens ein Produkt wählen.', '#g-ware']);
    if (!pruefeFeld('f-name')) f.push(['name', 'Bitte einen Namen angeben.', '#f-name']);
    if (!pruefeFeld('f-tel')) f.push(['tel', 'Bitte eine Telefonnummer angeben.', '#f-tel']);
    const fl = $('[data-fehlerliste]');
    if (f.length) {
      fl.innerHTML = `<div class="fehlerliste" tabindex="-1" role="alert"><h2>${f.length === 1 ? 'Eine Angabe fehlt' : f.length + ' Angaben fehlen'}</h2><ul>${f.map(([k, t, z]) => `<li><a href="${z}" data-f="${k}">${t}</a></li>`).join('')}</ul></div>`;
      const box = fl.firstElementChild; box.scrollIntoView({ block: 'start', behavior: reduziert() || laeuft ? 'auto' : 'smooth' }); box.focus({ preventScroll: true });
      return;
    }
    fl.innerHTML = '';
    const knopf = $('[data-abschicken]'); knopf.setAttribute('aria-busy', 'true'); knopf.textContent = 'Wird gesendet …';
    setTimeout(() => { // Demo: im Einsatz POST an den Betrieb, Antwort mit Bestellnummer
      const b = B(), laden = b.laeden.find(l => l.id === auswahl.laden), pos = positionen(), circa = pos.some(p => p.w.kg);
      fertig = { nr: (v === 'theke' ? 'B' : 'O') + '-' + String(tage[auswahl.tag].getDate()).padStart(2, '0') + String(tage[auswahl.tag].getMonth() + 1).padStart(2, '0') + '-' + (hash($('#f-name').value + Date.now()) % 900 + 100), laden, pos, circa };
      const summe = pos.reduce((s, p) => s + p.betrag, 0);
      const ok = $('[data-ok]'); form.hidden = true; $('#zusammenfassung').hidden = true; ok.hidden = false; ok.setAttribute('aria-labelledby', 'ok-titel');
      ok.innerHTML = `
        <div><p class="rubrik">Vorbestellung eingegangen</p><h2 id="ok-titel">Ist notiert, <em>${$('#f-name').value.trim().split(' ')[0].replace(/</g, '')}</em>.</h2>
          <p class="nr">Nr. ${fertig.nr}</p>
          <p style="margin-top:16px">Sagen Sie im Laden einfach Ihren Namen. Bezahlt wird bei Abholung, bar oder mit Karte.${circa ? ' Der Endpreis steht nach dem Wiegen fest.' : ''}</p>
          <div class="knoepfe"><button type="button" data-ok-ics>In den Kalender</button><button type="button" data-ok-druck>Drucken</button><button type="button" data-ok-neu>Neue Vorbestellung</button></div>
          <p class="klein" style="margin-top:14px" role="status" data-ok-status></p></div>
        <dl><dt>Abholung</dt><dd>${tagLang.format(tage[auswahl.tag])}, ${fensterText()}</dd><dt>Laden</dt><dd>${laden.name}, ${laden.adr}</dd>
          ${pos.map(p => `<dt>${p.anzahl || 'ca.'}</dt><dd>${p.text.replace(', ca. ', ', ')} · ${eur.format(p.betrag)}</dd>`).join('')}
          <dt>Summe</dt><dd>${circa ? 'ca. ' : ''}${eur.format(summe)}</dd><dt>Ändern</dt><dd>bis 18 Uhr am Vortag: 0991 000 00-0</dd></dl>`;
      $('[data-ok]').focus({ preventScroll: true });
      $('[data-ok]').scrollIntoView({ block: 'start', behavior: reduziert() || laeuft ? 'auto' : 'smooth' });
      $('[data-leiste]').hidden = true;
    }, 450);
  }

  // Bestätigung: Kalender, Drucken, neu
  document.addEventListener('click', e => {
    if (e.target.closest('[data-ok-ics]')) {
      const d = tage[auswahl.tag], m = auswahl.fenster, p2 = n => String(n).padStart(2, '0');
      const tag = `${d.getFullYear()}${p2(d.getMonth() + 1)}${p2(d.getDate())}`;
      const ics = ['BEGIN:VCALENDAR', 'VERSION:2.0', 'PRODID:-//Vorbestellung//DE', 'BEGIN:VEVENT', `UID:${fertig.nr}@ofenstein.example`, `DTSTAMP:${new Date().toISOString().replace(/[-:]/g, '').replace(/\.\d{3}/, '')}`,
        `DTSTART;TZID=Europe/Berlin:${tag}T${p2(Math.floor(m / 60))}${p2(m % 60)}00`, `DTEND;TZID=Europe/Berlin:${tag}T${p2(Math.floor((m + 30) / 60))}${p2((m + 30) % 60)}00`,
        `SUMMARY:Abholen: Vorbestellung ${fertig.nr}`, `LOCATION:${fertig.laden.name}\\, ${fertig.laden.adr}`, 'END:VEVENT', 'END:VCALENDAR'].join('\r\n') + '\r\n';
      if (!laeuft) { const u = URL.createObjectURL(new Blob([ics], { type: 'text/calendar' })); const a = document.createElement('a'); a.href = u; a.download = 'abholung-' + fertig.nr + '.ics'; a.click(); setTimeout(() => URL.revokeObjectURL(u), 2000); }
      $('[data-ok-status]').textContent = 'Kalenderdatei erstellt.';
    }
    if (e.target.closest('[data-ok-druck]')) { if (!laeuft) print(); }
    if (e.target.closest('[data-ok-neu]')) neuAufbauen(true);
  });

  function neuAufbauen(fokus) {
    fertig = null; $('[data-ok]').hidden = true; $('[data-ok]').innerHTML = ''; $('[data-ok]').removeAttribute('aria-labelledby'); form.hidden = false; $('#zusammenfassung').hidden = false; $('[data-leiste]').hidden = false;
    Object.keys(menge).forEach(k => delete menge[k]); Object.keys(schnitt).forEach(k => delete schnitt[k]);
    auswahl.tag = null; auswahl.fenster = null;
    ['f-name', 'f-tel', 'f-mail', 'f-notiz'].forEach(id => { const i = $('#' + id); i.value = ''; i.removeAttribute('aria-invalid'); });
    $('#e-name').hidden = true; $('#e-tel').hidden = true; $('[data-fehlerliste]').innerHTML = '';
    const k = $('[data-abschicken]'); k.removeAttribute('aria-busy'); k.textContent = 'Verbindlich vorbestellen';
    abholungBauen(); warenBauen(); zusammen();
    if (fokus) { $('#s1').scrollIntoView({ behavior: reduziert() ? 'auto' : 'smooth' }); $('[data-laeden] input').focus({ preventScroll: true }); }
  }

  // Menü
  const mk = $('.menueknopf'), nv = $('#hauptmenue');
  mk.addEventListener('click', () => { const auf = !nv.hasAttribute('data-offen'); nv.toggleAttribute('data-offen', auf); mk.setAttribute('aria-expanded', auf); mk.textContent = auf ? 'Schließen' : 'Menü'; });
  addEventListener('keydown', e => { if (e.key === 'Escape' && nv.hasAttribute('data-offen')) { mk.click(); mk.focus(); } });

  // Variante: anderer Betrieb, gleiches Gerüst
  function variante(neu, url = true) {
    v = BETRIEBE[neu] ? neu : 'backstube'; html.dataset.v = v;
    const b = B();
    ref('marke').textContent = b.marke; ref('marke-unter').textContent = b.unter; ref('offen').lastChild.textContent = b.offen;
    ref('h1').innerHTML = b.h1; ref('lead').textContent = b.lead; ref('regeln').innerHTML = b.regeln.map(r => `<li>${r}</li>`).join('');
    ref('bildplatz').innerHTML = b.bild;
    ref('fuss-name').textContent = b.marke; ref('fuss-adr').textContent = b.fussAdr;
    ref('fuss-laeden').innerHTML = b.laeden.map(l => `<li>${l.name}: ${l.text}</li>`).join('');
    $('[data-ref="h1"]').closest('section').querySelectorAll('img[data-laed]').forEach(img => { const ok = () => img.removeAttribute('data-laed'); img.complete ? requestAnimationFrame(ok) : img.addEventListener('load', ok, { once: true }); setTimeout(ok, 1500); });
    $$('.pult [data-v]').forEach(x => x.setAttribute('aria-pressed', x.dataset.v === v));
    $('[data-werte]').textContent = b.werte;
    auswahl.laden = null; neuAufbauen(false);
    ref('s2').textContent = b.s2; ref('s2-unter').textContent = b.s2unter;
    abholungBauen(); warenBauen(); zusammen();
    if (url) { const q = new URLSearchParams(location.search); q.set('v', v); history.replaceState(null, '', location.pathname + '?' + q.toString()); }
  }
  $$('.pult [data-v]').forEach(x => x.addEventListener('click', () => { pause(); variante(x.dataset.v); }));

  // Vorführung
  const zeiger = $('[data-zeiger]');
  let laeuft = false, timer = [];
  const warte = ms => new Promise(r => timer.push(setTimeout(r, ms)));
  async function gehe(el, klick = true) {
    if (!el || !laeuft) return;
    el.scrollIntoView({ block: 'center' });
    await warte(60);
    const r = el.getBoundingClientRect();
    zeiger.style.transition = reduziert() ? 'none' : 'transform 500ms cubic-bezier(0.16, 1, 0.3, 1), opacity 200ms';
    zeiger.style.transform = `translate(${r.left + Math.min(r.width / 2, 60)}px, ${r.top + r.height / 2}px)`;
    await warte(reduziert() ? 100 : 540);
    if (klick && laeuft) { zeiger.classList.add('drueckt'); await warte(100); zeiger.classList.remove('drueckt'); el.click(); }
  }
  async function tippe(el, text) { if (!laeuft) return; await gehe(el, false); el.value = ''; for (const c of text) { if (!laeuft) return; el.value += c; await warte(reduziert() ? 0 : 45); } el.dispatchEvent(new Event('input')); }
  async function runde() {
    if (!laeuft) return;
    neuAufbauen(false); scrollTo(0, 0);
    zeiger.style.transform = `translate(${innerWidth * .5}px, ${innerHeight * .7}px)`;
    await warte(900);
    const tag = $$('[data-tage] input:not(:disabled)')[1] || $('[data-tage] input:not(:disabled)');
    await gehe(tag?.nextElementSibling); await warte(500);
    await gehe($('[data-fenster] input:not(:disabled)')?.nextElementSibling); await warte(500);
    const plus = $$('[data-plus]:not(:disabled)');
    if (v === 'backstube') { for (const id of ['semmel', 'semmel', 'semmel', 'semmel', 'breze', 'breze', 'bauern']) { await gehe($(`[data-plus="${id}"]`)); await warte(160); } }
    else { for (const id of ['schwein', 'schwein', 'schwein', 'schwein', 'schwein', 'schwein', 'weisswurst', 'weisswurst', 'weisswurst', 'weisswurst']) { await gehe($(`[data-plus="${id}"]`)); await warte(160); } }
    await warte(500);
    await tippe($('#f-name'), 'Ida Kerscher'); await tippe($('#f-tel'), '0991 123456');
    await warte(400);
    await gehe($('[data-abschicken]')); await warte(3200);
    if (laeuft) timer.push(setTimeout(runde, 300));
  }
  function neustart() { pause(); neuAufbauen(false); scrollTo(0, 0); }
  function vorfuehren() { pause(); html.classList.add('vorfuehrung'); laeuft = true; runde(); }
  function pause() { laeuft = false; timer.forEach(clearTimeout); timer = []; html.classList.remove('vorfuehrung'); }
  function weiter() { if (!laeuft) vorfuehren(); }

  variante(new URLSearchParams(location.search).get('v') || 'backstube', false);

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