Standort-Detailseite
Eine Seite je Filiale: Öffnungsstatus von jetzt groß, Route und Anrufen, eigene Lageskizze mit Zwei-Klick-Karte, Wochenzeiten mit Sonderzeiten, Anfahrt, Parken und Zugang mit Maßen, was es nur hier gibt, Nachbarstandorte mit Entfernung und LocalBusiness-JSON-LD.
Fitness, Werkstatt, Praxisverbund: Kern- und Servicezeiten getrennt, Parken auf dem Hof, Aufzug und Geräte mit Maßen.
Eine eigene Seite je Filiale oder Standort. Oben stehen Name, Adresse und der Öffnungsstatus von jetzt („Geöffnet bis 22 Uhr“), dazu Anrufen und Route. Darunter die Wochenzeiten mit Sonderzeiten, die Anfahrt mit Bus, Rad und Auto, Parken mit Zahl und Kosten, Barrierefreiheit vor Ort mit konkreten Maßen (Stufen, Türbreite, WC, Aufzug) und was es nur an diesem Standort gibt. Die Karte ist eine eigene Lageskizze; die Anbieter-Karte lädt erst nach Klick. Jede Seite hat ihren eigenen LocalBusiness-Block.
Wer einen Standort sucht, will drei Dinge sofort: Hat er jetzt offen, wie komme ich hin, kann ich dort parken oder mit Rollstuhl hinein. Eine Seite je Standort beantwortet das an einer Adresse, die Suchmaschinen und Karten verlinken können. Google empfiehlt, jeden Standort als eigenes LocalBusiness auszuzeichnen. Die Zwei-Klick-Karte hält die Seite schnell und lädt Drittanbieter erst mit Zustimmung.
Wann einsetzen
Betriebe mit mehreren Standorten: Bäckerei, Apotheke, Optik, Fitness, Kfz, Pflege, Praxisverbund, Friseur-Kette. Auch bei einem Standort, wenn Anfahrt, Parken und Zugang erklärungsbedürftig sind (Hinterhof, Gewerbegebiet, Klinikgelände).
Wann nicht
Nicht als Kopie mit ausgetauschtem Ortsnamen: Jede Seite braucht echte Unterschiede (Zeiten, Team, Parken, Angebot), sonst lieber eine gemeinsame Seite mit standortfinder-liste. Keine Google-Maps-Einbettung beim Laden. Barrierefreiheit nicht pauschal behaupten („barrierefrei“), sondern messen und beschreiben.
Varianten
- Studio oder Werkstatt: Kern- und Servicezeiten getrennt, Parken auf dem Hof, Zugang mit Aufzug
- Filiale im Laden: Öffnungsstatus groß, Angebot nur hier, Lieferzone, Bus und Rad zuerst
- Kompakt als Teil einer Kontaktseite mit zwei Standorten nebeneinander
- Vorübergehend geschlossen: Hinweis oben mit Datum und nächstem Standort
Umsetzung
Ein Datensatz je Standort (Name, Adresse, geo, Telefon, Zeiten je Wochentag mit mehreren Spannen, Sonderzeiten, Parken, Zugang, Angebote) erzeugt sichtbaren Text und LocalBusiness-JSON-LD mit spezifischem Typ (z. B. ExerciseGym, Bakery) und parentOrganization. Status rechnet in Europe/Berlin über Intl.DateTimeFormat (wie oeffnungszeiten-status) und aktualisiert sich jede Minute; „schließt in 40 Min.“ ab einer Stunde vor Schluss. Wochenzeiten als Tabelle mit heute markiert, Sonderzeiten darüber. Anfahrt als eigene SVG-Lageskizze mit Haltestelle, Parkplatz und Eingang; die Anbieter-Karte erst nach Klick (karte-zwei-klick), Route als Link zur Karten-App mit Adresse, nicht als Einbettung. Barrierefreiheit als Liste mit Maßen und Stand-Datum. Weitere Standorte unten mit Entfernung in km aus geo, Brotkrumen Startseite › Standorte › Ort.
Robust umsetzen
Telefon. Status, Anrufen und Route ganz oben, als große Knöpfe. Wochenzeiten als Liste, heute zuerst. Lageskizze in voller Breite, darunter die Zwei-Klick-Karte. Feste Leiste unten mit Anrufen und Route.
Ohne Bewegung. Status und Karte wechseln ohne Übergang, keine Stufen-Animation.
Barrierefrei. Status als Text („Geöffnet, schließt um 22 Uhr“), nicht nur als grüner Punkt. Wochenzeiten als echte Tabelle mit th scope. Lageskizze mit Textbeschreibung der Anfahrt daneben, SVG dekorativ bzw. mit title. Kartenknopf nennt den Anbieter vor dem Laden. Barrierefreiheit vor Ort mit Maßen statt Symbolen allein.
Code
<!DOCTYPE html>
<!--
Standort-Detailseite
Fundus · Sektionen & Funktionen · https://cecodesign.de/lab/fundus/sektionen/standort-detailseite/
Eigenständige Datei. Bilder liegen hier unter bilder/ – eigene einsetzen.
-->
<html lang="de" data-v="studio">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Standort-Detailseite · Fundus-Demo</title>
<script>document.documentElement.classList.add('js');if(matchMedia('(prefers-reduced-motion: reduce)').matches)document.documentElement.classList.add('reduziert')</script>
<script type="application/ld+json" id="ld">{}</script>
<style>
@font-face { font-family: 'Anybody'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/anybody:vf@latest/latin-wdth-normal.woff2') format('woff'); font-weight: 100 900; font-stretch: 50% 150%; font-display: swap; }
@font-face { font-family: 'Funnel Sans'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/funnel-sans:vf@latest/latin-wght-normal.woff2') format('woff'); font-weight: 300 800; font-display: swap; }
@font-face { font-family: 'Azeret Mono'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/azeret-mono:vf@latest/latin-wght-normal.woff2') format('woff'); font-weight: 100 900; font-display: swap; }
@font-face { font-family: 'Fraunces'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/fraunces:vf@latest/latin-full-normal.woff2') format('woff2-variations'); font-weight: 100 900; font-style: normal; font-display: swap; }
@font-face { font-family: 'Commissioner'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/commissioner:vf@latest/latin-full-normal.woff2') format('woff2-variations'); font-weight: 100 900; font-display: swap; }
@font-face { font-family: 'Geist Mono'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/geist-mono:vf@latest/latin-wght-normal.woff2') format('woff2-variations'); font-weight: 100 900; font-display: swap; }
/* Gerüst fest, Token aus dem Rezept. Studio: fitness-studio-rhythmus */
:root {
--grund: #f2f1ec; --flaeche: #e2e1da; --karte: #f8f7f3; --text: #121412; --leise: #5e615c; --linie: #121412; --linie-fein: #d3d2ca;
--dunkel: #121412; --dunkel-text: #f2f1ec; --dunkel-leise: #b6b9b3;
--signal: #ccff33; --signal-text: #121412; --signal-rand: 2px solid #121412; --offen: #1d6b34; --zu: #b3261e;
--kurve: cubic-bezier(0.2, 0, 0.38, 0.9); --aus: cubic-bezier(0.2, 0, 1, 0.9);
--hover: 70ms; --reiter: 150ms; --panel: 240ms; --panel-zu: 150ms;
--h: 'Anybody', 'Arial Narrow', sans-serif; --h-gewicht: 800; --h-breite: 75%; --h-versal: uppercase; --h-lauf: -0.01em;
--t: 'Funnel Sans', system-ui, sans-serif; --m: 'Azeret Mono', ui-monospace, monospace;
--r: 0px; --r-knopf: 0px;
}
/* Filiale: gastro-warm-appetitlich */
:root[data-v="filiale"] {
--grund: #f0ece0; --flaeche: #e4dfcf; --karte: #f7f4ec; --text: #353839; --leise: #55595a; --linie: #353839; --linie-fein: rgba(53, 56, 57, .2);
--dunkel: #293e3a; --dunkel-text: #f0ece0; --dunkel-leise: #c3ccc4;
--signal: #293e3a; --signal-text: #f0ece0; --signal-rand: 0; --offen: #2f6b3a; --zu: #a33a2b;
--kurve: cubic-bezier(0.16, 1, 0.3, 1); --aus: cubic-bezier(0.7, 0, 0.84, 0);
--hover: 200ms; --reiter: 200ms; --panel: 400ms; --panel-zu: 250ms;
--h: 'Fraunces', Georgia, serif; --h-gewicht: 520; --h-breite: 100%; --h-versal: none; --h-lauf: -0.01em;
--t: 'Commissioner', system-ui, sans-serif; --m: 'Commissioner', system-ui, sans-serif;
--r: 12px; --r-knopf: 999px;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
html { background: var(--grund); color: var(--text); font-family: var(--t); -webkit-font-smoothing: antialiased; }
:root:has(:focus-visible) { scroll-behavior: auto; }
body { font-size: 17px; line-height: 1.55; }
a { color: inherit; text-underline-offset: 3px; text-decoration-thickness: 1px; }
button { font: inherit; color: inherit; background: none; border: 0; cursor: pointer; }
:focus-visible { outline: 3px solid var(--text); outline-offset: 2px; }
.vh { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
.vh:focus { position: fixed; left: 12px; top: 12px; width: auto; height: auto; clip-path: none; white-space: normal; z-index: 100; padding: 10px 14px; background: #fff; color: #111; outline: 3px solid #111; }
.ziffern { font-variant-numeric: tabular-nums lining-nums; }
.huelle { max-width: 1240px; margin: 0 auto; padding: 0 clamp(18px, 3.4vw, 44px); }
.mono { font: 500 13px/1.3 var(--m); letter-spacing: .08em; text-transform: uppercase; font-variant-numeric: tabular-nums; }
:root[data-v="filiale"] .mono { font-weight: 600; font-size: 12.5px; letter-spacing: .15em; }
h1, h2, h3 { font-family: var(--h); font-weight: var(--h-gewicht); font-stretch: var(--h-breite); text-transform: var(--h-versal); letter-spacing: var(--h-lauf); line-height: .95; text-wrap: balance; }
:root[data-v="filiale"] h1, :root[data-v="filiale"] h2, :root[data-v="filiale"] h3 { font-variation-settings: 'SOFT' 80, 'WONK' 0; line-height: 1.04; }
/* Kopf */
.kopf { background: var(--dunkel); color: var(--dunkel-text); }
.kopf .huelle { display: flex; align-items: center; gap: 24px; min-height: 64px; }
.marke { display: flex; align-items: center; gap: 10px; text-decoration: none; margin-right: auto; }
.marke svg { width: 30px; height: 30px; flex: none; }
.marke b { font: var(--h-gewicht) 21px/1 var(--h); font-stretch: var(--h-breite); text-transform: var(--h-versal); }
.kopf nav ul { display: flex; gap: 22px; list-style: none; }
.kopf nav a { text-decoration: none; font-size: 15px; color: var(--dunkel-leise); transition: color var(--hover) var(--kurve); }
.kopf nav a:hover, .kopf nav a[aria-current] { color: var(--dunkel-text); }
.kopf nav a[aria-current] { text-decoration: underline; text-decoration-thickness: 2px; text-underline-offset: 6px; }
.signal { display: inline-flex; align-items: center; gap: 8px; min-height: 44px; padding: 0 16px; background: var(--signal); color: var(--signal-text); border: var(--signal-rand); border-radius: var(--r-knopf); font-weight: 700; font-size: 15.5px; text-decoration: none; white-space: nowrap; transition: transform var(--hover) var(--kurve), box-shadow var(--hover) var(--kurve); }
.signal:hover { transform: translate(-2px, -2px); box-shadow: 4px 4px 0 var(--text); }
:root[data-v="filiale"] .signal:hover { transform: none; box-shadow: none; background: #1d2d2a; }
.kopf .signal { border: 0; }
:root[data-v="filiale"] .kopf .signal { background: #eac16e; color: #1d2d2a; }
.kopf :focus-visible { outline-color: var(--dunkel-text); }
.krumen { padding: 16px 0 0; font-size: 14.5px; color: var(--leise); }
.krumen ol { display: flex; flex-wrap: wrap; list-style: none; }
.krumen li + li::before { content: '/'; margin: 0 9px; }
.krumen [aria-current] { color: var(--text); }
/* Einstieg */
.einstieg { display: grid; grid-template-columns: minmax(0, 1.05fr) minmax(0, 1fr); gap: clamp(24px, 4vw, 64px); padding: clamp(18px, 2.6vw, 34px) 0 clamp(34px, 4vw, 56px); align-items: start; }
.einstieg .mono { color: var(--leise); }
h1 { font-size: clamp(3rem, 1.4rem + 6.2vw, 7rem); margin-top: 10px; overflow-wrap: anywhere; hyphens: manual; }
:root[data-v="filiale"] h1 { font-size: clamp(2.8rem, 1.5rem + 4.4vw, 5.4rem); }
h1.takt { animation: takt 480ms steps(6, jump-none) both; }
@keyframes takt { from { font-stretch: 50%; } to { font-stretch: 75%; } }
:root[data-v="filiale"] h1.takt { animation: auftauchen 700ms var(--kurve) both; }
@keyframes auftauchen { from { opacity: 0; transform: translateY(10px); } }
.adresse { font-size: 18px; margin-top: 14px; }
.status { display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 4px 14px; margin-top: 24px; padding: 18px 20px; background: var(--karte); border: 2px solid var(--linie); border-radius: var(--r); align-items: center; }
:root[data-v="filiale"] .status { border-width: 1px; border-color: var(--linie-fein); }
.status .punkt { width: 14px; height: 14px; background: var(--offen); grid-row: span 2; align-self: start; margin-top: 8px; }
:root[data-v="filiale"] .status .punkt { border-radius: 50%; }
.status[data-zustand="zu"] .punkt { background: var(--zu); }
.status[data-zustand="bald"] .punkt { background: #c98a00; }
.status strong { font: var(--h-gewicht) clamp(1.6rem, 1.2rem + 1.4vw, 2.3rem)/1 var(--h); font-stretch: var(--h-breite); text-transform: var(--h-versal); }
.status span { color: var(--leise); font-size: 15.5px; }
.status span b { color: var(--text); }
.handlungen { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 18px; }
.zweit { display: inline-flex; align-items: center; gap: 8px; min-height: 48px; padding: 0 16px; border: 2px solid var(--linie); border-radius: var(--r-knopf); font-weight: 700; font-size: 15.5px; text-decoration: none; transition: background-color var(--hover) var(--kurve), color var(--hover) var(--kurve); }
:root[data-v="filiale"] .zweit { border-width: 1.5px; }
.zweit:hover { background: var(--text); color: var(--grund); }
.zweit svg, .signal svg { width: 18px; height: 18px; flex: none; }
.handlungen .signal { min-height: 48px; }
.geschlossen-hinweis { display: none; margin-top: 18px; padding: 14px 16px; background: #fff1ef; border-left: 4px solid var(--zu); font-size: 15.5px; }
:root[data-pause] .geschlossen-hinweis { display: block; }
/* Lageskizze und Zwei-Klick-Karte */
.lage { position: relative; border: 2px solid var(--linie); border-radius: var(--r); overflow: hidden; background: var(--karte); }
:root[data-v="filiale"] .lage { border-width: 1px; border-color: var(--linie-fein); }
.lage .skizze, .lage .anbieter { display: block; width: 100%; height: auto; aspect-ratio: 4 / 3; }
.lage .anbieter { position: absolute; inset: 0; opacity: 0; visibility: hidden; transition: opacity var(--panel-zu) var(--aus), visibility 0s linear var(--panel-zu); }
.lage.geladen .anbieter { opacity: 1; visibility: visible; transition: opacity var(--panel) var(--kurve), visibility 0s; }
.lage figcaption { display: flex; flex-wrap: wrap; gap: 6px 16px; padding: 10px 14px; border-top: 1px solid var(--linie-fein); font-size: 13.5px; color: var(--leise); }
.lage figcaption i { display: inline-block; width: 10px; height: 10px; margin-right: 6px; vertical-align: -1px; }
.zweiklick { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 10px 16px; align-items: center; padding: 14px 16px; background: var(--dunkel); color: var(--dunkel-text); font-size: 14.5px; }
:root[data-v="filiale"] .zweiklick { border-radius: 0 0 var(--r) var(--r); }
.zweiklick p { color: var(--dunkel-leise); }
.zweiklick p b { color: var(--dunkel-text); }
.zweiklick button { min-height: 44px; padding: 0 14px; border: 2px solid var(--dunkel-text); border-radius: var(--r-knopf); font-weight: 700; color: var(--dunkel-text); white-space: nowrap; transition: background-color var(--hover) var(--kurve), color var(--hover) var(--kurve); }
.zweiklick button:hover { background: var(--dunkel-text); color: var(--dunkel); }
.zweiklick :focus-visible { outline-color: var(--dunkel-text); }
.zweiklick .laedt { opacity: .7; }
/* Blöcke */
.bloecke { display: grid; grid-template-columns: repeat(12, minmax(0, 1fr)); gap: 0; border-top: 2px solid var(--linie); }
:root[data-v="filiale"] .bloecke { border-top-width: 1px; }
.block { grid-column: span 6; padding: 28px 28px 32px 0; border-bottom: 1px solid var(--linie-fein); }
.block:nth-child(even) { padding-left: 28px; padding-right: 0; border-left: 1px solid var(--linie-fein); }
.block.breit { grid-column: 1 / -1; padding-right: 0; }
.block > .mono { color: var(--leise); display: block; margin-bottom: 10px; }
.block h2 { font-size: clamp(1.8rem, 1.3rem + 1.8vw, 2.8rem); margin-bottom: 16px; }
:root[data-v="filiale"] .block h2 { font-size: clamp(1.6rem, 1.2rem + 1.3vw, 2.3rem); }
.sonder { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 14px; }
.sonder span { padding: 5px 9px; background: var(--flaeche); font-size: 14px; border-radius: var(--r); }
table { width: 100%; border-collapse: collapse; font-size: 16px; }
th, td { text-align: left; padding: 9px 10px 9px 0; border-bottom: 1px solid var(--linie-fein); vertical-align: top; }
thead th { font: 500 12px/1.2 var(--m); letter-spacing: .08em; text-transform: uppercase; color: var(--leise); }
tbody th { font-weight: 600; width: 6.5em; }
tr.heute th, tr.heute td { background: var(--signal); color: var(--signal-text); }
:root[data-v="filiale"] tr.heute th, :root[data-v="filiale"] tr.heute td { background: #eac16e; color: #1d2d2a; }
tr.heute th { padding-left: 10px; }
tr.heute th::after { content: ' · heute'; font-weight: 400; }
td { font-variant-numeric: tabular-nums; }
.wege { list-style: none; display: grid; gap: 14px; }
.wege li { display: grid; grid-template-columns: 40px minmax(0, 1fr); gap: 12px; }
.wege .zeichen { width: 40px; height: 40px; display: grid; place-items: center; border: 2px solid var(--linie); border-radius: var(--r); font: 700 13px/1 var(--m); }
:root[data-v="filiale"] .wege .zeichen { border-width: 1px; border-radius: 50%; }
.wege b { display: block; }
.wege span { color: var(--leise); font-size: 15.5px; }
.masse { list-style: none; display: grid; }
.masse li { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 12px; padding: 10px 0; border-bottom: 1px solid var(--linie-fein); }
.masse li b { font-weight: 600; }
.masse li small { display: block; color: var(--leise); font-size: 14.5px; }
.masse li .wert { font: 600 15px/1.3 var(--m); text-align: right; white-space: nowrap; }
.masse li .wert.nein { color: var(--zu); }
.stand { margin-top: 10px; font-size: 14px; color: var(--leise); }
.nurhier { list-style: none; display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; }
.nurhier li { padding: 16px; background: var(--flaeche); border-radius: var(--r); }
.nurhier b { display: block; font: var(--h-gewicht) 1.25rem/1.1 var(--h); font-stretch: var(--h-breite); text-transform: var(--h-versal); margin-bottom: 6px; }
.nurhier span { font-size: 15.5px; }
.andere { padding-block: clamp(34px, 4vw, 56px) clamp(48px, 6vw, 88px); }
.andere h2 { font-size: clamp(1.6rem, 1.2rem + 1.4vw, 2.4rem); margin-bottom: 16px; }
.andere ul { list-style: none; display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; }
.andere a { display: grid; gap: 4px; padding: 16px; border: 2px solid var(--linie); border-radius: var(--r); text-decoration: none; transition: background-color var(--hover) var(--kurve), color var(--hover) var(--kurve); }
:root[data-v="filiale"] .andere a { border-width: 1px; border-color: var(--linie-fein); background: var(--karte); }
.andere a:hover { background: var(--text); color: var(--grund); }
.andere a b { font: var(--h-gewicht) 1.3rem/1.05 var(--h); font-stretch: var(--h-breite); text-transform: var(--h-versal); }
.andere a span { font-size: 14.5px; }
.andere .km { font: 600 13px/1 var(--m); }
.fuss { background: var(--dunkel); color: var(--dunkel-leise); font-size: 14.5px; padding: 36px 0 44px; }
.fuss .huelle { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(190px, 100%), 1fr)); gap: 20px 36px; }
.fuss b { color: var(--dunkel-text); display: block; margin-bottom: 6px; }
.fuss a { color: var(--dunkel-text); }
.fuss ul { list-style: none; display: grid; gap: 4px; }
.fuss :focus-visible { outline-color: var(--dunkel-text); }
.leiste { display: none; }
.pult { position: fixed; right: 14px; bottom: 14px; z-index: 40; width: 258px; padding: 12px; display: grid; gap: 9px; background: rgba(18, 20, 18, .95); color: #f2f1ec; box-shadow: 0 12px 34px rgba(0, 0, 0, .24); }
.pult .m { font: 500 10.5px/1.4 'Geist Mono', ui-monospace, monospace; letter-spacing: .14em; text-transform: uppercase; color: rgba(242, 241, 236, .72); }
.pult .reihe { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); border: 1px solid rgba(242, 241, 236, .24); }
.pult .reihe.vier { grid-template-columns: repeat(4, minmax(0, 1fr)); }
.pult button { padding: 8px 4px; font: 500 11px/1.2 'Geist Mono', monospace; color: rgba(242, 241, 236, .84); transition: background-color 120ms linear, color 120ms linear; }
.pult .reihe button + button { border-left: 1px solid rgba(242, 241, 236, .24); }
.pult button[aria-pressed="true"] { background: #f2f1ec; color: #121412; }
.pult .einzeln { border: 1px solid rgba(242, 241, 236, .24); text-align: left; padding: 8px 10px; display: flex; justify-content: space-between; }
.pult :focus-visible { outline: 2px solid #fff; outline-offset: 2px; }
.pult .werte { font: 400 10.5px/1.5 'Geist Mono', monospace; color: rgba(242, 241, 236, .72); }
@media (max-width: 1000px) { .kopf nav { display: none; } }
@media (max-width: 860px) {
.einstieg { grid-template-columns: minmax(0, 1fr); }
.block, .block:nth-child(even) { grid-column: 1 / -1; padding: 24px 0 28px; border-left: 0; }
.nurhier, .andere ul { grid-template-columns: minmax(0, 1fr); }
}
@media (max-width: 640px) {
.kopf .signal { display: none; }
.handlungen > * { flex: 1 1 100%; justify-content: center; }
.zweiklick { grid-template-columns: minmax(0, 1fr); }
.masse li { grid-template-columns: minmax(0, 1fr); gap: 2px; }
.masse li .wert { text-align: left; }
.leiste { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; position: fixed; left: 0; right: 0; bottom: 0; z-index: 25; padding: 10px 12px calc(10px + env(safe-area-inset-bottom)); background: var(--dunkel); }
.pult { bottom: calc(82px + env(safe-area-inset-bottom)); } /* über der Leiste, sonst verdeckt „Steuerung +“ den Hauptknopf */
.leiste a { display: flex; align-items: center; justify-content: center; gap: 8px; min-height: 48px; font-weight: 700; text-decoration: none; border-radius: var(--r-knopf); }
.leiste a:first-child { border: 2px solid var(--dunkel-text); color: var(--dunkel-text); }
.leiste a:last-child { background: var(--signal); color: var(--signal-text); }
:root[data-v="filiale"] .leiste a:last-child { background: #eac16e; color: #1d2d2a; }
.fuss { padding-bottom: 100px; }
}
.vorschau body { overflow: hidden; }
.vorschau .leiste { display: none; }
.reduziert *, .reduziert *::before, .reduziert *::after { animation: none !important; transition-duration: .01ms !important; }
@media (forced-colors: active) {
button, [role=button], [role=tab], [role=switch], input, select, textarea, .signal, .zweit { border: 1px solid ButtonText !important; }
tr.heute th, tr.heute td { forced-color-adjust: none; background: Highlight; color: HighlightText; }
.status .punkt { forced-color-adjust: none; }
}
</style>
</head>
<body>
<a class="vh" href="#titel">Zum Inhalt springen</a>
<header class="kopf"><div class="huelle">
<a class="marke" href="#" id="marke"></a>
<nav aria-label="Hauptmenü"><ul id="nav"></ul></nav>
<a class="signal" href="#" id="kopf-signal"></a>
</div></header>
<main>
<div class="huelle">
<nav class="krumen" aria-label="Brotkrumen"><ol><li><a href="#">Startseite</a></li><li><a href="#" id="krume-liste">Standorte</a></li><li><span aria-current="page" id="krume"></span></li></ol></nav>
<div class="einstieg">
<div>
<p class="mono" id="kennung"></p>
<h1 id="titel" tabindex="-1"></h1>
<p class="adresse" id="adresse"></p>
<div class="status" id="status" role="status" aria-live="polite"><span class="punkt" aria-hidden="true"></span><strong id="status-kopf"></strong><span id="status-detail"></span></div>
<p class="geschlossen-hinweis" id="geschlossen-hinweis"></p>
<div class="handlungen" id="handlungen"></div>
</div>
<figure class="lage" id="lage" aria-labelledby="lage-titel">
<div style="position:relative" id="lage-buehne"></div>
<figcaption id="lage-legende"></figcaption>
<div class="zweiklick" id="zweiklick"></div>
</figure>
</div>
<div class="bloecke" id="bloecke"></div>
</div>
<section class="huelle andere" aria-labelledby="andere-titel"><h2 id="andere-titel"></h2><ul id="andere"></ul></section>
</main>
<footer class="fuss"><div class="huelle" id="fuss"></div></footer>
<div class="leiste" id="leiste"></div>
<div class="pult" aria-label="Steuerung der Demo" role="group">
<span class="m">Standort</span>
<div class="reihe" id="pult-v">
<button type="button" data-v="studio" aria-pressed="true">Studio</button>
<button type="button" data-v="filiale" aria-pressed="false">Filiale</button>
</div>
<span class="m">Uhrzeit</span>
<div class="reihe vier" id="pult-zeit">
<button type="button" data-z="echt" aria-pressed="true">jetzt</button>
<button type="button" data-z="bald" aria-pressed="false">bald zu</button>
<button type="button" data-z="nacht" aria-pressed="false">nachts</button>
<button type="button" data-z="feiertag" aria-pressed="false">25.12.</button>
</div>
<button type="button" class="einzeln" id="pult-pause" aria-pressed="false"><span>Vorübergehend geschlossen</span><span aria-hidden="true">aus</span></button>
<p class="werte" id="pult-werte"></p>
</div>
<script>
(() => {
const h = document.documentElement;
const reduziert = () => h.classList.contains('reduziert');
const $ = (s, w = document) => w.querySelector(s);
const $$ = (s, w = document) => [...w.querySelectorAll(s)];
const TZ = 'Europe/Berlin';
const TAGE = ['Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag', 'Sonntag'];
const ICON = {
route: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" aria-hidden="true"><path d="M12 21s-7-6.2-7-11.5A7 7 0 0 1 19 9.5C19 14.8 12 21 12 21z"/><circle cx="12" cy="9.5" r="2.5"/></svg>',
tel: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.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>',
pfeil: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" aria-hidden="true"><path d="M5 12h14M13 6l6 6-6 6"/></svg>'
};
/* Ein Datensatz je Standort: Seite, Status und LocalBusiness-JSON-LD kommen daraus */
const ORTE = {
studio: {
marke: '<svg viewBox="0 0 30 30" aria-hidden="true"><rect width="30" height="30" fill="#ccff33"/><path d="M7 8v14M23 8v14M7 15h16" stroke="#121412" stroke-width="3"/><circle cx="15" cy="15" r="3.2" fill="#121412"/></svg>', markeName: 'Halle Neun',
nav: ['Kursplan', 'Standorte', 'Preise', 'Probetraining'], signal: 'Probetraining',
typ: 'ExerciseGym', kette: 'Halle Neun', name: 'Halle Neun Gera-Debschwitz', titel: 'Gera-Debschwitz', kennung: 'Standort 03 von 04 · seit 2019',
strasse: 'Zwötzener Straße 18', plz: '07548', ort: 'Gera', tel: '0365 773 90 30', telLink: '+493657739030', geo: [50.86154, 12.08716], url: 'https://halle-neun.de/standorte/gera-debschwitz',
zeiten: { spalten: ['Mitglieder', 'Team vor Ort'], tage: [[['06:00', '23:00'], ['09:00', '21:00']], [['06:00', '23:00'], ['09:00', '21:00']], [['06:00', '23:00'], ['09:00', '21:00']], [['06:00', '23:00'], ['09:00', '21:00']], [['06:00', '23:00'], ['09:00', '21:00']], [['08:00', '20:00'], ['10:00', '16:00']], [['08:00', '20:00'], ['10:00', '16:00']]] },
sonder: [['12-24', '06:00', '14:00', '24.12. 6 bis 14 Uhr'], ['12-25', null, null, '25. und 26.12. geschlossen'], ['12-26', null, null, null], ['12-31', '06:00', '16:00', '31.12. 6 bis 16 Uhr']],
zweitLabel: 'Team vor Ort', pauseText: 'Vom 17. bis 23. November ist die Halle wegen neuer Böden geschlossen. Dein Vertrag ruht in der Zeit nicht: Du trainierst kostenlos in Gera-Lusan, 3,1 km entfernt.',
handlungen: '<a class="signal" href="#">Probetraining hier' + ICON.pfeil + '</a><a class="zweit" href="#" data-route>' + ICON.route + 'Route</a><a class="zweit" href="tel:+493657739030">' + ICON.tel + 'Anrufen</a>',
skizze: 'studio', legende: [['#ccff33', 'Halle Neun, Eingang Hof'], ['#121412', 'Straßenbahn Linie 3'], ['#8a8d86', 'Parkplatz im Hof']],
anbieter: 'Kartendienst', bloecke: (z) => [
{ code: '01', t: 'Öffnungszeiten', breit: false, html: z },
{ code: '02', t: 'Anfahrt', html: `<ul class="wege"><li><span class="zeichen">3</span><span><b>Straßenbahn Linie 3, Haltestelle Debschwitz</b><span>2 Minuten zu Fuß, 9 Minuten vom Hauptbahnhof</span></span></li><li><span class="zeichen">Rad</span><span><b>Elsterradweg, Abfahrt Zwötzener Straße</b><span>16 überdachte Bügel am Eingang, eine Ladestation für E-Bikes</span></span></li><li><span class="zeichen">B2</span><span><b>Mit dem Auto über die B 2</b><span>Einfahrt in den Hof neben dem Getränkemarkt</span></span></li></ul>` },
{ code: '03', t: 'Parken', html: `<ul class="masse"><li><span><b>Hofparkplatz</b><small>2 Stunden frei mit Mitgliedskarte, danach 1 € je Stunde</small></span><span class="wert">42 Plätze</span></li><li><span><b>Mit Ladesäule</b><small>11 kW, Bezahlung per App des Betreibers</small></span><span class="wert">2 Plätze</span></li><li><span><b>Behindertenparkplatz</b><small>direkt vor dem Eingang, 3,50 m breit</small></span><span class="wert">3 Plätze</span></li></ul>` },
{ code: '04', t: 'Zugang vor Ort', html: `<ul class="masse"><li><span><b>Eingang</b><small>stufenlos, Tür öffnet automatisch</small></span><span class="wert">1,10 m</span></li><li><span><b>Aufzug zu den Kursräumen</b><small>1. Obergeschoss, Kabine</small></span><span class="wert">1,10 × 1,40 m</span></li><li><span><b>Umkleide und WC rollstuhlgerecht</b><small>Erdgeschoss, mit Notruf</small></span><span class="wert">ja</span></li><li><span><b>Geräte ohne Umsetzen nutzbar</b><small>Zugturm, Kabel, Handkurbel</small></span><span class="wert">6</span></li></ul><p class="stand">Selbst gemessen am <time datetime="2026-10-14">14.10.2026</time>. Fehlt dir eine Angabe? Frag das Team.</p>` },
{ code: '05', t: 'Nur in Debschwitz', breit: true, html: `<ul class="nurhier"><li><b>Boxring</b><span>6 × 6 m, freie Zeit Mo bis Fr ab 20 Uhr</span></li><li><b>Kinderecke</b><span>Sa 10 bis 12 Uhr betreut, Kinder ab 3 Jahren</span></li><li><b>Sauna</b><span>Mi nur für Frauen, sonst gemischt</span></li></ul>` }
],
andereTitel: 'Andere Standorte', andere: [['Gera-Lusan', 'Werner-Petzold-Straße 4', [50.8496, 12.1018]], ['Gera-Zentrum', 'Sorge 61', [50.8781, 12.0789]], ['Zeulenroda', 'Bahnhofstraße 9', [50.6503, 11.9806]]],
fuss: '<div><b>Halle Neun</b>Fitness in Gera und Zeulenroda<br>Zentrale: 0365 773 90 00</div><div><b>Standorte</b>Debschwitz · Lusan · Zentrum · Zeulenroda</div><ul><li><a href="#">Vertrag kündigen</a></li><li><a href="#">Impressum</a></li><li><a href="#">Datenschutz</a></li></ul>',
leiste: '<a href="tel:+493657739030">Anrufen</a><a href="#" data-route>Route</a>',
werte: 'fitness · H1 font-stretch 50→75 % in steps(6) 480 ms · UI (0.2,0,0.38,0.9) 70/150/240 ms · Status jede Minute'
},
filiale: {
marke: '<svg viewBox="0 0 30 30" aria-hidden="true"><circle cx="15" cy="15" r="15" fill="#eac16e"/><path d="M8 18c0-5 3-8 7-8s7 3 7 8z" fill="#293e3a"/><path d="M11 14l1.5 3M15 12.5v4M19 14l-1.5 3" stroke="#eac16e" stroke-width="1.4"/></svg>', markeName: 'Backstube Larsen',
nav: ['Brot', 'Vorbestellen', 'Filialen', 'Café'], signal: 'Vorbestellen',
typ: 'Bakery', kette: 'Backstube Larsen', name: 'Backstube Larsen Tönning Hafen', titel: 'Tönning, am Hafen', kennung: 'Filiale mit Café · eine von fünf',
strasse: 'Am Hafen 12', plz: '25832', ort: 'Tönning', tel: '04861 61 04 77', telLink: '+494861610477', geo: [54.31897, 8.94263], url: 'https://backstube-larsen.de/filialen/toenning-hafen',
zeiten: { spalten: ['Laden und Café'], tage: [[['06:30', '18:00']], [['06:30', '18:00']], [['06:30', '18:00']], [['06:30', '18:00']], [['06:30', '18:00']], [['06:30', '13:00']], [['07:30', '11:00']]] },
sonder: [['12-24', '06:30', '12:00', 'Heiligabend bis 12 Uhr'], ['12-25', null, null, '1. und 2. Weihnachtstag zu'], ['12-26', null, null, null], ['12-31', '06:30', '13:00', 'Silvester bis 13 Uhr']],
zweitLabel: null, pauseText: 'Bis 20. November bauen wir den Ofen um. Brot und Kaffee gibt es in der Zeit in der Filiale am Markt, 400 Meter die Straße hoch.',
handlungen: '<a class="signal" href="#">Für morgen vorbestellen' + ICON.pfeil + '</a><a class="zweit" href="#" data-route>' + ICON.route + 'Route</a><a class="zweit" href="tel:+494861610477">' + ICON.tel + 'Anrufen</a>',
skizze: 'filiale', legende: [['#eac16e', 'Backstube Larsen'], ['#7f937a', 'Eider und Hafenbecken'], ['#293e3a', 'Bus Linie 1064']],
anbieter: 'Kartendienst', bloecke: (z) => [
{ code: 'Zeiten', t: 'Geöffnet', html: z },
{ code: 'Anfahrt', t: 'So kommen Sie her', html: `<ul class="wege"><li><span class="zeichen">Bus</span><span><b>Haltestelle Tönning Hafen</b><span>Linie 1064 aus Husum, dann 60 Meter am Kai entlang</span></span></li><li><span class="zeichen">Rad</span><span><b>Eiderradweg führt am Laden vorbei</b><span>Bügel für 8 Räder vor dem Café</span></span></li><li><span class="zeichen">P</span><span><b>Hafenparkplatz, 80 Meter</b><span>gebührenpflichtig, die ersten 30 Minuten frei</span></span></li></ul>` },
{ code: 'Zugang', t: 'Zugang und Plätze', html: `<ul class="masse"><li><span><b>Eingang</b><small>eine Stufe, mobile Rampe: bitte klingeln</small></span><span class="wert">9 cm</span></li><li><span><b>Café-Tische unterfahrbar</b><small>zwei Tische am Fenster</small></span><span class="wert">ja</span></li><li><span><b>WC barrierefrei</b><small>das nächste liegt im Packhaus, 50 Meter</small></span><span class="wert nein">nein</span></li><li><span><b>Plätze im Café</b><small>drinnen, draußen am Kai bei gutem Wetter</small></span><span class="wert">24 + 16</span></li></ul><p class="stand">Gemessen am <time datetime="2026-09-30">30.09.2026</time>.</p>` },
{ code: 'Nur hier', t: 'Nur am Hafen', html: `<ul class="masse"><li><span><b>Frühstück am Kai</b><small>bis 11 Uhr, sonntags bis zum Schluss</small></span><span class="wert"></span></li><li><span><b>Krabbenbrötchen</b><small>Do bis Sa, solange da</small></span><span class="wert"></span></li><li><span><b>Abholfach für Vorbestellungen</b><small>ab 6:30 Uhr, auch vor dem Öffnen des Cafés</small></span><span class="wert"></span></li></ul>` }
],
andereTitel: 'Unsere anderen Filialen', andere: [['Tönning, am Markt', 'Markt 3', [54.31726, 8.94823]], ['Garding', 'Norderstraße 20', [54.33063, 8.78139]], ['Friedrichstadt', 'Prinzenstraße 7', [54.37522, 9.08688]]],
fuss: '<div><b>Backstube Larsen</b>Seit 1958 in Tönning<br>Backstube: Am Hafen 12</div><div><b>Filialen</b>Hafen · Markt · Garding · Friedrichstadt · St. Peter</div><ul><li><a href="#">Allergene</a></li><li><a href="#">Impressum</a></li><li><a href="#">Datenschutz</a></li></ul>',
leiste: '<a href="tel:+494861610477">Anrufen</a><a href="#" data-route>Route</a>',
werte: 'gastro · H1 700 ms (0.16,1,0.3,1), 10 px · UI 200 ms · Karte 400 ms, zu 250 ms (0.7,0,0.84,0)'
}
};
function skizze(art) {
if (art === 'studio') return `<svg class="skizze" viewBox="0 0 400 300" role="img" aria-labelledby="lage-titel"><title id="lage-titel">Lageskizze: Halle Neun im Hof an der Zwötzener Straße, Straßenbahn-Haltestelle Debschwitz zwei Minuten entfernt, Parkplatz im Hof</title>
<rect width="400" height="300" fill="#e2e1da"/>
<path d="M-10 238 C90 214 150 262 250 236 S380 206 410 222" stroke="#b9c8c2" stroke-width="22" fill="none"/>
<text x="300" y="250" font-family="Azeret Mono" font-size="9" fill="#5e615c" letter-spacing="1">WEISSE ELSTER</text>
<path d="M0 120H400" stroke="#f8f7f3" stroke-width="16"/><path d="M168 0V300" stroke="#f8f7f3" stroke-width="12"/>
<path d="M0 120H400" stroke="#121412" stroke-width="2.5" stroke-dasharray="1 0"/>
<text x="10" y="138" font-family="Azeret Mono" font-size="9" fill="#121412" letter-spacing="1">ZWÖTZENER STR.</text>
<rect x="104" y="104" width="34" height="32" fill="#121412"/><text x="121" y="125" font-family="Azeret Mono" font-size="13" font-weight="700" fill="#ccff33" text-anchor="middle">3</text>
<text x="98" y="150" font-family="Azeret Mono" font-size="8.5" fill="#121412">DEBSCHWITZ</text>
<rect x="186" y="138" width="190" height="80" fill="#cfcec6"/><g fill="#8a8d86">${[0, 1, 2, 3, 4, 5, 6, 7].map(i => `<rect x="${194 + i * 22}" y="182" width="16" height="28"/>`).join('')}</g>
<text x="196" y="174" font-family="Azeret Mono" font-size="9" fill="#121412">P 42</text>
<rect x="236" y="24" width="140" height="76" fill="#ccff33" stroke="#121412" stroke-width="2.5"/>
<text x="250" y="58" font-family="Anybody" font-weight="800" font-stretch="75%" font-size="22" fill="#121412">HALLE NEUN</text>
<text x="250" y="78" font-family="Azeret Mono" font-size="8.5" fill="#121412">EINGANG HOF ↓</text>
<path d="M138 130 C150 142 160 150 186 150 S 240 120 262 104" stroke="#121412" stroke-width="2" stroke-dasharray="3 5" fill="none"/>
<text x="140" y="170" font-family="Azeret Mono" font-size="8" fill="#5e615c">2 MIN</text>
<g transform="translate(16 16)"><path d="M0 18 6 0l6 18-6-5z" fill="#121412"/><text x="6" y="30" font-family="Azeret Mono" font-size="8" text-anchor="middle">N</text></g>
</svg>`;
return `<svg class="skizze" viewBox="0 0 400 300" role="img" aria-labelledby="lage-titel"><title id="lage-titel">Lageskizze: Backstube Larsen am Kai des Hafenbeckens, Bushaltestelle Tönning Hafen 60 Meter entfernt, Hafenparkplatz 80 Meter</title>
<rect width="400" height="300" fill="#e4dfcf"/>
<path d="M0 210 C120 190 220 230 400 196 V300 H0z" fill="#7f937a" opacity=".55"/>
<rect x="120" y="120" width="170" height="70" rx="10" fill="#7f937a" opacity=".75"/>
<text x="150" y="162" font-family="Commissioner" font-size="11" letter-spacing="2" fill="#f0ece0">HAFENBECKEN</text>
<text x="24" y="262" font-family="Commissioner" font-size="11" letter-spacing="2" fill="#293e3a">EIDER</text>
<path d="M0 96 H400" stroke="#f7f4ec" stroke-width="14"/><path d="M104 0 V300" stroke="#f7f4ec" stroke-width="10"/>
<text x="300" y="86" font-family="Commissioner" font-size="10" fill="#353839">AM HAFEN</text>
<rect x="196" y="40" width="96" height="58" rx="10" fill="#eac16e"/><text x="244" y="66" font-family="Fraunces" font-size="14" fill="#293e3a" text-anchor="middle">Larsen</text><text x="244" y="84" font-family="Commissioner" font-size="8.5" fill="#293e3a" text-anchor="middle" letter-spacing="1">CAFÉ · LADEN</text>
<circle cx="140" cy="96" r="13" fill="#293e3a"/><text x="140" y="100" font-family="Commissioner" font-size="9" font-weight="700" fill="#f0ece0" text-anchor="middle">BUS</text>
<rect x="306" y="110" width="60" height="44" rx="6" fill="#d6d0bd"/><text x="336" y="137" font-family="Commissioner" font-size="13" font-weight="700" fill="#353839" text-anchor="middle">P</text>
<path d="M154 100 C170 104 186 104 196 96" stroke="#293e3a" stroke-width="2" stroke-dasharray="3 5" fill="none"/>
<g transform="translate(372 20)"><path d="M0 18 6 0l6 18-6-5z" fill="#353839"/><text x="6" y="30" font-family="Commissioner" font-size="8" text-anchor="middle">N</text></g>
</svg>`;
}
function anbieterKarte() { // nachgebildet: im Einsatz das iframe des Kartendienstes
let wege = ''; for (let i = 0; i < 14; i++) { const y = 10 + i * 22 + (i % 3) * 4; wege += `<path d="M-10 ${y} Q 200 ${y + (i % 2 ? 18 : -14)} 410 ${y + 6}" stroke="#fff" stroke-width="${i % 4 ? 2 : 5}" fill="none"/>`; }
for (let i = 0; i < 12; i++) { const x = 8 + i * 36; wege += `<path d="M${x} -10 L ${x + (i % 2 ? 24 : -18)} 310" stroke="#fff" stroke-width="${i % 5 ? 2 : 4}" fill="none"/>`; }
return `<svg class="anbieter" viewBox="0 0 400 300" aria-hidden="true"><rect width="400" height="300" fill="#e8eae4"/>${wege}<circle cx="262" cy="70" r="9" fill="#d23f2f" stroke="#fff" stroke-width="3"/><rect x="8" y="270" width="150" height="20" fill="#fff" opacity=".8"/><text x="14" y="284" font-family="Geist Mono" font-size="9" fill="#444">Kartendienst · nachgebildet</text></svg>`;
}
/* Zeiten und Status in Europe/Berlin */
function berlinTeile(d) { const p = Object.fromEntries(new Intl.DateTimeFormat('en-CA', { timeZone: TZ, year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', hourCycle: 'h23', weekday: 'short' }).formatToParts(d).map(x => [x.type, x.value])); return { j: +p.year, m: p.month, t: p.day, min: +p.hour * 60 + +p.minute, wt: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'].indexOf(p.weekday) }; }
const zuMin = s => { const [a, b] = s.split(':').map(Number); return a * 60 + b; };
const uhr = s => s.replace(/^0/, '').replace(':00', '') + ' Uhr';
let zeitModus = 'echt', pauseAn = false;
function jetztTeile() {
const t = berlinTeile(new Date());
if (zeitModus === 'bald') { const sp = ORTE[v].zeiten.tage[t.wt][0]; t.min = zuMin(sp[1]) - 25; }
if (zeitModus === 'nacht') { t.min = 3 * 60 + 10; }
if (zeitModus === 'feiertag') { t.m = '12'; t.t = '25'; t.wt = (new Date(Date.UTC(t.j, 11, 25)).getUTCDay() + 6) % 7; t.min = 10 * 60; }
return t;
}
function spanneHeute(o, t, spalte = 0) {
const sd = o.sonder.find(s => s[0] === `${t.m}-${t.t}`);
if (sd) return sd[1] ? [sd[1], sd[2], sd[3]] : [null, null, sd[3] || 'Feiertag'];
return o.zeiten.tage[t.wt][spalte] || [null, null];
}
function naechsteOeffnung(o, t) {
const basis = Date.UTC(t.j, +t.m - 1, +t.t, 12);
for (let i = 1; i < 9; i++) {
const d = new Date(basis + i * 864e5), md = `${String(d.getUTCMonth() + 1).padStart(2, '0')}-${String(d.getUTCDate()).padStart(2, '0')}`, wt = (d.getUTCDay() + 6) % 7;
const sd = o.sonder.find(s => s[0] === md), sp = sd ? (sd[1] ? [sd[1], sd[2]] : null) : o.zeiten.tage[wt][0];
if (sp) return (i === 1 ? 'morgen' : TAGE[wt]) + ' ab ' + uhr(sp[0]);
}
return '';
}
function status() {
const o = ORTE[v], t = jetztTeile(), el = $('#status');
if (pauseAn) { el.dataset.zustand = 'zu'; $('#status-kopf').textContent = 'Vorübergehend zu'; $('#status-detail').innerHTML = 'Umbau, Details unten'; return; }
const [a, b, notiz] = spanneHeute(o, t);
let kopf, detail, z;
if (a && t.min >= zuMin(a) && t.min < zuMin(b)) {
const rest = zuMin(b) - t.min;
z = rest <= 60 ? 'bald' : 'offen';
kopf = rest <= 60 ? `Offen, noch ${rest} Min.` : `Offen bis ${uhr(b)}`;
if (o.zweitLabel) { const z2 = o.zeiten.tage[t.wt][1]; detail = z2 && t.min >= zuMin(z2[0]) && t.min < zuMin(z2[1]) ? `${o.zweitLabel} bis <b>${uhr(z2[1])}</b>` : `${o.zweitLabel} gerade nicht da, Check-in mit Karte`; }
else detail = `Heute ${uhr(a)} bis ${uhr(b)}`;
if (notiz) detail = notiz;
} else {
z = 'zu';
kopf = 'Gerade geschlossen';
detail = a && t.min < zuMin(a) ? `Öffnet heute um <b>${uhr(a)}</b>` : `${notiz ? notiz + '. ' : ''}Öffnet <b>${naechsteOeffnung(o, t)}</b>`;
}
el.dataset.zustand = z; $('#status-kopf').textContent = kopf; $('#status-detail').innerHTML = detail;
}
function zeitenTabelle(o, t) {
const sonder = o.sonder.filter(s => s[3]).map(s => `<span>${s[3]}</span>`).join('');
const kopf = o.zeiten.spalten.length > 1 ? `<thead><tr><th scope="col">Tag</th>${o.zeiten.spalten.map(s => `<th scope="col">${s}</th>`).join('')}</tr></thead>` : '';
const zeilen = o.zeiten.tage.map((sp, i) => `<tr${i === t.wt ? ' class="heute" aria-current="date"' : ''}><th scope="row">${TAGE[i]}</th>${sp.map(x => `<td>${x ? uhr(x[0]).replace(' Uhr', '') + '–' + uhr(x[1]) : 'geschlossen'}</td>`).join('')}</tr>`).join('');
return `<div class="sonder" aria-label="Sonderzeiten">${sonder}</div><table><caption class="vh">Öffnungszeiten ${o.titel}</caption>${kopf}<tbody>${zeilen}</tbody></table>`;
}
const km = (a, b) => { const R = 6371, r = x => x * Math.PI / 180, dLa = r(b[0] - a[0]), dLo = r(b[1] - a[1]); const s = Math.sin(dLa / 2) ** 2 + Math.cos(r(a[0])) * Math.cos(r(b[0])) * Math.sin(dLo / 2) ** 2; return 2 * R * Math.asin(Math.sqrt(s)); };
const kmText = x => x < 1 ? `${Math.round(x * 1000 / 50) * 50} m` : `${x.toFixed(1).replace('.', ',')} km`;
function jsonLd(o) {
const tageEn = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'];
const ohs = []; o.zeiten.tage.forEach((sp, i) => { const x = sp[0]; if (!x) return; const g = ohs.find(e => e.opens === x[0] && e.closes === x[1]); if (g) g.dayOfWeek.push(tageEn[i]); else ohs.push({ '@type': 'OpeningHoursSpecification', dayOfWeek: [tageEn[i]], opens: x[0], closes: x[1] }); });
const jahr = new Date().getFullYear();
o.sonder.forEach(s => ohs.push({ '@type': 'OpeningHoursSpecification', opens: s[1] || '00:00', closes: s[2] || '00:00', validFrom: `${jahr}-${s[0]}`, validThrough: `${jahr}-${s[0]}` }));
return { '@context': 'https://schema.org', '@type': o.typ, name: o.name, url: o.url, telephone: o.telLink, address: { '@type': 'PostalAddress', streetAddress: o.strasse, postalCode: o.plz, addressLocality: o.ort, addressCountry: 'DE' }, geo: { '@type': 'GeoCoordinates', latitude: o.geo[0], longitude: o.geo[1] }, openingHoursSpecification: ohs, parentOrganization: { '@type': 'Organization', name: o.kette } };
}
let v = new URLSearchParams(location.search).get('v');
if (!ORTE[v]) v = 'studio';
let ladeTimer = null;
function zeichne(anim) {
const o = ORTE[v], t = jetztTeile();
h.dataset.v = v;
document.title = `${o.name} · Fundus-Demo`;
$('#marke').innerHTML = `${o.marke}<b>${o.markeName}</b>`; $('#marke').setAttribute('aria-label', `${o.markeName}, Startseite`);
$('#nav').innerHTML = o.nav.map(n => `<li><a href="#"${/Standorte|Filialen/.test(n) ? ' aria-current="page"' : ''}>${n}</a></li>`).join('');
$('#kopf-signal').textContent = o.signal;
$('#krume-liste').textContent = v === 'filiale' ? 'Filialen' : 'Standorte';
$('#krume').textContent = o.titel;
$('#kennung').textContent = o.kennung;
const h1 = $('#titel'); h1.textContent = o.titel;
h1.classList.remove('takt'); if (anim && !reduziert()) { void h1.offsetWidth; h1.classList.add('takt'); }
$('#adresse').innerHTML = `${o.strasse}, <span style="white-space:nowrap">${o.plz} ${o.ort}</span> · <a href="tel:${o.telLink}" class="ziffern" style="white-space:nowrap">${o.tel}</a>`;
$('#geschlossen-hinweis').textContent = o.pauseText;
$('#handlungen').innerHTML = o.handlungen;
$('#lage-buehne').innerHTML = skizze(o.skizze) + anbieterKarte();
$('#lage').classList.remove('geladen');
$('#lage-legende').innerHTML = o.legende.map(([c, n]) => `<span><i style="background:${c}"></i>${n}</span>`).join('');
zweiKlick(false);
$('#bloecke').innerHTML = o.bloecke(zeitenTabelle(o, t)).map(b => `<section class="block${b.breit ? ' breit' : ''}" aria-labelledby="b-${b.code}"><span class="mono">${b.code}</span><h2 id="b-${b.code}">${b.t}</h2>${b.html}</section>`).join('');
$('#andere-titel').textContent = o.andereTitel;
$('#andere').innerHTML = o.andere.map(([n, s, g]) => `<li><a href="#"><span class="km">${kmText(km(o.geo, g))}</span><b>${n}</b><span>${s}</span></a></li>`).join('');
$('#fuss').innerHTML = o.fuss;
$('#leiste').innerHTML = o.leiste;
$$('[data-route]').forEach(a => { a.href = `https://www.openstreetmap.org/directions?to=${o.geo[0]}%2C${o.geo[1]}`; a.setAttribute('rel', 'noopener'); a.setAttribute('aria-label', `Route zu ${o.name} in der Karten-App`); a.addEventListener('click', e => e.preventDefault()); });
$('#ld').textContent = JSON.stringify(jsonLd(o));
$('#pult-werte').textContent = o.werte;
status();
}
function zweiKlick(geladen, laedt) {
const z = $('#zweiklick');
if (geladen) { z.innerHTML = `<p><b>Karte geladen.</b> Daten gehen jetzt an den ${ORTE[v].anbieter}.</p><button type="button" id="karte-weg">Karte entfernen</button>`; $('#karte-weg').onclick = () => { $('#lage').classList.remove('geladen'); zweiKlick(false); $('#karte-laden').focus(); }; return; }
z.innerHTML = `<p><b>Interaktive Karte?</b> Lädt erst nach Klick. Dabei gehen Daten wie Ihre IP-Adresse an den ${ORTE[v].anbieter}.</p><button type="button" id="karte-laden"${laedt ? ' class="laedt" aria-busy="true"' : ''}>${laedt ? 'Lädt …' : 'Karte laden'}</button>`;
$('#karte-laden').onclick = ladeKarte;
}
function ladeKarte() { zweiKlick(false, true); clearTimeout(ladeTimer); ladeTimer = setTimeout(() => { $('#lage').classList.add('geladen'); zweiKlick(true); }, reduziert() ? 0 : 520); }
function setzeV(neu, schreiben) {
v = neu;
$$('#pult-v button').forEach(b => b.setAttribute('aria-pressed', b.dataset.v === v));
if (schreiben) { const q = new URLSearchParams(location.search); q.set('v', v); history.replaceState(null, '', location.pathname + '?' + q); }
zeichne(true);
}
function setzeZeit(z) { zeitModus = z; $$('#pult-zeit button').forEach(b => b.setAttribute('aria-pressed', b.dataset.z === z)); zeichne(false); }
$$('#pult-v button').forEach(b => b.addEventListener('click', () => setzeV(b.dataset.v, true)));
$$('#pult-zeit button').forEach(b => b.addEventListener('click', () => setzeZeit(b.dataset.z)));
$('#pult-pause').addEventListener('click', () => { pauseAn = !pauseAn; const b = $('#pult-pause'); b.setAttribute('aria-pressed', pauseAn); b.lastElementChild.textContent = pauseAn ? 'an' : 'aus'; h.toggleAttribute('data-pause', pauseAn); status(); });
const q0 = new URLSearchParams(location.search);
if (q0.get('zeit')) zeitModus = q0.get('zeit');
if (q0.has('pause')) { pauseAn = true; h.setAttribute('data-pause', ''); }
setzeV(v, false);
if (q0.has('karte')) { $('#lage').classList.add('geladen'); zweiKlick(true); }
setInterval(status, 60000);
/* Vorführung: Uhrzeiten durchspielen, Karte per Zwei-Klick laden */
let uhren = [];
const spaeter = (f, ms) => uhren.push(setTimeout(f, ms));
function stopp() { uhren.forEach(clearTimeout); uhren = []; clearTimeout(ladeTimer); }
function neustart() { stopp(); zeitModus = 'echt'; $$('#pult-zeit button').forEach(b => b.setAttribute('aria-pressed', b.dataset.z === 'echt')); zeichne(true); }
function vorfuehren() {
neustart();
h.classList.add('vorfuehrung');
spaeter(() => setzeZeit('bald'), 1800);
spaeter(() => setzeZeit('nacht'), 3400);
spaeter(() => { setzeZeit('echt'); }, 5000);
spaeter(() => { const b = $('#karte-laden'); if (b) b.click(); }, 6200);
spaeter(() => { $('#lage').classList.remove('geladen'); zweiKlick(false); }, 9400);
spaeter(vorfuehren, 10400);
}
function pause() { stopp(); h.classList.remove('vorfuehrung'); }
function weiter() { vorfuehren(); }
})();
</script>
</body>
</html>