Ansprechperson-Band
Band am Ende einer Leistungsseite: Tischlermeisterin aus Uelzen mit Bild, Durchwahl, Antwortzeit und Live-Status aus den Telefonzeiten, im Urlaub mit Vertretung; als Kanzlei- und Praxisvariante.
Leistungsseiten von Tischlerei, SHK, Elektro und Bau: Meisterin oder Meister mit Rückrufzeit und Vertretung.
Statt „Jetzt Kontakt aufnehmen“ steht am Ende einer Leistungsseite eine echte Person: Foto oder Platzhalter, Name, Rolle, Zuständigkeit, Direktdurchwahl, E-Mail und eine ehrliche Antwortzeit („Rückruf heute bis 16 Uhr“). Ein Status rechnet aus den Sprechzeiten, ob die Person gerade erreichbar ist, und nennt sonst den nächsten Zeitpunkt oder die Vertretung. Das Band ist für Unterseiten von Handwerk, Kanzlei und Praxis gedacht: je Leistung die passende Person.
Ein Name mit Durchwahl senkt die Hürde zum Anruf stärker als jedes Formular. Man weiß, wer abnimmt und wann man mit einer Antwort rechnen kann. Ein Foto oder ein sauberer Platzhalter macht aus dem Betrieb Menschen. Die ehrliche Antwortzeit erspart Nachfragen und zeigt, dass der Betrieb seine Zusage kennt.
Wann einsetzen
Am Ende jeder Leistungs-, Produkt- oder Rechtsgebietsseite, wenn es eine zuständige Person gibt. Handwerk (Meisterin für Bäder), Kanzlei (Fachanwalt für Arbeitsrecht mit Sekretariat), Praxis (Rezeption mit Telefonzeiten), Industrie (Anwendungstechnik), Pflege (Pflegedienstleitung).
Wann nicht
Nicht, wenn die Person nicht zustimmt (Foto und Name nur mit Einwilligung, vom Kunden prüfen lassen). Nicht mit einer Antwortzeit, die der Betrieb nicht hält. Nicht als Ersatz für Notdienst- oder Notfallhinweise: In der Praxis steht bei Notfällen 112 und der ärztliche Bereitschaftsdienst 116 117 vor der Person. Nicht bei großen Teams, in denen niemand fest zuständig ist; dann zentrale Nummer.
Varianten
- Handwerk: Meisterin mit Foto, Durchwahl, Rückrufzeit und Vertretung im Urlaub
- Kanzlei: Fachanwalt mit Sekretariat, Rückruf-Zeitfenster und Hinweis zur Erstberatung
- Praxis: Rezeption mit Telefonzeiten, Online-Termin als Nebenweg und Notfallhinweis zuerst
Umsetzung
- Ein <aside aria-labelledby> am Ende der Leistungsseite, nicht im Footer.
- Foto als <img> mit Alternativtext „Porträt von …“, Seitenverhältnis 4 : 5, fest per aspect-ratio; ohne Foto ein Platzhalter mit Initialen in der Markenfarbe (kein Silhouetten-Icon).
- Name als Überschrift, darunter Rolle und Zuständigkeit in einem Satz.
- Durchwahl als tel:-Link mit E.164 im href und DIN-5008-Schreibweise im Text, E-Mail als mailto:-Link mit Betreff zur Leistung.
- Sprechzeiten als Daten (Wochentag, von, bis); ein kleines Skript rechnet in Europe/Berlin, ob jetzt erreichbar, und schreibt Status und nächsten Zeitpunkt in eine Zeile mit role="status". Ohne JS stehen die Sprechzeiten als Text.
- Antwortzeit als Satz, den der Betrieb hält („Rückruf am selben Werktag bei Anruf vor 14 Uhr“).
- Abwesenheit: Datum bis und Vertretung mit eigener Durchwahl.
- Layout per Container Query: breit als Band mit Foto links, schmal als Karte.
- Einwilligung für Foto und Namen vom Kunden prüfen lassen.
Robust umsetzen
Telefon. Als Karte: Foto oben im Format 4 : 5 bis höchstens 40 % der Bildschirmhöhe, darunter Name, Status und zwei große Knöpfe „Anrufen“ und „E-Mail“ über die volle Breite. Die Nummer bleibt als Text sichtbar.
Ohne Bewegung. Status und Foto erscheinen ohne Übergang.
Barrierefrei. Eigene Landmarke (aside) mit Namen. Statuszeile als Text, nicht nur als Farbpunkt; Wechsel per role="status". Links mit sichtbarer Nummer, kein „Hier klicken“. Platzhalter-Initialen sind aria-hidden, der Name steht als Text daneben. Kontrast 4,5 : 1 auch im Status.
Code
<!DOCTYPE html>
<!--
Ansprechperson-Band
Fundus · Hero & Section-Layouts · https://cecodesign.de/lab/fundus/hero-layouts/ansprechperson-band/
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>Ansprechperson-Band · 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: 'Special Gothic'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/special-gothic:vf@latest/latin-standard-normal.woff2') format('woff2-variations'); font-weight: 400 700; font-stretch: 75% 125%; font-display: swap; }
@font-face { font-family: 'Atkinson Hyperlegible Next'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/atkinson-hyperlegible-next:vf@latest/latin-wght-normal.woff2') format('woff2-variations'); font-weight: 200 800; font-display: swap; }
@font-face { font-family: 'Newsreader'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/newsreader:vf@latest/latin-opsz-normal.woff2') format('woff2-variations'); font-weight: 200 800; font-style: normal; font-display: swap; }
@font-face { font-family: 'Schibsted Grotesk'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/schibsted-grotesk:vf@latest/latin-wght-normal.woff2') format('woff2-variations'); font-weight: 400 900; font-display: swap; }
@font-face { font-family: '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; }
@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; }
/* Grundvariante: Rezept handwerk-werkstatt-klartext. Kanzlei und Praxis überschreiben nur die Token. */
:root {
--grund: #EFEDEA; --flaeche: #D3CDC2; --holz: #C3B4A5; --salbei: #BDC1A9; --text: #1A1A1A; --leise: #4F4B45; --linie: #D3CDC2;
--signal: #FB6100; --signal-text: #1A1A1A; --offen: #175A30; --warten: #6E4700; --zu: #4F4B45;
--headline: 'Special Gothic', system-ui, sans-serif; --schrift: 'Atkinson Hyperlegible Next', system-ui, sans-serif;
--r-klein: 4px; --r-gross: 8px; --r-knopf: 4px;
--ein: cubic-bezier(0, 0, 0.38, 0.9); --aus: cubic-bezier(0.2, 0, 1, 0.9); --kurve: cubic-bezier(0.2, 0, 0.38, 0.9);
--t-hover: 150ms; --t-status: 240ms; --t-bild: 400ms;
}
body[data-v="kanzlei"] {
--grund: #F9F8F8; --flaeche: #EFEBEB; --holz: #1E1E50; --salbei: #EFEBEB; --text: #212529; --leise: #6B5D50; --linie: #D2CDC7;
--signal: #1E1E50; --signal-text: #fff; --akzent: #00BEE6;
--headline: 'Newsreader', Georgia, serif; --schrift: 'Schibsted Grotesk', system-ui, sans-serif;
--r-klein: 0; --r-gross: 0; --r-knopf: 0;
--ein: cubic-bezier(0.25, 1, 0.5, 1); --aus: cubic-bezier(0.5, 0, 0.75, 0); --kurve: cubic-bezier(0.25, 1, 0.5, 1);
--t-hover: 125ms; --t-status: 250ms; --t-bild: 500ms;
}
body[data-v="praxis"] {
--grund: #F5F7F7; --flaeche: #E2E0DE; --holz: #005450; --salbei: #DCE7E5; --text: #363B3B; --leise: #596666; --linie: #E2E0DE;
--signal: #005450; --signal-text: #fff; --warm: #FFC776;
--headline: 'Hedvig Letters Serif', Georgia, serif; --schrift: 'Google Sans Flex', system-ui, sans-serif;
--r-klein: 12px; --r-gross: 16px; --r-knopf: 999px;
}
* { box-sizing: border-box; margin: 0; }
html { background: var(--grund); }
body { min-height: 100svh; background: var(--grund); color: var(--text); font-family: var(--schrift); -webkit-font-smoothing: antialiased; transition: background-color var(--t-bild) var(--kurve); }
:root:has(:focus-visible) { scroll-behavior: auto; }
html { scroll-padding-bottom: 96px; } /* Fokus nicht unter dem Pult */
:focus-visible { outline: 2px solid var(--text); outline-offset: 3px; }
body[data-v="kanzlei"] :focus-visible { outline-color: #0084A1; }
.label { font: 500 11px/1.3 'Geist Mono', ui-monospace, monospace; letter-spacing: .14em; text-transform: uppercase; color: var(--leise); }
.ziffern { font-variant-numeric: tabular-nums; }
[hidden] { display: none !important; }
/* ——— Seitenkontext ——— */
.kopf { display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 10px 24px; padding: 16px clamp(16px, 4vw, 48px); border-bottom: 1px solid var(--linie); }
.marke { font: 700 20px/1 var(--headline); text-decoration: none; color: var(--text); letter-spacing: -.01em; }
body[data-v="handwerk"] .marke { font-stretch: 75%; text-transform: uppercase; letter-spacing: .02em; font-size: 22px; }
body[data-v="kanzlei"] .marke { font-weight: 400; font-size: 22px; }
body[data-v="praxis"] .marke { font-weight: 400; font-size: 21px; }
.kopf nav { display: flex; gap: 20px; flex-wrap: wrap; font-size: .9375rem; }
.kopf nav a { color: var(--text); text-decoration: none; }
.kopf nav a[aria-current] { text-decoration: underline; text-underline-offset: 6px; text-decoration-thickness: 2px; }
.wrap { max-width: 1280px; margin: 0 auto; padding: 0 clamp(16px, 4vw, 48px); }
.kontext { padding: clamp(28px, 4vw, 56px) 0 clamp(24px, 3vw, 40px); display: grid; grid-template-columns: minmax(0, 1fr); gap: 12px 40px; }
@media (min-width: 900px) { .kontext { grid-template-columns: minmax(0, 5fr) minmax(0, 7fr); } }
.kontext h1 { font: 700 clamp(2rem, 1.4rem + 2.4vw, 3.25rem)/1.05 var(--headline); letter-spacing: -.01em; text-wrap: balance; hyphens: manual; }
body[data-v="kanzlei"] .kontext h1, body[data-v="praxis"] .kontext h1 { font-weight: 400; line-height: 1.1; }
.kontext ol { list-style: none; padding: 0; counter-reset: s; display: grid; gap: 0; }
.kontext li { counter-increment: s; display: grid; grid-template-columns: 2.2em minmax(0, 1fr); gap: 8px; padding: 12px 0; border-bottom: 1px solid var(--linie); font-size: 1.0625rem; line-height: 1.5; }
.kontext li::before { content: counter(s, decimal-leading-zero); font: 500 12px/1.9 'Geist Mono', monospace; color: var(--leise); }
/* ——— Das Band ——— */
.band { container: band / inline-size; margin: 8px 0 120px; }
.band-innen { display: grid; grid-template-columns: minmax(0, 1fr); gap: 0; background: var(--flaeche); border-radius: var(--r-gross); overflow: hidden; }
.bild { position: relative; aspect-ratio: 4 / 5; background: var(--salbei); max-height: 40svh; width: 100%; }
.bild img, .bild svg { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; display: block; }
.bild img, .bild svg.foto { animation: bild-ein var(--t-bild) var(--ein) both; }
@keyframes bild-ein { from { opacity: 0; } }
.initialen { position: absolute; inset: 0; display: grid; place-items: center; background: var(--holz); color: var(--grund); font: 400 clamp(3.5rem, 14cqi, 7rem)/1 var(--headline); letter-spacing: .02em; }
body[data-v="handwerk"] .initialen { color: var(--text); font-weight: 700; font-stretch: 75%; }
.initialen small { position: absolute; left: 14px; bottom: 12px; font: 500 10.5px/1.3 'Geist Mono', monospace; letter-spacing: .12em; text-transform: uppercase; opacity: .85; }
.person { padding: clamp(20px, 3cqi, 36px); display: grid; gap: 14px; align-content: start; min-width: 0; }
.person h2 { font: 700 clamp(1.75rem, 1.2rem + 2.4cqi, 2.75rem)/1.02 var(--headline); letter-spacing: -.01em; }
body[data-v="handwerk"] .person h2 { font-stretch: 85%; }
body[data-v="kanzlei"] .person h2, body[data-v="praxis"] .person h2 { font-weight: 400; line-height: 1.08; }
.rolle { font-size: 1.0625rem; line-height: 1.5; max-width: 46ch; text-wrap: pretty; }
.status { display: flex; align-items: center; gap: 10px; font-weight: 600; font-size: 1rem; line-height: 1.35; min-height: 1.35em; }
.status i { flex: none; width: 10px; height: 10px; border-radius: 50%; background: var(--zu); box-shadow: 0 0 0 4px color-mix(in srgb, currentColor 12%, transparent); transition: background-color var(--t-status) var(--kurve); }
.status[data-zustand="offen"] { color: var(--offen); } .status[data-zustand="offen"] i { background: var(--offen); }
.status[data-zustand="warten"] { color: var(--warten); } .status[data-zustand="warten"] i { background: var(--warten); }
.status[data-zustand="zu"], .status[data-zustand="urlaub"] { color: var(--zu); }
.status span { animation: status-ein var(--t-status) var(--ein) both; }
@keyframes status-ein { from { opacity: 0; translate: 0 4px; } }
.aktionen { padding: clamp(20px, 3cqi, 36px); padding-top: 0; display: grid; gap: 10px; align-content: start; min-width: 0; }
.knopf { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; text-decoration: none; border-radius: var(--r-knopf); padding: 15px 18px; font: 700 1.0625rem/1.2 var(--schrift); transition: background-color var(--t-hover) var(--kurve), color var(--t-hover) var(--kurve), box-shadow var(--t-hover) var(--kurve); }
.knopf.anruf { background: var(--signal); color: var(--signal-text); }
.knopf.anruf:hover { background: color-mix(in srgb, var(--signal) 86%, #000); }
.knopf.anruf b { font: 700 1.25rem/1 var(--headline); font-variant-numeric: tabular-nums; }
body[data-v="handwerk"] .knopf.anruf b { font-stretch: 85%; }
body[data-v="kanzlei"] .knopf.anruf b, body[data-v="praxis"] .knopf.anruf b { font-family: var(--schrift); font-weight: 700; }
.knopf.mail { color: var(--text); box-shadow: inset 0 0 0 1.5px var(--text); }
.knopf.mail:hover { background: rgba(0, 0, 0, .05); }
.knopf small { font: 500 11px/1.2 'Geist Mono', monospace; letter-spacing: .1em; text-transform: uppercase; opacity: .9; }
.fakten { margin-top: 6px; display: grid; gap: 0; font-size: .9375rem; line-height: 1.45; }
.fakten div { display: grid; grid-template-columns: 8.5em minmax(0, 1fr); gap: 12px; padding: 10px 0; border-top: 1px solid color-mix(in srgb, var(--text) 16%, transparent); }
.fakten dt { color: var(--leise); }
.fakten dd .z { white-space: nowrap; }
.fakten a { color: inherit; text-underline-offset: 3px; }
.zustaendig { display: flex; flex-wrap: wrap; gap: 6px; list-style: none; padding: 0; margin-top: 4px; }
.zustaendig li { font-size: .875rem; padding: 6px 10px; border-radius: var(--r-klein); background: color-mix(in srgb, var(--grund) 70%, transparent); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--text) 14%, transparent); }
.vertretung { background: var(--grund); border-radius: var(--r-klein); padding: 14px 16px; display: grid; gap: 6px; font-size: .9375rem; line-height: 1.45; animation: status-ein var(--t-status) var(--ein) both; }
.vertretung a { color: var(--text); font-weight: 700; font-variant-numeric: tabular-nums; }
.notfall { display: flex; gap: 12px; align-items: flex-start; background: var(--warm, #FFC776); color: #2B2B2B; border-radius: var(--r-klein); padding: 14px 16px; font-size: .9375rem; line-height: 1.45; margin-bottom: 14px; }
.notfall b { font-weight: 700; }
.notfall a { color: inherit; font-weight: 700; font-variant-numeric: tabular-nums; }
/* Breit: Band. Grundvariante 3 / 5 / 4, Kanzlei Monogramm klein und Spalten, Praxis Karte mit Bild oben rechts */
@container band (min-width: 46rem) {
.band-innen { grid-template-columns: minmax(0, 3fr) minmax(0, 5fr) minmax(0, 4fr); }
.bild { max-height: none; height: 100%; aspect-ratio: auto; min-height: 380px; }
.aktionen { padding-top: clamp(20px, 3cqi, 36px); padding-left: 0; }
}
body[data-v="kanzlei"] .band-innen { background: transparent; border-top: 1px solid var(--text); border-bottom: 1px solid var(--linie); }
@container band (min-width: 46rem) {
body[data-v="kanzlei"] .band-innen { grid-template-columns: minmax(0, 2fr) minmax(0, 6fr) minmax(0, 4fr); }
body[data-v="kanzlei"] .bild { min-height: 0; height: auto; aspect-ratio: 4 / 5; margin: clamp(20px, 3cqi, 36px) 0 clamp(20px, 3cqi, 36px); }
body[data-v="praxis"] .band-innen { grid-template-columns: minmax(0, 8fr) minmax(0, 4fr); grid-template-areas: 'person bild' 'aktionen bild'; }
body[data-v="praxis"] .person { grid-area: person; } body[data-v="praxis"] .aktionen { grid-area: aktionen; padding-left: clamp(20px, 3cqi, 36px); padding-top: 0; }
body[data-v="praxis"] .bild { grid-area: bild; height: auto; min-height: 0; aspect-ratio: 4 / 5; align-self: start; margin: clamp(20px, 3cqi, 36px); width: auto; border-radius: var(--r-klein); overflow: hidden; }
}
body[data-v="kanzlei"] .person { border-left: 0; }
body[data-v="kanzlei"] .knopf.mail { box-shadow: inset 0 0 0 1px var(--text); }
body[data-v="praxis"] .band-innen { background: #fff; box-shadow: 0 0 0 1px var(--linie); }
/* Pult */
.pult { position: fixed; right: 16px; bottom: 16px; z-index: 30; display: grid; gap: 6px; padding: 8px; border-radius: 10px; background: #1A1A1A; color: #EDEBE6; box-shadow: 0 10px 30px rgba(0, 0, 0, .25); max-width: calc(100vw - 32px); }
.pult .reihe { display: flex; flex-wrap: wrap; gap: 3px; align-items: center; }
.pult .label { color: #B8B3AB; padding: 0 6px; min-width: 5.4em; }
.pult button { font: 500 10.5px/1 'Geist Mono', monospace; letter-spacing: .1em; text-transform: uppercase; color: #EDEBE6; background: transparent; border: 0; border-radius: 6px; padding: 8px 9px; cursor: pointer; transition: background-color 150ms var(--kurve); }
.pult button:hover { background: rgba(255, 255, 255, .1); }
.pult button[aria-pressed="true"] { background: #EDEBE6; color: #1A1A1A; }
.pult button:focus-visible, .pult label:focus-within { outline: 2px solid #FB6100; outline-offset: 2px; }
.pult label { font: 500 10.5px/1 'Geist Mono', monospace; letter-spacing: .1em; text-transform: uppercase; padding: 8px 9px; border-radius: 6px; cursor: pointer; position: relative; }
.pult label:hover { background: rgba(255, 255, 255, .1); }
.pult input[type=file] { position: absolute; inset: 0; opacity: 0; width: 100%; cursor: pointer; }
.vorschau body { overflow: hidden; }
.vorschau .kontext ol, .vorschau .kopf nav { display: none; }
.vorschau .kontext { padding-bottom: 12px; }
:root.reduziert * { transition-duration: .01ms !important; animation-duration: .01ms !important; }
@media (hover: none) { .knopf { padding-block: 17px; } }
@media (max-width: 360px) { .fakten div { grid-template-columns: minmax(0, 1fr); gap: 2px; } }
@media (forced-colors: active) {
button, [role=button], [role=tab], [role=switch], input, select, textarea { border: 1px solid ButtonText !important; }
.knopf { border: 1px solid LinkText; } .status i { forced-color-adjust: none; }
}
</style>
</head>
<body data-v="handwerk">
<header class="kopf">
<a class="marke" href="#band" id="marke">Tischlerei Brandtner</a>
<nav aria-label="Hauptnavigation" id="hauptnav"></nav>
</header>
<div class="wrap">
<section class="kontext" aria-labelledby="h-kontext">
<div><p class="label" id="k-label"></p><h1 id="h-kontext"></h1></div>
<ol id="k-liste"></ol>
</section>
<aside class="band" id="band" aria-labelledby="p-name">
<div class="band-innen">
<div class="bild" id="bild"></div>
<div class="person">
<div id="p-notfall"></div>
<p class="label" id="p-label"></p>
<h2 id="p-name"></h2>
<p class="rolle" id="p-rolle"></p>
<p class="status" id="status" role="status"><i aria-hidden="true"></i><span></span></p>
<ul class="zustaendig" id="p-themen" aria-label="Zuständig für"></ul>
</div>
<div class="aktionen" id="aktionen"></div>
</div>
</aside>
</div>
<div class="pult" role="group" aria-label="Steuerung der Demo">
<div class="reihe" role="group" aria-label="Variante"><span class="label">Variante</span>
<button type="button" data-v="handwerk" aria-pressed="true">Handwerk</button>
<button type="button" data-v="kanzlei" aria-pressed="false">Kanzlei</button>
<button type="button" data-v="praxis" aria-pressed="false">Praxis</button>
</div>
<div class="reihe" role="group" aria-label="Zeitpunkt"><span class="label">Zeit</span>
<button type="button" data-zeit="jetzt" aria-pressed="true">Jetzt</button>
<button type="button" data-zeit="offen" aria-pressed="false">Erreichbar</button>
<button type="button" data-zeit="pause" aria-pressed="false">Pause</button>
<button type="button" data-zeit="zu" aria-pressed="false">Abend</button>
<button type="button" data-zeit="urlaub" aria-pressed="false">Urlaub</button>
</div>
<div class="reihe" role="group" aria-label="Bild"><span class="label">Bild</span>
<button type="button" data-bild="bild" aria-pressed="true">Bild</button>
<button type="button" data-bild="initialen" aria-pressed="false">Initialen</button>
<label>Eigenes Foto<input type="file" accept="image/*" id="eigen"></label>
</div>
</div>
<script>
(() => {
const H = document.documentElement;
const reduziert = () => H.classList.contains('reduziert');
const $ = s => document.querySelector(s);
const NB = ' ', BIS = '–';
/* Gezeichnete Platzhalter-Porträts (eigene SVG, keine Fotos) */
const PORTRAET = {
handwerk: `<svg class="foto" viewBox="0 0 400 500" preserveAspectRatio="xMidYMid slice" aria-hidden="true"><rect width="400" height="500" fill="#BDC1A9"/><rect x="0" y="330" width="400" height="170" fill="#C3B4A5"/><path d="M0 330h400" stroke="#A8977F" stroke-width="2"/><path d="M60 500c6-92 52-138 140-142 88 4 134 50 140 142z" fill="#2F3A33"/><path d="M168 360l32 44 32-44" fill="#EFEDEA"/><path d="M118 420c18-30 46-46 82-50l-10 130h-70z" fill="#3A4840"/><rect x="176" y="292" width="48" height="74" rx="20" fill="#C98F70"/><ellipse cx="200" cy="232" rx="66" ry="80" fill="#D8A283"/><path d="M134 236c-6-62 26-104 70-104 46 0 72 34 66 96-12-34-40-52-70-56-26 6-48 24-66 64z" fill="#3B2A20"/><circle cx="214" cy="128" r="30" fill="#3B2A20"/><rect x="258" y="196" width="10" height="70" rx="3" transform="rotate(18 263 231)" fill="#E3C47A"/><path d="M261 262l3 10 5-9z" fill="#3B2A20" transform="rotate(18 263 231)"/></svg>`,
kanzlei: `<svg class="foto" viewBox="0 0 400 500" preserveAspectRatio="xMidYMid slice" aria-hidden="true"><rect width="400" height="500" fill="#E6E1DD"/><rect x="250" y="40" width="110" height="300" fill="#D6CFC8"/><path d="M50 500c8-98 60-146 150-150 90 4 142 52 150 150z" fill="#1E1E50"/><path d="M170 352l30 70 30-70" fill="#F4F1EE"/><rect x="178" y="290" width="44" height="70" rx="18" fill="#B98568"/><ellipse cx="200" cy="230" rx="62" ry="78" fill="#C99577"/><path d="M136 250c-14-80 20-122 66-122 48 0 76 40 64 120-6-50-30-80-64-82-34 2-56 32-66 84z" fill="#6B4A36"/><path d="M136 250c-4 40 4 80 18 100l14-40zM266 248c4 40-4 80-18 100l-14-40z" fill="#6B4A36"/></svg>`,
praxis: `<svg class="foto" viewBox="0 0 400 500" preserveAspectRatio="xMidYMid slice" aria-hidden="true"><rect width="400" height="500" fill="#DCE7E5"/><circle cx="320" cy="90" r="60" fill="#FFC776" opacity=".6"/><path d="M56 500c8-96 58-142 144-146 86 4 136 50 144 146z" fill="#F7F7F5"/><path d="M150 380c16 30 32 46 50 46s34-16 50-46" fill="none" stroke="#005450" stroke-width="6"/><rect x="178" y="292" width="44" height="72" rx="18" fill="#8E5E45"/><ellipse cx="200" cy="230" rx="64" ry="78" fill="#9E6B50"/><path d="M134 226c0-60 30-96 66-96 40 0 70 34 66 96-10-22-34-40-66-42-30 2-54 18-66 42z" fill="#1F1A18"/><path d="M134 226c-10 30-6 70 6 96l18-48zM266 226c10 30 6 70-6 96l-18-48z" fill="#1F1A18"/></svg>`
};
/* Daten je Variante. Sprechzeiten: [Wochentag 1–5, von, bis] in Minuten, Europe/Berlin */
const W = (t, a, b) => [t, a[0] * 60 + a[1], b[0] * 60 + b[1]];
const VARIANTEN = {
handwerk: {
marke: 'Tischlerei Brandtner', nav: ['Küchen', 'Einbauschränke', 'Treppen', 'Werkstatt', 'Kontakt'],
kLabel: 'Leistung · Küchen nach Maß', kTitel: 'Küchen aus eigener Werkstatt in Uelzen.', kListe: ['Aufmaß bei Ihnen zu Hause, kostenlos im Landkreis Uelzen.', 'Entwurf mit Fronten- und Arbeitsplattenmustern zum Anfassen.', 'Festpreis-Angebot innerhalb von zehn Werktagen.'],
themen: ['Küchen nach Maß', 'Aufmaß im Landkreis Uelzen', 'Arbeitsplatten aus Massivholz'],
label: 'Ihre Ansprechperson für Küchen', name: 'Thea Brandtner', kurz: 'TB',
rolle: 'Tischlermeisterin. Sie misst bei Ihnen auf, plant die Küche und ist bis zur Montage Ihre Ansprechperson.',
tel: ['0581', '04417', '21'], mail: 'thea.brandtner@tischlerei-brandtner.example', betreff: 'Küche nach Maß',
zeiten: [W(1, [7, 30], [16, 30]), W(2, [7, 30], [16, 30]), W(3, [7, 30], [16, 30]), W(4, [7, 30], [16, 30]), W(5, [7, 30], [13, 0])],
pause: [12, 0, 12, 45], pauseText: 'Gerade auf Montage', antwort: 'Rückruf am selben Werktag, wenn Sie vor 14' + NB + 'Uhr anrufen.',
urlaub: { bis: '14. November', name: 'Jonas Wiechert', rolle: 'Geselle, Aufmaß und Termine', d: '23' },
alt: 'Gezeichnetes Porträt als Platzhalter: Frau mit Haarknoten und Arbeitsjacke, Bleistift hinter dem Ohr.'
},
kanzlei: {
marke: 'Weidmann Ohlrogge Rechtsanwälte', nav: ['Arbeitsrecht', 'Erbrecht', 'Mietrecht', 'Kanzlei', 'Kontakt'],
kLabel: 'Rechtsgebiet · Arbeitsrecht', kTitel: 'Kündigung erhalten? Die Frist läuft drei Wochen.', kListe: ['Wir prüfen die Kündigung und sagen Ihnen, ob sich eine Klage lohnt.', 'Für Arbeitgeber: Abmahnung, Aufhebungsvertrag, Betriebsrat.', 'Die Kosten der Erstberatung nennen wir vor dem Termin.'],
themen: ['Kündigungsschutz', 'Aufhebungsverträge', 'Abmahnung'],
label: 'Ihre Ansprechpartnerin im Arbeitsrecht', name: 'Dr. Jana Ohlrogge', kurz: 'JO',
rolle: 'Fachanwältin für Arbeitsrecht. Termine und Unterlagen über das Sekretariat von Frau Kemal.',
tel: ['0581', '02290', '14'], mail: 'ohlrogge@weidmann-ohlrogge.example', betreff: 'Anfrage Arbeitsrecht',
zeiten: [W(1, [8, 30], [17, 0]), W(2, [8, 30], [17, 0]), W(3, [8, 30], [17, 0]), W(4, [8, 30], [17, 0]), W(5, [8, 30], [14, 0])],
pause: [12, 30, 14, 0], pauseText: 'Gerade im Termin', antwort: 'Rückruf innerhalb eines Werktags. Bei Fristen sagen Sie es dem Sekretariat gleich.',
sekretariat: ['Sekretariat, Frau Kemal', '10'],
urlaub: { bis: '14. November', name: 'Henrik Weidmann', rolle: 'Fachanwalt für Arbeitsrecht', d: '12' },
alt: 'Gezeichnetes Porträt als Platzhalter: Frau mit schulterlangem Haar im dunkelblauen Blazer.'
},
praxis: {
marke: 'Hausarztpraxis an der Ilmenau', nav: ['Sprechzeiten', 'Leistungen', 'Team', 'Anfahrt', 'Kontakt'],
kLabel: 'Leistung · Gesundheits-Check-up', kTitel: 'Check-up ab 35: alle drei Jahre, von der Kasse bezahlt.', kListe: ['Blutentnahme nüchtern am Morgen, Ergebnis nach zwei Werktagen.', 'Gespräch mit Ihrer Ärztin, etwa 20 Minuten.', 'Bitte Versichertenkarte und Medikamentenplan mitbringen.'],
themen: ['Termine Check-up', 'Vorbereitung und Nüchternheit', 'Überweisungen'],
label: 'Ihre Ansprechpartnerin am Empfang', name: 'Svenja Lindhorst', kurz: 'SL',
rolle: 'Medizinische Fachangestellte. Sie vergibt die Termine für den Check-up und beantwortet Fragen zur Vorbereitung.',
tel: ['0581', '06630', '0'], mail: 'empfang@praxis-ilmenau.example', betreff: 'Check-up',
zeiten: [W(1, [8, 0], [12, 0]), W(2, [8, 0], [12, 0]), W(3, [8, 0], [12, 0]), W(4, [8, 0], [12, 0]), W(5, [8, 0], [12, 0]), W(1, [14, 0], [17, 0]), W(2, [14, 0], [17, 0]), W(4, [14, 0], [18, 0])],
pause: null, pauseText: 'Leitung besetzt', antwort: 'E-Mails beantworten wir am nächsten Werktag. Keine Befunde per E-Mail.',
notfall: true, online: true,
urlaub: { bis: '14. November', name: 'Empfangsteam', rolle: 'gleiche Nummer, gleiche Zeiten', d: '0' },
alt: 'Gezeichnetes Porträt als Platzhalter: Frau mit dunklem Haar in heller Praxiskleidung.'
}
};
const qs = new URLSearchParams(location.search);
let variante = VARIANTEN[qs.get('v')] ? qs.get('v') : 'handwerk';
let zeit = 'jetzt', bildArt = 'bild', eigenesBild = null;
const telText = ([v, n, d]) => v + NB + n + (d ? '‑' + d : '');
const telHref = ([v, n, d]) => 'tel:+49' + v.slice(1) + n + (d || '');
const uhr = m => { const h = Math.floor(m / 60), mi = m % 60; return mi ? `${h}:${String(mi).padStart(2, '0')}` : `${h}`; };
const TAGE = ['So', 'Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa'];
/* Jetzt in Europe/Berlin */
function berlin() {
const f = new Intl.DateTimeFormat('de-DE', { timeZone: 'Europe/Berlin', weekday: 'short', hour: '2-digit', minute: '2-digit', hourCycle: 'h23' });
const p = Object.fromEntries(f.formatToParts(new Date()).map(x => [x.type, x.value]));
return { tag: TAGE.indexOf(p.weekday.replace('.', '')), min: +p.hour * 60 + +p.minute };
}
function simuliert() {
const V = VARIANTEN[variante];
if (zeit === 'offen') return { tag: 2, min: 10 * 60 };
if (zeit === 'pause') return V.pause ? { tag: 2, min: V.pause[0] * 60 + 10 } : { tag: 2, min: 13 * 60 };
if (zeit === 'zu') return { tag: 5, min: 19 * 60 };
return berlin();
}
function zustand() {
const V = VARIANTEN[variante];
if (zeit === 'urlaub') return { z: 'urlaub', text: `Im Urlaub bis ${V.urlaub.bis}. Die Vertretung ist erreichbar.` };
const { tag, min } = simuliert();
const heute = V.zeiten.filter(z => z[0] === tag).sort((a, b) => a[1] - b[1]);
const jetzt = heute.find(z => min >= z[1] && min < z[2]);
if (jetzt) {
if (V.pause && min >= V.pause[0] * 60 + V.pause[1] && min < V.pause[2] * 60 + V.pause[3]) return { z: 'warten', text: `${V.pauseText}. Wieder am Telefon ab ${uhr(V.pause[2] * 60 + V.pause[3])}${NB}Uhr.` };
return { z: 'offen', text: `Jetzt erreichbar, heute bis ${uhr(jetzt[2])}${NB}Uhr.` };
}
const spaeter = heute.find(z => z[1] > min);
if (spaeter) return { z: 'warten', text: `Telefon wieder ab ${uhr(spaeter[1])}${NB}Uhr.` };
for (let i = 1; i <= 7; i++) {
const t = (tag + i) % 7, n = V.zeiten.filter(z => z[0] === t).sort((a, b) => a[1] - b[1])[0];
if (n) return { z: 'zu', text: `Außerhalb der Telefonzeit. Wieder erreichbar ${i === 1 ? 'morgen' : TAGE[t]}, ${uhr(n[1])}${NB}Uhr.` };
}
return { z: 'zu', text: 'Zurzeit nicht erreichbar.' };
}
function zeitenText(V) {
const gruppen = {};
V.zeiten.forEach(([t, a, b]) => { const k = `<span class="z">${uhr(a)}${BIS}${uhr(b)}</span>`; (gruppen[t] = gruppen[t] || []).push(k); });
const tage = Object.keys(gruppen).map(Number).sort();
const zeilen = []; let i = 0;
while (i < tage.length) {
let j = i; while (j + 1 < tage.length && tage[j + 1] === tage[j] + 1 && gruppen[tage[j + 1]].join() === gruppen[tage[i]].join()) j++;
zeilen.push(`${TAGE[tage[i]]}${j > i ? BIS + TAGE[tage[j]] : ''} ${gruppen[tage[i]].join(' und ').replace(/<\/span>$/, NB + 'Uhr</span>')}`); i = j + 1;
}
return zeilen.join('<br>');
}
function bild() {
const V = VARIANTEN[variante], b = $('#bild');
if (bildArt === 'eigen' && eigenesBild) b.innerHTML = `<img src="${eigenesBild}" alt="Porträt von ${V.name} (Ihr Testbild)">`;
else if (bildArt === 'bild') b.innerHTML = PORTRAET[variante].replace('aria-hidden="true"', `role="img" aria-label="${V.alt}"`);
else b.innerHTML = `<div class="initialen"><span aria-hidden="true">${V.kurz}</span><small>Foto folgt</small></div>`;
}
function render() {
const V = VARIANTEN[variante];
document.body.dataset.v = variante;
$('#marke').textContent = V.marke;
$('#hauptnav').innerHTML = V.nav.map((n, i) => `<a href="#band"${i === 0 ? ' aria-current="page"' : ''}>${n}</a>`).join('');
$('#k-label').textContent = V.kLabel; $('#h-kontext').textContent = V.kTitel;
$('#k-liste').innerHTML = V.kListe.map(x => `<li>${x}</li>`).join('');
$('#p-label').textContent = V.label; $('#p-name').textContent = V.name; $('#p-rolle').textContent = V.rolle;
$('#p-themen').innerHTML = V.themen.map(t => `<li>${t}</li>`).join('');
bild(); status();
}
function status() {
const V = VARIANTEN[variante], s = zustand();
const el = $('#status'); el.dataset.zustand = s.z;
const span = el.querySelector('span'); const neu = span.cloneNode(); neu.textContent = s.text; span.replaceWith(neu);
const mailHref = `mailto:${V.mail}?subject=${encodeURIComponent(V.betreff)}`;
const ur = s.z === 'urlaub' ? V.urlaub : null;
const vTel = ur ? [V.tel[0], V.tel[1], ur.d] : null;
$('#p-notfall').innerHTML = V.notfall ? `<p class="notfall"><span><b>Notfall?</b> Rufen Sie <a href="tel:112">112</a>. Außerhalb der Sprechzeiten hilft der ärztliche Bereitschaftsdienst unter <a href="tel:116117">116${NB}117</a>.</span></p>` : '';
$('#aktionen').innerHTML =
(ur ? `<div class="vertretung"><span class="label">Vertretung bis ${ur.bis}</span><span><b>${ur.name}</b>, ${ur.rolle}</span><a href="${telHref(vTel)}">${telText(vTel)}</a></div>` : '') +
`<a class="knopf anruf" href="${telHref(V.tel)}"><small>${ur ? 'Zentrale' : 'Direkt anrufen'}</small><b class="ziffern">${telText(V.tel)}</b></a>` +
`<a class="knopf mail" href="${mailHref}"><span>E-Mail schreiben</span><small aria-hidden="true">→</small></a>` +
(V.online ? `<a class="knopf mail" href="#band"><span>Termin online buchen</span><small aria-hidden="true">→</small></a>` : '') +
`<dl class="fakten">
<div><dt>Telefonzeiten</dt><dd>${zeitenText(V)}</dd></div>
<div><dt>Antwort</dt><dd>${V.antwort}</dd></div>
${V.sekretariat ? `<div><dt>${V.sekretariat[0]}</dt><dd><a href="${telHref([V.tel[0], V.tel[1], V.sekretariat[1]])}">${telText([V.tel[0], V.tel[1], V.sekretariat[1]])}</a></dd></div>` : ''}
<div><dt>E-Mail</dt><dd><a href="${mailHref}" style="overflow-wrap:anywhere">${V.mail}</a></dd></div>
</dl>`;
}
function druecke(sel, attr, wert) { document.querySelectorAll(sel).forEach(b => b.setAttribute('aria-pressed', b.getAttribute(attr) === wert ? 'true' : 'false')); }
function setzeVariante(v, still) {
variante = v; druecke('.pult [data-v]', 'data-v', v);
if (!still) { const q = new URLSearchParams(location.search); q.set('v', v); history.replaceState(null, '', '?' + q.toString()); }
render();
}
function setzeZeit(z) { zeit = z; druecke('[data-zeit]', 'data-zeit', z); status(); }
function setzeBild(b) { bildArt = b; druecke('[data-bild]', 'data-bild', b); bild(); }
document.querySelectorAll('.pult [data-v]').forEach(b => b.addEventListener('click', () => { pause(); setzeVariante(b.dataset.v); }));
document.querySelectorAll('[data-zeit]').forEach(b => b.addEventListener('click', () => { pause(); setzeZeit(b.dataset.zeit); }));
document.querySelectorAll('[data-bild]').forEach(b => b.addEventListener('click', () => { pause(); setzeBild(b.dataset.bild); }));
$('#eigen').addEventListener('change', e => {
const f = e.target.files && e.target.files[0];
if (!f || !f.type.startsWith('image/')) return;
if (eigenesBild) URL.revokeObjectURL(eigenesBild);
eigenesBild = URL.createObjectURL(f); pause(); setzeBild('eigen');
});
/* Status jede Minute neu rechnen */
setInterval(() => { if (zeit === 'jetzt') status(); }, 60000);
let lauf = null;
function neustart() { clearTimeout(lauf); setzeZeit('jetzt'); setzeBild('bild'); }
function vorfuehren() {
clearTimeout(lauf);
const s = [() => setzeZeit('offen'), () => setzeZeit('pause'), () => setzeZeit('zu'), () => setzeZeit('urlaub'), () => { setzeZeit('offen'); setzeBild('initialen'); }, () => setzeBild('bild')];
let i = 0;
const takt = () => { if (i < s.length) { s[i++](); lauf = setTimeout(takt, 1500); } else lauf = setTimeout(vorfuehren, 400); };
takt();
}
function pause() { clearTimeout(lauf); }
function weiter() { vorfuehren(); }
setzeVariante(variante, true);
})();
</script>
</body>
</html>