Zum Inhalt
← Navigation

Live-Uhr und Standort in Navigation oder Footer

Live-Ortszeit mit Stadt und Zeitzone in Navigation oder Footer, getaktet auf die volle Sekunde, wahlweise mit mehreren Standorten und Dienststatus.

Ein kleines Textelement zeigt Stadt oder Zeitzone und die aktuelle Ortszeit des Teams, etwa „Passau 14:32 MEZ“. Es steht meist monospaced oder in Versalien in der Kopfzeile neben Logo oder Sprachlink oder im Footer neben der Adresse. Die Uhr tickt live, je nach Auflösung im Sekunden- oder Minutentakt, oft mit blinkendem Doppelpunkt. Teams mit mehreren Standorten zeigen mehrere Städte nebeneinander. Manche ergänzen einen Status wie „Support live“ oder „geschlossen“.

Die Uhr macht ein digitales Team greifbar: Hier sitzen echte Menschen, und zwar gerade jetzt. Sie zeigt Internationalität und Erreichbarkeit auf einen Blick. Als kleines, sorgfältig gesetztes Detail signalisiert sie Handwerk.

Wann einsetzen

Studios, Freelancer und Agenturen, verteilte Teams, Software mit Support- oder Rufbereitschaftszeiten, Hotels und Restaurants mit Bezug zu Öffnungszeiten.

Wann nicht

Wenn der Standort für Besucher keine Rolle spielt und die Uhr nur Dekoration wäre. Wenn die Kopfzeile schon voll ist.

Varianten

  • Zeitzonenkürzel und Uhrzeit in der Navigation (Obys: „CET …“)
  • Standorttext im Header ohne Uhr (Locomotive: „Montréal, Quebec“)
  • Ortszeit im Footer mit „Located in …“ (Dennis Snellenberg)
  • Uhr-Widget neben der Büroadresse (Ask Phill: Amsterdam)
  • Mehrere Städte mit Zeiten (Rejouice: San Diego, Paris)

Umsetzung

Zeit über Intl.DateTimeFormat('de-DE', { timeZone: 'Europe/Berlin', hour: '2-digit', minute: '2-digit', timeZoneName: 'short' }) formatieren; formatToParts liefert Stunde, Minute und Kürzel einzeln für eigenes Markup. Aktualisieren per setTimeout, ausgerichtet auf die nächste volle Sekunde bzw. Minute, statt per setInterval, das driftet. font-variant-numeric: tabular-nums verhindert springende Ziffern. Die erste Zeit serverseitig rendern, damit kein leerer Platzhalter erscheint. In SSR-Frameworks droht ein Hydration-Mismatch: suppressHydrationWarning setzen oder die Zeit erst im Client ersetzen.

Robust umsetzen

Telefon. Im Header oft ausgeblendet, dafür im Menü-Overlay oder im Footer. Alternativ nur Stadt und Uhrzeit ohne Kürzel und Status.

Ohne Bewegung. Kein blinkender Doppelpunkt. Minuten statt Sekunden anzeigen und nur einmal pro Minute aktualisieren.

Barrierefrei. Die Zeit in <time datetime> setzen und das Attribut bei jedem Takt aktualisieren. Nicht als aria-live markieren, sonst kommen ständig Ansagen. Kontext wie „Ortszeit Passau“ als visuell versteckten Text ergänzen. Ist die Uhr klickbar, als Button mit aria-expanded umsetzen.

Code

<!DOCTYPE html>
<!--
  Live-Uhr und Standort in Navigation oder Footer
  Fundus · Navigation · https://cecodesign.de/lab/fundus/navigation/live-uhr-und-standort/
  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>Live-Uhr und Standort in Navigation oder Footer · 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: 'Geist'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/geist:vf@latest/latin-wght-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; }

/* Welt: Rezept „saas-produkt-praezise“. Graustufen, ein Markenton, Mono nur für Labels und Zahlen. */
:root {
  --grund: #08090a; --panel: #101112; --karte: #161718; --linie: #242728;
  --text: #f7f8f8; --text-2: #d0d6e0; --leise: #8a8f98;
  --marke: #5b9dff;                       /* Donaublau, einziger Markenton */
  --ok: #59d499; --warn: #ffc533;          /* Statusfarben nur im UI */
  --ui: cubic-bezier(0.25, 0.46, 0.45, 0.94);   /* easeOutQuad, 160–180 ms */
  --lade: cubic-bezier(0.32, 0.72, 0, 1);       /* Drawer-Kurve, 300 ms (Lupe) */
  --breite: 1160px;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
html { background: var(--grund); color: var(--text); font-family: 'Geist', system-ui, sans-serif; font-weight: 400; -webkit-font-smoothing: antialiased; }
body { min-height: 100svh; overflow-x: hidden; }
a { color: inherit; text-decoration: none; }
button { font: inherit; color: inherit; background: none; border: 0; cursor: pointer; }
.mono { font-family: 'Geist Mono', ui-monospace, monospace; font-size: 11px; font-weight: 500; letter-spacing: .14em; text-transform: uppercase; }
.vh { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap; }
.zahl { font-variant-numeric: tabular-nums; }

/* ---------- Kopfzeile ---------- */
.kopf { position: sticky; top: 0; z-index: 20; background: rgba(8, 9, 10, .82); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); border-bottom: 1px solid var(--linie); }
.kopf__innen { max-width: var(--breite); margin: 0 auto; height: 60px; padding: 0 24px; display: flex; align-items: center; gap: 28px; }
.marke { display: flex; align-items: center; gap: 9px; font-weight: 600; font-size: 15px; letter-spacing: -.012em; }
.marke svg { width: 22px; height: 22px; }
.nav { display: flex; gap: 22px; font-size: 13.5px; color: var(--leise); }
.nav a { transition: color .16s var(--ui); }
.nav a:hover { color: var(--text); }
.kopf__rechts { margin-left: auto; display: flex; align-items: center; gap: 18px; }
.anmelden { font-size: 13.5px; color: var(--text-2); }
.knopf { white-space: nowrap; display: inline-flex; align-items: center; gap: 8px; height: 32px; padding: 0 14px; border-radius: 8px; font-size: 13.5px; font-weight: 500; letter-spacing: -.006em; transition: background-color .16s var(--ui), border-color .16s var(--ui); }
.knopf--primaer { background: var(--marke); color: #06101f; }
.knopf--primaer:hover { background: #79afff; }
.knopf--zweit { border: 1px solid var(--linie); color: var(--text-2); background: var(--karte); }
.knopf--zweit:hover { border-color: #3a3e42; }
.burger { display: none; width: 32px; height: 32px; border: 1px solid var(--linie); border-radius: 8px; place-items: center; }
.burger i { display: block; width: 14px; height: 1.5px; background: var(--text-2); box-shadow: 0 -4px 0 var(--text-2), 0 4px 0 var(--text-2); }

/* ---------- Das Muster: Uhr und Standort ---------- */
.ort { position: relative; display: flex; align-items: center; height: 32px; }
.ort__var { display: flex; align-items: center; gap: 14px; transition: opacity .18s var(--ui), translate .18s var(--ui); }
.ort__var[hidden] { display: none; }
.uhr { display: inline-flex; align-items: center; gap: 10px; height: 32px; padding: 0 12px; border-radius: 8px; border: 1px solid transparent; font-family: 'Geist Mono', ui-monospace, monospace; font-size: 12px; font-weight: 500; letter-spacing: .06em; color: var(--text-2); white-space: nowrap; transition: border-color .16s var(--ui), background-color .16s var(--ui); }
.uhr:hover, .uhr[aria-expanded="true"] { border-color: var(--linie); background: var(--karte); }
.uhr:focus-visible { outline: 2px solid var(--marke); outline-offset: 2px; }
.uhr__stadt { color: var(--leise); letter-spacing: .14em; text-transform: uppercase; font-size: 11px; }
.uhr__zone { color: var(--leise); font-size: 11px; letter-spacing: .1em; }
time { font-variant-numeric: tabular-nums; color: var(--text); }
.dp { display: inline-block; width: .6em; text-align: center; }
.dp.aus { opacity: .28; }
.punkt { width: 6px; height: 6px; border-radius: 50%; background: var(--ok); box-shadow: 0 0 0 3px rgba(89, 212, 153, .14); flex: none; }
.punkt.ruf { background: var(--warn); box-shadow: 0 0 0 3px rgba(255, 197, 51, .14); }
.punkt.leer { background: transparent; box-shadow: inset 0 0 0 1px #4a4f55; }
.dienst { font-family: 'Geist Mono', ui-monospace, monospace; font-size: 11px; letter-spacing: .14em; text-transform: uppercase; color: var(--leise); display: flex; align-items: center; gap: 8px; white-space: nowrap; }
.trenner { width: 1px; height: 16px; background: var(--linie); }

/* Variante B: drei Orte */
.orte { display: flex; align-items: center; gap: 4px; }
.orte .uhr { padding: 0 10px; gap: 8px; }
.orte .uhr:not(.im-dienst) time { color: var(--leise); }

/* Variante C: nur Standorttext oben, Uhr im Footer */
.standort { font-size: 13px; color: var(--leise); white-space: nowrap; }
.standort b { font-weight: 500; color: var(--text-2); }

/* Popover: Ihre Zeit im Vergleich */
.pop { position: absolute; top: calc(100% + 10px); right: 0; z-index: 30; width: 268px; padding: 14px; border-radius: 10px; background: var(--karte); border: 1px solid var(--linie); box-shadow: 0 16px 40px rgba(0, 0, 0, .5); opacity: 0; translate: 0 -4px; pointer-events: none; transition: opacity .16s var(--ui), translate .16s var(--ui); }
.pop.an { opacity: 1; translate: 0 0; pointer-events: auto; }
.pop dl { display: grid; grid-template-columns: auto 1fr; gap: 8px 14px; font-size: 13px; }
.pop dt { color: var(--leise); }
.pop dd { text-align: right; color: var(--text); font-variant-numeric: tabular-nums; }
.pop p { margin-top: 12px; padding-top: 12px; border-top: 1px solid var(--linie); font-size: 12.5px; line-height: 1.5; color: var(--leise); }

/* ---------- Hero mit Produkt-UI ---------- */
.held { max-width: var(--breite); margin: 0 auto; padding: clamp(44px, 6vw, 80px) 24px 0; }
.held__kopf { display: grid; gap: 0; }
@media (min-width: 1100px) {
  .held__kopf { grid-template-columns: minmax(0, 1.25fr) minmax(0, 1fr); column-gap: 64px; align-items: end; }
  .held__kopf .neu { grid-column: 1 / -1; justify-self: start; }
  .held__text .held__unter { margin-top: 0; }
}
.neu { justify-self: start; display: inline-flex; align-items: center; gap: 10px; height: 28px; padding: 0 12px 0 6px; border: 1px solid var(--linie); border-radius: 999px; font-size: 12.5px; color: var(--text-2); background: var(--panel); }
.neu span { font-family: 'Geist Mono', monospace; font-size: 10px; letter-spacing: .14em; text-transform: uppercase; color: var(--marke); background: rgba(91, 157, 255, .12); border-radius: 999px; padding: 3px 7px; }
h1 { margin-top: 22px; font-size: clamp(2.5rem, 1.6rem + 3.6vw, 4.5rem); font-weight: 560; letter-spacing: -.022em; line-height: 1; max-width: 15ch; }
.held__unter { margin-top: 20px; max-width: 54ch; font-size: 1.0625rem; line-height: 1.6; letter-spacing: -.006em; color: var(--leise); }
.held__unter b { color: var(--text-2); font-weight: 500; }
.held__ctas { margin-top: 28px; display: flex; gap: 10px; flex-wrap: wrap; }
.held__ctas .knopf { height: 40px; padding: 0 18px; font-size: 14px; }

.app { margin: clamp(44px, 6vw, 72px) 0 0; border: 1px solid var(--linie); border-bottom: 0; border-radius: 14px 14px 0 0; background: var(--panel); overflow: hidden; }
.app__leiste { display: flex; align-items: center; gap: 14px; height: 44px; padding: 0 16px; border-bottom: 1px solid var(--linie); font-size: 13px; color: var(--leise); }
.app__leiste b { color: var(--text); font-weight: 500; }
.app__leiste .mono { margin-left: auto; font-size: 10.5px; color: var(--leise); display: flex; gap: 8px; align-items: center; }
.app__leiste .mono time { color: var(--text-2); letter-spacing: .06em; }
.app__kopfzeile, .monitor { display: grid; grid-template-columns: minmax(170px, 1.3fr) minmax(0, 2.4fr) 70px 64px; gap: 20px; align-items: center; padding: 0 16px; }
.app__kopfzeile { height: 34px; color: var(--leise); font-size: 10.5px; border-bottom: 1px solid var(--linie); }
.app__kopfzeile span:nth-child(n+3) { text-align: right; }
.monitor { height: 52px; border-bottom: 1px solid var(--linie); font-size: 13.5px; }
.monitor:last-child { border-bottom: 0; }
.monitor__name { display: flex; align-items: center; gap: 10px; min-width: 0; }
.monitor__name span:last-child { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.monitor__name small { display: block; font-family: 'Geist Mono', monospace; font-size: 10.5px; color: var(--leise); letter-spacing: .02em; margin-top: 2px; }
.balken { display: flex; gap: 2px; height: 22px; align-items: stretch; min-width: 0; overflow: hidden; }
.balken i { flex: 1 0 3px; border-radius: 1.5px; background: rgba(89, 212, 153, .72); }
.balken i.w { background: var(--warn); }
.balken i.l { background: #2a2d31; }
.monitor .zahl { text-align: right; font-family: 'Geist Mono', monospace; font-size: 12px; color: var(--text-2); }

/* ---------- Footer mit großer Ortszeit ---------- */
.fuss { margin-top: 0; border-top: 1px solid var(--linie); background: var(--grund); }
.fuss__innen { max-width: var(--breite); margin: 0 auto; padding: 64px 24px 112px; }
.fuss__uhr { display: grid; grid-template-columns: 1fr auto; gap: 24px 48px; align-items: end; padding-bottom: 48px; border-bottom: 1px solid var(--linie); }
.gross { display: block; font-size: clamp(4rem, 1.4rem + 9vw, 9.5rem); font-weight: 500; letter-spacing: -.035em; line-height: .9; color: var(--text); font-variant-numeric: tabular-nums; }
.gross .dp { width: .32em; }
.gross .sek { color: #62676f; }
.fuss__ort { margin-bottom: 16px; display: flex; gap: 14px; align-items: center; color: var(--leise); }
.fuss__ort b { color: var(--text-2); font-weight: 500; }
.fuss__meta { display: grid; gap: 10px; text-align: right; font-size: 13.5px; color: var(--leise); padding-bottom: 6px; }
.fuss__meta .dienst { justify-content: flex-end; }
.fuss__karte { display: grid; grid-template-columns: repeat(5, 1fr); gap: 24px; padding-top: 40px; }
.fuss__karte h3 { font-family: 'Geist Mono', monospace; font-size: 11px; font-weight: 500; letter-spacing: .14em; text-transform: uppercase; color: var(--leise); margin-bottom: 14px; }
.fuss__karte ul { list-style: none; display: grid; gap: 9px; font-size: 13.5px; color: var(--text-2); }
.fuss__karte a:hover { color: var(--text); }
.fuss__recht { display: flex; justify-content: space-between; gap: 16px; margin-top: 48px; color: var(--leise); font-size: 12.5px; }
.fuss__recht .dienst { gap: 8px; }

/* ---------- Bedienleiste der Demo (dezent) ---------- */
.steuer { position: fixed; left: 50%; bottom: 18px; translate: -50% 0; z-index: 40; display: flex; align-items: center; gap: 14px; padding: 5px 5px 5px 14px; border-radius: 12px; background: rgba(22, 23, 24, .9); backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px); border: 1px solid var(--linie); box-shadow: 0 10px 30px rgba(0, 0, 0, .45); }
.steuer .mono { font-size: 10px; color: var(--leise); }
.segment { position: relative; display: flex; }
.segment button { position: relative; z-index: 1; height: 28px; padding: 0 12px; font-family: 'Geist Mono', monospace; font-size: 10.5px; font-weight: 500; letter-spacing: .1em; text-transform: uppercase; color: var(--leise); transition: color .16s var(--ui); }
.segment button[aria-pressed="true"] { color: var(--text); }
.segment__pille { position: absolute; top: 0; left: 0; height: 28px; border-radius: 8px; background: #26292d; transition: translate .18s var(--ui), width .18s var(--ui); }
.werte { font-size: 10px; color: #6b7078; letter-spacing: .08em; white-space: nowrap; padding: 0 10px 0 14px; border-left: 1px solid var(--linie); line-height: 28px; }
.segment button { white-space: nowrap; }

/* ---------- Lupe (nur Vorführung) ---------- */
.lupe { position: fixed; z-index: 35; width: 260px; height: 260px; border-radius: 50%; overflow: hidden; pointer-events: none; background: var(--grund); border: 1px solid #3a3e42; box-shadow: 0 0 0 6px rgba(8, 9, 10, .7), 0 24px 60px rgba(0, 0, 0, .6); opacity: 0; scale: .94; transition: opacity .3s var(--lade), scale .3s var(--lade); }
.lupe.an { opacity: 1; scale: 1; }
.lupe__inhalt { position: absolute; pointer-events: none; }
.lupe__inhalt .pop { display: none; }
.leine { position: fixed; z-index: 34; width: 1px; background: linear-gradient(#5d626a, rgba(93, 98, 106, 0)); pointer-events: none; opacity: 0; transition: opacity .3s var(--lade); }
.leine.an { opacity: 1; }

/* Vorschau: nur die Bühne */
.vorschau body { overflow: hidden; }
.vorschau .steuer, .vorschau .werte { display: none; }

/* ---------- Breiten ---------- */
@media (max-width: 1180px) { .nav a.breit { display: none; } }
@media (max-width: 1480px) { .kopf[data-v="orte"] .nav a.breit, .kopf[data-v="orte"] .anmelden { display: none; } }
@media (max-width: 1240px) { .kopf[data-v="orte"] .nav { display: none; } }
@media (max-width: 1040px) { .nav { display: none; } .anmelden { display: none; } }
@media (max-width: 1100px) { .werte { display: none; } }
@media (max-width: 900px) {
  .fuss__karte { grid-template-columns: repeat(3, 1fr); row-gap: 32px; }
}
@media (max-width: 720px) {
  .kopf__innen { padding: 0 16px; gap: 12px; }
  .kopf__rechts { gap: 8px; }
  .kopf__rechts > .knopf, .dienst-kopf, .trenner { display: none; }
  .burger { display: grid; }
  .uhr { padding: 0 8px; gap: 8px; }
  .uhr__zone { display: none; }
  .orte .uhr:not(.im-dienst) { display: none; }
  .orte .mehr { display: inline; }
  .standort span { display: none; }
  .held { padding: 40px 16px 0; }
  .app__kopfzeile, .monitor { grid-template-columns: minmax(0, 1fr) 64px; gap: 12px; }
  .app__kopfzeile span:nth-child(2), .app__kopfzeile span:nth-child(3), .monitor .balken, .monitor .zahl.ms { display: none; }
  .app__leiste .mono span.lang { display: none; }
  .fuss__innen { padding: 48px 16px 104px; }
  .fuss__uhr { grid-template-columns: 1fr; }
  .fuss__meta { text-align: left; }
  .fuss__meta .dienst { justify-content: flex-start; }
  .fuss__karte { grid-template-columns: repeat(2, 1fr); }
  .fuss__recht { flex-direction: column; }
  .steuer { padding-left: 5px; bottom: 12px; }
  .steuer > .mono { display: none; }
  .ort { position: static; }
  .pop { right: auto; left: 50%; translate: -50% -4px; width: min(268px, calc(100vw - 24px)); }
  .pop.an { translate: -50% 0; }
}
@media (max-width: 380px) {
  .monitor__name span:last-child { white-space: normal; overflow-wrap: anywhere; }
}
.orte .mehr { display: none; color: var(--leise); font-family: 'Geist Mono', monospace; font-size: 11px; letter-spacing: .06em; }

/* Touch: kein Hover-Popover, Antippen öffnet und schließt */
@media (hover: none) { .uhr:hover { border-color: transparent; background: none; } .uhr[aria-expanded="true"] { border-color: var(--linie); background: var(--karte); } }

/* Reduzierte Bewegung: kein Blinken, keine Sekunden, Wechsel ohne Bewegung */
.reduziert .sek, .reduziert .dp-sek { display: none; }
.reduziert .dp.aus { opacity: 1; }
.reduziert .ort__var, .reduziert .pop, .reduziert .segment__pille, .reduziert .lupe, .reduziert .leine { transition-property: opacity; transition-duration: .15s; translate: none; scale: 1; }
.reduziert .pop.an { translate: none; }
@media (max-width: 720px) { .reduziert .pop, .reduziert .pop.an { translate: -50% 0; } }
/* Kontrastmodus: Knöpfe mit Rahmen */
@media (forced-colors: active) { button, [role=button], [role=tab], [role=switch] { border: 1px solid ButtonText !important; } }
</style>
</head>
<body>
<header class="kopf" data-kopf>
  <div class="kopf__innen">
    <a class="marke" href="#" aria-label="Dreistrom, Startseite">
      <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M2 6.5c5 0 7.5 1.2 10 5.5M2 12h10M2 17.5c5 0 7.5-1.2 10-5.5M12 12h10" fill="none" stroke="#5b9dff" stroke-width="2.2" stroke-linecap="round"/></svg>
      Dreistrom
    </a>
    <nav class="nav" aria-label="Hauptnavigation">
      <a href="#">Produkt</a><a href="#">Statusseiten</a><a href="#">Preise</a><a class="breit" href="#">Changelog</a><a class="breit" href="#">Doku</a>
    </nav>
    <div class="kopf__rechts">
      <div class="ort" data-ort>
        <!-- Variante A: Stadt, Uhrzeit, Zeitzone -->
        <div class="ort__var" data-var="nav">
          <span class="dienst dienst-kopf" data-dienst-kurz><i class="punkt"></i><span>Support live</span></span>
          <i class="trenner"></i>
          <button class="uhr" type="button" aria-expanded="false" aria-controls="pop" data-uhr-knopf>
            <span class="uhr__stadt">Passau</span>
            <span><span class="vh">Ortszeit Passau: </span><time data-uhr="Europe/Berlin" data-sek></time></span>
            <span class="uhr__zone" data-zone="Europe/Berlin">MEZ</span>
          </button>
        </div>
        <!-- Variante B: drei Orte, wer gerade Dienst hat, ist hell -->
        <div class="ort__var" data-var="orte" hidden>
          <div class="orte" data-orte>
            <button class="uhr" type="button" aria-expanded="false" aria-controls="pop" data-uhr-knopf data-stadt="Passau"><i class="punkt leer"></i><span class="uhr__stadt">Passau</span><span class="vh">Ortszeit: </span><time data-uhr="Europe/Berlin"></time></button>
            <button class="uhr" type="button" aria-expanded="false" aria-controls="pop" data-uhr-knopf data-stadt="Toronto"><i class="punkt leer"></i><span class="uhr__stadt">Toronto</span><span class="vh">Ortszeit: </span><time data-uhr="America/Toronto"></time></button>
            <button class="uhr" type="button" aria-expanded="false" aria-controls="pop" data-uhr-knopf data-stadt="Melbourne"><i class="punkt leer"></i><span class="uhr__stadt">Melbourne</span><span class="vh">Ortszeit: </span><time data-uhr="Australia/Melbourne"></time></button>
            <span class="mehr" aria-hidden="true">+2</span>
          </div>
        </div>
        <!-- Variante C: nur der Ort oben -->
        <div class="ort__var" data-var="fuss" hidden>
          <p class="standort"><b>Passau</b><span>, Niederbayern · Made in Germany</span></p>
        </div>
        <div class="pop" id="pop" role="dialog" aria-label="Zeitvergleich" data-pop>
          <dl>
            <dt data-pop-ort>Ortszeit Passau</dt><dd data-pop-passau>–</dd>
            <dt>Ihre Zeit</dt><dd data-pop-ihre>–</dd>
            <dt>Abstand</dt><dd data-pop-diff>–</dd>
          </dl>
          <p data-pop-text>Live-Support aus Passau täglich 7–15 Uhr. Danach übernimmt die Rufbereitschaft in Toronto und Melbourne.</p>
        </div>
      </div>
      <a class="anmelden" href="#">Anmelden</a>
      <a class="knopf knopf--primaer" href="#">Kostenlos testen</a>
      <button class="burger" type="button" aria-label="Menü"><i></i></button>
    </div>
  </div>
</header>

<main class="held">
  <div class="held__kopf">
    <a class="neu" href="#"><span>Neu</span>Statusseiten mit eigener Domain</a>
    <h1>Ausfälle sehen, bevor Kunden anrufen.</h1>
    <div class="held__text">
      <p class="held__unter">Dreistrom prüft Websites, APIs und Cronjobs <b>alle 30 Sekunden</b> aus drei Rechenzentren in Deutschland und weckt die richtige Person, nicht das ganze Team.</p>
      <div class="held__ctas"><a class="knopf knopf--primaer" href="#">Kostenlos testen</a><a class="knopf knopf--zweit" href="#">Demo ansehen</a></div>
    </div>
  </div>

  <section class="app" aria-label="Produktansicht: Monitore">
    <div class="app__leiste"><span>Kaffeerösterei Ilztal</span><span>/</span><b>Monitore</b>
      <span class="mono"><span class="lang">Letzte Prüfung</span><time data-uhr="Europe/Berlin" data-sek></time></span>
    </div>
    <div class="app__kopfzeile mono"><span>Monitor</span><span>Letzte 90 Tage</span><span>Antwort</span><span>Uptime</span></div>
    <div data-monitore></div>
  </section>
</main>

<footer class="fuss" data-fuss>
  <div class="fuss__innen">
    <div class="fuss__uhr">
      <div>
        <p class="fuss__ort mono"><b>Ortszeit Passau</b><span data-zone-lang="Europe/Berlin">MEZ · UTC+1</span></p>
        <span class="vh">Ortszeit Passau: </span><time class="gross" data-uhr="Europe/Berlin" data-sek data-gross></time>
      </div>
      <div class="fuss__meta">
        <span class="dienst" data-dienst-lang><i class="punkt"></i><span>Support live aus Passau</span></span>
        <span>Gebaut an der Dreiflüssestadt · 48,57° N · 13,43° O</span>
      </div>
    </div>
    <div class="fuss__karte">
      <div><h3>Produkt</h3><ul><li><a href="#">Uptime-Monitoring</a></li><li><a href="#">Cronjob-Wächter</a></li><li><a href="#">Statusseiten</a></li><li><a href="#">Rufbereitschaft</a></li></ul></div>
      <div><h3>Lösungen</h3><ul><li><a href="#">Onlineshops</a></li><li><a href="#">Agenturen</a></li><li><a href="#">SaaS-Teams</a></li></ul></div>
      <div><h3>Ressourcen</h3><ul><li><a href="#">Doku</a></li><li><a href="#">API</a></li><li><a href="#">Changelog</a></li><li><a href="#">Status</a></li></ul></div>
      <div><h3>Unternehmen</h3><ul><li><a href="#">Über uns</a></li><li><a href="#">Jobs in Passau</a></li><li><a href="#">Kontakt</a></li></ul></div>
      <div><h3>Rechtliches</h3><ul><li><a href="#">Sicherheit &amp; DSGVO</a></li><li><a href="#">AV-Vertrag</a></li><li><a href="#">Impressum</a></li><li><a href="#">Datenschutz</a></li></ul></div>
    </div>
    <div class="fuss__recht"><span>© 2026 Dreistrom GmbH · Passau</span><span class="dienst"><i class="punkt"></i>Alle Systeme betriebsbereit</span></div>
  </div>
</footer>

<div class="steuer" role="group" aria-label="Variante wählen">
  <span class="mono">Variante</span>
  <div class="segment" data-segment>
    <span class="segment__pille" data-pille aria-hidden="true"></span>
    <button type="button" aria-pressed="true" data-wahl="nav">Navigation</button>
    <button type="button" aria-pressed="false" data-wahl="orte">Drei Orte</button>
    <button type="button" aria-pressed="false" data-wahl="fuss">Footer</button>
  </div>
  <span class="werte mono" data-werte aria-hidden="true"></span>
</div>

<div class="leine" data-leine aria-hidden="true"></div>
<div class="lupe" data-lupe aria-hidden="true"><div class="lupe__inhalt" data-lupe-inhalt></div></div>

<script>
(() => {
  const html = document.documentElement;
  const reduziert = () => html.classList.contains('reduziert');
  const $ = s => document.querySelector(s), $$ = s => [...document.querySelectorAll(s)];

  /* ---------- Produkt-UI: Monitore mit 90-Tage-Balken (fest, damit jede Ansicht gleich aussieht) ---------- */
  const MONITORE = [
    ['Webshop · Checkout', 'shop.ilztal-kaffee.de/kasse', 142, '99,98'],
    ['API · Bestellungen', 'api.ilztal-kaffee.de/v2/orders', 88, '99,95', [61, 62]],
    ['Cronjob · Rechnungsexport', 'täglich 02:00 · Heartbeat', 0, '100,00'],
    ['Statusseite', 'status.ilztal-kaffee.de', 64, '100,00'],
    ['Warenwirtschaft · Sync', 'erp.ilztal-kaffee.de', 231, '99,71', [23, 77]]
  ];
  $('[data-monitore]').innerHTML = MONITORE.map(([n, u, ms, up, warn = []]) => {
    let b = '';
    for (let i = 0; i < 90; i++) b += `<i class="${warn.includes(i) ? 'w' : (n.startsWith('Statusseite') && i < 18 ? 'l' : '')}"></i>`;
    return `<div class="monitor"><div class="monitor__name"><i class="punkt${warn.length && warn.includes(89) ? ' ruf' : ''}"></i><span>${n}<small>${u}</small></span></div>` +
      `<div class="balken" aria-hidden="true">${b}</div><span class="zahl ms">${ms ? ms + ' ms' : 'ok'}</span><span class="zahl">${up} %</span></div>`;
  }).join('');

  /* ---------- Uhr: Intl.DateTimeFormat, ein Takt pro voller Sekunde ---------- */
  const fmt = {};
  const formatierer = (tz, sek) => fmt[tz + sek] || (fmt[tz + sek] = new Intl.DateTimeFormat('de-DE', {
    timeZone: tz, hour: '2-digit', minute: '2-digit', second: sek ? '2-digit' : undefined, hourCycle: 'h23', timeZoneName: 'short'
  }));
  const teile = (tz, d, sek) => Object.fromEntries(formatierer(tz, sek).formatToParts(d).map(p => [p.type, p.value]));
  // Abstand der Zone zu UTC in Minuten (für datetime-Attribut und Vergleich)
  function versatz(tz, d) {
    const p = Object.fromEntries(new Intl.DateTimeFormat('en-US', { timeZone: tz, hourCycle: 'h23', year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' }).formatToParts(d).map(x => [x.type, x.value]));
    return Math.round((Date.UTC(p.year, p.month - 1, p.day, p.hour, p.minute) - Math.floor(d.getTime() / 60000) * 60000) / 60000);
  }
  const zz = n => String(Math.abs(n)).padStart(2, '0');
  function isoLokal(tz, d) {
    const v = versatz(tz, d), l = new Date(d.getTime() + v * 60000);
    return l.toISOString().slice(0, 19) + (v < 0 ? '-' : '+') + zz(Math.trunc(v / 60)) + ':' + zz(v % 60);
  }

  // Rufbereitschaft nach Ortszeit: wer gerade 7/9–15/17 Uhr hat, ist im Dienst (im Winter lückenlos)
  const DIENST = [['Passau', 'Europe/Berlin', 7, 15], ['Toronto', 'America/Toronto', 9, 17], ['Melbourne', 'Australia/Melbourne', 9, 17]];
  const imDienst = d => (DIENST.find(([, tz, a, b]) => { const h = +teile(tz, d, false).hour; return h >= a && h < b; }) || [null])[0];

  let blink = null, takt = null;
  function zeichnen() {
    const d = new Date(), sek = !reduziert();
    $$('[data-uhr]').forEach(el => {
      const tz = el.dataset.uhr, mitSek = sek && el.hasAttribute('data-sek'), p = teile(tz, d, mitSek);
      el.setAttribute('datetime', isoLokal(tz, d));
      el.innerHTML = `${p.hour}<span class="dp" aria-hidden="true">:</span><span class="vh">:</span>${p.minute}` +
        (mitSek ? `<span class="sek"><span class="dp dp-sek" aria-hidden="true">:</span><span class="vh">:</span>${p.second}</span>` : '');
    });
    $$('[data-zone]').forEach(el => { el.textContent = teile(el.dataset.zone, d, false).timeZoneName; });
    $$('[data-zone-lang]').forEach(el => {
      const tz = el.dataset.zoneLang, v = versatz(tz, d) / 60;
      el.textContent = `${teile(tz, d, false).timeZoneName} · UTC${v >= 0 ? '+' : '−'}${Math.abs(v)}`;
    });
    // Dienststatus
    const wer = imDienst(d);
    $$('[data-stadt]').forEach(b => {
      const an = b.dataset.stadt === wer;
      b.classList.toggle('im-dienst', an);
      b.querySelector('.punkt').className = 'punkt' + (an ? (wer === 'Passau' ? '' : ' ruf') : ' leer');
    });
    const kurz = $('[data-dienst-kurz]'), lang = $('[data-dienst-lang]');
    const live = wer === 'Passau';
    kurz.querySelector('.punkt').className = 'punkt' + (live ? '' : ' ruf');
    kurz.querySelector('span').textContent = live ? 'Support live' : 'Rufbereitschaft ' + (wer || '');
    lang.querySelector('.punkt').className = 'punkt' + (live ? '' : ' ruf');
    lang.querySelector('span').textContent = live ? 'Support live aus Passau' : 'Rufbereitschaft aus ' + (wer || 'dem Team');
    popFuellen(d);
    // Doppelpunkt: 500 ms hell, 500 ms gedimmt, hart geschaltet und an die Sekunde gekoppelt
    clearTimeout(blink);
    if (sek) blink = setTimeout(() => $$('.dp').forEach(x => x.classList.add('aus')), 500);
  }
  function tick() {
    zeichnen();
    const schritt = reduziert() ? 60000 : 1000;          // Minutenauflösung bei reduzierter Bewegung
    takt = setTimeout(tick, schritt - (Date.now() % schritt) + 8);   // auf die nächste volle Sekunde/Minute, kein setInterval-Drift
  }

  /* ---------- Popover: Ihre Zeit im Vergleich ---------- */
  const pop = $('[data-pop]');
  const eigeneZone = Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC';
  let popOrt = DIENST[0];   // [Stadt, Zone, Dienstbeginn, Dienstende]
  function popFuellen(d) {
    const sek = false, [stadt, tz, a, b] = popOrt;
    const pp = teile(tz, d, sek), pi = teile(eigeneZone, d, sek);
    $('[data-pop-ort]').textContent = 'Ortszeit ' + stadt;
    $('[data-pop-passau]').textContent = `${pp.hour}:${pp.minute} ${pp.timeZoneName}`;
    $('[data-pop-ihre]').textContent = `${pi.hour}:${pi.minute} ${pi.timeZoneName}`;
    const diff = (versatz(tz, d) - versatz(eigeneZone, d)) / 60;
    const st = Math.abs(diff).toLocaleString('de-DE');
    $('[data-pop-diff]').textContent = diff === 0 ? 'gleiche Zeitzone' : `${stadt} ${st} Std. ${diff > 0 ? 'voraus' : 'zurück'}`;
    $('[data-pop-text]').textContent = stadt === 'Passau'
      ? 'Live-Support aus Passau täglich 7–15 Uhr. Danach übernimmt die Rufbereitschaft in Toronto und Melbourne.'
      : `Rufbereitschaft aus ${stadt} täglich ${a}–${b} Uhr Ortszeit. Störungen gehen rund um die Uhr an eine echte Person.`;
  }
  let offenVon = null, hoverUhr = null;
  function popAuf(knopf) {
    const ort = $('[data-ort]').getBoundingClientRect(), r = knopf.getBoundingClientRect();
    if (innerWidth > 720) pop.style.right = Math.max(0, ort.right - r.right) + 'px';
    $$('[data-uhr-knopf]').forEach(b => b.setAttribute('aria-expanded', String(b === knopf)));
    popOrt = DIENST.find(o => o[0] === knopf.dataset.stadt) || DIENST[0]; popFuellen(new Date());
    pop.classList.add('an'); offenVon = knopf;
  }
  function popZu() { pop.classList.remove('an'); $$('[data-uhr-knopf]').forEach(b => b.setAttribute('aria-expanded', 'false')); offenVon = null; }
  $$('[data-uhr-knopf]').forEach(b => {
    b.addEventListener('click', e => { e.stopPropagation(); offenVon === b ? popZu() : popAuf(b); });
    b.addEventListener('pointerenter', e => { if (e.pointerType !== 'mouse') return; clearTimeout(hoverUhr); hoverUhr = setTimeout(() => popAuf(b), 150); });
    b.addEventListener('pointerleave', e => { if (e.pointerType !== 'mouse') return; clearTimeout(hoverUhr); hoverUhr = setTimeout(popZu, 200); });
  });
  pop.addEventListener('pointerenter', () => clearTimeout(hoverUhr));
  pop.addEventListener('pointerleave', () => { hoverUhr = setTimeout(popZu, 200); });
  document.addEventListener('click', e => { if (!pop.contains(e.target)) popZu(); });
  document.addEventListener('keydown', e => { if (e.key === 'Escape' && offenVon) { const b = offenVon; popZu(); b.focus(); } });
  $$('a[href="#"]').forEach(a => a.addEventListener('click', e => e.preventDefault()));

  /* ---------- Varianten ---------- */
  const segment = $('[data-segment]'), pille = $('[data-pille]');
  let variante = 'nav';
  function pilleSetzen() {
    const b = segment.querySelector('[aria-pressed="true"]');
    pille.style.width = b.offsetWidth + 'px'; pille.style.translate = b.offsetLeft + 'px 0';
  }
  function wechseln(v, still) {
    variante = v; popZu();
    $('[data-kopf]').dataset.v = v;
    $$('[data-wahl]').forEach(b => b.setAttribute('aria-pressed', String(b.dataset.wahl === v)));
    pilleSetzen();
    $$('[data-ort] > [data-var]').forEach(el => {
      const an = el.dataset.var === v;
      if (an) { el.hidden = false; el.style.opacity = 0; el.style.translate = '0 4px'; requestAnimationFrame(() => requestAnimationFrame(() => { el.style.opacity = ''; el.style.translate = ''; })); }
      else el.hidden = true;
    });
    const ziel = v === 'fuss' ? document.documentElement.scrollHeight : 0;
    scrollTo({ top: ziel, behavior: still || reduziert() ? 'instant' : 'smooth' });
    werteZeigen();
  }
  $$('[data-wahl]').forEach(b => b.addEventListener('click', () => { if (html.classList.contains('vorfuehrung')) neustart(); wechseln(b.dataset.wahl); }));
  addEventListener('resize', () => { pilleSetzen(); if (lupeAn) lupeSetzen(); });

  function werteZeigen() {
    const r = reduziert();
    const was = { nav: 'Stadt · HH:MM:SS · Kürzel', orte: '3 Zonen · Dienst hell', fuss: 'Footer · ' + (r ? 'HH:MM' : 'HH:MM:SS') }[variante];
    $('[data-werte]').textContent = `${was} · Takt ${r ? '60000' : '1000'} ms, sync · Doppelpunkt ${r ? 'fest' : '500/500 ms'}`;
  }

  /* ---------- Lupe: zeigt in der Vorführung das kleine Detail vergrößert ---------- */
  const lupe = $('[data-lupe]'), inhalt = $('[data-lupe-inhalt]'), leine = $('[data-leine]');
  const D = 260, S = 2.2;
  let lupeAn = false;
  function lupeSetzen() {
    const kopf = $('[data-kopf]');
    const ziel = variante === 'orte' ? ($('[data-orte] .im-dienst') || $('[data-orte]')) : $('[data-var="nav"] time');
    const k = kopf.getBoundingClientRect(), z = ziel.getBoundingClientRect();
    const fx = z.left + z.width / 2, fy = z.top + z.height / 2;
    const lx = Math.min(innerWidth - D - 16, Math.max(16, fx - D / 2)), ly = k.bottom + 46;
    lupe.style.left = lx + 'px'; lupe.style.top = ly + 'px';
    leine.style.left = fx + 'px'; leine.style.top = (fy + z.height / 2 + 4) + 'px'; leine.style.height = Math.max(0, ly - fy - z.height / 2 - 4) + 'px';
    if (!inhalt.firstChild || inhalt.dataset.fuer !== variante) {
      inhalt.innerHTML = ''; const c = kopf.cloneNode(true);
      c.removeAttribute('data-kopf'); c.style.position = 'static'; c.style.width = k.width + 'px';
      c.querySelectorAll('[id]').forEach(n => n.removeAttribute('id'));
      inhalt.appendChild(c); inhalt.dataset.fuer = variante; inhalt.hidden = false;
    }
    const ox = fx - k.left, oy = fy - k.top;
    inhalt.style.width = k.width + 'px';
    inhalt.style.left = (D / 2 - ox) + 'px'; inhalt.style.top = (D / 2 - oy) + 'px';
    inhalt.style.transformOrigin = `${ox}px ${oy}px`; inhalt.style.transform = `scale(${S})`;
  }
  function lupeZeigen(an) {
    lupeAn = an;
    if (an) { lupeSetzen(); zeichnen(); }
    lupe.classList.toggle('an', an); leine.classList.toggle('an', an);
  }

  /* ---------- Vorführung: Varianten im Wechsel, Lupe auf der Uhr ---------- */
  const FOLGE = ['nav', 'orte', 'fuss'], JE = 4500;
  let schleife = [], schritt = 0;
  function planen(fn, ms) { schleife.push(setTimeout(fn, ms)); }
  function runde() {
    const v = FOLGE[schritt % FOLGE.length];
    lupeZeigen(false);
    wechseln(v);
    if (v !== 'fuss') planen(() => lupeZeigen(true), 700);
    planen(() => lupeZeigen(false), JE - 450);
    planen(() => { schritt++; runde(); }, JE);
  }
  function pause() { schleife.forEach(clearTimeout); schleife = []; }
  function neustart() { pause(); html.classList.remove('vorfuehrung'); lupeZeigen(false); schritt = 0; wechseln('nav', true); }
  function vorfuehren() { pause(); html.classList.add('vorfuehrung'); schritt = 0; runde(); }
  function weiter() { if (html.classList.contains('vorfuehrung') && !schleife.length) runde(); }

  tick();
  wechseln('nav', true);
  document.fonts && document.fonts.ready.then(pilleSetzen);
  // Wechsel von außen auf reduzierte Bewegung (Schalter der Detailseite): Takt neu ausrichten
  new MutationObserver(() => { clearTimeout(takt); tick(); werteZeigen(); }).observe(html, { attributes: true, attributeFilter: ['class'] });

})();
</script>
</body>
</html>