Notdienst mit Plan und Verweis
„Wir haben jetzt Notdienst, bis morgen 9 Uhr“ oder „Heute hat eine andere Apotheke Notdienst“: Status, nächster eigener Dienst und Plan mit Feiertagen, sonst Verweis auf die Notdienstsuche der Kammer, dazu Notfall- und Gebührenhinweis.
Öffentliche Apotheke mit Notdienst nach dem Plan der Landesapothekerkammer, als Reiter der Statuszeile oder eigene Sektion.
Ein Baustein für Betriebe mit Notdienst im Wechsel: Apotheke, Tierarztpraxis, SHK-Handwerk, Schlüsseldienst. Er sagt zuerst, was jetzt gilt: „Wir haben jetzt Notdienst, bis morgen 9 Uhr“, „Heute ab 18 Uhr Notdienst“ oder „Heute hat ein anderer Betrieb Notdienst“. Darunter steht der nächste eigene Notdienst mit Datum und Uhrzeit und ein Ausschnitt aus dem Plan der nächsten vier Wochen, Feiertage markiert. Hat der Betrieb heute keinen Dienst, verweist der Baustein auf die zentrale Notdienstsuche der Kammer oder Innung, mit Rufnummer als Platzhalter. Dazu kommen der Notfall-Hinweis der Branche (112, Giftinformation, Gasgeruch) und der Kostenhinweis, der weiß, ob die Notdienstgebühr gerade anfällt. Gerechnet wird in der Zeit des Ortes (Europe/Berlin über Intl), Feiertage kommen je Bundesland aus einer gepflegten Liste.
Wer nachts oder am Feiertag auf die Seite kommt, hat eine dringende Frage: Hilft mir dieser Betrieb jetzt, und wenn nicht, wer dann? Eine Öffnungszeiten-Tabelle beantwortet das nicht, ein „Notdienst laut Aushang“ auch nicht. Der Baustein beantwortet beides in einem Satz und gibt den nächsten Schritt: anrufen, klingeln oder zur Notdienstsuche. Der Kostenhinweis vorab nimmt die Überraschung an der Klappe oder in der Praxis.
Wann einsetzen
Apotheke (Notdienst nach Kammerplan), Tierarztpraxis im Notdienstring, SHK-, Elektro- und Schlüsseldienst mit eigenem oder Innungs-Notdienst, Bestatter mit Bereitschaft. Als eigene Sektion „Notdienst“, als Reiter in der Statuszeile oder als Karte im Hero; mobil als Punkt in der unteren Leiste.
Wann nicht
Wenn der Plan nicht gepflegt wird: ein falsches „Wir haben Notdienst“ schickt Menschen nachts vor eine verschlossene Tür. Dann lieber nur den Verweis auf die Notdienstsuche zeigen. Nicht für reine Öffnungszeiten (dafür oeffnungszeiten-status) und nicht für einen Anruf-Knopf mit Bürozeiten (dafür erreichbarkeits-button). Keine Termine schätzen, keine fremden Betriebe als „Notdienst heute“ erfinden: diese Angabe kommt aus den Daten der Kammer.
Varianten
- Apotheke: Reiter „Notdienst“ in der Statuszeile, 24-Stunden-Dienste mit Wechselzeit aus dem Kammerplan, Plan als Monatsausschnitt, Notdienstklappe oder Verweis auf die Notdienstsuche der Kammer, Gebühr mit Uhrzeitregel, 112
- Tierarzt: Notdienst-Karte im Hero, Wochenband mit Sprechstunde, Notdienstzeit und eigenem Dienst, erhöhter GOT-Satz und Notdienstgebühr, Ansage des Notdienstrings, Giftinformation und Tierklinik als Platzhalter
- Handwerk (SHK): Status mit Anruf, daneben zuerst „Es riecht nach Gas?“ (112 und Netzbetreiber von draußen), dann Rohrbruch, Heizung und Kosten, Notdienst-Wochenenden im Wechsel mit der Innung
- Schlüsseldienst: wie Handwerk, mit Festpreis vor der Anfahrt
Umsetzung
Daten wie aus dem CMS: PLAN als Liste eigener Dienste { von: '2026-11-12T09:00', bis: '2026-11-13T09:00' } in Ortszeit, dazu STAND (Datum und Quelle des Kammerplans), WOCHE (reguläre Zeiten, nur um „regulär geöffnet“ zu erkennen) und FEIERTAGE des Bundeslandes aus lab/fundus/feiertage.json. Ortszeit über new Intl.DateTimeFormat('de-DE', { timeZone: 'Europe/Berlin', hourCycle: 'h23', … }).formatToParts(); Dienste in absolute Minuten ab heute 0 Uhr umrechnen, Datumsrechnung auf UTC-Mittag. Zustand: läuft ein eigener Dienst → „jetzt“ (mit Ende, ab 60 Min. vor Ende mit Minuten); beginnt heute einer → „später“; sonst „nicht heute“ mit dem nächsten Dienst; kein Dienst mehr im Plan → Leerzustand „Plan ab … noch nicht veröffentlicht“. Feiertag heute → Zusatzzeile mit Namen. Die Gebührenregel ist eine eigene kleine Funktion je Branche (Apotheke 20 bis 6 Uhr, Sonn- und Feiertage ganztags, 24.12. ab 14 Uhr; Tierarzt 18 bis 8 Uhr, Fr 18 bis Mo 8, Feiertage). Der Verweis auf die Notdienstsuche ist ein normaler Link mit Postleitzahl, Rufnummer und Link der Kammer sind Platzhalter. Takt: setTimeout zur nächsten vollen Minute und bei visibilitychange. Ohne JS steht der Server-Text mit dem nächsten Dienst und dem Verweis da. Beträge, Zeiten und Notfall-Hinweise vom Kunden prüfen lassen.
Robust umsetzen
Telefon. Eine Spalte: Zustand und Knopf „Anrufen“ oder „Notdienst suchen“ zuerst, darunter der nächste Dienst, dann der Plan als 7 × 4-Raster. In der unteren Leiste ein Punkt „Notdienst“ mit Kurztext.
Ohne Bewegung. Zustandswechsel ohne Überblenden und Versatz. Kein Pulsieren des Punkts.
Barrierefrei. Zustand nie nur über Farbe: der Satz sagt „jetzt Notdienst“ oder „kein Notdienst“. Minuten-Aktualisierung still, nur Zustandswechsel per aria-live=polite. Plan als Tabelle mit Wochentag-Spaltenköpfen, jeder Tag mit vollem Text („Donnerstag, 12. November: Notdienst bei uns“). Zeiten als <time datetime>. 112 immer sichtbar, nicht erst im Notfallzustand.
Code
<!DOCTYPE html>
<!--
Notdienst mit Plan und Verweis
Fundus · Sektionen & Funktionen · https://cecodesign.de/lab/fundus/sektionen/notdienst-plan/
Eigenständige Datei. Bilder liegen hier unter bilder/ – eigene einsetzen.
-->
<html lang="de" data-v="apotheke">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Notdienst mit Plan und Verweis · Fundus-Demo</title>
<script>document.documentElement.classList.add('js');if(matchMedia('(prefers-reduced-motion: reduce)').matches)document.documentElement.classList.add('reduziert')</script>
<style>
/* Variante apotheke: Rezept apotheke-vor-ort */
@font-face { font-family: 'Funnel Display'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/funnel-display:vf@latest/latin-wght-normal.woff2') format('woff'); font-weight: 300 800; font-display: swap; }
@font-face { font-family: 'Funnel Sans'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/funnel-sans:vf@latest/latin-wght-normal.woff2') format('woff'); font-weight: 300 800; font-display: swap; }
/* Variante tierarzt: Rezept tierarztpraxis */
@font-face { font-family: 'Young Serif'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/young-serif@latest/latin-400-normal.woff2') format('woff2'); font-weight: 400; font-display: swap; }
@font-face { font-family: '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; }
/* Variante handwerk: Rezept handwerk-werkstatt-klartext */
@font-face { font-family: 'Special Gothic'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/special-gothic:vf@latest/latin-standard-normal.woff2') format('woff2-variations'); font-weight: 400 700; font-stretch: 75% 125%; font-display: swap; }
@font-face { font-family: 'Atkinson Hyperlegible Next'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/atkinson-hyperlegible-next:vf@latest/latin-wght-normal.woff2') format('woff2-variations'); font-weight: 200 800; font-display: swap; }
/* Pult */
@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; }
/* ---------- Farben und Kurven je Rezept ---------- */
:root, [data-v="apotheke"] {
/* apotheke-vor-ort · token-tech-praezise */
--grund: #FAFAF8; --text: #1A2321; --flaeche: #E0ECE8; --dunkel: #0F3B3A; --signal: #0D6A62; --leise: #55615E; --linie: #D2DAD7;
--kurve: cubic-bezier(0.2, 0, 0.38, 0.9); --kurve-ein: cubic-bezier(0, 0, 0.38, 0.9); --kurve-aus: cubic-bezier(0.2, 0, 1, 0.9);
--d-hover: 110ms; --d-zustand: 240ms; --d-zu: 160ms;
--rand: clamp(16px, 4vw, 48px); --max: 1200px;
}
[data-v="tierarzt"] {
/* tierarztpraxis · token-toggle-asymmetrisch; im Notfallbereich keine Bewegung */
--grund: #F4EFE6; --text: #1F2421; --flaeche: #DCE4DA; --dunkel: #23403A; --signal: #B4491B; --leise: #5C625D; --linie: #CFD3C9;
--kurve: cubic-bezier(0.16, 1, 0.3, 1); --kurve-ein: cubic-bezier(0.16, 1, 0.3, 1); --kurve-aus: cubic-bezier(0.7, 0, 0.84, 0);
--d-hover: 150ms; --d-zustand: 0ms; --d-zu: 0ms;
--rand: clamp(16px, 4vw, 56px); --max: 1240px;
}
[data-v="handwerk"] {
/* handwerk-werkstatt-klartext · token-tech-praezise */
--grund: #EFEDEA; --text: #1A1A1A; --flaeche: #D3CDC2; --holz: #C3B4A5; --gruen: #BDC1A9; --signal: #FB6100; --leise: #5E5D5C; --linie: #C9C6C2;
--kurve: cubic-bezier(0.2, 0, 0.38, 0.9); --kurve-ein: cubic-bezier(0, 0, 0.38, 0.9); --kurve-aus: cubic-bezier(0.2, 0, 1, 0.9);
--d-hover: 150ms; --d-zustand: 240ms; --d-zu: 160ms;
--rand: clamp(16px, 4vw, 48px); --max: 1280px;
}
* { box-sizing: border-box; margin: 0; }
html { background: var(--grund); color: var(--text); -webkit-font-smoothing: antialiased; }
body { min-height: 100svh; padding-bottom: 150px; }
button, input, select { font: inherit; color: inherit; }
a { color: inherit; }
img, svg { display: block; }
.sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
.zahl, time { font-variant-numeric: tabular-nums; }
[data-v="apotheke"] [data-nur]:not([data-nur~="apotheke"]), [data-v="tierarzt"] [data-nur]:not([data-nur~="tierarzt"]),
[data-v="handwerk"] [data-nur]:not([data-nur~="handwerk"]) { display: none !important; }
[hidden] { display: none !important; }
:focus-visible { outline: 2px solid var(--text); outline-offset: 3px; }
:root:has(:focus-visible) { scroll-behavior: auto; }
.seite { max-width: var(--max); margin: 0 auto; padding: 0 var(--rand); }
/* Platzhalter: Angaben, die der Betrieb oder die Kammer liefert */
.ph { font-variant-numeric: tabular-nums; background: color-mix(in srgb, currentColor 9%, transparent); border-bottom: 1.5px dotted currentColor; padding: 0 .2em; border-radius: 3px; white-space: nowrap; }
.ph-hinweis { font-size: .8125rem; }
/* Zustandswechsel: nur der Text, 4 px, Kurve des Rezepts; reduziert ohne */
.wechsel { animation: rein var(--d-zustand) var(--kurve-ein) both; }
@keyframes rein { from { opacity: 0; translate: 0 4px; } }
.reduziert .wechsel { animation: none; }
/* =====================================================================
Variante apotheke: Statuszeile mit Reitern, Reiter „Notdienst“
===================================================================== */
[data-v="apotheke"] body { font-family: 'Funnel Sans', system-ui, sans-serif; font-size: 1.125rem; line-height: 1.6; }
.apo-kopf { display: flex; align-items: center; justify-content: space-between; gap: 16px 28px; flex-wrap: wrap; padding-block: 18px; }
.apo-marke { display: flex; align-items: center; gap: 12px; text-decoration: none; }
.apo-marke svg { width: 40px; height: 40px; flex: none; }
.apo-marke b { display: block; font: 600 1.3125rem/1.1 'Funnel Display', sans-serif; letter-spacing: -.01em; }
.apo-marke small { display: block; font-size: .9375rem; color: var(--leise); }
.apo-kopf nav ul { list-style: none; padding: 0; display: flex; gap: 24px; font-size: 1.0625rem; font-weight: 500; }
.apo-kopf nav a { text-decoration: none; }
.apo-kopf nav a:hover { text-decoration: underline; text-underline-offset: 5px; }
.apo-tel { font-weight: 600; white-space: nowrap; text-decoration: none; font-variant-numeric: tabular-nums; }
.apo-zeile { background: var(--flaeche); border-radius: 12px; padding: 8px 24px 24px; }
.apo-reiter { position: relative; display: flex; gap: 4px; border-bottom: 1px solid #BFD2CB; margin-bottom: 24px; flex-wrap: wrap; }
.apo-reiter button { height: 52px; padding: 0 16px; border: 0; background: none; cursor: pointer; font-weight: 600; font-size: 1.0625rem; color: var(--leise); border-radius: 6px 6px 0 0; transition: color var(--d-hover) var(--kurve); }
.apo-reiter button:hover { color: var(--text); }
.apo-reiter button[aria-selected="true"] { color: var(--text); }
.apo-reiter button .zahl { font-weight: 500; }
.apo-strich { position: absolute; left: 0; bottom: -1px; height: 3px; width: 0; background: var(--signal); border-radius: 3px 3px 0 0; transition: translate var(--d-zustand) var(--kurve), width var(--d-zustand) var(--kurve); }
.reduziert .apo-strich { transition: none; }
.apo-not { display: grid; grid-template-columns: minmax(0, 8fr) minmax(0, 4fr); gap: 28px; align-items: start; }
.apo-meta { display: flex; flex-wrap: wrap; gap: 6px 14px; align-items: center; font-size: 1rem; color: var(--leise); margin-bottom: 14px; }
.chip { display: inline-flex; align-items: center; gap: 8px; height: 30px; padding: 0 12px; border-radius: 6px; font-size: .9375rem; font-weight: 600; white-space: nowrap; }
.chip i { width: 9px; height: 9px; border-radius: 50%; background: currentColor; flex: none; }
[data-v="apotheke"] .chip--zustand { background: #fff; color: var(--text); box-shadow: inset 0 0 0 1px #BFD2CB; }
[data-v="apotheke"] [data-zustand="jetzt"] .chip--zustand { background: var(--dunkel); color: #fff; box-shadow: none; }
[data-v="apotheke"] [data-zustand="spaeter"] .chip--zustand i { background: transparent; box-shadow: inset 0 0 0 2px var(--signal); }
[data-v="apotheke"] [data-zustand="nicht"] .chip--zustand i, [data-v="apotheke"] [data-zustand="leer"] .chip--zustand i { background: var(--leise); }
.chip--feiertag { background: #fff; color: var(--dunkel); box-shadow: inset 0 0 0 1px var(--dunkel); }
.apo-status { min-height: 7.5rem; }
.apo-status .status__kopf { font: 600 clamp(1.75rem, 1.15rem + 2.2vw, 2.75rem)/1.08 'Funnel Display', sans-serif; letter-spacing: -.02em; max-width: 22ch; text-wrap: balance; }
.apo-status .status__text { margin-top: 12px; max-width: 52ch; text-wrap: pretty; font-variant-numeric: tabular-nums; }
.apo-status .status__text b { font-weight: 600; }
/* Plan: vier Wochen als Tabelle */
.plan { margin-top: 26px; }
.plan__kopf { display: flex; justify-content: space-between; align-items: baseline; gap: 6px 16px; flex-wrap: wrap; margin-bottom: 10px; }
.plan__kopf h2 { font: 600 1.25rem/1.2 'Funnel Display', sans-serif; }
.plan__kopf p { font-size: 1rem; color: var(--leise); }
.kal { width: 100%; border-collapse: separate; border-spacing: 4px; margin: 0 -4px; table-layout: fixed; }
.kal th { font-size: .875rem; font-weight: 600; color: var(--leise); text-align: left; padding: 0 8px 2px; }
.kal td { position: relative; height: 58px; vertical-align: top; padding: 7px 8px; border-radius: 6px; background: var(--grund); font-size: 1rem; font-weight: 600; line-height: 1.2; font-variant-numeric: tabular-nums; }
.kal td.vorbei { background: transparent; color: var(--leise); font-weight: 500; }
.kal td.dienst { background: var(--signal); color: #fff; }
.kal td.dienst.vorbei { background: transparent; color: var(--leise); box-shadow: inset 0 0 0 1.5px #9DB8B0; }
.kal td.aktiv { background: var(--dunkel); color: #fff; }
.kal td.heute { box-shadow: inset 0 0 0 2px var(--text); }
.kal td.dienst.heute, .kal td.aktiv.heute { box-shadow: inset 0 0 0 2px var(--grund), 0 0 0 2px var(--text); }
.kal .kal__ft { position: absolute; left: 8px; right: 6px; bottom: 6px; font-size: .75rem; font-weight: 500; line-height: 1.15; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.kal .kal__ft::before { content: ''; display: inline-block; width: 6px; height: 6px; margin-right: 5px; vertical-align: .12em; border-radius: 50%; background: currentColor; }
.kal .kal__zeit { position: absolute; left: 8px; bottom: 6px; font-size: .75rem; font-weight: 500; }
.legende { list-style: none; padding: 0; display: flex; flex-wrap: wrap; gap: 6px 18px; margin-top: 10px; font-size: .9375rem; color: var(--leise); }
.legende li { display: flex; align-items: center; gap: 8px; }
.legende i { width: 16px; height: 16px; border-radius: 4px; flex: none; }
.apo-seite { background: #fff; border-radius: 12px; padding: 24px; display: grid; gap: 14px; }
.apo-seite h2 { font: 600 1.25rem/1.2 'Funnel Display', sans-serif; }
.apo-seite p { font-size: 1.0625rem; line-height: 1.55; }
.apo-heute { border: 1.5px dashed #9DB0AA; border-radius: 6px; padding: 12px 14px; display: grid; gap: 4px; }
.apo-heute small { font-size: .875rem; color: var(--leise); }
.apo-heute b { font-weight: 600; }
.apo-schritte { margin: 0; padding-left: 1.3em; display: grid; gap: 6px; font-size: 1.0625rem; line-height: 1.5; }
.apo-schritte li::marker { font-weight: 600; color: var(--signal); }
.knopf { display: inline-flex; align-items: center; justify-content: center; gap: 10px; min-height: 48px; padding: 10px 18px; border-radius: 6px; border: 0; background: var(--signal); color: #fff; font-weight: 600; font-size: 1.0625rem; line-height: 1.2; text-decoration: none; cursor: pointer; transition: background-color var(--d-hover) var(--kurve), box-shadow var(--d-hover) var(--kurve); }
.knopf:hover { background: var(--dunkel); }
.knopf .zahl { font-weight: 500; }
.knopf svg { width: 18px; height: 18px; flex: none; }
.knopf--kontur { background: transparent; color: var(--signal); box-shadow: inset 0 0 0 1.5px var(--signal); }
.knopf--kontur:hover { background: var(--flaeche); box-shadow: inset 0 0 0 2px var(--signal); }
.apo-band { margin-top: 16px; display: grid; grid-template-columns: auto minmax(0, 1fr) auto; gap: 14px 24px; align-items: center; padding: 18px 24px; border-radius: 12px; background: var(--dunkel); color: #fff; }
.apo-band svg { width: 28px; height: 28px; }
.apo-band p { font-size: 1.0625rem; line-height: 1.5; font-variant-numeric: tabular-nums; }
.apo-band p b { font-weight: 600; }
.apo-band small { display: block; font-size: .9375rem; color: #C6DAD5; margin-top: 2px; }
.knopf--hell { background: #fff; color: var(--dunkel); }
.knopf--hell:hover { background: var(--flaeche); }
.apo-band :focus-visible { outline-color: #fff; }
.fuss { margin-top: 14px; font-size: 1rem; line-height: 1.55; color: var(--leise); max-width: 90ch; }
@media (max-width: 1080px) { .apo-kopf nav { display: none; } }
@media (max-width: 860px) { .apo-not { grid-template-columns: minmax(0, 1fr); gap: 20px; } .apo-not > div:first-child { display: contents; } .apo-not .plan { order: 3; margin-top: 6px; } .apo-seite { order: 2; } .apo-meta { margin-bottom: 0; } }
@media (max-width: 640px) {
.apo-zeile { padding: 4px 14px 18px; margin-inline: calc(var(--rand) * -1 + 8px); }
.apo-reiter button { padding: 0 10px; }
.kal { border-spacing: 3px; margin: 0 -3px; }
.kal th { padding: 0 4px; font-size: .8125rem; }
.kal td { height: 46px; padding: 5px; font-size: .9375rem; }
.kal .kal__ft { left: auto; right: 5px; top: 7px; bottom: auto; width: 7px; height: 7px; font-size: 0; }
.kal .kal__ft::before { margin: 0; vertical-align: top; width: 7px; height: 7px; }
.kal .kal__zeit { display: none; }
.apo-band { grid-template-columns: minmax(0, 1fr); padding: 18px; }
.apo-band > svg { display: none; }
.apo-seite { padding: 18px; }
}
/* =====================================================================
Variante tierarzt: Notdienst-Karte in Tanne, Wochenband, Hinweise
===================================================================== */
[data-v="tierarzt"] body { font-family: 'Onest', system-ui, sans-serif; font-size: 1.0625rem; line-height: 1.6; }
.tier-kopf { display: flex; align-items: center; justify-content: space-between; gap: 14px 28px; flex-wrap: wrap; padding-block: 18px; }
.tier-marke { text-decoration: none; display: grid; }
.tier-marke b { font: 400 1.375rem/1.1 'Young Serif', serif; letter-spacing: -.01em; }
.tier-marke small { font-size: .9375rem; color: var(--leise); }
.tier-kopf nav ul { list-style: none; padding: 0; display: flex; gap: 24px; font-weight: 500; }
.tier-kopf nav a { text-decoration: none; }
.tier-kopf nav a:hover { text-decoration: underline; text-underline-offset: 5px; }
.knopf-k { display: inline-flex; align-items: center; justify-content: center; gap: 10px; min-height: 48px; padding: 10px 20px; border-radius: 8px; border: 0; background: var(--signal); color: #fff; font-weight: 600; font-size: 1rem; line-height: 1.2; text-decoration: none; cursor: pointer; transition: background-color var(--d-hover) var(--kurve-ein); }
.knopf-k:hover { background: #963A14; }
.tier-buehne { display: grid; grid-template-columns: minmax(0, 5fr) minmax(0, 7fr); gap: 28px 40px; align-items: end; padding-top: clamp(16px, 4vh, 48px); }
.tier-intro h1 { font: 400 clamp(2.25rem, 1.4rem + 3vw, 3.6rem)/1.05 'Young Serif', serif; letter-spacing: -.015em; text-wrap: balance; }
.tier-intro p { margin-top: 16px; max-width: 40ch; }
.tier-intro .tier-label { margin: 0 0 14px; font-weight: 600; color: var(--signal); font-size: 1rem; }
[data-v="tierarzt"] .tier-intro .tier-label { color: #9A3E17; }
.tier-karte { background: var(--dunkel); color: #fff; border-radius: 20px; padding: clamp(22px, 2.6vw, 34px); display: grid; gap: 14px; }
.tier-karte__meta { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
[data-v="tierarzt"] .chip--zustand { background: rgba(255, 255, 255, .1); color: #fff; box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .28); }
[data-v="tierarzt"] [data-zustand="jetzt"] .chip--zustand { background: #F4EFE6; color: var(--dunkel); box-shadow: none; }
[data-v="tierarzt"] [data-zustand="jetzt"] .chip--zustand i { background: #2F7A4F; }
[data-v="tierarzt"] [data-zustand="spaeter"] .chip--zustand i { background: transparent; box-shadow: inset 0 0 0 2px #fff; }
[data-v="tierarzt"] .chip--feiertag { background: transparent; color: #fff; box-shadow: inset 0 0 0 1px #E6C9A8; }
.tier-karte .chip__uhr { font-size: .9375rem; color: #CFE0D6; font-variant-numeric: tabular-nums; }
.tier-status { min-height: 7.4rem; }
.tier-status .status__kopf { font: 400 clamp(1.75rem, 1.2rem + 1.8vw, 2.5rem)/1.12 'Young Serif', serif; letter-spacing: -.01em; text-wrap: balance; }
.tier-status .status__text { margin-top: 10px; color: #E9F0EB; max-width: 50ch; font-variant-numeric: tabular-nums; }
.tier-status .status__text b { color: #fff; font-weight: 600; }
.tier-aktionen { display: flex; flex-wrap: wrap; gap: 10px 14px; align-items: center; }
.knopf-hafer { display: inline-flex; align-items: center; gap: 12px; min-height: 56px; padding: 10px 22px 10px 18px; border-radius: 8px; background: #F4EFE6; color: var(--text); font-weight: 600; text-decoration: none; transition: background-color var(--d-hover) var(--kurve-ein); }
.knopf-hafer:hover { background: #fff; }
.knopf-hafer svg { width: 22px; height: 22px; color: var(--signal); flex: none; }
.knopf-hafer span { display: grid; line-height: 1.25; }
.knopf-hafer small { font-weight: 500; font-size: .9375rem; color: var(--leise); font-variant-numeric: tabular-nums; }
.link-hell { color: #fff; font-weight: 500; text-underline-offset: 4px; }
.tier-karte :focus-visible { outline-color: #F4EFE6; }
.tier-kosten { border-top: 1px solid rgba(255, 255, 255, .22); padding-top: 14px; font-size: 1rem; line-height: 1.55; color: #E9F0EB; font-variant-numeric: tabular-nums; }
.tier-kosten b { color: #fff; font-weight: 600; }
.tier-plan { margin-top: 28px; background: var(--flaeche); border-radius: 20px; padding: clamp(22px, 2.6vw, 34px); display: grid; grid-template-columns: minmax(0, 8fr) minmax(0, 4fr); gap: 28px 40px; }
.tier-plan h2 { font: 400 clamp(1.5rem, 1.2rem + 1vw, 2rem)/1.15 'Young Serif', serif; }
.tier-plan > div > p { margin-top: 6px; color: var(--leise); max-width: 60ch; font-size: 1rem; }
.woche { list-style: none; padding: 0; margin-top: 18px; display: grid; gap: 8px; }
.woche li { display: grid; grid-template-columns: 9.5rem minmax(0, 1fr); gap: 12px; align-items: center; }
.woche__tag { font-size: .9375rem; font-weight: 500; line-height: 1.2; font-variant-numeric: tabular-nums; }
.woche__tag small { display: block; font-size: .8125rem; color: var(--leise); font-weight: 500; }
.woche li.heute .woche__tag { font-weight: 700; }
.woche__spur { position: relative; height: 30px; border-radius: 8px; background: #F4EFE6; overflow: hidden; }
.woche__spur > span { position: absolute; top: 0; bottom: 0; }
.seg-nd { background: repeating-linear-gradient(135deg, rgba(35, 64, 58, .26) 0 2px, transparent 2px 7px); }
.seg-sp { background: #B9CBB6; top: 7px !important; bottom: 7px !important; border-radius: 4px; }
.seg-wir { background: var(--dunkel); top: 5px !important; bottom: 5px !important; border-radius: 5px; }
.seg-jetzt { width: 3px; margin-left: -1.5px; background: var(--signal); }
.woche__achse { display: grid; grid-template-columns: 9.5rem minmax(0, 1fr); gap: 12px; margin-top: 4px; }
.woche__achse div { position: relative; height: 18px; font-size: .8125rem; color: var(--leise); font-variant-numeric: tabular-nums; }
.woche__achse div span { position: absolute; top: 0; translate: -50% 0; white-space: nowrap; }
.woche__achse div span:first-child { translate: 0 0; }
.woche__achse div span:last-child { translate: -100% 0; }
[data-v="tierarzt"] .legende i.l-nd { background: repeating-linear-gradient(135deg, rgba(35, 64, 58, .4) 0 2px, #F4EFE6 2px 6px); }
[data-v="tierarzt"] .legende i.l-sp { background: #B9CBB6; }
[data-v="tierarzt"] .legende i.l-wir { background: var(--dunkel); }
[data-v="tierarzt"] .legende i.l-jetzt { width: 3px; background: var(--signal); border-radius: 0; }
.dienste { list-style: none; padding: 0; margin-top: 14px; display: grid; }
.dienste li { padding: 12px 0; border-top: 1px solid #BCC7BA; display: grid; gap: 2px; font-variant-numeric: tabular-nums; }
.dienste li:last-child { border-bottom: 1px solid #BCC7BA; }
.dienste b { font-weight: 600; }
.dienste small { color: var(--leise); font-size: .9375rem; }
.dienste li.ist-jetzt b::after { content: 'läuft'; margin-left: 8px; padding: 1px 8px; border-radius: 4px; background: var(--dunkel); color: #fff; font-size: .8125rem; font-weight: 600; vertical-align: .1em; }
.dienste__leer { padding: 12px 0; border-top: 1px solid #BCC7BA; color: var(--leise); }
.tier-hinweise { margin-top: 28px; display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 16px; }
.hinweis-k { border: 1px solid var(--linie); border-radius: 20px; padding: 22px; background: #FBF8F2; display: grid; gap: 8px; align-content: start; }
.hinweis-k svg { width: 28px; height: 28px; color: var(--dunkel); }
.hinweis-k h3 { font: 400 1.25rem/1.2 'Young Serif', serif; }
.hinweis-k p { font-size: 1rem; line-height: 1.55; }
.hinweis-k a { font-weight: 600; }
@media (max-width: 1020px) { .tier-kopf nav { display: none; } .tier-buehne, .tier-plan { grid-template-columns: minmax(0, 1fr); } }
@media (max-width: 760px) { .tier-hinweise { grid-template-columns: minmax(0, 1fr); } }
@media (max-width: 560px) {
.woche li, .woche__achse { grid-template-columns: minmax(0, 1fr); gap: 4px; }
.woche__achse > i { display: none; }
.woche__tag small { display: inline; margin-left: 6px; }
.tier-karte { border-radius: 16px; }
.tier-intro h1 { font-size: 2rem; }
.tier-intro p:last-child { display: none; }
}
/* =====================================================================
Variante handwerk: Klartext, Notfall-Weiche, Notdienst-Wochenenden
===================================================================== */
[data-v="handwerk"] body { font-family: 'Atkinson Hyperlegible Next', system-ui, sans-serif; font-size: 1.0625rem; line-height: 1.55; }
.hw-kopf { display: flex; align-items: center; justify-content: space-between; gap: 14px 24px; flex-wrap: wrap; padding-block: 16px; border-bottom: 1px solid var(--linie); }
.hw-marke { text-decoration: none; display: flex; align-items: baseline; gap: 12px; flex-wrap: wrap; }
.hw-marke b { font: 700 1.625rem/1 'Special Gothic', sans-serif; font-stretch: 100%; letter-spacing: .02em; }
.hw-marke small, .hw-label { font: 600 .8125rem/1.2 'Special Gothic', sans-serif; font-stretch: 75%; letter-spacing: .04em; text-transform: uppercase; }
.hw-marke small { color: var(--leise); }
.knopf-o { display: inline-flex; align-items: center; justify-content: center; gap: 10px; min-height: 50px; padding: 10px 20px; border-radius: 4px; border: 0; background: var(--signal); color: #1A1A1A; font-weight: 700; font-size: 1.0625rem; line-height: 1.2; text-decoration: none; cursor: pointer; transition: background-color var(--d-hover) var(--kurve), box-shadow var(--d-hover) var(--kurve); }
.knopf-o:hover { background: #FF7A26; box-shadow: inset 0 0 0 2px #1A1A1A; }
.knopf-o svg { width: 20px; height: 20px; flex: none; }
.knopf-o .zahl { font-weight: 600; }
.knopf-o--linie { background: transparent; box-shadow: inset 0 0 0 2px #1A1A1A; }
.knopf-o--linie:hover { background: var(--flaeche); }
.hw-oben { display: grid; grid-template-columns: minmax(0, 7fr) minmax(0, 5fr); gap: 24px 40px; padding-top: clamp(24px, 5vh, 56px); align-items: start; }
.hw-label { color: var(--text); display: flex; gap: 10px; align-items: center; flex-wrap: wrap; }
[data-v="handwerk"] .chip { font: 600 .8125rem/1 'Special Gothic', sans-serif; font-stretch: 75%; letter-spacing: .04em; text-transform: uppercase; height: 28px; border-radius: 4px; }
[data-v="handwerk"] .chip--zustand { background: var(--flaeche); color: var(--text); }
[data-v="handwerk"] [data-zustand="jetzt"] .chip--zustand { background: #1A1A1A; color: #fff; }
[data-v="handwerk"] [data-zustand="jetzt"] .chip--zustand i { background: var(--signal); }
[data-v="handwerk"] [data-zustand="spaeter"] .chip--zustand i { background: transparent; box-shadow: inset 0 0 0 2px var(--text); }
[data-v="handwerk"] .chip--feiertag { background: transparent; box-shadow: inset 0 0 0 1.5px var(--text); color: var(--text); }
.hw-status { margin-top: 16px; min-height: 9.5rem; }
.hw-status .status__kopf { font: 700 clamp(2.25rem, 1.5rem + 3vw, 3.5rem)/1.02 'Special Gothic', sans-serif; letter-spacing: -.01em; text-wrap: balance; max-width: 16ch; }
.hw-status .status__text { margin-top: 14px; max-width: 48ch; font-size: 1.125rem; font-variant-numeric: tabular-nums; }
.hw-status .status__text b { font-weight: 700; }
.hw-aktionen { margin-top: 22px; display: flex; flex-wrap: wrap; gap: 12px; align-items: center; }
.hw-fakten { margin-top: 28px; display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16px 24px; max-width: 560px; }
.hw-fakten div { border-top: 2px solid #1A1A1A; padding-top: 10px; }
.hw-fakten dt { font: 600 .8125rem/1.2 'Special Gothic', sans-serif; font-stretch: 75%; letter-spacing: .04em; text-transform: uppercase; }
.hw-fakten dd { margin: 4px 0 0; font-size: 1rem; line-height: 1.45; }
@media (max-width: 420px) { .hw-fakten { grid-template-columns: minmax(0, 1fr); } }
.hw-gas { background: #1A1A1A; color: #F2F0ED; border-radius: 8px; padding: 26px; display: grid; gap: 14px; }
.hw-gas .hw-label { color: #FFB27A; }
.hw-gas h2 { font: 700 clamp(1.625rem, 1.2rem + 1.6vw, 2.25rem)/1.05 'Special Gothic', sans-serif; }
.hw-gas ol { margin: 0; padding: 0; list-style: none; counter-reset: s; display: grid; gap: 8px; }
.hw-gas li { counter-increment: s; display: grid; grid-template-columns: 2rem minmax(0, 1fr); gap: 8px; line-height: 1.45; }
.hw-gas li::before { content: counter(s, decimal-leading-zero); font: 600 .9375rem/1.55 'Special Gothic', sans-serif; font-stretch: 75%; color: #FFB27A; }
.hw-gas li b { color: #fff; }
.hw-gas p { font-size: 1rem; color: #D6D3CF; }
.hw-gas :focus-visible { outline-color: #fff; }
.hw-gas .ph { color: #fff; }
.hw-mitte { margin-top: 28px; display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; }
.hw-fall { border-radius: 8px; padding: 22px; display: grid; gap: 8px; align-content: start; }
.hw-fall:nth-child(1) { background: var(--flaeche); }
.hw-fall:nth-child(2) { background: var(--gruen); }
.hw-fall:nth-child(3) { background: var(--holz); }
.hw-fall h3 { font: 700 1.375rem/1.1 'Special Gothic', sans-serif; }
.hw-fall .hw-label { color: #3A3937; }
.hw-fall p { font-size: 1rem; line-height: 1.5; }
.hw-plan { margin-top: 28px; display: grid; grid-template-columns: minmax(0, 4fr) minmax(0, 8fr); gap: 16px 40px; align-items: start; }
.hw-plan h2 { font: 700 clamp(1.625rem, 1.2rem + 1.6vw, 2.25rem)/1.05 'Special Gothic', sans-serif; }
.hw-plan > div > p { margin-top: 10px; font-size: 1rem; color: var(--leise); }
.we { width: 100%; border-collapse: collapse; font-variant-numeric: tabular-nums; }
.we th { text-align: left; font: 600 .8125rem/1.2 'Special Gothic', sans-serif; font-stretch: 75%; letter-spacing: .04em; text-transform: uppercase; color: var(--leise); padding: 0 12px 8px 0; }
.we td { padding: 12px 12px 12px 0; border-top: 1px solid var(--linie); vertical-align: top; }
.we td:first-child { font: 600 .9375rem/1.5 'Special Gothic', sans-serif; font-stretch: 75%; letter-spacing: .04em; width: 4.2rem; }
.we td small { display: block; color: var(--leise); font-size: .9375rem; }
.we tr.ist-jetzt td { background: #fff; }
.we tr.ist-jetzt td:first-child { box-shadow: inset 4px 0 0 var(--signal); padding-left: 12px; }
.wer { display: inline-flex; align-items: center; height: 28px; padding: 0 10px; border-radius: 4px; font-weight: 700; font-size: .9375rem; white-space: nowrap; }
.wer--wir { background: #1A1A1A; color: #fff; }
.wer--innung { box-shadow: inset 0 0 0 1.5px #1A1A1A; }
@media (max-width: 980px) { .hw-oben, .hw-plan { grid-template-columns: minmax(0, 1fr); } .hw-mitte { grid-template-columns: minmax(0, 1fr); } .hw-status { min-height: 0; } }
@media (max-width: 560px) {
.we thead { display: none; }
.we tr { display: grid; grid-template-columns: 3.6rem minmax(0, 1fr); border-top: 1px solid var(--linie); padding: 10px 0; }
.we td { border: 0; padding: 0 0 0 0; }
.we td:first-child { grid-row: span 2; width: auto; }
.we td:last-child { padding-top: 6px; }
.we tr.ist-jetzt { background: #fff; }
.we tr.ist-jetzt td { background: transparent; }
.we tr.ist-jetzt td:first-child { padding-left: 10px; }
.hw-gas { padding: 20px; }
}
/* ---------- Pult: simulierte Uhr und Kalender ---------- */
.pult { position: fixed; left: 50%; bottom: 14px; translate: -50% 0; z-index: 30; width: min(1120px, calc(100vw - 24px)); display: grid; gap: 8px; padding: 12px 14px; border-radius: 14px; background: rgba(20, 24, 23, .95); color: #F1F2EF; box-shadow: 0 12px 40px rgba(0, 0, 0, .22); font: 500 .8125rem/1.2 'Geist Mono', ui-monospace, monospace; }
.pult__reihe { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; }
.pult__titel { width: 6.6em; font-size: .6875rem; letter-spacing: .14em; text-transform: uppercase; color: #A9B4B0; }
.pult button, .pult input { height: 32px; padding: 0 10px; border-radius: 7px; border: 1px solid rgba(241, 242, 239, .24); background: rgba(255, 255, 255, .06); color: inherit; cursor: pointer; font: inherit; font-size: .75rem; letter-spacing: .06em; text-transform: uppercase; }
.pult input { text-transform: none; letter-spacing: 0; color-scheme: dark; cursor: text; }
.pult button[aria-pressed="true"] { background: #F1F2EF; color: #141817; border-color: #F1F2EF; }
.pult button:hover { border-color: rgba(241, 242, 239, .6); }
.pult output { margin-left: auto; font-variant-numeric: tabular-nums; color: #D7DDDA; letter-spacing: .04em; }
.pult :focus-visible { outline: 2px solid #8FD3C4; outline-offset: 2px; }
@media (max-width: 760px) { .pult output { margin-left: 0; width: 100%; } .pult__titel { width: auto; margin-right: 4px; } }
.vorschau body { overflow: hidden; padding-bottom: 0; }
.vorschau .fuss, .vorschau .tier-hinweise, .vorschau .hw-plan { display: none; }
/* Windows-Kontrastmodus */
@media (forced-colors: active) {
button, [role=button], [role=tab], [role=switch], input, select, textarea { border: 1px solid ButtonText !important; }
.kal td.dienst, .kal td.aktiv { border: 2px solid CanvasText; }
.seg-wir, .seg-jetzt { background: CanvasText; forced-color-adjust: none; }
.apo-strich { background: Highlight; }
.chip, .wer { border: 1px solid CanvasText; }
}
</style>
</head>
<body>
<p class="sr" aria-live="polite" data-ansage></p>
<!-- =================== Variante apotheke =================== -->
<div class="seite" data-nur="apotheke" id="v-apotheke">
<header class="apo-kopf">
<a class="apo-marke" href="#">
<svg viewBox="0 0 40 40" aria-hidden="true"><rect width="40" height="40" rx="9" fill="#0F3B3A"/><path d="M7 24.5c6.2-.6 11.4-3.4 15.6-8.6 1.6-2 3.9-3.3 6.6-3.4L33 12l-3 3.1c-1.5 6.4-7 11-14 11.6l-3.6 4.3.8-4.4c-2.3-.2-4.4-.9-6.2-2.1z" fill="#E0ECE8"/><circle cx="27.6" cy="14.6" r="1" fill="#0F3B3A"/></svg>
<span><b>Schwalbenhof-Apotheke</b><small>Am Schwalbenhof 3 · 38855 Wernigerode</small></span>
</a>
<nav aria-label="Hauptmenü"><ul><li><a href="#">Services</a></li><li><a href="#">Vorbestellen</a></li><li><a href="#">Team</a></li><li><a href="#">Filialen</a></li></ul></nav>
<a class="apo-tel" href="tel:+491713920012">0171 39200 12</a>
</header>
<main>
<h1 class="sr">Schwalbenhof-Apotheke: heute, Notdienst und Vorbestellen</h1>
<div class="apo-zeile">
<div class="apo-reiter" role="tablist" aria-label="Apotheke heute">
<button type="button" role="tab" id="apo-t-heute" aria-controls="apo-p-heute" aria-selected="false" tabindex="-1">Heute</button>
<button type="button" role="tab" id="apo-t-not" aria-controls="apo-p-not" aria-selected="true">Notdienst</button>
<button type="button" role="tab" id="apo-t-vor" aria-controls="apo-p-vor" aria-selected="false" tabindex="-1">Vorbestellen</button>
<span class="apo-strich" aria-hidden="true"></span>
</div>
<div role="tabpanel" id="apo-p-heute" aria-labelledby="apo-t-heute" tabindex="0" hidden>
<p class="apo-status"><span class="status__kopf" style="display:block" data-heute-kopf>Mo bis Fr 8:00 bis 18:30 Uhr, Sa 8:30 bis 13:00 Uhr</span><span class="status__text" style="display:block" data-heute-text>Öffnungszeiten mit Live-Status: Muster oeffnungszeiten-status.</span></p>
</div>
<div role="tabpanel" id="apo-p-not" aria-labelledby="apo-t-not" tabindex="-1">
<div class="apo-not" data-box data-zustand="server">
<div>
<p class="apo-meta"><span class="chip chip--zustand"><i></i><span data-kurz>Notdienst nach Plan</span></span><span class="chip chip--feiertag" data-feiertag hidden></span><span data-uhr></span></p>
<div class="apo-status" data-status>
<p class="status__kopf" data-kopf>Notdienst nach dem Plan der Kammer</p>
<p class="status__text" data-text>Unsere Notdienste stehen im Plan darunter. Welche Apotheke heute Dienst hat, zeigt die Notdienstsuche der Landesapothekerkammer.</p>
</div>
<section class="plan" aria-labelledby="apo-plan-titel">
<div class="plan__kopf"><h2 id="apo-plan-titel">Unsere Notdienste</h2><p data-plan-zeitraum>nächste vier Wochen</p></div>
<table class="kal" data-kal>
<caption class="sr" data-kal-caption>Notdienstplan, vier Wochen</caption>
<thead><tr><th scope="col" abbr="Montag">Mo</th><th scope="col" abbr="Dienstag">Di</th><th scope="col" abbr="Mittwoch">Mi</th><th scope="col" abbr="Donnerstag">Do</th><th scope="col" abbr="Freitag">Fr</th><th scope="col" abbr="Samstag">Sa</th><th scope="col" abbr="Sonntag">So</th></tr></thead>
<tbody data-kal-body></tbody>
</table>
<ul class="legende">
<li><i style="background:#0D6A62"></i>Notdienst bei uns, 9:00 bis 9:00 Uhr</li>
<li><i style="background:#0F3B3A"></i>läuft gerade</li>
<li><i style="box-shadow:inset 0 0 0 2px #1A2321"></i>heute</li>
<li><i style="background:none;display:grid;place-items:center"><b style="width:7px;height:7px;border-radius:50%;background:#1A2321"></b></i>Feiertag in Sachsen-Anhalt</li>
</ul>
</section>
</div>
<aside class="apo-seite" aria-labelledby="apo-seite-titel">
<div data-modus="verweis" style="display:grid;gap:14px">
<h2 id="apo-seite-titel">Wer hat heute Notdienst?</h2>
<div class="apo-heute">
<small>Aus den Daten der Kammer · Platzhalter</small>
<span><b><span class="ph">[Apotheke]</span></b>, <span class="ph">[Straße]</span>, <span class="ph">[Ort]</span></span>
<span data-fremd-zeit>bis morgen, 9:00 Uhr</span>
</div>
<a class="knopf knopf--kontur" href="#" data-suche>Notdienstsuche der Kammer
<svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true"><path d="M6 14 14 6M8 6h6v6"/></svg></a>
<p>Telefonische Auskunft der Kammer: <span class="ph">[Rufnummer]</span></p>
<p class="ph-hinweis" style="color:var(--leise)">An unserer Tür hängt dieselbe Angabe aus.</p>
</div>
<div data-modus="klappe" style="display:grid;gap:14px" hidden>
<h2>So bekommen Sie Ihr Medikament</h2>
<ol class="apo-schritte">
<li data-klappe-1>Klingeln Sie an der Notdienstklappe rechts neben dem Eingang.</li>
<li>Bringen Sie Ihre Gesundheitskarte für das E-Rezept mit, ein Papierrezept oder die Packung.</li>
<li>Wir beraten Sie an der Klappe. Bitte haben Sie etwas Geduld, wenn vor Ihnen jemand wartet.</li>
</ol>
<a class="knopf" href="tel:+491713920012"><svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true"><path d="M4.5 3.5h3l1.5 4-2 1.2a9 9 0 0 0 4.3 4.3l1.2-2 4 1.5v3a1.5 1.5 0 0 1-1.6 1.5A14 14 0 0 1 3 5.1 1.5 1.5 0 0 1 4.5 3.5z" stroke-linejoin="round"/></svg>Anrufen <span class="zahl">0171 39200 12</span></a>
</div>
</aside>
</div>
</div>
<div role="tabpanel" id="apo-p-vor" aria-labelledby="apo-t-vor" tabindex="0" hidden>
<p class="apo-status"><span class="status__kopf" style="display:block">Vorbestellen und morgen abholen.</span><span class="status__text" style="display:block">Rezeptfreie Mittel mit Menge, Abholung ab Uhrzeit. E-Rezepte lösen Sie mit der Gesundheitskarte ein. Muster mehrschritt-anfrage.</span></p>
</div>
</div>
<div class="apo-band">
<svg viewBox="0 0 28 28" fill="none" stroke="#fff" stroke-width="1.7" aria-hidden="true"><circle cx="14" cy="14" r="11"/><path d="M14 7.5V14l4 2.5" stroke-linecap="round"/></svg>
<p data-gebuehr>Notdienstgebühr nach Arzneimittelpreisverordnung: <b>2,50 €</b> von 20 bis 6 Uhr, an Sonn- und Feiertagen ganztags.</p>
<a class="knopf knopf--hell" href="tel:112">Lebensgefahr: 112</a>
</div>
<p class="fuss">Termine aus dem Notdienstplan der Landesapothekerkammer Sachsen-Anhalt, Stand <time datetime="2026-10-28">28.10.2026</time>, Quelle: <span class="ph">[Link zum Plan]</span>. Die Notdienstgebühr darf die Apotheke erheben, sie muss es nicht; ist auf dem Rezept „noctu“ angekreuzt, rechnet sie mit der Kasse ab. Betrag, Zeiten und Wechselzeit vom Kunden prüfen lassen.</p>
</main>
</div>
<!-- =================== Variante tierarzt =================== -->
<div class="seite" data-nur="tierarzt" id="v-tierarzt">
<header class="tier-kopf">
<a class="tier-marke" href="#"><b>Tierarztpraxis am Ziegelhof</b><small>Kleintiere · Zittau</small></a>
<nav aria-label="Hauptmenü"><ul><li><a href="#">Tierarten</a></li><li><a href="#">Leistungen</a></li><li><a href="#">Was kostet das?</a></li><li><a href="#">Team</a></li></ul></nav>
<a class="knopf-k" href="#">Termin</a>
</header>
<main>
<div class="tier-buehne">
<div class="tier-intro">
<p class="tier-label">Notfall?</p>
<h1>Wenn es Ihrem Tier schlecht geht, zählt der nächste Schritt.</h1>
<p>Hier steht, wer gerade Notdienst hat, was er kostet und was Sie sofort tun können.</p>
</div>
<section class="tier-karte" data-box data-zustand="server" aria-labelledby="tier-karte-titel">
<h2 class="sr" id="tier-karte-titel">Notdienst jetzt</h2>
<p class="tier-karte__meta"><span class="chip chip--zustand"><i></i><span data-kurz>Notdienst im Ring</span></span><span class="chip chip--feiertag" data-feiertag hidden></span><span class="chip__uhr" data-uhr></span></p>
<div class="tier-status" data-status>
<p class="status__kopf" data-kopf>Notdienst im Notdienstring Zittau</p>
<p class="status__text" data-text>Nachts, am Wochenende und an Feiertagen wechseln sich die Praxen ab. Unsere Termine stehen darunter.</p>
</div>
<div class="tier-aktionen" data-cta="wir">
<a class="knopf-hafer" href="tel:+4917604069027"><svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true"><path d="M4.5 3.5h3l1.5 4-2 1.2a9 9 0 0 0 4.3 4.3l1.2-2 4 1.5v3a1.5 1.5 0 0 1-1.6 1.5A14 14 0 0 1 3 5.1 1.5 1.5 0 0 1 4.5 3.5z" stroke-linejoin="round"/></svg><span>Notdienst anrufen<small>0176 040690 27</small></span></a>
</div>
<div class="tier-aktionen" data-cta="andere" hidden>
<a class="knopf-hafer" href="#"><svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true"><path d="M4.5 3.5h3l1.5 4-2 1.2a9 9 0 0 0 4.3 4.3l1.2-2 4 1.5v3a1.5 1.5 0 0 1-1.6 1.5A14 14 0 0 1 3 5.1 1.5 1.5 0 0 1 4.5 3.5z" stroke-linejoin="round"/></svg><span>Ansage des Notdienstrings<small><span class="ph">[Rufnummer]</span></small></span></a>
<a class="link-hell" href="tel:+491713920027" data-cta-praxis>Praxis anrufen, <span class="zahl">0171 39200 27</span></a>
</div>
<p class="tier-kosten" data-gebuehr>Im Notdienst gilt mindestens der zweifache GOT-Satz, dazu eine Notdienstgebühr von <span class="ph">50 €</span>.</p>
</section>
</div>
<section class="tier-plan" aria-labelledby="tier-plan-titel">
<div>
<h2 id="tier-plan-titel">Wann Notdienst gilt</h2>
<p>Notdienstzeit ist nachts von 18 bis 8 Uhr, am Wochenende von Freitag 18 Uhr bis Montag 8 Uhr und an Feiertagen. Die Abendsprechstunde ist kein Notdienst.</p>
<ol class="woche" data-woche aria-label="Diese Woche"></ol>
<div class="woche__achse" aria-hidden="true"><i></i><div><span style="left:0">0</span><span style="left:25%">6</span><span style="left:50%">12</span><span style="left:75%">18</span><span style="left:100%">24 Uhr</span></div></div>
<ul class="legende">
<li><i class="l-sp"></i>Sprechstunde</li>
<li><i class="l-nd"></i>Notdienstzeit</li>
<li><i class="l-wir"></i>Notdienst bei uns</li>
<li><i class="l-jetzt"></i>jetzt</li>
</ul>
</div>
<div>
<h2>Unsere nächsten Notdienste</h2>
<ol class="dienste" data-dienste></ol>
</div>
</section>
<div class="tier-hinweise">
<section class="hinweis-k">
<svg viewBox="0 0 28 28" fill="none" stroke="currentColor" stroke-width="1.6" aria-hidden="true"><path d="M14 3.5 25 23H3z" stroke-linejoin="round"/><path d="M14 10.5v6M14 19.5v.5" stroke-linecap="round"/></svg>
<h3>Etwas Giftiges gefressen?</h3>
<p>Rufen Sie sofort an, auch nachts. Das Giftinformationszentrum Ihres Landes berät rund um die Uhr, auch bei Tieren: <span class="ph">[Rufnummer]</span>.</p>
</section>
<section class="hinweis-k">
<svg viewBox="0 0 28 28" fill="none" stroke="currentColor" stroke-width="1.6" aria-hidden="true"><rect x="4" y="7" width="20" height="16" rx="3"/><path d="M14 11v8M10 15h8" stroke-linecap="round"/></svg>
<h3>Lebensbedrohlich?</h3>
<p>Erreichen Sie niemanden im Notdienst, fahren Sie in die Tierklinik mit 24-Stunden-Notaufnahme: <span class="ph">[Name, Ort]</span>. Dort geht es nach Dringlichkeit.</p>
</section>
<section class="hinweis-k">
<svg viewBox="0 0 28 28" fill="none" stroke="currentColor" stroke-width="1.6" aria-hidden="true"><circle cx="14" cy="14" r="10.5"/><path d="M9 14h10" stroke-linecap="round"/></svg>
<h3>Ist ein Mensch verletzt?</h3>
<p>Bei Bissverletzungen oder einem Unfall mit Menschen: <a href="tel:112">112</a>. Danach kümmern wir uns um das Tier.</p>
</section>
</div>
<p class="fuss">Notdienstsätze nach § 4 Gebührenordnung für Tierärzte (GOT): mindestens zweifacher Satz, bis zum vierfachen, dazu eine Notdienstgebühr je Behandlungsfall. Termine aus dem Plan des Notdienstrings, Stand <time datetime="2026-10-30">30.10.2026</time>. Beträge, Zeiten und Rufnummern vom Kunden prüfen lassen.</p>
</main>
</div>
<!-- =================== Variante handwerk =================== -->
<div class="seite" data-nur="handwerk" id="v-handwerk">
<header class="hw-kopf">
<a class="hw-marke" href="#"><b>AHLBACH</b><small>Heizung · Sanitär · Schwandorf</small></a>
<a class="knopf-o" href="tel:+491713920063"><svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.9" aria-hidden="true"><path d="M4.5 3.5h3l1.5 4-2 1.2a9 9 0 0 0 4.3 4.3l1.2-2 4 1.5v3a1.5 1.5 0 0 1-1.6 1.5A14 14 0 0 1 3 5.1 1.5 1.5 0 0 1 4.5 3.5z" stroke-linejoin="round"/></svg>Büro <span class="zahl">0171 39200 63</span></a>
</header>
<main>
<div class="hw-oben">
<section data-box data-zustand="server" aria-labelledby="hw-titel">
<h1 class="hw-label" id="hw-titel"><span>Notdienst Heizung und Sanitär</span></h1>
<p class="hw-label" style="margin-top:12px"><span class="chip chip--zustand"><i></i><span data-kurz>Notdienst im Wechsel</span></span><span class="chip chip--feiertag" data-feiertag hidden></span><span data-uhr style="font-stretch:75%"></span></p>
<div class="hw-status" data-status>
<p class="status__kopf" data-kopf>Notdienst im Wechsel mit der Innung.</p>
<p class="status__text" data-text>An unseren Notdienst-Wochenenden kommen wir. An den anderen hilft der Notdienst der Innung.</p>
</div>
<div class="hw-aktionen" data-cta="wir">
<a class="knopf-o" href="tel:+4917604069063"><svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.9" aria-hidden="true"><path d="M4.5 3.5h3l1.5 4-2 1.2a9 9 0 0 0 4.3 4.3l1.2-2 4 1.5v3a1.5 1.5 0 0 1-1.6 1.5A14 14 0 0 1 3 5.1 1.5 1.5 0 0 1 4.5 3.5z" stroke-linejoin="round"/></svg>Notdienst anrufen <span class="zahl">0176 040690 63</span></a>
</div>
<div class="hw-aktionen" data-cta="andere" hidden>
<a class="knopf-o knopf-o--linie" href="#">Notdienst der Innung <span class="ph">[Rufnummer]</span></a>
<a class="knopf-o" href="tel:+491713920063" data-cta-buero>Büro anrufen <span class="zahl">0171 39200 63</span></a>
</div>
<dl class="hw-fakten">
<div><dt>Im Notdienst</dt><dd>Heizung, Warmwasser, Rohrbruch, verstopfter Abfluss</dd></div>
<div><dt>Einsatzgebiet</dt><dd>Schwandorf und <span class="ph">[25] km</span> rundum</dd></div>
</dl>
</section>
<section class="hw-gas" aria-labelledby="hw-gas-titel">
<p class="hw-label">01 · Zuerst Ihre Sicherheit</p>
<h2 id="hw-gas-titel">Es riecht nach Gas?</h2>
<ol>
<li><span><b>Keine Schalter</b>, kein Licht, kein Feuer, keine Klingel.</span></li>
<li><span><b>Fenster und Türen auf.</b> Wenn möglich: Gashahn am Zähler zu.</span></li>
<li><span><b>Haus verlassen</b> und alle anderen im Haus warnen.</span></li>
<li><span><b>Von draußen anrufen:</b> Notruf 112 und den Entstörungsdienst Ihres Netzbetreibers, <span class="ph">[Rufnummer]</span>.</span></li>
</ol>
<p>Nicht zuerst uns: Bei Gasgeruch sind Feuerwehr und Netzbetreiber schneller und dürfen absperren.</p>
<a class="knopf-o" href="tel:112">Notruf 112</a>
</section>
</div>
<div class="hw-mitte">
<section class="hw-fall">
<p class="hw-label">02 · Rohrbruch</p>
<h3>Wasser läuft aus</h3>
<p>Haupthahn am Wasserzähler zudrehen. Strom im nassen Raum an der Sicherung aus. Dann rufen Sie uns an.</p>
</section>
<section class="hw-fall">
<p class="hw-label">03 · Heizung</p>
<h3>Heizung oder Warmwasser aus</h3>
<p>Fehlercode am Gerät notieren und ein Foto vom Typenschild machen. Das spart am Telefon Zeit und oft eine Anfahrt.</p>
</section>
<section class="hw-fall" aria-labelledby="hw-kosten-titel">
<p class="hw-label">Kosten im Notdienst</p>
<h3 id="hw-kosten-titel">Preis vor der Anfahrt</h3>
<p data-gebuehr>Außerhalb der Bürozeit: Pauschale <span class="ph">[Betrag] €</span>, Arbeitszeit mit <span class="ph">[Zuschlag] %</span>. Den Preis nennen wir am Telefon.</p>
</section>
</div>
<section class="hw-plan" aria-labelledby="hw-plan-titel">
<div>
<h2 id="hw-plan-titel">Notdienst-Wochenenden</h2>
<p>Wir wechseln uns mit den Betrieben der Innung ab. Plan der Innung, Stand <time datetime="2026-10-15">15.10.2026</time>.</p>
</div>
<table class="we">
<caption class="sr">Notdienst-Wochenenden der nächsten Wochen</caption>
<thead><tr><th scope="col">KW</th><th scope="col">Zeitraum</th><th scope="col">Notdienst</th></tr></thead>
<tbody data-we></tbody>
</table>
</section>
<p class="fuss">Hinweise bei Gasgeruch nach Feuerwehr und Netzbetreibern; vor Ort gelten die Regeln Ihres Netzbetreibers. Zuschläge, Rufnummern und Plan vom Kunden prüfen lassen.</p>
</main>
</div>
<!-- =================== Pult =================== -->
<div class="pult" role="region" aria-label="Steuerung der Demo">
<div class="pult__reihe" role="group" aria-label="Variante">
<span class="pult__titel">Variante</span>
<button type="button" data-variante="apotheke" aria-pressed="true">Apotheke</button>
<button type="button" data-variante="tierarzt" aria-pressed="false">Tierarzt</button>
<button type="button" data-variante="handwerk" aria-pressed="false">Handwerk</button>
</div>
<div class="pult__reihe" role="group" aria-label="Uhr der Demo">
<span class="pult__titel">Uhr</span>
<button type="button" data-szene="echt" aria-pressed="true">Echtzeit</button>
<button type="button" data-szene="jetzt" aria-pressed="false">Jetzt Notdienst</button>
<button type="button" data-szene="spaeter" aria-pressed="false">Heute später</button>
<button type="button" data-szene="nicht" aria-pressed="false">Nicht heute</button>
<button type="button" data-szene="feiertag" aria-pressed="false">Feiertag</button>
<button type="button" data-szene="leer" aria-pressed="false">Plan zu Ende</button>
<label class="sr" for="sim-datum">Datum</label><input id="sim-datum" type="date" min="2026-10-26" max="2027-03-31" data-sim-datum>
<label class="sr" for="sim-zeit">Uhrzeit</label><input id="sim-zeit" type="time" step="300" data-sim-zeit>
<output data-sim-anzeige for="sim-datum sim-zeit"></output>
</div>
</div>
<script>
(() => {
const html = document.documentElement;
const $ = (s, r = document) => r.querySelector(s), $$ = (s, r = document) => [...r.querySelectorAll(s)];
const ZONE = 'Europe/Berlin';
/* ---------- Ortszeit über Intl, unabhängig von der Zeitzone des Besuchers ---------- */
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]));
const datum = `${p.year}-${p.month}-${p.day}`;
return { datum, min: (+p.hour % 24) * 60 + +p.minute };
}
// Datum rechnen ohne Zeitzonen-Fallen: immer UTC-Mittag
const tagDatum = s => new Date(s + 'T12:00:00Z');
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();
const tageZwischen = (a, b) => Math.round((tagDatum(b) - tagDatum(a)) / 864e5);
const zuMin = s => { const [h, m] = s.split(':'); return +h * 60 + +m; };
const fmtLang = new Intl.DateTimeFormat('de-DE', { timeZone: 'UTC', weekday: 'long', day: 'numeric', month: 'long' });
const fmtWT = new Intl.DateTimeFormat('de-DE', { timeZone: 'UTC', weekday: 'long' });
const fmtKurz = new Intl.DateTimeFormat('de-DE', { timeZone: 'UTC', weekday: 'short', day: '2-digit', month: '2-digit' });
const fmtTM = new Intl.DateTimeFormat('de-DE', { timeZone: 'UTC', day: 'numeric', month: 'long' });
const fmtMonat = new Intl.DateTimeFormat('de-DE', { timeZone: 'UTC', month: 'long', year: 'numeric' });
const kurzTag = s => fmtKurz.format(tagDatum(s)).replace('.,', ''); // „Do 12.11.“
function kw(s) { const d = tagDatum(s), w = (d.getUTCDay() + 6) % 7; d.setUTCDate(d.getUTCDate() - w + 3); const j = new Date(Date.UTC(d.getUTCFullYear(), 0, 4)); return 1 + Math.round(((d - j) / 864e5 - 3 + (j.getUTCDay() + 6) % 7) / 7); }
/* ---------- Feiertage je Land (Auszug aus lab/fundus/feiertage.json, Stand 02.11.2026) ---------- */
const FEIERTAGE = {
ST: { '2026-12-25': '1. Weihnachtstag', '2026-12-26': '2. Weihnachtstag', '2027-01-01': 'Neujahr', '2027-01-06': 'Heilige Drei Könige', '2027-03-26': 'Karfreitag', '2027-03-29': 'Ostermontag' },
SN: { '2026-11-18': 'Buß- und Bettag', '2026-12-25': '1. Weihnachtstag', '2026-12-26': '2. Weihnachtstag', '2027-01-01': 'Neujahr', '2027-03-26': 'Karfreitag', '2027-03-29': 'Ostermontag' },
BY: { '2026-11-01': 'Allerheiligen', '2026-12-25': '1. Weihnachtstag', '2026-12-26': '2. Weihnachtstag', '2027-01-01': 'Neujahr', '2027-01-06': 'Heilige Drei Könige', '2027-03-26': 'Karfreitag', '2027-03-29': 'Ostermontag' },
};
/* ---------- Daten je Variante (so käme es aus dem CMS bzw. dem Plan der Kammer) ----------
plan: eigene Dienste in Ortszeit [von, bis]; woche: reguläre Zeiten (0 = Sonntag); sonder: Tage mit anderen Zeiten. */
const W = (mo, sa) => ({ 1: mo, 2: mo, 3: mo, 4: mo, 5: mo, 6: sa, 0: [] });
const DATEN = {
apotheke: {
land: 'ST', kurz: false, planBis: '2027-01-31',
woche: W([['08:00', '18:30']], [['08:30', '13:00']]),
sonder: { '2026-12-24': [['08:00', '14:00']], '2026-12-31': [['08:00', '14:00']] },
// 24-Stunden-Dienste, Wechsel um 9:00 Uhr (Wechselzeit aus dem Kammerplan übernehmen)
plan: ['2026-11-01', '2026-11-12', '2026-11-24', '2026-12-06', '2026-12-18', '2026-12-25', '2027-01-06', '2027-01-17', '2027-01-29']
.map(d => [d + ' 09:00', plusTage(d, 1) + ' 09:00']),
szenen: { jetzt: ['2026-11-12', 22 * 60 + 40], spaeter: ['2026-11-12', 7 * 60 + 50], nicht: ['2026-11-05', 21 * 60 + 30], feiertag: ['2026-12-25', 11 * 60], leer: ['2027-02-03', 10 * 60] },
},
tierarzt: {
land: 'SN', kurz: true, planBis: '2027-01-31',
woche: { 1: [['09:00', '12:00'], ['15:00', '19:00']], 2: [['09:00', '12:00'], ['15:00', '19:00']], 3: [['09:00', '12:00'], ['14:00', '17:00']], 4: [['09:00', '12:00'], ['15:00', '19:00']], 5: [['09:00', '12:00'], ['14:00', '17:00']], 6: [['09:00', '11:00']], 0: [] },
sonder: { '2026-12-24': [['09:00', '12:00']], '2026-12-31': [['09:00', '12:00']] },
plan: [['2026-11-06 18:00', '2026-11-09 08:00'], ['2026-11-11 18:00', '2026-11-12 08:00'], ['2026-11-24 18:00', '2026-11-25 08:00'], ['2026-12-04 18:00', '2026-12-07 08:00'],
['2026-12-16 18:00', '2026-12-17 08:00'], ['2026-12-24 18:00', '2026-12-28 08:00'], ['2027-01-13 18:00', '2027-01-14 08:00'], ['2027-01-22 18:00', '2027-01-25 08:00']],
szenen: { jetzt: ['2026-11-07', 14 * 60 + 20], spaeter: ['2026-11-11', 15 * 60 + 30], nicht: ['2026-11-10', 21 * 60], feiertag: ['2026-11-18', 10 * 60], leer: ['2027-02-02', 20 * 60] },
},
handwerk: {
land: 'BY', kurz: true, planBis: '2027-01-31',
woche: { 1: [['07:00', '16:30']], 2: [['07:00', '16:30']], 3: [['07:00', '16:30']], 4: [['07:00', '16:30']], 5: [['07:00', '13:00']], 6: [], 0: [] },
sonder: { '2026-12-24': [['07:00', '12:00']], '2026-12-31': [['07:00', '12:00']] },
plan: [['2026-11-13 13:00', '2026-11-16 07:00'], ['2026-11-27 13:00', '2026-11-30 07:00'], ['2026-12-11 13:00', '2026-12-14 07:00'],
['2026-12-24 12:00', '2026-12-28 07:00'], ['2027-01-08 13:00', '2027-01-11 07:00'], ['2027-01-22 13:00', '2027-01-25 07:00']],
szenen: { jetzt: ['2026-11-14', 9 * 60 + 15], spaeter: ['2026-11-13', 10 * 60 + 30], nicht: ['2026-11-07', 20 * 60], feiertag: ['2026-12-25', 19 * 60 + 30], leer: ['2027-02-06', 11 * 60] },
},
};
let variante = 'apotheke', D = DATEN.apotheke;
const feiertag = s => FEIERTAGE[D.land][s] || '';
const U = '\u00a0Uhr';
const uhr = m => { m = ((m % 1440) + 1440) % 1440; const h = Math.floor(m / 60), mm = m % 60; return D.kurz && !mm ? `${h}${U}` : `${h}:${String(mm).padStart(2, '0')}${U}`; };
const dauer = m => m < 60 ? `${m} Min.` : `${Math.floor(m / 60)} Std.${m % 60 ? ' ' + (m % 60) + ' Min.' : ''}`;
const teile = s => { const [d, t] = s.split(' '); return { d, t: zuMin(t) }; };
// „heute, 9:00 Uhr“ · „morgen, 8 Uhr“ · „Montag, 8 Uhr“ · „Donnerstag, 12. November, 9:00 Uhr“
function wann(s, H) {
const { d, t } = teile(s), k = tageZwischen(H, d);
if (k === 0) return `heute,\u00a0${uhr(t)}`;
if (k === 1) return `morgen,\u00a0${uhr(t)}`;
if (k > 1 && k < 7) return `${fmtWT.format(tagDatum(d))},\u00a0${uhr(t)}`;
return `${fmtLang.format(tagDatum(d))},\u00a0${uhr(t)}`;
}
// Ende eines Dienstes relativ zu seinem Beginn: „bis Freitag, 9:00 Uhr“
function bisText(dienst) {
const a = teile(dienst.von), b = teile(dienst.bis), k = tageZwischen(a.d, b.d);
return k === 0 ? uhr(b.t) : k < 7 ? `${fmtWT.format(tagDatum(b.d))},\u00a0${uhr(b.t)}` : `${fmtLang.format(tagDatum(b.d))},\u00a0${uhr(b.t)}`;
}
function startText(dienst, H) {
const { d, t } = teile(dienst.von), k = tageZwischen(H, d);
const tag = fmtLang.format(tagDatum(d));
return { tag: (k === 0 ? 'heute, ' : k === 1 ? 'morgen, ' : '') + tag, zeit: uhr(t) };
}
/* ---------- Reguläre Zeiten (nur, um „geöffnet“ von „Notdienst“ zu trennen) ---------- */
function zeitenAm(s) { if (D.sonder[s]) return D.sonder[s]; if (feiertag(s)) return []; return D.woche[wochentag(s)]; }
function regulaer(H, m) { const z = zeitenAm(H).map(([a, b]) => [zuMin(a), zuMin(b)]).find(([a, b]) => a <= m && m < b); return z ? { bis: z[1] } : null; }
/* ---------- Zustand ---------- */
function zustand(jetzt) {
const H = jetzt.datum, m = jetzt.min, wt = wochentag(H);
const abs = s => { const { d, t } = teile(s); return tageZwischen(H, d) * 1440 + t; };
const dienste = D.plan.map(([von, bis]) => ({ von, bis, a: abs(von), b: abs(bis) }));
const aktiv = dienste.find(x => x.a <= m && m < x.b) || null;
const naechster = dienste.find(x => x.a > m) || null;
const art = aktiv ? 'jetzt' : naechster && naechster.a < 1440 ? 'spaeter' : naechster ? 'nicht' : 'leer';
return { H, m, wt, art, aktiv, naechster, dienste, ft: feiertag(H), offen: regulaer(H, m) };
}
/* ---------- Texte je Branche ---------- */
function texte(z) {
const { H, aktiv, naechster: n, offen, ft } = z;
const rest = aktiv ? aktiv.b - z.m : 0;
const naechsterSatz = n ? (() => { const s = startText(n, H); return `<b>${s.tag}</b>, ab ${s.zeit} bis ${bisText(n)}`; })() : '';
const planEnde = fmtLang.format(tagDatum(D.planBis));
if (variante === 'apotheke') {
if (z.art === 'jetzt') return {
kurz: 'Notdienst jetzt', kopf: offen ? 'Wir haben heute Notdienst.' : 'Wir haben jetzt Notdienst.',
text: offen ? `Regulär geöffnet bis ${uhr(offen.bis)}, danach Notdienst bis <b>${wann(aktiv.bis, H)}</b>.`
: rest <= 60 ? `Noch <b>${dauer(rest)}</b>, bis ${wann(aktiv.bis, H)}. Danach hat eine andere Apotheke Notdienst.`
: `Bis <b>${wann(aktiv.bis, H)}</b>. Bitte klingeln Sie an der Notdienstklappe.`, seite: 'klappe' };
if (z.art === 'spaeter') return {
kurz: `Notdienst ab ${uhr(n.a)}`, kopf: `Heute ab ${uhr(n.a)} haben wir Notdienst.`,
text: `24 Stunden, bis <b>${bisText(n)}</b>. ${offen ? `Jetzt regulär geöffnet bis ${uhr(offen.bis)}.` : `Bis dahin hat noch eine andere Apotheke Notdienst.`}`, seite: 'verweis', fremdBis: `bis heute, ${uhr(n.a)}` };
if (z.art === 'nicht') return {
kurz: 'Heute kein Notdienst bei uns', kopf: 'Heute hat eine andere Apotheke Notdienst.',
text: `${offen ? `Wir sind jetzt regulär geöffnet, bis ${uhr(offen.bis)}. ` : ft ? `Heute ist ${ft}, wir haben geschlossen. ` : ''}Unser nächster Notdienst: ${naechsterSatz}.`, seite: 'verweis' };
return { kurz: 'Plan folgt', kopf: 'Unser nächster Notdienst steht noch nicht fest.',
text: `Der Notdienstplan der Kammer reicht bis ${planEnde}. Sobald der neue Plan erscheint, stehen unsere Termine hier. Bis dahin hilft die Notdienstsuche.`, seite: 'verweis', fremdBis: 'laut Notdienstsuche' };
}
if (variante === 'tierarzt') {
if (z.art === 'jetzt') return {
kurz: 'Notdienst jetzt', kopf: 'Wir haben jetzt Notdienst.', cta: 'wir',
text: rest <= 60 ? `Noch <b>${dauer(rest)}</b>, bis ${wann(aktiv.bis, H)}. Bitte rufen Sie vorher an.`
: `Bis <b>${wann(aktiv.bis, H)}</b>. Bitte rufen Sie vorher an, damit wir uns vorbereiten können. Wir behandeln nach Dringlichkeit, nicht nach Ankunft.` };
if (z.art === 'spaeter') return {
kurz: `Notdienst ab ${uhr(n.a)}`, kopf: `Heute ab ${uhr(n.a)} haben wir Notdienst.`, cta: 'wir',
text: `Bis <b>${bisText(n)}</b>. ${offen ? `Jetzt ist Sprechstunde, bis ${uhr(offen.bis)}.` : 'Bis dahin hilft im Notfall der Notdienstring.'}` };
if (z.art === 'nicht') return {
kurz: 'Heute kein Notdienst bei uns', kopf: offen ? `Jetzt ist Sprechstunde, bis ${uhr(offen.bis)}.` : 'Heute hat eine andere Praxis Notdienst.', cta: 'andere',
text: `${offen ? 'Den Notdienst danach hat heute eine andere Praxis im Ring. ' : ft ? `Heute ist ${ft}, die Praxis ist geschlossen. ` : ''}Wer Dienst hat, sagt die Ansage des Notdienstrings. Unser nächster Notdienst: ${naechsterSatz}.` };
return { kurz: 'Plan folgt', kopf: 'Der Plan ab Februar steht noch nicht fest.', cta: 'andere',
text: `Unsere Termine im Notdienstring reichen bis ${planEnde}. Wer jetzt Dienst hat, sagt die Ansage des Notdienstrings.` };
}
// handwerk
if (z.art === 'jetzt') return {
kurz: 'Notdienst jetzt', kopf: 'Jetzt Notdienst. Wir kommen.', cta: 'wir',
text: rest <= 60 ? `Noch <b>${dauer(rest)}</b>, bis ${wann(aktiv.bis, H)}. Danach übernimmt das Büro.`
: `Unser Notdienst läuft bis <b>${wann(aktiv.bis, H)}</b>. Rufen Sie an: Wir nennen den Preis, bevor wir losfahren.` };
if (z.art === 'spaeter') return {
kurz: `Notdienst ab ${uhr(n.a)}`, kopf: `Heute ab ${uhr(n.a)} haben wir Notdienst.`, cta: 'wir',
text: `Bis <b>${bisText(n)}</b>. ${offen ? `Jetzt erreichen Sie uns im Büro, bis ${uhr(offen.bis)}.` : ''}` };
if (z.art === 'nicht') return {
kurz: 'Heute kein Notdienst bei uns', kopf: offen ? `Jetzt im Büro, bis ${uhr(offen.bis)}.` : 'Gerade hat die Innung Notdienst.', cta: 'andere',
text: `${offen ? 'Danach hilft der Notdienst der Innung. ' : ft ? `Heute ist ${ft}. Wer Dienst hat, sagt die Innung. ` : 'Wer Dienst hat, sagt die Innung. '}Unser nächster Notdienst: ${naechsterSatz}.` };
return { kurz: 'Plan folgt', kopf: 'Der Plan ab Februar kommt noch.', cta: 'andere',
text: `Unsere Notdienst-Wochenenden stehen bis ${planEnde} fest. Bis der neue Plan der Innung da ist, hilft der Notdienst der Innung.` };
}
/* ---------- Kosten: Uhrzeitregel je Branche ---------- */
function kosten(z) {
const { H, m, wt, ft, aktiv } = z;
if (variante === 'apotheke') {
const an = !!ft || wt === 0 || m >= 20 * 60 || m < 6 * 60 || (H.endsWith('-12-24') && m >= 14 * 60);
const regel = 'von 20 bis 6 Uhr, an Sonn- und Feiertagen ganztags';
if (an) return `${aktiv ? 'Jetzt fällt bei uns' : 'Jetzt fällt in der Notdienstapotheke'} die Notdienstgebühr an: <b><span class="ph">2,50 €</span></b>, einmal je Besuch.<small>Sie gilt ${regel}.</small>`;
return `Jetzt keine Notdienstgebühr.<small>Sie gilt ${regel}: <span class="ph">2,50 €</span>, einmal je Besuch.</small>`;
}
if (variante === 'tierarzt') {
const zeit = !!ft || wt === 6 || wt === 0 || m >= 18 * 60 || m < 8 * 60;
if (aktiv) return `Jetzt gelten Notdienstsätze: mindestens der <b>zweifache GOT-Satz</b>, dazu eine Notdienstgebühr von <b><span class="ph">50 €</span></b>.`;
if (zeit) return `Jetzt ist Notdienstzeit: In der Praxis mit Dienst gelten mindestens der zweifache GOT-Satz und eine Notdienstgebühr von <span class="ph">50 €</span>.`;
return `Jetzt gilt der normale GOT-Satz. Im Notdienst: mindestens der zweifache Satz und <span class="ph">50 €</span> Notdienstgebühr.`;
}
if (!z.offen) return `Jetzt außerhalb der Bürozeit: Pauschale <span class="ph">[Betrag] €</span>, Arbeitszeit mit <span class="ph">[Zuschlag] %</span>. Den Preis nennen wir am Telefon, bevor wir losfahren.`;
return `Jetzt Bürozeit: normale Preise. Im Notdienst: Pauschale <span class="ph">[Betrag] €</span> und <span class="ph">[Zuschlag] %</span> auf die Arbeitszeit.`;
}
/* ---------- Plan-Ansichten ---------- */
function kalender(z, root) {
const start = plusTage(z.H, -((z.wt + 6) % 7)), body = $('[data-kal-body]', root);
const dienstTage = new Map(z.dienste.map(x => [teile(x.von).d, x]));
const aktivTag = z.aktiv ? teile(z.aktiv.von).d : '';
const rows = [];
for (let w = 0; w < 4; w++) {
const tr = document.createElement('tr');
for (let t = 0; t < 7; t++) {
const s = plusTage(start, w * 7 + t), td = document.createElement('td'), ft = feiertag(s), d = dienstTage.get(s);
const vorbei = s < z.H && s !== aktivTag;
td.className = [vorbei && 'vorbei', s === z.H && 'heute', d && 'dienst', s === aktivTag && 'aktiv'].filter(Boolean).join(' ');
let sr = fmtLang.format(tagDatum(s)) + (s === z.H ? ', heute' : '');
if (d) sr += s === aktivTag ? ': Notdienst bei uns, läuft gerade' : `: Notdienst bei uns ab ${uhr(teile(d.von).t)}`;
if (ft) sr += `, Feiertag (${ft})`;
td.innerHTML = `<span aria-hidden="true">${+s.slice(8)}${s.slice(8) === '01' || (w === 0 && t === 0) ? '.' + (+s.slice(5, 7)) + '.' : ''}</span><span class="sr">${sr}</span>`
+ (ft ? `<span class="kal__ft" aria-hidden="true">${ft}</span>` : d && !vorbei ? `<span class="kal__zeit" aria-hidden="true">ab ${uhr(teile(d.von).t).replace(U, '')}</span>` : '');
tr.append(td);
}
rows.push(tr);
}
body.replaceChildren(...rows);
const ende = plusTage(start, 27);
$('[data-plan-zeitraum]', root).textContent = `${fmtTM.format(tagDatum(start))} bis ${fmtTM.format(tagDatum(ende))} ${ende.slice(0, 4)}`;
$('[data-kal-caption]', root).textContent = `Notdienstplan vom ${fmtTM.format(tagDatum(start))} bis ${fmtTM.format(tagDatum(ende))}`;
}
// Notdienstzeit (GOT): nachts 18 bis 8 Uhr, Wochenende, Feiertage, in 30-Minuten-Schritten zu Strecken zusammengefasst
function notdienstzeiten(s) {
const wt = wochentag(s), ft = feiertag(s), aus = [];
for (let m = 0; m < 1440; m += 30) {
const an = !!ft || wt === 6 || wt === 0 || m >= 18 * 60 || m < 8 * 60;
if (an) { const l = aus[aus.length - 1]; if (l && l[1] === m) l[1] = m + 30; else aus.push([m, m + 30]); }
}
return aus;
}
function wochenband(z, root) {
const start = plusTage(z.H, -((z.wt + 6) % 7)), ol = $('[data-woche]', root);
const prozent = m => (m / 1440 * 100).toFixed(3) + '%';
const items = [];
for (let t = 0; t < 7; t++) {
const s = plusTage(start, t), k = tageZwischen(z.H, s), ft = feiertag(s);
const li = document.createElement('li');
if (s === z.H) li.className = 'heute';
const segs = [];
const nd = notdienstzeiten(s), sp = zeitenAm(s).map(([a, b]) => [zuMin(a), zuMin(b)]);
const wir = z.dienste.map(x => [Math.max(x.a - k * 1440, 0), Math.min(x.b - k * 1440, 1440)]).filter(([a, b]) => b > a);
nd.forEach(([a, b]) => segs.push(`<span class="seg-nd" style="left:${prozent(a)};width:${prozent(b - a)}"></span>`));
sp.forEach(([a, b]) => segs.push(`<span class="seg-sp" style="left:${prozent(a)};width:${prozent(b - a)}"></span>`));
wir.forEach(([a, b]) => segs.push(`<span class="seg-wir" style="left:${prozent(a)};width:${prozent(b - a)}"></span>`));
if (s === z.H) segs.push(`<span class="seg-jetzt" style="left:${prozent(z.m)}"></span>`);
const strecke = l => l.map(([a, b]) => `${uhr(a).replace(U, '')} bis ${b === 1440 ? '24' : uhr(b).replace(U, '')}`).join(' und ');
const sr = `${fmtLang.format(tagDatum(s))}${s === z.H ? ', heute' : ''}${ft ? ', ' + ft : ''}: `
+ (sp.length ? `Sprechstunde ${strecke(sp)} Uhr; ` : 'keine Sprechstunde; ')
+ `Notdienstzeit ${strecke(nd)} Uhr` + (wir.length ? `; Notdienst bei uns ${strecke(wir)} Uhr` : '');
li.innerHTML = `<span class="woche__tag" aria-hidden="true">${kurzTag(s)}${s === z.H ? ' · heute' : ''}${ft ? `<small>${ft}</small>` : ''}</span>`
+ `<span class="woche__spur" aria-hidden="true">${segs.join('')}</span><span class="sr">${sr}</span>`;
items.push(li);
}
ol.replaceChildren(...items);
// Liste der nächsten Dienste
const liste = z.dienste.filter(x => x.b > z.m).slice(0, 4);
const ul = $('[data-dienste]', root);
ul.replaceChildren(...(liste.length ? liste.map(x => {
const li = document.createElement('li'), a = teile(x.von);
if (x === z.aktiv) li.className = 'ist-jetzt';
const ftN = [...new Set(Array.from({ length: tageZwischen(a.d, teile(x.bis).d) + 1 }, (_, i) => feiertag(plusTage(a.d, i))).filter(Boolean))];
li.innerHTML = `<b>${kurzTag(a.d)}, ${uhr(a.t)}</b><span>bis ${bisText(x)}</span>${ftN.length ? `<small>mit ${ftN.join(', ')}</small>` : ''}`;
return li;
}) : [Object.assign(document.createElement('li'), { className: 'dienste__leer', textContent: `Plan bis ${fmtLang.format(tagDatum(D.planBis))} veröffentlicht, neue Termine folgen.` })]));
}
function wochenenden(z, root) {
const tb = $('[data-we]', root);
let sa = plusTage(z.H, (6 - z.wt + 7) % 7);
if (z.wt === 0) sa = plusTage(z.H, -1);
if (z.wt === 1 && z.m < 7 * 60) sa = plusTage(z.H, -2);
const rows = [];
for (let i = 0; i < 6; i++, sa = plusTage(sa, 7)) {
const mitte = tageZwischen(z.H, sa) * 1440 + 12 * 60;
const x = z.dienste.find(d => d.a <= mitte && mitte < d.b);
const tr = document.createElement('tr');
const diesesWE = i === 0 && (z.wt === 6 || z.wt === 0 || (z.wt === 1 && z.m < 7 * 60) || (z.wt === 5 && z.m >= 13 * 60));
if ((x && x === z.aktiv) || diesesWE) tr.className = 'ist-jetzt';
const von = x ? teile(x.von) : { d: plusTage(sa, -1), t: 0 }, bis = x ? teile(x.bis) : { d: plusTage(sa, 2), t: 0 };
const ftN = [...new Set(Array.from({ length: tageZwischen(von.d, bis.d) + 1 }, (_, j) => feiertag(plusTage(von.d, j))).filter(Boolean))];
const nachPlan = sa <= D.planBis;
const zeitraum = x ? `${kurzTag(von.d)} ${uhr(von.t)} bis ${kurzTag(bis.d)} ${uhr(bis.t)}` : `${kurzTag(sa)} und ${kurzTag(plusTage(sa, 1))}`;
tr.innerHTML = `<td>${kw(sa)}</td><td>${zeitraum}${ftN.length ? `<small>mit ${ftN.join(', ')}</small>` : ''}${(x && x === z.aktiv) || diesesWE ? '<small>dieses Wochenende</small>' : ''}</td>`
+ `<td>${!nachPlan ? '<span class="wer wer--innung">Plan folgt</span>' : x ? '<span class="wer wer--wir">Ahlbach</span>' : '<span class="wer wer--innung">Innung</span>'}</td>`;
rows.push(tr);
}
tb.replaceChildren(...rows);
}
/* ---------- Zeichnen ---------- */
let sim = null, letzte = '', letzteArt = '';
const ansage = $('[data-ansage]');
const fmtUhrzeile = (H, m) => `${kurzTag(H)}, ${uhr(m)}`;
function zeichnen() {
const jetzt = sim || ortszeit(new Date());
const z = zustand(jetzt), t = texte(z), root = $('#v-' + variante);
const box = $('[data-box]', root), status = $('[data-status]', root);
const schluessel = variante + t.kopf + t.text;
box.dataset.zustand = z.art;
if (schluessel !== letzte) {
$('[data-kopf]', root).textContent = t.kopf;
$('[data-text]', root).innerHTML = t.text;
$('[data-kurz]', root).textContent = t.kurz;
if (letzte && letzte.startsWith(variante)) { status.classList.remove('wechsel'); void status.offsetWidth; status.classList.add('wechsel'); }
letzte = schluessel;
}
// Ansage nur bei Zustandswechsel, die Minuten-Aktualisierung bleibt still
const artKey = variante + z.art;
if (letzteArt && artKey !== letzteArt) ansage.textContent = `${t.kurz}. ${t.kopf}`;
letzteArt = artKey;
const ftChip = $('[data-feiertag]', root);
ftChip.hidden = !z.ft; ftChip.textContent = z.ft ? `Feiertag: ${z.ft}` : '';
$('[data-uhr]', root).textContent = `Stand ${fmtUhrzeile(z.H, z.m)}`;
$('[data-gebuehr]', root).innerHTML = kosten(z);
if (variante === 'apotheke') {
$$('[data-modus]', root).forEach(n => { n.hidden = n.dataset.modus !== t.seite; });
$('[data-klappe-1]', root).textContent = z.offen ? `Bis ${uhr(z.offen.bis)} kommen Sie ganz normal in die Apotheke, danach an die Notdienstklappe rechts neben dem Eingang.` : 'Klingeln Sie an der Notdienstklappe rechts neben dem Eingang.';
$('[data-fremd-zeit]', root).textContent = t.fremdBis || (z.m < 9 * 60 ? 'bis heute, 9:00 Uhr' : 'bis morgen, 9:00 Uhr');
const r = regulaer(z.H, z.m);
$('[data-heute-kopf]', root).textContent = r ? `Jetzt geöffnet, bis ${uhr(r.bis)}.` : z.ft ? `Heute geschlossen (${z.ft}).` : 'Jetzt geschlossen.';
kalender(z, root);
} else {
$$('[data-cta]', root).forEach(n => { n.hidden = n.dataset.cta !== t.cta; });
if (variante === 'tierarzt') { $('[data-cta-praxis]', root).hidden = !z.offen; wochenband(z, root); }
else { $('[data-cta-buero]', root).hidden = !z.offen; wochenenden(z, root); }
}
// Pult
$('[data-sim-anzeige]').textContent = (sim ? 'Simuliert · ' : 'Echtzeit · ') + `${fmtLang.format(tagDatum(z.H))}, ${uhr(z.m).replace(U, '')}${D.kurz && !(z.m % 60) ? ':00' : ''}`;
if (document.activeElement !== $('[data-sim-datum]')) $('[data-sim-datum]').value = z.H;
if (document.activeElement !== $('[data-sim-zeit]')) $('[data-sim-zeit]').value = `${String(Math.floor(z.m / 60)).padStart(2, '0')}:${String(z.m % 60).padStart(2, '0')}`;
}
/* ---------- Reiter der Apotheke mit gleitender Markierung (Pfeiltasten) ---------- */
const tabs = $$('#v-apotheke [role="tab"]'), strich = $('.apo-strich');
function markierung() {
const t = tabs.find(x => x.getAttribute('aria-selected') === 'true');
if (!t || !t.offsetWidth) return;
const erst = !strich.dataset.bereit;
if (erst) strich.style.transition = 'none';
strich.style.width = t.offsetWidth + 'px'; strich.style.translate = t.offsetLeft + 'px 0';
if (erst) { void strich.offsetWidth; strich.style.transition = ''; strich.dataset.bereit = '1'; }
}
function waehle(t, fokus) {
tabs.forEach(x => { const an = x === t; x.setAttribute('aria-selected', String(an)); x.tabIndex = an ? 0 : -1; $('#' + x.getAttribute('aria-controls')).hidden = !an; });
if (fokus) t.focus();
markierung();
}
tabs.forEach((t, i) => {
t.addEventListener('click', () => waehle(t));
t.addEventListener('keydown', e => {
const n = { ArrowRight: i + 1, ArrowLeft: i - 1, Home: 0, End: tabs.length - 1 }[e.key];
if (n === undefined) return;
e.preventDefault(); waehle(tabs[(n + tabs.length) % tabs.length], true);
});
});
new ResizeObserver(markierung).observe($('.apo-reiter'));
/* ---------- Pult ---------- */
const szenenKnoepfe = $$('[data-szene]');
const markiereSzene = k => szenenKnoepfe.forEach(b => b.setAttribute('aria-pressed', String(b.dataset.szene === k)));
function szene(k) {
if (k === 'echt') sim = null; else { const [d, m] = D.szenen[k]; sim = { datum: d, min: m }; }
markiereSzene(k); zeichnen();
}
szenenKnoepfe.forEach(b => b.addEventListener('click', () => { stopp(); szene(b.dataset.szene); }));
function ausEingabe() {
const d = $('[data-sim-datum]').value, t = $('[data-sim-zeit]').value;
if (!/^\d{4}-\d{2}-\d{2}$/.test(d) || !/^\d{2}:\d{2}/.test(t)) return;
stopp(); sim = { datum: d, min: zuMin(t) }; markiereSzene(''); zeichnen();
}
$('[data-sim-datum]').addEventListener('change', ausEingabe);
$('[data-sim-zeit]').addEventListener('change', ausEingabe);
const VARIANTEN = Object.keys(DATEN);
function setzeVariante(v, schreiben) {
variante = VARIANTEN.includes(v) ? v : 'apotheke'; D = DATEN[variante];
html.dataset.v = variante; letzte = ''; letzteArt = '';
$$('[data-variante]').forEach(b => b.setAttribute('aria-pressed', String(b.dataset.variante === variante)));
const k = szenenKnoepfe.find(b => b.getAttribute('aria-pressed') === 'true');
if (k && k.dataset.szene !== 'echt') { const [d, m] = D.szenen[k.dataset.szene]; sim = { datum: d, min: m }; }
zeichnen(); markierung();
if (schreiben) { const p = new URLSearchParams(location.search); p.set('v', variante); history.replaceState(null, '', location.pathname + '?' + p.toString().replace(/=(?=&|$)/g, '')); }
}
$$('[data-variante]').forEach(b => b.addEventListener('click', () => { stopp(); setzeVariante(b.dataset.variante, true); }));
// Echtzeit: zur nächsten vollen Minute neu rechnen und wenn der Tab wieder sichtbar wird
let takt = null;
function ticken() { clearTimeout(takt); if (!sim) zeichnen(); takt = setTimeout(ticken, 60000 - Date.now() % 60000 + 50); }
document.addEventListener('visibilitychange', () => { if (!document.hidden) ticken(); });
setzeVariante(new URLSearchParams(location.search).get('v'), false);
ticken();
/* ---------- Vorführung: die Uhr läuft durch die Zustände der aktuellen Variante ---------- */
// [Szene, Datum, von Minute, bis Minute, Dauer ms]
const PLAENE = {
apotheke: [['nicht', '2026-11-05', 21 * 60 + 30, 21 * 60 + 32, 1800], ['spaeter', '2026-11-12', 8 * 60 + 44, 9 * 60 + 1, 2400], ['jetzt', '2026-11-12', 19 * 60 + 46, 20 * 60 + 2, 2400], ['feiertag', '2026-12-25', 11 * 60, 11 * 60 + 2, 1800], ['leer', '2027-02-03', 10 * 60, 10 * 60 + 1, 1500]],
tierarzt: [['nicht', '2026-11-10', 21 * 60, 21 * 60 + 2, 1800], ['spaeter', '2026-11-11', 16 * 60 + 50, 18 * 60 + 2, 2600], ['jetzt', '2026-11-07', 14 * 60 + 20, 14 * 60 + 22, 1800], ['feiertag', '2026-11-18', 10 * 60, 10 * 60 + 2, 1800], ['leer', '2027-02-02', 20 * 60, 20 * 60 + 1, 1500]],
handwerk: [['nicht', '2026-11-07', 20 * 60, 20 * 60 + 2, 1800], ['spaeter', '2026-11-13', 12 * 60 + 40, 13 * 60 + 2, 2400], ['jetzt', '2026-11-14', 9 * 60 + 15, 9 * 60 + 17, 1800], ['feiertag', '2026-12-25', 19 * 60 + 30, 19 * 60 + 32, 1800], ['leer', '2027-02-06', 11 * 60, 11 * 60 + 1, 1500]],
};
const HALT = 700;
let lauf = null, t0 = 0, angehalten = 0;
function schritt(t) {
const P = PLAENE[variante], gesamt = P.reduce((s, p) => s + p[4] + HALT, 0);
let r = (t - t0) % gesamt;
for (const [k, d, a, b, ms] of P) {
if (r < ms + HALT) {
const min = Math.round(a + (b - a) * Math.min(1, r / ms));
if (!sim || sim.datum !== d || sim.min !== min) { sim = { datum: d, min }; markiereSzene(k); zeichnen(); }
break;
}
r -= ms + HALT;
}
lauf = requestAnimationFrame(schritt);
}
function stopp() { cancelAnimationFrame(lauf); lauf = null; html.classList.remove('vorfuehrung'); }
function vorfuehren() { pause(); html.classList.add('vorfuehrung'); t0 = performance.now(); lauf = requestAnimationFrame(schritt); }
function pause() { if (lauf) angehalten = performance.now() - t0; cancelAnimationFrame(lauf); lauf = null; }
function weiter() { if (lauf || !html.classList.contains('vorfuehrung')) return; t0 = performance.now() - angehalten; lauf = requestAnimationFrame(schritt); }
function neustart() { stopp(); szene('echt'); }
})();
</script>
</body>
</html>