Öffnungszeiten mit Live-Status
„Jetzt geöffnet, schließt in 40 Min.“ aus echten Zeiten mit Mittagspause, Feiertagen und Sonderzeiten, gerechnet in Europe/Berlin, ohne Layout-Sprung.
Praxis, Therapie, Amt, Werkstatt: eigene Sektion „Sprechzeiten“ mit Status, Tagesleiste, Wochentabelle, Feiertagen und Notfallnummern.
Eine Statuszeile sagt, was jetzt gilt: „Jetzt geöffnet, schließt in 40 Min.“, „Mittagspause, ab 14.00 Uhr wieder da“ oder „Heute geschlossen (Allerheiligen), öffnet Montag um 8.00 Uhr“. Grundlage sind die echten Zeiten: Wochenplan mit Mittagspause, dazu Feiertage, Urlaub und Sonderzeiten mit Datum. Gerechnet wird immer in der Zeit des Ortes (Europe/Berlin über Intl), nicht in der Zeit des Besuchers. Der Platz für die Statuszeile ist reserviert, der Server liefert einen neutralen Text, JS ersetzt ihn ohne Layout-Sprung.
Die häufigste Frage an eine Praxis, ein Lokal oder einen Laden ist „Haben die gerade offen?“. Eine Wochentabelle beantwortet das erst nach Rechnen und Abgleichen mit dem Kalender. Die Statuszeile beantwortet es sofort und nennt den nächsten Schritt: anrufen, vorbeikommen oder wann es wieder geht.
Wann einsetzen
Praxis, Therapie, Apotheke, Restaurant, Café, Laden, Werkstatt, Museum, Bürgerbüro. Im Kopf der Seite, in der Kontakt- oder Anfahrt-Sektion und in der unteren Leiste auf dem Telefon.
Wann nicht
Wenn die Zeiten nicht gepflegt werden: Ein falsches „Jetzt geöffnet“ an einem Feiertag ist schlimmer als gar keine Anzeige. Bei Terminbetrieb ohne offene Sprechstunde lieber „Telefon erreichbar“ statt „geöffnet“.
Varianten
- Sprechzeiten-Karte: Status, Tagesleiste mit Mittagspause, Wochentabelle und Sonderzeiten
- Kopfzeile kompakt: Status als Pille im Kopf, Woche im Popover
- Termin nach Vereinbarung (Kanzlei, Architekturbüro): Status nur fürs Telefon, „Telefon erreichbar bis 17 Uhr“
- Auslastung jetzt (Museum, Sauna, Studio) aus echten Zählwerten mit dem üblichen Tagesverlauf; ohne frischen Zählwert entfällt der Block
- Gastronomie über Mitternacht (18 bis 1 Uhr) und Küche schließt früher als die Bar
- Mehrere Standorte mit eigenem Status in einer Liste
- Hinweisleiste über dem Kopf aus denselben Sonderzeiten (Betriebsferien, Sonderöffnung): im ersten HTML, schließbar mit Merken der Meldungs-id, klebt nicht
Umsetzung
Daten nach schema.org-Logik: WOCHE je Wochentag (0 = Sonntag) als Liste von Zeiträumen ['08:00','12:00'], Mittagspause = zwei Zeiträume; SONDER als Liste { von, bis?, grund, zeiten } für Feiertage, Urlaub und Aktionen, die den Tag ganz überschreiben. Ortszeit: Intl.DateTimeFormat('de-DE', { timeZone: 'Europe/Berlin', hourCycle: 'h23', year, month, day, hour, minute, weekday }).formatToParts(new Date()). Daraus eine Liste absoluter Intervalle in Minuten ab heute 0 Uhr (gestern bis + 21 Tage; Ende ≤ Beginn → + 1440 für Zeiten über Mitternacht). Zustand: aktuelles Intervall → geöffnet (ab 60 Min. Countdown, ab 30 Min. „schließt bald“); vorheriges und nächstes Intervall heute → Mittagspause; sonst nächster Beginn (heute, morgen, Wochentag, ab 7 Tagen mit Datum). Takt: setTimeout auf die nächste volle Minute und bei visibilitychange. Ohne Layout-Sprung: Die Statusbox hat eine feste Mindesthöhe; der Server schreibt einen neutralen Text mit den regulären Zeiten hinein, JS ersetzt nur den Inhalt. Die Wochentabelle kommt fertig vom Server, JS markiert nur den heutigen Tag und hängt geänderte Zeiten an. Zeitzone des Besuchers: Status bleibt Ortszeit; weicht die Zone ab, zeigt eine reservierte Zeile die Uhrzeit beim Besucher. In der Demo ersetzt das Pult die Uhr (Tag, Uhrzeit, Echtzeit) und schaltet den Server-Text ein.
Robust umsetzen
Telefon. Eine Spalte. In der Kopfzeilen-Variante steht der Status in der unteren Leiste neben Anrufen und Termin; Tippen öffnet die Woche als Blatt. Kurztexte statt ganzer Sätze.
Ohne Bewegung. Keine Bewegung nötig. Farbwechsel des Statuspunkts ohne Übergang, kein Pulsieren.
Barrierefrei. Status nie nur über Farbe: Kopfzeile sagt immer „geöffnet“, „Mittagspause“ oder „geschlossen“. Die minütliche Aktualisierung ist still; nur ein Zustandswechsel wird über eine eigene aria-live=polite-Region angesagt. Tabelle mit scope=row und scope=col, der heutige Tag zusätzlich mit dem Wort „heute“ markiert. Notfallnummern 116 117 und 112 als tel:-Links immer sichtbar. Text ab 17 px, Kontrast leiser Text #596666 auf Weiß 5,97:1. Feiertage sind Ländersache: Liste je Standort pflegen und vom Kunden prüfen lassen.
Code
<!DOCTYPE html>
<!--
Öffnungszeiten mit Live-Status
Fundus · Sektionen & Funktionen · https://cecodesign.de/lab/fundus/sektionen/oeffnungszeiten-status/
Eigenständige Datei. Bilder liegen hier unter bilder/ – eigene einsetzen.
-->
<html lang="de" data-v="karte">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Öffnungszeiten 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: 'Hedvig Letters Serif'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/hedvig-letters-serif:vf@latest/latin-opsz-normal.woff2') format('woff2-variations'); font-weight: 400; font-display: swap; }
/* Variante Termin (Rezept kanzlei-beratung-souveraen) und Auslastung (Rezept kultur-programm-haltung) */
@font-face { font-family: 'Newsreader'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/newsreader:vf@latest/latin-opsz-normal.woff2') format('woff2-variations'); font-weight: 200 800; font-display: swap; }
@font-face { font-family: 'Schibsted Grotesk'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/schibsted-grotesk:vf@latest/latin-wght-normal.woff2') format('woff2-variations'); font-weight: 400 900; 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; }
@font-face { font-family: 'IBM Plex Mono'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/ibm-plex-mono@latest/latin-500-normal.woff2') format('woff2'); font-weight: 500; font-display: swap; }
@font-face { font-family: 'Google Sans Flex'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/google-sans-flex:vf@latest/latin-wght-normal.woff2') format('woff2-variations'); font-weight: 100 1000; font-display: swap; }
:root {
/* Praxis am Ammerbogen · Rezept praxis-gesundheit-ruhig: Petrol statt Klinikblau, warme Off-Whites */
--grund: #f5f7f7; --karte: #ffffff; --flaeche: #e2e0de; --flaeche-hell: #eeecea; --text: #363b3b; --leise: #596666; --linie: rgba(54, 59, 59, .13);
--marke: #005450; --marke-hell: #e3eeec; --offen: #0f7a5c; --bald: #ffc776; --bald-text: #6b4a00; --zu: #8a9494;
--kurve: cubic-bezier(0.2, 0, 0.38, 0.9); /* token-tech-praezise */
--ui: 200ms;
--rand: clamp(18px, 3.4vw, 48px);
}
* { box-sizing: border-box; margin: 0; }
html { background: var(--grund); color: var(--text); font-family: 'Google Sans Flex', system-ui, sans-serif; -webkit-font-smoothing: antialiased; }
body { min-height: 100svh; font-size: 1.0625rem; line-height: 1.6; padding-bottom: 120px; }
button, input, select { font: inherit; color: inherit; }
a { color: inherit; }
.sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
.label { font-size: .75rem; font-weight: 560; letter-spacing: .08em; text-transform: uppercase; color: var(--leise); }
.zahl { font-variant-numeric: tabular-nums; }
:focus-visible { outline: 3px solid var(--marke); outline-offset: 2px; border-radius: 6px; }
[data-v="karte"] [data-nur]:not([data-nur~="karte"]), [data-v="kompakt"] [data-nur]:not([data-nur~="kompakt"]),
[data-v="termin"] [data-nur]:not([data-nur~="termin"]), [data-v="auslastung"] [data-nur]:not([data-nur~="auslastung"]),
[data-v="hinweisleiste"] [data-nur]:not([data-nur~="hinweisleiste"]) { display: none !important; }
/* ---------- Hinweisleiste (Variante): seitenweit über dem Kopf, im ersten HTML, klebt nicht ---------- */
.hinweis { display: grid; grid-template-rows: 1fr; background: var(--marke); color: #fff; transition: grid-template-rows 150ms var(--kurve); }
.hinweis[hidden] { display: none; }
.hinweis.geht { grid-template-rows: 0fr; }
.hinweis__huelle { overflow: hidden; min-height: 0; }
.hinweis__innen { max-width: 1280px; margin: 0 auto; display: grid; grid-template-columns: auto minmax(0, 1fr) auto auto; align-items: center; gap: 4px 16px; padding: 9px var(--rand); font-size: .9375rem; line-height: 1.45; }
.hinweis__innen > svg { width: 22px; height: 22px; color: var(--bald); flex: none; }
.hinweis__text b { font-weight: 620; }
.hinweis a { color: inherit; text-decoration: underline; text-underline-offset: 3px; text-decoration-thickness: 1px; }
.hinweis a:hover { text-decoration-thickness: 2px; }
.hinweis__mehr { white-space: nowrap; font-weight: 520; }
.hinweis__zu { display: grid; place-items: center; width: 44px; height: 44px; margin: -4px -10px -4px 0; border: 0; border-radius: 50%; background: transparent; color: inherit; cursor: pointer; transition: background-color var(--ui) var(--kurve); }
.hinweis__zu:hover, .hinweis__zu.druck { background: rgba(255, 255, 255, .14); }
.hinweis__zu svg { width: 16px; height: 16px; }
.hinweis :focus-visible { outline: 3px solid var(--bald); outline-offset: 2px; }
/* Sonderöffnung oder Info: helle Leiste statt Petrol */
.hinweis[data-art="info"] { background: var(--marke-hell); color: var(--text); }
.hinweis[data-art="info"] .hinweis__innen > svg { color: var(--marke); }
.hinweis[data-art="info"] .hinweis__zu:hover, .hinweis[data-art="info"] .hinweis__zu.druck { background: rgba(0, 84, 80, .1); }
.hinweis[data-art="info"] :focus-visible { outline-color: var(--marke); }
@media (max-width: 760px) {
.hinweis__innen { grid-template-columns: auto minmax(0, 1fr) auto; align-items: start; padding-block: 10px; }
.hinweis__innen > svg { margin-top: 1px; }
.hinweis__mehr { grid-column: 2; grid-row: 2; justify-self: start; }
.hinweis__zu { grid-column: 3; grid-row: 1 / span 2; margin-top: -10px; }
}
@media (forced-colors: active) { .hinweis { border-bottom: 2px solid CanvasText; } .hinweis__zu { border: 1px solid ButtonText; } }
/* ---------- Kopf ---------- */
.kopf { position: relative; z-index: 5; display: flex; align-items: center; justify-content: space-between; gap: 20px; padding: 16px var(--rand); background: var(--grund); border-bottom: 1px solid var(--linie); }
.marke { display: flex; align-items: center; gap: 12px; text-decoration: none; }
.marke svg { width: 38px; height: 38px; flex: none; }
.marke b { display: block; font: 400 1.25rem/1.1 'Hedvig Letters Serif', serif; font-variation-settings: "opsz" 24; }
.marke span { display: block; font-size: .8125rem; color: var(--leise); }
.kopf nav ul { list-style: none; padding: 0; display: flex; gap: clamp(14px, 2vw, 28px); font-size: .9375rem; font-weight: 500; }
.kopf nav a { text-decoration: none; }
.kopf nav a[aria-current] { color: var(--marke); text-decoration: underline; text-underline-offset: 6px; text-decoration-thickness: 2px; }
.kopf__rechts { display: flex; align-items: center; gap: 12px; }
.knopf { display: inline-flex; align-items: center; justify-content: center; gap: 8px; height: 46px; padding: 0 20px; border-radius: 999px; border: 0; background: var(--marke); color: #fff; font-size: 1rem; font-weight: 520; text-decoration: none; cursor: pointer; transition: background-color var(--ui) var(--kurve); }
.knopf:hover { background: #003f3c; }
.knopf--linie { background: transparent; color: var(--marke); box-shadow: inset 0 0 0 1.5px var(--marke); }
.knopf--linie:hover { background: var(--marke-hell); }
.knopf svg { width: 18px; height: 18px; }
.tel { font-weight: 560; text-decoration: none; white-space: nowrap; }
/* ---------- Statuspille (Variante Kopfzeile) ---------- */
.pille { display: inline-flex; align-items: center; gap: 9px; height: 40px; padding: 0 16px 0 13px; border: 1px solid var(--linie); border-radius: 999px; background: var(--karte); cursor: pointer; font-size: .9375rem; font-weight: 500; white-space: nowrap; min-width: 15.5em; anchor-name: --pille; transition: border-color var(--ui) var(--kurve); }
.pille:hover { border-color: var(--leise); }
.pille svg { width: 14px; height: 14px; margin-left: auto; color: var(--leise); transition: rotate var(--ui) var(--kurve); }
.pille[aria-expanded="true"] svg { rotate: 180deg; }
/* ---------- Statuspunkt ---------- */
.punkt { position: relative; flex: none; width: 10px; height: 10px; border-radius: 50%; background: var(--zu); transition: background-color var(--ui) var(--kurve); }
[data-zustand="offen"] .punkt { background: var(--offen); box-shadow: 0 0 0 4px rgba(15, 122, 92, .14); }
[data-zustand="bald"] .punkt { background: var(--bald); box-shadow: 0 0 0 4px rgba(255, 199, 118, .3); }
[data-zustand="pause"] .punkt { background: transparent; box-shadow: inset 0 0 0 2px var(--offen); }
/* ---------- Sektion Sprechzeiten (Variante Karte) ---------- */
.seite { max-width: 1280px; margin: 0 auto; padding: clamp(28px, 5vh, 56px) var(--rand) 0; }
.einstieg { display: flex; justify-content: space-between; align-items: end; gap: 24px; flex-wrap: wrap; margin-bottom: clamp(22px, 3.6vh, 36px); }
.einstieg h1 { font: 400 clamp(2.25rem, 1.4rem + 3.2vw, 3.6rem)/1.08 'Hedvig Letters Serif', serif; font-variation-settings: "opsz" 24; letter-spacing: -.01em; max-width: 22ch; text-wrap: balance; }
.einstieg p { max-width: 40ch; color: var(--leise); }
.raster { display: grid; grid-template-columns: minmax(0, 7fr) minmax(0, 5fr); gap: clamp(16px, 2vw, 28px); align-items: start; }
.status-karte { background: var(--karte); border-radius: 24px; padding: clamp(24px, 3.2vw, 44px); display: grid; gap: 26px; box-shadow: 0 1px 2px rgba(54, 59, 59, .05); }
.status-karte__kopf { display: flex; justify-content: space-between; gap: 12px; align-items: baseline; flex-wrap: wrap; }
/* Der Platz ist reserviert: zwei Zeilen Kopf, zwei Zeilen Text. Server-Text und Live-Text haben dieselbe Box. */
.status { min-height: 7.5rem; display: grid; align-content: start; gap: 10px; }
.status__kopf { display: flex; align-items: center; gap: 14px; font: 400 clamp(2rem, 1.3rem + 2vw, 2.9rem)/1.1 'Hedvig Letters Serif', serif; font-variation-settings: "opsz" 24; letter-spacing: -.01em; }
.status__kopf .punkt { width: 14px; height: 14px; }
[data-zustand="offen"] .status__kopf .punkt { box-shadow: 0 0 0 6px rgba(15, 122, 92, .14); }
[data-zustand="bald"] .status__kopf .punkt { box-shadow: 0 0 0 6px rgba(255, 199, 118, .32); }
.status__text { font-size: 1.1875rem; line-height: 1.5; max-width: 40ch; font-variant-numeric: tabular-nums; }
.status__text b { font-weight: 600; }
.status__grund { display: inline-flex; align-items: center; height: 28px; padding: 0 12px; border-radius: 999px; background: var(--bald); color: #3d2c00; font-size: .875rem; font-weight: 560; vertical-align: .15em; margin-left: 4px; }
.status--server .status__kopf { font-size: clamp(1.5rem, 1.2rem + 1vw, 1.9rem); }
/* Tagesleiste: Zeiträume des Tages und ein Jetzt-Strich */
.tag { display: grid; gap: 8px; }
.tag__spur { position: relative; height: 40px; border-radius: 10px; background: repeating-linear-gradient(90deg, transparent 0 calc(100% / 13 - 1px), var(--linie) calc(100% / 13 - 1px) calc(100% / 13)); background-color: var(--flaeche-hell); overflow: visible; }
.tag__block { position: absolute; top: 0; bottom: 0; border-radius: 10px; background: var(--marke-hell); box-shadow: inset 0 0 0 1.5px rgba(0, 84, 80, .35); display: grid; place-items: center; font-size: .8125rem; font-weight: 560; color: var(--marke); font-variant-numeric: tabular-nums; white-space: nowrap; overflow: hidden; }
@media (max-width: 400px) { .tag__block { white-space: normal; text-align: center; line-height: 1.1; font-size: .75rem; } }
.tag__block.ist-jetzt { background: var(--marke); color: #fff; box-shadow: none; }
.tag__jetzt { position: absolute; top: -8px; bottom: -8px; width: 2px; margin-left: -1px; background: var(--text); border-radius: 2px; transition: left var(--ui) var(--kurve); }
.tag__jetzt span { position: absolute; bottom: calc(100% + 2px); left: 50%; translate: -50% 0; padding: 1px 7px; border-radius: 6px; background: var(--text); color: #fff; font-size: .75rem; font-weight: 560; font-variant-numeric: tabular-nums; white-space: nowrap; }
.tag__jetzt[hidden] { display: none; }
.tag__achse { display: flex; justify-content: space-between; font-size: .75rem; color: var(--leise); font-variant-numeric: tabular-nums; }
.tag__achse span { width: 0; display: flex; justify-content: center; }
.aktionen { display: flex; flex-wrap: wrap; gap: 10px; }
.notdienst { display: grid; grid-template-columns: auto 1fr; gap: 4px 14px; align-items: start; padding: 16px 18px; border-radius: 14px; background: var(--flaeche-hell); font-size: .9375rem; transition: background-color var(--ui) var(--kurve); }
.notdienst svg { width: 22px; height: 22px; color: var(--marke); grid-row: span 2; margin-top: 2px; }
.notdienst b { font-weight: 600; }
.notdienst a { font-weight: 600; white-space: nowrap; }
.notdienst p:last-child { color: var(--leise); }
[data-zustand="zu"] .notdienst, [data-zustand="pause"] .notdienst { background: #fff4e0; }
/* Woche und Sonderzeiten */
.spalte { display: grid; grid-template-columns: minmax(0, 1fr); gap: clamp(16px, 2vw, 28px); }
.woche-karte, .sonder-karte { background: var(--karte); border-radius: 24px; padding: clamp(20px, 2.4vw, 32px); box-shadow: 0 1px 2px rgba(54, 59, 59, .05); }
.woche-karte h2, .sonder-karte h2 { font: 400 1.5rem/1.2 'Hedvig Letters Serif', serif; margin-bottom: 14px; }
.woche { width: 100%; border-collapse: collapse; font-size: .9375rem; font-variant-numeric: tabular-nums; }
.woche th, .woche td { text-align: left; padding: 9px 8px; white-space: nowrap; border-bottom: 1px solid var(--linie); vertical-align: baseline; }
.woche thead th { font-size: .75rem; font-weight: 560; letter-spacing: .06em; text-transform: uppercase; color: var(--leise); padding-top: 0; }
.woche tbody th { font-weight: 500; width: 30%; }
.woche td.zu { color: var(--leise); }
.woche tr:last-child > * { border-bottom: 0; }
.woche tr.heute > * { background: var(--marke-hell); }
.woche tr.heute > :first-child { border-radius: 10px 0 0 10px; }
.woche tr.heute > :last-child { border-radius: 0 10px 10px 0; }
.heute-marke { display: none; margin-left: 8px; font-size: .6875rem; font-weight: 600; letter-spacing: .06em; text-transform: uppercase; color: var(--marke); }
.heute .heute-marke { display: inline; }
.woche .geaendert { display: block; font-size: .8125rem; color: var(--bald-text); font-weight: 560; white-space: normal; }
.tag__leer { position: absolute; inset: 0; display: grid; place-items: center; font-size: .875rem; color: var(--leise); }
.nur-schmal { display: none; }
.sonder { list-style: none; padding: 0; display: grid; font-size: .9375rem; }
.sonder li { display: grid; grid-template-columns: 7.2em 1fr; gap: 12px; padding: 9px 0; border-bottom: 1px solid var(--linie); }
.sonder li:last-child { border-bottom: 0; }
.sonder time { font-weight: 560; font-variant-numeric: tabular-nums; }
.sonder span { color: var(--leise); }
.sonder li.vorbei { color: var(--leise); } /* vorbei: grau und durchgestrichen statt Deckkraft .45 (Kontrast 1.4.3) */
.sonder li.vorbei time { text-decoration: line-through; }
.sonder li.jetzt { color: var(--text); }
.sonder li.jetzt span { color: var(--text); }
.zeitzone { margin-top: 14px; font-size: .8125rem; color: var(--leise); min-height: 2.6em; }
/* ---------- Variante Kopfzeile: Seite mit Statuspille ---------- */
.held { max-width: 1280px; margin: 0 auto; padding: clamp(40px, 9vh, 110px) var(--rand) 0; display: grid; grid-template-columns: minmax(0, 7fr) minmax(0, 5fr); gap: 48px; align-items: end; }
.held h1 { font: 400 clamp(2.5rem, 1.4rem + 4vw, 4.5rem)/1.06 'Hedvig Letters Serif', serif; letter-spacing: -.01em; }
.held p { color: var(--leise); max-width: 44ch; margin-top: 20px; }
.held__bild { aspect-ratio: 4 / 3.3; border-radius: 24px; overflow: hidden; background: var(--flaeche); }
.held__bild svg { display: block; width: 100%; height: 100%; }
.leistungen { max-width: 1280px; margin: 40px auto 0; padding: 0 var(--rand); display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; }
.leistungen article { background: var(--karte); border-radius: 20px; padding: 22px 24px; }
.leistungen h2 { font: 400 1.375rem/1.2 'Hedvig Letters Serif', serif; }
.leistungen p { color: var(--leise); font-size: .9375rem; margin-top: 6px; }
.woche-pop { width: min(380px, calc(100vw - 24px)); margin: 0; padding: 20px 22px; border: 1px solid var(--linie); border-radius: 18px; background: var(--karte); color: var(--text); box-shadow: 0 24px 60px rgba(30, 40, 40, .18);
position-anchor: --pille; inset: auto; top: anchor(bottom); left: anchor(left); margin-top: 8px; position-try-fallbacks: flip-inline;
opacity: 1; translate: 0 0; transition: opacity var(--ui) var(--kurve), translate var(--ui) var(--kurve), overlay var(--ui) allow-discrete, display var(--ui) allow-discrete; }
.woche-pop:not(:popover-open) { opacity: 0; translate: 0 -6px; }
@starting-style { .woche-pop:popover-open { opacity: 0; translate: 0 -6px; } }
.woche-pop h2 { font: 400 1.25rem/1.2 'Hedvig Letters Serif', serif; margin-bottom: 6px; }
.woche-pop .status__text { font-size: 1rem; margin-bottom: 10px; }
.woche-pop .notdienst { margin-top: 12px; font-size: .875rem; }
.unten { display: none; }
/* ---------- Variante Termin nach Vereinbarung (Kanzlei Weidemann & Thiel, Lüneburg) ---------- */
[data-v="termin"] {
--grund: #f9f8f8; --karte: #ffffff; --flaeche-hell: #efebeb; --text: #212529; --leise: #5e5248; --linie: #d2cdc7;
--marke: #1e1e50; --marke-hell: #e8e7f0; --offen: #0f7a5c; --zu: #8e7c6d;
--kurve: cubic-bezier(0.25, 1, 0.5, 1); /* token-swiss-knackig */
--ui: 250ms;
}
[data-v="termin"] body { font-family: 'Schibsted Grotesk', system-ui, sans-serif; }
[data-v="termin"] .label { font-weight: 500; letter-spacing: .06em; }
[data-v="termin"] .marke b, .t-titel, .t-karte h2, .t-status__kopf { font-family: 'Newsreader', serif; font-weight: 400; }
[data-v="termin"] :focus-visible { outline-color: #1e1e50; }
[data-v="termin"] .knopf:hover { background: #12123a; }
.t-seite { max-width: 1240px; margin: 0 auto; padding: clamp(32px, 6vh, 72px) var(--rand) 0; display: grid; grid-template-columns: minmax(0, 6fr) minmax(0, 5fr); gap: clamp(28px, 5vw, 80px); align-items: start; }
.t-titel { font-size: clamp(2.25rem, 1.3rem + 2.6vw, 3.5rem); line-height: 1.1; letter-spacing: -.01em; color: var(--marke); text-wrap: balance; margin: 10px 0 18px; }
.t-lead { max-width: 52ch; color: var(--leise); margin-bottom: 28px; }
.t-status { border-top: 2px solid var(--marke); padding-top: 22px; display: grid; gap: 14px; }
.t-status__kopf { display: flex; align-items: center; gap: 12px; font-size: clamp(1.5rem, 1.2rem + 1vw, 2rem); line-height: 1.2; min-height: 2.4em; }
.t-status__kopf .punkt { width: 12px; height: 12px; }
.t-status__text { max-width: 48ch; min-height: 3.2em; }
/* Schibsted setzt mit tnum auch den Punkt auf Ziffernbreite: in dieser Variante nur Versalziffern */
[data-v="termin"] .woche, [data-v="termin"] .zahl, [data-v="termin"] .sonder time { font-variant-numeric: lining-nums; }
.t-status__text b { font-weight: 600; }
[data-v="termin"] .status__grund { background: var(--flaeche-hell); color: var(--text); font-weight: 500; }
.t-hinweis { font-size: .9375rem; color: var(--leise); max-width: 52ch; padding: 14px 16px; background: var(--flaeche-hell); border-radius: 4px; }
.t-hinweis b { color: var(--text); font-weight: 600; }
.t-spalte { display: grid; gap: 22px; }
.t-karte { background: var(--karte); border: 1px solid var(--linie); border-radius: 4px; padding: clamp(20px, 2.4vw, 30px); }
.t-karte h2 { font-size: 1.5rem; line-height: 1.2; color: var(--marke); margin-bottom: 12px; }
.t-karte .woche tr.heute > * { background: var(--marke-hell); }
.t-karte .woche tr.heute > :first-child { border-radius: 2px 0 0 2px; }
.t-karte .woche tr.heute > :last-child { border-radius: 0 2px 2px 0; }
[data-v="termin"] .heute-marke { color: var(--marke); }
.t-karte .woche tbody th { width: 40%; }
.t-besprechung { margin-top: 14px; font-size: .9375rem; color: var(--leise); }
.t-besprechung b { color: var(--text); font-weight: 600; }
.t-weg { list-style: none; padding: 0; counter-reset: weg; display: grid; gap: 14px; font-size: .9375rem; }
.t-weg li { counter-increment: weg; display: grid; grid-template-columns: 2rem 1fr; gap: 2px 10px; }
.t-weg li::before { content: counter(weg); grid-row: span 2; font: 400 1.5rem/1 'Newsreader', serif; color: var(--marke); }
.t-weg b { font-weight: 600; }
.t-weg span { color: var(--leise); }
/* ---------- Variante Auslastung jetzt (Kunsthalle Warnowkai, Rostock) ---------- */
[data-v="auslastung"] {
--grund: #efede9; --karte: #f8f7f4; --flaeche-hell: #e4e1db; --text: #0b0b0b; --leise: #57534c; --linie: rgba(11, 11, 11, .16);
--marke: #0b0b0b; --marke-hell: #e4e1db; --offen: #2f7d4a; --zu: #8a857c; --akzent: #c4421a;
--kurve: cubic-bezier(0.25, 1, 0.5, 1); /* token-swiss-knackig */
--ui: 250ms;
}
[data-v="auslastung"] body { font-family: 'Atkinson Hyperlegible Next', system-ui, sans-serif; font-size: 1.125rem; }
[data-v="auslastung"] .label, .mono { font-family: 'IBM Plex Mono', ui-monospace, monospace; font-weight: 500; font-size: .8125rem; letter-spacing: .06em; font-variant-numeric: tabular-nums; }
[data-v="auslastung"] .marke b, .a-titel, .a-karte h2, .a-status__kopf, .last__wort { font-family: 'Schibsted Grotesk', sans-serif; }
[data-v="auslastung"] .marke b { font-weight: 800; font-size: 1.375rem; letter-spacing: -.01em; }
[data-v="auslastung"] .knopf { border-radius: 0; background: var(--text); }
[data-v="auslastung"] .knopf:hover { background: #2b2b2b; }
[data-v="auslastung"] :focus-visible { outline-color: var(--akzent); }
.a-seite { max-width: 1280px; margin: 0 auto; padding: clamp(32px, 6vh, 64px) var(--rand) 0; }
.a-titel { font-weight: 800; font-size: clamp(2.75rem, 1.4rem + 4.6vw, 5.5rem); line-height: 1; letter-spacing: -.02em; margin: 8px 0 clamp(24px, 4vh, 40px); }
.a-raster { display: grid; grid-template-columns: minmax(0, 7fr) minmax(0, 4fr); gap: clamp(20px, 3vw, 40px); align-items: start; }
.a-karte { background: var(--karte); border-top: 3px solid var(--text); padding: clamp(20px, 2.6vw, 34px); display: grid; gap: 22px; }
.a-karte h2 { font-weight: 700; font-size: 1.5rem; line-height: 1.15; }
.a-status__kopf { display: flex; align-items: center; gap: 12px; font-weight: 800; font-size: clamp(1.75rem, 1.3rem + 1.4vw, 2.5rem); line-height: 1.1; letter-spacing: -.01em; min-height: 1.2em; }
.a-status__kopf .punkt { width: 13px; height: 13px; }
.a-status__text { min-height: 1.6em; font-variant-numeric: tabular-nums; }
.last { display: grid; gap: 14px; padding-top: 20px; border-top: 1px solid var(--linie); }
.last__kopf { display: flex; justify-content: space-between; align-items: baseline; gap: 12px; flex-wrap: wrap; }
.beispiel { display: inline-flex; align-items: center; gap: 6px; padding: 3px 9px; border: 1px dashed var(--leise); color: var(--leise); }
.last__wert { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 14px; }
.last__wort { font-weight: 800; font-size: clamp(1.75rem, 1.4rem + 1.2vw, 2.25rem); line-height: 1.1; }
.last__zahl { font-variant-numeric: tabular-nums; }
.last__meter { position: relative; height: 14px; background: var(--flaeche-hell); }
.last__meter span { position: absolute; inset: 0 auto 0 0; width: calc(var(--a, 0) * 100%); background: var(--text); transition: width 300ms var(--kurve); }
.last__meter i { position: absolute; top: -5px; bottom: -5px; width: 2px; background: var(--akzent); left: calc(var(--t, 0) * 100%); transition: left 300ms var(--kurve); }
.last__legende { display: flex; flex-wrap: wrap; gap: 4px 18px; color: var(--leise); }
.last__legende span { display: inline-flex; align-items: center; gap: 6px; }
.last__legende span::before { content: ""; width: 14px; height: 8px; background: var(--text); }
.last__legende span + span::before { width: 2px; height: 14px; background: var(--akzent); }
.last__vergleich { max-width: 52ch; }
.verlauf { display: grid; gap: 8px; margin: 0; }
.verlauf__saeulen { display: grid; grid-auto-flow: column; grid-auto-columns: minmax(0, 1fr); align-items: end; gap: 4px; height: 120px; border-bottom: 1px solid var(--text); }
.verlauf__saeulen div { position: relative; height: calc(var(--h) * 100%); min-height: 3px; background: #c9c4bb; transition: height 300ms var(--kurve), background-color var(--ui) var(--kurve); }
.verlauf__saeulen div.jetzt { background: var(--text); }
.verlauf__saeulen div b { position: absolute; left: -2px; right: -2px; height: 2px; bottom: calc(var(--l) / var(--h) * 100%); background: var(--akzent); }
.verlauf__achse { display: grid; grid-auto-flow: column; grid-auto-columns: minmax(0, 1fr); gap: 4px; color: var(--leise); text-align: center; }
.verlauf figcaption { color: var(--leise); font-size: 1rem; }
.a-woche .woche th, .a-woche .woche td { font-size: 1rem; }
.a-woche .woche tr.heute > * { background: var(--flaeche-hell); border-radius: 0 !important; }
[data-v="auslastung"] .heute-marke { color: var(--text); }
[data-v="auslastung"] .status__grund { background: var(--text); color: var(--grund); border-radius: 0; }
.a-hinweis { font-size: 1rem; color: var(--leise); }
.a-hinweis b { color: var(--text); }
/* ---------- Ohne JS: Server-Text (Pult „Ohne JS“) ---------- */
.ohne-js [data-live] { display: none !important; }
[data-server] { display: none; }
.ohne-js [data-server] { display: grid; }
/* ---------- Demo-Pult: simulierte Uhr ---------- */
.pult { position: fixed; left: 50%; bottom: 14px; translate: -50% 0; z-index: 20; width: min(1080px, calc(100vw - 24px)); display: grid; grid-template-columns: auto auto minmax(160px, 1fr) auto auto; align-items: center; gap: 8px 16px; padding: 10px 12px 10px 18px; border-radius: 18px; background: rgba(0, 46, 44, .95); color: #f5f7f7; box-shadow: 0 12px 40px rgba(0, 0, 0, .22); font-size: .8125rem; }
.pult__titel { font-size: .6875rem; font-weight: 600; letter-spacing: .1em; text-transform: uppercase; color: #9ec9c1; }
.pult__uhr { display: flex; align-items: center; gap: 8px; }
.pult select, .pult button { height: 32px; padding: 0 10px; border-radius: 9px; border: 1px solid rgba(245, 247, 247, .22); background: rgba(255, 255, 255, .06); color: inherit; cursor: pointer; font-size: .8125rem; }
.pult select option { color: #363b3b; }
.pult button[aria-pressed="true"] { background: #f5f7f7; color: #003f3c; border-color: #f5f7f7; }
.pult__zeit { display: flex; align-items: center; gap: 10px; min-width: 0; }
.pult__zeit output { min-width: 3.4em; font-weight: 600; font-variant-numeric: tabular-nums; font-size: .9375rem; }
.pult input[type="range"] { flex: 1; min-width: 80px; accent-color: #ffc776; }
.pult__gruppe { display: flex; gap: 4px; align-items: center; }
.pult__gruppe > span { color: #9ec9c1; margin-right: 4px; font-size: .75rem; }
.pult__titel, .pult__gruppe > span, .pult button { white-space: nowrap; }
.pult select[data-sim-tag] { width: 12.5em; text-overflow: ellipsis; }
.vorschau body { overflow: hidden; padding-bottom: 0; }
.vorschau .einstieg p { display: none; }
/* ---------- Reduzierte Bewegung ---------- */
.reduziert *, .reduziert *::before, .reduziert *::after { transition: none !important; animation: none !important; }
/* ---------- Schmal und Touch ---------- */
@media (max-width: 1180px) { .kopf nav { display: none; } .pult { grid-template-columns: auto minmax(140px, 1fr) auto; } .pult__titel { display: none; } }
@media (max-width: 1100px) { .raster { grid-template-columns: minmax(0, 1.12fr) minmax(0, 1fr); } .woche th, .woche td { padding-inline: 5px; } }
@media (max-width: 920px) {
.raster, .held, .t-seite, .a-raster { grid-template-columns: minmax(0, 1fr); }
.leistungen { grid-template-columns: 1fr; }
.tel { display: none; }
}
@media (max-width: 640px) {
body { font-size: 1.0625rem; padding-bottom: 92px; }
.kopf { padding-block: 12px; }
.marke span { display: none; }
.kopf__rechts .knopf { display: none; }
.einstieg { margin-bottom: 18px; }
.einstieg p { font-size: 1rem; }
.status { min-height: 8rem; }
.marke b { font-size: 1.0625rem; }
.status__text { font-size: 1.0625rem; }
.aktionen .knopf { flex: 1; padding: 0 14px; }
.nur-breit { display: none; } .nur-schmal { display: inline; }
.tag__achse span:nth-child(even) { visibility: hidden; }
.pult { grid-template-columns: 1fr; }
[data-v="kompakt"] .kopf .pille { display: none; }
[data-v="kompakt"] .held__bild { display: none; }
/* Untere Leiste: Status, Anrufen, Termin */
[data-v="kompakt"] .unten { position: fixed; left: 0; right: 0; bottom: 0; z-index: 6; display: grid; grid-template-columns: 1fr auto auto; gap: 8px; padding: 10px 12px calc(10px + env(safe-area-inset-bottom)); background: var(--karte); border-top: 1px solid var(--linie); }
.unten .pille { min-width: 0; width: 100%; padding-inline: 12px 10px; font-size: .875rem; overflow: hidden; }
.unten .pille span:not(.punkt) { overflow: hidden; text-overflow: ellipsis; }
.unten .knopf { height: 40px; padding: 0 16px; font-size: .9375rem; }
.unten .knopf--rund { width: 40px; padding: 0; }
[data-v="kompakt"] .pult { top: 10px; bottom: auto; }
.t-status .aktionen .knopf, .a-karte .aktionen .knopf { flex: 1 1 auto; }
.verlauf__achse span:nth-child(even) { visibility: hidden; }
.verlauf__saeulen { height: 96px; gap: 3px; }
}
/* Sehr schmal (320 px): Wochentabelle umbrechen statt querscrollen */
@media (max-width: 400px) {
.woche th, .woche td { white-space: normal; padding-inline: 4px; }
.woche thead th { letter-spacing: .02em; font-size: .6875rem; }
}
</style>
</head>
<body>
<!-- Variante Hinweisleiste: kommt fertig vom Server, steht vor dem Kopf und reserviert ihre Höhe von Anfang an.
Gleiche Datenquelle wie die Sonderzeiten (SONDER: von, bis, Grund). Höchstens eine Leiste, kein Laufband, klebt nicht. -->
<div class="hinweis" role="region" aria-labelledby="hinweis-titel" data-nur="hinweisleiste" data-hinweis data-id="urlaub-2026-11-16" data-art="zu">
<div class="hinweis__huelle"><div class="hinweis__innen">
<svg viewBox="0 0 22 22" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="1.6"><rect x="2.8" y="4.2" width="16.4" height="14.6" rx="2.4"/><path d="M2.8 8.6h16.4M7.2 2.4v3.4M14.8 2.4v3.4M8 12.6l6 4M14 12.6l-6 4" stroke-linecap="round"/></svg>
<p class="hinweis__text"><b id="hinweis-titel" data-hinweis-titel>Praxisurlaub vom 16. bis 20. November.</b> <span data-hinweis-text>Vertretung: Praxis Hagelloch, <a href="tel:+497071663180">07071 66 31 80</a>. Rezepte bitte bis Freitag, 13. November bestellen.</span></p>
<a class="hinweis__mehr" href="#sonderzeiten">Alle Sonderzeiten</a>
<button type="button" class="hinweis__zu" data-hinweis-zu aria-label="Hinweis schließen"><svg viewBox="0 0 16 16" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"><path d="M3 3l10 10M13 3 3 13"/></svg></button>
</div></div>
</div>
<script>/* Vor dem ersten Zeichnen: eine schon geschlossene Meldung bleibt zu, ohne Sprung. */
try { var hw = document.querySelector('[data-hinweis]'); if (localStorage.getItem('praxis-hinweis-zu') === hw.dataset.id) hw.hidden = true; } catch (e) {}</script>
<header class="kopf">
<a class="marke" href="#start" data-nur="termin"><div><b>Weidemann & Thiel</b><span>Steuerberatung · Lüneburg</span></div></a>
<a class="marke" href="#start" data-nur="auslastung"><div><b>Kunsthalle Warnowkai</b><span>Rostock</span></div></a>
<a class="marke" href="#start" data-nur="karte kompakt hinweisleiste">
<svg viewBox="0 0 38 38" aria-hidden="true"><rect width="38" height="38" rx="11" fill="#005450"/><path d="M9 27c0-7.2 4.5-13 10-13s10 5.8 10 13" fill="none" stroke="#f5f7f7" stroke-width="2.2" stroke-linecap="round"/><path d="M14 27c0-4.3 2.2-7.6 5-7.6s5 3.3 5 7.6" fill="none" stroke="#ffc776" stroke-width="2.2" stroke-linecap="round"/></svg>
<div><b>Praxis am Ammerbogen</b><span>Hausarztpraxis · Tübingen-Lustnau</span></div>
</a>
<nav aria-label="Hauptnavigation"><ul data-nur="karte kompakt hinweisleiste">
<li><a href="#leistungen">Leistungen</a></li><li><a href="#team">Team</a></li><li><a href="#sprechzeiten" aria-current="page">Sprechzeiten</a></li><li><a href="#anfahrt">Anfahrt</a></li>
</ul><ul data-nur="termin">
<li><a href="#leistungen">Leistungen</a></li><li><a href="#team">Kanzlei</a></li><li><a href="#kontakt" aria-current="page">Kontakt</a></li><li><a href="#mandanten">Für Mandanten</a></li>
</ul><ul data-nur="auslastung">
<li><a href="#programm">Programm</a></li><li><a href="#besuch" aria-current="page">Besuch</a></li><li><a href="#vermittlung">Vermittlung</a></li><li><a href="#haus">Haus</a></li>
</ul></nav>
<div class="kopf__rechts">
<button type="button" class="pille" data-nur="kompakt" data-pille popovertarget="woche-pop" aria-expanded="false" data-zustand="zu"><span class="punkt" aria-hidden="true"></span><span data-pille-text>Sprechzeiten</span><svg viewBox="0 0 14 14" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="1.6"><path d="m3.5 5.5 3.5 3.5 3.5-3.5"/></svg></button>
<a class="tel" href="tel:+4970715562040" data-nur="karte hinweisleiste">07071 556 20 40</a>
<a class="knopf" href="#termin" data-nur="karte kompakt hinweisleiste">Termin vereinbaren</a>
<a class="tel" href="tel:+494131228540" data-nur="termin">04131 22 85 40</a>
<a class="knopf" href="#anfrage" data-nur="termin">Termin anfragen</a>
<a class="knopf" href="#tickets" data-nur="auslastung">Tickets</a>
</div>
</header>
<!-- Variante Karte: Sektion Sprechzeiten -->
<main class="seite" id="sprechzeiten" data-nur="karte hinweisleiste">
<div class="einstieg">
<h1>Wann wir für Sie da sind.</h1>
<p>Ohne Termin kommen Sie in die offene Sprechstunde. Für Vorsorge und Impfungen vereinbaren Sie bitte einen Termin.</p>
</div>
<div class="raster">
<section class="status-karte" aria-labelledby="status-titel" data-zustand="zu" data-box>
<div class="status-karte__kopf"><h2 class="label" id="status-titel" data-heute>Sprechzeiten heute</h2><span class="label" data-ort>Ortszeit Tübingen</span></div>
<!-- Server liefert diesen Text. JS ersetzt ihn in derselben, reservierten Box. -->
<div class="status status--server" data-server>
<p class="status__kopf">Sprechzeiten</p>
<p class="status__text">Mo, Di 8.00–12.00 und 14.00–18.00 Uhr · Mi 8.00–12.00 · Do 8.00–12.00 und 14.00–19.00 · Fr 8.00–13.00 Uhr</p>
</div>
<div class="status" data-live>
<p class="status__kopf"><span class="punkt" aria-hidden="true"></span><span data-kopf>Jetzt geöffnet</span></p>
<p class="status__text" data-text> </p>
</div>
<p class="sr" aria-live="polite" data-ansage></p>
<div class="tag" data-live aria-hidden="true">
<div class="tag__spur" data-spur><div class="tag__jetzt" data-jetzt hidden><span data-jetzt-zeit>11.20</span></div></div>
<div class="tag__achse"><span>7</span><span>8</span><span>9</span><span>10</span><span>11</span><span>12</span><span>13</span><span>14</span><span>15</span><span>16</span><span>17</span><span>18</span><span>19</span><span>20</span></div>
</div>
<div class="aktionen">
<a class="knopf" href="tel:+4970715562040"><svg viewBox="0 0 18 18" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="1.6"><path d="M4.2 2.5h2.6l1.3 3.3-1.7 1.1a8 8 0 0 0 4.7 4.7l1.1-1.7 3.3 1.3v2.6a1.6 1.6 0 0 1-1.7 1.6A12.6 12.6 0 0 1 2.6 4.2a1.6 1.6 0 0 1 1.6-1.7Z" stroke-linejoin="round"/></svg><span class="nur-breit">07071 556 20 40</span><span class="nur-schmal">Anrufen</span></a>
<a class="knopf knopf--linie" href="#termin">Online-Termin</a>
</div>
<div class="notdienst">
<svg viewBox="0 0 22 22" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="1.6"><circle cx="11" cy="11" r="9"/><path d="M11 6.5v5M11 14.6v.4" stroke-linecap="round"/></svg>
<p><b>Außerhalb der Sprechzeiten:</b> Ärztlicher Bereitschaftsdienst <a href="tel:116117">116 117</a></p>
<p>Bei Lebensgefahr sofort <a href="tel:112">112</a> wählen.</p>
</div>
</section>
<div class="spalte">
<section class="woche-karte" aria-labelledby="woche-titel">
<h2 id="woche-titel">Sprechzeiten</h2>
<table class="woche" data-woche>
<thead><tr><th scope="col">Tag</th><th scope="col">Vormittag</th><th scope="col">Nachmittag</th></tr></thead>
<tbody>
<tr data-wt="1"><th scope="row">Montag<span class="heute-marke">heute</span></th><td><time>8.00</time>–<time>12.00</time></td><td><time>14.00</time>–<time>18.00</time></td></tr>
<tr data-wt="2"><th scope="row">Dienstag<span class="heute-marke">heute</span></th><td><time>8.00</time>–<time>12.00</time></td><td><time>14.00</time>–<time>18.00</time></td></tr>
<tr data-wt="3"><th scope="row">Mittwoch<span class="heute-marke">heute</span></th><td><time>8.00</time>–<time>12.00</time></td><td class="zu">geschlossen</td></tr>
<tr data-wt="4"><th scope="row">Donnerstag<span class="heute-marke">heute</span></th><td><time>8.00</time>–<time>12.00</time></td><td><time>14.00</time>–<time>19.00</time></td></tr>
<tr data-wt="5"><th scope="row">Freitag<span class="heute-marke">heute</span></th><td><time>8.00</time>–<time>13.00</time></td><td class="zu">geschlossen</td></tr>
<tr data-wt="6"><th scope="row">Samstag<span class="heute-marke">heute</span></th><td class="zu" colspan="2">geschlossen</td></tr>
<tr data-wt="0"><th scope="row">Sonntag<span class="heute-marke">heute</span></th><td class="zu" colspan="2">geschlossen</td></tr>
</tbody>
</table>
</section>
<section class="sonder-karte" id="sonderzeiten" aria-labelledby="sonder-titel">
<h2 id="sonder-titel">Feiertage und Sonderzeiten</h2>
<ul class="sonder" data-sonder-liste></ul>
<p class="zeitzone" data-zeitzone>Alle Zeiten in deutscher Zeit.</p>
</section>
</div>
</div>
</main>
<!-- Variante Kopfzeile: Status als Pille, Woche im Popover -->
<main data-nur="kompakt">
<section class="held">
<div>
<p class="label">Hausarztpraxis in Tübingen-Lustnau</p>
<h1>Ihre Hausärzte, zwei Haltestellen vom Neckar.</h1>
<p>Dr. Ruth Kienle und Jonas Ebinger begleiten Familien, Studierende und ältere Menschen. Offene Sprechstunde am Vormittag, Termine am Nachmittag.</p>
</div>
<div class="held__bild" aria-hidden="true">
<svg viewBox="0 0 400 330" preserveAspectRatio="xMidYMid slice">
<rect width="400" height="330" fill="#e2e0de"/><rect y="236" width="400" height="94" fill="#d3cfca"/>
<rect x="34" y="44" width="150" height="176" rx="8" fill="#eef3f2"/><circle cx="140" cy="92" r="20" fill="#ffc776" opacity=".85"/><path d="M34 196c34-22 70-30 150-14v38H34Z" fill="#c9d6cf"/><path d="M34 132h150M109 44v176" stroke="#e2e0de" stroke-width="7"/>
<path d="M232 236V146a62 62 0 0 1 124 0v90Z" fill="#005450"/><path d="M252 236V156a42 42 0 0 1 84 0v80Z" fill="#0b6560"/>
<rect x="210" y="214" width="168" height="12" rx="6" fill="#b4ada5"/><rect x="226" y="226" width="8" height="60" rx="3" fill="#9c958d"/><rect x="354" y="226" width="8" height="60" rx="3" fill="#9c958d"/>
<g fill="#7b9a77"><ellipse cx="78" cy="226" rx="11" ry="32" transform="rotate(-28 78 226)"/><ellipse cx="114" cy="224" rx="11" ry="32" transform="rotate(26 114 224)"/></g>
<g fill="#8fae89"><ellipse cx="96" cy="214" rx="12" ry="38"/><ellipse cx="70" cy="246" rx="9" ry="24" transform="rotate(-58 70 246)"/><ellipse cx="122" cy="246" rx="9" ry="24" transform="rotate(58 122 246)"/></g>
<path d="M74 258h44l-6 42H80Z" fill="#c98f6b"/><rect x="70" y="252" width="52" height="10" rx="3" fill="#b97d5b"/>
</svg>
</div>
</section>
<section class="leistungen" aria-label="Leistungen">
<article><h2>Offene Sprechstunde</h2><p>Mo bis Fr am Vormittag, ohne Termin. Bitte Versichertenkarte mitbringen.</p></article>
<article><h2>Vorsorge und Impfungen</h2><p>Check-up ab 35, Hautkrebs-Screening, Grippe- und Reiseimpfungen mit Termin.</p></article>
<article><h2>Hausbesuche</h2><p>Für Patientinnen und Patienten, die nicht in die Praxis kommen können.</p></article>
</section>
<div class="unten">
<button type="button" class="pille" data-pille popovertarget="woche-pop" aria-expanded="false" data-zustand="zu"><span class="punkt" aria-hidden="true"></span><span data-pille-kurz>Sprechzeiten</span></button>
<a class="knopf knopf--linie knopf--rund" href="tel:+4970715562040" aria-label="Anrufen: 07071 556 20 40"><svg viewBox="0 0 18 18" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="1.6"><path d="M4.2 2.5h2.6l1.3 3.3-1.7 1.1a8 8 0 0 0 4.7 4.7l1.1-1.7 3.3 1.3v2.6a1.6 1.6 0 0 1-1.7 1.6A12.6 12.6 0 0 1 2.6 4.2a1.6 1.6 0 0 1 1.6-1.7Z" stroke-linejoin="round"/></svg></a>
<a class="knopf" href="#termin">Termin</a>
</div>
</main>
<!-- Variante Termin: Termin nach Vereinbarung, Status nur fürs Telefon -->
<main class="t-seite" id="kontakt" data-nur="termin">
<div>
<p class="label">Kontakt</p>
<h1 class="t-titel">Termine nach Vereinbarung.</h1>
<p class="t-lead">Für Ihr Anliegen nehmen wir uns Zeit. Deshalb planen wir jede Besprechung vorab, gern auch vor 8 oder nach 18 Uhr. Am Telefon sind wir zu festen Zeiten für Sie da.</p>
<section class="t-status" aria-labelledby="t-status-titel" data-zustand="zu">
<h2 class="label" id="t-status-titel" data-t-heute>Telefon · Sekretariat</h2>
<div class="status status--server" data-server>
<p class="t-status__kopf">Telefon: Mo–Do 8.30–12.30 und 13.30–17 Uhr, Fr 8.30–13 Uhr</p>
</div>
<p class="t-status__kopf" data-live><span class="punkt" aria-hidden="true"></span><span data-t-kopf>Telefon erreichbar bis 17 Uhr</span></p>
<p class="t-status__text" data-live data-t-text> </p>
<p class="sr" aria-live="polite" data-t-ansage></p>
<div class="aktionen">
<a class="knopf" href="tel:+494131228540"><svg viewBox="0 0 18 18" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="1.6"><path d="M4.2 2.5h2.6l1.3 3.3-1.7 1.1a8 8 0 0 0 4.7 4.7l1.1-1.7 3.3 1.3v2.6a1.6 1.6 0 0 1-1.7 1.6A12.6 12.6 0 0 1 2.6 4.2a1.6 1.6 0 0 1 1.6-1.7Z" stroke-linejoin="round"/></svg>04131 22 85 40</a>
<a class="knopf knopf--linie" href="#rueckruf">Rückruf vereinbaren</a>
</div>
<p class="t-hinweis"><b>Eine Frist läuft ab?</b> Schreiben Sie uns mit dem Betreff „Frist“. Das Postfach lesen wir auch außerhalb der Telefonzeiten.</p>
</section>
</div>
<div class="t-spalte">
<section class="t-karte" aria-labelledby="t-woche-titel">
<h2 id="t-woche-titel">Telefonzeiten</h2>
<table class="woche" data-t-woche>
<thead><tr><th scope="col">Tag</th><th scope="col">Telefon</th></tr></thead>
<tbody>
<tr data-twt="1"><th scope="row">Montag<span class="heute-marke">heute</span></th><td><time>8.30</time>–<time>12.30</time>, <time>13.30</time>–<time>17.00</time></td></tr>
<tr data-twt="2"><th scope="row">Dienstag<span class="heute-marke">heute</span></th><td><time>8.30</time>–<time>12.30</time>, <time>13.30</time>–<time>17.00</time></td></tr>
<tr data-twt="3"><th scope="row">Mittwoch<span class="heute-marke">heute</span></th><td><time>8.30</time>–<time>12.30</time>, <time>13.30</time>–<time>17.00</time></td></tr>
<tr data-twt="4"><th scope="row">Donnerstag<span class="heute-marke">heute</span></th><td><time>8.30</time>–<time>12.30</time>, <time>13.30</time>–<time>17.00</time></td></tr>
<tr data-twt="5"><th scope="row">Freitag<span class="heute-marke">heute</span></th><td><time>8.30</time>–<time>13.00</time></td></tr>
<tr data-twt="6 0"><th scope="row">Wochenende<span class="heute-marke">heute</span></th><td class="zu">nicht besetzt</td></tr>
</tbody>
</table>
<p class="t-besprechung"><b>Besprechungen:</b> nach Vereinbarung, Mo–Fr zwischen 7.30 und 20 Uhr, in der Kanzlei oder per Video.</p>
</section>
<section class="t-karte" aria-labelledby="t-weg-titel">
<h2 id="t-weg-titel">So kommen Sie zu einem Termin</h2>
<ol class="t-weg">
<li><b>Anrufen oder Anfrage schicken</b><span>Ein Satz zum Anliegen genügt.</span></li>
<li><b>Vorschlag am nächsten Werktag</b><span>Wir nennen zwei Termine und sagen, welche Unterlagen Sie mitbringen.</span></li>
<li><b>Erstgespräch, 30 Minuten</b><span>Kostenlos, wenn daraus kein Auftrag wird.</span></li>
</ol>
</section>
</div>
</main>
<!-- Variante Auslastung: nur mit echten Zählwerten, sonst entfällt der Block -->
<main class="a-seite" id="besuch" data-nur="auslastung">
<p class="label">Besuch planen</p>
<h1 class="a-titel">Heute in der Kunsthalle</h1>
<div class="a-raster">
<section class="a-karte" aria-labelledby="a-status-titel" data-zustand="zu">
<h2 class="label" id="a-status-titel" data-a-heute>Heute</h2>
<div class="status status--server" data-server>
<p class="a-status__kopf">Di–So 10–18 Uhr, Do bis 21 Uhr</p>
</div>
<p class="a-status__kopf" data-live><span class="punkt" aria-hidden="true"></span><span data-a-kopf>Geöffnet</span></p>
<p class="a-status__text" data-live data-a-text> </p>
<p class="sr" aria-live="polite" data-a-ansage></p>
<div class="last" data-live data-last>
<div class="last__kopf"><h2 class="label" id="last-titel">Auslastung jetzt</h2><span class="beispiel mono" title="In der Demo erfunden. Auf der echten Seite kommen die Werte aus der Einlasszählung.">Beispielwerte</span></div>
<p class="last__wert"><span class="last__wort" data-a-wort>ruhig</span><span class="last__zahl" data-a-zahl></span></p>
<div class="last__meter" role="meter" aria-labelledby="last-titel" aria-valuemin="0" aria-valuemax="350" aria-valuenow="0" data-a-meter><span></span><i></i></div>
<p class="last__legende mono"><span>jetzt</span><span>üblich um diese Zeit</span></p>
<p class="last__vergleich" data-a-vergleich></p>
<p class="mono" style="color:var(--leise)" data-a-stand>Stand –</p>
</div>
<figure class="verlauf" data-live data-verlauf>
<div class="verlauf__saeulen" data-saeulen aria-hidden="true"></div>
<div class="verlauf__achse mono" data-achse aria-hidden="true"></div>
<figcaption data-verlauf-text>Übliche Auslastung</figcaption>
</figure>
<div class="aktionen">
<a class="knopf" href="#tickets">Zeitfenster-Ticket buchen</a>
</div>
</section>
<div class="t-spalte">
<section class="a-karte a-woche" aria-labelledby="a-woche-titel">
<h2 id="a-woche-titel">Öffnungszeiten</h2>
<table class="woche">
<tbody>
<tr data-awt="1"><th scope="row">Montag<span class="heute-marke">heute</span></th><td class="zu">geschlossen</td></tr>
<tr data-awt="2 3 5"><th scope="row">Di, Mi, Fr<span class="heute-marke">heute</span></th><td><time>10</time>–<time>18</time> Uhr</td></tr>
<tr data-awt="4"><th scope="row">Donnerstag<span class="heute-marke">heute</span></th><td><time>10</time>–<time>21</time> Uhr</td></tr>
<tr data-awt="6 0"><th scope="row">Sa, So<span class="heute-marke">heute</span></th><td><time>10</time>–<time>18</time> Uhr</td></tr>
</tbody>
</table>
<p class="a-hinweis">Letzter Einlass 30 Minuten vor Schluss. <b>Am ruhigsten:</b> Donnerstag ab 19 Uhr.</p>
</section>
<section class="a-karte" aria-labelledby="a-sonder-titel">
<h2 id="a-sonder-titel">Feiertage</h2>
<ul class="sonder" data-a-sonder></ul>
</section>
</div>
</div>
</main>
<div id="woche-pop" class="woche-pop" popover aria-labelledby="pop-titel">
<h2 id="pop-titel">Sprechzeiten</h2>
<p class="status__text" data-pop-text></p>
<div data-pop-woche></div>
<div class="notdienst"><svg viewBox="0 0 22 22" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="1.6"><circle cx="11" cy="11" r="9"/><path d="M11 6.5v5M11 14.6v.4" stroke-linecap="round"/></svg><p><b>Bereitschaftsdienst</b> <a href="tel:116117">116 117</a></p><p>Lebensgefahr: <a href="tel:112">112</a></p></div>
</div>
<div class="pult">
<span class="pult__titel">Uhr der Demo</span>
<div class="pult__uhr">
<button type="button" data-live-knopf aria-pressed="true">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="0" max="1439" step="5" value="680" data-sim-zeit>
<output for="sim-zeit" data-sim-anzeige>11.20</output>
</div>
<div class="pult__gruppe" role="group" aria-label="Zeitzone des Besuchers"><span>Besucher in</span>
<select data-besucher aria-label="Zeitzone des Besuchers">
<option value="">eigener Zeitzone</option><option value="Europe/Berlin">Berlin</option><option value="America/New_York">New York</option><option value="Asia/Tokyo">Tokio</option>
</select>
</div>
<div class="pult__gruppe">
<button type="button" data-variante="karte" aria-pressed="true">Karte</button>
<button type="button" data-variante="kompakt" aria-pressed="false">Kopfzeile</button>
<button type="button" data-variante="termin" aria-pressed="false">Termin</button>
<button type="button" data-variante="auslastung" aria-pressed="false">Auslastung</button>
<button type="button" data-variante="hinweisleiste" aria-pressed="false">Hinweisleiste</button>
<button type="button" data-hinweis-neu data-nur="hinweisleiste">Leiste zurücksetzen</button>
<button type="button" data-zaehlung aria-pressed="true" data-nur="auslastung">Zählung an</button>
<button type="button" data-ohne-js aria-pressed="false">Ohne JS</button>
</div>
</div>
<script>
(() => {
const html = document.documentElement;
const reduziert = () => html.classList.contains('reduziert');
const ZONE = 'Europe/Berlin';
/* ---------- Daten (so käme es aus dem CMS) ---------- */
// Woche: 0 = Sonntag … 6 = Samstag. Mittagspause = zwei Zeiträume. Ende vor Beginn hieße: über Mitternacht.
const WOCHE = {
1: [['08:00', '12:00'], ['14:00', '18:00']],
2: [['08:00', '12:00'], ['14:00', '18:00']],
3: [['08:00', '12:00']],
4: [['08:00', '12:00'], ['14:00', '19:00']],
5: [['08:00', '13:00']],
6: [], 0: [],
};
// Sonderzeiten überschreiben die Woche (Feiertage Baden-Württemberg, Urlaub, Aktionen)
const SONDER = [
{ von: '2026-11-01', grund: 'Allerheiligen', zeiten: [] },
{ von: '2026-11-14', grund: 'Grippeimpfung ohne Termin', zeiten: [['09:00', '12:00']] },
{ von: '2026-11-19', grund: 'Teamfortbildung am Nachmittag', zeiten: [['08:00', '12:00']] },
{ von: '2026-12-23', bis: '2027-01-01', grund: 'Praxisurlaub · Vertretung: Praxis Hagelloch, 07071 66 31 80', zeiten: [] },
{ von: '2027-01-06', grund: 'Heilige Drei Könige', zeiten: [] },
];
/* ---------- 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', weekday: 'short' });
const WT = { So: 0, Mo: 1, Di: 2, Mi: 3, Do: 4, Fr: 5, Sa: 6 };
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, wt: WT[p.weekday.replace('.', '')] };
}
// 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 fmtTag = new Intl.DateTimeFormat('de-DE', { timeZone: 'UTC', weekday: 'long', day: 'numeric', month: 'long' });
const fmtKurz = new Intl.DateTimeFormat('de-DE', { timeZone: 'UTC', weekday: 'short', day: '2-digit', month: '2-digit' });
const fmtDatum = new Intl.DateTimeFormat('de-DE', { timeZone: 'UTC', day: 'numeric', month: 'long' });
const fmtWT = new Intl.DateTimeFormat('de-DE', { timeZone: 'UTC', weekday: 'long' });
const zuMin = s => { const [h, m] = s.split(':'); return +h * 60 + +m; };
const uhr = m => { m = ((m % 1440) + 1440) % 1440; return 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.' : ''}`;
// Augenblick aus Berliner Wanduhr (für die Uhrzeit des Besuchers)
function augenblick(datum, min) {
const [y, mo, d] = datum.split('-').map(Number);
const ziel = Date.UTC(y, mo - 1, d) + min * 60000; // Wanduhr, als wäre sie UTC
let t = ziel;
for (let i = 0; i < 2; i++) {
const o = ortszeit(new Date(t)), [oy, om, od] = o.datum.split('-').map(Number);
t += ziel - (Date.UTC(oy, om - 1, od) + o.min * 60000);
}
return new Date(t);
}
/* Variante Termin: Telefonzeiten einer Kanzlei; Variante Auslastung: Öffnungszeiten einer Kunsthalle */
const DATEN = {
praxis: { W: WOCHE, S: SONDER },
termin: {
W: { 1: [['08:30', '12:30'], ['13:30', '17:00']], 2: [['08:30', '12:30'], ['13:30', '17:00']], 3: [['08:30', '12:30'], ['13:30', '17:00']], 4: [['08:30', '12:30'], ['13:30', '17:00']], 5: [['08:30', '13:00']], 6: [], 0: [] },
S: [
{ von: '2026-11-25', grund: 'Teamtag: Telefon erst ab 13.30 Uhr', zeiten: [['13:30', '17:00']] },
{ von: '2026-12-24', bis: '2026-12-27', grund: 'Weihnachten', zeiten: [] },
{ von: '2026-12-28', bis: '2026-12-31', grund: 'Zwischen den Jahren: nur vormittags', zeiten: [['09:00', '12:00']] },
{ von: '2027-01-01', grund: 'Neujahr', zeiten: [] },
],
},
auslastung: {
W: { 1: [], 2: [['10:00', '18:00']], 3: [['10:00', '18:00']], 4: [['10:00', '21:00']], 5: [['10:00', '18:00']], 6: [['10:00', '18:00']], 0: [['10:00', '18:00']] },
S: [
{ von: '2026-11-12', grund: 'Lange Nacht der Museen', zeiten: [['10:00', '24:00']] },
{ von: '2026-12-24', grund: 'Heiligabend', zeiten: [] },
{ von: '2026-12-31', grund: 'Silvester', zeiten: [['10:00', '14:00']] },
{ von: '2027-01-01', grund: 'Neujahr', zeiten: [['13:00', '18:00']] },
],
},
};
/* Variante Hinweisleiste: dieselbe Liste SONDER, Einträge mit „leiste“ erscheinen zusätzlich als Leiste über dem Kopf.
In der Demo liegen Urlaub und Samstagssprechstunde relativ zu heute, damit immer eine Meldung aktiv ist. */
const MO = (() => { let d = ortszeit(new Date()).datum; while (wochentag(d) !== 1) d = plusTage(d, 1); return d; })();
const URLAUB = { von: plusTage(MO, 7), bis: plusTage(MO, 11) }, SAMSTAG = plusTage(MO, 19);
DATEN.hinweis = {
W: WOCHE,
S: [
{ id: 'urlaub-' + URLAUB.von, von: URLAUB.von, bis: URLAUB.bis, grund: 'Praxisurlaub · Vertretung: Praxis Hagelloch, 07071 66 31 80', zeiten: [],
leiste: { ab: plusTage(URLAUB.von, -21), art: 'zu', titel: 'Praxisurlaub', vertretung: ['Praxis Hagelloch', '07071 66 31 80', 'tel:+497071663180'] } },
{ id: 'samstag-' + SAMSTAG, von: SAMSTAG, grund: 'Samstagssprechstunde nach dem Urlaub', zeiten: [['09:00', '12:00']],
leiste: { ab: plusTage(URLAUB.bis, 1), art: 'info', titel: 'Samstagssprechstunde', text: 'Ohne Termin, für alles, was im Urlaub liegen geblieben ist.' } },
...SONDER.filter(x => (x.bis || x.von) < URLAUB.von || x.von > SAMSTAG || (x.von > URLAUB.bis && x.von < SAMSTAG)),
].sort((a, b) => a.von < b.von ? -1 : 1),
};
let D = DATEN.praxis;
function sonderFuer(datum) { return D.S.find(s => datum >= s.von && datum <= (s.bis || s.von)); }
function zeitenFuer(datum) { const s = sonderFuer(datum); return s ? s.zeiten : D.W[wochentag(datum)]; }
// Absolute Intervalle in Minuten ab heute 0 Uhr (gestern bis in drei Wochen)
function intervalle(heute) {
const liste = [];
for (let k = -1; k <= 21; k++) {
const datum = plusTage(heute, k);
zeitenFuer(datum).forEach(([a, b]) => {
const s = zuMin(a), e = zuMin(b);
liste.push({ von: k * 1440 + s, bis: k * 1440 + (e <= s ? e + 1440 : e), tag: k, datum });
});
}
return liste.sort((x, y) => x.von - y.von);
}
/* ---------- Zustand berechnen ---------- */
function zustand(jetzt) {
const iv = intervalle(jetzt.datum), m = jetzt.min;
const heuteSonder = sonderFuer(jetzt.datum);
const offen = iv.find(i => i.von <= m && m < i.bis);
const naechstes = iv.find(i => i.von > m);
const vorher = [...iv].reverse().find(i => i.bis <= m);
const wann = i => {
if (!i) return 'demnächst wieder';
if (i.tag === 0) return `heute um ${uhr(i.von)} Uhr`;
if (i.tag === 1) return `morgen um ${uhr(i.von)} Uhr`;
if (i.tag < 7) return `${fmtWT.format(tagDatum(i.datum))} um ${uhr(i.von - i.tag * 1440)} Uhr`;
return `am ${fmtDatum.format(tagDatum(i.datum))} um ${uhr(i.von - i.tag * 1440)} Uhr`;
};
if (offen) {
const rest = offen.bis - m;
const pauseDanach = naechstes && naechstes.tag === offen.tag && naechstes.von - offen.bis < 240;
const bis = uhr(offen.bis);
let text;
if (rest <= 60) text = pauseDanach ? `Schließt in <b>${dauer(rest)}</b> zur Mittagspause. Ab ${uhr(naechstes.von)} Uhr sind wir wieder da.` : `Schließt in <b>${dauer(rest)}</b>, um ${bis} Uhr. Danach erreichen Sie den Bereitschaftsdienst.`;
else text = pauseDanach ? `Bis ${bis} Uhr, dann Mittagspause bis ${uhr(naechstes.von)} Uhr.` : `Heute bis ${bis} Uhr.`;
return { art: rest <= 30 ? 'bald' : 'offen', kopf: rest <= 30 ? 'Schließt bald' : 'Jetzt geöffnet', text, pille: rest <= 60 ? `Geöffnet · schließt in ${dauer(rest)}` : `Geöffnet · bis ${bis} Uhr`, kurz: rest <= 60 ? `Schließt in ${dauer(rest)}` : `Offen bis ${bis}`, sonder: heuteSonder };
}
if (vorher && vorher.tag === 0 && naechstes && naechstes.tag === 0) {
const bis = naechstes.von - m;
return { art: 'pause', kopf: 'Mittagspause', text: `Ab <b>${uhr(naechstes.von)} Uhr</b> sind wir wieder da${bis <= 60 ? `, in ${dauer(bis)}` : ''}. Rezepte und Überweisungen können Sie jederzeit online bestellen.`, pille: `Mittagspause · ab ${uhr(naechstes.von)} Uhr`, kurz: `Pause bis ${uhr(naechstes.von)}`, sonder: heuteSonder };
}
if (naechstes && naechstes.tag === 0 && naechstes.von - m <= 60) {
return { art: 'zu', kopf: 'Öffnet gleich', text: `Wir öffnen in <b>${dauer(naechstes.von - m)}</b>, um ${uhr(naechstes.von)} Uhr.`, pille: `Öffnet in ${dauer(naechstes.von - m)}`, kurz: `Öffnet in ${dauer(naechstes.von - m)}`, sonder: heuteSonder };
}
const geschlossenHeute = heuteSonder && !heuteSonder.zeiten.length;
return {
art: 'zu', kopf: geschlossenHeute ? 'Heute geschlossen' : 'Geschlossen',
text: `Wir öffnen ${wann(naechstes)}.`, pille: `Geschlossen · öffnet ${wann(naechstes).replace(' um ', ' ').replace(' Uhr', '')}`,
kurz: `Zu · ${naechstes && naechstes.tag < 7 ? (naechstes.tag === 1 ? 'morgen' : naechstes.tag === 0 ? 'heute' : fmtKurz.format(tagDatum(naechstes.datum)).slice(0, 2)) + ' ' + uhr(naechstes.von - naechstes.tag * 1440) : naechstes ? 'ab ' + fmtKurz.format(tagDatum(naechstes.datum)).replace(/^\S+ /, '') : 'siehe Zeiten'}`,
sonder: heuteSonder,
};
}
/* ---------- Darstellung ---------- */
const $ = s => document.querySelector(s), $$ = s => [...document.querySelectorAll(s)];
const box = $('[data-box]'), kopf = $('[data-kopf]'), text = $('[data-text]'), ansage = $('[data-ansage]');
const spur = $('[data-spur]'), jetztStrich = $('[data-jetzt]'), jetztZeit = $('[data-jetzt-zeit]');
const ACHSE = [7 * 60, 20 * 60];
let letzteArt = null, besucherZone = '';
function sonderListe(heute, ul = $('[data-sonder-liste]')) {
ul.replaceChildren(...D.S.map(s => {
const li = document.createElement('li');
const ende = s.bis || s.von;
li.className = ende < heute ? 'vorbei' : (s.von <= heute ? 'jetzt' : '');
const datum = s.bis ? `${fmtKurz.format(tagDatum(s.von)).replace(/^\w+\.?, /, '')}–${fmtKurz.format(tagDatum(s.bis)).replace(/^\w+\.?, /, '')}` : fmtKurz.format(tagDatum(s.von));
const zeiten = s.zeiten.length ? s.zeiten.map(([a, b]) => `${uhr(zuMin(a))}–${zuMin(b) === 1440 ? '24.00' : uhr(zuMin(b))} Uhr`).join(', ') : 'geschlossen';
li.innerHTML = `<time datetime="${s.von}">${datum}</time><span><b>${zeiten}</b> · ${s.grund}</span>`;
return li;
}));
}
function tagesleiste(jetzt, z) {
const [a, b] = ACHSE, breite = b - a;
spur.querySelectorAll('.tag__block, .tag__leer').forEach(n => n.remove());
if (!zeitenFuer(jetzt.datum).length) { const l = document.createElement('div'); l.className = 'tag__leer'; l.textContent = 'Heute keine Sprechzeiten'; spur.prepend(l); }
zeitenFuer(jetzt.datum).forEach(([von, bis]) => {
const s = zuMin(von), e = zuMin(bis) <= s ? 1440 : zuMin(bis);
const n = document.createElement('div');
n.className = 'tag__block' + (jetzt.min >= s && jetzt.min < e ? ' ist-jetzt' : '');
n.style.left = (Math.max(0, s - a) / breite * 100) + '%';
n.style.width = ((Math.min(b, e) - Math.max(a, s)) / breite * 100) + '%';
n.textContent = `${uhr(s)}–${uhr(e)}`;
spur.prepend(n);
});
const drin = jetzt.min >= a && jetzt.min <= b;
jetztStrich.hidden = !drin;
if (drin) { jetztStrich.style.left = ((jetzt.min - a) / breite * 100) + '%'; jetztZeit.textContent = uhr(jetzt.min); }
}
function wochentabelle(jetzt) {
const s = sonderFuer(jetzt.datum);
$$('[data-wt]').forEach(tr => {
const heute = +tr.dataset.wt === jetzt.wt;
tr.classList.toggle('heute', heute);
tr.querySelectorAll('.geaendert').forEach(n => n.remove());
if (heute && s) {
const g = document.createElement('span');
g.className = 'geaendert';
g.textContent = s.zeiten.length ? `heute ${s.zeiten.map(([a, b]) => uhr(zuMin(a)) + '–' + uhr(zuMin(b))).join(', ')} · ${s.grund.split(' · ')[0]}` : `heute zu · ${s.grund.split(' · ')[0]}`;
tr.lastElementChild.append(g);
}
});
}
/* ---------- Termin und Auslastung: eigener Status, gleiche Zeitlogik ---------- */
const uhrK = m => { m = ((m % 1440) + 1440) % 1440; return m % 60 ? uhr(m) : String(Math.floor(m / 60) || 24); };
function wannK(i) {
if (!i) return 'bald wieder';
const zeit = uhrK(i.von - i.tag * 1440) + ' Uhr';
if (i.tag === 0) return `heute um ${zeit}`;
if (i.tag === 1) return `morgen um ${zeit}`;
if (i.tag < 7) return `${fmtWT.format(tagDatum(i.datum))} um ${zeit}`;
return `am ${fmtDatum.format(tagDatum(i.datum))} um ${zeit}`;
}
function lage(jetzt) {
const iv = intervalle(jetzt.datum), m = jetzt.min;
const offen = iv.find(i => i.von <= m && m < i.bis), naechstes = iv.find(i => i.von > m);
const vorher = [...iv].reverse().find(i => i.bis <= m);
return { offen, naechstes, pause: !offen && vorher && vorher.tag === 0 && naechstes && naechstes.tag === 0, sonder: sonderFuer(jetzt.datum), m };
}
const grundBadge = l => l.sonder ? ` <span class="status__grund">${l.sonder.grund.split(':')[0]}</span>` : '';
let letzteT = null, letzteA = null;
function heuteZeile(attr, wt) { $$(`[${attr}]`).forEach(tr => tr.classList.toggle('heute', tr.getAttribute(attr).split(' ').includes(String(wt)))); }
function zeichneTermin(jetzt) {
const l = lage(jetzt);
let art, kopfT, textT;
if (l.offen) {
const rest = l.offen.bis - l.m, pauseDanach = l.naechstes && l.naechstes.tag === l.offen.tag && l.naechstes.von - l.offen.bis < 180;
art = rest <= 15 ? 'bald' : 'offen';
kopfT = `Telefon erreichbar bis ${uhrK(l.offen.bis)} Uhr`;
textT = rest <= 15 ? `Noch <b>${dauer(rest)}</b>. Danach nehmen wir Ihre Nachricht auf und rufen zurück.`
: pauseDanach ? `Danach Mittagspause bis ${uhrK(l.naechstes.von)} Uhr. Besprechungen nur mit Termin.`
: 'Frau Lüders und Herr Aydın nehmen Ihren Anruf an. Besprechungen nur mit Termin.';
} else if (l.pause) {
art = 'pause';
kopfT = `Telefon ab ${uhrK(l.naechstes.von)} Uhr wieder besetzt`;
textT = 'Mittagspause. Hinterlassen Sie eine Nachricht oder vereinbaren Sie einen Rückruf.';
} else {
art = 'zu';
kopfT = 'Telefon gerade nicht besetzt';
textT = `Wieder erreichbar ${wannK(l.naechstes)}. Ihre Nachricht beantworten wir am nächsten Werktag.`;
}
$$('.t-status[data-zustand]').forEach(n => n.dataset.zustand = art);
$('[data-t-kopf]').textContent = kopfT;
$('[data-t-text]').innerHTML = textT + grundBadge(l);
$('[data-t-heute]').textContent = `Telefon · ${fmtTag.format(tagDatum(jetzt.datum))}`;
heuteZeile('data-twt', jetzt.wt);
if (letzteT && letzteT !== art) $('[data-t-ansage]').textContent = kopfT + '.';
letzteT = art;
}
// Beispielwerte: übliche Auslastung je Stunde (Anteil der Kapazität). Echt: Mittel der letzten acht Wochen aus der Einlasszählung.
const KAP = 350;
const TYP = {
werktag: { 10: .08, 11: .18, 12: .26, 13: .3, 14: .36, 15: .42, 16: .34, 17: .18 },
donnerstag: { 10: .08, 11: .16, 12: .22, 13: .26, 14: .3, 15: .34, 16: .3, 17: .26, 18: .3, 19: .2, 20: .1 },
wochenende: { 10: .2, 11: .4, 12: .54, 13: .62, 14: .78, 15: .86, 16: .7, 17: .38 },
lang: { 10: .1, 11: .2, 12: .26, 13: .3, 14: .36, 15: .42, 16: .4, 17: .46, 18: .62, 19: .8, 20: .9, 21: .84, 22: .66, 23: .4 },
};
const typFuer = (datum) => { const s = sonderFuer(datum); if (s && s.von === '2026-11-12') return TYP.lang; const wt = wochentag(datum); return wt === 4 ? TYP.donnerstag : (wt === 0 || wt === 6) ? TYP.wochenende : TYP.werktag; };
function typAnteil(datum, m) {
const t = typFuer(datum), x = m / 60 - .5, h = Math.floor(x), f = x - h;
const a = t[h] ?? t[h + 1] ?? 0, b = t[h + 1] ?? t[h] ?? 0;
return a + (b - a) * f;
}
let zaehlung = true;
function zeichneAuslastung(jetzt) {
const l = lage(jetzt);
let art, kopfT, textT;
if (l.offen) {
const rest = l.offen.bis - l.m, schluss = uhrK(l.offen.bis), einlass = uhrK(l.offen.bis - 30);
art = rest <= 30 ? 'bald' : 'offen';
kopfT = rest <= 30 ? 'Letzter Einlass vorbei' : `Geöffnet bis ${schluss} Uhr`;
textT = rest <= 30 ? `Wir schließen um ${schluss} Uhr.` : `Letzter Einlass ${einlass} Uhr.`;
} else {
art = 'zu';
kopfT = l.sonder && !l.sonder.zeiten.length ? 'Heute geschlossen' : 'Geschlossen';
textT = `Wir öffnen ${wannK(l.naechstes)}.`;
}
$$('.a-karte[data-zustand]').forEach(n => n.dataset.zustand = art);
$('[data-a-kopf]').textContent = kopfT;
$('[data-a-text]').innerHTML = textT + grundBadge(l);
$('[data-a-heute]').textContent = `Heute · ${fmtTag.format(tagDatum(jetzt.datum))}`;
heuteZeile('data-awt', jetzt.wt);
sonderListe(jetzt.datum, $('[data-a-sonder]'));
// Auslastung jetzt: nur bei geöffnetem Haus und frischem Zählwert, sonst entfällt der Block ganz
const block = $('[data-last]');
block.hidden = !(l.offen && zaehlung);
if (!block.hidden) {
const typ = typAnteil(jetzt.datum, l.m), wt = wochentag(jetzt.datum);
const anteil = Math.min(1, typ * (wt === 0 || wt === 6 ? 1.08 : .86));
const n = Math.round(anteil * KAP);
const wort = anteil < .35 ? 'ruhig' : anteil < .65 ? 'gut besucht' : 'voll';
$('[data-a-wort]').textContent = wort[0].toUpperCase() + wort.slice(1);
$('[data-a-zahl]').textContent = `${n} von ${KAP} Personen im Haus`;
const meter = $('[data-a-meter]');
meter.style.setProperty('--a', anteil.toFixed(3)); meter.style.setProperty('--t', typ.toFixed(3));
meter.setAttribute('aria-valuenow', n); meter.setAttribute('aria-valuetext', `${wort}, ${n} von ${KAP} Personen`);
const diff = anteil - typ;
$('[data-a-vergleich]').textContent = Math.abs(diff) < .03 ? 'So viel wie üblich um diese Zeit.'
: diff < 0 ? 'Gerade weniger los als üblich um diese Zeit.' : 'Gerade mehr los als üblich um diese Zeit.' + (anteil >= .65 ? ' Am Einlass kann es kurz dauern.' : '');
const stand = l.m - l.m % 5;
$('[data-a-stand]').textContent = `Stand ${uhr(stand)} Uhr · Einlasszählung, alle 5 Min.`;
}
// Übliche Auslastung des Tages (oder des nächsten Öffnungstags)
let tag = jetzt.datum, k = 0;
while (!zeitenFuer(tag).length && k < 8) { tag = plusTage(tag, 1); k++; }
const t = typFuer(tag), stunden = Object.keys(t).map(Number).filter(h => zeitenFuer(tag).some(([a, b]) => h * 60 >= zuMin(a) && h * 60 < (zuMin(b) || 1440)));
const sae = $('[data-saeulen]'), achse = $('[data-achse]');
if (sae.dataset.tag !== tag + stunden.length) {
sae.dataset.tag = tag + stunden.length;
sae.replaceChildren(...stunden.map(h => { const d = document.createElement('div'); d.dataset.h = h; d.style.setProperty('--h', t[h]); return d; }));
achse.replaceChildren(...stunden.map(h => { const sp = document.createElement('span'); sp.textContent = h; return sp; }));
}
const h = Math.floor(l.m / 60);
sae.querySelectorAll('div').forEach(d => {
const an = tag === jetzt.datum && +d.dataset.h === h && !!l.offen;
d.classList.toggle('jetzt', an);
d.replaceChildren();
if (an && zaehlung) { const b = document.createElement('b'); const live = Math.min(1, typAnteil(jetzt.datum, l.m) * (wochentag(jetzt.datum) % 6 === 0 ? 1.08 : .86)); d.style.setProperty('--l', live.toFixed(3)); b.style.bottom = `calc(${(live / +d.style.getPropertyValue('--h')).toFixed(3)} * 100% - 1px)`; d.append(b); }
});
const ruhig = stunden.filter(x => t[x] < .25);
$('[data-verlauf-text]').textContent = `Üblich ${tag === jetzt.datum ? 'heute' : 'am ' + fmtWT.format(tagDatum(tag))} (Mittel der letzten acht Wochen)` + (ruhig.length ? `. Ruhig: ${ruhig.map(x => x + ' Uhr').join(', ')}.` : '.');
if (letzteA && letzteA !== art) $('[data-a-ansage]').textContent = kopfT + '.';
letzteA = art;
}
function zeichnen() {
const jetzt = sim ? { ...sim, wt: wochentag(sim.datum) } : ortszeit(new Date());
if (variante === 'termin' || variante === 'auslastung') {
(variante === 'termin' ? zeichneTermin : zeichneAuslastung)(jetzt);
$('[data-sim-anzeige]').textContent = uhr(jetzt.min);
if (!sim) { $('[data-sim-zeit]').value = jetzt.min; setzeTagAuswahl(jetzt.datum); }
return;
}
if (variante === 'hinweisleiste') hinweisZeichnen(jetzt);
const z = zustand(jetzt);
$$('[data-zustand]').forEach(n => n.dataset.zustand = z.art);
kopf.textContent = z.kopf;
text.innerHTML = z.text + (z.sonder ? ` <span class="status__grund">${z.sonder.grund.split(' · ')[0]}</span>` : '');
$$('[data-pille-text]').forEach(n => n.textContent = z.pille);
$$('[data-pille-kurz]').forEach(n => n.textContent = z.kurz);
$('[data-pop-text]').innerHTML = `<b>${z.kopf}.</b> ${z.text}`;
$('[data-heute]').textContent = `Heute · ${fmtTag.format(tagDatum(jetzt.datum))}`;
tagesleiste(jetzt, z);
wochentabelle(jetzt);
sonderListe(jetzt.datum);
// Zeitzone des Besuchers: Status bleibt Ortszeit, Hinweis nur, wenn die Zonen abweichen
const zone = besucherZone || Intl.DateTimeFormat().resolvedOptions().timeZone;
const zz = $('[data-zeitzone]');
if (zone !== ZONE) {
const t = new Intl.DateTimeFormat('de-DE', { timeZone: zone, hour: 'numeric', minute: '2-digit', weekday: 'short' }).format(augenblick(jetzt.datum, jetzt.min));
const stadt = zone.split('/').pop().replace('_', ' ').replace('New York', 'New York').replace('Tokyo', 'Tokio');
zz.textContent = `Alle Zeiten in deutscher Zeit (Tübingen). Bei Ihnen in ${stadt} ist es jetzt ${t.replace(':', '.')} Uhr.`;
} else zz.textContent = 'Alle Zeiten in deutscher Zeit (Tübingen).';
// Screenreader: nur Zustandswechsel ansagen, nicht jede Minute
if (letzteArt && letzteArt !== z.art) ansage.textContent = `${z.kopf}. ${text.textContent}`;
letzteArt = z.art;
$('[data-sim-anzeige]').textContent = uhr(jetzt.min);
if (!sim) { $('[data-sim-zeit]').value = jetzt.min; setzeTagAuswahl(jetzt.datum); }
}
/* ---------- Hinweisleiste: aktive Meldung aus SONDER, geschlossene id in localStorage ---------- */
const leiste = $('[data-hinweis]'), SCHLUESSEL = 'praxis-hinweis-zu';
let zuId = null;
try { zuId = localStorage.getItem(SCHLUESSEL); } catch (e) {}
const tagOhneMonat = d => fmtDatum.format(tagDatum(d)).split(' ')[0];
const tagLang = d => `${fmtWT.format(tagDatum(d))}, ${fmtDatum.format(tagDatum(d))}`;
function letzterWerktagVor(d) { let x = plusTage(d, -1); while (!WOCHE[wochentag(x)].length) x = plusTage(x, -1); return x; }
function ersterOffenNach(d) { let x = plusTage(d, 1), k = 0; while (!zeitenFuer(x).length && k++ < 21) x = plusTage(x, 1); return x; }
function aktiveMeldung(datum) {
return D.S.filter(x => x.leiste && datum >= x.leiste.ab && datum <= (x.bis || x.von))
.sort((a, b) => (a.leiste.art === 'zu' ? 0 : 1) - (b.leiste.art === 'zu' ? 0 : 1))[0] || null;
}
function hinweisZeichnen(jetzt) {
const m = aktiveMeldung(jetzt.datum);
leiste.classList.remove('geht');
if (!m || m.id === zuId) { leiste.hidden = true; return; }
const L = m.leiste, ende = m.bis || m.von, drin = jetzt.datum >= m.von;
let titel, text;
const vert = L.vertretung ? `Vertretung: ${L.vertretung[0]}, <a href="${L.vertretung[2]}">${L.vertretung[1]}</a>.` : '';
if (L.art === 'zu') {
const zurueck = ersterOffenNach(ende), ab = zeitenFuer(zurueck)[0][0].replace(':', '.');
if (!drin) {
const monatGleich = m.von.slice(5, 7) === ende.slice(5, 7);
titel = `${L.titel} vom ${monatGleich ? tagOhneMonat(m.von) : fmtDatum.format(tagDatum(m.von))} bis ${fmtDatum.format(tagDatum(ende))}.`;
text = `${vert} Rezepte bitte bis ${tagLang(letzterWerktagVor(m.von))} bestellen.`;
} else {
titel = `Heute geschlossen: ${L.titel} bis ${tagLang(ende)}.`;
text = `Ab ${tagLang(zurueck)}, ${ab} Uhr sind wir wieder da. ${vert}`;
}
} else {
const zeiten = m.zeiten.map(([a, b]) => `${uhrK(zuMin(a))} bis ${uhrK(zuMin(b))} Uhr`).join(' und ');
titel = drin ? `Heute ${L.titel}, ${zeiten}.` : `${tagLang(m.von)}: ${L.titel}, ${zeiten}.`;
text = L.text || '';
}
leiste.dataset.id = m.id; leiste.dataset.art = L.art;
$('[data-hinweis-titel]').textContent = titel;
$('[data-hinweis-text]').innerHTML = text;
leiste.hidden = false;
}
function hinweisSchliessen(merken, fokus) {
zuId = leiste.dataset.id;
if (merken) { try { localStorage.setItem(SCHLUESSEL, zuId); } catch (e) {} }
const fertig = () => {
leiste.hidden = true; leiste.classList.remove('geht');
if (fokus) { const ziel = $$('.kopf a').find(a => a.offsetParent); if (ziel) ziel.focus(); }
};
if (reduziert()) fertig(); else { leiste.classList.add('geht'); setTimeout(fertig, 150); }
}
$('[data-hinweis-zu]').addEventListener('click', () => hinweisSchliessen(!html.classList.contains('vorfuehrung'), true));
$('[data-hinweis-neu]').addEventListener('click', () => { zuId = null; try { localStorage.removeItem(SCHLUESSEL); } catch (e) {} zeichnen(); });
/* ---------- Simulierte Uhr ---------- */
let sim = null, takt = null;
const tagWahl = $('[data-sim-tag]'), zeitWahl = $('[data-sim-zeit]'), liveKnopf = $('[data-live-knopf]');
function tagOptionen() {
const heute = ortszeit(new Date()).datum;
const tage = new Set();
for (let k = 0; k < 8; k++) tage.add(plusTage(heute, k));
D.S.forEach(s => tage.add(s.von));
tagWahl.replaceChildren(...[...tage].sort().map(d => {
const o = document.createElement('option');
const s = sonderFuer(d);
o.value = d; o.textContent = fmtKurz.format(tagDatum(d)) + (s ? ' · ' + s.grund.split(' · ')[0] : '');
return o;
}));
}
function setzeTagAuswahl(d) { if (![...tagWahl.options].some(o => o.value === d)) tagOptionen(); tagWahl.value = d; }
function simSetzen(datum, min) {
sim = { datum, min };
liveKnopf.setAttribute('aria-pressed', 'false');
tagWahl.value = datum; zeitWahl.value = min;
zeichnen();
}
tagWahl.addEventListener('change', () => simSetzen(tagWahl.value, +zeitWahl.value));
zeitWahl.addEventListener('input', () => simSetzen(tagWahl.value, +zeitWahl.value));
liveKnopf.addEventListener('click', () => { sim = null; liveKnopf.setAttribute('aria-pressed', 'true'); zeichnen(); });
$('[data-besucher]').addEventListener('change', e => { besucherZone = e.target.value; zeichnen(); });
// Echtzeit: zur nächsten vollen Minute neu rechnen, und wenn der Tab wieder sichtbar wird
function ticken() {
clearTimeout(takt);
if (!sim) zeichnen();
takt = setTimeout(ticken, 60000 - Date.now() % 60000 + 50);
}
document.addEventListener('visibilitychange', () => { if (!document.hidden) ticken(); });
/* ---------- Variante und Ohne-JS-Ansicht ---------- */
const VARIANTEN = ['karte', 'kompakt', 'termin', 'auslastung', 'hinweisleiste'];
const q = new URLSearchParams(location.search);
let variante = VARIANTEN.includes(q.get('v')) ? q.get('v') : 'karte';
function setzeVariante(v, schreiben) {
variante = v; html.dataset.v = v;
D = DATEN[v === 'termin' || v === 'auslastung' ? v : v === 'hinweisleiste' ? 'hinweis' : 'praxis'];
letzteArt = letzteT = letzteA = null;
tagOptionen(); if (sim) setzeTagAuswahl(sim.datum);
if (schreiben) zeichnen();
$$('[data-variante]').forEach(b => b.setAttribute('aria-pressed', String(b.dataset.variante === v)));
if (pop.matches(':popover-open')) pop.hidePopover();
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)));
$('[data-zaehlung]').addEventListener('click', e => { zaehlung = !zaehlung; e.currentTarget.setAttribute('aria-pressed', String(zaehlung)); e.currentTarget.textContent = zaehlung ? 'Zählung an' : 'Zählung aus'; zeichnen(); });
$('[data-ohne-js]').addEventListener('click', e => { const an = !html.classList.contains('ohne-js'); html.classList.toggle('ohne-js', an); e.currentTarget.setAttribute('aria-pressed', String(an)); });
/* ---------- Popover der Kopfzeile: Woche ---------- */
const pop = $('#woche-pop');
$('[data-pop-woche]').append($('[data-woche]').cloneNode(true));
pop.addEventListener('toggle', e => $$('[data-pille]').forEach(b => b.setAttribute('aria-expanded', String(e.newState === 'open'))));
// Die untere Leiste auf dem Telefon nutzt dasselbe Popover: dort von unten statt am Kopf
$$('[data-pille]').forEach(b => b.addEventListener('click', () => { pop.style.positionAnchor = b.closest('.unten') ? 'none' : ''; pop.classList.toggle('von-unten', !!b.closest('.unten')); }));
const st = document.createElement('style');
st.textContent = '.woche-pop.von-unten{position-anchor:none;top:auto;left:12px;right:12px;bottom:76px;width:auto;margin:0}';
document.head.append(st);
tagOptionen();
setzeVariante(variante, false);
ticken();
/* ---------- Vorführung: die Uhr läuft durch typische Zustände ---------- */
// [Tag relativ zum nächsten Montag, von Minute, bis Minute, Dauer ms]
const PLAENE = {};
PLAENE.karte = PLAENE.kompakt = [
[0, 7 * 60 + 20, 8 * 60 + 5, 1500], // öffnet gleich → geöffnet
[0, 11 * 60 + 15, 12 * 60 + 5, 2600], // schließt in 45 … 1 Min. → Mittagspause
[0, 13 * 60 + 30, 14 * 60 + 2, 1300], // Mittagspause → geöffnet
[0, 17 * 60 + 35, 18 * 60 + 10, 1900], // schließt bald → geschlossen, öffnet morgen
[2, 11 * 60 + 50, 12 * 60 + 20, 1500], // Mittwoch: nachmittags zu
[4, 12 * 60 + 40, 13 * 60 + 10, 1300], // Freitag 13 Uhr → öffnet Montag
['2026-11-14', 9 * 60 + 30, 9 * 60 + 40, 1200], // Samstag: Impfaktion
['2026-11-01', 10 * 60, 10 * 60 + 5, 1500], // Allerheiligen
];
PLAENE.termin = [
[0, 8 * 60 + 10, 8 * 60 + 40, 1500], // nicht besetzt → erreichbar bis 12.30
[0, 12 * 60 + 5, 12 * 60 + 50, 2200], // noch 15 Min. → Mittagspause, ab 13.30 wieder besetzt
[0, 16 * 60 + 30, 17 * 60 + 10, 2200], // erreichbar bis 17 Uhr → nicht besetzt, morgen 8.30
[4, 12 * 60 + 40, 13 * 60 + 10, 1600], // Freitag 13 Uhr → Montag
['2026-11-25', 9 * 60, 9 * 60 + 10, 1300], // Teamtag
['2026-12-29', 10 * 60, 10 * 60 + 10, 1300], // zwischen den Jahren
];
PLAENE.auslastung = [
[1, 9 * 60 + 40, 11 * 60 + 30, 2200], // öffnet um 10, wird voller
[1, 13 * 60, 16 * 60 + 30, 2600], // Nachmittag, Spitze um 15 Uhr
[1, 14 * 60, 14 * 60 + 30, 1600, true], // Zählung fällt aus → Block entfällt
[1, 17 * 60 + 15, 18 * 60 + 5, 1600], // letzter Einlass → geschlossen
[5, 12 * 60, 15 * 60, 2200], // Samstag: voll
[0, 11 * 60, 11 * 60 + 5, 1200], // Montag geschlossen
];
// Hinweisleiste: [Tag relativ zum Montag dieser Rechnung, von, bis, Dauer, –, Aktion beim Eintritt]
PLAENE.hinweisleiste = [
['heute', 10 * 60, 10 * 60 + 20, 2200, false, 'neu'], // Urlaub angekündigt: Leiste über dem Kopf
[7, 9 * 60, 9 * 60 + 20, 2000], // im Urlaub: „Heute geschlossen … bis Freitag“
[7, 9 * 60 + 20, 9 * 60 + 25, 1300, false, 'zu'], // Besucher schließt die Leiste
[9, 11 * 60, 11 * 60 + 10, 1500], // gleiche Meldung: bleibt zu
[12, 10 * 60, 10 * 60 + 10, 2200], // neue Meldung (Samstagssprechstunde): erscheint wieder
[19, 9 * 60 + 30, 9 * 60 + 40, 1800], // am Samstag selbst
[20, 10 * 60, 10 * 60 + 5, 1400], // vorbei: keine Leiste
];
const gesamt = pl => pl.reduce((s, p) => s + p[3] + 700, 0);
let letztesSeg = -1;
let laeuft = null, t0 = 0, angehalten = 0;
const naechsterMontag = () => { const h = ortszeit(new Date()).datum; let d = h; while (wochentag(d) !== 1) d = plusTage(d, 1); return d; };
function schritt(t) {
const PLAN = PLAENE[variante];
let z = (t - t0) % gesamt(PLAN), mo = variante === 'hinweisleiste' ? MO : naechsterMontag();
for (const [k, [tag, a, b, d, ohne, aktion]] of PLAN.entries()) {
const datum = tag === 'heute' ? ortszeit(new Date()).datum : typeof tag === 'string' ? tag : plusTage(mo, tag);
if (z < d + 700) {
if (k !== letztesSeg) {
letztesSeg = k;
if (aktion === 'neu') zuId = null;
if (aktion === 'zu') { const zk = $('[data-hinweis-zu]'); zk.classList.add('druck'); setTimeout(() => { zk.classList.remove('druck'); if (laeuft && !leiste.hidden) hinweisSchliessen(false, false); }, 450); }
}
const p = Math.min(1, z / d);
const min = Math.round(a + (b - a) * p);
const zl = !ohne, zk = $('[data-zaehlung]');
if (zaehlung !== zl) { zaehlung = zl; zk.setAttribute('aria-pressed', String(zl)); zk.textContent = zl ? 'Zählung an' : 'Zählung aus'; sim = null; }
if (!sim || sim.datum !== datum || sim.min !== min) simSetzen(datum, min);
break;
}
z -= d + 700;
}
laeuft = requestAnimationFrame(schritt);
}
function vorfuehren() { pause(); html.classList.add('vorfuehrung'); t0 = performance.now(); letztesSeg = -1; laeuft = requestAnimationFrame(schritt); }
function pause() { if (laeuft) angehalten = performance.now() - t0; cancelAnimationFrame(laeuft); laeuft = null; }
function weiter() { if (laeuft || !html.classList.contains('vorfuehrung')) return; t0 = performance.now() - angehalten; laeuft = requestAnimationFrame(schritt); }
function neustart() { pause(); html.classList.remove('vorfuehrung'); sim = null; try { zuId = localStorage.getItem(SCHLUESSEL); } catch (e) { zuId = null; } zaehlung = true; const zk = $('[data-zaehlung]'); zk.setAttribute('aria-pressed', 'true'); zk.textContent = 'Zählung an'; liveKnopf.setAttribute('aria-pressed', 'true'); zeichnen(); }
})();
</script>
</body>
</html>