Zum Inhalt
← Sektionen & Funktionen

Wochen-Kursplan mit Live-Status

Wiederkehrender Wochenplan als Raster aus Tagen und Stunden, mobil mit Tages-Reitern, mit Filtern, „läuft gerade“ und „als Nächstes“ in Ortszeit, Ausfällen am Termin, Probetraining je Termin und .ics je Kurs.

Turn- und Sportverein mit Abteilungen, Hallen und Altersgruppen, Probetraining ohne Buchungssystem.

Ein wiederkehrender Wochenplan für Kurse und Trainingsgruppen. Auf dem Desktop ist er ein Raster aus Wochentagen und Uhrzeiten, auf dem Telefon eine Leiste mit Tages-Reitern. Filter nach Kurs oder Abteilung, Trainer:in, Niveau oder Alter und Halle bzw. Raum wirken auf beide Ansichten. Oben steht in Ortszeit, was gerade läuft und was als Nächstes beginnt. Jeder Termin nennt freie Plätze als Text, Ausfälle und Vertretungen stehen direkt am Termin. Je Termin gibt es Probetraining oder Anmeldung, je Kurs eine .ics-Datei mit Wiederholungsregel und Ausnahmen.

Wer einen Kurs sucht, fragt zuerst: An welchem Tag, um wie viel Uhr, wo und für wen? Das Raster beantwortet das auf einen Blick, die Filter machen aus 40 Gruppen die drei passenden. „Läuft gerade“ und „als Nächstes“ helfen kurz vor dem Losgehen. Ausfälle am Termin selbst ersparen den Blick in Messenger-Gruppen. Die Kalenderdatei mit Wiederholung macht aus Interesse einen festen Wochentermin.

Wann einsetzen

Sportverein mit Abteilungen und Hallenzeiten, Fitness-, Yoga- oder Pilatesstudio, Tanzschule, Kampfsportschule, Schwimmschule, Physio- und Präventionskurse, Volkshochschule mit festen Wochenkursen, Theorieunterricht der Fahrschule. Immer dann, wenn sich die Termine jede Woche wiederholen.

Wann nicht

Für Einzeltermine wie Konzerte, Workshops oder Tage der offenen Tür ist programm-kalender da (Liste und Monat). Bei weniger als etwa acht Wochenterminen reicht eine Tabelle oder Liste. Liefert eine Buchungssoftware schon einen gepflegten Plan, die Sektion nur als Teaser mit „läuft gerade“ und Link bauen, statt Daten doppelt zu pflegen.

Varianten

  • Sportverein: Abteilungen, Hallen, Altersgruppen, Probetraining, Hallenpause in den Ferien
  • Fitness- oder Yogastudio: Kursarten, Räume, Niveau, Probestunde und Warteliste
  • Teaser „Heute im Studio“: nur läuft gerade und die nächsten drei Termine mit Link zum Plan
  • Ein Raum als Belegungsplan (Halle, Studio), nur lesbar ohne Anmeldung

Umsetzung

Daten als Liste wiederkehrender Kurse (id, wochentag, beginn, dauer, rubrik, titel, trainer, gruppe, halle, plaetze, belegt, anmeldung, info) plus Ausnahmen je Woche oder Datum (fällt aus, Vertretung, anderer Raum) und Ferienpause. Ein Filterzustand für beide Ansichten.

Desktop: CSS-Grid mit Zeitspalte und 7 Tagesspalten. Zeilen sind nur die Stunden, in denen nach dem Filter etwas beginnt; laufen an einem Tag zwei Kurse in derselben Stunde, bekommt diese Stunde zwei Unterzeilen. Jede Tagesspalte ist ein <section> mit Überschrift und <ol>, beide mit grid-template-rows: subgrid. So bleibt die Lese- und Tab-Reihenfolge Tag für Tag, die Karten stehen trotzdem in gemeinsamen Stundenzeilen. Spalten explizit setzen (grid-column), sonst schiebt die Auto-Platzierung die Tage in implizite Spalten.

Telefon: dieselben Abschnitte als Tab-Panels, darüber eine Tab-Leiste Mo bis So mit Datum. Pfeiltasten mit Umlauf, Pos1 und Ende wie in tabs-pfeiltasten-focusgroup.

Live-Status wie in oeffnungszeiten-status: Ortszeit über Intl.DateTimeFormat mit timeZone 'Europe/Berlin' und formatToParts, Datumsrechnung in UTC. Neu rechnen zur vollen Minute und bei visibilitychange. Screenreader hören nur Wechsel.

Filter und Wochenwechsel per document.startViewTransition, jede Karte mit view-transition-name je Kurs.

Termin öffnet ein natives <dialog> mit allen Angaben, Status (Ausfall, Vertretung, Raum) und dem Formular für Probetraining oder Warteliste.

ICS je Kurs: VEVENT mit RRULE:FREQ=WEEKLY;BYDAY=…;UNTIL=… (Saisonende, UNTIL in UTC), EXDATE;TZID=Europe/Berlin für Ausfälle und Ferienpause, Vertretung und Raumwechsel als zweite VEVENT mit gleicher UID und RECURRENCE-ID. VTIMEZONE, CRLF, Faltung nach 75 Oktetten, Blob mit <a download>.

Im Einsatz: Plan und Ausnahmen aus CMS oder Vereinsverwaltung, Ausnahmen mit echtem Datum statt Wochen-Versatz, Filter in der URL (?abteilung=turnen&halle=glacis), Anmeldung an das Buchungssystem oder per Mail an die Übungsleitung.

Robust umsetzen

Telefon. Tages-Reiter Mo bis So mit Datum, heute vorgewählt. Darunter die Termine des Tages als Liste mit Uhrzeit links und großen Zielen. Filter klappen hinter einem Knopf „Filter“ mit Zähler ein.

Ohne Bewegung. Kein Einblenden und keine Neuordnung per Übergang; Raster, Reiter und Dialog wechseln sofort. Zustände und Texte bleiben gleich.

Barrierefrei. Status nie nur über Farbe: „läuft“, „als Nächstes“, „fällt aus“, „Vertretung“ und freie Plätze stehen als Text. Tagesspalten als Abschnitte mit Überschrift (Wochentag und Datum), Termine als Listen. Mobil echtes Tab-Muster mit Pfeiltasten. Filter als Schalter mit aria-pressed bzw. native select, Trefferzahl in aria-live. Termin-Knöpfe nennen Kurs, Tag und Uhrzeit im zugänglichen Namen. Dialog nativ mit Fokus auf der Überschrift und Rückkehr zum Termin. Live-Status sagt nur Wechsel an, nicht jede Minute.

Code

<!DOCTYPE html>
<!--
  Wochen-Kursplan mit Live-Status
  Fundus · Sektionen & Funktionen · https://cecodesign.de/lab/fundus/sektionen/wochen-kursplan/
  Eigenständige Datei. Bilder liegen hier unter bilder/ – eigene einsetzen.
-->
<html lang="de" data-v="verein">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Wochen-Kursplan mit Live-Status · 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: 'Big Shoulders'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/big-shoulders:vf@latest/latin-wght-normal.woff2') format('woff2-variations'); font-weight: 100 900; font-display: swap; }
@font-face { font-family: 'Archivo'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/archivo:vf@latest/latin-full-normal.woff2') format('woff2-variations'); font-weight: 100 900; font-stretch: 62% 125%; font-display: swap; }
@font-face { font-family: 'Bricolage Grotesque'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/bricolage-grotesque:vf@latest/latin-opsz-normal.woff2') format('woff2-variations'); font-weight: 200 800; font-display: swap; }
@font-face { font-family: 'Onest'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/onest:vf@latest/latin-wght-normal.woff2') format('woff2-variations'); font-weight: 100 900; font-display: swap; }
@font-face { font-family: 'Shantell Sans'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/shantell-sans:vf@latest/latin-full-normal.woff2') format('woff2-variations'); font-weight: 300 800; font-display: swap; }
@font-face { font-family: 'Geist Mono'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/geist-mono:vf@latest/latin-wght-normal.woff2') format('woff2-variations'); font-weight: 100 900; font-display: swap; }

/* ---------- Token: Variante Sportverein (Rezept verein-nonprofit-wirkung) ---------- */
:root {
  --grund: #ffffff; --flaeche: #edf5fa; --warm: #f3f4f1; --karte: #ffffff;
  --text: #222520; --leise: #5c6066; --linie: rgba(34, 37, 32, .13); --linie-stark: rgba(34, 37, 32, .28);
  --marke: #002e6d; --marke-tief: #001f4d; --marke-hell: #dbebf5;
  --cta: #ffca0a; --cta-text: #222520; --cta-hover: #f2bd00;
  --knapp: #9a3412; --aus: #b42318; --aus-flaeche: #fff4f2;
  --naechstes: #dbebf5; --naechstes-text: #002e6d;
  --f-kopf: 'Big Shoulders', 'Archivo', system-ui, sans-serif; --f-text: 'Archivo', system-ui, sans-serif; --f-notiz: 'Archivo', system-ui, sans-serif;
  --kopf-gewicht: 800; --kopf-versal: uppercase; --kopf-laufweite: .005em;
  --kurve: cubic-bezier(0.645, 0.045, 0.355, 1);   /* easeInOutCubic, Rezept verein */
  --ui: 180ms; --neu: 600ms;
  --rund: 4px; --rund-gross: 6px;
  --rand: clamp(16px, 3.4vw, 48px);
}
/* ---------- Token: Variante Yogastudio (Rezept kurs-coaching-lernweg) ---------- */
[data-v="studio"] {
  --grund: #f4f6fa; --flaeche: #ffffff; --warm: #eaedf4; --karte: #ffffff;
  --text: #141414; --leise: #555b66; --linie: rgba(20, 20, 20, .1); --linie-stark: rgba(20, 20, 20, .24);
  --marke: #3f4fc4; --marke-tief: #2f3da3; --marke-hell: #e6e9fb;
  --cta: #3f4fc4; --cta-text: #ffffff; --cta-hover: #2f3da3;
  --naechstes: #f7c325; --naechstes-text: #141414;
  --f-kopf: 'Bricolage Grotesque', 'Onest', system-ui, sans-serif; --f-text: 'Onest', system-ui, sans-serif; --f-notiz: 'Shantell Sans', 'Onest', cursive;
  --kopf-gewicht: 700; --kopf-versal: none; --kopf-laufweite: -.02em;
  --kurve: cubic-bezier(0.22, 1, 0.36, 1);   /* easeOutQuint, Rezept kurs */
  --ui: 150ms; --neu: 500ms;
  --rund: 14px; --rund-gross: 22px;
}
* { box-sizing: border-box; margin: 0; }
html { background: var(--grund); color: var(--text); font-family: var(--f-text); -webkit-font-smoothing: antialiased; }
body { min-height: 100svh; font-size: 1.0625rem; line-height: 1.55; padding-bottom: 110px; overflow-x: clip; }
button, input, select { font: inherit; color: inherit; }
a { color: inherit; }
[hidden] { display: none !important; }
.sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
.zahl { font-variant-numeric: tabular-nums; }
.label { font: 600 .75rem/1.2 var(--f-text); letter-spacing: .1em; text-transform: uppercase; color: var(--leise); }
:focus-visible { outline: 3px solid var(--marke); outline-offset: 2px; }
[data-v="verein"] [data-nur="studio"], [data-v="studio"] [data-nur="verein"] { display: none !important; }

/* ---------- Kopf ---------- */
.kopf { display: flex; align-items: center; justify-content: space-between; gap: 20px; padding: 14px var(--rand); border-bottom: 1px solid var(--linie); background: var(--grund); }
.marke { display: flex; align-items: center; gap: 12px; text-decoration: none; min-width: 0; }
.marke svg { width: 42px; height: 42px; flex: none; }
.marke b { display: block; font: var(--kopf-gewicht) 1.375rem/1 var(--f-kopf); text-transform: var(--kopf-versal); letter-spacing: var(--kopf-laufweite); color: var(--marke); }
[data-v="studio"] .marke b { color: var(--text); font-size: 1.3rem; }
.marke span { display: block; font-size: .8125rem; color: var(--leise); margin-top: 3px; }
.kopf nav ul { list-style: none; padding: 0; display: flex; gap: clamp(14px, 2vw, 30px); font-size: .9375rem; font-weight: 500; }
.kopf nav a { text-decoration: none; }
.kopf nav a[aria-current] { text-decoration: underline; text-decoration-thickness: 2px; text-underline-offset: 7px; }
.knopf { display: inline-flex; align-items: center; justify-content: center; gap: 8px; min-height: 44px; padding: 0 20px; border: 0; border-radius: var(--rund); background: var(--marke); color: #fff; font-weight: 600; font-size: .9375rem; text-decoration: none; cursor: pointer; transition: background-color var(--ui) var(--kurve), box-shadow var(--ui) var(--kurve); }
.knopf:hover { background: var(--marke-tief); }
.knopf--cta { background: var(--cta); color: var(--cta-text); }
.knopf--cta:hover { background: var(--cta-hover); }
.knopf--linie { background: transparent; color: var(--marke); box-shadow: inset 0 0 0 1.5px currentColor; }
.knopf--linie:hover { background: var(--marke-hell); }
.knopf svg { width: 18px; height: 18px; flex: none; }
[data-v="studio"] .knopf { border-radius: 999px; }

/* ---------- Einstieg und Live-Status ---------- */
.plan { padding: clamp(24px, 4vw, 56px) var(--rand) 0; }
.einstieg { display: grid; grid-template-columns: minmax(0, 1.05fr) minmax(0, 1fr); gap: clamp(20px, 4vw, 64px); align-items: end; }
.einstieg h1 { font: var(--kopf-gewicht) clamp(2.75rem, 1.6rem + 5vw, 6rem)/.92 var(--f-kopf); text-transform: var(--kopf-versal); letter-spacing: var(--kopf-laufweite); color: var(--marke); margin-top: 10px; }
[data-v="studio"] .einstieg h1 { color: var(--text); font-size: clamp(2.5rem, 1.4rem + 3.4vw, 4.25rem); line-height: 1.02; font-variation-settings: "opsz" 96; }
.einstieg h1 em { font-style: normal; color: var(--text); }
[data-v="studio"] .einstieg h1 em { color: var(--marke); }
.einstieg .lead { max-width: 52ch; margin-top: 14px; color: var(--text); }
.einstieg .lead b { font-weight: 600; }

.jetzt { border-radius: var(--rund-gross); background: var(--marke); color: #fff; padding: 18px 20px 16px; display: grid; gap: 12px; min-height: 172px; align-content: start; }
[data-v="studio"] .jetzt { background: var(--karte); color: var(--text); box-shadow: 0 1px 0 var(--linie), 0 18px 40px -24px rgba(20, 20, 60, .35); }
.jetzt__kopf { display: flex; justify-content: space-between; gap: 12px; align-items: baseline; font-size: .8125rem; color: #c9dcef; }
[data-v="studio"] .jetzt__kopf { color: var(--leise); }
.jetzt__kopf b { font: 600 .75rem/1 var(--f-text); letter-spacing: .1em; text-transform: uppercase; color: #fff; }
[data-v="studio"] .jetzt__kopf b { color: var(--text); }
.jetzt__spalten { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
.jetzt h2 { font: 600 .75rem/1.2 var(--f-text); letter-spacing: .1em; text-transform: uppercase; color: #c9dcef; margin-bottom: 6px; display: flex; align-items: center; gap: 8px; }
[data-v="studio"] .jetzt h2 { color: var(--leise); }
.jetzt ul { list-style: none; padding: 0; display: grid; gap: 8px; }
.jetzt li { line-height: 1.3; }
.jetzt li button { all: unset; cursor: pointer; display: block; border-radius: 3px; }
.jetzt li button:focus-visible { outline: 3px solid var(--cta); outline-offset: 3px; }
[data-v="studio"] .jetzt li button:focus-visible { outline-color: var(--marke); }
.jetzt li strong { display: block; font: 700 1.25rem/1.05 var(--f-kopf); text-transform: var(--kopf-versal); letter-spacing: var(--kopf-laufweite); }
[data-v="studio"] .jetzt li strong { font-size: 1.125rem; font-weight: 650; }
.jetzt li small { display: block; font-size: .8125rem; color: #c9dcef; margin-top: 2px; }
[data-v="studio"] .jetzt li small { color: var(--leise); }
.jetzt li button:hover strong { text-decoration: underline; text-underline-offset: 3px; }
.jetzt .ruhig { font-size: .9375rem; color: #c9dcef; }
[data-v="studio"] .jetzt .ruhig { color: var(--leise); font-family: var(--f-notiz); }
.puls { width: 9px; height: 9px; border-radius: 50%; background: #4ade80; box-shadow: 0 0 0 3px rgba(74, 222, 128, .25); flex: none; }
[data-v="studio"] .puls { background: #0b7a4f; box-shadow: 0 0 0 3px rgba(11, 122, 79, .18); }
.jetzt__fuss { font-size: .75rem; color: #c9dcef; border-top: 1px solid rgba(255, 255, 255, .16); padding-top: 9px; }
[data-v="studio"] .jetzt__fuss { color: var(--leise); border-color: var(--linie); }

/* ---------- Werkzeuge: Woche und Filter ---------- */
.werkzeuge { margin-top: clamp(22px, 3vw, 36px); padding: 14px 0; border-top: 2px solid var(--marke); border-bottom: 1px solid var(--linie); display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 14px 28px; align-items: center; }
[data-v="studio"] .werkzeuge { border-top: 0; border-bottom: 0; background: var(--karte); border-radius: var(--rund-gross); padding: 14px 16px; }
.woche { border: 0; padding: 0; display: inline-grid; grid-auto-flow: column; background: var(--warm); border-radius: var(--rund); padding: 3px; position: relative; }
.woche legend { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); }
.woche label { position: relative; z-index: 1; display: grid; padding: 7px 14px; cursor: pointer; border-radius: calc(var(--rund) - 1px); font-size: .875rem; font-weight: 600; line-height: 1.15; white-space: nowrap; transition: color var(--ui) var(--kurve); }
.woche label small { font-weight: 400; font-size: .75rem; color: var(--leise); }
.woche input { position: absolute; opacity: 0; inset: 0; cursor: pointer; margin: 0; }
.woche label:has(input:checked) { color: #fff; }
.woche label:has(input:checked) small { color: #dbe6f4; }
.woche label:has(input:focus-visible) { outline: 3px solid var(--marke); outline-offset: 2px; }
.woche__pille { position: absolute; z-index: 0; top: 3px; bottom: 3px; left: 0; border-radius: calc(var(--rund) - 1px); background: var(--marke); transition: translate var(--ui) var(--kurve), width var(--ui) var(--kurve); }
.filter-knopf { display: none; }
.filter { display: flex; flex-wrap: wrap; align-items: center; gap: 10px 14px; min-width: 0; }
.chips { display: flex; flex-wrap: wrap; gap: 6px; border: 0; padding: 0; }
.chips legend { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); }
.chip { display: inline-flex; align-items: center; gap: 7px; min-height: 36px; padding: 0 12px; border-radius: var(--rund); border: 1px solid var(--linie-stark); background: var(--karte); cursor: pointer; font-size: .875rem; font-weight: 500; transition: background-color var(--ui) var(--kurve), border-color var(--ui) var(--kurve), color var(--ui) var(--kurve); }
[data-v="studio"] .chip { border-radius: 999px; }
.chip i { width: 9px; height: 9px; border-radius: 2px; background: var(--farbe); flex: none; }
[data-v="studio"] .chip i { border-radius: 50%; }
.chip:hover { border-color: var(--text); }
.chip[aria-pressed="true"] { background: var(--text); border-color: var(--text); color: var(--grund); }
.chip[aria-pressed="true"] i { box-shadow: 0 0 0 1.5px var(--grund); }
.chip small { font-size: .75rem; color: var(--leise); }
.chip[aria-pressed="true"] small { color: inherit; opacity: .8; }
.wahl { display: flex; flex-wrap: wrap; gap: 8px; }
.wahl label { display: grid; gap: 2px; font-size: .6875rem; font-weight: 600; letter-spacing: .08em; text-transform: uppercase; color: var(--leise); }
.wahl select { appearance: none; min-height: 36px; padding: 0 30px 0 10px; border-radius: var(--rund); border: 1px solid var(--linie-stark); background: var(--karte) url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'%3E%3Cpath d='M3 4.5 6 7.5 9 4.5' fill='none' stroke='%235c6066' stroke-width='1.5'/%3E%3C/svg%3E") no-repeat right 9px center / 12px; font-size: .875rem; font-weight: 500; letter-spacing: 0; text-transform: none; color: var(--text); cursor: pointer; max-width: 15rem; }
[data-v="studio"] .wahl select { border-radius: 10px; }
.wahl select:hover { border-color: var(--text); }
.wahl select.aktiv { border-color: var(--text); box-shadow: inset 0 0 0 1px var(--text); }
.treffer { display: flex; align-items: center; gap: 12px; margin-left: auto; font-size: .875rem; color: var(--leise); white-space: nowrap; }
.treffer b { color: var(--text); font-weight: 600; }
.zuruecksetzen { border: 0; background: none; padding: 6px 2px; cursor: pointer; font-size: .875rem; font-weight: 600; color: var(--marke); text-decoration: underline; text-underline-offset: 4px; }
.zuruecksetzen[hidden] { display: none; }

/* ---------- Tages-Reiter (Telefon) ---------- */
.reiter { display: none; }
.reiter[hidden], .raster[hidden] { display: none !important; }

/* ---------- Wochenraster (Desktop) ---------- */
.raster { position: relative; display: grid; grid-template-columns: 4.25rem repeat(7, minmax(0, 1fr)); column-gap: 6px; margin-top: 6px; }
.raster__zeile { grid-column: 1 / -1; border-top: 1px solid var(--linie); pointer-events: none; }
.zeitachse { grid-column: 1; grid-row: 1 / -1; display: grid; grid-template-rows: subgrid; }
.zeitachse span { padding-top: 10px; font: 600 .8125rem/1 var(--f-text); font-variant-numeric: tabular-nums; color: var(--leise); }
.tag { grid-row: 1 / -1; display: grid; grid-template-rows: subgrid; min-width: 0; }
.tag__kopf { position: sticky; top: 0; z-index: 2; display: flex; align-items: baseline; gap: 6px; padding: 12px 2px 10px; background: var(--grund); border-bottom: 2px solid var(--text); font: 600 .9375rem/1 var(--f-text); }
.tag__kopf .wt { font: var(--kopf-gewicht) 1.375rem/1 var(--f-kopf); text-transform: var(--kopf-versal); letter-spacing: var(--kopf-laufweite); }
[data-v="studio"] .tag__kopf .wt { font-size: 1.125rem; }
.tag__kopf .wt-k { display: none; }
.tag__kopf .dt { color: var(--leise); font-weight: 500; font-variant-numeric: tabular-nums; font-size: .875rem; }
.tag__kopf .heute { margin-left: auto; font-size: .6875rem; font-weight: 700; letter-spacing: .08em; text-transform: uppercase; color: #fff; background: var(--marke); padding: 3px 6px; border-radius: 3px; }
[data-v="studio"] .tag__kopf .heute { border-radius: 999px; }
.tag.ist-heute .tag__kopf { border-bottom-color: var(--marke); color: var(--marke); }
.tag.ist-vorbei .tag__kopf .wt, .tag.ist-vorbei .tag__kopf { color: var(--leise); border-bottom-color: var(--linie-stark); }
.tag__liste { grid-row: 2 / -1; display: grid; grid-template-rows: subgrid; list-style: none; padding: 0; }
.tag__leer { grid-row: 2 / -1; padding: 14px 4px; font-size: .8125rem; color: var(--leise); }
.tag__liste > li { padding: 4px 0; min-width: 0; }

/* ---------- Termin-Karte ---------- */
.termin { position: relative; display: grid; gap: 3px; width: 100%; height: 100%; text-align: left; padding: 8px 9px 9px 12px; border: 1px solid var(--linie); border-radius: var(--rund); background: var(--karte); cursor: pointer; transition: border-color var(--ui) var(--kurve), background-color var(--ui) var(--kurve), box-shadow var(--ui) var(--kurve); align-content: start; }
.termin::before { content: ""; position: absolute; left: -1px; top: -1px; bottom: -1px; width: 4px; border-radius: var(--rund) 0 0 var(--rund); background: var(--farbe); }
[data-v="studio"] .termin { border-color: transparent; box-shadow: 0 1px 2px rgba(20, 20, 60, .06); }
[data-v="studio"] .termin::before { left: 8px; top: 10px; bottom: auto; width: 8px; height: 8px; border-radius: 50%; }
[data-v="studio"] .termin { padding-left: 24px; }
.termin:hover { border-color: var(--text); }
.termin__zeit { font: 600 .75rem/1.1 var(--f-text); font-variant-numeric: tabular-nums; color: var(--leise); }
.termin__abt { font: 600 .625rem/1.1 var(--f-text); letter-spacing: .1em; text-transform: uppercase; color: var(--leise); }
.termin__titel { font: 700 1.0625rem/1.02 var(--f-kopf); text-transform: var(--kopf-versal); letter-spacing: .01em; overflow-wrap: anywhere; hyphens: auto; }
[data-v="studio"] .termin__titel { font: 600 .9375rem/1.2 var(--f-text); letter-spacing: 0; text-transform: none; }
.termin__ort { font-size: .75rem; line-height: 1.3; color: var(--leise); }
.termin__platz { font-size: .75rem; font-weight: 600; line-height: 1.25; margin-top: 2px; }
.termin__platz.knapp { color: var(--knapp); }
.termin__platz.voll { color: var(--leise); }
.termin__hinweis { display: block; margin-top: 3px; font-size: .75rem; font-weight: 600; line-height: 1.25; color: var(--marke); }
.termin__hinweis::before { content: "↻ "; }
.termin__marke { justify-self: start; font-size: .6875rem; font-weight: 700; letter-spacing: .06em; text-transform: uppercase; padding: 2px 6px; border-radius: 3px; margin-bottom: 2px; }
[data-v="studio"] .termin__marke { border-radius: 999px; font-family: var(--f-notiz); letter-spacing: 0; text-transform: none; font-size: .75rem; font-weight: 600; }
/* läuft gerade */
.termin[data-zustand="laeuft"] { background: var(--marke); border-color: var(--marke); color: #fff; }
.termin[data-zustand="laeuft"] .termin__zeit, .termin[data-zustand="laeuft"] .termin__ort, .termin[data-zustand="laeuft"] .termin__abt, .termin[data-zustand="laeuft"] .termin__platz { color: #dbe6f4; }
.termin[data-zustand="laeuft"] .termin__hinweis { color: #fff; }
.termin[data-zustand="laeuft"] .termin__marke { background: #fff; color: var(--marke); }
.termin[data-zustand="laeuft"]::before { background: #fff; }
/* als Nächstes */
.termin[data-zustand="naechstes"] { box-shadow: inset 0 0 0 2px var(--marke); border-color: var(--marke); }
.termin[data-zustand="naechstes"] .termin__marke { background: var(--naechstes); color: var(--naechstes-text); }
/* vorbei */
.termin[data-zustand="vorbei"] { background: transparent; border-style: dashed; border-color: var(--linie-stark); box-shadow: none; }
.termin[data-zustand="vorbei"] .termin__titel { color: var(--leise); }
.termin[data-zustand="vorbei"] .termin__ort, .termin[data-zustand="vorbei"] .termin__platz, .termin[data-zustand="vorbei"] .termin__hinweis { display: none; }
.termin[data-zustand="vorbei"]::before { opacity: .35; }
/* fällt aus */
.termin.ist-aus { background: repeating-linear-gradient(135deg, var(--aus-flaeche, #fff4f2) 0 6px, var(--karte) 6px 12px); border-color: rgba(180, 35, 24, .35); }
.termin.ist-aus .termin__titel { text-decoration: line-through; text-decoration-thickness: 2px; color: var(--leise); }
.termin.ist-aus .termin__platz { display: none; }
.termin__aus { font-size: .75rem; font-weight: 700; line-height: 1.25; color: var(--aus); }
.termin__aus span { font-weight: 500; }
.termin.ist-aus[data-zustand="vorbei"] { background: transparent; }

/* ---------- Leerzustand ---------- */
.leer { margin: 26px 0 0; padding: 30px clamp(18px, 4vw, 40px); border: 1.5px dashed var(--linie-stark); border-radius: var(--rund-gross); display: grid; gap: 12px; justify-items: start; }
.leer[hidden] { display: none; }
.leer h2 { font: var(--kopf-gewicht) clamp(1.5rem, 1.1rem + 1.4vw, 2.25rem)/1 var(--f-kopf); text-transform: var(--kopf-versal); color: var(--marke); }
[data-v="studio"] .leer h2 { color: var(--text); }
.leer p { max-width: 56ch; }
.leer__wege { display: flex; flex-wrap: wrap; gap: 10px; }

.fuss { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 18px 40px; margin: 34px 0 0; padding: 22px 0 10px; border-top: 1px solid var(--linie); font-size: .875rem; color: var(--leise); }
.fuss b { display: block; color: var(--text); font-weight: 600; margin-bottom: 2px; }

/* ---------- Dialog Termin ---------- */
dialog.termin-dialog { margin: auto; width: min(540px, calc(100vw - 24px)); max-height: min(88svh, 760px); padding: 0; border: 0; border-radius: var(--rund-gross); background: var(--karte); color: var(--text); box-shadow: 0 30px 80px -20px rgba(0, 20, 50, .45); overflow: auto; overscroll-behavior: contain; }
dialog.termin-dialog::backdrop { background: rgba(0, 20, 45, .5); }
dialog.termin-dialog[open] { animation: dlg-rein var(--neu) var(--kurve); }
@keyframes dlg-rein { from { opacity: 0; translate: 0 14px; } }
.dlg__kopf { position: sticky; top: 0; z-index: 1; display: grid; grid-template-columns: 1fr auto; gap: 6px 14px; padding: 20px 22px 16px; background: var(--karte); border-bottom: 1px solid var(--linie); }
.dlg__kopf::before { content: ""; position: absolute; left: 0; right: 0; top: 0; height: 5px; background: var(--farbe); }
.dlg__kopf h2 { grid-column: 1; font: var(--kopf-gewicht) clamp(1.75rem, 1.4rem + 1.4vw, 2.375rem)/.95 var(--f-kopf); text-transform: var(--kopf-versal); letter-spacing: var(--kopf-laufweite); }
[data-v="studio"] .dlg__kopf h2 { font-size: 1.75rem; line-height: 1.1; }
.dlg__kopf h2:focus { outline: none; }
.dlg__kopf h2:focus-visible { outline: 2px solid var(--marke); outline-offset: 4px; border-radius: 2px; }
.dlg__zu { grid-column: 2; grid-row: 1 / span 2; align-self: start; width: 44px; height: 44px; border-radius: 50%; border: 1px solid var(--linie-stark); background: var(--karte); cursor: pointer; display: grid; place-items: center; }
.dlg__zu:hover { border-color: var(--text); }
.dlg__zu svg { width: 16px; height: 16px; }
.dlg__inhalt { padding: 18px 22px 22px; display: grid; gap: 16px; }
.dlg__status { display: flex; gap: 10px; align-items: flex-start; padding: 12px 14px; border-radius: var(--rund); background: var(--flaeche); font-size: .9375rem; line-height: 1.4; }
[data-v="studio"] .dlg__status { background: var(--warm); }
.dlg__status.ist-aus { background: var(--aus-flaeche, #fff4f2); color: var(--aus); }
.dlg__status.ist-aus b { color: var(--aus); }
.dlg__status svg { width: 18px; height: 18px; flex: none; margin-top: 1px; }
dl.angaben { display: grid; grid-template-columns: max-content 1fr; gap: 9px 18px; font-size: .9375rem; }
dl.angaben dt { font-size: .75rem; font-weight: 600; letter-spacing: .08em; text-transform: uppercase; color: var(--leise); padding-top: 3px; }
dl.angaben dd small { display: block; color: var(--leise); font-size: .8125rem; }
.dlg__text { color: var(--text); }
.dlg__aktionen { display: flex; flex-wrap: wrap; gap: 10px; }
.dlg__aktionen .knopf { flex: 1 1 auto; }
.dlg__notiz { font-size: .8125rem; color: var(--leise); }
[data-v="studio"] .dlg__notiz { font-family: var(--f-notiz); }
.formular { display: grid; gap: 14px; }
.feld { display: grid; gap: 5px; }
.feld label { font-size: .875rem; font-weight: 600; }
.feld input[type="text"], .feld input[type="email"] { min-height: 46px; padding: 0 12px; border-radius: var(--rund); border: 1.5px solid var(--linie-stark); background: var(--karte); font-size: 1rem; }
[data-v="studio"] .feld input[type="text"], [data-v="studio"] .feld input[type="email"] { border-radius: 10px; }
.feld input:focus-visible { outline: 3px solid var(--marke); outline-offset: 1px; border-color: var(--marke); }
.feld[data-fehler] input { border-color: var(--aus); }
.feld .fehler { font-size: .8125rem; font-weight: 600; color: var(--aus); }
.feld .fehler:empty { display: none; }
.feld--check { grid-template-columns: auto 1fr; align-items: start; gap: 10px; }
.feld--check input { width: 22px; height: 22px; margin-top: 1px; accent-color: var(--marke); }
.feld--check label { font-weight: 400; font-size: .875rem; }
.feld--check .fehler { grid-column: 2; }
.erfolg { display: grid; gap: 10px; }
.erfolg h3 { font: var(--kopf-gewicht) 1.75rem/1 var(--f-kopf); text-transform: var(--kopf-versal); color: var(--marke); }
.erfolg ul { padding-left: 1.1em; display: grid; gap: 4px; }

/* ---------- Meldung (ICS) ---------- */
.meldung { position: fixed; left: 50%; bottom: 98px; z-index: 40; translate: -50% 12px; opacity: 0; pointer-events: none; display: grid; gap: 2px; width: max-content; max-width: calc(100vw - 24px); padding: 12px 16px; border-radius: var(--rund); background: var(--text); color: var(--grund); font-size: .875rem; box-shadow: 0 16px 40px -12px rgba(0, 0, 0, .4); transition: opacity var(--ui) var(--kurve), translate var(--ui) var(--kurve); }
.meldung.an { opacity: 1; translate: -50% 0; }
.meldung small { opacity: .8; }

/* ---------- Zeiger der Vorführung ---------- */
.zeiger { position: fixed; left: 0; top: 0; z-index: 60; width: 22px; height: 22px; pointer-events: none; opacity: 0; transition: opacity 200ms var(--kurve); }
.vorfuehrung .zeiger { opacity: 1; }
.zeiger svg { width: 22px; height: 22px; filter: drop-shadow(0 2px 4px rgba(0, 0, 0, .3)); }
.zeiger.drueckt svg { scale: .86; }

/* ---------- Pult ---------- */
.pult { position: fixed; left: 50%; bottom: 14px; translate: -50% 0; z-index: 30; width: min(980px, calc(100vw - 24px)); display: flex; flex-wrap: wrap; align-items: center; gap: 8px 14px; padding: 10px 12px 10px 16px; border-radius: 14px; background: rgba(16, 20, 28, .94); color: #eef1f5; box-shadow: 0 12px 40px rgba(0, 0, 0, .25); font: 500 .75rem/1.2 'Geist Mono', ui-monospace, monospace; }
.pult__titel { letter-spacing: .14em; text-transform: uppercase; color: #9fb1c8; }
.pult__gruppe { display: flex; align-items: center; gap: 6px; }
.pult__gruppe > span { letter-spacing: .14em; text-transform: uppercase; color: #9fb1c8; margin-right: 2px; }
.pult button, .pult select { height: 32px; padding: 0 10px; border-radius: 8px; border: 1px solid rgba(238, 241, 245, .24); background: rgba(255, 255, 255, .06); color: inherit; font: inherit; cursor: pointer; }
.pult select option { color: #222520; }
.pult button[aria-pressed="true"] { background: #eef1f5; color: #10141c; border-color: #eef1f5; }
.pult button:focus-visible, .pult select:focus-visible, .pult input:focus-visible { outline: 2px solid #ffca0a; outline-offset: 2px; }
.pult__zeit { display: flex; align-items: center; gap: 10px; flex: 1 1 180px; min-width: 150px; }
.pult__zeit input { flex: 1; min-width: 80px; accent-color: #ffca0a; }
.pult__zeit output { min-width: 3.2em; font-size: .875rem; font-weight: 600; font-variant-numeric: tabular-nums; }

/* ---------- Vorschau ---------- */
.vorschau body { overflow: hidden; padding-bottom: 0; }
.vorschau .einstieg .lead, .vorschau .kopf nav { display: none; }
.vorschau .plan { padding-top: 16px; }
.vorschau .werkzeuge { margin-top: 16px; padding-block: 10px; }
.vorschau .einstieg h1 { font-size: 3.5rem; }
.vorschau .einstieg h1 { hyphens: manual; }
.vorschau .jetzt { min-height: 0; }
.vorschau .chip { padding: 0 9px; font-size: .8125rem; }
.vorschau .chips { gap: 5px; }

/* ---------- View Transition beim Filtern ---------- */
::view-transition-group(*) { animation-duration: var(--neu); animation-timing-function: var(--kurve); }
::view-transition-old(root), ::view-transition-new(root) { animation-duration: var(--ui); }

/* ---------- Reduzierte Bewegung ---------- */
.reduziert *, .reduziert *::before, .reduziert *::after { transition: none !important; animation: none !important; }

/* ---------- Kontrastmodus ---------- */
@media (forced-colors: active) {
  button, [role=button], [role=tab], .chip, .termin { border: 1px solid ButtonText; }
  .termin[data-zustand="laeuft"], .termin[data-zustand="naechstes"] { outline: 3px solid Highlight; }
  .woche label:has(input:checked) { outline: 2px solid Highlight; }
  .woche__pille { display: none; }
  .jetzt { border: 1px solid CanvasText; }
  .jetzt li button { border: 1px solid ButtonText; padding: 2px 4px; }
}

/* ---------- Mittel ---------- */
@media (max-width: 1180px) {
  .kopf nav { display: none; }
  .einstieg { grid-template-columns: 1fr; align-items: start; }
  .einstieg h1 br { display: none; }
  .werkzeuge { grid-template-columns: 1fr; }
  .treffer { margin-left: 0; }
  .raster { grid-template-columns: 3.4rem repeat(7, minmax(0, 1fr)); column-gap: 4px; }
  .termin { padding: 7px 6px 8px 9px; }
  [data-v="studio"] .termin { padding-left: 20px; }
  [data-v="studio"] .termin::before { left: 7px; }
  .termin__titel { font-size: .9375rem; }
  [data-v="studio"] .termin__titel { font-size: .875rem; }
  .tag__kopf .wt { font-size: 1.125rem; }
  .tag__kopf .heute { display: none; }
  .tag__kopf .wt-l { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
  .tag__kopf .wt-k { display: inline; }
  .termin__abt { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
  .vorschau .einstieg { grid-template-columns: minmax(0, .9fr) minmax(0, 1fr); align-items: end; }
  .vorschau .werkzeuge { grid-template-columns: auto minmax(0, 1fr); }
}

/* ---------- Telefon: Tages-Reiter statt Raster ---------- */
@media (max-width: 860px) {
  .jetzt__spalten { grid-template-columns: 1fr; gap: 12px; }
  .jetzt { min-height: 0; }
  .werkzeuge { grid-template-columns: 1fr auto; gap: 12px; }
  [data-v="studio"] .werkzeuge { padding: 12px; }
  .woche label { padding: 6px 10px; }
  .filter-knopf { display: inline-flex; align-items: center; gap: 8px; min-height: 44px; padding: 0 14px; border-radius: var(--rund); border: 1px solid var(--linie-stark); background: var(--karte); font-weight: 600; font-size: .9375rem; cursor: pointer; }
  [data-v="studio"] .filter-knopf { border-radius: 999px; }
  .filter-knopf b { min-width: 1.5em; height: 1.5em; display: inline-grid; place-items: center; border-radius: 99px; background: var(--marke); color: #fff; font-size: .75rem; }
  .filter-knopf b:empty { display: none; }
  .filter-knopf svg { width: 16px; height: 16px; }
  .filter { grid-column: 1 / -1; display: grid; gap: 14px; }
  .filter[hidden] { display: none; }
  .wahl { display: grid; grid-template-columns: 1fr 1fr; }
  .wahl select { max-width: none; width: 100%; min-height: 44px; }
  .chip { min-height: 40px; }
  .treffer { grid-column: 1 / -1; margin: 0; justify-content: space-between; white-space: normal; }
  .reiter { display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); gap: 4px; margin-top: 14px; position: sticky; top: 0; z-index: 5; background: var(--grund); padding: 8px 0; }
  .reiter button { display: grid; justify-items: center; gap: 2px; min-height: 58px; padding: 6px 0; border: 1px solid var(--linie); border-radius: var(--rund); background: var(--karte); cursor: pointer; font-size: .75rem; color: var(--leise); }
  .reiter button .wt { font: var(--kopf-gewicht) 1.125rem/1 var(--f-kopf); text-transform: var(--kopf-versal); color: var(--text); }
  .reiter button .n { font-size: .6875rem; }
  .reiter button.ist-heute { border-color: var(--marke); }
  .reiter button.ist-heute .wt { color: var(--marke); }
  .reiter button[aria-selected="true"] { background: var(--marke); border-color: var(--marke); color: #dbe6f4; }
  .reiter button[aria-selected="true"] .wt { color: #fff; }
  .reiter button.ist-vorbei:not([aria-selected="true"]) .wt { color: var(--leise); }
  .raster { display: block; margin-top: 4px; }
  .raster__zeile, .zeitachse { display: none; }
  .tag { display: block; }
  .tag[hidden] { display: none; }
  .tag__kopf { position: static; padding: 10px 0; border-bottom-width: 1px; }
  .tag__kopf .heute { display: inline-block; }
  .tag__kopf .wt-l { position: static; width: auto; height: auto; overflow: visible; clip-path: none; }
  .tag__kopf .wt-k { display: none; }
  .termin__abt { position: static; width: auto; height: auto; overflow: visible; clip-path: none; }
  .termin[data-zustand="laeuft"] .termin__zeit .ende { color: #dbe6f4; }
  .tag__liste { display: grid; gap: 8px; padding-top: 10px; }
  .tag__liste > li { grid-row: auto !important; padding: 0; }
  .termin { grid-template-columns: 4.6rem minmax(0, 1fr); column-gap: 12px; padding: 12px 12px 12px 14px; }
  .termin > * { grid-column: 2; }
  .termin__zeit { grid-column: 1; grid-row: 1 / span 6; font-size: .9375rem; line-height: 1.15; color: var(--text); }
  .termin__zeit .ende { display: block; font-weight: 500; font-size: .75rem; color: var(--leise); }
  .termin[data-zustand="laeuft"] .termin__zeit { color: #fff; }
  .termin__titel { font-size: 1.25rem; }
  [data-v="studio"] .termin__titel { font-size: 1.0625rem; }
  .termin__ort, .termin__platz, .termin__hinweis, .termin__aus { font-size: .875rem; }
  [data-v="studio"] .termin { padding-left: 26px; }
  .tag__leer { padding: 18px 0; font-size: .9375rem; }
  .fuss { grid-template-columns: 1fr; }
}
@media (max-width: 640px) {
  .marke span { display: none; }
  .marke b { font-size: 1.125rem; }
  .kopf .knopf { padding: 0 14px; }
  .einstieg h1 { font-size: clamp(2.6rem, 13vw, 3.5rem); }
  [data-v="studio"] .einstieg h1 { font-size: clamp(2.2rem, 10vw, 3rem); }
  dialog.termin-dialog { width: 100vw; max-width: 100vw; max-height: 92svh; margin: auto 0 0; border-radius: var(--rund-gross) var(--rund-gross) 0 0; }
  dl.angaben { grid-template-columns: 1fr; gap: 2px; }
  dl.angaben dd { margin-bottom: 8px; }
  .pult { bottom: 8px; }
  .meldung { bottom: 76px; }
}
@media (max-width: 380px) {
  .woche label { padding: 6px 8px; font-size: .8125rem; }
  .woche label small { display: none; }
  .wahl { grid-template-columns: 1fr; }
  .reiter { gap: 2px; }
  .reiter button .wt { font-size: 1rem; }
  .kopf .knopf { flex: none; padding: 0 12px; }
  .kopf .knopf svg { display: none; }
  .marke b { font-size: 1.0625rem; }
  .werkzeuge { grid-template-columns: 1fr; }
  .woche { grid-auto-columns: 1fr; }
  .woche label { justify-items: center; }
  .filter-knopf { justify-content: center; }
}
@media (hover: none) {
  .termin:hover, .chip:hover, .wahl select:hover { border-color: var(--linie-stark); }
  .termin:active { scale: .985; }
}
</style>
</head>
<body>
<header class="kopf">
  <a class="marke" href="#plan" data-nur="verein">
    <svg viewBox="0 0 42 42" aria-hidden="true"><rect width="42" height="42" rx="4" fill="#002e6d"/><path d="M7 27c5-3.6 9.4-3.6 14 0s9 3.6 14 0" fill="none" stroke="#ffca0a" stroke-width="2.6" stroke-linecap="round"/><path d="M7 33c5-3.6 9.4-3.6 14 0s9 3.6 14 0" fill="none" stroke="#9db5d6" stroke-width="2.6" stroke-linecap="round"/><path d="M12 9h18M21 9v12" stroke="#fff" stroke-width="3.2" stroke-linecap="square"/></svg>
    <div><b>TuS Weserbogen</b><span>Minden 1889 e.V.</span></div>
  </a>
  <a class="marke" href="#plan" data-nur="studio">
    <svg viewBox="0 0 42 42" aria-hidden="true"><circle cx="21" cy="21" r="21" fill="#3f4fc4"/><path d="M10 25c3.5-6 7-9 11-9s7.5 3 11 9" fill="none" stroke="#fff" stroke-width="2.4" stroke-linecap="round"/><circle cx="21" cy="12" r="3" fill="#f7c325"/><path d="M12 30h18" stroke="#fff" stroke-width="2.4" stroke-linecap="round"/></svg>
    <div><b>Atemhaus</b><span>Yoga &amp; Pilates · Minden</span></div>
  </a>
  <nav aria-label="Hauptnavigation"><ul data-nav></ul></nav>
  <a class="knopf knopf--cta" href="#plan" data-cta-kopf><svg viewBox="0 0 18 18" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="1.7"><path d="M3 9.5 7 13.5 15 4.5"/></svg><span data-cta-text>Probetraining</span></a>
</header>

<main class="plan" id="plan">
  <div class="einstieg">
    <div>
      <p class="label" data-text="ueberzeile">Kursplan · Hallensaison 2026/27</p>
      <h1 data-text="titel">Trainings&shy;zeiten</h1>
      <p class="lead" data-text="lead"></p>
    </div>
    <section class="jetzt" aria-labelledby="jetzt-titel" data-jetzt>
      <div class="jetzt__kopf"><b id="jetzt-titel">Jetzt</b><span class="zahl" data-jetzt-zeit>Ortszeit Minden</span></div>
      <div class="jetzt__spalten">
        <div><h2><span class="puls" aria-hidden="true"></span>Läuft gerade</h2><div data-laeuft></div></div>
        <div><h2>Als Nächstes</h2><div data-naechstes></div></div>
      </div>
      <p class="jetzt__fuss" data-jetzt-fuss></p>
      <p class="sr" aria-live="polite" data-ansage></p>
    </section>
  </div>

  <div class="werkzeuge">
    <fieldset class="woche" data-woche>
      <legend>Woche</legend>
      <span class="woche__pille" aria-hidden="true" data-woche-pille></span>
      <label><input type="radio" name="woche" value="0" checked><span>Diese Woche</span><small class="zahl" data-woche-text="0"></small></label>
      <label><input type="radio" name="woche" value="1"><span>Nächste Woche</span><small class="zahl" data-woche-text="1"></small></label>
    </fieldset>
    <button type="button" class="filter-knopf" aria-expanded="false" aria-controls="filter" data-filter-knopf><svg viewBox="0 0 16 16" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="1.6"><path d="M2 4h12M4.5 8h7M7 12h2"/></svg>Filter <b data-filter-zahl></b></button>
    <div class="filter" id="filter" data-filter>
      <fieldset class="chips" data-chips><legend data-text="abt-legende">Abteilung</legend></fieldset>
      <div class="wahl">
        <label><span data-text="trainer-label">Trainer:in</span><select data-wahl="trainer"></select></label>
        <label><span data-text="gruppe-label">Alter</span><select data-wahl="gruppe"></select></label>
        <label><span data-text="halle-label">Halle</span><select data-wahl="halle"></select></label>
      </div>
      <p class="treffer"><span aria-live="polite" data-treffer></span><button type="button" class="zuruecksetzen" data-zuruecksetzen hidden>Alle Filter löschen</button></p>
    </div>
  </div>

  <div class="reiter" role="tablist" aria-label="Wochentag" data-reiter hidden></div>
  <div class="raster" data-raster></div>

  <div class="leer" data-leer hidden>
    <p class="label">Keine Treffer</p>
    <h2 data-leer-titel>Diese Woche passt nichts.</h2>
    <p data-leer-text></p>
    <div class="leer__wege"><button type="button" class="knopf" data-zuruecksetzen>Alle Filter löschen</button><button type="button" class="knopf knopf--linie" data-leer-woche hidden></button></div>
  </div>

  <div class="fuss" data-fuss></div>
</main>

<dialog class="termin-dialog" aria-labelledby="dlg-titel" data-dialog>
  <div class="dlg__kopf">
    <p class="label" data-dlg-abt></p>
    <h2 id="dlg-titel" tabindex="-1" data-dlg-titel></h2>
    <button type="button" class="dlg__zu" aria-label="Schließen" data-dlg-zu><svg viewBox="0 0 16 16" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="1.8"><path d="m3 3 10 10M13 3 3 13"/></svg></button>
  </div>
  <div class="dlg__inhalt" data-dlg-inhalt></div>
</dialog>

<div class="meldung" role="status" data-meldung><b></b><small></small></div>
<div class="zeiger" aria-hidden="true" data-zeiger><svg viewBox="0 0 22 22"><path d="M4 2.5 18 11l-6.2 1.4L8.6 18.6z" fill="#fff" stroke="#10141c" stroke-width="1.5" stroke-linejoin="round"/></svg></div>

<div class="pult">
  <span class="pult__titel">Uhr der Demo</span>
  <div class="pult__gruppe">
    <button type="button" aria-pressed="true" data-echtzeit>Echtzeit</button>
    <label class="sr" for="sim-tag">Tag</label>
    <select id="sim-tag" data-sim-tag></select>
  </div>
  <div class="pult__zeit">
    <label class="sr" for="sim-zeit">Uhrzeit</label>
    <input id="sim-zeit" type="range" min="360" max="1380" step="5" value="1060" data-sim-zeit>
    <output for="sim-zeit" data-sim-anzeige>17.40</output>
  </div>
  <div class="pult__gruppe" role="group" aria-label="Variante"><span>Variante</span>
    <button type="button" data-variante="verein" aria-pressed="true">Sportverein</button>
    <button type="button" data-variante="studio" aria-pressed="false">Yogastudio</button>
  </div>
</div>

<script>
(() => {
  const html = document.documentElement;
  const reduziert = () => html.classList.contains('reduziert');
  const $ = (s, r = document) => r.querySelector(s), $$ = (s, r = document) => [...r.querySelectorAll(s)];
  const ZONE = 'Europe/Berlin';

  /* =====================================================================
     Daten (so kämen sie aus CMS oder Vereinsverwaltung)
     Kurs: [id, wochentag 1=Mo…7=So, beginn, dauer Min., rubrik, titel, trainer, gruppe, halle, plätze, belegt, anmeldung, info]
     anmeldung: probe = Probetraining jederzeit · kurs = feste Plätze, Anmeldung · frei = ohne Anmeldung
     Ausnahme: { w: Woche (0 = diese, 1 = nächste), kurs, art: aus | vertretung | raum, … }
     ===================================================================== */
  const WELTEN = {
    verein: {
      marke: 'TuS Weserbogen Minden', kurzmarke: 'TuS Weserbogen', domain: 'tus-weserbogen.example', datei: 'tus-weserbogen',
      nav: ['Abteilungen', 'Kursplan', 'Verein', 'Mitglied werden'],
      text: {
        ueberzeile: 'Kursplan · Hallensaison 2026/27',
        titel: 'Trainings&shy;zeiten',
        lead: '<b>33 Gruppen in fünf Abteilungen</b>, vom Eltern-Kind-Turnen bis zum Lauftreff an der Weser. Jede Gruppe kannst du zweimal kostenlos ausprobieren.',
        'abt-legende': 'Abteilung', 'trainer-label': 'Trainer:in', 'gruppe-label': 'Alter', 'halle-label': 'Halle',
      },
      cta: 'Probetraining', ctaLang: 'Probetraining an diesem Termin', warteliste: 'Auf die Warteliste', probeWort: 'Probetraining',
      rubriken: {
        turnen: { name: 'Turnen', farbe: '#002e6d' }, leicht: { name: 'Leichtathletik', farbe: '#0077a8' },
        hand: { name: 'Handball', farbe: '#a8461a' }, gesund: { name: 'Gesundheitssport', farbe: '#00707a' }, judo: { name: 'Judo', farbe: '#6e5a3a' },
      },
      gruppen: { kinder: 'Kinder bis 10', jugend: 'Jugend 10–17', erw: 'Erwachsene', senioren: 'Ältere (60+)' },
      gruppeWort: 'Altersgruppe',
      hallen: {
        glacis: { name: 'Halle Glacis', kurz: 'Halle Glacis', adr: 'Glacisweg 4, 32423 Minden' },
        fischer: { name: 'Turnhalle Fischerstadt', kurz: 'Fischerstadt', adr: 'An der Fischerstadt 9, 32423 Minden' },
        gym: { name: 'Gymnastikraum im Vereinsheim', kurz: 'Gymnastikraum', adr: 'Weserbogen 2, 32425 Minden' },
        dojo: { name: 'Dojo im Vereinsheim', kurz: 'Dojo', adr: 'Weserbogen 2, 32425 Minden' },
        bruecke: { name: 'Treffpunkt Weserbrücke (draußen)', kurz: 'Weserbrücke', adr: 'Weserpromenade, 32423 Minden' },
      },
      halleWort: 'Halle',
      trainer: { brockmeyer: 'Hanne Brockmeyer', kampmann: 'Edith Kampmann', vehling: 'Jannis Vehling', hoecker: 'Malte Höcker', oevermann: 'Svenja Oevermann', rehage: 'Lutz Rehage', yildiz: 'Okan Yıldız', struck: 'Mareike Struck', dreier: 'Ilka Dreier', wiebking: 'Lasse Wiebking', rodenberg: 'Pia Rodenberg' },
      kurse: [
        ['rueckenfit-mo', 1, '09:30', 60, 'gesund', 'Rückenfit', 'kampmann', 'erw', 'gym', 16, 13, 'kurs', 'Kräftigung und Mobilisation für den Alltag. Bitte eine Matte mitbringen.'],
        ['eltern-kind-mo', 1, '15:00', 60, 'turnen', 'Eltern-Kind-Turnen', 'brockmeyer', 'kinder', 'fischer', 14, 14, 'kurs', 'Für Kinder von 1 bis 3 Jahren mit einer erwachsenen Begleitung.'],
        ['kinderturnen-mo', 1, '16:30', 60, 'turnen', 'Kinderturnen 4–6', 'brockmeyer', 'kinder', 'fischer', 18, 15, 'probe', 'Laufen, Klettern, Balancieren an Geräten und in Bewegungslandschaften.'],
        ['la-u10-mo', 1, '17:00', 75, 'leicht', 'Leichtathletik U10', 'vehling', 'kinder', 'glacis', 20, 16, 'probe', 'Kinderleichtathletik: Sprint, Sprung und Wurf als Spiel.'],
        ['hb-d-mo', 1, '18:00', 90, 'hand', 'Handball D-Jugend', 'hoecker', 'jugend', 'glacis', 20, 17, 'probe', 'Jahrgänge 2014 und 2015, gemischtes Team.'],
        ['funktion-mo', 1, '19:00', 60, 'gesund', 'Funktions­gymnastik', 'dreier', 'erw', 'gym', 16, 9, 'kurs', 'Ganzkörpertraining mit dem eigenen Körpergewicht.'],
        ['hb-damen-mo', 1, '19:30', 90, 'hand', 'Handball Damen', 'oevermann', 'erw', 'glacis', 22, 15, 'probe', 'Kreisliga. Neue Spielerinnen jeden Alters sind willkommen.'],
        ['sturz-di', 2, '10:00', 60, 'gesund', 'Sturzprävention 60+', 'kampmann', 'senioren', 'gym', 12, 10, 'kurs', 'Gleichgewicht, Kraft und sicheres Gehen.'],
        ['judo-kinder-di', 2, '16:00', 60, 'judo', 'Judo Kinder 6–9', 'yildiz', 'kinder', 'dojo', 16, 12, 'probe', 'Fallschule und erste Würfe. Für den Start reichen Jogginghose und langes Shirt.'],
        ['judo-jugend-di', 2, '17:15', 75, 'judo', 'Judo Jugend', 'yildiz', 'jugend', 'dojo', 16, 8, 'probe', 'Ab 10 Jahren, auch für Einsteiger.'],
        ['geraete-di', 2, '17:30', 90, 'turnen', 'Geräteturnen Mädchen', 'struck', 'kinder', 'fischer', 16, 14, 'probe', '8 bis 12 Jahre. Boden, Balken, Sprung und Stufenbarren.'],
        ['la-jugend-di', 2, '18:30', 90, 'leicht', 'Leichtathletik Jugend', 'vehling', 'jugend', 'glacis', 24, 18, 'probe', 'U14 bis U18: Lauf, Sprung und Wurf.'],
        ['judo-erw-di', 2, '20:00', 90, 'judo', 'Judo und Selbst­verteidigung', 'yildiz', 'erw', 'dojo', 18, 11, 'probe', 'Für Erwachsene, mit und ohne Vorerfahrung.'],
        ['wirbel-mi', 3, '09:00', 60, 'gesund', 'Wirbelsäulen­gymnastik', 'dreier', 'senioren', 'gym', 14, 14, 'kurs', 'Ruhiges Training für Rücken und Haltung.'],
        ['kinderturnen-mi', 3, '15:30', 45, 'turnen', 'Kinderturnen 3–4', 'brockmeyer', 'kinder', 'fischer', 14, 11, 'probe', 'Bewegungslandschaften für die Kleinsten.'],
        ['hb-e-mi', 3, '17:00', 75, 'hand', 'Handball E-Jugend', 'hoecker', 'kinder', 'glacis', 18, 12, 'probe', 'Jahrgänge 2016 und 2017.'],
        ['hb-herren-mi', 3, '18:30', 90, 'hand', 'Handball Herren', 'rehage', 'erw', 'glacis', 22, 19, 'probe', 'Bezirksliga, Training mit Ball und Athletik.'],
        ['lauftreff-mi', 3, '19:00', 60, 'leicht', 'Lauftreff am Abend', 'vehling', 'erw', 'bruecke', 0, 0, 'frei', 'Zwei Gruppen, 6 und 10 km. Ab Oktober mit Stirnlampe.'],
        ['fitmix-mi', 3, '19:30', 60, 'gesund', 'Fitness­gymnastik', 'oevermann', 'erw', 'gym', 18, 15, 'kurs', 'Ausdauer und Kraft zu Musik.'],
        ['gym60-do', 4, '10:00', 60, 'gesund', 'Gymnastik 60+', 'kampmann', 'senioren', 'gym', 16, 12, 'kurs', 'Beweglich bleiben, im Sitzen und im Stehen.'],
        ['judo-kinder-do', 4, '16:30', 60, 'judo', 'Judo Kinder 6–9', 'yildiz', 'kinder', 'dojo', 16, 13, 'probe', 'Fallschule und erste Würfe.'],
        ['la-u10-do', 4, '17:00', 75, 'leicht', 'Leichtathletik U10', 'vehling', 'kinder', 'glacis', 20, 18, 'probe', 'Kinderleichtathletik: Sprint, Sprung und Wurf als Spiel.'],
        ['geraete-do', 4, '18:00', 90, 'turnen', 'Geräteturnen Mädchen', 'struck', 'kinder', 'fischer', 16, 14, 'probe', '8 bis 12 Jahre. Boden, Balken, Sprung und Stufenbarren.'],
        ['hb-d-do', 4, '18:30', 90, 'hand', 'Handball D-Jugend', 'hoecker', 'jugend', 'glacis', 20, 17, 'probe', 'Jahrgänge 2014 und 2015, gemischtes Team.'],
        ['ruecken-abend-do', 4, '19:00', 60, 'gesund', 'Rückenfit am Abend', 'dreier', 'erw', 'gym', 16, 14, 'kurs', 'Kräftigung und Mobilisation nach dem Arbeitstag.'],
        ['hb-damen-do', 4, '20:00', 90, 'hand', 'Handball Damen', 'oevermann', 'erw', 'glacis', 22, 15, 'probe', 'Kreisliga. Neue Spielerinnen jeden Alters sind willkommen.'],
        ['eltern-kind-fr', 5, '15:30', 60, 'turnen', 'Eltern-Kind-Turnen', 'brockmeyer', 'kinder', 'fischer', 14, 12, 'kurs', 'Für Kinder von 1 bis 3 Jahren mit einer erwachsenen Begleitung.'],
        ['la-jugend-fr', 5, '16:30', 90, 'leicht', 'Leichtathletik Jugend', 'vehling', 'jugend', 'glacis', 24, 18, 'probe', 'U14 bis U18: Lauf, Sprung und Wurf.'],
        ['parkour-fr', 5, '17:30', 90, 'turnen', 'Parkour und Turnen 10–14', 'wiebking', 'jugend', 'fischer', 16, 15, 'probe', 'Springen, Landen, Rollen. Sicher an Kästen und Matten.'],
        ['hb-herren-fr', 5, '19:30', 90, 'hand', 'Handball Herren', 'rehage', 'erw', 'glacis', 22, 19, 'probe', 'Bezirksliga, Training mit Ball und Athletik.'],
        ['hb-minis-sa', 6, '10:00', 60, 'hand', 'Handball Minis', 'hoecker', 'kinder', 'glacis', 20, 14, 'probe', '4 bis 7 Jahre, erste Ballspiele.'],
        ['trampolin-sa', 6, '11:00', 90, 'turnen', 'Trampolin', 'wiebking', 'jugend', 'fischer', 10, 9, 'probe', 'Ab 10 Jahren, mit Hallenschuhen oder barfuß.'],
        ['lauftreff-so', 7, '09:30', 90, 'leicht', 'Lauftreff Sonntag', 'vehling', 'erw', 'bruecke', 0, 0, 'frei', 'Ruhige Runde an der Weser, 8 bis 15 km.'],
      ],
      ausnahmen: [
        { w: 0, kurs: 'kinderturnen-mo', art: 'vertretung', trainer: 'struck', grund: 'Hanne ist auf Fortbildung.' },
        { w: 0, kurs: 'geraete-di', art: 'aus', grund: 'Halle gesperrt: Elternsprechtag der Grundschule.' },
        { w: 0, kurs: 'hb-herren-mi', art: 'vertretung', trainer: 'hoecker' },
        { w: 0, kurs: 'ruecken-abend-do', art: 'raum', halle: 'glacis', grund: 'Der Gymnastikraum wird gestrichen.' },
        { w: 0, kurs: 'la-jugend-fr', art: 'aus', grund: 'Hallensportfest am Samstag, die Halle wird aufgebaut.' },
        { w: 1, kurs: 'rueckenfit-mo', art: 'aus', grund: 'Edith ist auf Fortbildung.' },
        { w: 1, kurs: 'judo-erw-di', art: 'vertretung', trainer: 'rodenberg' },
        { w: 1, kurs: 'hb-minis-sa', art: 'aus', grund: 'Heimspieltag in der Halle. Zuschauen lohnt sich.' },
      ],
      pause: { von: '2026-12-21', bis: '2027-01-03', grund: 'Weihnachtspause, die Hallen sind geschlossen.' },
      saisonEnde: '2027-06-25', saisonText: 'Hallensaison bis 25. Juni 2027',
      fuss: [
        ['Probetraining', 'Zweimal kostenlos in jeder Gruppe. Danach fragen wir nach der Mitgliedschaft.'],
        ['Ausfälle', 'Stehen am Termin. Übungsleitungen pflegen sie selbst, spätestens zwei Stunden vorher.'],
        ['Ortszeit', 'Alle Zeiten in deutscher Zeit (Minden). Weihnachtspause 21.12. bis 3.1.'],
      ],
    },
    studio: {
      marke: 'Atemhaus Minden', kurzmarke: 'Atemhaus', domain: 'atemhaus-minden.example', datei: 'atemhaus',
      nav: ['Kurse', 'Wochenplan', 'Preise', 'Lehrerinnen'],
      text: {
        ueberzeile: 'Wochenplan · Herbst 2026',
        titel: 'Dein Platz auf der <em>Matte</em>',
        lead: '<b>24 Stunden pro Woche</b> in drei Räumen, von sanftem Hatha bis Power-Vinyasa. Die erste Stunde ist eine Probestunde für 9 €.',
        'abt-legende': 'Kursart', 'trainer-label': 'Lehrer:in', 'gruppe-label': 'Niveau', 'halle-label': 'Raum',
      },
      cta: 'Probestunde', ctaLang: 'Probestunde an diesem Termin', warteliste: 'Auf die Warteliste', probeWort: 'Probestunde',
      rubriken: {
        yoga: { name: 'Yoga', farbe: '#3f4fc4' }, pilates: { name: 'Pilates', farbe: '#0b7a4f' }, yin: { name: 'Yin & Faszien', farbe: '#8a3fb0' },
        ruecken: { name: 'Rücken & Mobility', farbe: '#a8461a' }, mama: { name: 'Schwanger & danach', farbe: '#b4547a' },
      },
      gruppen: { ein: 'Einsteiger', alle: 'Alle Level', fort: 'Fortgeschritten' },
      gruppeWort: 'Niveau',
      hallen: {
        weser: { name: 'Raum Weser', kurz: 'Raum Weser', adr: 'Lindenstraße 14, 32423 Minden, 1. OG' },
        bastau: { name: 'Raum Bastau', kurz: 'Raum Bastau', adr: 'Lindenstraße 14, 32423 Minden, EG' },
        dach: { name: 'Dachstudio', kurz: 'Dachstudio', adr: 'Lindenstraße 14, 32423 Minden, 2. OG' },
      },
      halleWort: 'Raum',
      trainer: { wiechers: 'Lea Wiechers', aydin: 'Nalan Aydın', busche: 'Frieda Busche', linnemann: 'Kaspar Linnemann' },
      kurse: [
        ['vinyasa-mo', 1, '07:00', 60, 'yoga', 'Morgen-Vinyasa', 'wiechers', 'alle', 'weser', 18, 11, 'kurs', 'Wach werden mit fließenden Sonnengrüßen.'],
        ['pilates-mo', 1, '09:30', 60, 'pilates', 'Pilates Matte', 'busche', 'ein', 'bastau', 12, 10, 'kurs', 'Körpermitte, Atmung, saubere Ausführung.'],
        ['mittag-mo', 1, '12:15', 45, 'yoga', 'Mittagsyoga', 'wiechers', 'alle', 'weser', 18, 7, 'kurs', 'Kurze Pause vom Schreibtisch, ohne Schwitzen.'],
        ['hatha-mo', 1, '17:30', 75, 'yoga', 'Hatha sanft', 'aydin', 'ein', 'weser', 18, 16, 'kurs', 'Langsam, mit Zeit für jede Haltung.'],
        ['pilatesflow-mo', 1, '19:00', 60, 'pilates', 'Pilates Flow', 'busche', 'alle', 'bastau', 12, 12, 'kurs', 'Pilates im Fluss, mit kleinen Geräten.'],
        ['power-mo', 1, '19:15', 75, 'yoga', 'Power-Vinyasa', 'linnemann', 'fort', 'weser', 18, 14, 'kurs', 'Kraftvoll und schnell. Grundlagen sollten sitzen.'],
        ['rueckbildung-di', 2, '09:00', 75, 'mama', 'Rückbildung', 'aydin', 'ein', 'dach', 10, 7, 'kurs', 'Ab acht Wochen nach der Geburt, Baby darf mit.'],
        ['ruecken-di', 2, '18:00', 60, 'ruecken', 'Rücken & Mobility', 'linnemann', 'alle', 'bastau', 12, 9, 'kurs', 'Beweglichkeit für Schreibtischmenschen.'],
        ['yin-di', 2, '19:30', 75, 'yin', 'Yin Yoga', 'wiechers', 'alle', 'weser', 18, 15, 'kurs', 'Lange Haltungen, ruhiger Atem, viel Boden.'],
        ['vinyasa-mi', 3, '07:00', 60, 'yoga', 'Morgen-Vinyasa', 'wiechers', 'alle', 'weser', 18, 9, 'kurs', 'Wach werden mit fließenden Sonnengrüßen.'],
        ['schwanger-mi', 3, '10:30', 60, 'mama', 'Schwangerenyoga', 'aydin', 'ein', 'dach', 10, 6, 'kurs', 'Ab der 13. Woche, nach Rücksprache mit Hebamme oder Ärztin.'],
        ['pilates-mi', 3, '17:30', 60, 'pilates', 'Pilates Matte', 'busche', 'ein', 'bastau', 12, 8, 'kurs', 'Körpermitte, Atmung, saubere Ausführung.'],
        ['ashtanga-mi', 3, '18:45', 75, 'yoga', 'Ashtanga-Grundlagen', 'linnemann', 'fort', 'weser', 18, 12, 'kurs', 'Die erste Serie in Ruhe gelernt.'],
        ['faszien-mi', 3, '20:00', 60, 'yin', 'Faszien & Atem', 'aydin', 'alle', 'dach', 10, 8, 'kurs', 'Rollen, dehnen, durchatmen.'],
        ['hatha-do', 4, '09:30', 75, 'yoga', 'Hatha sanft', 'aydin', 'ein', 'weser', 18, 10, 'kurs', 'Langsam, mit Zeit für jede Haltung.'],
        ['mittag-do', 4, '12:15', 45, 'yoga', 'Mittagsyoga', 'wiechers', 'alle', 'weser', 18, 6, 'kurs', 'Kurze Pause vom Schreibtisch, ohne Schwitzen.'],
        ['pilatesflow-do', 4, '18:00', 60, 'pilates', 'Pilates Flow', 'busche', 'alle', 'bastau', 12, 11, 'kurs', 'Pilates im Fluss, mit kleinen Geräten.'],
        ['vinyasa-do', 4, '19:30', 75, 'yoga', 'Vinyasa', 'linnemann', 'alle', 'weser', 18, 13, 'kurs', 'Fließende Abfolgen mit Musik.'],
        ['vinyasa-fr', 5, '07:00', 60, 'yoga', 'Morgen-Vinyasa', 'wiechers', 'alle', 'weser', 18, 8, 'kurs', 'Wach werden mit fließenden Sonnengrüßen.'],
        ['ruecken-fr', 5, '17:00', 60, 'ruecken', 'Rücken & Mobility', 'linnemann', 'alle', 'bastau', 12, 10, 'kurs', 'Beweglichkeit für Schreibtischmenschen.'],
        ['yinklang-fr', 5, '18:30', 75, 'yin', 'Yin & Klang', 'wiechers', 'alle', 'weser', 18, 18, 'kurs', 'Yin Yoga mit Klangschalen zum Ausklang der Woche.'],
        ['vinyasa-sa', 6, '09:00', 90, 'yoga', 'Vinyasa am Wochenende', 'linnemann', 'alle', 'weser', 18, 12, 'kurs', 'Eine lange Stunde mit Zeit für Umkehrhaltungen.'],
        ['pilates-sa', 6, '11:00', 60, 'pilates', 'Pilates für Einsteiger', 'busche', 'ein', 'bastau', 12, 5, 'kurs', 'Alle Grundübungen Schritt für Schritt.'],
        ['yin-so', 7, '10:00', 90, 'yin', 'Yin Yoga', 'aydin', 'alle', 'weser', 18, 14, 'kurs', 'Lange Haltungen, ruhiger Atem, viel Boden.'],
      ],
      ausnahmen: [
        { w: 0, kurs: 'yin-di', art: 'vertretung', trainer: 'aydin' },
        { w: 0, kurs: 'faszien-mi', art: 'aus', grund: 'Nalan ist krank. Gute Besserung!' },
        { w: 0, kurs: 'ruecken-fr', art: 'raum', halle: 'dach', grund: 'Im Raum Bastau läuft ein Workshop.' },
        { w: 1, kurs: 'pilates-mo', art: 'vertretung', trainer: 'linnemann' },
        { w: 1, kurs: 'vinyasa-sa', art: 'aus', grund: 'Workshop-Wochenende im Raum Weser.' },
      ],
      pause: { von: '2026-12-24', bis: '2026-12-27', grund: 'Über die Feiertage ist das Studio geschlossen.' },
      saisonEnde: '2027-03-26', saisonText: 'Herbst- und Winterplan bis 26. März 2027',
      fuss: [
        ['Probestunde', 'Die erste Stunde kostet 9 €. Matten und Blöcke liegen bereit.'],
        ['Warteliste', 'Wird ein Platz frei, bekommst du eine Nachricht und hast 2 Stunden Zeit.'],
        ['Ortszeit', 'Alle Zeiten in deutscher Zeit (Minden). Vom 24. bis 27.12. geschlossen.'],
      ],
    },
  };

  /* =====================================================================
     Ortszeit über Intl (wie oeffnungszeiten-status): unabhängig von der Zeitzone des Geräts
     ===================================================================== */
  const teileFmt = new Intl.DateTimeFormat('de-DE', { timeZone: ZONE, hourCycle: 'h23', year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' });
  function ortszeit(date) {
    const p = Object.fromEntries(teileFmt.formatToParts(date).map(x => [x.type, x.value]));
    return { datum: `${p.year}-${p.month}-${p.day}`, min: (+p.hour % 24) * 60 + +p.minute };
  }
  const tagDatum = s => new Date(s + 'T12:00:00Z');          // Datum rechnen immer in UTC-Mittag
  const plusTage = (s, n) => { const d = tagDatum(s); d.setUTCDate(d.getUTCDate() + n); return d.toISOString().slice(0, 10); };
  const wochentag = s => (tagDatum(s).getUTCDay() + 6) % 7 + 1;   // 1 = Mo … 7 = So
  const montag = s => plusTage(s, 1 - wochentag(s));
  const fWT = new Intl.DateTimeFormat('de-DE', { timeZone: 'UTC', weekday: 'long' });
  const fWTk = new Intl.DateTimeFormat('de-DE', { timeZone: 'UTC', weekday: 'short' });
  const fLang = new Intl.DateTimeFormat('de-DE', { timeZone: 'UTC', weekday: 'long', day: 'numeric', month: 'long' });
  const fKurz = new Intl.DateTimeFormat('de-DE', { timeZone: 'UTC', day: 'numeric', month: 'numeric' });
  const fMon = new Intl.DateTimeFormat('de-DE', { timeZone: 'UTC', day: 'numeric', month: 'short' });
  const zuMin = s => { const [h, m] = s.split(':'); return +h * 60 + +m; };
  const uhr = m => Math.floor(m / 60) + '.' + String(m % 60).padStart(2, '0');
  const dauer = m => m < 60 ? `${m} Min.` : `${Math.floor(m / 60)} Std.${m % 60 ? ' ' + (m % 60) + ' Min.' : ''}`;
  const wtKurz = s => fWTk.format(tagDatum(s)).replace('.', '');
  const kurzDatum = s => fKurz.format(tagDatum(s));

  /* =====================================================================
     Zustand
     ===================================================================== */
  const VARIANTEN = ['verein', 'studio'];
  const qv = new URLSearchParams(location.search).get('v');
  const z = { v: VARIANTEN.includes(qv) ? qv : 'verein', woche: 0, rubriken: new Set(), trainer: '', gruppe: '', halle: '', tag: null, filterOffen: false };
  let sim = null;                                      // { datum, min } oder null = Echtzeit
  const jetztHolen = () => sim ? { ...sim } : ortszeit(new Date());
  const W = () => WELTEN[z.v];
  const schmal = matchMedia('(max-width: 860px)');

  // Termine einer Woche (0 = Woche von heute, 1 = nächste) mit Ausnahmen und Ferienpause
  function termineDerWoche(woche, jetzt) {
    const w = W(), mo = plusTage(montag(jetzt.datum), woche * 7);
    return w.kurse.map(k => {
      const [id, wt, beginn, dauerMin, rubrik, titel, trainer, gruppe, halle, plaetze, belegt, anmeldung, info] = k;
      const datum = plusTage(mo, wt - 1);
      const a = w.ausnahmen.find(x => x.kurs === id && x.w === woche);
      const imUrlaub = w.pause && datum >= w.pause.von && datum <= w.pause.bis;
      const von = zuMin(beginn), bis = von + dauerMin;
      // nächste Woche ist noch weniger gebucht
      const b = Math.max(0, belegt - (woche ? (id.length % 3) + 2 : 0));
      return {
        id, key: id + '@' + datum, wt, datum, von, bis, rubrik, titel, gruppe, plaetze, belegt: b, anmeldung, info,
        trainer: a && a.art === 'vertretung' ? a.trainer : trainer, stammTrainer: trainer,
        halle: a && a.art === 'raum' ? a.halle : halle, stammHalle: halle,
        aus: imUrlaub ? { grund: w.pause.grund } : (a && a.art === 'aus' ? a : null),
        ausnahme: a || null,
      };
    });
  }
  const passt = t => (!z.rubriken.size || z.rubriken.has(t.rubrik)) && (!z.trainer || t.trainer === z.trainer) && (!z.gruppe || t.gruppe === z.gruppe) && (!z.halle || t.halle === z.halle);
  const filterAktiv = () => z.rubriken.size + (z.trainer ? 1 : 0) + (z.gruppe ? 1 : 0) + (z.halle ? 1 : 0);

  function zustandVon(t, jetzt, woche) {
    if (woche > 0) return 'zukunft';
    if (t.datum < jetzt.datum) return 'vorbei';
    if (t.datum > jetzt.datum) return 'zukunft';
    if (t.bis <= jetzt.min) return 'vorbei';
    if (t.von <= jetzt.min && !t.aus) return 'laeuft';
    if (t.von <= jetzt.min) return 'vorbei';
    return 'zukunft';
  }
  function platzText(t) {
    if (t.anmeldung === 'frei') return { text: 'Ohne Anmeldung, einfach kommen', klasse: '' };
    const frei = t.plaetze - t.belegt;
    if (frei <= 0) return { text: 'Ausgebucht · Warteliste', klasse: 'voll' };
    if (frei <= 3) return { text: frei === 1 ? 'Noch 1 Platz frei' : `Noch ${frei} Plätze frei`, klasse: 'knapp' };
    return { text: `${frei} Plätze frei`, klasse: '' };
  }

  // „Läuft gerade“ und „als Nächstes“ über diese und nächste Woche
  function liveLage(jetzt) {
    const alle = [...termineDerWoche(0, jetzt), ...termineDerWoche(1, jetzt)].filter(passt).filter(t => !t.aus);
    const laeuft = alle.filter(t => t.datum === jetzt.datum && t.von <= jetzt.min && jetzt.min < t.bis).sort((a, b) => a.bis - b.bis);
    const kommend = alle.filter(t => t.datum > jetzt.datum || (t.datum === jetzt.datum && t.von > jetzt.min)).sort((a, b) => (a.datum + uhr(a.von).padStart(5, '0')).localeCompare(b.datum + uhr(b.von).padStart(5, '0')));
    const erster = kommend[0];
    const naechste = erster ? kommend.filter(t => t.datum === erster.datum && t.von === erster.von) : [];
    return { laeuft, naechste, kommend };
  }
  function wannText(t, jetzt) {
    if (t.datum === jetzt.datum) { const d = t.von - jetzt.min; return d <= 90 ? `in ${dauer(d)}` : `heute ${uhr(t.von)} Uhr`; }
    if (t.datum === plusTage(jetzt.datum, 1)) return `morgen ${uhr(t.von)} Uhr`;
    return `${wtKurz(t.datum)} ${kurzDatum(t.datum)} ${uhr(t.von)} Uhr`;
  }

  /* =====================================================================
     Darstellung
     ===================================================================== */
  const raster = $('[data-raster]'), reiter = $('[data-reiter]'), leer = $('[data-leer]');
  const esc = s => String(s).replace(/[&<>"]/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[c]));
  let letzteLive = '', aktuelleTermine = [];

  function welt() {
    const w = W();
    html.dataset.v = z.v;
    $$('[data-text]').forEach(n => { const k = n.dataset.text; if (w.text[k] != null) n.innerHTML = w.text[k]; });
    $('[data-nav]').innerHTML = w.nav.map((n, i) => `<li><a href="#plan"${i === 1 ? ' aria-current="page"' : ''}>${n}</a></li>`).join('');
    $('[data-cta-text]').textContent = w.cta;
    $('[data-fuss]').innerHTML = w.fuss.map(([t, x]) => `<p><b>${t}</b>${x}</p>`).join('');
    // Filter aufbauen
    const chips = $('[data-chips]'), legende = chips.querySelector('legend');
    chips.replaceChildren(legende, ...Object.entries(w.rubriken).map(([id, r]) => {
      const b = document.createElement('button');
      b.type = 'button'; b.className = 'chip'; b.dataset.rubrik = id; b.style.setProperty('--farbe', r.farbe);
      b.setAttribute('aria-pressed', 'false');
      b.innerHTML = `<i aria-hidden="true"></i>${r.name}`;
      return b;
    }));
    const optionen = (sel, alle, eintraege) => { sel.innerHTML = `<option value="">${alle}</option>` + eintraege.map(([v, n]) => `<option value="${v}">${esc(n)}</option>`).join(''); };
    const genutzt = new Set([...w.kurse.map(k => k[6]), ...w.ausnahmen.filter(a => a.trainer).map(a => a.trainer)]);
    optionen($('[data-wahl="trainer"]'), 'Alle', Object.entries(w.trainer).filter(([k]) => genutzt.has(k)).sort((a, b) => a[1].localeCompare(b[1], 'de')));
    optionen($('[data-wahl="gruppe"]'), 'Alle', Object.entries(w.gruppen));
    optionen($('[data-wahl="halle"]'), 'Alle', Object.entries(w.hallen).map(([k, h]) => [k, h.name]));
  }

  function karte(t, zust, jetzt, naechsteKeys) {
    const w = W(), r = w.rubriken[t.rubrik], h = w.hallen[t.halle], p = platzText(t);
    let marke = '';
    if (zust === 'laeuft') marke = `<span class="termin__marke">Läuft · noch ${dauer(t.bis - jetzt.min)}</span>`;
    else if (naechsteKeys.has(t.key)) marke = `<span class="termin__marke">Als Nächstes${t.datum === jetzt.datum ? ' · ' + wannText(t, jetzt) : ''}</span>`;
    else if (zust === 'vorbei') marke = `<span class="sr">Vorbei. </span>`;
    let hinweis = '';
    if (t.aus) hinweis = `<span class="termin__aus">Fällt aus<span>${t.aus.grund ? ' · ' + esc(t.aus.grund.replace(/\.$/, '')) : ''}</span></span>`;
    else if (t.ausnahme && t.ausnahme.art === 'vertretung') hinweis = `<span class="termin__hinweis">Vertretung: ${esc(w.trainer[t.trainer])}</span>`;
    else if (t.ausnahme && t.ausnahme.art === 'raum') hinweis = `<span class="termin__hinweis">Diesmal: ${esc(h.kurz)}</span>`;
    const ende = `<span class="sr"> bis </span><span aria-hidden="true">–</span>${uhr(t.bis)}`;
    const zz = zust === 'zukunft' && naechsteKeys.has(t.key) ? 'naechstes' : zust;
    return `<button type="button" class="termin${t.aus ? ' ist-aus' : ''}" data-zustand="${zz}" data-key="${t.key}" style="--farbe:${r.farbe};view-transition-name:k-${t.id}">
      ${marke}
      <span class="termin__zeit">${uhr(t.von)}<span class="ende">${ende} Uhr</span></span>
      <span class="termin__abt">${r.name}</span>
      <span class="termin__titel">${t.titel}</span>
      <span class="termin__ort">${esc(h.kurz)} · ${esc(w.trainer[t.trainer].split(' ')[0])}</span>
      ${t.aus ? '' : `<span class="termin__platz ${p.klasse}">${p.text}</span>`}
      ${hinweis}
    </button>`;
  }

  function zeichnen() {
    const w = W(), jetzt = jetztHolen();
    const termine = termineDerWoche(z.woche, jetzt);
    aktuelleTermine = termine;
    const sichtbar = termine.filter(passt);
    const live = liveLage(jetzt);
    const naechsteKeys = new Set(live.naechste.map(t => t.key));
    const mo = plusTage(montag(jetzt.datum), z.woche * 7);
    const tage = [1, 2, 3, 4, 5, 6, 7].map(i => plusTage(mo, i - 1));

    // Wochenumschalter
    [0, 1].forEach(i => { const m = plusTage(montag(jetzt.datum), i * 7); const so = plusTage(m, 6); $(`[data-woche-text="${i}"]`).textContent = `${m.slice(5, 7) === so.slice(5, 7) ? tagDatum(m).getUTCDate() + '.' : fMon.format(tagDatum(m))}–${fMon.format(tagDatum(so))}`; });

    // Zeilen: Stunden mit Beginn im Filter; je Stunde so viele Unterzeilen wie parallel an einem Tag
    const stunden = [...new Set(sichtbar.map(t => Math.floor(t.von / 60)))].sort((a, b) => a - b);
    const tiefe = {};
    stunden.forEach(s => { tiefe[s] = Math.max(1, ...tage.map(d => sichtbar.filter(t => t.datum === d && Math.floor(t.von / 60) === s).length)); });
    const start = {}; let zeile = 2;
    stunden.forEach(s => { start[s] = zeile; zeile += tiefe[s]; });
    raster.style.gridTemplateRows = `auto ${stunden.map(s => `repeat(${tiefe[s]}, auto)`).join(' ')}`;

    // Tag für die Telefon-Reiter: heute, sonst Montag
    if (!z.tag || !tage.includes(z.tag)) z.tag = tage.includes(jetzt.datum) ? jetzt.datum : tage[0];

    const teile = [];
    stunden.forEach(s => teile.push(`<div class="raster__zeile" style="grid-row:${start[s]} / span ${tiefe[s]}" aria-hidden="true"></div>`));
    teile.push(`<div class="zeitachse" aria-hidden="true"><span></span>${stunden.map(s => `<span style="grid-row:${start[s]} / span ${tiefe[s]}">${s}.00</span>`).join('')}</div>`);
    tage.forEach((d, i) => {
      const liste = sichtbar.filter(t => t.datum === d).sort((a, b) => a.von - b.von);
      const heute = d === jetzt.datum, vorbei = d < jetzt.datum;
      const zaehler = {};
      const items = liste.map(t => {
        const s = Math.floor(t.von / 60); zaehler[s] = (zaehler[s] || 0);
        const r = start[s] - 1 + zaehler[s]++;      // Zeile innerhalb der Liste (Liste beginnt in Rasterzeile 2)
        return `<li style="grid-row:${r}">${karte(t, zustandVon(t, jetzt, z.woche), jetzt, naechsteKeys)}</li>`;
      }).join('');
      const kopf = `<h2 class="tag__kopf" id="tag-${i}"><span class="wt"><span class="wt-l">${fWT.format(tagDatum(d))}</span><span class="wt-k" aria-hidden="true">${wtKurz(d)}</span></span><span class="dt">${kurzDatum(d)}</span>${heute ? '<span class="heute">heute</span>' : ''}</h2>`;
      const inhalt = liste.length ? `<ol class="tag__liste" aria-label="${liste.length} ${liste.length === 1 ? 'Termin' : 'Termine'}">${items}</ol>` : `<p class="tag__leer">${termine.some(t => t.datum === d) ? 'Kein Treffer' : (z.v === 'verein' ? 'Kein Training' : 'Keine Stunde')}</p>`;
      teile.push(`<section class="tag${heute ? ' ist-heute' : ''}${vorbei ? ' ist-vorbei' : ''}" style="grid-column:${i + 2}" data-tag="${d}" aria-labelledby="tag-${i}" id="panel-${i}">${kopf}${inhalt}</section>`);
    });
    raster.innerHTML = teile.join('');

    // Telefon: Tages-Reiter
    reiter.innerHTML = tage.map((d, i) => {
      const n = sichtbar.filter(t => t.datum === d && !t.aus).length;
      return `<button type="button" role="tab" id="reiter-${i}" aria-controls="panel-${i}" data-tag="${d}" aria-selected="${d === z.tag}" class="${d === jetzt.datum ? 'ist-heute' : ''}${d < jetzt.datum ? ' ist-vorbei' : ''}"><span class="wt">${wtKurz(d)}</span><span class="zahl">${kurzDatum(d)}</span><span class="n">${n ? n + '<span class="sr"> Termine</span>' : '–<span class="sr">keine Termine</span>'}</span></button>`;
    }).join('');
    modus();

    // Leerzustand
    const n = sichtbar.length;
    const anzahlWoche = termine.length;
    const nAndere = termineDerWoche(z.woche ? 0 : 1, jetzt).filter(passt).length;
    leer.hidden = n > 0;
    raster.hidden = n === 0;
    reiter.hidden = n === 0 || !schmal.matches;
    if (!n) {
      $('[data-leer-titel]').textContent = z.woche ? 'Nächste Woche passt nichts.' : 'Diese Woche passt nichts.';
      $('[data-leer-text]').textContent = nAndere ? `In der ${z.woche ? 'Woche davor' : 'nächsten Woche'} gibt es ${nAndere} passende ${nAndere === 1 ? 'Termin' : 'Termine'}. Oder du lockerst die Auswahl.` : `Diese Kombination gibt es im ganzen Plan nicht. Lockere die Auswahl oder ruf uns an, wir finden eine passende Gruppe.`;
      const wb = $('[data-leer-woche]');
      wb.hidden = !nAndere; wb.textContent = z.woche ? 'Zur Woche davor' : 'Zur nächsten Woche';
    }
    const fa = filterAktiv();
    $('[data-treffer]').innerHTML = fa ? `<b>${n}</b> von ${anzahlWoche} Terminen` : `<b>${anzahlWoche}</b> Termine ${z.woche ? 'nächste' : 'diese'} Woche`;
    $$('[data-zuruecksetzen]').forEach(b => { if (b.closest('.treffer')) b.hidden = !fa; });
    $('[data-filter-zahl]').textContent = fa || '';
    $$('[data-rubrik]').forEach(b => b.setAttribute('aria-pressed', String(z.rubriken.has(b.dataset.rubrik))));
    ['trainer', 'gruppe', 'halle'].forEach(k => { const s = $(`[data-wahl="${k}"]`); s.value = z[k]; s.classList.toggle('aktiv', !!z[k]); });

    jetztZeichnen(jetzt, live);
    pille();
  }

  function jetztZeichnen(jetzt, live) {
    const w = W();
    const zeile = t => `<li><button type="button" data-oeffne="${t.key}" data-w="${t.datum >= plusTage(montag(jetzt.datum), 7) ? 1 : 0}"><strong>${t.titel}</strong><small>${esc(w.hallen[t.halle].kurz)} · ${t === undefined ? '' : (live.laeuft.includes(t) ? `bis ${uhr(t.bis)} Uhr` : wannText(t, jetzt))}</small></button></li>`;
    const lauf = live.laeuft.slice(0, 3);
    $('[data-laeuft]').innerHTML = lauf.length ? `<ul>${lauf.map(zeile).join('')}</ul>${live.laeuft.length > 3 ? `<p class="ruhig">und ${live.laeuft.length - 3} weitere</p>` : ''}` : `<p class="ruhig">${jetzt.min >= 21 * 60 + 30 || jetzt.min < 7 * 60 ? 'Gerade ist Ruhe in den Hallen.' : 'Gerade läuft nichts.'}</p>`;
    $$('[data-v="studio"] [data-laeuft] .ruhig').forEach(p => { if (!lauf.length) p.textContent = jetzt.min >= 21 * 60 + 30 || jetzt.min < 7 * 60 ? 'Das Studio schläft.' : 'Gerade läuft keine Stunde.'; });
    const nae = live.naechste.slice(0, 2);
    $('[data-naechstes]').innerHTML = nae.length ? `<ul>${nae.map(zeile).join('')}</ul>${live.naechste.length > 2 ? `<p class="ruhig">und ${live.naechste.length - 2} weitere zur selben Zeit</p>` : ''}` : `<p class="ruhig">In den nächsten zwei Wochen nichts für diese Auswahl.</p>`;
    $('[data-jetzt-zeit]').textContent = `${wtKurz(jetzt.datum)} ${kurzDatum(jetzt.datum)} · ${uhr(jetzt.min)} Uhr, Ortszeit Minden`;
    $('[data-jetzt-fuss]').textContent = filterAktiv() ? 'Für deine Auswahl. Ausfälle sind schon berücksichtigt.' : 'Ausfälle sind schon berücksichtigt.';
    // Screenreader: nur Wechsel ansagen
    const sig = live.laeuft.map(t => t.key).join() + '|' + live.naechste.map(t => t.key).join();
    if (letzteLive && sig !== letzteLive && !html.classList.contains('vorfuehrung')) {
      $('[data-ansage]').textContent = (live.laeuft.length ? `Läuft gerade: ${live.laeuft.map(t => t.titel.replace(/\u00ad/g, '')).join(', ')}. ` : '') + (live.naechste[0] ? `Als Nächstes: ${live.naechste[0].titel.replace(/\u00ad/g, '')}, ${wannText(live.naechste[0], jetzt)}.` : '');
    }
    letzteLive = sig;
    $('[data-sim-anzeige]').textContent = uhr(jetzt.min);
    if (!sim) { $('[data-sim-zeit]').value = Math.min(1380, Math.max(360, jetzt.min)); tagAuswahl(jetzt.datum); }
  }

  /* ---------- Raster oder Reiter ---------- */
  const KANN_FG = 'focusGroup' in HTMLElement.prototype;
  function modus() {
    const tabs = $$('[role="tab"]', reiter), sektionen = $$('.tag', raster);
    if (schmal.matches) {
      reiter.hidden = false;
      if (KANN_FG) reiter.setAttribute('focusgroup', 'tablist nomemory'); else reiter.removeAttribute('focusgroup');
      tabs.forEach(b => { const an = b.dataset.tag === z.tag; b.setAttribute('aria-selected', String(an)); if (KANN_FG) { b.removeAttribute('tabindex'); b.toggleAttribute('focusgroupstart', an); } else b.tabIndex = an ? 0 : -1; });
      sektionen.forEach((s, i) => { s.setAttribute('role', 'tabpanel'); s.setAttribute('aria-labelledby', 'reiter-' + i); s.hidden = s.dataset.tag !== z.tag; s.tabIndex = -1; });
    } else {
      reiter.hidden = true;
      sektionen.forEach((s, i) => { s.removeAttribute('role'); s.setAttribute('aria-labelledby', 'tag-' + i); s.hidden = false; s.removeAttribute('tabindex'); });
    }
  }
  function waehleTag(d, fokus) {
    z.tag = d;
    modus();
    const b = $(`[role="tab"][data-tag="${d}"]`, reiter);
    if (fokus && b) b.focus();
  }
  reiter.addEventListener('click', e => { const b = e.target.closest('[role="tab"]'); if (b) waehleTag(b.dataset.tag, false); });
  reiter.addEventListener('keydown', e => {
    const tabs = $$('[role="tab"]', reiter), i = tabs.indexOf(document.activeElement);
    if (i < 0) return;
    let n = null;
    if (e.key === 'ArrowRight') n = (i + 1) % tabs.length;
    else if (e.key === 'ArrowLeft') n = (i - 1 + tabs.length) % tabs.length;
    else if (e.key === 'Home') n = 0;
    else if (e.key === 'End') n = tabs.length - 1;
    if (n === null) return;
    e.preventDefault();
    waehleTag(tabs[n].dataset.tag, true);       // Auswahl folgt dem Fokus
  });
  reiter.addEventListener('focusin', e => { const b = e.target.closest('[role="tab"]'); if (b && KANN_FG && b.dataset.tag !== z.tag) waehleTag(b.dataset.tag, false); });

  /* ---------- Filter ---------- */
  function neu(fn) {                          // Zustandswechsel mit View Transition (wenn möglich)
    fn();
    if (document.startViewTransition && !reduziert() && !schmal.matches) document.startViewTransition(zeichnen);
    else zeichnen();
  }
  $('[data-chips]').addEventListener('click', e => {
    const b = e.target.closest('[data-rubrik]'); if (!b) return;
    neu(() => { const r = b.dataset.rubrik; z.rubriken.has(r) ? z.rubriken.delete(r) : z.rubriken.add(r); });
  });
  $$('[data-wahl]').forEach(s => s.addEventListener('change', () => neu(() => { z[s.dataset.wahl] = s.value; })));
  $$('[data-zuruecksetzen]').forEach(b => b.addEventListener('click', () => {
    neu(() => { z.rubriken.clear(); z.trainer = z.gruppe = z.halle = ''; });
    const ziel = $('[data-rubrik]'); if (b.closest('.leer') && ziel) ziel.focus();
  }));
  $('[data-woche]').addEventListener('change', e => neu(() => { z.woche = +e.target.value; z.tag = null; }));
  $('[data-leer-woche]').addEventListener('click', () => { const w = z.woche ? 0 : 1; $(`[name="woche"][value="${w}"]`).checked = true; neu(() => { z.woche = w; z.tag = null; }); });
  function pille() {
    const l = $('[data-woche] input:checked').parentElement, p = $('[data-woche-pille]');
    p.style.width = l.offsetWidth + 'px'; p.style.translate = l.offsetLeft + 'px 0';
  }
  const fKnopf = $('[data-filter-knopf]'), fPanel = $('[data-filter]');
  function filterPanel() {
    const zu = schmal.matches && !z.filterOffen;
    fPanel.hidden = zu;
    fKnopf.setAttribute('aria-expanded', String(!zu));
  }
  fKnopf.addEventListener('click', () => { z.filterOffen = !z.filterOffen; filterPanel(); });
  schmal.addEventListener('change', () => { filterPanel(); zeichnen(); });

  /* =====================================================================
     Dialog: Termin, Probetraining, Warteliste, ICS
     ===================================================================== */
  const dlg = $('[data-dialog]'), dInhalt = $('[data-dlg-inhalt]');
  let dTermin = null, dAusloeser = null;
  const ikon = {
    info: '<svg viewBox="0 0 18 18" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="1.6"><circle cx="9" cy="9" r="7.2"/><path d="M9 8v4.6M9 5.4v.4" stroke-linecap="round"/></svg>',
    kal: '<svg viewBox="0 0 18 18" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="1.6"><rect x="2.2" y="3.4" width="13.6" height="12.4" rx="1.5"/><path d="M2.2 7.2h13.6M6 1.6v3.4M12 1.6v3.4M9 9.6v4M7 11.6h4"/></svg>',
    ok: '<svg viewBox="0 0 18 18" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="1.7"><path d="M3 9.5 7 13.5 15 4.5"/></svg>',
  };
  function oeffne(key, woche) {
    const jetzt = jetztHolen();
    const liste = woche == null ? aktuelleTermine : termineDerWoche(woche, jetzt);
    const t = liste.find(x => x.key === key) || [...termineDerWoche(0, jetzt), ...termineDerWoche(1, jetzt)].find(x => x.key === key);
    if (!t) return;
    dTermin = t; dAusloeser = document.activeElement;
    ansicht('termin');
    if (!dlg.open) dlg.showModal();
    $('[data-dlg-titel]').focus();
  }
  function ansicht(art, extra) {
    const w = W(), t = dTermin, r = w.rubriken[t.rubrik], h = w.hallen[t.halle], jetzt = jetztHolen();
    dlg.style.setProperty('--farbe', r.farbe);
    $('[data-dlg-abt]').textContent = `${r.name} · ${w.gruppen[t.gruppe]}`;
    $('[data-dlg-titel]').textContent = t.titel.replace(/\u00ad/g, '');
    const datumLang = fLang.format(tagDatum(t.datum));
    const zust = zustandVon(t, jetzt, t.datum >= plusTage(montag(jetzt.datum), 7) ? 1 : 0);
    const p = platzText(t);
    if (art === 'termin') {
      let status = '';
      if (t.aus) {
        const naechster = naechsterTermin(t);
        status = `<div class="dlg__status ist-aus">${ikon.info}<p><b>Fällt am ${datumLang} aus.</b> ${esc(t.aus.grund || '')}${naechster ? ` Nächster Termin: ${wtKurz(naechster.datum)} ${kurzDatum(naechster.datum)}, ${uhr(naechster.von)} Uhr.` : ''}</p></div>`;
      } else if (zust === 'laeuft') status = `<div class="dlg__status">${ikon.info}<p><b>Läuft gerade,</b> noch ${dauer(t.bis - jetzt.min)}. Zum Reinschnuppern lieber den nächsten Termin nehmen.</p></div>`;
      else if (zust === 'vorbei') status = `<div class="dlg__status">${ikon.info}<p><b>Dieser Termin ist vorbei.</b> Die Gruppe trifft sich wieder am ${fLang.format(tagDatum(plusTage(t.datum, 7)))}.</p></div>`;
      else if (t.ausnahme && t.ausnahme.art === 'vertretung') status = `<div class="dlg__status">${ikon.info}<p><b>Vertretung:</b> ${esc(w.trainer[t.trainer])} übernimmt für ${esc(w.trainer[t.stammTrainer])}. ${esc(t.ausnahme.grund || '')}</p></div>`;
      else if (t.ausnahme && t.ausnahme.art === 'raum') status = `<div class="dlg__status">${ikon.info}<p><b>Diesmal in ${esc(h.name)}.</b> ${esc(t.ausnahme.grund || '')}</p></div>`;
      const kannBuchen = !t.aus && zust === 'zukunft';
      const voll = p.klasse === 'voll';
      const aktion = !kannBuchen ? '' : t.anmeldung === 'frei'
        ? `<button type="button" class="knopf knopf--cta" data-formular="probe">${ikon.ok}Bescheid geben, dass ich komme</button>`
        : `<button type="button" class="knopf knopf--cta" data-formular="${voll ? 'warte' : 'probe'}">${voll ? w.warteliste : w.ctaLang}</button>`;
      const ausnahmenZahl = exdates(t).length;
      dInhalt.innerHTML = `${status}
        <dl class="angaben">
          <dt>Termin</dt><dd class="zahl">${datumLang}, ${uhr(t.von)}–${uhr(t.bis)} Uhr<small>jede Woche ${fWT.format(tagDatum(t.datum))} · ${w.saisonText}</small></dd>
          <dt>${w.halleWort}</dt><dd>${esc(h.name)}<small>${esc(h.adr)}</small></dd>
          <dt>${w.text['trainer-label']}</dt><dd>${esc(w.trainer[t.trainer])}${t.trainer !== t.stammTrainer ? `<small>Vertretung für ${esc(w.trainer[t.stammTrainer])}</small>` : ''}</dd>
          <dt>${w.gruppeWort}</dt><dd>${w.gruppen[t.gruppe]}</dd>
          ${t.aus ? '' : `<dt>Plätze</dt><dd>${p.text}${t.anmeldung === 'kurs' && p.klasse !== 'voll' ? '<small>Anmeldung nötig, Plätze sind begrenzt</small>' : ''}</dd>`}
        </dl>
        <p class="dlg__text">${esc(t.info)}</p>
        <div class="dlg__aktionen">${aktion}<button type="button" class="knopf knopf--linie" data-ics aria-label="Alle Termine von ${esc(t.titel.replace(/\u00ad/g, ''))} in den Kalender, als .ics-Datei">${ikon.kal}In den Kalender</button></div>
        <p class="dlg__notiz">Die Kalenderdatei wiederholt sich jede Woche und lässt ${ausnahmenZahl === 1 ? 'einen Ausfall' : ausnahmenZahl + ' Ausfälle'} und Pausen weg.</p>`;
    } else if (art === 'form') {
      const warte = extra === 'warte', kind = t.gruppe === 'kinder' || t.gruppe === 'jugend';
      dInhalt.innerHTML = `
        <div class="dlg__status">${ikon.info}<p><b>${warte ? w.warteliste : (t.anmeldung === 'frei' ? 'Kurz Bescheid geben' : w.probeWort)}:</b> ${wtKurz(t.datum)} ${kurzDatum(t.datum)}, ${uhr(t.von)}–${uhr(t.bis)} Uhr, ${esc(h.kurz)}.</p></div>
        <form class="formular" novalidate data-form="${warte ? 'warte' : 'probe'}">
          <div class="feld"><label for="f-name">${kind ? 'Dein Name (erziehungsberechtigt)' : 'Vor- und Nachname'}</label><input id="f-name" type="text" autocomplete="name" required aria-describedby="e-name"><span class="fehler" id="e-name"></span></div>
          ${kind ? `<div class="feld"><label for="f-kind">Name und Alter des Kindes</label><input id="f-kind" type="text" required aria-describedby="e-kind" placeholder="z. B. Emil, 7"><span class="fehler" id="e-kind"></span></div>` : ''}
          <div class="feld"><label for="f-mail">E-Mail für die Bestätigung</label><input id="f-mail" type="email" autocomplete="email" required aria-describedby="e-mail"><span class="fehler" id="e-mail"></span></div>
          <div class="feld feld--check"><input id="f-ok" type="checkbox" required aria-describedby="e-ok"><label for="f-ok">Ich habe die <a href="#plan">Datenschutzhinweise</a> gelesen. Die Daten gehen nur an die Übungsleitung.</label><span class="fehler" id="e-ok"></span></div>
          <div class="dlg__aktionen"><button type="submit" class="knopf knopf--cta">${warte ? 'Auf die Warteliste setzen' : 'Platz vormerken'}</button><button type="button" class="knopf knopf--linie" data-zurueck>Zurück</button></div>
          <p class="dlg__notiz">Demo: Es wird nichts gesendet.</p>
        </form>`;
    } else if (art === 'erfolg') {
      const warte = extra === 'warte';
      dInhalt.innerHTML = `<div class="erfolg" tabindex="-1" data-erfolg>
        <h3>${warte ? 'Du stehst auf der Warteliste.' : 'Platz ist vorgemerkt.'}</h3>
        <p>${warte ? 'Wird ein Platz frei, schreiben wir dir sofort. Du hast dann 2 Stunden Zeit zum Bestätigen.' : `Bis ${wtKurz(t.datum)} ${kurzDatum(t.datum)} um ${uhr(t.von)} Uhr. ${esc(w.trainer[t.trainer].split(' ')[0])} weiß Bescheid.`}</p>
        <ul><li>${esc(h.name)}, ${esc(h.adr)}</li><li>${z.v === 'verein' ? 'Hallenschuhe, Getränk, bequeme Kleidung' : 'Bequeme Kleidung. Matten und Blöcke liegen bereit.'}</li><li>Bitte 10 Minuten vorher da sein.</li></ul>
        <div class="dlg__aktionen"><button type="button" class="knopf knopf--linie" data-ics>${ikon.kal}In den Kalender</button><button type="button" class="knopf" data-dlg-schliessen>Fertig</button></div>
      </div>`;
      $('[data-erfolg]').focus();
    }
  }
  function naechsterTermin(t) {
    const jetzt = jetztHolen();
    return [...termineDerWoche(0, jetzt), ...termineDerWoche(1, jetzt)].filter(x => x.id === t.id && x.datum > t.datum && !x.aus)[0];
  }
  dInhalt.addEventListener('click', e => {
    const f = e.target.closest('[data-formular]');
    if (f) { ansicht('form', f.dataset.formular); $('#f-name').focus(); return; }
    if (e.target.closest('[data-zurueck]')) { ansicht('termin'); $('[data-dlg-titel]').focus(); return; }
    if (e.target.closest('[data-ics]')) { herunterladen(dTermin, e.target.closest('[data-ics]')); return; }
    if (e.target.closest('[data-dlg-schliessen]')) dlg.close();
  });
  dInhalt.addEventListener('submit', e => {
    e.preventDefault();
    const form = e.target, fehler = [];
    const pruefe = (id, ok, text) => { const el = $('#' + id); if (!el) return; const feld = el.closest('.feld'); const sp = $('#e-' + id.slice(2)); if (ok(el)) { feld.removeAttribute('data-fehler'); el.removeAttribute('aria-invalid'); sp.textContent = ''; } else { feld.setAttribute('data-fehler', ''); el.setAttribute('aria-invalid', 'true'); sp.textContent = text; fehler.push(el); } };
    pruefe('f-name', el => el.value.trim().length > 1, 'Bitte gib deinen Namen ein.');
    pruefe('f-kind', el => el.value.trim().length > 1, 'Bitte Name und Alter des Kindes eintragen.');
    pruefe('f-mail', el => /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(el.value.trim()), 'Bitte eine E-Mail-Adresse wie name@beispiel.de eingeben.');
    pruefe('f-ok', el => el.checked, 'Bitte bestätige die Datenschutzhinweise.');
    if (fehler.length) { fehler[0].focus(); return; }
    ansicht('erfolg', form.dataset.form);
  });
  $('[data-dlg-zu]').addEventListener('click', () => dlg.close());
  dlg.addEventListener('click', e => { if (e.target === dlg) dlg.close(); });   // Klick auf den Hintergrund
  dlg.addEventListener('close', () => {
    const key = dTermin && dTermin.key;
    const ziel = (dAusloeser && dAusloeser.isConnected) ? dAusloeser : (key && $(`[data-key="${key}"]`));
    if (ziel && !html.classList.contains('vorfuehrung')) ziel.focus();
  });
  document.addEventListener('click', e => {
    const k = e.target.closest('.termin[data-key]'); if (k) { oeffne(k.dataset.key); return; }
    const j = e.target.closest('[data-oeffne]'); if (j) oeffne(j.dataset.oeffne, +j.dataset.w);
  });

  /* ---------- ICS je Kurs (RFC 5545): Wiederholung, Ausfälle als EXDATE, Vertretung/Raum als RECURRENCE-ID ---------- */
  const enc = new TextEncoder();
  const icsText = s => String(s).replace(/\u00ad/g, '').replace(/\\/g, '\\\\').replace(/;/g, '\\;').replace(/,/g, '\\,').replace(/\r?\n/g, '\\n');
  function falte(zeile) {
    let aus = '', laenge = 0;
    for (const c of zeile) { const b = enc.encode(c).length; if (laenge + b > 75) { aus += '\r\n '; laenge = 1; } aus += c; laenge += b; }
    return aus;
  }
  const lokal = (datum, min) => datum.replace(/-/g, '') + 'T' + String(Math.floor(min / 60)).padStart(2, '0') + String(min % 60).padStart(2, '0') + '00';
  const stempel = () => new Date().toISOString().replace(/[-:]/g, '').replace(/\.\d+/, '');
  const BYDAY = ['', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA', 'SU'];
  const VTIMEZONE = ['BEGIN:VTIMEZONE', 'TZID:Europe/Berlin',
    'BEGIN:DAYLIGHT', 'TZOFFSETFROM:+0100', 'TZOFFSETTO:+0200', 'TZNAME:CEST', 'DTSTART:19700329T020000', 'RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU', 'END:DAYLIGHT',
    'BEGIN:STANDARD', 'TZOFFSETFROM:+0200', 'TZOFFSETTO:+0100', 'TZNAME:CET', 'DTSTART:19701025T030000', 'RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU', 'END:STANDARD',
    'END:VTIMEZONE'];
  function erster(t) { const jetzt = jetztHolen(); return termineDerWoche(0, jetzt).find(x => x.id === t.id); }
  function exdates(t) {
    const w = W(), e = erster(t), aus = [];
    w.ausnahmen.filter(a => a.kurs === t.id && a.art === 'aus').forEach(a => aus.push(plusTage(e.datum, a.w * 7)));
    if (w.pause) for (let d = e.datum; d <= w.pause.bis; d = plusTage(d, 7)) if (d >= w.pause.von) aus.push(d);
    return [...new Set(aus)].sort();
  }
  function ics(t) {
    const w = W(), e = erster(t), h = w.hallen[e.stammHalle], r = w.rubriken[t.rubrik];
    const uid = `${t.id}@${w.domain}`;
    // UNTIL in UTC: Saisonende 23.59 Uhr Ortszeit (Sommerzeit +2, sonst +1)
    const ende = w.saisonEnde.replace(/-/g, '') + 'T' + (w.saisonEnde.slice(5, 7) > '03' && w.saisonEnde.slice(5, 7) < '11' ? '215959Z' : '225959Z');
    const z1 = ['BEGIN:VCALENDAR', 'VERSION:2.0', `PRODID:-//${w.marke}//Kursplan//DE`, 'CALSCALE:GREGORIAN', 'METHOD:PUBLISH', ...VTIMEZONE,
      'BEGIN:VEVENT', 'UID:' + uid, 'DTSTAMP:' + stempel(),
      'DTSTART;TZID=Europe/Berlin:' + lokal(e.datum, e.von), 'DTEND;TZID=Europe/Berlin:' + lokal(e.datum, e.bis),
      `RRULE:FREQ=WEEKLY;BYDAY=${BYDAY[t.wt]};UNTIL=${ende}`];
    const ex = exdates(t);
    if (ex.length) z1.push('EXDATE;TZID=Europe/Berlin:' + ex.map(d => lokal(d, e.von)).join(','));
    z1.push('SUMMARY:' + icsText(`${t.titel} · ${w.kurzmarke}`), 'LOCATION:' + icsText(`${h.name}, ${h.adr}`),
      'DESCRIPTION:' + icsText(`${r.name}, ${w.gruppen[t.gruppe]}. ${w.text['trainer-label']}: ${w.trainer[t.stammTrainer]}.\nAusfälle und Vertretungen stehen im Kursplan: https://${w.domain}/kursplan`),
      'CATEGORIES:' + icsText(r.name), `URL:https://${w.domain}/kursplan#${t.id}`, 'END:VEVENT');
    // Einzelne Änderungen (Vertretung, anderer Raum) als eigene VEVENT mit RECURRENCE-ID
    w.ausnahmen.filter(a => a.kurs === t.id && a.art !== 'aus').forEach(a => {
      const d = plusTage(e.datum, a.w * 7), hh = w.hallen[a.halle || e.stammHalle];
      z1.push('BEGIN:VEVENT', 'UID:' + uid, 'DTSTAMP:' + stempel(), 'RECURRENCE-ID;TZID=Europe/Berlin:' + lokal(d, e.von),
        'DTSTART;TZID=Europe/Berlin:' + lokal(d, e.von), 'DTEND;TZID=Europe/Berlin:' + lokal(d, e.bis),
        'SUMMARY:' + icsText(`${t.titel} · ${a.art === 'vertretung' ? 'Vertretung' : 'anderer Raum'}`), 'LOCATION:' + icsText(`${hh.name}, ${hh.adr}`),
        'DESCRIPTION:' + icsText(a.art === 'vertretung' ? `Vertretung: ${w.trainer[a.trainer]}.` : `Diesmal in ${hh.name}. ${a.grund || ''}`), 'END:VEVENT');
    });
    z1.push('END:VCALENDAR');
    return z1.map(falte).join('\r\n') + '\r\n';
  }
  const meldung = $('[data-meldung]'); let meldungUhr;
  function melde(titel, info) {
    $('b', meldung).textContent = titel; $('small', meldung).textContent = info;
    meldung.classList.add('an'); clearTimeout(meldungUhr);
    meldungUhr = setTimeout(() => meldung.classList.remove('an'), 3600);
  }
  function herunterladen(t, knopf) {
    const text = ics(t), w = W();
    const blob = new Blob([text], { type: 'text/calendar;charset=utf-8' });
    const name = `${w.datei}-${t.id}.ics`;
    if (!html.classList.contains('vorfuehrung')) {       // in der Vorführung nur erzeugen
      const url = URL.createObjectURL(blob), a = document.createElement('a');
      a.href = url; a.download = name; document.body.append(a); a.click(); a.remove();
      setTimeout(() => URL.revokeObjectURL(url), 4000);
    }
    const ex = exdates(t).length;
    melde(name, `jede Woche ${fWT.format(tagDatum(erster(t).datum))} ${uhr(t.von)} Uhr · ${ex} ${ex === 1 ? 'Ausnahme' : 'Ausnahmen'} · ${(blob.size / 1024).toLocaleString('de-DE', { maximumFractionDigits: 1 })} KB`);
    if (knopf) knopf.innerHTML = `${ikon.ok}Datei erstellt`;
  }

  /* =====================================================================
     Simulierte Uhr im Pult
     ===================================================================== */
  const tagWahl = $('[data-sim-tag]'), zeitWahl = $('[data-sim-zeit]'), echt = $('[data-echtzeit]');
  function tagOptionen() {
    const mo = montag(ortszeit(new Date()).datum);
    tagWahl.innerHTML = [0, 1, 2, 3, 4, 5, 6].map(i => { const d = plusTage(mo, i); return `<option value="${d}">${wtKurz(d)} ${kurzDatum(d)}</option>`; }).join('');
  }
  function tagAuswahl(d) { if (![...tagWahl.options].some(o => o.value === d)) tagOptionen(); tagWahl.value = d; }
  function simSetzen(datum, min) {
    sim = { datum, min }; echt.setAttribute('aria-pressed', 'false');
    tagAuswahl(datum); zeitWahl.value = min;
    zeichnen();
  }
  tagWahl.addEventListener('change', () => { z.tag = null; simSetzen(tagWahl.value, +zeitWahl.value); });
  zeitWahl.addEventListener('input', () => simSetzen(tagWahl.value, +zeitWahl.value));
  echt.addEventListener('click', () => { sim = null; z.tag = null; echt.setAttribute('aria-pressed', 'true'); zeichnen(); });
  let takt = null;
  function ticken() { clearTimeout(takt); if (!sim && !dlg.open) zeichnen(); takt = setTimeout(ticken, 60000 - Date.now() % 60000 + 50); }
  document.addEventListener('visibilitychange', () => { if (!document.hidden) ticken(); });

  /* ---------- Variante ---------- */
  function setzeVariante(v, schreiben) {
    z.v = v; z.rubriken.clear(); z.trainer = z.gruppe = z.halle = ''; z.tag = null;
    if (dlg.open) dlg.close();
    $$('[data-variante]').forEach(b => b.setAttribute('aria-pressed', String(b.dataset.variante === v)));
    welt(); zeichnen();
    if (schreiben) { const p = new URLSearchParams(location.search); p.set('v', v); history.replaceState(null, '', location.pathname + '?' + p.toString().replace(/=(?=&|$)/g, '')); }
  }
  $$('[data-variante]').forEach(b => b.addEventListener('click', () => setzeVariante(b.dataset.variante, true)));

  tagOptionen();
  filterPanel();
  setzeVariante(z.v, false);
  ticken();
  addEventListener('resize', pille);
  if (document.fonts) document.fonts.ready.then(pille);

  /* =====================================================================
     Vorführung: Uhr läuft, Filter setzen, Termin öffnen, Woche wechseln
     ===================================================================== */
  const zeiger = $('[data-zeiger]');
  let lauf = 0, angehalten = false, wartend = null;
  const warte = (ms, id) => new Promise(res => { const ende = performance.now() + ms; const tick = () => { if (id !== lauf) return; if (angehalten) { wartend = tick; return; } const r = ende - performance.now(); r <= 0 ? res() : setTimeout(tick, Math.min(r, 120)); }; tick(); });
  let zx = innerWidth * .6, zy = innerHeight * .8;
  async function zeigeAuf(el, id) {
    if (!el) return;
    const r = el.getBoundingClientRect(), x = r.left + Math.min(r.width * .5, 60), y = r.top + Math.min(r.height * .5, 22);
    if (reduziert()) { zeiger.style.transition = 'none'; } else zeiger.style.transition = `opacity 200ms var(--kurve), translate 620ms var(--kurve)`;
    zeiger.style.translate = `${x}px ${y}px`; zx = x; zy = y;
    await warte(reduziert() ? 120 : 680, id);
  }
  async function klick(el, id) { await zeigeAuf(el, id); if (id !== lauf || !el) return; zeiger.classList.add('drueckt'); await warte(140, id); zeiger.classList.remove('drueckt'); el.click(); }
  async function uhrLaeuft(datum, von, bis, ms, id) {
    const t0 = performance.now();
    while (id === lauf) {
      if (angehalten) { await warte(100, id); continue; }
      const p = Math.min(1, (performance.now() - t0) / ms);
      const m = Math.round((von + (bis - von) * p) / 5) * 5;
      if (!sim || sim.min !== m || sim.datum !== datum) simSetzen(datum, m);
      if (p >= 1) break;
      await warte(90, id);
    }
  }
  async function schleife(id) {
    const mo = montag(ortszeit(new Date()).datum);
    const verein = z.v === 'verein';
    while (id === lauf) {
      if (dlg.open) dlg.close();
      z.rubriken.clear(); z.trainer = z.gruppe = z.halle = ''; z.woche = 0; $('[name="woche"][value="0"]').checked = true;
      // 1. Uhr läuft: was gerade läuft und was als Nächstes kommt, wechselt
      const tag = plusTage(mo, verein ? 0 : 0);
      await uhrLaeuft(tag, verein ? 16 * 60 + 20 : 16 * 60 + 50, verein ? 17 * 60 + 25 : 17 * 60 + 45, 2600, id);
      await warte(700, id);
      // 2. Filter: eine Abteilung, dann eine zweite
      await klick($(`[data-rubrik="${verein ? 'hand' : 'pilates'}"]`), id); await warte(1500, id);
      await klick($(`[data-rubrik="${verein ? 'gesund' : 'yin'}"]`), id); await warte(1500, id);
      // 3. Altersgruppe/Niveau per Auswahl
      const sel = $('[data-wahl="gruppe"]');
      await zeigeAuf(sel, id); if (id !== lauf) return;
      neu(() => { z.gruppe = verein ? 'senioren' : 'ein'; }); await warte(1700, id);
      // 4. Termin öffnen, Probetraining wählen
      const ziel = $$('.termin[data-key]').find(b => b.dataset.zustand === 'zukunft' && !b.classList.contains('ist-aus')) || $('.termin[data-key]');
      await klick(ziel, id); await warte(1500, id);
      const cta = $('[data-formular]', dInhalt);
      if (cta) { await klick(cta, id); await warte(1300, id); }
      await klick($('[data-dlg-zu]'), id); await warte(500, id);
      // 5. Alle Filter löschen, nächste Woche mit Ausfällen
      await klick($('.treffer [data-zuruecksetzen]'), id); await warte(900, id);
      const nw = $('[name="woche"][value="1"]');
      await zeigeAuf(nw.parentElement, id); if (id !== lauf) return;
      nw.checked = true; nw.dispatchEvent(new Event('change', { bubbles: true })); await warte(2000, id);
      const vw = $('[name="woche"][value="0"]');
      await zeigeAuf(vw.parentElement, id); if (id !== lauf) return;
      vw.checked = true; vw.dispatchEvent(new Event('change', { bubbles: true })); await warte(900, id);
    }
  }
  function vorfuehren() {
    lauf++; angehalten = false;
    html.classList.add('vorfuehrung');
    zeiger.style.translate = `${innerWidth * .6}px ${innerHeight * .85}px`;
    schleife(lauf);
  }
  function pause() { angehalten = true; }
  function weiter() { if (!angehalten) return; angehalten = false; const w = wartend; wartend = null; if (w) w(); }
  function neustart() {
    lauf++; angehalten = false;
    html.classList.remove('vorfuehrung');
    if (dlg.open) dlg.close();
    sim = null; echt.setAttribute('aria-pressed', 'true');
    z.rubriken.clear(); z.trainer = z.gruppe = z.halle = ''; z.woche = 0; z.tag = null; $('[name="woche"][value="0"]').checked = true;
    zeichnen();
  }

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