Zum Inhalt
← Sektionen & Funktionen

Wein-Steckbrief mit Pflichtangaben

Weinseite, deren Pflichtangaben wie ein Etikett gesetzt sind: Raster in Mono, Klappe mit Zutaten und Nährwerten je nach Herstellungsdatum, Kaufbox mit Grundpreis für Flasche, Magnum und Karton, ausgetrunkene Jahrgänge mit Nachfolger und Jugendschutz am Knopf.

Weingut, Winzerhof, Genossenschaft: Weinseite mit Jahrgangswahl, Flasche, Magnum und Karton, Lage als Link.

Die Weinseite im Shop zeigt alles, was auf dem Etikett steht, als eigenes Etikett: Kopf mit Lage, Rebsorte, Jahrgang und Qualitätsstufe, daneben die Kaufbox mit Gesamtpreis inkl. MwSt., Grundpreis je Liter, Füllmenge und Versand. Darunter ein Raster in Mono mit Alkohol in % vol, „enthält Sulfite“, Herkunft, Anbaugebiet und Erzeugerabfüllung mit vollständiger Anschrift; freiwillige Angaben wie Restzucker und Säure sind als freiwillig markiert. Eine Klappe „Zutaten und Nährwerte“ hält die vollständige Tabelle je 100 ml und das Zutatenverzeichnis mit hervorgehobenen Allergenen direkt auf der Seite. Der Jahrgang steuert die Pflicht: Wurde der Wein vor dem 08.12.2023 hergestellt, steht ein Hinweis statt einer leeren Tabelle. Wechselt das Gebinde (Flasche, Magnum, Karton), rechnet der Grundpreis mit. Ein ausgetrunkener Jahrgang behält seinen Steckbrief, nur die Kaufbox geht aus und nennt den Nachfolger. Sekt und Brände haben eigene Pflichtbegriffe und eigene Altersgrenzen.

Weinkäufer lesen Etiketten. Wenn die Pflichtangaben wie ein Etikett gesetzt sind, wirken sie wie ein Teil des Weins und nicht wie Kleingedrucktes. Die Mono mit Tabellenziffern macht Preise, Grundpreise und Analysewerte vergleichbar. Der Shop erfüllt dabei, was im Fernabsatz vor dem Kauf gelten muss: alle Etikettangaben, bei neueren Weinen Zutaten und Nährwerte, Grundpreis je Liter, Jugendschutz bei der Zustellung. Wer den Grundpreis bei Magnum und Karton sieht, versteht den Preis sofort.

Wann einsetzen

Weingüter, Winzergenossenschaften, Sektkellereien, Brennereien und Hofläden mit Onlineshop; Weinseite, Brand-Seite, Schnellansicht aus der Liste und Warenkorbzeile. Auch ohne Shop (Hofladen ohne Versand), dann ohne Kaufbox, mit denselben Angaben.

Wann nicht

Für Non-Food im Shop (Gläser, Geschenkkisten) gelten andere Pflichten: dort produkt-kaufbox-varianten mit Hersteller- und Sicherheitsangaben nach GPSR. Für Weinlisten in der Gastronomie (Glas und Flasche im Restaurant) passt speisekarte-editorial. Die Werte nie schätzen oder aus einem anderen Jahrgang übernehmen: Ohne Analyse des Jahrgangs bleibt die Tabelle ein markierter Platzhalter. Den QR-Code des E-Labels nicht als Ersatz für die Angaben im Shop einsetzen.

Varianten

  • Wein: Stillwein mit Lage, Jahrgangswahl (nach Stichtag mit Tabelle, vor Stichtag mit Hinweis, ausgetrunken mit Nachfolger), Flasche, Magnum und Karton
  • Sekt: Schaumwein mit Geschmacksangabe (brut usw.), „Hersteller“ statt „Erzeugerabfüllung“, Datum der abgeschlossenen zweiten Gärung entscheidet über Zutaten und Nährwerte
  • Brand: Spirituose aus der Brennerei, Abgabe ab 18, 0,35 l und 0,5 l, Zutaten und Nährwerte freiwillig
  • Liste: kompakte Zeile für Liste und Warenkorb mit Jahrgang, Füllmenge, Preis, Grundpreis und % vol

Umsetzung

Daten je Wein aus einer Quelle (Sortiment, Warenwirtschaft): Lage, Rebsorte, Jahrgang, Qualitätsstufe, Gebinde mit Füllmenge in Litern und Preis, Alkohol, Restzucker, Säure, Allergene, Abfüllung mit Anschrift, Herstellungsdatum (bei Sekt die abgeschlossene zweite Gärung), Zutaten als Liste mit Allergen-Kennzeichen, Nährwerte je 100 ml, Bestand, Nachfolger. Der Grundpreis wird gerechnet, nie getippt: Preis ÷ Füllmenge in Litern, Intl.NumberFormat de-DE mit zwei Nachkommastellen, „19,33 € / 1 l“. Pflichtrahmen als Funktion des Datums: hergestellt ab 08.12.2023 → Tabelle Pflicht; davor → Hinweis statt leerer Tabelle. Raster als <dl> mit <dt>/<dd>, freiwillige Angaben mit eigenem Kennzeichen. Zutaten und Nährwerte als <details> mit <table> (Kopfzeilen, Bezug je 100 ml); details ist für die Seitensuche durchsuchbar und öffnet sich bei einem Treffer. Höhe mit interpolate-size: allow-keywords und ::details-content, öffnen 300 ms, schließen 200 ms. Allergene im Text als <strong class="allergen"> mit Unterstreichung und Hinterlegung, darunter noch einmal als Liste. Kaufbox: Gebinde als Radio-Gruppe, Knopf mit Zuständen (bereit, wird hinzugefügt, im Warenkorb), Versandkosten und Lieferzeit vor dem Kauf, Jugendschutz-Satz direkt am Knopf. Ausgetrunken: Kaufbox mit inert und Hinweis, Link zum Nachfolger. Sekt: Geschmacksangabe und „Hersteller:“ statt Erzeugerabfüllung. Spirituose: Altersgrenze 18, auch für gemischte Sendungen. Alle Rechtstexte als markierte Platzhalter, vom Kunden prüfen lassen.

Robust umsetzen

Telefon. Eine Spalte: Kopf, dann Kaufbox mit Preis und Grundpreis, dann das Etikett-Raster und die Klappe. Das Raster wird zweizeilig (Begriff über Wert), Tabellen bleiben zweispaltig. Gebinde als drei gleich breite Felder mit mindestens 44 px. Die Liste wird zu Karten mit Preis und Grundpreis in einer Zeile.

Ohne Bewegung. Die Klappe springt ohne Höhenübergang auf und zu, der Knopf zeigt „Wird hinzugefügt“ ohne Kreisel, die Markierung des Grundpreises entfällt.

Barrierefrei. Raster als <dl> mit <dt>/<dd>, Nährwerte als <table> mit <caption> und <th scope>, Ziffern in Tabellenziffern. Klappe als <details>/<summary>, durchsuchbar. Allergene hervorgehoben durch Unterstreichung und Fläche, nicht nur fett; darunter stehen sie noch einmal als Text („Enthalten: Sulfite“), damit Screenreader sie nennen. Gebinde als <fieldset> mit <legend> und Radios; Preis und Grundpreis in einer aria-live-Region (polite), damit der Wechsel angesagt wird. Ausgetrunkene Kaufbox mit inert, der Hinweis bleibt lesbar. Knöpfe mindestens 44 px hoch, Fokus sichtbar. Jugendschutz als Text, nicht als Sperrbildschirm.

Code

<!DOCTYPE html>
<!--
  Wein-Steckbrief mit Pflichtangaben
  Fundus · Sektionen & Funktionen · https://cecodesign.de/lab/fundus/sektionen/wein-steckbrief-pflichtangaben/
  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>Wein-Steckbrief mit Pflichtangaben · 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: 'EB Garamond'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/eb-garamond:vf@latest/latin-wght-normal.woff') format('woff'); font-weight: 400 800; font-style: normal; font-display: swap; }
@font-face { font-family: 'EB Garamond'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/eb-garamond:vf@latest/latin-wght-italic.woff2') format('woff2-variations'); font-weight: 400 800; font-style: italic; font-display: swap; }
@font-face { font-family: 'DM Mono'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/dm-mono@latest/latin-400-normal.woff2') format('woff2'); font-weight: 400; font-display: swap; }
@font-face { font-family: 'DM Mono'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/dm-mono@latest/latin-500-normal.woff2') format('woff2'); font-weight: 500; font-display: swap; }

:root {
  /* Rezept weingut-hofladen-herkunft: Grund aus dem Boden der Hauptlage */
  --kalk: #f1ece2; --text: #22211f; --loess: #e6dccb; --schiefer: #2f3236;
  --sand: #8a3a2b; --sand-tief: #74301f; --leise: #5a564f; --linie: #d8cfc0; --linie-stark: #b3a893;
  --auf: cubic-bezier(0.16, 1, 0.3, 1);      /* öffnen 300 ms */
  --zu: cubic-bezier(0.7, 0, 0.84, 0);       /* schließen 200 ms */
  --ui: cubic-bezier(0.4, 0, 0.2, 1);        /* Hover 150 ms */
  --serif: 'EB Garamond', Garamond, 'Times New Roman', serif;
  --mono: 'DM Mono', ui-monospace, 'SFMono-Regular', Menlo, monospace;
  interpolate-size: allow-keywords;
}
* { box-sizing: border-box; margin: 0; }
html { background: var(--kalk); color: var(--text); font: 400 1.25rem/1.55 var(--serif); -webkit-text-size-adjust: 100%; }
:root:has(:focus-visible) { scroll-behavior: auto; }
body { min-height: 100svh; }
:focus-visible { outline: 2px solid var(--text); outline-offset: 3px; }
a { color: inherit; }
button { font: inherit; color: inherit; background: none; border: 0; cursor: pointer; }
.sr { position: absolute !important; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap; }
.nw { white-space: nowrap; }
.mono { font-family: var(--mono); font-variant-numeric: tabular-nums; }
.label { font: 500 .75rem/1.3 var(--mono); letter-spacing: .1em; text-transform: uppercase; color: var(--leise); }

/* ---------- Kopf des Shops (Umgebung, nicht das Muster) ---------- */
.shopkopf { display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 14px clamp(16px, 4vw, 48px); border-bottom: 1px solid var(--linie); }
.marke { display: grid; line-height: 1; text-decoration: none; }
.marke b { font: 500 1.5rem/1 var(--serif); letter-spacing: .02em; }
.marke span { font: 400 .6875rem/1.4 var(--mono); letter-spacing: .1em; text-transform: uppercase; color: var(--leise); margin-top: 3px; }
.shopnav { display: flex; gap: 22px; font: 500 .75rem/1 var(--mono); letter-spacing: .1em; text-transform: uppercase; }
.shopnav a { text-decoration: none; padding: 14px 0; }
.shopnav a[aria-current] { text-decoration: underline; text-underline-offset: 6px; }
.korb { display: inline-flex; align-items: center; gap: 8px; min-height: 44px; padding: 0 4px; text-decoration: none; font: 500 .75rem/1 var(--mono); letter-spacing: .1em; text-transform: uppercase; }
.korb output { display: inline-grid; place-items: center; min-width: 24px; height: 24px; padding: 0 6px; border: 1px solid var(--text); font-variant-numeric: tabular-nums; }

/* ---------- Seite ---------- */
.seite { max-width: 1240px; margin: 0 auto; padding: 18px clamp(16px, 4vw, 48px) 120px; }
.pfad { font: 400 .75rem/1.4 var(--mono); letter-spacing: .06em; color: var(--leise); margin-bottom: 22px; }
.pfad a { text-decoration: none; } .pfad a:hover { text-decoration: underline; }
.pfad span[aria-hidden] { margin: 0 .5em; }

.steckbrief { display: grid; grid-template-columns: minmax(0, 1fr) minmax(300px, 360px); gap: 34px clamp(24px, 4vw, 56px); align-items: start; container: sb / inline-size; }
.steckbrief > * { grid-column: 1; min-width: 0; }
.steckbrief > .kaufbox { grid-column: 2; grid-row: 1 / span 5; }
@container sb (max-width: 1100px) {
  .klappe-inhalt { grid-template-columns: minmax(0, 1fr); }
  .aus-lage { grid-template-columns: minmax(0, 1fr); }
  .aus-lage figure { min-height: 0; aspect-ratio: 5 / 3; }
}

/* Kopf: Flasche + Titel */
.kopf { display: grid; grid-template-columns: clamp(150px, 17vw, 210px) minmax(0, 1fr); grid-template-areas: "fl ti" "fl me"; gap: 0 clamp(18px, 3vw, 40px); align-items: start; }
.kopf .titel { grid-area: ti; } .kopf .mehr { grid-area: me; }
.flasche { grid-area: fl; align-self: stretch; min-height: 300px; display: grid; place-items: end center; background: var(--loess); padding: 30px 8px 10px; position: relative; margin: 0; }
.flasche svg { width: 100%; max-height: 100%; height: auto; display: block; }
.flasche figcaption { position: absolute; left: 8px; top: 8px; font: 500 .6875rem/1 var(--mono); letter-spacing: .08em; color: var(--leise); }
.kopf .augenbraue { margin-bottom: 14px; }
.kopf h1 { font: 400 clamp(2.5rem, 5.6cqi, 5.25rem)/0.98 var(--serif); letter-spacing: -0.015em; text-wrap: balance; hyphens: auto; overflow-wrap: break-word; }
.kopf h1 .jahr { display: block; font: 400 clamp(1.125rem, .9rem + .8vw, 1.5rem)/1.2 var(--mono); letter-spacing: 0; margin-top: 12px; }
.kopf .lage { display: inline-block; margin-top: 12px; font: italic 400 1.5rem/1.25 var(--serif); text-decoration-thickness: 1px; text-underline-offset: 5px; }
.kopf .lage:hover { text-decoration-thickness: 2px; }
.kopf .lead { margin-top: 14px; max-width: 54ch; font-variant-numeric: oldstyle-nums; text-wrap: pretty; }
.marken { display: flex; flex-wrap: wrap; gap: 6px 8px; margin-top: 16px; padding: 0; list-style: none; }
.marken li { font: 400 .75rem/1 var(--mono); letter-spacing: .06em; padding: 7px 9px; border: 1px solid var(--linie-stark); }

/* Etikett: Pflichtangaben als Raster */
.etikett { position: relative; background: var(--loess); padding: clamp(20px, 3vw, 34px); }
.etikett::before, .etikett::after { content: ''; position: absolute; width: 14px; height: 14px; border: 1.5px solid var(--text); pointer-events: none; }
.etikett::before { left: 8px; top: 8px; border-right: 0; border-bottom: 0; }
.etikett::after { right: 8px; bottom: 8px; border-left: 0; border-top: 0; }
.etikett-kopf { display: flex; flex-wrap: wrap; align-items: baseline; justify-content: space-between; gap: 8px 16px; margin-bottom: 18px; }
.etikett h2 { font: 500 .8125rem/1.3 var(--mono); letter-spacing: .1em; text-transform: uppercase; }
.pruefen { display: inline-flex; align-items: center; gap: 6px; font: 500 .6875rem/1.3 var(--mono); letter-spacing: .06em; color: var(--sand-tief); border: 1px dashed var(--sand); padding: 4px 7px; }
.pruefen::before { content: '§'; font-weight: 500; }
.raster { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(178px, 100%), 1fr)); gap: 0 24px; }
.raster div { display: grid; align-content: start; gap: 3px; padding: 10px 0 12px; border-top: 1px solid var(--linie-stark); }
.raster div.breit { grid-column: 1 / -1; }
.raster dt { font: 500 .75rem/1.5 var(--mono); letter-spacing: .08em; text-transform: uppercase; color: var(--leise); padding-top: 1px; }
.raster dd { font: 400 .9375rem/1.45 var(--mono); font-variant-numeric: tabular-nums; overflow-wrap: anywhere; }
.raster dd .gross { font-size: 1.125rem; font-weight: 500; }
.unterkopf { margin: 22px 0 6px; font: 500 .75rem/1.3 var(--mono); letter-spacing: .1em; text-transform: uppercase; }
.unterkopf small { font: inherit; text-transform: none; letter-spacing: .02em; color: var(--leise); margin-left: 6px; }
.raster.frei dd { color: #3d3a35; }
.etikett .fuss { margin-top: 14px; font: 400 .8125rem/1.5 var(--mono); color: var(--leise); }
.allergen { font-weight: 500; text-decoration: underline 2px; text-underline-offset: .2em; background: rgba(138, 58, 43, .13); padding: 0 .1em; color: var(--text); }

/* Klappe Zutaten und Nährwerte */
.klappe { border-top: 1.5px solid var(--text); border-bottom: 1px solid var(--linie-stark); }
.klappe summary { display: flex; align-items: center; justify-content: space-between; gap: 16px; min-height: 60px; padding: 10px 0; cursor: pointer; list-style: none; }
.klappe summary::-webkit-details-marker { display: none; }
.klappe summary h2 { font: 400 1.625rem/1.15 var(--serif); }
.klappe summary .kurz { font: 400 .8125rem/1.35 var(--mono); color: var(--leise); text-align: right; }
.klappe summary .zeichen { flex: none; position: relative; width: 22px; height: 22px; }
.klappe summary .zeichen::before, .klappe summary .zeichen::after { content: ''; position: absolute; left: 3px; right: 3px; top: 10px; height: 1.5px; background: currentColor; transition: transform 200ms var(--zu); }
.klappe summary .zeichen::after { transform: rotate(90deg); }
.klappe[open] summary .zeichen::after { transform: rotate(0deg); transition: transform 300ms var(--auf); }
.klappe summary:hover h2 { text-decoration: underline; text-decoration-thickness: 1px; text-underline-offset: 5px; }
.klappe::details-content { block-size: 0; overflow-y: clip; transition: block-size 200ms var(--zu), content-visibility 200ms var(--zu) allow-discrete; }
.klappe[open]::details-content { block-size: auto; transition-duration: 300ms; transition-timing-function: var(--auf); }
.klappe-inhalt { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1.1fr); gap: 18px clamp(20px, 4vw, 48px); padding: 6px 0 26px; }
.klappe-inhalt > * { min-width: 0; }
.klappe-inhalt .hinweis-pflicht { grid-column: 1 / -1; font: 400 .875rem/1.5 var(--mono); color: var(--leise); max-width: 70ch; }
.zutaten h3, .naehr caption { font: 500 .75rem/1.3 var(--mono); letter-spacing: .1em; text-transform: uppercase; text-align: left; margin-bottom: 10px; }
.zutaten p { font: 400 1.125rem/1.6 var(--serif); }
.zutaten .legende { margin-top: 10px; font: 400 .8125rem/1.5 var(--mono); color: var(--leise); }
.naehr { width: 100%; border-collapse: collapse; font: 400 .9375rem/1.4 var(--mono); font-variant-numeric: tabular-nums; }
.naehr th, .naehr td { padding: 8px 0; border-top: 1px solid var(--linie-stark); text-align: left; font-weight: 400; vertical-align: top; }
.naehr thead th { font: 500 .6875rem/1.3 var(--mono); letter-spacing: .08em; text-transform: uppercase; color: var(--leise); border-top: 1.5px solid var(--text); }
.naehr td { text-align: right; white-space: nowrap; padding-left: 12px; }
.naehr thead th:last-child { text-align: right; }
.naehr .einzug th { padding-left: 1.2em; color: #3d3a35; }
.naehr-fuss { margin-top: 10px; font: 400 .75rem/1.5 var(--mono); color: var(--leise); }
.leer-hinweis { grid-column: 1 / -1; display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 14px; align-items: start; padding: 18px; background: var(--loess); }
.leer-hinweis .datum { font: 500 .75rem/1.2 var(--mono); letter-spacing: .06em; padding: 8px 9px; border: 1px solid var(--text); white-space: nowrap; }
.leer-hinweis p { font: 400 1.0625rem/1.5 var(--serif); max-width: 60ch; }

/* Aus der Lage: Schiefer-Band */
.aus-lage { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); background: var(--schiefer); color: var(--kalk); }
.aus-lage figure { margin: 0; position: relative; min-height: 220px; }
.aus-lage img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; display: block; }
.aus-lage figcaption { position: absolute; left: 0; bottom: 0; padding: 6px 10px; background: rgba(34, 33, 31, .78); font: 400 .6875rem/1.3 var(--mono); letter-spacing: .04em; color: var(--kalk); }
.aus-lage .text { padding: clamp(20px, 3vw, 30px); display: grid; gap: 14px; align-content: start; }
.aus-lage .label { color: rgba(241, 236, 226, .78); }
.aus-lage h2 { font: italic 400 1.75rem/1.15 var(--serif); }
.aus-lage dl { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px 18px; }
.aus-lage dt { font: 500 .6875rem/1.3 var(--mono); letter-spacing: .08em; text-transform: uppercase; color: rgba(241, 236, 226, .78); }
.aus-lage dd { font: 400 .9375rem/1.35 var(--mono); font-variant-numeric: tabular-nums; }

/* ---------- Kaufbox ---------- */
.kaufbox { position: sticky; top: 18px; background: var(--loess); padding: 24px clamp(18px, 2.4vw, 26px) 22px; display: grid; gap: 18px; }
.kaufbox fieldset { border: 0; padding: 0; min-width: 0; }
.kaufbox legend { font: 500 .75rem/1.3 var(--mono); letter-spacing: .1em; text-transform: uppercase; color: var(--leise); margin-bottom: 8px; padding: 0; }
.wahl { display: grid; grid-template-columns: repeat(var(--n, 3), minmax(0, 1fr)); gap: 6px; }
.wahl label { position: relative; display: grid; align-content: center; gap: 2px; min-height: 48px; padding: 8px 9px; border: 1px solid var(--linie-stark); background: var(--kalk); cursor: pointer; transition: border-color 150ms var(--ui), background-color 150ms var(--ui); }
.wahl input { position: absolute; opacity: 0; inset: 0; width: 100%; height: 100%; margin: 0; cursor: pointer; }
.wahl .n { font: 500 .8125rem/1.2 var(--mono); letter-spacing: .04em; }
.wahl .d { font: 400 .6875rem/1.25 var(--mono); color: var(--leise); font-variant-numeric: tabular-nums; hyphens: manual; }
.wahl input[name=gebinde]:not(:disabled) ~ .d { white-space: nowrap; letter-spacing: -.02em; }
.wahl label:hover { border-color: var(--text); }
.wahl label:has(input:checked) { border-color: var(--text); box-shadow: inset 0 0 0 1px var(--text); background: #fbf8f2; }
.wahl label:has(input:focus-visible) { outline: 2px solid var(--text); outline-offset: 2px; }
.wahl label:has(input:disabled) { cursor: not-allowed; background: transparent; border-style: dashed; }
.wahl label:has(input:disabled) .n { text-decoration: line-through; text-decoration-thickness: 1px; color: var(--leise); }
.preisblock { display: grid; gap: 4px; padding-top: 14px; border-top: 1.5px solid var(--text); }
.preis { display: flex; align-items: baseline; flex-wrap: wrap; gap: 4px 12px; }
.preis .betrag { font: 500 2rem/1.1 var(--mono); font-variant-numeric: tabular-nums; letter-spacing: -.01em; }
.preis .mwst { font: 400 .8125rem/1.3 var(--mono); color: var(--leise); }
.grundpreis { font: 500 1rem/1.4 var(--mono); font-variant-numeric: tabular-nums; justify-self: start; background: linear-gradient(var(--sand), var(--sand)) 0 100% / 0 2px no-repeat; }
.preisblock.neu .grundpreis { animation: markier 600ms var(--auf) both; }
@keyframes markier { from { background-size: 0 2px; } to { background-size: 100% 2px; } }
.preisblock .klein { font: 400 .8125rem/1.5 var(--mono); color: var(--leise); }
.preisblock .klein a { color: var(--text); }
.bestand { font: 400 .8125rem/1.4 var(--mono); display: flex; align-items: center; gap: 8px; }
.bestand::before { content: ''; width: 8px; height: 8px; border-radius: 50%; background: #3f6b3a; flex: none; }
.bestand.wenig::before { background: #9a5b00; }
.menge-zeile { display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 10px; align-items: stretch; }
.menge { display: inline-grid; grid-template-columns: 40px 30px 40px; border: 1px solid var(--linie-stark); background: var(--kalk); }
.menge button { min-height: 52px; font: 500 1.125rem/1 var(--mono); transition: background-color 150ms var(--ui); }
.menge button:hover { background: #fbf8f2; }
.menge output { display: grid; place-items: center; font: 500 1rem/1 var(--mono); font-variant-numeric: tabular-nums; }

/* Knopf: Buntsandstein, Zustände nach button-zustaende */
.kaufen { position: relative; display: grid; place-items: center; min-height: 52px; padding: 0 14px; white-space: nowrap; background: var(--sand); color: #fff; font: 500 .75rem/1 var(--mono); letter-spacing: .07em; text-transform: uppercase; overflow: hidden; transition: background-color 150ms var(--ui), transform 150ms var(--ui); }
.kaufen:hover { background: var(--sand-tief); }
.kaufen:active { transform: scale(.97); }
.kaufen:focus-visible { outline: 2px solid var(--text); outline-offset: 3px; }
.kaufen > span { grid-area: 1 / 1; display: inline-flex; align-items: center; gap: 10px; transition: opacity 200ms cubic-bezier(0.42, 0, 0.58, 1), transform 200ms cubic-bezier(0.42, 0, 0.58, 1); }
.kaufen .k-laden, .kaufen .k-fertig { position: absolute; inset: 0; justify-content: center; opacity: 0; transform: translateY(8px); }
.kaufen[data-zustand="laden"] .k-bereit, .kaufen[data-zustand="fertig"] .k-bereit { opacity: 0; transform: translateY(-8px); }
.kaufen[data-zustand="laden"] .k-laden, .kaufen[data-zustand="fertig"] .k-fertig { opacity: 1; transform: none; }
.kaufen[data-zustand="laden"] { pointer-events: none; }
.kaufen[data-zustand="fertig"] { background: var(--schiefer); }
.kreisel { width: 14px; height: 14px; border: 1.5px solid rgba(255, 255, 255, .35); border-top-color: #fff; border-radius: 50%; animation: dreh 600ms linear infinite; }
@keyframes dreh { to { transform: rotate(360deg); } }
.haken { width: 14px; height: 10px; }
.haken path { stroke-dasharray: 1; stroke-dashoffset: 1; }
.kaufen[data-zustand="fertig"] .haken path { stroke-dashoffset: 0; transition: stroke-dashoffset 300ms cubic-bezier(0.42, 0, 0.58, 1) 120ms; }

/* Nebenweg: Kontur mit Eckklammern (kontur-button-zeichnen, Variante ecken) */
.ecken { position: relative; display: inline-flex; align-items: center; justify-content: center; gap: 10px; min-height: 48px; padding: 0 20px; font: 500 .8125rem/1 var(--mono); letter-spacing: .08em; text-transform: uppercase; text-decoration: none; color: var(--text); }
.ecken::before, .ecken::after { content: ''; position: absolute; width: 12px; height: 12px; border: 1.5px solid currentColor; transition: width 310ms var(--zu), height 310ms var(--zu); }
.ecken::before { left: 0; top: 0; border-right: 0; border-bottom: 0; }
.ecken::after { right: 0; bottom: 0; border-left: 0; border-top: 0; }
.ecken:hover::before, .ecken:hover::after, .ecken:focus-visible::before, .ecken:focus-visible::after { width: calc(100% + 1px); height: calc(100% + 1px); transition: width 480ms var(--auf), height 480ms var(--auf); }
.ecken:focus-visible { outline: 1px solid var(--sand); outline-offset: 5px; }

.jugend { display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 12px; align-items: start; padding-top: 14px; border-top: 1px solid var(--linie-stark); }
.jugend .alter { display: grid; place-items: center; width: 44px; height: 44px; border: 1.5px solid var(--text); border-radius: 50%; font: 500 .9375rem/1 var(--mono); }
.jugend p { font: 400 .8125rem/1.5 var(--mono); }
.jugend .pruefen { margin-top: 8px; }
.aus { display: grid; gap: 12px; padding: 16px; border: 1.5px solid var(--text); background: var(--kalk); }
.aus strong { font: 400 1.5rem/1.2 var(--serif); }
.aus p { font: 400 .875rem/1.5 var(--mono); }
.nachfolger { justify-self: start; }

/* ---------- Variante Liste ---------- */
.liste-seite { display: grid; grid-template-columns: minmax(0, 1fr) minmax(280px, 340px); gap: clamp(24px, 4vw, 56px); align-items: start; }
.liste-seite h1 { font: 400 clamp(2.5rem, 1.6rem + 3vw, 4rem)/1 var(--serif); letter-spacing: -0.015em; margin-bottom: 8px; }
.liste-seite .einleitung { color: var(--leise); font-size: 1.0625rem; margin-bottom: 22px; max-width: 58ch; }
.liste-haupt { min-width: 0; container-type: inline-size; }
.spalten { display: none; }
.zeilen { list-style: none; padding: 0; border-top: 1.5px solid var(--text); }
.zeile { display: grid; grid-template-columns: 3.4rem minmax(0, 1fr) auto; gap: 4px 14px; align-items: start; padding: 14px 0; border-bottom: 1px solid var(--linie-stark); }
.zeile .jg { grid-row: 1 / span 2; font: 500 1rem/1.5 var(--mono); font-variant-numeric: tabular-nums; }
.zeile .wein { grid-column: 2; min-width: 0; }
.zeile .wein a { font: 400 1.25rem/1.2 var(--serif); text-decoration: none; }
.zeile .wein a:hover { text-decoration: underline; text-decoration-thickness: 1px; text-underline-offset: 4px; }
.zeile .wein small { display: block; font: italic 400 1rem/1.3 var(--serif); color: var(--leise); }
.zeile .wein .ab { display: inline-block; margin-left: 8px; font: 500 .6875rem/1 var(--mono); padding: 3px 5px; border: 1px solid var(--text); vertical-align: 3px; }
.zeile .werte { grid-column: 2; grid-row: 2; display: flex; flex-wrap: wrap; gap: 4px 16px; font: 400 .875rem/1.4 var(--mono); font-variant-numeric: tabular-nums; }
.zeile .pr { grid-column: 3; grid-row: 1; text-align: right; }
.zeile .pr b { display: block; font: 500 1rem/1.25 var(--mono); font-variant-numeric: tabular-nums; }
.zeile .pr > span { font: 400 .8125rem/1.3 var(--mono); color: var(--leise); font-variant-numeric: tabular-nums; white-space: nowrap; }
.zeile .tun { grid-column: 3; grid-row: 2; display: flex; justify-content: flex-end; }
@container (min-width: 860px) {
  .spalten { display: grid; grid-template-columns: 3.4rem minmax(0, 1fr) 4.6rem 6.4rem 8.6rem 8.4rem; gap: 14px; padding: 0 0 8px; }
  .spalten span { font: 500 .6875rem/1.3 var(--mono); letter-spacing: .08em; text-transform: uppercase; color: var(--leise); }
  .spalten span:nth-child(n+3) { text-align: right; }
  .zeilen { border-top-width: 1.5px; }
  .zeile { grid-template-columns: 3.4rem minmax(0, 1fr) 4.6rem 6.4rem 8.6rem 8.4rem; align-items: center; }
  .zeile .jg { grid-row: auto; }
  .zeile .werte { display: contents; }
  .zeile .werte > span { text-align: right; }
  .zeile .me { grid-column: 3; } .zeile .al { grid-column: 4; }
  .zeile .pr { grid-column: 5; grid-row: auto; } .zeile .tun { grid-column: 6; grid-row: auto; }
}
@container (max-width: 420px) {
  .zeile { grid-template-columns: 3rem minmax(0, 1fr); }
  .zeile .pr { grid-column: 2; grid-row: 3; text-align: left; }
  .zeile .tun { grid-column: 1 / -1; grid-row: 4; justify-content: stretch; }
  .zeile .tun .mini { width: 100%; }
}
.mini { display: inline-grid; place-items: center; min-height: 44px; min-width: 116px; padding: 0 12px; background: var(--sand); color: #fff; font: 500 .75rem/1 var(--mono); letter-spacing: .08em; text-transform: uppercase; white-space: nowrap; transition: background-color 150ms var(--ui), transform 150ms var(--ui); }
.mini:hover { background: var(--sand-tief); }
.mini:active { transform: scale(.97); }
.mini[data-zustand="fertig"] { background: var(--schiefer); }
.zeile.leer .pr b { font: 400 .8125rem/1.3 var(--mono); color: var(--leise); }
.zeile .ausgetrunken { font: 400 .75rem/1.4 var(--mono); text-align: right; color: var(--leise); }
.zeile .ausgetrunken a { color: var(--text); }
.korbbox { position: sticky; top: 18px; background: var(--loess); padding: 22px; display: grid; gap: 14px; }
.korbbox h2 { font: 400 1.75rem/1.1 var(--serif); }
.korbzeilen { list-style: none; padding: 0; display: grid; }
.korbzeilen li { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 2px 12px; padding: 10px 0; border-top: 1px solid var(--linie-stark); }
.korbzeilen .w { font: 400 1.0625rem/1.25 var(--serif); }
.korbzeilen .w small { display: block; font: 400 .75rem/1.4 var(--mono); color: var(--leise); }
.korbzeilen .s { font: 500 .9375rem/1.25 var(--mono); font-variant-numeric: tabular-nums; text-align: right; }
.korbzeilen .g { grid-column: 1 / -1; font: 400 .75rem/1.3 var(--mono); color: var(--leise); font-variant-numeric: tabular-nums; }
.korbzeilen .neu-zeile { animation: zeile-ein 300ms var(--auf); }
@keyframes zeile-ein { from { background: rgba(138, 58, 43, .14); } to { background: transparent; } }
.summen { display: grid; gap: 4px; padding-top: 10px; border-top: 1.5px solid var(--text); }
.summen div { display: flex; justify-content: space-between; gap: 12px; font: 400 .875rem/1.4 var(--mono); font-variant-numeric: tabular-nums; }
.summen .gesamt { font: 500 1.0625rem/1.4 var(--mono); }
.summen .mw { color: var(--leise); font-size: .75rem; }
.korbbox .jugend { border-top: 0; padding-top: 0; }
.korbbox .leerkorb { font: 400 .875rem/1.5 var(--mono); color: var(--leise); padding: 10px 0; border-top: 1px solid var(--linie-stark); }

/* ---------- Pult ---------- */
.pult { position: fixed; right: 14px; bottom: 14px; z-index: 40; display: grid; gap: 8px; max-width: min(430px, calc(100vw - 28px)); padding: 12px 14px; background: rgba(34, 33, 31, .96); color: var(--kalk); font: 500 .6875rem/1.45 var(--mono); letter-spacing: .06em; text-transform: uppercase; }
.pult .zeile-p { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }
.pult button { min-height: 30px; padding: 0 10px; border: 1px solid rgba(241, 236, 226, .5); color: var(--kalk); font: inherit; letter-spacing: inherit; text-transform: inherit; }
.pult button[aria-pressed="true"] { background: var(--kalk); color: var(--text); }
.pult button:focus-visible { outline: 2px solid var(--kalk); outline-offset: 2px; }
.pult .werte { text-transform: none; letter-spacing: .02em; color: #c9c2b5; }
.pult .werte b { color: var(--kalk); font-weight: 500; }

[hidden] { display: none !important; }

/* ---------- Telefon ---------- */
@media (max-width: 900px) {
  .steckbrief, .liste-seite { grid-template-columns: minmax(0, 1fr); }
  .steckbrief > .kaufbox { grid-column: 1; grid-row: auto; position: static; }
  .korbbox { position: static; }
  .shopnav { display: none; }
}
@media (max-width: 640px) {
  .kopf { grid-template-columns: 112px minmax(0, 1fr); grid-template-areas: "fl ti" "me me"; }
  .flasche { min-height: 190px; padding-top: 26px; }
  .kopf .mehr { margin-top: 4px; }
  .kopf .lage { font-size: 1.25rem; }
  .klappe-inhalt { grid-template-columns: minmax(0, 1fr); }
  .klappe summary .kurz { display: none; }
  .aus-lage { grid-template-columns: minmax(0, 1fr); }
  .aus-lage figure { min-height: 0; aspect-ratio: 5 / 3; }
}
@media (max-width: 420px) {
  .naehr { font-size: .875rem; }
  .naehr td { white-space: normal; text-align: right; }
  .zutaten p { overflow-wrap: anywhere; }
}
@media (max-width: 360px) {
  .wahl { grid-template-columns: minmax(0, 1fr); }
  .kopf { grid-template-columns: 96px minmax(0, 1fr); }
  .kopf h1 { font-size: 2.375rem; }
  .menge-zeile { grid-template-columns: minmax(0, 1fr); }
  .aus-lage dl { grid-template-columns: minmax(0, 1fr); }
}
@media (hover: none) {
  .ecken::before, .ecken::after { width: 12px; height: 12px; }
  .ecken:active::before, .ecken:active::after { width: calc(100% + 1px); height: calc(100% + 1px); transition-duration: 150ms; }
}

.vorschau body { overflow: hidden; }
.vorschau .shopkopf { padding-top: 10px; padding-bottom: 10px; }

:root.reduziert *, :root.reduziert *::before, :root.reduziert *::after, :root.reduziert ::details-content { transition-duration: .01ms !important; animation-duration: .01ms !important; }
:root.reduziert .kreisel { display: none; }
@media (prefers-reduced-motion: reduce) { .kreisel { display: none; } }
@media (forced-colors: active) {
  button, [role=button], [role=tab], [role=switch], input, select, textarea, .wahl label { border: 1px solid ButtonText !important; }
  .allergen { forced-color-adjust: none; background: Mark; color: MarkText; }
  .grundpreis { background: none; }
}
@media print {
  .shopkopf, .pult, .kaufen, .menge-zeile, .ecken { display: none !important; }
  .kaufbox { position: static; }
  .klappe::details-content { block-size: auto; content-visibility: visible; }
}
</style>
</head>
<body>
<header class="shopkopf">
  <a class="marke" href="#"><b>Hörlein</b><span>Weingut · Wellbach am Main</span></a>
  <nav class="shopnav" aria-label="Hauptmenü">
    <a href="#" aria-current="page">Weine</a><a href="#">Lagen</a><a href="#">Weinproben</a><a href="#">Hofladen</a>
  </nav>
  <a class="korb" href="#" aria-label="Warenkorb"><span aria-hidden="true">Warenkorb</span><output id="korbzahl" aria-live="polite">0</output></a>
</header>

<main id="inhalt" class="seite"></main>

<div class="pult" role="group" aria-label="Demo-Steuerung">
  <div class="zeile-p" role="group" aria-label="Variante">
    <span>Variante</span>
    <button type="button" data-v="wein">Wein</button>
    <button type="button" data-v="sekt">Sekt</button>
    <button type="button" data-v="brand">Brand</button>
    <button type="button" data-v="liste">Liste</button>
  </div>
  <div class="werte" id="werte"></div>
</div>

<script>
(() => {
  const html = document.documentElement;
  const reduziert = () => html.classList.contains('reduziert');
  const q = new URLSearchParams(location.search);
  const VARIANTEN = ['wein', 'sekt', 'brand', 'liste'];
  const main = document.getElementById('inhalt');
  const korbzahl = document.getElementById('korbzahl');

  const STICHTAG = '2023-12-08';
  const BETRIEB = 'Weingut Hörlein, Kirchgasse 7, 97339 Wellbach am Main, Deutschland';
  const euro = new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' });
  const zahl = (n, d = 2) => new Intl.NumberFormat('de-DE', { minimumFractionDigits: d, maximumFractionDigits: d }).format(n);
  const liter = l => zahl(l, l * 100 % 10 ? 2 : (l % 1 ? 1 : 1)).replace(/,0$/, ',0') + ' l';
  const fuell = g => g.n ? `${g.n} × ${liter(g.l)}` : liter(g.l);
  const gesamtLiter = g => (g.n || 1) * g.l;
  const grund = g => euro.format(g.preis / gesamtLiter(g)) + ' / 1 l';
  const pflichtTabelle = j => j.hergestellt >= STICHTAG;
  const datum = iso => new Intl.DateTimeFormat('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' }).format(new Date(iso + 'T12:00:00'));
  const monat = iso => new Intl.DateTimeFormat('de-DE', { month: 'long', year: 'numeric' }).format(new Date(iso + 'T12:00:00'));

  /* ---------- Daten (eine Quelle für Steckbrief, Liste und Warenkorb) ---------- */
  const P = {
    wein: {
      art: 'wein', titel: 'Silvaner', zusatz: 'trocken', lage: 'Wellbacher Sonnwendberg', lageId: 'sonnwendberg', bild: 'sonnwendberg',
      augenbraue: 'Lagenwein · Franken', jahrWort: 'Jahrgang', alter: 16, flasche: 'bocksbeutel', farbe: 'silvaner',
      lageDaten: [['Boden', 'Muschelkalk'], ['Ausrichtung', 'Süd-Südwest'], ['Neigung', '18 bis 34 %'], ['Höhe', '235 bis 280 m'], ['Fläche', '4,8 ha'], ['Rebsorten', 'Silvaner, Riesling']],
      jahre: [
        { j: '2024', alk: '12,5', rz: '2,1', saeure: '6,2', hergestellt: '2025-02-14', apnr: '4 731 018 012 25', analyse: '12.03.2025',
          lead: 'Vom Muschelkalk des Sonnwendbergs, Süd-Südwest über dem Main. Von Hand gelesen in der ersten Oktoberwoche, im großen Holzfass vergoren, einen Winter auf der Hefe.',
          marken: ['trocken', 'Muschelkalk', 'Holzfass'],
          gebinde: [{ id: 'flasche', name: 'Flasche', detail: 'Bocksbeutel', l: .75, preis: 14.5, bestand: 240 }, { id: 'magnum', name: 'Magnum', detail: 'Bocksbeutel', l: 1.5, preis: 32, bestand: 12 }, { id: 'karton', name: 'Karton', detail: '6 Bocksbeutel', n: 6, l: .75, preis: 81, bestand: 40 }],
          zutaten: [['Trauben'], ['Säureregulator: Weinsäure'], ['Konservierungsstoff: ', 'Sulfite']], nach: 'Abgefüllt in Schutzatmosphäre.',
          naehr: { kj: 315, kcal: 76, fett: '0', gfs: '0', kh: '1,2', zucker: '0,2', eiweiss: '0', salz: '0' } },
        { j: '2023', alk: '13', rz: '3,4', saeure: '5,6', hergestellt: '2023-11-21', apnr: '4 731 018 008 24',
          lead: 'Ein warmer, trockener Sommer, gelesen Mitte September. Mehr Schmelz als 2024, die Säure etwas weicher. Die letzten Flaschen aus dem Keller.',
          marken: ['trocken', 'Muschelkalk', 'letzte Flaschen'],
          gebinde: [{ id: 'flasche', name: 'Flasche', detail: 'Bocksbeutel', l: .75, preis: 15.5, bestand: 23 }, { id: 'magnum', name: 'Magnum', detail: 'Bocksbeutel', l: 1.5, preis: 34, bestand: 0 }, { id: 'karton', name: 'Karton', detail: '6 Bocksbeutel', n: 6, l: .75, preis: 87, bestand: 3 }] },
        { j: '2022', alk: '12,5', rz: '1,8', saeure: '6,0', hergestellt: '2022-11-30', apnr: '4 731 018 011 23', aus: true, nachfolger: '2024',
          lead: 'Der Jahrgang nach dem Hitzesommer, früh gelesen und straff. Ausgetrunken seit Mai 2026; der Steckbrief bleibt für alle, die ihn noch im Keller haben.',
          marken: ['trocken', 'Muschelkalk', 'ausgetrunken'],
          gebinde: [{ id: 'flasche', name: 'Flasche', detail: 'Bocksbeutel', l: .75, preis: 14, bestand: 0 }, { id: 'magnum', name: 'Magnum', detail: 'Bocksbeutel', l: 1.5, preis: 31, bestand: 0 }, { id: 'karton', name: 'Karton', detail: '6 Bocksbeutel', n: 6, l: .75, preis: 78, bestand: 0 }] }
      ]
    },
    sekt: {
      art: 'sekt', titel: 'Riesling Sekt', zusatz: 'brut', lage: 'Wellbacher Grauer Bühl', lageId: 'grauer-buehl', bild: 'grauer-buehl',
      augenbraue: 'Sekt b. A. · Franken · Winzersekt', jahrWort: 'Jahrgang', alter: 16, flasche: 'sekt', farbe: 'sekt',
      lageDaten: [['Boden', 'Muschelkalk'], ['Ausrichtung', 'Süd-Südost'], ['Neigung', '23 bis 32 %'], ['Höhe', '215 bis 270 m'], ['Fläche', '3,0 ha'], ['Rebsorten', 'Riesling, Weißburgunder']],
      jahre: [
        { j: '2022', alk: '12', rz: '7,0', saeure: '7,4', tirage: '2023-05-10', hergestellt: '2024-01-22', degorgiert: '2025-09-15', apnr: '4 731 018 021 25', analyse: '02.10.2025',
          lead: 'Grundwein vom Grauen Bühl, zweite Gärung in dieser Flasche, gut zwei Jahre auf der Hefe. Von Hand gerüttelt und im September 2025 degorgiert.',
          marken: ['brut', 'traditionelle Flaschengärung', '28 Monate Hefe'],
          gebinde: [{ id: 'flasche', name: 'Flasche', detail: 'Sektflasche', l: .75, preis: 19, bestand: 160 }, { id: 'magnum', name: 'Magnum', detail: 'Sektflasche', l: 1.5, preis: 42, bestand: 8 }, { id: 'karton', name: 'Karton', detail: '6 Flaschen', n: 6, l: .75, preis: 108, bestand: 20 }],
          zutaten: [['Trauben'], ['Fülldosage'], ['Versanddosage'], ['Konservierungsstoff: ', 'Sulfite']], nach: '',
          naehr: { kj: 312, kcal: 75, fett: '0', gfs: '0', kh: '1,7', zucker: '0,7', eiweiss: '0', salz: '0' } },
        { j: '2021', alk: '11,5', rz: '5,2', saeure: '8,1', tirage: '2022-04-06', hergestellt: '2022-09-19', degorgiert: '2024-03-04', apnr: '4 731 018 017 24',
          lead: 'Ein kühles Jahr, viel Säure, lange auf der Hefe. Nur noch in der Magnum; die Flaschen sind ausgetrunken.',
          marken: ['brut', 'traditionelle Flaschengärung', 'nur Magnum'],
          gebinde: [{ id: 'flasche', name: 'Flasche', detail: 'Sektflasche', l: .75, preis: 19, bestand: 0 }, { id: 'magnum', name: 'Magnum', detail: 'Sektflasche', l: 1.5, preis: 46, bestand: 4 }, { id: 'karton', name: 'Karton', detail: '6 Flaschen', n: 6, l: .75, preis: 108, bestand: 0 }] }
      ]
    },
    brand: {
      art: 'brand', titel: 'Zwetschgenwasser', zusatz: '', lage: 'Streuobstwiese Zwetschgengrund', lageId: 'zwetschgengrund', bild: 'zwetschgengrund',
      augenbraue: 'Edelbrand · Brennerei im Weingut', jahrWort: 'Ernte', alter: 18, flasche: 'brand', farbe: 'brand',
      lageDaten: [['Boden', 'Löss-Lehm'], ['Ausrichtung', 'Ost'], ['Bäume', '62 Hauszwetschgen'], ['Baumalter', '40 bis 70 Jahre'], ['Höhe', '225 bis 265 m'], ['Fläche', '1,8 ha']],
      jahre: [
        { j: '2024', alk: '42', hergestellt: '2025-01-28', los: 'L 25-03', ernte: 'September 2024',
          lead: 'Hauszwetschgen von alten Hochstämmen im Zwetschgengrund, vollreif aufgelesen, sechs Wochen vergoren, zweimal gebrannt und ein Jahr im Steingut gelagert.',
          marken: ['Obstbrand', 'eigene Früchte', '12 Monate Steingut'],
          gebinde: [{ id: 'gross', name: 'Flasche', detail: 'Flasche', l: .5, preis: 32, bestand: 64 }, { id: 'klein', name: 'Klein', detail: 'Kleine Flasche', l: .35, preis: 24, bestand: 30 }] },
        { j: '2023', alk: '42', hergestellt: '2024-01-15', los: 'L 24-02', ernte: 'September 2023', aus: true, nachfolger: '2024',
          lead: 'Die Ernte 2023 war klein, der Brand ist seit dem Sommer ausgetrunken. Der Nachfolger aus der Ernte 2024 ist da.',
          marken: ['Obstbrand', 'eigene Früchte', 'ausgetrunken'],
          gebinde: [{ id: 'gross', name: 'Flasche', detail: 'Flasche', l: .5, preis: 30, bestand: 0 }, { id: 'klein', name: 'Klein', detail: 'Kleine Flasche', l: .35, preis: 23, bestand: 0 }] }
      ]
    }
  };

  /* ---------- Zustand ---------- */
  const zustand = { v: VARIANTEN.includes(q.get('v')) ? q.get('v') : 'wein', jahr: {}, gebinde: null, menge: 1, korb: 0 };
  const produkt = () => P[zustand.v];
  const jahrDaten = () => { const p = produkt(); return p.jahre.find(x => x.j === (zustand.jahr[zustand.v] || p.jahre[0].j)) || p.jahre[0]; };
  const lieferbar = g => g.bestand > 0;

  /* ---------- Flaschen (eigene SVG-Zeichnungen) ---------- */
  function etikettSvg(cx, y, b, h, p, j, klein) {
    return `<g><rect x="${cx - b / 2}" y="${y}" width="${b}" height="${h}" fill="#efe6d4"/>
      <rect x="${cx - b / 2 + 4}" y="${y + 4}" width="${b - 8}" height="${h - 8}" fill="none" stroke="#22211f" stroke-width=".6"/>
      <text x="${cx}" y="${y + h * .25}" text-anchor="middle" font-family="DM Mono, monospace" font-size="${klein ? 6 : 7}" letter-spacing="2" fill="#22211f">HÖRLEIN</text>
      <text x="${cx}" y="${y + h * .53}" text-anchor="middle" font-family="EB Garamond, serif" font-style="italic" font-size="${klein ? 12 : 15}" fill="#22211f">${p.art === 'brand' ? 'Zwetschge' : p.art === 'sekt' ? 'Riesling' : 'Silvaner'}</text>
      <text x="${cx}" y="${y + h * .74}" text-anchor="middle" font-family="DM Mono, monospace" font-size="${klein ? 7 : 9}" fill="#22211f">${j.j}</text>
      <text x="${cx}" y="${y + h * .89}" text-anchor="middle" font-family="DM Mono, monospace" font-size="5" letter-spacing=".6" fill="#5a564f">${p.art === 'brand' ? 'ZWETSCHGENGRUND' : p.art === 'sekt' ? 'GRAUER BÜHL · BRUT' : 'SONNWENDBERG'}</text></g>`;
  }
  function eineFlasche(p, j, dx = 0, s = 1, ton = 1) {
    const t = `transform="translate(${dx} ${(1 - s) * 320}) scale(${s})" opacity="${ton}"`;
    if (p.flasche === 'bocksbeutel') return `<g ${t}>
      <path d="M86 6h28v8h-2v66c0 14 18 22 34 32 26 17 40 46 40 80 0 64-44 120-86 120S14 256 14 192c0-34 14-63 40-80 16-10 34-18 34-32V14h-2z" fill="#46532f"/>
      <path d="M86 6h28v8h-2v46H88V14h-2z" fill="#2f3236"/>
      <path d="M34 150c-12 22-14 52-6 78" fill="none" stroke="#f1ece2" stroke-opacity=".32" stroke-width="7" stroke-linecap="round"/>
      ${etikettSvg(100, 162, 98, 92, p, j)}</g>`;
    if (p.flasche === 'sekt') return `<g ${t}>
      <path d="M88 4h24v96c0 24 30 30 34 70v128c0 10-8 18-18 18H72c-10 0-18-8-18-18V170c4-40 34-46 34-70z" fill="#2c3526"/>
      <path d="M86 2h28v92c0 6-4 12-14 12s-14-6-14-12z" fill="#b9ad94"/>
      <path d="M86 40h28M86 60h28M86 80h28" stroke="#22211f" stroke-opacity=".25"/>
      <path d="M66 176c-4 30-4 74 0 110" fill="none" stroke="#f1ece2" stroke-opacity=".28" stroke-width="6" stroke-linecap="round"/>
      ${etikettSvg(100, 196, 84, 84, p, j, true)}</g>`;
    return `<g ${t}>
      <path d="M90 34h20v54c0 14 34 20 34 50v160c0 10-8 18-18 18H74c-10 0-18-8-18-18V138c0-30 34-36 34-50z" fill="#e3e6df" stroke="#8e968c" stroke-width="1.2"/>
      <path d="M60 170h80v126c0 8-6 14-14 14H74c-8 0-14-6-14-14z" fill="#f3f4ee"/>
      <rect x="86" y="8" width="28" height="30" rx="3" fill="#8c6f4f"/>
      <path d="M70 150c-4 40-4 100 0 140" fill="none" stroke="#fff" stroke-opacity=".8" stroke-width="5" stroke-linecap="round"/>
      ${etikettSvg(100, 186, 72, 88, p, j, true)}</g>`;
  }
  function flaschenSvg(p, j, g) {
    let innen;
    if (g && g.id === 'karton') innen = eineFlasche(p, j, -40, .74, .5) + eineFlasche(p, j, 40, .74, .5) + eineFlasche(p, j, 0, .8);
    else if (g && g.id === 'magnum') innen = eineFlasche(p, j, 0, 1);
    else if (g && g.id === 'klein') innen = eineFlasche(p, j, 0, .84);
    else innen = eineFlasche(p, j, 0, p.flasche === 'brand' ? 1 : .8);
    return `<svg viewBox="${g && g.id === 'karton' ? '-30 0 260 330' : '0 0 200 330'}" role="img" aria-label="${g ? g.detail + ' ' + fuell(g) : 'Flasche'}, Etikett ${p.titel} ${j.j}">${innen}</svg>`;
  }

  /* ---------- Bausteine ---------- */
  const pruefen = (t = 'Platzhalter · vom Kunden prüfen lassen') => `<span class="pruefen">${t}</span>`;
  const zutatenText = j => 'Zutaten: ' + j.zutaten.map(z => z[1] ? `${z[0]}<strong class="allergen">${z[1]}</strong>` : z[0]).join(', ') + '.' + (j.nach ? ' ' + j.nach : '');

  function rasterZeilen(p, j, g) {
    const pf = [], fr = [];
    if (p.art === 'wein') {
      pf.push(['Bezeichnung', 'Qualitätswein'], ['Anbaugebiet', 'Franken'], ['Herkunft', 'Wein aus Deutschland'],
        ['Alkohol', `<span class="gross">${j.alk} % vol</span>`], ['Füllmenge', fuell(g)], ['Allergene', 'enthält <strong class="allergen">Sulfite</strong>'],
        ['Abfüllung', `Erzeugerabfüllung ${BETRIEB}`, 'breit']);
      fr.push(['Lage', p.lage], ['Rebsorte', p.titel], ['Jahrgang', j.j], ['Geschmack', p.zusatz], ['Restzucker', `${j.rz} g/l`], ['Säure', `${j.saeure} g/l`],
        ['Hergestellt', `Gärung abgeschlossen am ${datum(j.hergestellt)}`, 'breit'], ['Prüfungsnummer', `A. P. Nr. ${j.apnr} (auf dem Etikett, online nicht Pflicht)`, 'breit']);
    } else if (p.art === 'sekt') {
      pf.push(['Bezeichnung', 'Sekt b. A.'], ['Anbaugebiet', 'Franken'], ['Herkunft', 'Deutsches Erzeugnis'], ['Geschmack', `brut <span style="color:var(--leise)">(0 bis 12 g/l Zucker)</span>`],
        ['Alkohol', `<span class="gross">${j.alk.includes(',') ? j.alk : j.alk + ',0'} % vol</span>`], ['Füllmenge', fuell(g)], ['Allergene', 'enthält <strong class="allergen">Sulfite</strong>'],
        ['Hersteller', BETRIEB, 'breit']);
      fr.push(['Lage', p.lage], ['Rebsorte', 'Riesling'], ['Jahrgang', j.j], ['Bezeichnung', 'Winzersekt, traditionelle Flaschengärung', 'breit'], ['Restzucker', `${j.rz} g/l`], ['Säure', `${j.saeure} g/l`],
        ['Tirage', monat(j.tirage)], ['2. Gärung fertig', monat(j.hergestellt)], ['Degorgiert', monat(j.degorgiert)], ['Prüfungsnummer', `A. P. Nr. ${j.apnr}`]);
    } else {
      pf.push(['Bezeichnung', 'Zwetschgenwasser'], ['Kategorie', 'Spirituose, Obstbrand'], ['Alkohol', `<span class="gross">${j.alk} % vol</span>`], ['Füllmenge', fuell(g)],
        ['Hersteller', BETRIEB.replace('Weingut Hörlein', 'Weingut Hörlein · Brennerei'), 'breit']);
      fr.push(['Früchte', 'Hauszwetschgen, eigene Streuobstwiese Zwetschgengrund', 'breit'], ['Ernte', j.ernte], ['Gebrannt', monat(j.hergestellt)],
        ['Lagerung', '12 Monate im Steingut'], ['Allergene', 'keine kennzeichnungspflichtigen'], ['Los', `${j.los} (auf der Flasche)`]);
    }
    const dl = (z) => z.map(([dt, dd, k]) => `<div${k ? ' class="breit"' : ''}><dt>${dt}</dt><dd>${dd}</dd></div>`).join('');
    const fuss = p.art === 'sekt' ? 'Bei Schaumwein: Hersteller statt Erzeugerabfüllung, Geschmacksangabe ist Pflicht. Als hergestellt gilt der Sekt, wenn die zweite Gärung abgeschlossen ist.'
      : p.art === 'brand' ? 'Spirituose: Abgabe erst ab 18 Jahren. Zutaten und Nährwerte sind nicht vorgeschrieben (Art. 16 Abs. 4 LMIV).'
      : 'Alles, was auf dem Etikett steht, steht vor dem Kauf auch hier. Alkohol in ganzen oder halben Einheiten.';
    return `<section class="etikett" aria-labelledby="et-h">
      <div class="etikett-kopf"><h2 id="et-h">Etikett · Pflichtangaben</h2>${pruefen()}</div>
      <dl class="raster">${dl(pf)}</dl>
      <h3 class="unterkopf">Weitere Angaben <small>freiwillig</small></h3>
      <dl class="raster frei">${dl(fr)}</dl>
      <p class="fuss">${fuss}</p>
    </section>`;
  }

  function klappe(p, j) {
    let kurz, inhalt;
    if (p.art === 'brand') {
      kurz = 'freiwillig · 961 kJ / 232 kcal je 100 ml';
      inhalt = `<p class="hinweis-pflicht">Für Spirituosen sind Zutatenverzeichnis und Nährwertdeklaration nicht vorgeschrieben (Art. 16 Abs. 4 LMIV, Getränke über 1,2 % vol; für Wein gelten eigene Regeln). Wir nennen den Brennwert freiwillig. ${pruefen()}</p>
        <div class="zutaten"><h3>Zutaten</h3><p>Freiwillige Angabe, Wortlaut vom Kunden mit der Lebensmittelüberwachung klären.</p></div>
        <table class="naehr"><caption>Brennwert je 100 ml</caption><thead><tr><th scope="col">Angabe</th><th scope="col">je 100 ml</th></tr></thead>
        <tbody><tr><th scope="row">Brennwert</th><td>961 kJ / 232 kcal</td></tr></tbody></table>`;
    } else if (!pflichtTabelle(j)) {
      kurz = 'vor dem 08.12.2023 hergestellt';
      const wort = p.art === 'sekt' ? `Die zweite Gärung dieser Füllung war im ${monat(j.hergestellt)} abgeschlossen` : `Dieser Jahrgang wurde am ${datum(j.hergestellt)} fertig vergoren`;
      inhalt = `<div class="leer-hinweis"><span class="datum">vor 08.12.2023</span><p>${wort}, also vor dem 8. Dezember 2023. Zutatenverzeichnis und Nährwertdeklaration sind für ihn nicht vorgeschrieben. Pflicht bleibt der Hinweis: enthält <strong class="allergen">Sulfite</strong>. Analysewerte nennen wir auf Anfrage.</p></div>
        <p class="hinweis-pflicht">${pruefen()}</p>`;
    } else {
      const n = j.naehr;
      kurz = `je 100 ml · ${n.kj} kJ / ${n.kcal} kcal`;
      const warum = p.art === 'sekt' ? `Grundwein ${j.j}, hergestellt im ${monat(j.hergestellt)} (zweite Gärung abgeschlossen): nach dem 08.12.2023, deshalb Pflicht.` : `Hergestellt nach dem 08.12.2023: Zutaten und Nährwerte sind Pflicht und stehen vollständig hier, nicht nur hinter einem QR-Code.`;
      inhalt = `<p class="hinweis-pflicht">${warum} ${pruefen()}</p>
        <div class="zutaten"><h3>Zutatenverzeichnis</h3><p>${zutatenText(j)}</p><p class="legende">Allergene sind unterstrichen und hinterlegt. Enthalten: ${j.zutaten.filter(z => z[1]).map(z => z[1]).join(', ')}.</p></div>
        <div><table class="naehr"><caption>Nährwerte je 100 ml</caption>
          <thead><tr><th scope="col">Nährwert</th><th scope="col">je 100 ml</th></tr></thead>
          <tbody>
            <tr><th scope="row">Brennwert</th><td>${n.kj} kJ / ${n.kcal} kcal</td></tr>
            <tr><th scope="row">Fett</th><td>${n.fett} g</td></tr>
            <tr class="einzug"><th scope="row">davon gesättigte Fettsäuren</th><td>${n.gfs} g</td></tr>
            <tr><th scope="row">Kohlenhydrate</th><td>${n.kh} g</td></tr>
            <tr class="einzug"><th scope="row">davon Zucker</th><td>${n.zucker} g</td></tr>
            <tr><th scope="row">Eiweiß</th><td>${n.eiweiss} g</td></tr>
            <tr><th scope="row">Salz</th><td>${n.salz} g</td></tr>
          </tbody></table>
          <p class="naehr-fuss">Beispielwerte, berechnet aus der Analyse vom ${j.analyse} mit den Faktoren der LMIV (Alkohol 29 kJ/g, Kohlenhydrate 17 kJ/g, organische Säuren 13 kJ/g).</p></div>`;
    }
    return `<details class="klappe" id="klappe"><summary><h2>Zutaten und Nährwerte</h2><span class="kurz">${kurz}</span><span class="zeichen" aria-hidden="true"></span></summary>
      <div class="klappe-inhalt">${inhalt}</div></details>`;
  }

  function kaufbox(p, j) {
    const gs = j.gebinde;
    let g = gs.find(x => x.id === zustand.gebinde && lieferbar(x)) || gs.find(lieferbar) || gs[0];
    zustand.gebinde = g.id;
    const jahrWahl = p.jahre.length > 1 ? `<fieldset><legend>${p.jahrWort}</legend><div class="wahl" style="--n:${p.jahre.length}">
        ${p.jahre.map(x => `<label><input type="radio" name="jahr" value="${x.j}" ${x.j === j.j ? 'checked' : ''}><span class="n">${x.j}</span><span class="d">${x.aus ? 'ausge&shy;trunken' : x.gebinde.some(lieferbar) ? (x.gebinde.reduce((s, y) => s + y.bestand, 0) < 40 ? 'wenige' : 'lieferbar') : ''}</span></label>`).join('')}
      </div></fieldset>` : '';
    const jugend = `<div class="jugend"><span class="alter" aria-hidden="true">${p.alter}+</span><div><p>Abgabe nur an Personen ab ${p.alter} Jahren (§ 9 JuSchG). Zustellung nur persönlich mit Alters- und Identitätsprüfung, Abholung im Hofladen mit Ausweis.${p.alter === 18 ? ' Liegt Wein mit im Paket, gilt 18 für die ganze Sendung.' : ''}</p>${pruefen()}</div></div>`;
    if (j.aus) {
      return `<aside class="kaufbox" aria-label="Kaufen">${jahrWahl}
        <div class="aus" role="status"><span class="label">${p.jahrWort} ${j.j}</span><strong>Zurzeit ausgetrunken</strong>
          <p>Der Steckbrief bleibt stehen. Nachfolger: ${p.jahrWort} ${j.nachfolger}, ${euro.format(p.jahre.find(x => x.j === j.nachfolger).gebinde[0].preis)} (${grund(p.jahre.find(x => x.j === j.nachfolger).gebinde[0])}).</p>
          <button type="button" class="ecken nachfolger" data-nachfolger="${j.nachfolger}">Zum ${p.jahrWort} ${j.nachfolger}</button></div>
        ${jugend}</aside>`;
    }
    const wenig = g.bestand < 25;
    return `<aside class="kaufbox" aria-label="Kaufen">${jahrWahl}
      <fieldset><legend>Gebinde</legend><div class="wahl" style="--n:${gs.length}">
        ${gs.map(x => `<label><input type="radio" name="gebinde" value="${x.id}" ${x.id === g.id ? 'checked' : ''} ${lieferbar(x) ? '' : 'disabled'}><span class="n">${x.name}</span><span class="d">${lieferbar(x) ? fuell(x) : 'ausge&shy;trunken'}</span></label>`).join('')}
      </div></fieldset>
      <div class="preisblock" id="preisblock" aria-live="polite">
        <div class="preis"><span class="betrag">${euro.format(g.preis)}</span><span class="mwst">inkl. 19 % MwSt.</span></div>
        <span class="grundpreis">${grund(g)}</span>
        <span class="klein">${g.detail} · ${fuell(g)} · zzgl. <a href="#">Versand</a> (6,90 € je Sendung, ab 150 € frei)</span>
      </div>
      <p class="bestand${wenig ? ' wenig' : ''}">${wenig ? `Nur noch ${g.bestand} ${g.id === 'karton' ? 'Kartons' : 'Flaschen'}` : 'Lieferbar'} · Lieferung in 2 bis 4 Werktagen</p>
      <div class="menge-zeile">
        <div class="menge" role="group" aria-label="Menge"><button type="button" data-menge="-1" aria-label="Eins weniger">−</button><output id="menge" aria-live="polite">${zustand.menge}</output><button type="button" data-menge="1" aria-label="Eins mehr">+</button></div>
        <button type="button" class="kaufen" id="kaufen" data-zustand="bereit"><span class="k-bereit">In den Warenkorb</span><span class="k-laden" aria-hidden="true"><i class="kreisel"></i>Wird hinzugefügt</span><span class="k-fertig" aria-hidden="true"><svg class="haken" viewBox="0 0 14 10" aria-hidden="true"><path d="M1 5l4 4 8-8" fill="none" stroke="#fff" stroke-width="1.8" pathLength="1"/></svg>Im Warenkorb</span></button>
      </div>
      <span class="sr" id="kauf-status" role="status"></span>
      <a class="ecken" href="#aus-lage">${p.art === 'brand' ? 'Zur Wiese' : 'Zur Lage ' + p.lage.replace('Wellbacher ', '')}</a>
      ${jugend}</aside>`;
  }

  function seite() {
    const p = produkt(), j = jahrDaten();
    const g = j.gebinde.find(x => x.id === zustand.gebinde && lieferbar(x)) || j.gebinde.find(lieferbar) || j.gebinde[0];
    const kb = kaufbox(p, j);
    return `<nav class="pfad" aria-label="Brotkrumen"><a href="#">Weine</a><span aria-hidden="true">/</span><wbr><a href="#">${p.art === 'brand' ? 'Brände' : p.art === 'sekt' ? 'Sekt' : 'Lagenweine'}</a><span aria-hidden="true">/</span><wbr><span aria-current="page">${p.titel} ${j.j}</span></nav>
    <div class="steckbrief">
        <div class="kopf">
          <figure class="flasche" id="flasche"><figcaption>${fuell(g)}</figcaption>${flaschenSvg(p, j, g)}</figure>
          <div class="titel">
            <p class="label augenbraue">${p.augenbraue}</p>
            <h1>${p.titel}<span class="jahr">${p.jahrWort} ${j.j}${p.zusatz ? ' · ' + p.zusatz : ''}</span></h1>
            <a class="lage" href="#aus-lage">${p.lage}</a>
          </div>
          <div class="mehr">
            <p class="lead">${j.lead}</p>
            <ul class="marken" aria-label="Kurz">${j.marken.map(m => `<li>${m}</li>`).join('')}<li>${j.alk.includes(',') ? j.alk : j.alk + (p.art === 'brand' ? '' : ',0')} % vol</li></ul>
          </div>
        </div>
        ${kb}
        ${rasterZeilen(p, j, g)}
        ${klappe(p, j)}
        <section class="aus-lage" id="aus-lage" aria-labelledby="lage-h" tabindex="-1">
          <figure><img src="bilder/wein-steckbrief-pflichtangaben-${p.bild}.webp" width="720" height="432" alt="${p.art === 'brand' ? 'Seitental mit Streuobstbäumen am Osthang, rechts und links Rebzeilen im Herbstlaub' : 'Südhang über dem Main mit Rebzeilen im gelben Herbstlaub, oben Waldsaum'}" loading="lazy"><figcaption>Eigene Ansicht · Wellbach am Main</figcaption></figure>
          <div class="text"><span class="label">${p.art === 'brand' ? 'Aus der Wiese' : 'Aus der Lage'}</span><h2 id="lage-h">${p.lage}</h2>
            <dl>${p.lageDaten.map(([a, b]) => `<div><dt>${a}</dt><dd>${b}</dd></div>`).join('')}</dl>
            <a class="ecken" style="color:var(--kalk);justify-self:start" href="#">Alle Weine dieser ${p.art === 'brand' ? 'Wiese' : 'Lage'}</a></div>
        </section>
    </div>`;
  }

  /* ---------- Variante Liste ---------- */
  const LISTE = [
    ['wein', '2024', 'flasche'], ['wein', '2024', 'magnum'], ['wein', '2023', 'flasche'], ['wein', '2022', 'flasche'],
    ['sekt', '2022', 'flasche'], ['sekt', '2021', 'magnum'], ['brand', '2024', 'gross'], ['brand', '2024', 'klein']
  ];
  const korbListe = [{ k: 'wein|2024|flasche', n: 2 }, { k: 'brand|2024|klein', n: 1 }];
  const eintrag = k => { const [v, jj, gid] = k.split('|'); const p = P[v], j = p.jahre.find(x => x.j === jj), g = j.gebinde.find(x => x.id === gid); return { v, p, j, g }; };
  function listeSeite() {
    const zeilen = LISTE.map(([v, jj, gid]) => {
      const { p, j, g } = eintrag(`${v}|${jj}|${gid}`);
      const name = `${p.titel}${p.zusatz ? ' ' + p.zusatz : ''}`;
      const aus = j.aus || !lieferbar(g);
      const alk = j.alk.includes(',') ? j.alk : j.alk + (p.art === 'brand' ? '' : ',0');
      return `<li class="zeile${aus ? ' leer' : ''}">
        <span class="jg"><span class="sr">${p.jahrWort} </span>${j.j}</span>
        <span class="wein"><a href="?v=${v}" data-zu="${v}" data-jahr="${jj}" data-geb="${gid}">${name}</a>${p.alter === 18 ? '<span class="ab"><span class="sr">Abgabe ab </span>18+</span>' : ''}<small>${p.lage}</small></span>
        <span class="werte"><span class="me"><span class="sr">Füllmenge </span>${fuell(g)}</span><span class="al"><span class="sr">Alkohol </span>${alk} % vol</span></span>
        <span class="pr">${aus ? '<b>ausgetrunken</b>' : `<b>${euro.format(g.preis)}</b><span><span class="sr">Grundpreis </span>${grund(g)}</span>`}</span>
        <span class="tun">${aus ? `<span class="ausgetrunken">${j.nachfolger ? `Nachfolger <a href="?v=${v}" data-zu="${v}" data-jahr="${j.nachfolger}" data-geb="${gid}">${p.jahrWort} ${j.nachfolger}</a>` : 'nur in der Magnum'}</span>` : `<button type="button" class="mini" data-dazu="${v}|${jj}|${gid}" aria-label="${name} ${j.j}, ${fuell(g)}, in den Warenkorb">In den Korb</button>`}</span></li>`;
    }).join('');
    return `<nav class="pfad" aria-label="Brotkrumen"><a href="#">Weine</a><span aria-hidden="true">/</span><wbr><span aria-current="page">Alle Weine und Brände</span></nav>
    <div class="liste-seite">
      <div class="liste-haupt">
        <h1>Weine und Brände</h1>
        <p class="einleitung">Jede Zeile mit Jahrgang, Füllmenge, Alkohol, Preis und Grundpreis. Ausgetrunkene Jahrgänge bleiben mit Nachfolger stehen.</p>
        <div class="spalten" aria-hidden="true"><span>Jahr</span><span>Wein</span><span>Menge</span><span>Alkohol</span><span>Preis · Grundpreis</span><span></span></div>
        <ol class="zeilen">${zeilen}</ol>
      </div>
      <aside class="korbbox" aria-labelledby="korb-h"><h2 id="korb-h">Warenkorb</h2><div id="korbinhalt"></div></aside>
    </div>`;
  }
  function korbZeichnen(neu) {
    const ziel = document.getElementById('korbinhalt'); if (!ziel) return;
    let summe = 0, ab18 = false;
    const li = korbListe.map(e => {
      const { p, j, g } = eintrag(e.k); const s = g.preis * e.n; summe += s; if (p.alter === 18) ab18 = true;
      return `<li class="${e.k === neu ? 'neu-zeile' : ''}"><span class="w">${e.n} × ${p.titel} ${j.j}<small>${p.lage} · <span class="nw">${fuell(g)}</span></small></span><span class="s">${euro.format(s)}</span><span class="g">je ${euro.format(g.preis)} · ${grund(g)}</span></li>`;
    }).join('');
    const versand = summe >= 150 || !summe ? 0 : 6.9, gesamt = summe + versand;
    ziel.innerHTML = (li ? `<ol class="korbzeilen">${li}</ol>` : '<p class="leerkorb">Noch leer.</p>') + `
      <div class="summen" aria-live="polite"><div><span>Zwischensumme</span><span>${euro.format(summe)}</span></div><div><span>Versand</span><span>${versand ? euro.format(versand) : 'frei'}</span></div>
      <div class="gesamt"><span>Gesamt</span><span>${euro.format(gesamt)}</span></div><div class="mw"><span>darin 19 % MwSt.</span><span>${euro.format(gesamt - gesamt / 1.19)}</span></div></div>
      <div class="jugend"><span class="alter" aria-hidden="true">${ab18 ? 18 : 16}+</span><div><p>${ab18 ? 'Ihre Sendung enthält eine Spirituose: Zustellung nur an Personen ab 18 Jahren mit Alters- und Identitätsprüfung.' : 'Zustellung nur an Personen ab 16 Jahren mit Alters- und Identitätsprüfung.'}</p>${pruefen()}</div></div>`;
    korbzahl.textContent = korbListe.reduce((s, e) => s + e.n, 0);
  }

  /* ---------- Zeichnen und Ereignisse ---------- */
  function zeichne(markier) {
    if (zustand.v === 'liste') { main.innerHTML = listeSeite(); korbZeichnen(); }
    else { main.innerHTML = seite(); if (!zustand.korbGesetzt) korbzahl.textContent = zustand.korb; }
    document.querySelectorAll('.pult [data-v]').forEach(b => b.setAttribute('aria-pressed', String(b.dataset.v === zustand.v)));
    werte();
    if (markier && !reduziert()) { const pb = document.getElementById('preisblock'); if (pb) { pb.classList.remove('neu'); void pb.offsetWidth; pb.classList.add('neu'); } }
  }
  function werte() {
    const w = document.getElementById('werte');
    if (zustand.v === 'liste') { w.innerHTML = 'Zeile: <b>Jahr · Wein · Menge · % vol · Preis + Grundpreis</b> · Korb rechnet Versand und Altersgrenze'; return; }
    const j = jahrDaten(), g = j.gebinde.find(x => x.id === zustand.gebinde) || j.gebinde[0];
    w.innerHTML = `Grundpreis <b>${euro.format(g.preis)} ÷ ${zahl(gesamtLiter(g), 2)} l</b> · Stichtag <b>08.12.2023</b> · ${j.aus ? 'Kaufbox <b>aus</b>' : pflichtTabelle(j) ? 'Tabelle <b>Pflicht</b>' : 'Tabelle <b>nicht Pflicht</b>'} · Klappe <b>300/200 ms</b>`;
  }
  function setzeVariante(v, schreiben = true) {
    zustand.v = v; zustand.gebinde = null; zustand.menge = 1;
    if (schreiben) { const p = new URLSearchParams(location.search); p.set('v', v); history.replaceState(null, '', '?' + p.toString().replace(/=(?=&|$)/g, '')); }
    zeichne();
  }
  function setzeJahr(jj) { zustand.jahr[zustand.v] = jj; const offen = document.getElementById('klappe')?.open; zeichne(true); if (offen) document.getElementById('klappe').open = true; }

  main.addEventListener('change', e => {
    const t = e.target;
    if (t.name === 'gebinde') { zustand.gebinde = t.value; const offen = document.getElementById('klappe')?.open; zeichne(true); if (offen) document.getElementById('klappe').open = true; document.querySelector(`input[name=gebinde][value="${t.value}"]`)?.focus(); }
    if (t.name === 'jahr') { setzeJahr(t.value); document.querySelector(`input[name=jahr][value="${t.value}"]`)?.focus(); }
  });
  main.addEventListener('click', e => {
    const m = e.target.closest('[data-menge]');
    if (m) { zustand.menge = Math.max(1, Math.min(24, zustand.menge + Number(m.dataset.menge))); document.getElementById('menge').textContent = zustand.menge; return; }
    const k = e.target.closest('#kaufen'); if (k) { kaufen(k); return; }
    const n = e.target.closest('[data-nachfolger]'); if (n) { setzeJahr(n.dataset.nachfolger); document.querySelector('input[name=jahr]:checked')?.focus(); return; }
    const z = e.target.closest('[data-zu]'); if (z) { e.preventDefault(); zustand.jahr[z.dataset.zu] = z.dataset.jahr; setzeVariante(z.dataset.zu); zustand.gebinde = z.dataset.geb; zeichne(); main.querySelector('h1')?.setAttribute('tabindex', '-1'); main.querySelector('h1')?.focus(); scrollTo(0, 0); return; }
    const d = e.target.closest('[data-dazu]'); if (d) { dazu(d); return; }
    const a = e.target.closest('a[href="#aus-lage"]'); if (a) { e.preventDefault(); const s = document.getElementById('aus-lage'); s.scrollIntoView({ behavior: reduziert() ? 'auto' : 'smooth', block: 'start' }); s.focus({ preventScroll: true }); }
  });
  document.querySelector('.pult').addEventListener('click', e => { const b = e.target.closest('[data-v]'); if (b) { pause(); setzeVariante(b.dataset.v); } });

  let kaufT = [];
  function kaufen(k) {
    kaufT.forEach(clearTimeout); kaufT = [];
    const st = document.getElementById('kauf-status');
    k.dataset.zustand = 'laden'; k.setAttribute('aria-disabled', 'true'); st.textContent = 'Wird hinzugefügt';
    kaufT.push(setTimeout(() => {
      zustand.korb += zustand.menge; korbzahl.textContent = zustand.korb;
      k.dataset.zustand = 'fertig'; st.textContent = `Im Warenkorb, ${zustand.korb} Artikel`;
      kaufT.push(setTimeout(() => { k.dataset.zustand = 'bereit'; k.removeAttribute('aria-disabled'); }, 2000));
    }, 900));
  }
  function dazu(b) {
    const k = b.dataset.dazu, e = korbListe.find(x => x.k === k);
    if (e) e.n++; else korbListe.push({ k, n: 1 });
    korbZeichnen(k);
    b.dataset.zustand = 'fertig'; b.textContent = 'Im Korb';
    setTimeout(() => { b.dataset.zustand = ''; b.textContent = 'In den Korb'; }, 1600);
  }

  /* ---------- Vorführung ---------- */
  let schritte = [], laeuft = false;
  const warte = (ms, f) => schritte.push(setTimeout(f, ms));
  function klick(sel) { const el = typeof sel === 'string' ? document.querySelector(sel) : sel; if (el) el.click(); }
  function waehle(name, wert) { const el = document.querySelector(`input[name=${name}][value="${wert}"]`); if (el && !el.disabled) { el.checked = true; el.dispatchEvent(new Event('change', { bubbles: true })); } }
  function rolle(y) { scrollTo({ top: y, behavior: reduziert() ? 'auto' : 'smooth' }); }
  function zuKlappe(sofort) { const k = document.getElementById('klappe'); if (k) scrollTo({ top: Math.max(0, k.getBoundingClientRect().top + scrollY - 130), behavior: sofort || reduziert() ? 'auto' : 'smooth' }); }
  const jahrZeigen = jj => { waehle('jahr', jj); zuKlappe(true); };

  function neustart() {
    schritte.forEach(clearTimeout); schritte = []; kaufT.forEach(clearTimeout);
    zustand.jahr = {}; zustand.gebinde = null; zustand.menge = 1;
    if (zustand.v === 'liste') { korbListe.length = 0; korbListe.push({ k: 'wein|2024|flasche', n: 2 }, { k: 'brand|2024|klein', n: 1 }); }
    zeichne(); scrollTo(0, 0);
  }
  function vorfuehren() {
    pause(); laeuft = true; html.classList.add('vorfuehrung');
    neustart();
    const v = zustand.v;
    if (v === 'liste') {
      warte(1200, () => klick('[data-dazu="sekt|2022|flasche"]'));
      warte(2600, () => klick('[data-dazu="wein|2024|magnum"]'));
      warte(4200, () => { korbListe.splice(1, 1); korbZeichnen(); });
      warte(5400, () => klick('[data-dazu="sekt|2022|flasche"]'));
      warte(8200, () => { if (laeuft) vorfuehren(); });
      return;
    }
    const zweites = v === 'brand' ? 'klein' : 'magnum';
    warte(1100, () => waehle('gebinde', zweites));
    if (v !== 'brand') warte(2400, () => waehle('gebinde', 'karton'));
    warte(3700, () => { waehle('gebinde', v === 'brand' ? 'gross' : 'flasche'); });
    warte(4300, () => { zuKlappe(); });
    warte(5000, () => { const k = document.getElementById('klappe'); if (k) k.open = true; });
    if (v === 'wein') {
      warte(7400, () => jahrZeigen('2023'));
      warte(9600, () => jahrZeigen('2022'));
      warte(11800, () => klick('[data-nachfolger]'));
      warte(12900, () => rolle(0));
      warte(13700, () => klick('#kaufen'));
      warte(17400, () => { if (laeuft) vorfuehren(); });
    } else if (v === 'sekt') {
      warte(7600, () => jahrZeigen('2021'));
      warte(10200, () => jahrZeigen('2022'));
      warte(11000, () => rolle(0));
      warte(11800, () => klick('#kaufen'));
      warte(15400, () => { if (laeuft) vorfuehren(); });
    } else {
      warte(7400, () => jahrZeigen('2023'));
      warte(9600, () => klick('[data-nachfolger]'));
      warte(10600, () => rolle(0));
      warte(11400, () => klick('#kaufen'));
      warte(15000, () => { if (laeuft) vorfuehren(); });
    }
  }
  function pause() { laeuft = false; schritte.forEach(clearTimeout); schritte = []; html.classList.remove('vorfuehrung'); }
  function weiter() { vorfuehren(); }

  zeichne();

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