Button-Gruppe expressiv (Toggle, Auswahl, Split)
Verbundene Knopfgruppe nach Material 3 Expressive: Druck macht breiter und eckiger, Auswahl macht rund, der Split-Button öffnet sein Menü mit drehendem Pfeil.
Filter mit zwei bis fünf Eigenschaften über einer Ergebnisliste: Shop-Kategorie, Kursplan, Rezepte; mit Trefferzahl und Leerzustand.
Mehrere Knöpfe bilden eine Einheit und reagieren aufeinander. In der verbundenen Gruppe liegen die Knöpfe 2 px auseinander, außen ganz rund, innen mit kleinen Ecken. Wer drückt, macht seinen Knopf breiter und eckiger, die Nachbarn geben nach. Wer auswählt, macht ihn ganz rund und farbig, ein Häkchen kommt dazu. Drei Rollen: Toggle-Gruppe mit Mehrfachauswahl (aria-pressed), Einfachauswahl als Radiogruppe und Split-Button aus Hauptaktion und Menü-Knopf. Beim Split-Button dreht sich der Pfeil, und der Menü-Knopf wird beim Öffnen ganz rund. Das Prinzip stammt aus Material 3 Expressive (2025) und wird hier mit CSS und wenig JS nachgebaut.
Die Form selbst zeigt den Zustand: rund heißt gewählt, eckig heißt gerade gedrückt. Das ist neben Farbe und Häkchen ein drittes Signal und macht die Auswahl auch ohne Farbsehen klar. Die Breitenänderung beim Druck gibt sofort spürbare Rückmeldung, ohne Ripple oder Wackeln. Der Split-Button hält die häufigste Handlung mit einem Klick erreichbar und versteckt Varianten der Handlung, statt drei Knöpfe nebeneinander zu stellen.
Wann einsetzen
Filter mit wenigen Eigenschaften über einer Ergebnisliste (Shop, Kursplan, Rezepte). Sortierung oder Ansicht (Raster oder Liste). Antwortwahl in Kursen und Quiz. Split-Button in der Kaufbox oder bei Produkt-UI, wenn eine Hauptaktion klare Nebenformen hat (In den Korb, als Geschenk, merken).
Wann nicht
Mehr als fünf Optionen (dann Auswahlliste oder Filter-Schublade). Navigation zwischen Seiten (dann Links in einem nav). Größenwahl in der Kaufbox bleibt eine echte Radiogruppe ohne Breitenwechsel. Ruhige Rezepte (Luxus, Kanzlei, Praxis): ohne Breitenwechsel, nur Farbe und Ecken. Split-Button nicht, wenn die Nebenaktionen gleich wichtig sind wie die Hauptaktion, und nie mit einer gefährlichen Aktion im Menü ohne Rückfrage.
Varianten
- Toggle: verbundene Gruppe mit Mehrfachauswahl, aria-pressed, Auswahl morpht die Ecken
- Einzeln: Einfachauswahl als Radiogruppe (role=radiogroup, aria-checked), Pfeile wählen
- Split: Hauptaktion plus Menü-Knopf mit Popover-Menü, Pfeil dreht sich, Knopf wird rund
- Standard-Gruppe mit 12 px Abstand statt verbunden (jeder Knopf behält seine Form)
- FAB-Menü auf dem Telefon (siehe mobile-bottom-navigation)
Umsetzung
Verbundene Gruppe: display:flex, gap 2px, Knöpfe flex:1 1 0. min-width bleibt auto, damit lange Beschriftungen bei größeren Textabständen nicht abgeschnitten werden; erst unter 480 px min-width:0 mit Auslassung. Ecken über zwei Variablen (links, rechts): innen 8px, außen Hälfte der Höhe in px (nicht 999px, sonst springt die Animation). Druck: Klasse .drueckt bei pointerdown bzw. Leertaste/Enter, mindestens 150 ms, setzt flex-grow 1.2 und innere Ecken 4px. Auswahl: alle Ecken rund, Markengrün, Häkchen fährt über grid-template-columns ein. Toggle: role=toolbar mit Namen, aria-pressed, roving tabindex (Pfeile, Pos1, Ende). Einfachauswahl: role=radiogroup, role=radio, aria-checked, nur der gewählte Knopf hat tabindex=0, Pfeile wählen mit Umlauf. Split: zwei buttons in role=group; Menü-Knopf mit aria-haspopup=menu, aria-expanded, aria-controls, aria-label; Menü als popover mit role=menu, menuitem und menuitemcheckbox, Lage per JS (beforetoggle) unter dem Knopf. Enter, Leertaste und Pfeil runter öffnen und fokussieren den ersten Eintrag, Pfeil hoch den letzten; im Menü Pfeile mit Umlauf, Pos1, Ende, Anfangsbuchstabe, Escape schließt und gibt den Fokus zurück, Tab schließt. Light-Dismiss schließt schon beim pointerdown, deshalb den Offen-Zustand vor dem Klick merken. Ohne Popover-Unterstützung: Attribut entfernen und Klasse .offen. Hauptaktion: Laden (Kreisel, „Wird hinzugefügt …“), Erfolg („Liegt im Korb“, Grün), Ansage per role=status. Nicht lieferbar: aria-disabled mit lesbarem Text und gestrichelter Kante, Menü bleibt bedienbar (Benachrichtigen).
Robust umsetzen
Telefon. Gruppe nimmt die volle Breite, Beschriftungen kurz halten (höchstens vier Optionen auf 320 px). Trefferfläche mindestens 44 px hoch. Kein Hover; Druck zeigt sich über :active. Menü des Split-Buttons öffnet unter dem Knopf und bleibt im Viewport.
Ohne Bewegung. Kein Breitenwechsel, Ecken und Farbe wechseln ohne Übergang, Menü erscheint ohne Gleiten, Pfeil springt. Umordnen ohne FLIP.
Barrierefrei. Echte button-Elemente. Toggle: aria-pressed, Gruppe role=toolbar oder group mit Namen. Einfachauswahl: role=radiogroup mit Namen, role=radio, aria-checked, roving tabindex (WAI-ARIA APG Radio Group). Split: Menü-Knopf mit eigenem Namen (aria-label „Weitere Optionen …“), aria-haspopup=menu, aria-expanded, aria-controls; Menü mit Pfeiltasten, Escape gibt den Fokus zurück (APG Menu Button). Zustand nie nur über Farbe (WCAG 1.4.1): Form und Häkchen zeigen die Auswahl. Fokusring sichtbar und vom Auswahlzustand getrennt (2.4.7). Ergebniszahl und Warenkorb per aria-live (4.1.3). Ziele mindestens 44 px (2.5.8 verlangt 24 px). Kontrastmodus: Rahmen an jedem Knopf, Auswahl über Highlight.
Code
<!DOCTYPE html>
<!--
Button-Gruppe expressiv (Toggle, Auswahl, Split)
Fundus · Buttons · https://cecodesign.de/lab/fundus/buttons/button-gruppe-expressiv/
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>Button-Gruppe expressiv (Toggle, Auswahl, Split) · Fundus-Demo</title>
<script>document.documentElement.classList.add('js');if(matchMedia('(prefers-reduced-motion: reduce)').matches)document.documentElement.classList.add('reduziert')</script>
<style>
@font-face { font-family: 'Instrument Sans'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/instrument-sans:vf@latest/latin-wght-normal.woff2') format('woff2-variations'); font-weight: 400 700; font-display: swap; }
@font-face { font-family: 'Young Serif'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/young-serif@latest/latin-400-normal.woff2') format('woff2'); font-weight: 400; font-display: swap; }
@font-face { font-family: 'Geist Mono'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/geist-mono:vf@latest/latin-wght-normal.woff2') format('woff2-variations'); font-weight: 100 900; font-display: swap; }
:root {
/* Rezept d2c-produkt-zuerst: warme Neutraltöne, fast schwarze Tinte, Grün für „verfügbar“ */
--grund: #F5F4F1; --kachel: #ECE9E2; --flaeche: #E4E0D7; --flaeche-hover: #DAD5CA;
--tinte: #212121; --leise: #5E5E5E; --linie: rgba(33, 33, 33, .12);
--gruen: #10714E; --gruen-tief: #0D5C40; --gruen-hell: #DCEBE2; --gruen-schrift: #0B5A3E;
--tomate: #C8452C; --tomate-hell: #F3DCD2; --tomate-schrift: #8E2A17;
/* Bewegung: token-toggle-asymmetrisch */
--auf: cubic-bezier(0.16, 1, 0.3, 1);
--zu: cubic-bezier(0.7, 0, 0.84, 0);
--t-druck: 150ms; --t-form: 220ms; --t-menue-auf: 250ms; --t-menue-zu: 160ms;
/* Form-Token der verbundenen Gruppe (MDC: 2 dp Fuge, 8 dp innen, außen rund, Druck +15 %) */
--fuge: 2px; --r-innen: 8px; --r-druck: 4px;
}
* { box-sizing: border-box; margin: 0; }
html { background: var(--grund); color: var(--tinte); font-family: 'Instrument Sans', system-ui, sans-serif; -webkit-font-smoothing: antialiased; }
:root:has(:focus-visible) { scroll-behavior: auto; }
body { min-height: 100svh; display: grid; grid-template-columns: minmax(0, 1fr); align-content: start; }
button { font: inherit; color: inherit; }
a { color: inherit; }
:focus-visible { outline: 2px solid var(--tinte); outline-offset: 2px; }
.sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
.label { font: 600 12px/1.2 'Instrument Sans', sans-serif; letter-spacing: .06em; text-transform: uppercase; color: var(--leise); }
.ziffern { font-variant-numeric: tabular-nums; }
/* ---------- Seite ---------- */
.seite { inline-size: min(100%, 1240px); margin-inline: auto; padding: 0 clamp(16px, 4vw, 40px) 40px; }
.ansage-leiste { padding: 9px 16px; background: var(--tinte); color: var(--grund); font-size: 13px; text-align: center; }
.ansage-leiste b { font-weight: 600; }
.kopf { display: flex; align-items: center; gap: 28px; padding: 18px 0; border-bottom: 1px solid var(--linie); }
.marke { display: inline-flex; align-items: center; gap: 10px; text-decoration: none; }
.marke svg { width: 34px; height: 24px; flex: none; }
.marke span { font: 400 19px/1 'Young Serif', Georgia, serif; letter-spacing: -.01em; white-space: nowrap; }
.kopf nav { display: flex; gap: 22px; font-size: 15px; font-weight: 500; }
.kopf nav a { text-decoration: none; padding: 6px 0; }
.kopf nav a[aria-current] { text-decoration: underline; text-underline-offset: 6px; text-decoration-thickness: 1.5px; }
.kopf-aktionen { margin-left: auto; display: flex; gap: 6px; }
.kopf-knopf { position: relative; display: inline-grid; place-items: center; width: 44px; height: 44px; border-radius: 22px; text-decoration: none; }
.kopf-knopf svg { width: 22px; height: 22px; }
.kopf-knopf .zahl { position: absolute; top: 3px; right: 1px; min-width: 18px; height: 18px; padding: 0 5px; border-radius: 9px; background: var(--tinte); color: var(--grund); font-size: 11px; font-weight: 650; line-height: 18px; text-align: center; font-variant-numeric: tabular-nums; transition: scale 250ms var(--auf); }
.kopf-knopf .zahl.puls { scale: 1.3; transition-duration: 0ms; }
.kopf-knopf .zahl[data-null] { display: none; }
@media (hover: hover) { .kopf nav a:hover { text-decoration: underline; text-underline-offset: 6px; } .kopf-knopf:hover { background: var(--kachel); } }
.krumen { display: flex; flex-wrap: wrap; gap: 8px; padding: 22px 0 0; font-size: 13.5px; color: var(--leise); list-style: none; padding-left: 0; }
.krumen li + li::before { content: '/'; margin-right: 8px; opacity: .7; }
.krumen a { text-underline-offset: 3px; }
/* ---------- Liste ---------- */
.liste-kopf { display: grid; grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr); gap: 16px 40px; align-items: end; padding: 14px 0 26px; }
h1, .titel { font: 400 clamp(2.25rem, 1.4rem + 3.6vw, 4.5rem)/1.05 'Young Serif', Georgia, serif; letter-spacing: -.01em; text-wrap: balance; }
.liste-kopf h1 { font-size: clamp(2.1rem, 1.2rem + 3vw, 3.75rem); }
.lead { font-size: 16px; line-height: 1.55; color: var(--leise); max-width: 34rem; text-wrap: pretty; }
.werkzeug { display: flex; flex-wrap: wrap; align-items: flex-end; gap: 14px 28px; padding: 18px 0 22px; border-top: 1px solid var(--linie); }
.feld { display: grid; gap: 10px; min-width: 0; flex: 1 1 30rem; max-width: 40rem; }
.feld[hidden] { display: none; }
.feld-kopf { display: flex; align-items: baseline; gap: 14px; }
.zuruecksetzen { border: 0; background: none; padding: 4px 2px; font-size: 13.5px; font-weight: 550; text-decoration: underline; text-underline-offset: 3px; cursor: pointer; }
.zuruecksetzen[hidden] { display: none; }
.treffer { margin-left: auto; font-size: 14px; color: var(--leise); padding-bottom: 14px; white-space: nowrap; }
.treffer b { color: var(--tinte); font-weight: 650; }
/* ---------- Die verbundene Button-Gruppe (das Muster) ---------- */
.gruppe { --h: 48px; --r-voll: calc(var(--h) / 2); display: flex; gap: var(--fuge); inline-size: 100%; }
.knopf {
--r-l: var(--r-innen); --r-r: var(--r-innen);
position: relative; flex: 1 1 0; height: var(--h); /* min-width bleibt auto: lange Beschriftung wird nie abgeschnitten (1.4.12) */
display: inline-flex; align-items: center; justify-content: center;
padding: 0 14px; border: 0; cursor: pointer;
border-radius: var(--r-l) var(--r-r) var(--r-r) var(--r-l);
background: var(--flaeche); color: var(--tinte);
font-size: 15px; font-weight: 560; white-space: nowrap;
transition: flex-grow 250ms var(--auf), border-radius var(--t-form) var(--auf), background-color var(--t-druck) var(--auf), color var(--t-druck) var(--auf);
-webkit-tap-highlight-color: transparent; touch-action: manipulation;
}
.knopf:first-child { --r-l: var(--r-voll); }
.knopf:last-child { --r-r: var(--r-voll); }
.knopf .txt { min-width: 0; }
.knopf .kurz { display: none; }
/* Häkchen fährt über die Spaltenbreite ein: Breite wächst mit, nichts springt */
.haken { display: grid; grid-template-columns: 0fr; transition: grid-template-columns var(--t-form) var(--auf), margin var(--t-form) var(--auf); }
.haken > span { min-width: 0; overflow: hidden; display: flex; }
.haken svg { width: 18px; height: 18px; flex: none; }
/* gewählt: ganz rund, Markengrün, Häkchen */
.knopf[aria-pressed="true"], .knopf[aria-checked="true"] { --r-l: var(--r-voll); --r-r: var(--r-voll); background: var(--gruen); color: #fff; }
.knopf[aria-pressed="true"] .haken, .knopf[aria-checked="true"] .haken { grid-template-columns: 1fr; margin-right: 6px; }
/* gedrückt: breiter (Nachbarn geben nach) und innen eckiger */
.knopf.drueckt { flex-grow: 1.2; transition-duration: var(--t-druck); }
.knopf.drueckt:not(:first-child) { --r-l: var(--r-druck); }
.knopf.drueckt:not(:last-child) { --r-r: var(--r-druck); }
.knopf.drueckt[aria-pressed="true"], .knopf.drueckt[aria-checked="true"] { --r-l: var(--r-innen); --r-r: var(--r-innen); }
.knopf:focus-visible { z-index: 1; outline-offset: 3px; }
@media (hover: hover) {
.knopf:hover { background: var(--flaeche-hover); }
.knopf[aria-pressed="true"]:hover, .knopf[aria-checked="true"]:hover { background: var(--gruen-tief); }
}
/* ---------- Produktraster ---------- */
.raster { list-style: none; padding: 0; display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 28px 18px; }
.raster[hidden] { display: none; }
.karte[hidden] { display: none; }
.karte a { display: grid; gap: 10px; text-decoration: none; border-radius: 16px; }
.bild { position: relative; aspect-ratio: 4 / 3; border-radius: 14px; background: var(--kachel); overflow: hidden; display: grid; place-items: center; }
.bild svg { width: 86%; height: 86%; transition: scale 400ms var(--auf); }
.karte-text { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 2px 12px; padding: 0 2px; }
.karte h3 { font-size: 16px; font-weight: 600; letter-spacing: -.005em; }
.karte .typ { grid-column: 1; font-size: 14px; color: var(--leise); }
.karte .preis { grid-column: 2; grid-row: 1; font-size: 16px; font-weight: 600; font-variant-numeric: tabular-nums; }
.plakette { position: absolute; top: 10px; left: 10px; padding: 4px 9px; border-radius: 999px; background: var(--tomate-hell); color: var(--tomate-schrift); font-size: 11.5px; font-weight: 650; letter-spacing: .06em; text-transform: uppercase; }
.plakette.frueh { background: var(--gruen-hell); color: var(--gruen-schrift); }
@media (hover: hover) { .karte a:hover .bild svg { scale: 1.04; } .karte a:hover h3 { text-decoration: underline; text-underline-offset: 3px; } }
.leer { display: grid; justify-items: start; gap: 12px; padding: 40px clamp(20px, 4vw, 48px); border-radius: 20px; background: var(--kachel); }
.leer[hidden] { display: none; }
.leer svg { width: 64px; height: 48px; }
.leer h2 { font: 400 26px/1.15 'Young Serif', Georgia, serif; }
.leer p { color: var(--leise); max-width: 36rem; line-height: 1.55; }
.leer-knopf { height: 44px; padding: 0 20px; border: 0; border-radius: 22px; background: var(--tinte); color: var(--grund); font-weight: 600; cursor: pointer; }
@media (hover: hover) { .leer-knopf:hover { background: #3a3a3a; } }
/* ---------- Detail mit Split-Button ---------- */
.detail[hidden], .liste[hidden] { display: none; }
.detail-raster { display: grid; grid-template-columns: minmax(0, 1.05fr) minmax(0, 1fr); gap: clamp(24px, 4vw, 56px); padding: 18px 0 10px; align-items: start; }
.packung { margin: 0; aspect-ratio: 5 / 5.4; max-height: 600px; border-radius: 22px; background: var(--kachel); display: grid; place-items: center; overflow: hidden; }
.packung svg { width: 78%; height: 90%; }
.kaufbox { display: grid; gap: 14px; align-content: start; padding-top: 6px; }
.kaufbox .titel { font-size: clamp(2.25rem, 1.6rem + 2.4vw, 3.6rem); }
.formel { font-size: 15.5px; color: var(--leise); line-height: 1.5; }
.preiszeile { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 12px; }
.preiszeile .preis { font-size: 26px; font-weight: 600; font-variant-numeric: tabular-nums; }
.preiszeile small { font-size: 13.5px; color: var(--leise); }
.geschenk-hinweis { font-size: 14px; color: var(--gruen-schrift); font-weight: 550; }
.geschenk-hinweis[hidden] { display: none; }
.lieferbar { display: flex; align-items: center; gap: 8px; font-size: 14.5px; color: var(--gruen-schrift); font-weight: 550; }
.lieferbar::before { content: ''; width: 8px; height: 8px; border-radius: 50%; background: var(--gruen); }
/* Split-Button: Hauptaktion + Menü-Knopf, 2 px Fuge */
.split { --h: 56px; --r-voll: calc(var(--h) / 2); --r-i: 6px; --r-i-druck: 14px; display: inline-flex; gap: var(--fuge); max-width: 100%; }
.split.klein { --h: 44px; --r-i: 5px; --r-i-druck: 11px; }
.split-haupt, .split-menu {
height: var(--h); border: 0; cursor: pointer; background: var(--tinte); color: var(--grund);
display: inline-flex; align-items: center; justify-content: center;
transition: border-radius var(--t-form) var(--auf), background-color var(--t-druck) var(--auf), scale var(--t-druck) var(--auf);
-webkit-tap-highlight-color: transparent; touch-action: manipulation;
}
.split-haupt { gap: 10px; min-width: 15.5rem; padding: 0 24px 0 20px; border-radius: var(--r-voll) var(--r-i) var(--r-i) var(--r-voll); font-size: 16px; font-weight: 600; }
.split.klein .split-haupt { min-width: 10rem; padding: 0 16px 0 14px; font-size: 14.5px; gap: 8px; }
.split-haupt svg { width: 20px; height: 20px; flex: none; }
.split.klein .split-haupt svg { width: 18px; height: 18px; }
.split-menu { width: calc(var(--h) - 4px); border-radius: var(--r-i) var(--r-voll) var(--r-voll) var(--r-i); }
.split-menu svg { width: 20px; height: 20px; transition: rotate var(--t-menue-auf) var(--auf); }
/* Druck: innere Ecken runden sich, die Fläche gibt 2 % nach */
.split-haupt.drueckt { border-radius: var(--r-voll) var(--r-i-druck) var(--r-i-druck) var(--r-voll); scale: .98; }
.split-menu.drueckt { border-radius: var(--r-i-druck) var(--r-voll) var(--r-voll) var(--r-i-druck); scale: .96; }
/* Menü offen: Menü-Knopf wird ganz rund, Pfeil dreht sich */
.split-menu[aria-expanded="true"] { border-radius: var(--r-voll); background: var(--gruen); color: #fff; }
.split-menu[aria-expanded="true"] svg { rotate: 180deg; }
.split-haupt[data-zustand="laedt"] { cursor: progress; }
.split-haupt[data-zustand="fertig"] { background: var(--gruen); color: #fff; }
.split-haupt .kreisel { animation: drehen 800ms linear infinite; }
@keyframes drehen { to { rotate: 360deg; } }
/* nicht lieferbar: aria-disabled, lesbar, gestrichelt */
.split-haupt[aria-disabled="true"]:not([data-zustand]) { background: transparent; color: var(--leise); box-shadow: inset 0 0 0 1.5px #9A958B; cursor: not-allowed; }
.split-haupt[aria-disabled="true"]:not([data-zustand]) + .split-menu:not([aria-expanded="true"]) { background: var(--flaeche); color: var(--tinte); }
@media (hover: hover) {
.split-haupt:not([aria-disabled="true"]):not([data-zustand]):hover, .split-menu:not([aria-expanded="true"]):hover { background: #3a3a3a; }
.split-haupt[aria-disabled="true"]:not([data-zustand]) + .split-menu:not([aria-expanded="true"]):hover { background: var(--flaeche-hover); }
}
.menue {
position: fixed; inset: auto; margin: 0; padding: 6px; border: 0; border-radius: 18px;
width: min(18.5rem, calc(100vw - 24px)); background: #fff; color: var(--tinte);
box-shadow: 0 0 0 1px rgba(33, 33, 33, .08), 0 18px 40px -8px rgba(33, 33, 33, .22);
transform-origin: top right;
opacity: 0; translate: 0 -6px; scale: .96;
transition: opacity var(--t-menue-zu) var(--zu), translate var(--t-menue-zu) var(--zu), scale var(--t-menue-zu) var(--zu), overlay var(--t-menue-zu) allow-discrete, display var(--t-menue-zu) allow-discrete;
}
.menue.oben { transform-origin: bottom right; }
.menue:popover-open, .menue.offen { opacity: 1; translate: 0 0; scale: 1; transition-duration: var(--t-menue-auf); transition-timing-function: var(--auf); }
@starting-style { .menue:popover-open { opacity: 0; translate: 0 -6px; scale: .96; } }
.menue:not([popover]):not(.offen) { display: none; }
.eintrag { width: 100%; min-height: 48px; display: grid; grid-template-columns: 22px minmax(0, 1fr); align-items: center; gap: 2px 12px; padding: 8px 12px; border: 0; border-radius: 12px; background: none; text-align: left; cursor: pointer; font-size: 15px; font-weight: 550; }
.eintrag svg { width: 20px; height: 20px; grid-row: span 2; }
.eintrag small { grid-column: 2; font-size: 13px; font-weight: 450; color: var(--leise); }
.eintrag:focus-visible { outline-offset: -2px; background: var(--kachel); }
.eintrag.sim { background: var(--kachel); }
.eintrag .kasten { stroke: currentColor; }
.eintrag .kasten-haken { opacity: 0; }
.eintrag[aria-checked="true"] .kasten-haken { opacity: 1; }
.eintrag[aria-checked="true"] .kasten-flaeche { fill: var(--gruen); stroke: var(--gruen); }
.trenner { height: 1px; margin: 5px 10px; background: var(--linie); }
@media (hover: hover) { .eintrag:hover { background: var(--kachel); } }
.vertrauen { list-style: none; padding: 14px 0 0; display: grid; gap: 8px; font-size: 14.5px; border-top: 1px solid var(--linie); margin-top: 6px; }
.vertrauen li { display: flex; gap: 10px; align-items: center; }
.vertrauen svg { width: 18px; height: 18px; flex: none; color: var(--gruen); }
.dazu { display: grid; gap: 10px; padding-top: 18px; }
.dazu h2 { font-size: 13px; }
.dazu-zeile { display: grid; grid-template-columns: 56px minmax(0, 1fr) auto; gap: 14px; align-items: center; padding: 10px; border-radius: 16px; background: var(--kachel); }
.dazu-zeile .mini { width: 56px; height: 56px; border-radius: 10px; background: var(--grund); display: grid; place-items: center; }
.dazu-zeile .mini svg { width: 40px; height: 40px; }
.dazu-zeile p { font-size: 14.5px; font-weight: 550; line-height: 1.35; }
.dazu-zeile p small { display: block; font-weight: 450; font-size: 13px; color: var(--leise); font-variant-numeric: tabular-nums; }
/* Ansage (sichtbar als kleiner Hinweis, zugleich Live-Region) */
.ansage { position: fixed; left: 50%; bottom: 22px; z-index: 30; translate: -50% 0; max-width: calc(100vw - 32px); padding: 12px 18px; border-radius: 14px; background: var(--tinte); color: var(--grund); font-size: 14.5px; font-weight: 500; box-shadow: 0 14px 30px -10px rgba(0,0,0,.35); opacity: 0; transition: opacity 200ms var(--zu), translate 200ms var(--zu); pointer-events: none; }
.ansage.zeigen { opacity: 1; translate: -50% -6px; transition: opacity 250ms var(--auf), translate 250ms var(--auf); }
/* Gedachter Zeiger für die Vorführung */
.zeiger { position: fixed; left: 0; top: 0; z-index: 40; width: 26px; height: 26px; margin: -13px 0 0 -13px; border-radius: 50%; background: rgba(33, 33, 33, .28); border: 2px solid rgba(255, 255, 255, .9); box-shadow: 0 2px 10px rgba(0,0,0,.2); pointer-events: none; display: none; transition: translate 650ms var(--auf), scale var(--t-druck) var(--auf); }
.zeiger.tippt { scale: .7; }
.vorfuehrung .zeiger { display: block; }
/* ---------- Demo-Pult (inline, kein fester Rahmen) ---------- */
.pult { margin-top: 40px; display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 14px 28px; align-items: start; padding: 16px 18px; border: 1px dashed #C9C4B9; border-radius: 16px; font: 500 11.5px/1.5 'Geist Mono', ui-monospace, monospace; letter-spacing: .04em; color: var(--leise); }
.pult .pk { font-size: 10.5px; letter-spacing: .14em; text-transform: uppercase; color: var(--leise); margin-bottom: 8px; }
.pult-wahl { display: flex; flex-wrap: wrap; gap: 6px; }
.pult-wahl button { height: 32px; padding: 0 12px; border: 1px solid #C9C4B9; border-radius: 8px; background: transparent; font: 500 10.5px/1 'Geist Mono', ui-monospace, monospace; letter-spacing: .14em; text-transform: uppercase; color: var(--tinte); cursor: pointer; }
.pult-wahl button[aria-pressed="true"] { background: var(--tinte); border-color: var(--tinte); color: var(--grund); }
.pult dl { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(15rem, 100%), 1fr)); gap: 4px 20px; }
.pult dl div { display: flex; gap: 8px; min-width: 0; }
.pult dt { color: var(--leise); white-space: nowrap; }
.pult dd { color: var(--tinte); overflow-wrap: anywhere; }
.pult .tasten { grid-column: 1 / -1; color: var(--leise); }
.pult kbd { font: inherit; padding: 1px 5px; border: 1px solid #C9C4B9; border-radius: 4px; color: var(--tinte); }
/* ---------- Vorschau 960 × 720 ---------- */
.vorschau body { overflow: hidden; }
.vorschau .ansage-leiste, .vorschau .lead, .vorschau .pult, .vorschau .dazu, .vorschau .krumen { display: none; }
.vorschau .liste-kopf { padding: 22px 0 18px; }
.vorschau .liste-kopf h1 { font-size: 44px; }
.vorschau .werkzeug { padding: 16px 0 20px; }
.vorschau .detail-raster { padding-top: 26px; }
.vorschau .packung { max-height: 560px; }
/* ---------- Schmal ---------- */
@media (max-width: 1000px) { .raster { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
@media (max-width: 760px) {
.kopf { flex-wrap: wrap; gap: 6px 18px; padding: 12px 0; }
.kopf nav { order: 3; width: 100%; gap: 18px; font-size: 14px; }
.liste-kopf, .detail-raster { grid-template-columns: minmax(0, 1fr); }
.raster { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 22px 12px; }
.feld { flex-basis: 100%; max-width: none; }
.treffer { margin-left: 0; padding-bottom: 0; }
.packung { aspect-ratio: 4 / 3; }
.packung svg { height: 92%; }
.pult { grid-template-columns: minmax(0, 1fr); }
}
@media (max-width: 480px) {
.knopf { padding: 0 8px; font-size: 14px; min-width: 0; }
.knopf .txt { overflow: hidden; text-overflow: ellipsis; }
.knopf .lang { display: none; }
.knopf .kurz { display: inline; }
.knopf[aria-pressed="true"] .haken, .knopf[aria-checked="true"] .haken { margin-right: 4px; }
.haken svg { width: 16px; height: 16px; }
.split { display: flex; }
.split-haupt { flex: 1; min-width: 0; }
.karte h3, .karte .preis { font-size: 15px; }
.karte-text { grid-template-columns: minmax(0, 1fr); }
.karte .preis { grid-column: 1; grid-row: auto; }
.dazu-zeile { grid-template-columns: 48px minmax(0, 1fr); }
.dazu-zeile .mini { width: 48px; height: 48px; }
.dazu-zeile .split { grid-column: 1 / -1; }
}
@media (max-width: 360px) {
.knopf { padding: 0 6px; font-size: 13.5px; }
.marke span { font-size: 17px; }
}
/* ---------- Reduzierte Bewegung ---------- */
:root.reduziert * { transition-duration: .01ms !important; animation-duration: .01ms !important; }
.reduziert .knopf.drueckt { flex-grow: 1; }
.reduziert .split-haupt.drueckt, .reduziert .split-menu.drueckt { scale: 1; }
.reduziert .split-haupt .kreisel { animation: none; }
.reduziert .zeiger { transition: none; }
@media (hover: none) { .zeiger { width: 34px; height: 34px; margin: -17px 0 0 -17px; } }
/* Windows-Kontrastmodus: Bedienelemente brauchen eine Kante */
@media (forced-colors: active) {
button, [role=button], [role=tab], [role=switch], input, select, textarea { border: 1px solid ButtonText !important; }
.knopf[aria-pressed="true"], .knopf[aria-checked="true"], .split-menu[aria-expanded="true"] { border: 3px solid Highlight !important; }
.eintrag[aria-checked="true"] .kasten-flaeche { fill: Highlight; stroke: Highlight; }
.menue { border: 1px solid CanvasText; }
.ansage { border: 1px solid CanvasText; }
}
</style>
</head>
<body>
<p class="ansage-leiste"><b>Versand ab 25 € kostenfrei</b> · Samenfest und nachbaubar · Aus Neubrandenburg</p>
<div class="seite" id="seite">
<header class="kopf">
<a class="marke" href="#seite" aria-label="Saatkontor Viertor, zur Startseite">
<svg viewBox="0 0 34 24" aria-hidden="true"><g fill="none" stroke="currentColor" stroke-width="2"><path d="M2 23V9a3 3 0 0 1 6 0v14"/><path d="M10 23V7a3 3 0 0 1 6 0v16"/><path d="M18 23V7a3 3 0 0 1 6 0v16"/><path d="M26 23V9a3 3 0 0 1 6 0v14"/><path d="M1 23h32"/></g></svg>
<span>Saatkontor Viertor</span>
</a>
<nav aria-label="Hauptnavigation">
<a href="#seite" aria-current="page">Saatgut</a>
<a href="#seite">Pflanzkalender</a>
<a href="#seite">Gartenwissen</a>
</nav>
<div class="kopf-aktionen">
<a class="kopf-knopf" href="#seite" id="merk-link" aria-label="Merkzettel, leer">
<svg viewBox="0 0 24 24" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round"><path d="M12 20s-7-4.4-7-10a4 4 0 0 1 7-2.6A4 4 0 0 1 19 10c0 5.6-7 10-7 10Z"/></svg>
<span class="zahl" id="merk-zahl" data-null aria-hidden="true">0</span>
</a>
<a class="kopf-knopf" href="#seite" id="korb-link" aria-label="Warenkorb, 1 Artikel">
<svg viewBox="0 0 24 24" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round"><path d="M5 8h14l-1.2 11.2a1 1 0 0 1-1 .8H7.2a1 1 0 0 1-1-.8Z"/><path d="M9 8V6.5a3 3 0 0 1 6 0V8"/></svg>
<span class="zahl" id="korb-zahl" aria-hidden="true">1</span>
</a>
</div>
</header>
<main>
<!-- Kategorieseite: Varianten „toggle“ und „einzeln“ -->
<section class="liste" id="liste" aria-labelledby="liste-titel">
<ol class="krumen" aria-label="Brotkrumen">
<li><a href="#seite">Saatgut</a></li><li><a href="#seite">Gemüse</a></li><li aria-current="page">Tomaten</li>
</ol>
<div class="liste-kopf">
<h1 id="liste-titel">Tomaten, die im Norden reif werden.</h1>
<p class="lead">Acht samenfeste Sorten, jede Saison auf unseren Beeten am Tollensesee geprüft. Aussaat ab Mitte März auf der Fensterbank, ins Beet nach den Eisheiligen.</p>
</div>
<div class="werkzeug">
<div class="feld" id="feld-filter">
<div class="feld-kopf">
<span class="label" id="lbl-filter">Geeignet für</span>
<button type="button" class="zuruecksetzen" id="zuruecksetzen" hidden>Filter zurücksetzen</button>
</div>
<div class="gruppe" id="gruppe-filter" role="toolbar" aria-labelledby="lbl-filter" aria-controls="raster">
<button type="button" class="knopf drueckbar" aria-pressed="false" data-wert="balkon"><span class="haken" aria-hidden="true"><span><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round"><path d="m5 12.5 4.5 4.5L19 7.5"/></svg></span></span><span class="txt">Balkon</span></button>
<button type="button" class="knopf drueckbar" aria-pressed="false" data-wert="freiland"><span class="haken" aria-hidden="true"><span><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round"><path d="m5 12.5 4.5 4.5L19 7.5"/></svg></span></span><span class="txt">Freiland</span></button>
<button type="button" class="knopf drueckbar" aria-pressed="false" data-wert="glashaus"><span class="haken" aria-hidden="true"><span><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round"><path d="m5 12.5 4.5 4.5L19 7.5"/></svg></span></span><span class="txt"><span class="lang">Gewächshaus</span><span class="kurz">Glashaus</span></span></button>
<button type="button" class="knopf drueckbar" aria-pressed="false" data-wert="frueh"><span class="haken" aria-hidden="true"><span><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round"><path d="m5 12.5 4.5 4.5L19 7.5"/></svg></span></span><span class="txt">Früh reif</span></button>
</div>
</div>
<div class="feld" id="feld-sort">
<div class="feld-kopf"><span class="label" id="lbl-sort">Sortieren nach</span></div>
<div class="gruppe" id="gruppe-sort" role="radiogroup" aria-labelledby="lbl-sort">
<button type="button" class="knopf drueckbar" role="radio" aria-checked="true" data-wert="beliebt"><span class="haken" aria-hidden="true"><span><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round"><path d="m5 12.5 4.5 4.5L19 7.5"/></svg></span></span><span class="txt">Beliebt</span></button>
<button type="button" class="knopf drueckbar" role="radio" aria-checked="false" data-wert="frueh"><span class="haken" aria-hidden="true"><span><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round"><path d="m5 12.5 4.5 4.5L19 7.5"/></svg></span></span><span class="txt">Erntezeit</span></button>
<button type="button" class="knopf drueckbar" role="radio" aria-checked="false" data-wert="neu"><span class="haken" aria-hidden="true"><span><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round"><path d="m5 12.5 4.5 4.5L19 7.5"/></svg></span></span><span class="txt">Neu</span></button>
<button type="button" class="knopf drueckbar" role="radio" aria-checked="false" data-wert="preis"><span class="haken" aria-hidden="true"><span><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round"><path d="m5 12.5 4.5 4.5L19 7.5"/></svg></span></span><span class="txt">Preis</span></button>
</div>
</div>
<p class="treffer" id="treffer" aria-live="polite"><b class="ziffern" id="treffer-zahl">8</b> <span id="treffer-wort">von 8 Sorten</span></p>
</div>
<ul class="raster" id="raster" aria-label="Tomatensorten"></ul>
<div class="leer" id="leer" hidden>
<svg viewBox="0 0 64 48" aria-hidden="true" fill="none" stroke="#5E5E5E" stroke-width="2" stroke-linecap="round"><path d="M6 42h52"/><path d="M32 42V22"/><path d="M32 30c-6 0-11-4-12-10 6 0 11 4 12 10Z"/><path d="M32 24c5 0 9-3 10-8-5 0-9 3-10 8Z"/></svg>
<h2>Keine Sorte passt zu allem.</h2>
<p id="leer-text">Diese Kombination schafft keine unserer Sorten. Nimm einen Filter heraus oder setze alle zurück.</p>
<button type="button" class="leer-knopf" id="leer-knopf">Filter zurücksetzen</button>
</div>
</section>
<!-- Produktseite: Variante „split“ -->
<section class="detail" id="detail" aria-labelledby="detail-titel" hidden>
<ol class="krumen" aria-label="Brotkrumen">
<li><a href="#seite">Saatgut</a></li><li><a href="#liste" id="zur-liste">Tomaten</a></li><li aria-current="page" id="krume-name">Rote Murmel</li>
</ol>
<div class="detail-raster">
<figure class="packung" id="packung"></figure>
<div class="kaufbox">
<p class="label" id="detail-typ">Wildtomate · samenfest</p>
<h1 class="titel" id="detail-titel">Rote Murmel</h1>
<p class="formel" id="detail-formel">Etwa 20 Samen · Ernte nach 62 Tagen · Balkon und Freiland. Kleine, süße Früchte in langen Rispen, platzen kaum.</p>
<div class="preiszeile"><span class="preis" id="detail-preis">3,20 €</span><small>inkl. MwSt., zzgl. Versand</small></div>
<p class="geschenk-hinweis" id="geschenk-hinweis" hidden>Wird als Geschenk verpackt (+ 1,50 €)</p>
<p class="lieferbar">Lieferbar, in 1–2 Werktagen bei dir</p>
<div class="split" role="group" aria-label="Kaufen">
<button type="button" class="split-haupt drueckbar" id="kauf-haupt" data-produkt="sorte">
<svg viewBox="0 0 24 24" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linejoin="round"><path d="M5 8h14l-1.2 11.2a1 1 0 0 1-1 .8H7.2a1 1 0 0 1-1-.8Z"/><path d="M9 8V6.5a3 3 0 0 1 6 0V8"/></svg>
<span class="split-text">In den Korb</span>
</button>
<button type="button" class="split-menu drueckbar" id="kauf-menu" aria-haspopup="menu" aria-expanded="false" aria-controls="menue-kauf" aria-label="Weitere Kaufoptionen">
<svg viewBox="0 0 24 24" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="m6.5 9.5 5.5 5.5 5.5-5.5"/></svg>
</button>
<div class="menue" id="menue-kauf" role="menu" aria-label="Weitere Kaufoptionen" popover>
<button type="button" class="eintrag" role="menuitemcheckbox" aria-checked="false" data-aktion="geschenk" tabindex="-1">
<svg viewBox="0 0 20 20" aria-hidden="true"><rect class="kasten kasten-flaeche" x="2.5" y="2.5" width="15" height="15" rx="4" fill="none" stroke-width="1.6"/><path class="kasten-haken" d="m6 10.2 2.8 2.8L14.2 7.4" fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
<span>Als Geschenk verpacken</span><small>Papiertüte mit Karte, + 1,50 €</small>
</button>
<button type="button" class="eintrag" role="menuitem" data-aktion="paket" tabindex="-1">
<svg viewBox="0 0 20 20" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round"><path d="M3 6.5 10 3l7 3.5v7L10 17l-7-3.5Z"/><path d="M3 6.5 10 10l7-3.5M10 10v7"/></svg>
<span>Drei Tüten zum Paketpreis</span><small class="ziffern" id="paket-preis">8,60 € statt 9,60 €</small>
</button>
<div class="trenner" role="separator"></div>
<button type="button" class="eintrag" role="menuitem" data-aktion="merken" tabindex="-1">
<svg viewBox="0 0 20 20" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round"><path d="M10 16.5s-6-3.7-6-8.4A3.3 3.3 0 0 1 10 6a3.3 3.3 0 0 1 6 2.1c0 4.7-6 8.4-6 8.4Z"/></svg>
<span>Auf den Merkzettel</span><small>Für die nächste Bestellung</small>
</button>
<button type="button" class="eintrag" role="menuitem" data-aktion="erinnern" tabindex="-1">
<svg viewBox="0 0 20 20" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"><rect x="3" y="4" width="14" height="13" rx="2.5"/><path d="M3 8h14M7 2.5v3M13 2.5v3"/></svg>
<span>An die Aussaat erinnern</span><small>E-Mail Mitte März, einmalig</small>
</button>
</div>
</div>
<ul class="vertrauen">
<li><svg viewBox="0 0 20 20" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="m4.5 10.5 3.5 3.5 7.5-8"/></svg>Keimprobe für jede Charge, Ergebnis auf der Tüte</li>
<li><svg viewBox="0 0 20 20" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="m4.5 10.5 3.5 3.5 7.5-8"/></svg>Versand ab 25 € kostenfrei, sonst 2,90 €</li>
<li><svg viewBox="0 0 20 20" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="m4.5 10.5 3.5 3.5 7.5-8"/></svg>14 Tage Widerruf, Rücksendung kostenlos</li>
</ul>
<div class="dazu">
<h2 class="label">Passt dazu</h2>
<div class="dazu-zeile">
<div class="mini" aria-hidden="true"><svg viewBox="0 0 40 40"><path d="M10 12h20l-2 22H12Z" fill="#cdbb95"/><path d="M10 12h20l-1 5H11Z" fill="#b9a57c"/><circle cx="20" cy="24" r="5" fill="#f1ece2"/></svg></div>
<p>Tomatendünger aus Schafwolle <small>1 kg · 6,90 € · 6,90 €/kg</small></p>
<div class="split klein" role="group" aria-label="Tomatendünger kaufen">
<button type="button" class="split-haupt drueckbar" data-produkt="duenger"><svg viewBox="0 0 24 24" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linejoin="round"><path d="M5 8h14l-1.2 11.2a1 1 0 0 1-1 .8H7.2a1 1 0 0 1-1-.8Z"/><path d="M9 8V6.5a3 3 0 0 1 6 0V8"/></svg><span class="split-text">In den Korb</span></button>
<button type="button" class="split-menu drueckbar" aria-haspopup="menu" aria-expanded="false" aria-controls="menue-duenger" aria-label="Weitere Optionen für den Tomatendünger"><svg viewBox="0 0 24 24" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="m6.5 9.5 5.5 5.5 5.5-5.5"/></svg></button>
<div class="menue" id="menue-duenger" role="menu" aria-label="Weitere Optionen für den Tomatendünger" popover>
<button type="button" class="eintrag" role="menuitem" data-aktion="duenger-zwei" tabindex="-1"><svg viewBox="0 0 20 20" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round"><path d="M3 6.5 10 3l7 3.5v7L10 17l-7-3.5Z"/><path d="M3 6.5 10 10l7-3.5M10 10v7"/></svg><span>Zwei Beutel</span><small class="ziffern">12,90 € · 6,45 €/kg</small></button>
<button type="button" class="eintrag" role="menuitem" data-aktion="merken" tabindex="-1"><svg viewBox="0 0 20 20" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round"><path d="M10 16.5s-6-3.7-6-8.4A3.3 3.3 0 0 1 10 6a3.3 3.3 0 0 1 6 2.1c0 4.7-6 8.4-6 8.4Z"/></svg><span>Auf den Merkzettel</span><small>Für die nächste Bestellung</small></button>
</div>
</div>
</div>
<div class="dazu-zeile">
<div class="mini" aria-hidden="true"><svg viewBox="0 0 40 40"><path d="M9 14h22l-3 18H12Z" fill="#a8835c"/><path d="M8 12h24v3H8Z" fill="#8f6c48"/></svg></div>
<p>Anzuchttöpfe aus Holzfaser <small>24 Stück · 4,50 € · ab 14.11. wieder da</small></p>
<div class="split klein" role="group" aria-label="Anzuchttöpfe">
<button type="button" class="split-haupt drueckbar" aria-disabled="true" data-produkt="toepfe" aria-describedby="toepfe-grund"><span class="split-text">Ausverkauft</span></button>
<button type="button" class="split-menu drueckbar" aria-haspopup="menu" aria-expanded="false" aria-controls="menue-toepfe" aria-label="Optionen für die Anzuchttöpfe"><svg viewBox="0 0 24 24" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="m6.5 9.5 5.5 5.5 5.5-5.5"/></svg></button>
<span class="sr" id="toepfe-grund">Wieder lieferbar ab 14. November</span>
<div class="menue" id="menue-toepfe" role="menu" aria-label="Optionen für die Anzuchttöpfe" popover>
<button type="button" class="eintrag" role="menuitem" data-aktion="melden" tabindex="-1"><svg viewBox="0 0 20 20" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round"><path d="M5 14V9a5 5 0 0 1 10 0v5l1.5 2h-13Z"/><path d="M8.5 17.5a1.5 1.5 0 0 0 3 0"/></svg><span>Benachrichtigen, wenn lieferbar</span><small>Eine E-Mail, dann nie wieder</small></button>
<button type="button" class="eintrag" role="menuitem" data-aktion="merken" tabindex="-1"><svg viewBox="0 0 20 20" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round"><path d="M10 16.5s-6-3.7-6-8.4A3.3 3.3 0 0 1 10 6a3.3 3.3 0 0 1 6 2.1c0 4.7-6 8.4-6 8.4Z"/></svg><span>Auf den Merkzettel</span><small>Für die nächste Bestellung</small></button>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
</main>
<section class="pult" aria-label="Demo-Steuerung">
<div>
<p class="pk" id="pult-titel">Variante</p>
<div class="pult-wahl" role="group" aria-labelledby="pult-titel">
<button type="button" data-v="toggle" aria-pressed="true">Toggle</button>
<button type="button" data-v="einzeln" aria-pressed="false">Einzeln</button>
<button type="button" data-v="split" aria-pressed="false">Split</button>
</div>
</div>
<div>
<p class="pk">Werte dieser Variante</p>
<dl id="pult-werte"></dl>
</div>
<p class="tasten" id="pult-tasten"></p>
</section>
</div>
<p class="ansage" id="ansage" role="status" aria-live="polite"></p>
<div class="zeiger" id="zeiger" aria-hidden="true"></div>
<script>
(() => {
const html = document.documentElement;
const reduziert = () => html.classList.contains('reduziert');
const $ = (s, r = document) => r.querySelector(s);
const $$ = (s, r = document) => [...r.querySelectorAll(s)];
const eur = n => n.toFixed(2).replace('.', ',') + ' €';
/* ---------- Sorten ---------- */
const SORTEN = [
{ id: 'murmel', name: 'Rote Murmel', typ: 'Wildtomate', tage: 62, preis: 3.20, beliebt: 1, neu: 9, f: ['balkon', 'freiland', 'frueh'], form: 'rispe', c: ['#e8553a', '#a92a1a'], text: 'Kleine, süße Früchte in langen Rispen, platzen kaum.' },
{ id: 'matina', name: 'Matina', typ: 'Salattomate', tage: 60, preis: 3.40, beliebt: 2, neu: 9, f: ['freiland', 'glashaus', 'frueh'], form: 'rund', c: ['#ef5f3f', '#b3301c'], text: 'Frühe, runde Früchte mit Kartoffelblatt, robust im Freiland.' },
{ id: 'birne', name: 'Gelbe Birne', typ: 'Cocktailtomate', tage: 70, preis: 3.40, beliebt: 3, neu: 9, f: ['balkon', 'glashaus'], form: 'birne', c: ['#f6cb3c', '#c98f0e'], text: 'Birnenförmige Früchte, mild und fast säurefrei.' },
{ id: 'ochsenherz', name: 'Ochsenherz', typ: 'Fleischtomate', tage: 85, preis: 3.80, beliebt: 4, neu: 9, f: ['glashaus'], form: 'herz', c: ['#ec6444', '#b53820'], text: 'Herzförmige Früchte bis 400 g, wenige Kerne, ideal für Soße.' },
{ id: 'krim', name: 'Schwarze Krim', typ: 'Fleischtomate', tage: 80, preis: 3.90, beliebt: 5, neu: 9, f: ['glashaus'], form: 'flach', c: ['#8c4438', '#4d211c'], schulter: '#55663a', text: 'Dunkelrote, flache Früchte mit grünen Schultern, kräftig im Geschmack.' },
{ id: 'tigerella', name: 'Tigerella', typ: 'Salattomate', tage: 68, preis: 3.50, beliebt: 6, neu: 2, f: ['balkon', 'freiland'], form: 'streifen', c: ['#e4552f', '#a8321b'], streifen: '#f2a72e', text: 'Rot mit gelben Streifen, mittelgroß, ertragreich.' },
{ id: 'johannis', name: 'Goldene Johannisbeere', typ: 'Wildtomate', tage: 58, preis: 3.20, beliebt: 7, neu: 1, f: ['balkon', 'freiland', 'frueh'], form: 'rispe', c: ['#f7be36', '#cf870c'], text: 'Erbsengroße goldene Früchte, die früheste Sorte im Sortiment.' },
{ id: 'berner', name: 'Berner Rose', typ: 'Fleischtomate', tage: 78, preis: 3.60, beliebt: 8, neu: 9, f: ['glashaus', 'freiland'], form: 'flach', c: ['#f08a90', '#c4525f'], text: 'Rosafarben und zart, eine alte Sorte für Salat und Brot.' }
];
const ORT = { balkon: 'Balkon', freiland: 'Freiland', glashaus: 'Gewächshaus', frueh: 'früh reif' };
const byId = Object.fromEntries(SORTEN.map(s => [s.id, s]));
/* ---------- Tomaten als SVG ---------- */
function kelch(x, y, s = 1) {
return `<g transform="translate(${x} ${y}) scale(${s})"><path d="M0-2C4-10 10-14 18-12 10-8 6-4 4 0c6 2 12 8 12 14C8 10 3 6 0 4c-3 2-8 6-16 10 0-6 6-12 12-14-2-4-6-8-14-12C-10-14-4-10 0-2Z" fill="#3f6b35"/><path d="M0-2c1-6 3-10 6-14" stroke="#3f6b35" stroke-width="3" fill="none" stroke-linecap="round"/></g>`;
}
function tomate(s, k) {
const g = `g-${s.id}-${k}`, [hell, dunkel] = s.c;
const defs = `<defs><radialGradient id="${g}" cx=".36" cy=".3" r=".8"><stop offset="0" stop-color="${hell}"/><stop offset=".55" stop-color="${hell}"/><stop offset="1" stop-color="${dunkel}"/></radialGradient></defs>`;
const schatten = `<ellipse cx="100" cy="134" rx="50" ry="6" fill="#212121" opacity=".09"/>`;
const glanz = (x, y, rx, ry) => `<ellipse cx="${x}" cy="${y}" rx="${rx}" ry="${ry}" fill="#fff" opacity=".32" transform="rotate(-30 ${x} ${y})"/>`;
let k2 = '';
if (s.form === 'rund' || s.form === 'streifen') {
k2 = `<ellipse cx="100" cy="86" rx="48" ry="44" fill="url(#${g})"/>`;
if (s.streifen) k2 += `<g clip-path="url(#k-${g})" fill="none" stroke="${s.streifen}" stroke-width="5" opacity=".85" stroke-linecap="round"><path d="M80 46c-10 20-10 56 0 82"/><path d="M100 42c-4 26-4 60 0 88"/><path d="M120 46c10 20 10 56 0 82"/><path d="M64 60c-8 16-8 36 0 56"/><path d="M136 60c8 16 8 36 0 56"/></g><clipPath id="k-${g}"><ellipse cx="100" cy="86" rx="48" ry="44"/></clipPath>`;
k2 += `<path d="M100 46c-10 18-10 58 0 84M100 46c10 18 10 58 0 84" stroke="${dunkel}" stroke-width="2" fill="none" opacity=".22"/>` + glanz(80, 66, 14, 8) + kelch(100, 46, 1.05);
} else if (s.form === 'flach') {
k2 = `<path d="M100 52c26-6 58 6 58 34s-26 46-58 46-58-18-58-46 32-40 58-34Z" fill="url(#${g})"/>`;
if (s.schulter) k2 += `<ellipse cx="100" cy="62" rx="36" ry="12" fill="${s.schulter}" opacity=".55"/>`;
k2 += `<path d="M76 56c-10 20-8 52 6 72M124 56c10 20 8 52-6 72M100 52v80" stroke="${dunkel}" stroke-width="2" fill="none" opacity=".22"/>` + glanz(74, 74, 14, 7) + kelch(100, 54, 1.1);
} else if (s.form === 'herz') {
k2 = `<path d="M100 134C70 124 50 100 52 74c2-20 20-30 34-24 6 2 10 4 14 6 4-2 8-4 14-6 14-6 32 4 34 24 2 26-18 50-48 60Z" fill="url(#${g})"/><path d="M100 56c-6 24-4 54 0 78" stroke="${dunkel}" stroke-width="2" fill="none" opacity=".22"/>` + glanz(76, 74, 13, 8) + kelch(100, 54, 1.05);
} else if (s.form === 'birne') {
const birne = (x, y, sc, id2) => `<g transform="translate(${x} ${y}) scale(${sc})"><path d="M0 64C-22 64-30 46-26 32-22 20-12 14-10 0-8-12-5-20 0-20s8 8 10 20c2 14 12 20 16 32C30 46 22 64 0 64Z" fill="url(#${g})"/><ellipse cx="-12" cy="34" rx="6" ry="11" fill="#fff" opacity=".3" transform="rotate(14 -12 34)"/>${kelch(0, -20, .7)}</g>`;
k2 = birne(76, 74, .82) + birne(116, 66, 1);
} else if (s.form === 'rispe') {
const p = [[52, 70, 13], [72, 86, 14], [92, 72, 13], [112, 92, 15], [132, 76, 13], [150, 96, 14], [86, 108, 13], [128, 114, 13]];
k2 = `<path d="M34 40C70 46 110 48 168 70" stroke="#3f6b35" stroke-width="3" fill="none" stroke-linecap="round"/>`;
k2 += p.map(([x, y]) => `<path d="M${x} ${y - 12}L${x - 2} ${y - 26}" stroke="#3f6b35" stroke-width="2"/>`).join('');
k2 += p.map(([x, y, r]) => `<circle cx="${x}" cy="${y}" r="${r}" fill="url(#${g})"/><ellipse cx="${x - r * .35}" cy="${y - r * .38}" rx="${r * .32}" ry="${r * .2}" fill="#fff" opacity=".4" transform="rotate(-30 ${x - r * .35} ${y - r * .38})"/><path d="M${x - 5} ${y - r + 1}l5 3 5-3" stroke="#3f6b35" stroke-width="2" fill="none" stroke-linecap="round"/>`).join('');
}
return `<svg viewBox="0 0 200 150" aria-hidden="true">${defs}${schatten}${k2}</svg>`;
}
function packung(s) {
return `<svg viewBox="0 0 400 470" role="img" aria-label="Samentüte ${s.name} von Saatkontor Viertor">
<defs><linearGradient id="kraft" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#dccaa6"/><stop offset="1" stop-color="#c9b48c"/></linearGradient></defs>
<ellipse cx="200" cy="452" rx="150" ry="10" fill="#212121" opacity=".08"/>
<path d="M58 52h284v380a8 8 0 0 1-8 8H66a8 8 0 0 1-8-8Z" fill="url(#kraft)"/>
<path d="M58 52h284l-14 34H72Z" fill="#b9a37a"/>
<path d="M58 52l14 34M342 52l-14 34" stroke="#a88f64" stroke-width="1"/>
<rect x="86" y="108" width="228" height="276" rx="6" fill="#f7f2e8"/>
<g transform="translate(108 128)" fill="none" stroke="#212121" stroke-width="1.6"><path d="M0 18V8a2.5 2.5 0 0 1 5 0v10M7 18V6a2.5 2.5 0 0 1 5 0v12M14 18V6a2.5 2.5 0 0 1 5 0v12M21 18V8a2.5 2.5 0 0 1 5 0v10M-1 18h28"/></g>
<text x="144" y="144" font-family="Instrument Sans, sans-serif" font-size="11" font-weight="600" letter-spacing="1.6" fill="#212121">SAATKONTOR VIERTOR</text>
<svg x="100" y="160" width="200" height="150" viewBox="0 0 200 150">${tomate(s, 'p').replace(/^<svg[^>]*>|<\/svg>$/g, '')}</svg>
<text x="200" y="338" text-anchor="middle" font-family="Young Serif, Georgia, serif" font-size="28" fill="#212121">${s.name.length > 16 ? s.name.split(' ')[1] : s.name}</text>
<text x="200" y="362" text-anchor="middle" font-family="Instrument Sans, sans-serif" font-size="12" fill="#4a4a4a">${s.typ} · etwa 20 Samen · samenfest</text>
<text x="200" y="408" text-anchor="middle" font-family="Instrument Sans, sans-serif" font-size="10.5" letter-spacing="1.4" fill="#4a3d27">NEUBRANDENBURG · CHARGE 26/03</text>
<text x="200" y="424" text-anchor="middle" font-family="Instrument Sans, sans-serif" font-size="10.5" letter-spacing="1.4" fill="#4a3d27">KEIMRATE 92 %</text>
</svg>`;
}
/* ---------- Raster ---------- */
const raster = $('#raster');
raster.innerHTML = SORTEN.map(s => `<li class="karte" data-id="${s.id}"><a href="#detail">
<div class="bild">${tomate(s, 'k')}${s.neu < 9 ? '<span class="plakette">Neu</span>' : s.tage <= 62 ? '<span class="plakette frueh">Früh</span>' : ''}</div>
<div class="karte-text"><h3>${s.name}</h3><p class="preis">${eur(s.preis)}</p><p class="typ">${s.typ} · ${s.tage} Tage</p></div></a></li>`).join('');
const karten = $$('.karte', raster);
function flip(aenderung) {
const alt = new Map(karten.map(k => [k, k.hidden ? null : k.getBoundingClientRect()]));
aenderung();
if (reduziert()) return;
karten.forEach(k => {
const a = alt.get(k);
if (k.hidden || !a) return;
const b = k.getBoundingClientRect(), dx = a.left - b.left, dy = a.top - b.top;
if (Math.abs(dx) < 1 && Math.abs(dy) < 1) return;
k.animate([{ translate: `${dx}px ${dy}px` }, { translate: '0 0' }], { duration: 250, easing: 'cubic-bezier(0.16, 1, 0.3, 1)' });
});
}
/* ---------- Toggle-Gruppe: Filter mit Mehrfachauswahl ---------- */
const gFilter = $('#gruppe-filter'), gSort = $('#gruppe-sort');
const leer = $('#leer'), zurueck = $('#zuruecksetzen');
function filterAnwenden() {
const aktiv = $$('.knopf', gFilter).filter(b => b.getAttribute('aria-pressed') === 'true').map(b => b.dataset.wert);
let n = 0;
flip(() => {
karten.forEach(k => { const ok = aktiv.every(f => byId[k.dataset.id].f.includes(f)); k.hidden = !ok; if (ok) n++; });
});
raster.hidden = n === 0; leer.hidden = n !== 0;
if (n === 0) $('#leer-text').textContent = `${aktiv.map(f => ORT[f]).join(', ').replace(/, ([^,]*)$/, ' und $1')}: diese Kombination schafft keine unserer Sorten. Nimm einen Filter heraus oder setze alle zurück.`;
zurueck.hidden = aktiv.length === 0;
$('#treffer-zahl').textContent = n;
$('#treffer-wort').textContent = `von 8 Sorten`;
}
function filterLeeren() { $$('.knopf', gFilter).forEach(b => b.setAttribute('aria-pressed', 'false')); filterAnwenden(); }
gFilter.addEventListener('click', e => {
const b = e.target.closest('.knopf'); if (!b) return;
b.setAttribute('aria-pressed', b.getAttribute('aria-pressed') === 'true' ? 'false' : 'true');
filterAnwenden();
});
zurueck.addEventListener('click', () => { filterLeeren(); $('.knopf', gFilter).focus(); });
$('#leer-knopf').addEventListener('click', () => { filterLeeren(); $('.knopf', gFilter).focus(); });
/* ---------- Einfachauswahl: Sortierung als Radiogruppe ---------- */
const SORT = {
beliebt: (a, b) => a.beliebt - b.beliebt,
frueh: (a, b) => a.tage - b.tage,
neu: (a, b) => a.neu - b.neu || a.beliebt - b.beliebt,
preis: (a, b) => a.preis - b.preis || a.beliebt - b.beliebt
};
const SORT_NAME = { beliebt: 'Beliebtheit', frueh: 'Erntezeit, früheste zuerst', neu: 'Neuheiten zuerst', preis: 'Preis, günstigste zuerst' };
function sortWaehlen(b, ansagen) {
if (b.getAttribute('aria-checked') === 'true') return;
$$('.knopf', gSort).forEach(x => { const an = x === b; x.setAttribute('aria-checked', an); x.tabIndex = an ? 0 : -1; });
const vergl = SORT[b.dataset.wert];
flip(() => { [...karten].sort((x, y) => vergl(byId[x.dataset.id], byId[y.dataset.id])).forEach(k => raster.appendChild(k)); });
if (ansagen) melde(`Sortiert nach ${SORT_NAME[b.dataset.wert]}.`, true);
}
gSort.addEventListener('click', e => { const b = e.target.closest('.knopf'); if (b) sortWaehlen(b, true); });
/* ---------- Pfeiltasten: roving tabindex ---------- */
function roving(gruppe, auswahlFolgt) {
const knoepfe = () => $$('.knopf', gruppe);
const start = auswahlFolgt ? knoepfe().find(b => b.getAttribute('aria-checked') === 'true') : knoepfe()[0];
knoepfe().forEach(b => b.tabIndex = b === start ? 0 : -1);
gruppe.addEventListener('keydown', e => {
const k = knoepfe(), i = k.indexOf(document.activeElement);
if (i < 0) return;
let j = null;
if (e.key === 'ArrowRight' || e.key === 'ArrowDown') j = (i + 1) % k.length;
else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') j = (i - 1 + k.length) % k.length;
else if (e.key === 'Home') j = 0;
else if (e.key === 'End') j = k.length - 1;
else if (auswahlFolgt && e.key === ' ') { e.preventDefault(); sortWaehlen(k[i], true); return; }
if (j === null) return;
e.preventDefault();
k.forEach(b => b.tabIndex = -1); k[j].tabIndex = 0; k[j].focus();
if (auswahlFolgt) { druckKurz(k[j]); sortWaehlen(k[j], true); }
});
if (!auswahlFolgt) gruppe.addEventListener('focusin', e => { const b = e.target.closest('.knopf'); if (b) knoepfe().forEach(x => x.tabIndex = x === b ? 0 : -1); });
}
roving(gFilter, false);
roving(gSort, true);
/* ---------- Druck: breiter und eckiger, mindestens 150 ms sichtbar ---------- */
function druck(b) { if (b.getAttribute('aria-disabled') === 'true') return; clearTimeout(b._los); b.classList.add('drueckt'); b._t0 = performance.now(); }
function los(b) { const rest = Math.max(0, 150 - (performance.now() - (b._t0 || 0))); clearTimeout(b._los); b._los = setTimeout(() => b.classList.remove('drueckt'), rest); }
function druckKurz(b) { druck(b); los(b); }
$$('.drueckbar').forEach(b => {
b.addEventListener('pointerdown', () => druck(b));
['pointerup', 'pointerleave', 'pointercancel'].forEach(t => b.addEventListener(t, () => los(b)));
b.addEventListener('keydown', e => { if ((e.key === ' ' || e.key === 'Enter') && !e.repeat) druck(b); });
b.addEventListener('keyup', e => { if (e.key === ' ' || e.key === 'Enter') los(b); });
b.addEventListener('blur', () => los(b));
});
/* ---------- Ansage und Zähler ---------- */
const ansage = $('#ansage');
let ansageUhr = null, korb = 1, merk = 0;
function melde(text, still) {
ansage.textContent = '';
clearTimeout(ansageUhr);
requestAnimationFrame(() => {
ansage.textContent = text;
ansage.classList.toggle('zeigen', !still);
ansageUhr = setTimeout(() => ansage.classList.remove('zeigen'), 2800);
});
}
function puls(el) { el.classList.add('puls'); requestAnimationFrame(() => requestAnimationFrame(() => el.classList.remove('puls'))); }
function zaehler() {
const kz = $('#korb-zahl'), mz = $('#merk-zahl');
kz.textContent = korb; mz.textContent = merk;
kz.toggleAttribute('data-null', korb === 0); mz.toggleAttribute('data-null', merk === 0);
$('#korb-link').setAttribute('aria-label', `Warenkorb, ${korb} ${korb === 1 ? 'Artikel' : 'Artikel'}`);
$('#merk-link').setAttribute('aria-label', merk ? `Merkzettel, ${merk} ${merk === 1 ? 'Eintrag' : 'Einträge'}` : 'Merkzettel, leer');
}
/* ---------- Detail ---------- */
let sorte = byId.murmel;
function zeigeSorte(s) {
sorte = s;
$('#packung').innerHTML = packung(s);
$('#detail-titel').textContent = s.name;
$('#krume-name').textContent = s.name;
$('#detail-typ').textContent = `${s.typ} · samenfest`;
const orte = s.f.filter(f => f !== 'frueh').map(f => ORT[f]);
$('#detail-formel').textContent = `Etwa 20 Samen · Ernte nach ${s.tage} Tagen · ${orte.join(' und ')}. ${s.text}`;
$('#detail-preis').textContent = eur(s.preis);
$('#paket-preis').textContent = `${eur(Math.round(s.preis * 3 * 0.9 * 10) / 10)} statt ${eur(s.preis * 3)}`;
const g = $('[data-aktion="geschenk"]'); g.setAttribute('aria-checked', 'false'); $('#geschenk-hinweis').hidden = true;
}
/* ---------- Split-Buttons ---------- */
const POP = typeof HTMLElement.prototype.showPopover === 'function';
const splits = [];
let sim = false; // Vorführung: kein Fokus verschieben
function splitInit(wrap) {
const haupt = $('.split-haupt', wrap), knopf = $('.split-menu', wrap), menue = $('#' + knopf.getAttribute('aria-controls'));
const eintraege = () => $$('.eintrag', menue);
if (!POP) menue.removeAttribute('popover');
let ziel = 0;
const offen = () => POP ? menue.matches(':popover-open') : menue.classList.contains('offen');
function lage() {
const r = knopf.getBoundingClientRect();
const h = menue.offsetHeight || (eintraege().length * 62 + 22);
const rechts = Math.max(12, innerWidth - r.right);
const oben = r.bottom + 8 + h > innerHeight - 8 && r.top - 8 - h > 8;
menue.classList.toggle('oben', oben);
menue.style.right = rechts + 'px';
menue.style.top = (oben ? r.top - 8 - h : r.bottom + 8) + 'px';
}
function setzeOffen(o) {
knopf.setAttribute('aria-expanded', o);
if (o) {
lage();
requestAnimationFrame(lage);
if (!sim && ziel !== null) { const e = eintraege(); (e[ziel < 0 ? e.length - 1 : ziel] || e[0]).focus(); }
} else {
eintraege().forEach(e => e.classList.remove('sim'));
}
}
function oeffnen(z) {
ziel = z;
if (offen()) { setzeOffen(true); return; }
if (POP) { lage(); menue.showPopover(); } else { menue.classList.add('offen'); setzeOffen(true); }
}
function schliessen(fokusZurueck) {
if (!offen()) return;
if (POP) menue.hidePopover(); else { menue.classList.remove('offen'); setzeOffen(false); }
if (fokusZurueck && !sim) knopf.focus();
}
if (POP) {
menue.addEventListener('beforetoggle', e => { if (e.newState === 'open') lage(); });
menue.addEventListener('toggle', e => {
const o = e.newState === 'open';
setzeOffen(o);
if (!o && (document.activeElement === document.body || menue.contains(document.activeElement))) { if (!sim) knopf.focus(); }
});
} else {
document.addEventListener('pointerdown', e => { if (offen() && !menue.contains(e.target) && !knopf.contains(e.target)) schliessen(false); });
}
// Klick: Light-Dismiss schließt schon beim pointerdown, darum den Zustand davor merken
let warOffen = null;
knopf.addEventListener('pointerdown', () => { warOffen = offen(); });
knopf.addEventListener('click', () => {
const war = warOffen === null ? offen() : warOffen; warOffen = null;
if (war) schliessen(false); else oeffnen(0);
});
knopf.addEventListener('keydown', e => {
if (e.key === 'ArrowDown') { e.preventDefault(); druckKurz(knopf); oeffnen(0); }
else if (e.key === 'ArrowUp') { e.preventDefault(); druckKurz(knopf); oeffnen(-1); }
});
menue.addEventListener('keydown', e => {
const ee = eintraege(), i = ee.indexOf(document.activeElement);
let j = null;
if (e.key === 'ArrowDown') j = (i + 1) % ee.length;
else if (e.key === 'ArrowUp') j = (i - 1 + ee.length) % ee.length;
else if (e.key === 'Home') j = 0;
else if (e.key === 'End') j = ee.length - 1;
else if (e.key === 'Escape') { e.preventDefault(); e.stopPropagation(); schliessen(true); return; }
else if (e.key === 'Tab') { schliessen(false); return; }
else if (e.key.length === 1 && /\S/.test(e.key)) {
const t = e.key.toLowerCase(), r = [...ee.slice(i + 1), ...ee.slice(0, i + 1)].find(x => x.textContent.trim().toLowerCase().startsWith(t));
if (r) { e.preventDefault(); r.focus(); }
return;
}
if (j === null) return;
e.preventDefault(); ee[j].focus();
});
menue.addEventListener('click', e => {
const it = e.target.closest('.eintrag'); if (!it) return;
aktion(it, haupt);
schliessen(true);
});
addEventListener('scroll', () => { if (offen()) lage(); }, { passive: true, capture: true });
addEventListener('resize', () => { if (offen()) lage(); });
haupt.addEventListener('click', () => {
if (haupt.dataset.zustand) return;
if (haupt.getAttribute('aria-disabled') === 'true') { melde('Ausverkauft. Wieder lieferbar ab 14. November. Im Menü daneben kannst du dich benachrichtigen lassen.'); return; }
kaufen(haupt, 1);
});
const api = { wrap, haupt, knopf, menue, eintraege, oeffnen, schliessen, offen };
splits.push(api);
return api;
}
const TEXT_HAUPT = new WeakMap();
const ICON_KORB = '<svg viewBox="0 0 24 24" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linejoin="round"><path d="M5 8h14l-1.2 11.2a1 1 0 0 1-1 .8H7.2a1 1 0 0 1-1-.8Z"/><path d="M9 8V6.5a3 3 0 0 1 6 0V8"/></svg>';
const ICON_LADEN = '<svg viewBox="0 0 24 24" aria-hidden="true" class="kreisel" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><path d="M12 4a8 8 0 1 1-8 8" /></svg>';
const ICON_HAKEN = '<svg viewBox="0 0 24 24" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="m5 12.5 4.5 4.5L19 7.5"/></svg>';
const uhren = new Set();
const spaeter = (fn, ms) => { const t = setTimeout(() => { uhren.delete(t); fn(); }, ms); uhren.add(t); return t; };
function hauptZustand(haupt, z) {
const text = $('.split-text', haupt);
if (!TEXT_HAUPT.has(haupt)) TEXT_HAUPT.set(haupt, text.textContent);
const icon = haupt.querySelector('svg');
if (z === 'laedt') { haupt.dataset.zustand = 'laedt'; haupt.setAttribute('aria-disabled', 'true'); text.textContent = 'Wird hinzugefügt …'; icon.outerHTML = ICON_LADEN; }
else if (z === 'fertig') { haupt.dataset.zustand = 'fertig'; text.textContent = 'Liegt im Korb'; haupt.querySelector('svg').outerHTML = ICON_HAKEN; }
else { delete haupt.dataset.zustand; haupt.removeAttribute('aria-disabled'); text.textContent = TEXT_HAUPT.get(haupt); haupt.querySelector('svg').outerHTML = ICON_KORB; }
}
function kaufen(haupt, menge, name) {
const wer = name || (haupt.dataset.produkt === 'duenger' ? 'Tomatendünger' : sorte.name);
hauptZustand(haupt, 'laedt');
spaeter(() => {
korb += menge; zaehler(); puls($('#korb-zahl'));
hauptZustand(haupt, 'fertig');
melde(`${wer} liegt im Korb. ${korb} Artikel im Warenkorb.`);
spaeter(() => hauptZustand(haupt, null), 1800);
}, 700);
}
function aktion(it, haupt) {
const a = it.dataset.aktion;
if (a === 'geschenk') {
const an = it.getAttribute('aria-checked') !== 'true';
it.setAttribute('aria-checked', an);
$('#geschenk-hinweis').hidden = !an;
melde(an ? 'Wird als Geschenk verpackt, plus 1,50 €.' : 'Geschenkverpackung entfernt.');
} else if (a === 'paket') {
korb += 3; zaehler(); puls($('#korb-zahl'));
melde(`Drei Tüten ${sorte.name} zum Paketpreis im Korb. ${korb} Artikel im Warenkorb.`);
} else if (a === 'duenger-zwei') {
korb += 2; zaehler(); puls($('#korb-zahl'));
melde(`Zwei Beutel Tomatendünger im Korb. ${korb} Artikel im Warenkorb.`);
} else if (a === 'merken') {
merk += 1; zaehler(); puls($('#merk-zahl'));
melde('Auf dem Merkzettel gespeichert.');
} else if (a === 'erinnern') {
melde(`Erinnerung vorgemerkt: Mitte März schreiben wir dir, dass ${sorte.name} jetzt gesät werden kann.`);
} else if (a === 'melden') {
melde('Wir schreiben dir eine E-Mail, sobald die Anzuchttöpfe wieder da sind.');
}
}
$$('.split').forEach(splitInit);
const kauf = splits[0];
/* ---------- Karte öffnet Produktseite ---------- */
raster.addEventListener('click', e => {
const a = e.target.closest('.karte a'); if (!a) return;
e.preventDefault();
zeigeSorte(byId[a.closest('.karte').dataset.id]);
setzeVariante('split', true);
$('#detail-titel').setAttribute('tabindex', '-1'); $('#detail-titel').focus({ preventScroll: true });
scrollTo({ top: 0 });
});
$('#zur-liste').addEventListener('click', e => { e.preventDefault(); setzeVariante(letzteListe, true); $('#liste-titel').setAttribute('tabindex', '-1'); $('#liste-titel').focus({ preventScroll: true }); });
/* ---------- Varianten ---------- */
const WERTE = {
toggle: [['Rolle', 'role=toolbar, aria-pressed'], ['Fuge', '2 px, verbunden'], ['Ecken', 'innen 8 → gewählt 24 px'], ['Druck', '+15 % Breite, innen 4 px, 150 ms'], ['Form', '220 ms cubic-bezier(0.16, 1, 0.3, 1)'], ['Umordnen', 'FLIP 250 ms, nur Position']],
einzeln: [['Rolle', 'role=radiogroup, aria-checked'], ['Fuge', '2 px, verbunden'], ['Ecken', 'innen 8 → gewählt 24 px'], ['Druck', '+15 % Breite, innen 4 px, 150 ms'], ['Auswahl', 'folgt dem Fokus (Pfeile)'], ['Umordnen', 'FLIP 250 ms, nur Position']],
split: [['Rolle', 'button + aria-haspopup=menu'], ['Größen', 'L 56 px, S 44 px, Fuge 2 px'], ['Ecken', 'innen 6 → Druck 14 px, offen rund'], ['Pfeil', '180°, 250 ms'], ['Menü', 'auf 250 ms (0.16, 1, 0.3, 1), zu 160 ms (0.7, 0, 0.84, 0)'], ['Kaufen', 'laden 700 ms, Erfolg 1800 ms']]
};
const TASTEN = {
toggle: '<kbd>Tab</kbd> in die Gruppe · <kbd>←</kbd> <kbd>→</kbd> <kbd>Pos1</kbd> <kbd>Ende</kbd> wandern · <kbd>Leertaste</kbd> schaltet an und aus',
einzeln: '<kbd>Tab</kbd> auf die Auswahl · <kbd>←</kbd> <kbd>→</kbd> wählen mit Umlauf · Fokus und Auswahl bleiben gekoppelt',
split: '<kbd>Enter</kbd> kauft · Menü-Knopf: <kbd>Enter</kbd> oder <kbd>↓</kbd> öffnet, <kbd>↑</kbd> öffnet am Ende · <kbd>Esc</kbd> schließt und gibt den Fokus zurück'
};
const VARIANTEN = ['toggle', 'einzeln', 'split'];
const v = new URLSearchParams(location.search).get('v');
let variante = VARIANTEN.includes(v) ? v : 'toggle';
let letzteListe = variante === 'split' ? 'toggle' : variante;
function setzeVariante(neu, url) {
variante = neu;
if (neu !== 'split') letzteListe = neu;
splits.forEach(s => s.schliessen(false));
$('#liste').hidden = neu === 'split';
$('#detail').hidden = neu !== 'split';
$('#feld-filter').hidden = neu !== 'toggle';
$('#feld-sort').hidden = neu !== 'einzeln';
$$('.pult-wahl button').forEach(b => b.setAttribute('aria-pressed', b.dataset.v === neu));
$('#pult-werte').innerHTML = WERTE[neu].map(([k, w]) => `<div><dt>${k}</dt><dd>${w}</dd></div>`).join('');
$('#pult-tasten').innerHTML = TASTEN[neu];
if (url) {
const q = new URLSearchParams(location.search); q.set('v', neu);
history.replaceState(null, '', location.pathname + '?' + q.toString() + location.hash);
}
}
$$('.pult-wahl button').forEach(b => b.addEventListener('click', () => { pause(); setzeVariante(b.dataset.v, true); }));
/* ---------- Vorführung ---------- */
const zeiger = $('#zeiger');
let lauf = 0;
const halt = Symbol('halt');
function warte(ms, id) { return new Promise((ok, nein) => spaeter(() => id === lauf ? ok() : nein(halt), ms)); }
async function zeigeAuf(el, id) {
const r = el.getBoundingClientRect();
zeiger.style.translate = `${Math.round(r.left + r.width * .74)}px ${Math.round(r.top + r.height * .7)}px`;
await warte(reduziert() ? 200 : 700, id);
}
async function tippe(el, id, ohneKlick) {
await zeigeAuf(el, id);
zeiger.classList.add('tippt'); druck(el);
await warte(170, id);
zeiger.classList.remove('tippt'); los(el);
if (!ohneKlick) el.click();
}
function grundzustand() {
uhren.forEach(clearTimeout); uhren.clear();
splits.forEach(s => { s.schliessen(false); if (s.haupt.dataset.zustand) hauptZustand(s.haupt, null); });
$$('.knopf', gFilter).forEach(b => b.setAttribute('aria-pressed', 'false'));
karten.forEach(k => k.hidden = false);
$$('.knopf', gSort).forEach((b, i) => { b.setAttribute('aria-checked', i === 0); b.tabIndex = i === 0 ? 0 : -1; });
[...karten].sort((x, y) => SORT.beliebt(byId[x.dataset.id], byId[y.dataset.id])).forEach(k => raster.appendChild(k));
filterAnwenden();
zeigeSorte(sorte);
korb = 1; merk = 0; zaehler();
ansage.classList.remove('zeigen'); ansage.textContent = '';
$$('.knopf, .split-haupt, .split-menu').forEach(b => b.classList.remove('drueckt'));
}
async function szene(id) {
const fk = n => $(`.knopf[data-wert="${n}"]`, gFilter), sk = n => $(`.knopf[data-wert="${n}"]`, gSort);
zeiger.style.translate = `${Math.round(innerWidth * .62)}px ${Math.round(innerHeight * .9)}px`;
await warte(500, id);
for (;;) {
if (variante === 'toggle') {
await tippe(fk('balkon'), id); await warte(1100, id);
await tippe(fk('frueh'), id); await warte(1100, id);
await tippe(fk('glashaus'), id); await warte(1500, id);
await tippe($('#leer-knopf'), id); await warte(900, id);
await tippe(fk('glashaus'), id); await warte(1300, id);
await tippe(fk('glashaus'), id); await warte(1000, id);
} else if (variante === 'einzeln') {
await tippe(sk('frueh'), id); await warte(1400, id);
await tippe(sk('preis'), id); await warte(1400, id);
await tippe(sk('neu'), id); await warte(1400, id);
await tippe(sk('beliebt'), id); await warte(1400, id);
} else {
await tippe(kauf.haupt, id); await warte(2700, id);
await tippe(kauf.knopf, id); await warte(800, id);
const ee = kauf.eintraege();
for (const e of ee.slice(0, 2)) { await zeigeAuf(e, id); ee.forEach(x => x.classList.toggle('sim', x === e)); await warte(350, id); }
await tippe(ee[0], id); await warte(1500, id);
await tippe(kauf.knopf, id); await warte(800, id);
await zeigeAuf(ee[2], id); ee.forEach(x => x.classList.toggle('sim', x === ee[2])); await warte(400, id);
await tippe(ee[2], id); await warte(1500, id);
await tippe(kauf.knopf, id); await warte(500, id);
await tippe(ee[0], id); await warte(1200, id);
}
}
}
function neustart() {
pause();
grundzustand();
setzeVariante(variante, false);
}
function vorfuehren() {
neustart();
sim = true;
html.classList.add('vorfuehrung');
const id = ++lauf;
szene(id).catch(e => { if (e !== halt) console.error(e); });
}
function pause() {
lauf++;
uhren.forEach(clearTimeout); uhren.clear();
sim = false;
html.classList.remove('vorfuehrung');
splits.forEach(s => { if (s.haupt.dataset.zustand) hauptZustand(s.haupt, null); });
$$('.eintrag.sim').forEach(x => x.classList.remove('sim'));
}
function weiter() { vorfuehren(); }
// echte Eingabe beendet die Vorführung
addEventListener('pointerdown', e => { if (e.isTrusted && html.classList.contains('vorfuehrung') && !html.classList.contains('vorschau')) pause(); }, true);
addEventListener('keydown', e => { if (e.isTrusted && html.classList.contains('vorfuehrung')) pause(); }, true);
zeigeSorte(sorte);
zaehler();
setzeVariante(variante, false);
filterAnwenden();
})();
</script>
</body>
</html>