Status- und Störungsseite
Ein Satz sagt, ob alles läuft; darunter die laufende Störung mit Beginn, letztem Update und Zeitleiste, die Dienste mit 90-Tage-Verlauf, geplante Wartung und ein E-Mail-Abo, für Systemhaus, Stadtwerk oder als Hinweisleiste der Praxis.
IT-Systemhaus, Managed Services, SaaS: Status der betreuten Dienste mit Zeitleiste, Wartung und Abo.
Eine eigene Seite, die in einem Satz sagt, ob alles läuft. Darunter die Dienste einzeln (Telefonanlage, E-Mail, Fernwartung, Datensicherung oder Strom, Gas, Wasser), jede mit Wort und Symbol. Eine laufende Störung steht ganz oben: Titel, betroffene Dienste, Beginn, letztes Update und die Updates als Zeitleiste mit Uhrzeit (untersucht, Ursache gefunden, beobachtet, behoben). Geplante Wartungen stehen getrennt. Ein Verlauf über 30 oder 90 Tage zeigt jeden Tag als Feld mit Text. Unten ein Abo per E-Mail als Platzhalter mit Bestätigungsmail. Fällt die Statusquelle selbst aus, sagt die Seite das ehrlich und zeigt die Hotline.
Bei einer Störung rufen alle gleichzeitig an. Eine Seite, die Beginn, Stand und nächstes Update nennt, beantwortet die drei Fragen vorab: Liegt es an mir? Weiß der Betrieb Bescheid? Wann geht es weiter? Das entlastet die Hotline und zeigt Verlässlichkeit gerade dann, wenn etwas nicht geht. Der Verlauf mit echten Tagen wirkt glaubwürdiger als ein Versprechen wie „99,9 %“.
Wann einsetzen
IT-Systemhaus und Managed Services (Status der betreuten Dienste), Stadtwerk und Versorger (Störungen nach Ortsteil, geplante Unterbrechungen), Praxis, Kanzlei oder Amt, wenn Telefon oder Online-Termin ausfällt (kurzer Hinweis mit Ausweichwegen). Auch für SaaS-Anbieter.
Wann nicht
Wenn niemand die Seite im Störfall pflegt: ein veralteter grüner Status schadet mehr als keine Seite. Nicht auf demselben Server wie der gestörte Dienst betreiben. Keine Verfügbarkeitszahlen oder Reaktionszeiten erfinden; die Werte kommen aus dem Monitoring bzw. Vertrag des Betriebs. Bei Gasgeruch, Stromunfall oder medizinischem Notfall nie auf die Seite verweisen, sondern Notrufnummern groß zeigen. Daten und Abo vom Kunden prüfen lassen.
Varianten
- Systemhaus: Dienste der Kunden mit 90-Tage-Verlauf, laufende Störung mit Zeitleiste, geplante Wartung, E-Mail-Abo je Dienst
- Versorger: Strom, Gas, Wasser, Fernwärme; Störungen nach Ortsteil mit Suche nach Straße oder PLZ, Notrufnummern bei Gasgeruch zuerst, geplante Unterbrechungen
- Praxis-Hinweis: kompakte Leiste auf der Kundenseite, wenn das Telefon gestört ist, mit Ausweichwegen (E-Mail, Online-Termin, Ausweichnummer, 116117 und 112)
Umsetzung
Der Status kommt als kleines JSON vom Server oder aus dem Monitoring (dienste, stoerungen mit updates, wartungen, verlauf je Tag, stand). Die Seite rendert daraus per textContent: Gesamtsatz, Dienstliste als <ul> mit Wort und Symbol, laufende Störung als <article> mit <time datetime> für Beginn und letztes Update, Updates als <ol> neueste zuerst. Relative Zeiten („vor 12 Minuten“) mit Intl.RelativeTimeFormat, absolute Zeit immer daneben. Der Verlauf ist ein <ol> mit einem Feld je Tag, jedes Feld hat einen Text („3. Nov.: 41 Minuten eingeschränkt“) und ist per Tastatur erreichbar; die Prozentzahl wird aus den Tagen berechnet, nicht behauptet. Neu laden alle 60 s mit fetch und If-None-Match, nur Änderungen werden in einer role=status-Zeile angesagt. Ist die Quelle nicht erreichbar, bleibt der letzte Stand mit Uhrzeit stehen und ein Hinweis mit Hotline erscheint. Das Abo ist ein Formular mit E-Mail, Auswahl der Dienste und Hinweis auf die Bestätigungsmail (Double-Opt-in); der Versand läuft über den Statusdienst des Betriebs. Seite als eigene Subdomain auf getrenntem Hosting.
Robust umsetzen
Telefon. Gesamtsatz und laufende Störung zuerst, Hotline als Knopf direkt darunter. Dienste als Liste ohne Tabelle. Der Verlauf zeigt auf dem Telefon 30 statt 90 Tage, Felder als Tippziel mindestens 24 px hoch mit Text darunter. Suche nach Straße mit inputmode und autocomplete.
Ohne Bewegung. Statusmeldungen erscheinen ohne Weg und ohne Überblendung. Es gibt keine Dauerbewegung.
Barrierefrei. Status nie nur über Farbe: Wort und Symbol (Haken, Dreieck, Kreuz, Schraubenschlüssel). Laufende Störung als Überschrift mit Zeitangaben in <time>. Änderungen beim Neuladen über eine role=status-Zeile, nicht über aria-live auf der ganzen Liste. Verlauf als Liste mit Text je Tag statt Grafik ohne Alternative. Abo-Formular mit Label, Fehler am Feld, Ergebnis in role=status. Kontrast der Statusfarben mindestens 4,5 : 1.
Code
<!DOCTYPE html>
<!--
Status- und Störungsseite
Fundus · Sektionen & Funktionen · https://cecodesign.de/lab/fundus/sektionen/stoerungs-statusseite/
Eigenständige Datei. Bilder liegen hier unter bilder/ – eigene einsetzen.
-->
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Status- und Störungsseite · 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: 'Host Grotesk'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/host-grotesk:vf@latest/latin-wght-normal.woff2') format('woff2-variations'); font-weight: 300 800; font-display: swap; }
@font-face { font-family: 'Martian Mono'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/martian-mono:vf@latest/latin-wght-normal.woff') format('woff'); font-weight: 100 800; font-stretch: 75% 112.5%; font-display: swap; }
@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; }
@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; }
/* Rezept it-systemhaus-verlaesslich (tokens.css) */
:root {
--grund: #F1F2F3; --text: #16191D; --flaeche: #E1E4E8; --dunkel: #1B1F24; --signal: #2946B0;
--leise: #565C64; --ok: #1E6E43; --warn: #7F5400; --stoer: #B3261E; --wartung: #2946B0; --linie: #CDD1D6;
--ok-flaeche: #DCEBE2; --warn-flaeche: #F3E6CC; --stoer-flaeche: #F6DEDC; --wartung-flaeche: #DDE3F6;
--ease: cubic-bezier(0.2, 0, 0.38, 0.9); --ease-ein: cubic-bezier(0, 0, 0.38, 0.9); --ease-aus: cubic-bezier(0.2, 0, 1, 0.9);
--d-hover: 70ms; --d-hover-ein: 110ms; --d-panel: 240ms; --d-zu: 160ms; --d-meldung: 400ms;
--mono: 'Martian Mono', ui-monospace, monospace; --sans: 'Host Grotesk', system-ui, sans-serif;
}
* { box-sizing: border-box; margin: 0; }
html { background: var(--grund); color: var(--text); font: 400 17px/1.6 var(--sans); -webkit-text-size-adjust: 100%; }
:root:has(:focus-visible) { scroll-behavior: auto; }
body { min-height: 100svh; }
button, input { font: inherit; color: inherit; }
button { cursor: pointer; background: none; border: 0; }
a { color: inherit; }
:focus-visible { outline: 2px solid var(--signal); outline-offset: 3px; border-radius: 2px; }
.vh { position: absolute !important; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap; }
.mono { font-family: var(--mono); font-stretch: 87%; font-size: .8125rem; letter-spacing: 0; font-variant-numeric: tabular-nums; }
.label { font: 500 .6875rem/1.2 var(--mono); font-stretch: 87%; letter-spacing: .08em; text-transform: uppercase; color: var(--leise); }
.welt[hidden] { display: none !important; }
/* ── Kopf ─────────────────────────────── */
.kopf { position: sticky; top: 0; z-index: 5; background: rgba(241, 242, 243, .94); -webkit-backdrop-filter: blur(8px); backdrop-filter: blur(8px); border-bottom: 1px solid var(--linie); }
.kopf__innen { max-width: 1120px; margin: 0 auto; padding: 14px clamp(16px, 4vw, 40px); display: flex; flex-wrap: wrap; align-items: center; gap: 10px 24px; }
.marke { display: inline-flex; align-items: center; gap: 10px; text-decoration: none; font-weight: 700; letter-spacing: -.02em; font-size: 1.0625rem; }
.marke svg { width: 28px; height: 28px; flex: none; }
.marke small { font: 500 .6875rem/1 var(--mono); letter-spacing: .08em; text-transform: uppercase; color: var(--leise); margin-left: 2px; padding-left: 10px; border-left: 1px solid var(--linie); }
.kopf__rechts { margin-left: auto; display: flex; flex-wrap: wrap; align-items: center; gap: 8px 18px; }
.kopf__link { font-size: .9375rem; text-decoration: underline; text-decoration-color: var(--linie); text-underline-offset: 4px; transition: text-decoration-color var(--d-hover) var(--ease-aus); }
.kopf__link:hover { text-decoration-color: currentColor; transition-duration: var(--d-hover-ein); transition-timing-function: var(--ease-ein); }
.knopf { display: inline-flex; align-items: center; gap: 8px; min-height: 44px; padding: 10px 16px; border-radius: 6px; font-weight: 600; text-decoration: none; background: var(--dunkel); color: #fff; transition: background-color var(--d-hover) var(--ease-aus); }
.knopf:hover { background: #000; transition-duration: var(--d-hover-ein); }
.knopf--signal { background: var(--signal); }
.knopf--signal:hover { background: #1f378f; }
.knopf .mono { font-size: .875rem; color: inherit; }
.knopf svg { width: 18px; height: 18px; flex: none; }
/* ── Bühne mit Punktraster ────────────── */
.buehne { position: relative; border-bottom: 1px solid var(--linie); background-image: radial-gradient(circle at 1px 1px, rgba(22, 25, 29, .16) 1px, transparent 1.2px); background-size: 20px 20px; background-position: 10px 10px; }
.buehne__innen { max-width: 1120px; margin: 0 auto; padding: clamp(36px, 7vw, 84px) clamp(16px, 4vw, 40px) clamp(28px, 5vw, 56px); display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 24px 48px; align-items: end; }
.gesamt { display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 0 18px; align-items: start; }
.gesamt__symbol { width: clamp(44px, 6vw, 64px); height: clamp(44px, 6vw, 64px); border-radius: 50%; display: grid; place-items: center; color: #fff; margin-top: .2em; }
.gesamt__symbol svg { width: 55%; height: 55%; }
.gesamt h1 { font-weight: 700; font-size: clamp(2.25rem, 1.4rem + 3.4vw, 4rem); line-height: 1.02; letter-spacing: -.03em; text-wrap: balance; max-width: 20ch; }
.gesamt p { grid-column: 2; margin-top: 14px; max-width: 58ch; color: var(--text); }
.stand { grid-column: 2; margin-top: 18px; display: flex; flex-wrap: wrap; gap: 6px 18px; color: var(--leise); }
.stand b { font-weight: 500; color: var(--text); }
[data-ton="ok"] { --ton: var(--ok); --ton-flaeche: var(--ok-flaeche); }
[data-ton="warn"] { --ton: var(--warn); --ton-flaeche: var(--warn-flaeche); }
[data-ton="stoer"] { --ton: var(--stoer); --ton-flaeche: var(--stoer-flaeche); }
[data-ton="wartung"] { --ton: var(--wartung); --ton-flaeche: var(--wartung-flaeche); }
[data-ton="unbekannt"] { --ton: var(--leise); --ton-flaeche: var(--flaeche); }
.gesamt__symbol { background: var(--ton); }
.hilfe { justify-self: end; display: grid; gap: 8px; padding: 18px 20px; background: #fff; border: 1px solid var(--linie); border-radius: 8px; min-width: 260px; }
.hilfe p { font-size: .9375rem; }
.hilfe .knopf { justify-content: center; }
.auto { display: flex; align-items: center; gap: 10px; font-size: .875rem; color: var(--leise); margin-top: 4px; }
.schalter { position: relative; width: 38px; height: 22px; border-radius: 11px; background: #9aa0a8; flex: none; transition: background-color var(--d-panel) var(--ease); }
.schalter::after { content: ''; position: absolute; top: 3px; left: 3px; width: 16px; height: 16px; border-radius: 50%; background: #fff; transition: transform var(--d-panel) var(--ease); }
.schalter[aria-checked="true"] { background: var(--ok); }
.schalter[aria-checked="true"]::after { transform: translateX(16px); }
/* ── Seite ────────────────────────────── */
.seite { max-width: 1120px; margin: 0 auto; padding: clamp(28px, 5vw, 56px) clamp(16px, 4vw, 40px) 80px; display: grid; grid-template-columns: minmax(0, 1fr); gap: clamp(36px, 6vw, 64px); }
.abschnitt__kopf { display: flex; flex-wrap: wrap; align-items: baseline; justify-content: space-between; gap: 6px 18px; margin-bottom: 18px; }
h2 { font-weight: 600; font-size: clamp(1.5rem, 1.2rem + 1.2vw, 2.125rem); line-height: 1.12; letter-spacing: -.02em; }
h3 { font-weight: 600; font-size: 1.125rem; line-height: 1.3; letter-spacing: -.01em; }
/* Laufende Störung */
.vorfall { background: #fff; border: 1px solid var(--linie); border-left: 4px solid var(--ton); border-radius: 8px; display: grid; grid-template-columns: minmax(0, 1.1fr) minmax(0, 1fr); }
.vorfall__kopf { padding: clamp(20px, 3vw, 32px); border-right: 1px solid var(--linie); }
.vorfall__kopf h3 { font-size: clamp(1.25rem, 1.05rem + .8vw, 1.625rem); letter-spacing: -.02em; margin: 10px 0 12px; text-wrap: balance; }
.vorfall__kopf p { max-width: 52ch; }
.marker { display: inline-flex; align-items: center; gap: 7px; padding: 4px 9px 4px 7px; border-radius: 4px; background: var(--ton-flaeche); color: var(--ton); font: 600 .6875rem/1.2 var(--mono); font-stretch: 87%; letter-spacing: .08em; text-transform: uppercase; }
.marker svg { width: 14px; height: 14px; flex: none; }
.fakten { display: grid; grid-template-columns: max-content minmax(0, 1fr); gap: 6px 16px; margin-top: 18px; font-size: .9375rem; }
.fakten dt { color: var(--leise); }
.fakten dd { font-variant-numeric: tabular-nums; }
.chips { display: flex; flex-wrap: wrap; gap: 6px; }
.chip { padding: 2px 8px; border: 1px solid var(--linie); border-radius: 4px; font-size: .8125rem; background: var(--grund); }
.zeitleiste { list-style: none; padding: clamp(20px, 3vw, 32px); display: grid; gap: 0; }
.zeitleiste li { position: relative; padding: 0 0 18px 26px; }
.zeitleiste li:last-child { padding-bottom: 0; }
.zeitleiste li::before { content: ''; position: absolute; left: 5px; top: 8px; bottom: -8px; width: 1px; background: var(--linie); }
.zeitleiste li:last-child::before { display: none; }
.zeitleiste li::after { content: ''; position: absolute; left: 0; top: 4px; width: 11px; height: 11px; border-radius: 50%; background: #fff; border: 2px solid var(--leise); }
.zeitleiste li:first-child::after { background: var(--ton); border-color: var(--ton); }
.zeitleiste .label { color: var(--text); display: flex; flex-wrap: wrap; gap: 4px 10px; }
.zeitleiste .label time { color: var(--leise); }
.zeitleiste p { font-size: .9375rem; margin-top: 4px; max-width: 54ch; }
.neu { animation: neu var(--d-meldung) var(--ease-ein) both; }
@keyframes neu { from { opacity: 0; transform: translateY(4px); } }
/* Dienste */
.dienste { background: #fff; border: 1px solid var(--linie); border-radius: 8px; }
.gruppe + .gruppe { border-top: 1px solid var(--linie); }
.gruppe__titel { padding: 14px clamp(16px, 2.4vw, 24px) 0; }
.dienst { display: grid; grid-template-columns: minmax(0, 1fr) auto; grid-template-areas: "name zustand" "verlauf verlauf" "info info"; gap: 6px 16px; padding: 14px clamp(16px, 2.4vw, 24px); }
.dienst + .dienst { border-top: 1px dashed var(--linie); }
.dienst__name { grid-area: name; font-weight: 500; }
.dienst__name small { display: block; font-size: .8125rem; color: var(--leise); font-weight: 400; }
.zustand { grid-area: zustand; display: inline-flex; align-items: center; gap: 7px; color: var(--ton); font-weight: 600; font-size: .9375rem; white-space: nowrap; align-self: start; }
.zustand svg { width: 18px; height: 18px; flex: none; }
.verlauf { grid-area: verlauf; display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 4px 14px; align-items: center; border-radius: 4px; }
.balken { display: grid; grid-auto-flow: column; grid-auto-columns: minmax(0, 1fr); gap: 2px; height: 28px; list-style: none; padding: 0; }
.balken li { border-radius: 1.5px; background: #8DBFA2; position: relative; }
.balken li[data-ton="warn"] { background: var(--warn); opacity: 1; }
.balken li[data-ton="stoer"] { background: var(--stoer); opacity: 1; }
.balken li[data-ton="wartung"] { background: var(--wartung); opacity: 1; }
.balken li.aktiv { outline: 2px solid var(--text); outline-offset: 1px; }
.verlauf__zahl { font-variant-numeric: tabular-nums; }
.verlauf__achse { display: flex; justify-content: space-between; grid-column: 1; color: var(--leise); }
.dienst__info { grid-area: info; min-height: 1.4em; font-size: .8125rem; color: var(--leise); }
.verlauf:focus-visible { outline-offset: 6px; }
/* Wartung, Verlauf, Abo */
.zwei { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: clamp(24px, 4vw, 48px); align-items: start; }
.liste { list-style: none; padding: 0; border-top: 1px solid var(--linie); }
.liste li { padding: 14px 0; border-bottom: 1px solid var(--linie); display: grid; grid-template-columns: 7.5rem minmax(0, 1fr); gap: 4px 16px; }
.liste time { color: var(--leise); }
.liste p { font-size: .9375rem; color: var(--leise); }
.liste .leer { grid-template-columns: minmax(0, 1fr); color: var(--leise); }
.abo { background: var(--dunkel); color: #F1F2F3; border-radius: 8px; padding: clamp(20px, 3vw, 32px); }
.abo h2 { color: #fff; }
.abo p { color: rgba(241, 242, 243, .78); font-size: .9375rem; max-width: 54ch; }
.abo form { display: grid; gap: 14px; margin-top: 18px; }
.abo label.feld { display: grid; gap: 6px; font-size: .9375rem; }
.abo input[type="email"] { min-height: 48px; padding: 10px 14px; border-radius: 6px; border: 1px solid #5d646d; background: #262b31; color: #fff; }
.abo input[type="email"]:focus-visible { outline-color: #fff; }
.abo input[aria-invalid="true"] { border-color: #ff8f86; }
.fehler { color: #ffb4ad; font-size: .875rem; }
.fehler:empty { display: none; }
.wahl { display: flex; flex-wrap: wrap; gap: 8px; border: 0; padding: 0; }
.wahl legend { font-size: .9375rem; margin-bottom: 8px; }
.wahl label { display: inline-flex; align-items: center; gap: 8px; padding: 8px 12px; border: 1px solid #4a5058; border-radius: 6px; font-size: .875rem; cursor: pointer; }
.wahl input { accent-color: #9fb3ff; width: 16px; height: 16px; }
.abo .knopf { justify-self: start; background: #F1F2F3; color: var(--dunkel); }
.abo .knopf:hover { background: #fff; }
.abo :focus-visible { outline-color: #fff; }
.abo__hinweis { font-size: .8125rem !important; color: rgba(241, 242, 243, .72) !important; }
.platzhalter { background: rgba(255, 200, 90, .14); border: 1px dashed rgba(255, 200, 90, .55); border-radius: 3px; padding: 0 4px; }
.meldung { padding: 12px 14px; border-radius: 6px; background: #22362b; color: #d9f2e2; font-size: .9375rem; }
.meldung:empty { display: none; }
.fuss { border-top: 1px solid var(--linie); }
.fuss__innen { max-width: 1120px; margin: 0 auto; padding: 24px clamp(16px, 4vw, 40px) 40px; display: flex; flex-wrap: wrap; gap: 8px 24px; justify-content: space-between; color: var(--leise); font-size: .875rem; }
/* Quelle nicht erreichbar */
.quellehinweis { display: flex; gap: 12px; align-items: start; padding: 14px 16px; border-radius: 8px; background: #fff; border: 1px dashed var(--leise); font-size: .9375rem; max-width: 62ch; margin-top: 18px; grid-column: 2; }
.quellehinweis[hidden] { display: none; }
.quellehinweis svg { width: 20px; height: 20px; flex: none; margin-top: 2px; }
.veraltet .dienste, .veraltet .vorfall { filter: grayscale(1); }
.veraltet .zustand { color: var(--leise); }
/* ── Versorger ────────────────────────── */
.notruf { display: grid; grid-template-columns: auto minmax(0, 1fr) auto; gap: 14px 20px; align-items: center; padding: 18px 20px; border-radius: 8px; background: #fff; border: 2px solid var(--stoer); }
.notruf svg { width: 34px; height: 34px; color: var(--stoer); }
.notruf b { display: block; font-size: 1.125rem; letter-spacing: -.01em; }
.notruf p { font-size: .9375rem; }
.notruf .knopf { background: var(--stoer); }
.notruf .knopf:hover { background: #8f1d17; }
.sparten { list-style: none; padding: 0; display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: 8px; }
.sparten li { background: #fff; border: 1px solid var(--linie); border-top: 3px solid var(--ton); border-radius: 6px; padding: 12px 14px; display: grid; gap: 4px; }
.sparten b { font-weight: 600; }
.sparten .zustand { font-size: .875rem; grid-area: auto; white-space: normal; }
.suche { display: grid; gap: 6px; max-width: 520px; }
.suche__feld { display: flex; gap: 8px; }
.suche input { flex: 1; min-width: 0; min-height: 48px; padding: 10px 14px; border-radius: 6px; border: 1px solid #8a9099; background: #fff; }
.treffer { margin-top: 18px; display: grid; gap: 10px; list-style: none; padding: 0; }
.ort { background: #fff; border: 1px solid var(--linie); border-left: 4px solid var(--ton); border-radius: 6px; padding: 14px 18px; display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 4px 16px; }
.ort h3 { font-size: 1.0625rem; }
.ort p { font-size: .9375rem; color: var(--leise); }
.ort .mono { color: var(--text); text-align: right; }
.treffer .leer { background: #fff; border: 1px dashed var(--leise); border-radius: 6px; padding: 16px 18px; font-size: .9375rem; }
.treffer .leer ol { margin: 8px 0 0 1.2em; }
/* ── Praxis (Kundenseite) ─────────────── */
.welt[data-welt="praxis"] { --p-grund: #F5F7F7; --p-text: #363B3B; --p-marke: #005450; --p-akzent: #FFC776; --p-flaeche: #E2E0DE; background: var(--p-grund); color: var(--p-text); font-family: 'Google Sans Flex', system-ui, sans-serif; min-height: 100svh; }
.leiste { background: #FFF1D6; border-bottom: 1px solid #E8C98A; }
.leiste[data-ton="wartung"] { background: #E6EEF0; border-color: #B9CDD1; }
.leiste__innen { max-width: 1120px; margin: 0 auto; padding: 14px clamp(16px, 4vw, 40px); display: grid; grid-template-columns: auto minmax(0, 1fr) auto; gap: 10px 18px; align-items: start; }
.leiste__symbol { width: 26px; height: 26px; color: #6B4600; margin-top: 2px; }
.leiste[data-ton="wartung"] .leiste__symbol { color: var(--p-marke); }
.leiste h2 { font: 600 1.0625rem/1.35 'Google Sans Flex', system-ui, sans-serif; letter-spacing: 0; color: #2b2410; }
.leiste p { font-size: .9375rem; margin-top: 2px; }
.leiste__zeit { font-size: .8125rem; color: #5a4d33; margin-top: 4px; }
.wege { list-style: none; padding: 0; margin-top: 10px; display: flex; flex-wrap: wrap; gap: 8px; }
.wege a { display: inline-flex; align-items: center; gap: 8px; min-height: 44px; padding: 8px 14px; border-radius: 22px; background: #fff; border: 1px solid #d9c18d; text-decoration: none; font-size: .9375rem; font-weight: 500; color: #2b2410; transition: border-color var(--d-hover) var(--ease-aus); }
.wege a:hover { border-color: #2b2410; transition-duration: var(--d-hover-ein); }
.wege a svg { width: 18px; height: 18px; color: var(--p-marke); }
.leiste__zu { min-width: 44px; min-height: 44px; display: grid; place-items: center; border-radius: 50%; color: #4a3d20; }
.leiste__zu:hover { background: rgba(0, 0, 0, .06); }
.leiste__zu svg { width: 18px; height: 18px; }
.notfall { margin-top: 10px; font-size: .875rem; }
.notfall b { font-weight: 600; }
.p-kopf { max-width: 1120px; margin: 0 auto; padding: 18px clamp(16px, 4vw, 40px); display: flex; flex-wrap: wrap; gap: 10px 28px; align-items: center; }
.p-marke { display: flex; align-items: center; gap: 10px; font: 400 1.375rem/1 'Hedvig Letters Serif', Georgia, serif; color: var(--p-marke); text-decoration: none; }
.p-marke svg { width: 30px; height: 30px; }
.p-nav { display: flex; flex-wrap: wrap; gap: 4px 22px; margin-left: auto; font-size: .9375rem; }
.p-nav a { text-decoration: none; }
.p-nav a:hover { text-decoration: underline; text-underline-offset: 4px; }
.p-held { max-width: 1120px; margin: 0 auto; padding: clamp(28px, 6vw, 72px) clamp(16px, 4vw, 40px); display: grid; grid-template-columns: minmax(0, 1.2fr) minmax(0, 1fr); gap: 32px; align-items: center; }
.p-held h1 { font: 400 clamp(2.25rem, 1.4rem + 3.2vw, 4.25rem)/1.08 'Hedvig Letters Serif', Georgia, serif; letter-spacing: -.01em; color: var(--p-marke); text-wrap: balance; }
.p-held p { margin-top: 16px; font-size: 1.0625rem; max-width: 48ch; }
.p-knoepfe { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 24px; }
.p-knopf { display: inline-flex; align-items: center; min-height: 48px; padding: 12px 20px; border-radius: 24px; background: var(--p-marke); color: #fff; text-decoration: none; font-weight: 500; }
.p-knopf--zart { background: transparent; color: var(--p-marke); border: 1.5px solid var(--p-marke); }
.p-zeiten { background: #fff; border-radius: 16px; padding: 22px 24px; display: grid; gap: 10px; }
.p-zeiten h2 { font: 400 1.5rem/1.2 'Hedvig Letters Serif', Georgia, serif; color: var(--p-marke); letter-spacing: 0; }
.p-zeiten dl { display: grid; grid-template-columns: max-content minmax(0, 1fr); gap: 4px 18px; font-size: .9375rem; font-variant-numeric: tabular-nums; }
.p-zeiten .tel { display: flex; justify-content: space-between; flex-wrap: wrap; gap: 4px 12px; padding-top: 10px; border-top: 1px solid var(--p-flaeche); }
.p-zeiten .tel s { color: #596666; }
.p-zeiten .tel em { font-style: normal; font-size: .8125rem; color: #6B4600; background: #FFF1D6; padding: 2px 8px; border-radius: 10px; }
.p-quelle { max-width: 1120px; margin: 0 auto; padding: 0 clamp(16px, 4vw, 40px) 48px; font-size: .8125rem; color: #596666; }
.welt[data-welt="praxis"] .mono { font-family: inherit; font-stretch: normal; font-size: inherit; font-variant-numeric: tabular-nums; }
.leiste.raus { animation: raus var(--d-zu) var(--ease-aus) both; }
@keyframes raus { to { opacity: 0; transform: translateY(-4px); } }
/* ── Pult ─────────────────────────────── */
.pult { position: fixed; right: 16px; bottom: 16px; z-index: 20; display: grid; gap: 8px; padding: 10px 12px; background: rgba(27, 31, 36, .95); color: #e6e8eb; border-radius: 8px; box-shadow: 0 12px 32px -14px rgba(0, 0, 0, .5); max-width: calc(100vw - 32px); }
.pult .reihe { display: flex; flex-wrap: wrap; align-items: center; gap: 4px; }
.pult .reihe > span { font: 500 10px/1 'Martian Mono', ui-monospace, monospace; letter-spacing: .1em; text-transform: uppercase; color: rgba(230, 232, 235, .7); width: 72px; }
.pult button { font: 500 10.5px/1 'Martian Mono', ui-monospace, monospace; letter-spacing: .06em; text-transform: uppercase; color: rgba(230, 232, 235, .78); padding: 7px 8px; border-radius: 4px; transition: background-color var(--d-hover) var(--ease-aus); }
.pult button:hover { background: rgba(255, 255, 255, .08); }
.pult button[aria-pressed="true"] { background: #e6e8eb; color: var(--dunkel); }
.pult :focus-visible { outline-color: #fff; }
/* ── Größen ───────────────────────────── */
@media (max-width: 1040px) {
.buehne__innen { grid-template-columns: minmax(0, 1fr); }
.hilfe { justify-self: start; min-width: 0; grid-template-columns: auto auto; align-items: center; column-gap: 18px; }
.hilfe .label { grid-column: 1 / -1; }
}
@media (max-width: 900px) {
.vorfall { grid-template-columns: minmax(0, 1fr); }
.vorfall__kopf { border-right: 0; border-bottom: 1px solid var(--linie); }
.zwei { grid-template-columns: minmax(0, 1fr); }
.sparten { grid-template-columns: repeat(auto-fill, minmax(min(150px, 100%), 1fr)); }
.p-held { grid-template-columns: minmax(0, 1fr); }
}
@media (max-width: 640px) {
.hilfe { grid-template-columns: minmax(0, 1fr); justify-self: stretch; }
.gesamt { grid-template-columns: minmax(0, 1fr); }
.gesamt__symbol { margin-bottom: 14px; }
.gesamt p, .stand, .quellehinweis { grid-column: 1; }
.liste li { grid-template-columns: minmax(0, 1fr); }
.notruf { grid-template-columns: auto minmax(0, 1fr); }
.notruf .knopf { grid-column: 1 / -1; justify-content: center; }
.ort { grid-template-columns: minmax(0, 1fr); }
.ort .mono { text-align: left; }
.fakten { grid-template-columns: minmax(0, 1fr); gap: 0; }
.fakten dd { margin-bottom: 8px; }
.kopf__rechts .kopf__link { display: none; }
.leiste__innen { grid-template-columns: auto minmax(0, 1fr); }
.leiste__zu { grid-column: 2; grid-row: 1; justify-self: end; margin: -8px -8px 0 0; }
.p-nav { display: none; }
}
@media (max-width: 360px) {
.knopf { flex-wrap: wrap; }
.suche__feld { flex-wrap: wrap; }
.suche__feld .knopf { flex: 1; justify-content: center; }
.marke small { display: none; }
.fakten { grid-template-columns: minmax(0, 1fr); }
}
/* Vorschau: nur die Bühne */
.vorschau body { overflow: hidden; }
.vorschau .fuss { display: none; }
:root.reduziert *, :root.reduziert *::before, :root.reduziert *::after { transition-duration: .01ms !important; animation-duration: .01ms !important; }
@media (forced-colors: active) {
button, [role=button], [role=tab], [role=switch], input, select, textarea { border: 1px solid ButtonText !important; }
.balken li { forced-color-adjust: none; }
.gesamt__symbol, .marker { border: 1px solid CanvasText; }
}
</style>
</head>
<body>
<!-- ═════════ Welt 1: Systemhaus ═════════ -->
<div class="welt" data-welt="systemhaus">
<header class="kopf">
<div class="kopf__innen">
<a class="marke" href="#" aria-label="Netzkontor, Status-Startseite">
<svg viewBox="0 0 28 28" aria-hidden="true"><rect width="28" height="28" rx="6" fill="#1B1F24"/><g fill="#F1F2F3"><circle cx="8" cy="8" r="2"/><circle cx="14" cy="8" r="2"/><circle cx="20" cy="8" r="2"/><circle cx="8" cy="14" r="2"/><circle cx="8" cy="20" r="2"/></g><path d="M14 14h6v6" fill="none" stroke="#7F9BFF" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"/></svg>
Netzkontor <small>Status</small>
</a>
<div class="kopf__rechts">
<a class="kopf__link" href="#">Zur Website</a>
<a class="knopf" href="#"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M5 4h4l2 5-2.5 1.5a11 11 0 0 0 5 5L15 13l5 2v4a2 2 0 0 1-2 2A16 16 0 0 1 3 6a2 2 0 0 1 2-2"/></svg>Hotline <span class="mono">0591 0812 400</span></a>
</div>
</div>
</header>
<main id="inhalt-systemhaus">
<section class="buehne" aria-labelledby="gesamt-titel">
<div class="buehne__innen">
<div class="gesamt" data-feld="gesamt" data-ton="ok">
<div class="gesamt__symbol" data-feld="gesamt-symbol"></div>
<h1 id="gesamt-titel" tabindex="-1" data-feld="gesamt-titel">Alle Dienste laufen.</h1>
<p data-feld="gesamt-satz"></p>
<div class="stand mono">
<span>Stand <b><time data-feld="stand"></time></b></span>
<span data-feld="stand-relativ"></span>
</div>
<div class="quellehinweis" data-feld="quelle" hidden>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M12 8v5m0 3.5v.5M10.3 3.9 2.4 18a2 2 0 0 0 1.7 3h15.8a2 2 0 0 0 1.7-3L13.7 3.9a2 2 0 0 0-3.4 0"/></svg>
<span>Der Status lässt sich gerade nicht abrufen. Sie sehen den letzten bekannten Stand. Bei einem Ausfall rufen Sie bitte die Hotline an.</span>
</div>
</div>
<aside class="hilfe" aria-label="Hilfe">
<p class="label">Vertragskunden, rund um die Uhr</p>
<a class="knopf knopf--signal" href="#"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M5 4h4l2 5-2.5 1.5a11 11 0 0 0 5 5L15 13l5 2v4a2 2 0 0 1-2 2A16 16 0 0 1 3 6a2 2 0 0 1 2-2"/></svg><span class="mono">0591 0812 400</span></a>
<p>Ticket: <a href="#">support@netzkontor-ems.de</a></p>
<div class="auto">
<button class="schalter" type="button" role="switch" aria-checked="true" aria-labelledby="auto-text" data-auto></button>
<span id="auto-text">Jede Minute neu laden</span>
</div>
</aside>
</div>
</section>
<div class="seite">
<section aria-labelledby="vorfall-titel" data-feld="vorfall-bereich">
<div class="abschnitt__kopf"><h2 id="vorfall-titel">Laufende Störung</h2><span class="label" data-feld="vorfall-zahl"></span></div>
<div data-feld="vorfall"></div>
</section>
<section aria-labelledby="dienste-titel">
<div class="abschnitt__kopf"><h2 id="dienste-titel">Dienste</h2><span class="label" data-feld="tage-label">Verlauf 90 Tage</span></div>
<div class="dienste" data-feld="dienste"></div>
</section>
<div class="zwei">
<section aria-labelledby="wartung-titel">
<div class="abschnitt__kopf"><h2 id="wartung-titel">Geplante Wartung</h2></div>
<ul class="liste" data-feld="wartung"></ul>
<div class="abschnitt__kopf" style="margin-top:40px"><h2 id="historie-titel">Vergangene Störungen</h2><span class="label">14 Tage</span></div>
<ul class="liste" data-feld="historie" aria-labelledby="historie-titel"></ul>
</section>
<section class="abo" aria-labelledby="abo-titel">
<h2 id="abo-titel">Bei Störung Bescheid bekommen</h2>
<p>Wir schreiben, wenn eine Störung beginnt, wenn es Neues gibt und wenn sie behoben ist. Sie können jederzeit abbestellen.</p>
<form novalidate data-abo>
<label class="feld">E-Mail-Adresse
<input type="email" name="email" autocomplete="email" inputmode="email" aria-describedby="abo-fehler" required>
</label>
<p class="fehler" id="abo-fehler" aria-live="polite"></p>
<fieldset class="wahl">
<legend>Für welche Dienste?</legend>
<label><input type="checkbox" name="d" value="telefon" checked> Telefonanlage</label>
<label><input type="checkbox" name="d" value="mail" checked> E-Mail</label>
<label><input type="checkbox" name="d" value="fernwartung"> Fernwartung</label>
<label><input type="checkbox" name="d" value="backup"> Datensicherung</label>
<label><input type="checkbox" name="d" value="vpn"> VPN</label>
</fieldset>
<button class="knopf" type="submit">Abonnieren</button>
<p class="meldung" role="status" data-abo-meldung></p>
<p class="abo__hinweis">Sie bekommen zuerst eine E-Mail zur Bestätigung. Ohne Bestätigung schreiben wir nichts. <span class="platzhalter">Datenschutzhinweis und Versanddienst vom Kunden ergänzen und prüfen lassen.</span></p>
</form>
</section>
</div>
</div>
</main>
<footer class="fuss"><div class="fuss__innen"><span>Netzkontor · IT für Praxen und Betriebe · Lingen (Ems)</span><span>Diese Seite läuft getrennt von unseren Systemen.</span></div></footer>
</div>
<!-- ═════════ Welt 2: Versorger ═════════ -->
<div class="welt" data-welt="versorger" hidden>
<header class="kopf">
<div class="kopf__innen">
<a class="marke" href="#" aria-label="Stadtwerke Ilmbrück, Startseite">
<svg viewBox="0 0 28 28" aria-hidden="true"><rect width="28" height="28" rx="14" fill="#1B1F24"/><path d="M7 17c3-6 5-6 7 0s4 6 7 0" fill="none" stroke="#7FD1A6" stroke-width="2.6" stroke-linecap="round"/><path d="M15 5l-4 7h4l-2 6" fill="none" stroke="#FFD27F" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
Stadtwerke Ilmbrück <small>Störungen</small>
</a>
<div class="kopf__rechts"><a class="kopf__link" href="#">Zur Website</a><a class="knopf" href="#"><span class="mono">Störung 05823 0 4410</span></a></div>
</div>
</header>
<main>
<section class="buehne" aria-labelledby="v-titel">
<div class="buehne__innen">
<div class="gesamt" data-feld="gesamt" data-ton="ok">
<div class="gesamt__symbol" data-feld="gesamt-symbol"></div>
<h1 id="v-titel" tabindex="-1" data-feld="gesamt-titel"></h1>
<p data-feld="gesamt-satz"></p>
<div class="stand mono"><span>Stand <b><time data-feld="stand"></time></b></span><span data-feld="stand-relativ"></span></div>
<div class="quellehinweis" data-feld="quelle" hidden>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M12 8v5m0 3.5v.5M10.3 3.9 2.4 18a2 2 0 0 0 1.7 3h15.8a2 2 0 0 0 1.7-3L13.7 3.9a2 2 0 0 0-3.4 0"/></svg>
<span>Die Störungsliste lässt sich gerade nicht abrufen. Ist bei Ihnen Strom, Gas oder Wasser weg, rufen Sie bitte die Störungsnummer an.</span>
</div>
</div>
</div>
</section>
<div class="seite">
<div class="notruf" role="note">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M12 3c2 3 5 5 5 9a5 5 0 0 1-10 0c0-2 1-3 2-4 0 2 1 3 2 3 0-3-1-5 1-8"/></svg>
<div><b>Riecht es nach Gas? Nicht hier melden.</b><p>Kein Licht und kein Handy im Raum benutzen. Fenster auf, Haus verlassen, von draußen anrufen.</p></div>
<a class="knopf" href="#"><span class="mono">Gas-Notruf 05823 0 4411</span></a>
</div>
<section aria-labelledby="sparten-titel">
<div class="abschnitt__kopf"><h2 id="sparten-titel">Versorgung im Netz</h2></div>
<ul class="sparten" data-feld="sparten"></ul>
</section>
<section aria-labelledby="orte-titel">
<div class="abschnitt__kopf"><h2 id="orte-titel">Störungen und Arbeiten nach Ort</h2><span class="label" data-feld="orte-zahl"></span></div>
<form class="suche" role="search" data-suche>
<label for="suche-feld">Straße, Ortsteil oder Postleitzahl</label>
<div class="suche__feld">
<input id="suche-feld" type="search" autocomplete="street-address" enterkeyhint="search" placeholder="z. B. Mühlenweg oder 29587">
<button class="knopf" type="submit">Suchen</button>
</div>
</form>
<p class="vh" role="status" data-suche-status></p>
<ul class="treffer" data-feld="orte"></ul>
</section>
</div>
</main>
<footer class="fuss"><div class="fuss__innen"><span>Stadtwerke Ilmbrück (erfunden) · Netzbetreiber für Strom, Gas, Wasser</span><span>Liste aus der Leitstelle, alle 60 Sekunden neu.</span></div></footer>
</div>
<!-- ═════════ Welt 3: Praxis (Kundenseite mit Hinweisleiste) ═════════ -->
<div class="welt" data-welt="praxis" hidden>
<div data-feld="leiste-ort"></div>
<header class="p-kopf">
<a class="p-marke" href="#">
<svg viewBox="0 0 30 30" aria-hidden="true"><circle cx="15" cy="15" r="15" fill="#005450"/><path d="M15 7v16M9 12c2 2 4 3 6 3s4-1 6-3M10 21c1.5-1.5 3-2 5-2s3.5.5 5 2" fill="none" stroke="#F5F7F7" stroke-width="1.8" stroke-linecap="round"/></svg>
Praxis am Kiefernweg
</a>
<nav class="p-nav" aria-label="Praxis"><a href="#">Sprechzeiten</a><a href="#">Leistungen</a><a href="#">Team</a><a href="#">Anfahrt</a></nav>
</header>
<main class="p-held">
<div>
<h1>Hausärztliche Versorgung in Lingen-Darme</h1>
<p>Dr. Hanna Wübbels und Dr. Okan Yıldız. Termine online oder am Telefon, Akutsprechstunde jeden Vormittag ohne Termin.</p>
<div class="p-knoepfe"><a class="p-knopf" href="#">Termin online buchen</a><a class="p-knopf p-knopf--zart" href="#">Rezept bestellen</a></div>
</div>
<div class="p-zeiten">
<h2>Heute</h2>
<dl><dt>Sprechzeit</dt><dd>08:00–12:00, 15:00–18:00</dd><dt>Akut</dt><dd>08:00–10:00 ohne Termin</dd></dl>
<div class="tel" data-feld="praxis-tel"></div>
</div>
</main>
<p class="p-quelle">Die Leiste oben liest den Status der Telefonanlage aus dem Status-JSON des Systemhauses (Netzkontor) und erscheint nur bei Störung oder Wartung.</p>
</div>
<div class="pult" role="group" aria-label="Steuerung der Demo">
<div class="reihe" role="group" aria-label="Variante"><span aria-hidden="true">Variante</span>
<button type="button" data-v="systemhaus" aria-pressed="true">Systemhaus</button>
<button type="button" data-v="versorger" aria-pressed="false">Versorger</button>
<button type="button" data-v="praxis" aria-pressed="false">Praxis</button>
</div>
<div class="reihe" role="group" aria-label="Zustand"><span aria-hidden="true">Zustand</span>
<button type="button" data-z="ok" aria-pressed="false">Läuft</button>
<button type="button" data-z="stoer" aria-pressed="true">Störung</button>
<button type="button" data-z="wartung" aria-pressed="false">Wartung</button>
<button type="button" data-z="weg" aria-pressed="false">Quelle weg</button>
<button type="button" data-update>+ Update</button>
</div>
</div>
<p class="vh" role="status" data-ansage></p>
<script>
(() => {
const H = document.documentElement;
const reduziert = () => H.classList.contains('reduziert');
const $ = (s, r = document) => r.querySelector(s);
const $$ = (s, r = document) => [...r.querySelectorAll(s)];
const MIN = 60000, TAG = 86400000;
const zeitFmt = new Intl.DateTimeFormat('de-DE', { hour: '2-digit', minute: '2-digit', timeZone: 'Europe/Berlin' });
const datumFmt = new Intl.DateTimeFormat('de-DE', { day: 'numeric', month: 'short', timeZone: 'Europe/Berlin' });
const tagFmt = new Intl.DateTimeFormat('de-DE', { weekday: 'short', day: '2-digit', month: '2-digit', timeZone: 'Europe/Berlin' });
const rel = new Intl.RelativeTimeFormat('de', { numeric: 'auto' });
const prozent = new Intl.NumberFormat('de-DE', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
const SYM = {
ok: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12.5l4.5 4.5L19 7.5"/></svg>',
warn: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 8.5v4.5m0 3.4v.1M10.3 3.9 2.4 18a2 2 0 0 0 1.7 3h15.8a2 2 0 0 0 1.7-3L13.7 3.9a2 2 0 0 0-3.4 0"/></svg>',
stoer: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" aria-hidden="true"><path d="M7 7l10 10M17 7 7 17"/></svg>',
wartung: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M14.5 5.5a4 4 0 0 0-5 5L4 16l4 4 5.5-5.5a4 4 0 0 0 5-5L16 12l-4-4z"/></svg>',
unbekannt: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" aria-hidden="true"><path d="M9.5 9a2.5 2.5 0 1 1 3.5 2.3c-.6.3-1 .9-1 1.6v.6m0 3v.1"/></svg>'
};
const WORT = { ok: 'läuft', warn: 'eingeschränkt', stoer: 'gestört', wartung: 'Wartung', unbekannt: 'unbekannt' };
const esc = s => String(s).replace(/[&<>"]/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c]));
const iso = t => new Date(t).toISOString();
const relativ = t => { const m = Math.round((t - Date.now()) / MIN); return Math.abs(m) < 60 ? rel.format(m, 'minute') : rel.format(Math.round(m / 60), 'hour'); };
/* ── Daten, wie sie der Statusdienst als JSON liefern würde ── */
let jetzt = Date.now();
function zufall(seed) { let s = seed; return () => (s = (s * 16807) % 2147483647) / 2147483647; }
function verlauf(seed, vorfaelle) {
const r = zufall(seed), tage = [];
for (let i = 89; i >= 0; i--) {
const t = jetzt - i * TAG; let ton = 'ok', min = 0, text = 'ohne Störung';
const v = vorfaelle[i];
if (v) { ton = v[0]; min = v[1]; text = v[2]; }
else if (r() < .015) { ton = 'warn'; min = 6 + Math.round(r() * 30); text = min + ' Minuten eingeschränkt'; }
tage.push({ t, ton, min, text });
}
return tage;
}
const SYSTEM = () => ({
gruppen: [
{ titel: 'Kommunikation', dienste: [
{ id: 'telefon', name: 'Telefonanlage (Cloud)', hinweis: 'Durchwahlen, Warteschleife, Faxdienst', seed: 11, v: { 23: ['stoer', 47, '47 Minuten gestört: Anbieter-Ausfall'] } },
{ id: 'mail', name: 'E-Mail und Kalender', hinweis: 'Postfächer, Versand, Kalenderfreigaben', seed: 23, v: { 6: ['warn', 31, '31 Minuten Versand verzögert'] } },
{ id: 'video', name: 'Videokonferenz', hinweis: 'Besprechungsräume, Wartezimmer', seed: 31, v: {} } ] },
{ titel: 'Betrieb', dienste: [
{ id: 'fernwartung', name: 'Fernwartung', hinweis: 'Sitzungen nur mit Ihrer Zustimmung', seed: 41, v: {} },
{ id: 'backup', name: 'Datensicherung', hinweis: 'Nächtliche Sicherung, Wiederherstellung', seed: 53, v: { 40: ['wartung', 90, '90 Minuten geplante Wartung'] } },
{ id: 'vpn', name: 'VPN-Zugang', hinweis: 'Homeoffice, Filialen', seed: 61, v: { 12: ['stoer', 18, '18 Minuten gestört: Zertifikat erneuert'] } },
{ id: 'portal', name: 'Ticket-Portal', hinweis: 'Anfragen, Rechnungen', seed: 71, v: {} } ] }
]
});
const VORFALL = {
titel: 'E-Mail-Versand nach außen verzögert',
dienste: ['mail'],
ton: 'warn',
beginn: () => jetzt - 74 * MIN,
naechstes: () => jetzt + 26 * MIN,
satz: 'Ausgehende E-Mails kommen mit bis zu 40 Minuten Verspätung an. Eingehende E-Mails, Kalender und Telefon laufen normal. Sie müssen nichts tun; Nachrichten gehen nicht verloren.',
updates: [
{ art: 'Beobachtung', vor: 9, text: 'Die Warteschlange baut sich ab. Neue E-Mails gehen wieder innerhalb weniger Minuten raus. Ältere Nachrichten folgen in der nächsten halben Stunde.' },
{ art: 'Ursache gefunden', vor: 38, text: 'Der Versanddienst unseres Rechenzentrums drosselt nach einem Update. Der Anbieter hat die Änderung zurückgenommen.' },
{ art: 'Wird untersucht', vor: 74, text: 'Mehrere Kunden melden verzögerte E-Mails. Wir prüfen den Versand.' }
],
nachzug: { art: 'Behoben', text: 'Alle wartenden E-Mails sind zugestellt. Wir beobachten den Versand noch bis heute Abend.' }
};
const WARTUNG_JETZT = { titel: 'Firewall-Update im Rechenzentrum', dienste: ['vpn', 'fernwartung'], beginn: () => jetzt - 12 * MIN, ende: () => jetzt + 33 * MIN, satz: 'VPN und Fernwartung sind bis etwa 30 Minuten nicht erreichbar. Telefon und E-Mail laufen.' };
let variante = 'systemhaus', zustand = 'stoer', extraUpdates = [], auto = true, autoTimer = null, behoben = false;
/* ── Systemhaus ── */
function dienstTon(id) {
if (zustand === 'stoer' && !behoben && VORFALL.dienste.includes(id)) return VORFALL.ton;
if (zustand === 'wartung' && WARTUNG_JETZT.dienste.includes(id)) return 'wartung';
return 'ok';
}
function gesamtSetzen(root, ton, titel, satz) {
const g = $('[data-feld="gesamt"]', root);
g.dataset.ton = ton;
$('[data-feld="gesamt-symbol"]', root).innerHTML = SYM[ton];
const h = $('[data-feld="gesamt-titel"]', root);
if (h.textContent !== titel) { h.textContent = titel; meldeNeu(g); }
$('[data-feld="gesamt-satz"]', root).textContent = satz;
const stand = $('[data-feld="stand"]', root);
const t = zustand === 'weg' ? jetzt - 23 * MIN : jetzt;
stand.dateTime = iso(t); stand.textContent = zeitFmt.format(t) + ' Uhr';
$('[data-feld="stand-relativ"]', root).textContent = zustand === 'weg' ? 'letzter bekannter Stand, ' + relativ(t) : 'aktualisiert ' + relativ(t - 30000).replace('vor 0 Minuten', 'gerade eben');
$('[data-feld="quelle"]', root).hidden = zustand !== 'weg';
root.classList.toggle('veraltet', zustand === 'weg');
}
function meldeNeu(el) { if (reduziert()) return; el.classList.remove('neu'); void el.offsetWidth; el.classList.add('neu'); }
function systemhaus() {
const root = $('[data-welt="systemhaus"]');
const daten = SYSTEM();
const telefon = matchMedia('(max-width: 640px)').matches, tageZahl = telefon ? 30 : 90;
$('[data-feld="tage-label"]', root).textContent = 'Verlauf ' + tageZahl + ' Tage';
if (zustand === 'ok' || (zustand === 'stoer' && behoben)) gesamtSetzen(root, 'ok', 'Alle Dienste laufen.', behoben ? 'Die Störung beim E-Mail-Versand ist behoben. Alle Dienste laufen normal.' : 'Telefonanlage, E-Mail, Fernwartung, Datensicherung und VPN laufen normal. Wir prüfen jede Minute.');
else if (zustand === 'stoer') gesamtSetzen(root, 'warn', 'E-Mail-Versand verzögert. Alles andere läuft.', 'Wir kennen das Problem und arbeiten daran. Das nächste Update kommt spätestens um ' + zeitFmt.format(VORFALL.naechstes()) + ' Uhr.');
else if (zustand === 'wartung') gesamtSetzen(root, 'wartung', 'Geplante Wartung: VPN und Fernwartung kurz weg.', 'Bis etwa ' + zeitFmt.format(WARTUNG_JETZT.ende()) + ' Uhr. Telefon und E-Mail laufen.');
else gesamtSetzen(root, 'unbekannt', 'Status gerade nicht abrufbar.', 'Wir sehen den Statusdienst nicht. Das heißt nicht, dass etwas ausgefallen ist.');
/* laufende Störung */
const vb = $('[data-feld="vorfall"]', root);
const aktiv = (zustand === 'stoer' || zustand === 'weg');
$('[data-feld="vorfall-zahl"]', root).textContent = aktiv && !behoben ? '1 offen' : (zustand === 'wartung' ? 'Wartung läuft' : 'keine offen');
if (zustand === 'wartung') {
vb.innerHTML = `<article class="vorfall" data-ton="wartung" aria-labelledby="vf-h"><div class="vorfall__kopf"><span class="marker">${SYM.wartung}Wartung · läuft</span><h3 id="vf-h">${esc(WARTUNG_JETZT.titel)}</h3><p>${esc(WARTUNG_JETZT.satz)}</p>${fakten(WARTUNG_JETZT.beginn(), null, WARTUNG_JETZT.ende(), WARTUNG_JETZT.dienste, 'Ende')}</div><ol class="zeitleiste" aria-label="Updates, neueste zuerst"><li><p class="label"><span>Begonnen</span><time datetime="${iso(WARTUNG_JETZT.beginn())}">${zeitFmt.format(WARTUNG_JETZT.beginn())}</time></p><p>Angekündigt am Montag per E-Mail an alle Abonnenten.</p></li></ol></article>`;
} else if (aktiv && !behoben) {
const ups = [...extraUpdates.slice().reverse(), ...VORFALL.updates.map(u => ({ ...u, t: jetzt - u.vor * MIN }))];
const letztes = ups[0].t;
vb.innerHTML = `<article class="vorfall" data-ton="warn" aria-labelledby="vf-h"><div class="vorfall__kopf"><span class="marker">${SYM.warn}Eingeschränkt · seit ${zeitFmt.format(VORFALL.beginn())}</span><h3 id="vf-h">${esc(VORFALL.titel)}</h3><p>${esc(VORFALL.satz)}</p>${fakten(VORFALL.beginn(), letztes, VORFALL.naechstes(), VORFALL.dienste, 'Nächstes Update')}</div><ol class="zeitleiste" aria-label="Updates, neueste zuerst">${ups.map((u, i) => `<li${u.neu ? ' class="neu"' : ''}><p class="label"><span>${esc(u.art)}</span><time datetime="${iso(u.t)}">${zeitFmt.format(u.t)}</time></p><p>${esc(u.text)}</p></li>`).join('')}</ol></article>`;
extraUpdates.forEach(u => u.neu = false);
} else {
vb.innerHTML = `<ul class="liste"><li class="leer"><span>Gerade gibt es keine Störung. ${behoben ? 'Die letzte ist seit ' + zeitFmt.format(jetzt) + ' Uhr behoben.' : 'Die letzte liegt 6 Tage zurück.'}</span></li></ul>`;
}
/* Dienste mit Verlauf */
const box = $('[data-feld="dienste"]', root);
box.innerHTML = daten.gruppen.map(g => `<div class="gruppe" role="group" aria-labelledby="gr-${g.titel}"><p class="label gruppe__titel" id="gr-${g.titel}">${g.titel}</p>${g.dienste.map(d => {
const ton = zustand === 'weg' ? 'unbekannt' : dienstTon(d.id);
const tage = verlauf(d.seed, d.v).slice(-tageZahl);
if (ton !== 'ok' && ton !== 'unbekannt') tage[tage.length - 1] = { t: jetzt, ton, min: 0, text: 'heute: ' + WORT[ton] };
const ausfall = tage.reduce((s, x) => s + (x.ton === 'stoer' ? x.min : x.ton === 'warn' ? x.min * .5 : 0), 0);
const quote = 100 - ausfall / (tageZahl * 1440) * 100;
return `<div class="dienst" data-ton="${ton}"><p class="dienst__name">${esc(d.name)}<small>${esc(d.hinweis)}</small></p><p class="zustand">${SYM[ton]}${WORT[ton]}</p>
<div class="verlauf" tabindex="0" role="group" aria-label="Verlauf ${esc(d.name)}, ${tageZahl} Tage, ${prozent.format(quote)} Prozent verfügbar. Mit den Pfeiltasten einen Tag wählen." aria-describedby="info-${d.id}">
<ol class="balken" aria-hidden="true">${tage.map((x, i) => `<li data-ton="${x.ton}" data-i="${i}" data-text="${esc(tagFmt.format(x.t) + ': ' + x.text)}"></li>`).join('')}</ol>
<span class="mono verlauf__zahl">${prozent.format(quote)} %</span>
<span class="verlauf__achse mono" aria-hidden="true"><span>vor ${tageZahl} Tagen</span><span>heute</span></span>
</div>
<p class="dienst__info" id="info-${d.id}" aria-live="polite"></p></div>`;
}).join('')}</div>`).join('');
verlaufBedienen(box);
/* Wartung und Historie */
const w = $('[data-feld="wartung"]', root);
const wt = jetzt + 8 * TAG;
w.innerHTML = `<li><time class="mono" datetime="${iso(wt)}">${tagFmt.format(wt)}<br>22:00–23:30</time><div><h3>Telefonanlage: neue Version beim Anbieter</h3><p>Anrufe brechen in dieser Zeit zweimal für je etwa 2 Minuten ab. Die Mailbox nimmt weiter auf.</p></div></li>`;
const h = $('[data-feld="historie"]', root);
const e1 = jetzt - 6 * TAG, e2 = jetzt - 12 * TAG;
h.innerHTML = `<li><time class="mono" datetime="${iso(e1)}">${tagFmt.format(e1)}</time><div><h3>E-Mail: Versand 31 Minuten verzögert</h3><p>Ursache: überlastetes Relay beim Anbieter. Behoben um 14:52.</p></div></li><li><time class="mono" datetime="${iso(e2)}">${tagFmt.format(e2)}</time><div><h3>VPN: 18 Minuten keine Anmeldung</h3><p>Zertifikat zu spät erneuert. Wir erneuern jetzt 30 Tage vorher automatisch.</p></div></li>`;
}
function fakten(beginn, letztes, naechstes, dienste, wort) {
const namen = { mail: 'E-Mail und Kalender', vpn: 'VPN-Zugang', fernwartung: 'Fernwartung', telefon: 'Telefonanlage' };
return `<dl class="fakten mono"><dt>Beginn</dt><dd><time datetime="${iso(beginn)}">${zeitFmt.format(beginn)} Uhr</time> · ${relativ(beginn)}</dd>${letztes ? `<dt>Letztes Update</dt><dd><time datetime="${iso(letztes)}">${zeitFmt.format(letztes)} Uhr</time> · ${relativ(letztes).replace('vor 0 Minuten', 'gerade eben')}</dd>` : ''}<dt>${wort}</dt><dd><time datetime="${iso(naechstes)}">spätestens ${zeitFmt.format(naechstes)} Uhr</time></dd><dt>Betroffen</dt><dd class="chips">${dienste.map(d => `<span class="chip">${namen[d]}</span>`).join('')}</dd></dl>`;
}
function verlaufBedienen(box) {
$$('.verlauf', box).forEach(v => {
const li = $$('li', v), info = $('#' + v.getAttribute('aria-describedby'));
let i = li.length - 1;
const zeige = n => { li.forEach(x => x.classList.remove('aktiv')); i = Math.max(0, Math.min(li.length - 1, n)); li[i].classList.add('aktiv'); info.textContent = li[i].dataset.text; };
v.addEventListener('keydown', e => {
const m = { ArrowLeft: -1, ArrowRight: 1, Home: -1e3, End: 1e3 }[e.key];
if (m === undefined) return; e.preventDefault(); zeige(i + m);
});
v.addEventListener('focus', () => zeige(i));
v.addEventListener('blur', () => { li.forEach(x => x.classList.remove('aktiv')); info.textContent = ''; });
$('.balken', v).addEventListener('pointermove', e => { const t = e.target.closest('li'); if (t) zeige(+t.dataset.i); });
$('.balken', v).addEventListener('pointerleave', () => { if (document.activeElement !== v) { li.forEach(x => x.classList.remove('aktiv')); info.textContent = ''; } });
$('.balken', v).addEventListener('click', e => { const t = e.target.closest('li'); if (t) zeige(+t.dataset.i); });
});
}
/* ── Versorger ── */
const SPARTEN = ['Strom', 'Gas', 'Wasser', 'Fernwärme', 'Straßenlicht'];
const ORTE = [
{ sparte: 'Strom', ton: 'stoer', ort: 'Ilmbrück-Nord', plz: '29587', strassen: ['Mühlenweg 1–27', 'Am Anger', 'Kesselstraße 2–14'], status: 'Techniker vor Ort, Kabelfehler gefunden', beginn: 52, bis: 75, nur: 'stoer' },
{ sparte: 'Wasser', ton: 'warn', ort: 'Oberdorf', plz: '29587', strassen: ['Lindenstraße 30–48'], status: 'Wasser trüb nach Rohrspülung, kurz laufen lassen', beginn: 140, bis: 40, nur: 'stoer' },
{ sparte: 'Wasser', ton: 'wartung', ort: 'Ilmbrück-Mitte', plz: '29585', strassen: ['Marktplatz', 'Schulgasse'], status: 'Hausanschlüsse werden erneuert, Wasser von 09:00 bis 13:00 abgestellt', beginn: 30, bis: 180, nur: 'wartung' },
{ sparte: 'Straßenlicht', ton: 'warn', ort: 'Bruchhausen', plz: '29589', strassen: ['Feldweg', 'Zum Sportplatz'], status: 'Leuchten aus, Reparatur am Donnerstag', beginn: 1440 * 2, bis: 1440 * 2, nur: 'immer' }
];
let suchwort = '';
function versorger() {
const root = $('[data-welt="versorger"]');
const orte = ORTE.filter(o => o.nur === 'immer' || o.nur === zustand);
const strom = orte.find(o => o.sparte === 'Strom');
if (zustand === 'weg') gesamtSetzen(root, 'unbekannt', 'Störungsliste gerade nicht abrufbar.', 'Bitte rufen Sie bei Ausfall die Störungsnummer an.');
else if (zustand === 'stoer') gesamtSetzen(root, 'stoer', 'Stromausfall in Ilmbrück-Nord.', 'Rund 140 Haushalte sind betroffen. Wir rechnen damit, dass der Strom bis ' + zeitFmt.format(jetzt + strom.bis * MIN) + ' Uhr zurück ist.');
else if (zustand === 'wartung') gesamtSetzen(root, 'wartung', 'Geplante Arbeiten am Wassernetz.', 'In Ilmbrück-Mitte ist das Wasser heute bis 13:00 Uhr abgestellt. Sonst läuft alles.');
else gesamtSetzen(root, 'ok', 'Strom, Gas und Wasser laufen.', 'Uns ist keine Unterbrechung im Netz bekannt. Nur einzelne Straßenleuchten sind aus.');
$('[data-feld="sparten"]', root).innerHTML = SPARTEN.map(s => {
const o = orte.filter(x => x.sparte === s);
const ton = zustand === 'weg' ? 'unbekannt' : (o.find(x => x.ton === 'stoer') ? 'stoer' : o.find(x => x.ton === 'wartung') ? 'wartung' : o.length ? 'warn' : 'ok');
const wort = { ok: 'ohne Störung', stoer: 'Ausfall', warn: 'eingeschränkt', wartung: 'Arbeiten', unbekannt: 'unbekannt' }[ton];
return `<li data-ton="${ton}"><b>${s}</b><span class="zustand">${SYM[ton]}${wort}</span></li>`;
}).join('');
orteZeigen(root, orte);
}
function orteZeigen(root, orte) {
const q = suchwort.trim().toLowerCase();
const treffer = zustand === 'weg' ? [] : orte.filter(o => !q || o.ort.toLowerCase().includes(q) || o.plz.startsWith(q) || o.strassen.some(s => s.toLowerCase().includes(q)));
$('[data-feld="orte-zahl"]', root).textContent = zustand === 'weg' ? 'nicht abrufbar' : treffer.length + (treffer.length === 1 ? ' Meldung' : ' Meldungen');
const ul = $('[data-feld="orte"]', root);
if (!treffer.length) {
const li = document.createElement('li'); li.className = 'leer';
const p = document.createElement('p');
p.textContent = zustand === 'weg' ? 'Die Liste ist gerade nicht erreichbar.' : (q ? 'Für „' + suchwort.trim() + '“ ist uns keine Störung bekannt.' : 'Keine Meldungen.');
li.append(p);
li.insertAdjacentHTML('beforeend', '<p style="margin-top:8px">Ist bei Ihnen trotzdem etwas ausgefallen?</p><ol><li>Prüfen Sie Sicherungen und FI-Schalter im Haus.</li><li>Fragen Sie Nachbarn, ob sie auch betroffen sind.</li><li>Rufen Sie die Störungsnummer <a href="#">05823 0 4410</a> an.</li></ol>');
ul.replaceChildren(li);
} else {
ul.innerHTML = treffer.map(o => `<li class="ort" data-ton="${o.ton}"><div><span class="marker">${SYM[o.ton]}${o.sparte} · ${{ stoer: 'Ausfall', warn: 'eingeschränkt', wartung: 'geplant' }[o.ton]}</span><h3 style="margin-top:8px">${esc(o.ort)} <span class="mono" style="font-weight:400;color:var(--leise)">${o.plz}</span></h3><p>${esc(o.strassen.join(', '))}</p><p style="color:var(--text);margin-top:4px">${esc(o.status)}</p></div><p class="mono">seit ${o.beginn > 1440 ? datumFmt.format(jetzt - o.beginn * MIN) : zeitFmt.format(jetzt - o.beginn * MIN)}<br>${o.ton === 'wartung' ? 'bis 13:00' : o.bis > 1440 ? 'Reparatur Do' : 'voraussichtlich bis ' + zeitFmt.format(jetzt + o.bis * MIN)}</p></li>`).join('');
}
const st = $('[data-suche-status]'); st.textContent = q ? $('[data-feld="orte-zahl"]', root).textContent + ' für „' + suchwort.trim() + '“' : '';
}
$('[data-suche]').addEventListener('submit', e => { e.preventDefault(); suchwort = $('#suche-feld').value; versorger(); });
$('#suche-feld').addEventListener('input', e => { suchwort = e.target.value; const root = $('[data-welt="versorger"]'); orteZeigen(root, ORTE.filter(o => o.nur === 'immer' || o.nur === zustand)); });
/* ── Praxis ── */
let leisteZu = false;
function praxis() {
const ort = $('[data-feld="leiste-ort"]');
const tel = $('[data-feld="praxis-tel"]');
const sichtbar = (zustand === 'stoer' || zustand === 'wartung') && !leisteZu && !behoben;
if ((zustand === 'stoer' || zustand === 'wartung') && !behoben) tel.innerHTML = zustand === 'stoer' ? '<span>Telefon <s class="mono">0591 0 66 31 20</s></span><em>gestört</em>' : '<span>Telefon <span class="mono">0591 0 66 31 20</span></span><em>12:00–12:30 Wartung</em>';
else tel.innerHTML = '<span>Telefon <span class="mono">0591 0 66 31 20</span></span>';
if (!sichtbar) { ort.innerHTML = ''; return; }
const beginn = jetzt - 41 * MIN;
const stoer = zustand === 'stoer';
ort.innerHTML = `<section class="leiste" data-ton="${stoer ? 'stoer' : 'wartung'}" aria-labelledby="leiste-h"><div class="leiste__innen">
<svg class="leiste__symbol" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true">${stoer ? '<path d="M5 4h4l2 5-2.5 1.5a11 11 0 0 0 5 5L15 13l5 2v4a2 2 0 0 1-2 2A16 16 0 0 1 3 6a2 2 0 0 1 2-2M3 3l18 18"/>' : '<path d="M14.5 5.5a4 4 0 0 0-5 5L4 16l4 4 5.5-5.5a4 4 0 0 0 5-5L16 12l-4-4z"/>'}</svg>
<div>
<h2 id="leiste-h">${stoer ? 'Unser Telefon ist gestört. So erreichen Sie uns:' : 'Heute 12:00 bis 12:30 ist unser Telefon wegen Wartung nicht erreichbar.'}</h2>
${stoer ? `<ul class="wege">
<li><a href="#"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><rect x="3" y="5" width="18" height="16" rx="2"/><path d="M3 10h18M8 3v4M16 3v4"/></svg>Termin online</a></li>
<li><a href="#"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><rect x="3" y="5" width="18" height="14" rx="2"/><path d="m3 7 9 6 9-6"/></svg>praxis@kiefernweg-lingen.de</a></li>
<li><a href="#"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><rect x="7" y="2" width="10" height="20" rx="2"/><path d="M11 18h2"/></svg>Ausweichnummer <span class="mono">0170 0 44 82 61</span></a></li>
</ul>
<p class="notfall">Außerhalb der Sprechzeiten: ärztlicher Bereitschaftsdienst <b class="mono">116 117</b>. Im Notfall: <b class="mono">112</b>.</p>` : '<p>Termine buchen Sie wie immer online. Im Notfall: <b class="mono">112</b>.</p>'}
<p class="leiste__zeit mono">${stoer ? `Seit <time datetime="${iso(beginn)}">${zeitFmt.format(beginn)} Uhr</time> · Letztes Update <time datetime="${iso(jetzt - 6 * MIN)}">${zeitFmt.format(jetzt - 6 * MIN)} Uhr</time>: Der Anbieter arbeitet daran.` : 'Angekündigt vom IT-Dienstleister der Praxis.'}</p>
</div>
<button class="leiste__zu" type="button" aria-label="Hinweis ausblenden"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="M6 6l12 12M18 6 6 18"/></svg></button>
</div></section>`;
const zu = $('.leiste__zu', ort);
zu.addEventListener('click', () => {
const l = $('.leiste', ort);
const fertig = () => { leisteZu = true; praxis(); $('.p-marke').focus(); };
if (reduziert()) return fertig();
l.classList.add('raus'); l.addEventListener('animationend', fertig, { once: true });
});
}
/* ── Steuerung ── */
function zeichnen() {
jetzt = Date.now();
$$('.welt').forEach(w => w.hidden = w.dataset.welt !== variante);
if (variante === 'systemhaus') systemhaus();
else if (variante === 'versorger') versorger();
else praxis();
$$('[data-v]').forEach(b => b.setAttribute('aria-pressed', String(b.dataset.v === variante)));
$$('[data-z]').forEach(b => b.setAttribute('aria-pressed', String(b.dataset.z === zustand)));
$('[data-update]').hidden = !(variante === 'systemhaus' && zustand === 'stoer');
}
function ansage(t) { const a = $('[data-ansage]'); a.textContent = ''; setTimeout(() => a.textContent = t, 30); }
function setzeVariante(v, still) {
variante = ['systemhaus', 'versorger', 'praxis'].includes(v) ? v : 'systemhaus';
const q = new URLSearchParams(location.search); q.set('v', variante);
if (!still) history.replaceState(null, '', location.pathname + '?' + q.toString().replace(/=(?=&|$)/g, '') + location.hash);
zeichnen();
}
function setzeZustand(z) {
zustand = z; behoben = false; extraUpdates = []; leisteZu = false;
zeichnen();
ansage({ ok: 'Alles läuft', stoer: 'Störung', wartung: 'Wartung', weg: 'Statusquelle nicht erreichbar' }[z]);
}
function update() {
if (behoben) return;
if (extraUpdates.length >= 1) { behoben = true; zeichnen(); ansage('Störung behoben'); return; }
extraUpdates.push({ art: 'Update', t: Date.now(), text: 'Die letzten wartenden E-Mails gehen jetzt raus. Wir melden uns, sobald alles zugestellt ist.', neu: true });
zeichnen(); ansage('Neues Update zur Störung');
}
$$('[data-v]').forEach(b => b.addEventListener('click', () => setzeVariante(b.dataset.v)));
$$('[data-z]').forEach(b => b.addEventListener('click', () => setzeZustand(b.dataset.z)));
$('[data-update]').addEventListener('click', update);
/* Automatisch neu laden (abschaltbar, WCAG 2.2.2) */
const sch = $('[data-auto]');
function autoStart() { clearInterval(autoTimer); if (auto) autoTimer = setInterval(() => { if (!H.classList.contains('vorfuehrung')) zeichnen(); }, 60000); }
sch.addEventListener('click', () => { auto = !auto; sch.setAttribute('aria-checked', String(auto)); autoStart(); });
autoStart();
/* Abo */
const form = $('[data-abo]'), feld = form.email, fehler = $('#abo-fehler'), meldung = $('[data-abo-meldung]');
let beruehrt = false;
function pruefe() {
const v = feld.value.trim(); let f = '';
if (!v) f = 'Bitte geben Sie Ihre E-Mail-Adresse ein.';
else if (!/^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(v)) f = 'Bitte prüfen Sie die Adresse, z. B. name@firma.de.';
else if (!form.querySelectorAll('input[name="d"]:checked').length) f = 'Bitte wählen Sie mindestens einen Dienst.';
fehler.textContent = f; feld.setAttribute('aria-invalid', String(!!f && f.includes('Adresse')));
return !f;
}
feld.addEventListener('blur', () => { beruehrt = true; if (feld.value) pruefe(); });
feld.addEventListener('input', () => { if (beruehrt) pruefe(); });
form.addEventListener('submit', e => {
e.preventDefault(); meldung.textContent = '';
if (!pruefe()) { feld.focus(); return; }
const knopf = form.querySelector('button[type="submit"]'); knopf.disabled = true; knopf.textContent = 'Wird gesendet …';
setTimeout(() => { knopf.disabled = false; knopf.textContent = 'Abonnieren'; meldung.textContent = 'Fast fertig: Wir haben eine E-Mail an ' + feld.value.trim() + ' geschickt. Bitte klicken Sie dort auf „Bestätigen“.'; }, reduziert() ? 50 : 700);
});
addEventListener('resize', () => { if (variante === 'systemhaus') systemhaus(); }, { passive: true });
/* Start */
const qv = new URLSearchParams(location.search).get('v');
setzeVariante(qv, true);
let schleife = null, schritt = 0;
function neustart() { clearTimeout(schleife); H.classList.remove('vorfuehrung'); setzeZustand('stoer'); }
const FOLGE = {
systemhaus: [() => setzeZustand('ok'), () => setzeZustand('stoer'), () => update(), () => update()],
versorger: [() => setzeZustand('ok'), () => setzeZustand('stoer'), () => { suchwort = 'Mühlen'; $('#suche-feld').value = suchwort; versorger(); }, () => { suchwort = ''; $('#suche-feld').value = ''; setzeZustand('wartung'); }],
praxis: [() => setzeZustand('ok'), () => setzeZustand('stoer'), () => setzeZustand('wartung')]
};
function vorfuehren() {
clearTimeout(schleife); H.classList.add('vorfuehrung');
const f = FOLGE[variante];
f[schritt % f.length](); schritt++;
schleife = setTimeout(vorfuehren, 1700);
}
function pause() { clearTimeout(schleife); }
function weiter() { vorfuehren(); }
})();
</script>
</body>
</html>