Zum Inhalt
← Animationsstile

Generatives Markenzeichen

Das Zeichen eines Kemptener Studios entsteht live aus Regeln: Die Öffnung im Ring folgt der Sonne, der Wochentag schneidet, ein Wort gibt Ringe und Weite vor, ein Zufallswert pro Besuch kommt aus CSS random() oder JS mit Saat.

Studio- oder Kulturseite, deren Zeichen im Kopf und auf der Startseite mit Uhrzeit und Datum lebt; die Woche zeigt die Verwandtschaft.

Ein Logo, das nach festen Regeln aus Daten entsteht statt als Datei vorzuliegen. Ein Gerüst bleibt immer gleich und macht das Zeichen wiedererkennbar. Einzelne Parameter folgen Regeln: Uhrzeit, Datum, eine Saat aus Text (Name, Projekt, Veranstaltung) und auf Wunsch ein Zufallswert pro Besuch. Der Zufall kommt aus CSS random(), wo der Browser es kann (Prüfung mit @supports), sonst aus JavaScript mit Saat. Wechseln die Werte, verwandelt sich das Zeichen weich in die neue Form. Jede Form lässt sich als SVG herunterladen.

Das Zeichen lebt, ohne beliebig zu werden: Das Gerüst trägt die Wiedererkennung, die Regeln erzählen etwas über Ort, Zeit oder Person. Für Studios und Kulturhäuser ist das Haltung, die man sieht. Bekannte Vorbilder sind das Zeichen des MIT Media Lab mit rund 40.000 Varianten aus drei Formen und zwölf Farben und das Wetter-Logo von Nordkyn.

Wann einsetzen

Kreativstudios, Festivals, Museen, Theater, Hochschulen, Veranstaltungsreihen. Wenn es viele Anwendungen gibt, die verwandt, aber nicht gleich sein sollen: Visitenkarten je Person, Plakate je Veranstaltung, Social-Kacheln je Tag.

Wann nicht

Bei Marken, die vor allem Verlässlichkeit ausstrahlen müssen (Kanzlei, Praxis, Bank) und wenn es kein Gerüst gibt, das alle Varianten trägt. Nicht, wenn Druckvorlagen, Favicons und Schilder ohnehin eine feste Form brauchen und niemand die Regeln pflegt.

Varianten

  • Uhrzeit: Öffnung oder Farbe wandert mit der Sonne, Tag hell, Nacht dunkel
  • Datum: Wochentag oder Tag des Jahres steuert Zahl oder Drehung von Elementen
  • Saat aus Text: Name einer Person oder Veranstaltung ergibt eine feste Variante (Visitenkarte, Plakat)
  • Zufall pro Besuch: CSS random() mit @supports, sonst JavaScript mit Saat
  • Daten von außen (Wetter, Pegel, Besucherzahl), wie bei Nordkyn

Umsetzung

Zeichen als SVG aus Parametern zeichnen. Gerüst fest (hier Ring mit Öffnung und Signalpunkt), Ausschnitte über <mask>, damit der Export transparent bleibt. Regeln als reine Funktionen: Uhrzeit → Winkel der Öffnung, Wochentag → Zahl der Schnitte, Saat-Text → FNV-1a-Hash → mulberry32 → Ringe und Öffnungsweite. Zufall pro Besuch: @property --zufall { syntax: '<number>'; inherits: true; initial-value: .5 } und @supports (opacity: random(0, 1)) { .buehne { --zufall: random(0, 1) } }; JS liest den berechneten Wert, sonst mulberry32 mit einer Saat aus sessionStorage. Übergänge: alle Zahlen mit expo.out 1200 ms tweenen, Winkel auf kürzestem Weg, diskrete Werte über Präsenz 0 → 1. Download: SVG-Text mit festen Farben als Blob. Für Druck und Favicon eine feste Grundform festlegen (z. B. 12 Uhr, Saat = Markenname). Browserstand (02.11.2026): random() ist seit Chrome 156 stabil (20.10.2026) und in Safari seit 26.2, Firefox fehlt noch; deshalb bleibt die @supports-Weiche mit JS-Rückfall. Für einen Wert je Element gilt das Schlüsselwort element-scoped, das ältere per-element lehnt Chrome 156 ab.

Robust umsetzen

Telefon. Gleich, Bühne über den Reglern. Uhrzeit-Regler und Saat-Feld groß genug für Daumen; die Woche als Raster aus sieben kleinen Zeichen.

Ohne Bewegung. Statische Form des Tages: Uhrzeit fest auf 12 Uhr, kein Zufall, kein Tween, keine Live-Uhr. Änderungen an den Reglern erscheinen sofort.

Barrierefrei. Das Zeichen ist ein Bild mit beschreibendem Namen (role="img", aria-label mit Stand und Saat). Regler mit sichtbaren Labels und Ausgabe (<output>). Kontrast des Zeichens zum Grund bei Tag und Nacht über 15 : 1. Keine Bewegung über 5 Sekunden ohne Nutzeraktion außer der langsamen Uhrzeit (eine Änderung pro Minute).

Code

<!DOCTYPE html>
<!--
  Generatives Markenzeichen
  Fundus · Animationsstile · https://cecodesign.de/lab/fundus/animationsstile/generatives-markenzeichen/
  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>Generatives Markenzeichen · 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: 'TikTok Sans'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/tiktok-sans:vf@latest/latin-wdth-normal.woff2') format('woff2-variations'); font-weight: 300 900; font-stretch: 75% 150%; font-display: swap; }
@font-face { font-family: 'DM Mono'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/dm-mono@latest/latin-400-normal.woff2') format('woff2'); font-weight: 400; font-display: swap; }
@font-face { font-family: 'DM Mono'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/dm-mono@latest/latin-500-normal.woff2') format('woff2'); font-weight: 500; font-display: swap; }

/* Zufall pro Besuch: CSS random() in einer registrierten Custom Property (ein berechneter Wert) */
@property --zufall { syntax: '<number>'; inherits: true; initial-value: 0.5; }
@supports (opacity: random(0, 1)) {
  .buehne { --zufall: random(0, 1); }
}

:root {
  /* Rezept „Studio-Bühne“: Schwarz und Weiß, eine Signalfarbe, eine Kurve */
  --schwarz: #0b0b0b; --hell: #f2f2f2; --weiss: #ffffff; --meta: #8a8a8a;
  --linie: rgba(255, 255, 255, .14); --linie-hell: rgba(11, 11, 11, .14);
  --signal: #ff5a1f;
  --kurve: cubic-bezier(0.16, 1, 0.3, 1);   /* expo.out, Cuberto */
  --t-form: 1200ms; --t-tag: 800ms; --t-ui: 300ms; --t-fade: 350ms;
  --rand: clamp(18px, 3vw, 44px);
}
* { box-sizing: border-box; margin: 0; padding: 0; }
html { background: var(--schwarz); color: var(--weiss); font-family: 'TikTok Sans', system-ui, sans-serif; font-stretch: 100%; -webkit-font-smoothing: antialiased; }
body { min-height: 100svh; display: flex; flex-direction: column; }
button, input { font: inherit; color: inherit; }
.mono { font: 500 11px/1.4 'DM Mono', ui-monospace, monospace; letter-spacing: .04em; text-transform: uppercase; color: var(--meta); }
.ziffern { font-variant-numeric: tabular-nums; }
.unsichtbar { position: absolute !important; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }

/* ---------- Demo-Leiste ---------- */
.demo { display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 8px 24px; padding: 8px var(--rand); border-bottom: 1px solid var(--linie); }
.demo .gruppe { display: flex; align-items: center; gap: 12px; }
.varianten { display: flex; gap: 2px; }
.varianten button { font: inherit; letter-spacing: inherit; text-transform: inherit; color: var(--meta); background: none; border: 1px solid transparent; border-radius: 99px; padding: 5px 10px; cursor: pointer; transition: color var(--t-ui) var(--kurve), border-color var(--t-ui) var(--kurve); }
.varianten button:hover { color: var(--weiss); }
.varianten button[aria-pressed="true"] { color: var(--weiss); border-color: var(--weiss); }
.varianten button:focus-visible { outline: 2px solid var(--signal); outline-offset: 2px; }
.werte { display: flex; flex-wrap: wrap; gap: 4px 16px; }
.werte b { color: var(--weiss); font-weight: 500; }

/* ---------- Kopf ---------- */
.kopf { display: flex; align-items: center; justify-content: space-between; gap: 20px; padding: 18px var(--rand); }
.wortmarke { display: flex; align-items: center; gap: 12px; color: inherit; text-decoration: none; }
.wortmarke [data-mini] { width: 30px; height: 30px; }
.wortmarke b { font-weight: 800; font-stretch: 75%; font-size: 26px; letter-spacing: -.02em; line-height: 1; }
.wortmarke span { font: 400 11px/1.3 'DM Mono', monospace; letter-spacing: .04em; text-transform: uppercase; color: var(--meta); max-width: 24ch; }
.kopf nav { display: flex; gap: clamp(14px, 2.4vw, 32px); }
.kopf nav a { font: 500 11px/1 'DM Mono', monospace; letter-spacing: .04em; text-transform: uppercase; color: var(--meta); text-decoration: none; padding: 8px 0; transition: color var(--t-ui) var(--kurve); }
.kopf nav a:hover, .kopf nav a[aria-current] { color: var(--weiss); }
.kopf nav a[aria-current]::before { content: ''; display: inline-block; width: 6px; height: 6px; margin-right: 8px; background: var(--signal); border-radius: 50%; vertical-align: 1px; }
.kopf nav a:focus-visible { outline: 2px solid var(--signal); outline-offset: 3px; }

/* ---------- Sektion ---------- */
.sektion { flex: 1; display: grid; grid-template-columns: minmax(0, 7fr) minmax(0, 5fr); gap: clamp(20px, 3vw, 48px); padding: 0 var(--rand) clamp(20px, 3vw, 40px); align-items: start; }

/* Bühne: Tag hell, Nacht dunkel */
.buehne { --grund: var(--hell); --zeichen: var(--schwarz); --meta-b: #6e6e6e;
  position: sticky; top: 16px; aspect-ratio: 1 / 1; max-height: calc(100svh - 150px); width: 100%; margin-inline: auto;
  background: var(--grund); color: var(--zeichen); border: 1px solid var(--linie); overflow: hidden;
  display: grid; place-items: center; transition: background-color var(--t-tag) var(--kurve), color var(--t-tag) var(--kurve); }
.buehne.nacht { --grund: #111111; --zeichen: var(--weiss); --meta-b: #8a8a8a; }
.zeichen-gross { width: 74%; aspect-ratio: 1; }
.zeichen-gross svg, [data-mini] svg, .klein svg { display: block; width: 100%; height: 100%; overflow: visible; }
.ecke { position: absolute; font: 500 11px/1.45 'DM Mono', monospace; letter-spacing: .04em; text-transform: uppercase; color: var(--meta-b); transition: color var(--t-tag) var(--kurve); }
.ecke b { color: var(--zeichen); font-weight: 500; transition: color var(--t-tag) var(--kurve); }
.ecke.ol { top: 16px; left: 18px; } .ecke.or { top: 16px; right: 18px; text-align: right; }
.ecke.ul { bottom: 16px; left: 18px; } .ecke.ur { bottom: 16px; right: 18px; text-align: right; }
.sonne { display: inline-block; width: 8px; height: 8px; border-radius: 50%; background: var(--signal); margin-right: 8px; vertical-align: 1px; }

/* Rechte Spalte */
.text { display: flex; flex-direction: column; min-width: 0; padding-top: clamp(4px, 1vw, 12px); }
.augenbraue { display: flex; align-items: center; gap: 10px; }
.augenbraue::before { content: ''; width: 22px; height: 1px; background: var(--signal); }
h1 { margin-top: 16px; font-weight: 800; font-stretch: 75%; font-size: clamp(2.75rem, 1.2rem + 4.6vw, 6rem); line-height: .9; letter-spacing: -.03em; text-wrap: balance; }
.vorspann { margin-top: 18px; font-size: 1.125rem; line-height: 1.4; color: rgba(255, 255, 255, .78); max-width: 42ch; text-wrap: pretty; }

.regeln { list-style: none; margin-top: clamp(18px, 2.4vw, 30px); border-top: 1px solid var(--linie); }
.regeln li { display: grid; grid-template-columns: 2.4em minmax(0, 1fr) auto; gap: 12px; align-items: baseline; padding: 10px 0; border-bottom: 1px solid var(--linie); }
.regeln .nr { font: 500 11px/1 'DM Mono', monospace; color: var(--meta); }
.regeln b { font-weight: 600; font-size: 15.5px; }
.regeln span.erkl { display: block; font-size: 14px; color: var(--meta); line-height: 1.35; margin-top: 2px; }
.regeln output { font: 500 12px/1.3 'DM Mono', monospace; letter-spacing: .02em; text-align: right; white-space: nowrap; }
.regeln li.neu output { color: var(--signal); }
.regeln output { transition: color var(--t-tag) var(--kurve); }

/* Regler */
.regler { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 14px 16px; margin-top: 20px; }
.feld { display: grid; gap: 7px; min-width: 0; }
.feld.voll { grid-column: 1 / -1; }
.feld > label, .feld > .titel { font: 500 11px/1 'DM Mono', monospace; letter-spacing: .04em; text-transform: uppercase; color: var(--meta); display: flex; justify-content: space-between; gap: 8px; }
.feld label output { color: var(--weiss); }
.eingabe { width: 100%; min-height: 46px; padding: 0 14px; background: transparent; border: 1px solid rgba(255, 255, 255, .32); border-radius: 0; font-size: 17px; transition: border-color var(--t-ui) var(--kurve); color-scheme: dark; }
.eingabe:hover { border-color: rgba(255, 255, 255, .6); }
.eingabe:focus-visible { outline: none; border-color: var(--weiss); box-shadow: inset 0 0 0 1px var(--weiss); }
.zeitzeile { display: flex; gap: 10px; align-items: center; }
input[type=range] { flex: 1; appearance: none; height: 46px; background: transparent; cursor: pointer; }
input[type=range]::-webkit-slider-runnable-track { height: 2px; background: linear-gradient(90deg, #333 0 25%, var(--hell) 25% 83.3%, #333 83.3%); }
input[type=range]::-moz-range-track { height: 2px; background: linear-gradient(90deg, #333 0 25%, var(--hell) 25% 83.3%, #333 83.3%); }
input[type=range]::-webkit-slider-thumb { appearance: none; width: 22px; height: 22px; margin-top: -10px; border-radius: 50%; background: var(--signal); border: 3px solid var(--schwarz); }
input[type=range]::-moz-range-thumb { width: 16px; height: 16px; border-radius: 50%; background: var(--signal); border: 3px solid var(--schwarz); }
input[type=range]:focus-visible { outline: 2px solid var(--weiss); outline-offset: 2px; }
.knopf { min-height: 46px; padding: 0 16px; border: 1px solid rgba(255, 255, 255, .32); background: none; cursor: pointer; font: 500 11px/1 'DM Mono', monospace; letter-spacing: .04em; text-transform: uppercase; white-space: nowrap; transition: background-color var(--t-ui) var(--kurve), color var(--t-ui) var(--kurve), border-color var(--t-ui) var(--kurve); }
.knopf:hover { background: var(--weiss); color: var(--schwarz); border-color: var(--weiss); }
.knopf:focus-visible { outline: 2px solid var(--signal); outline-offset: 2px; }
.knopf[aria-pressed="true"] { background: var(--weiss); color: var(--schwarz); }
.schalter { display: flex; align-items: center; gap: 12px; min-height: 46px; cursor: pointer; font-size: 15px; }
.schalter input { appearance: none; width: 44px; height: 24px; border-radius: 99px; border: 1px solid rgba(255, 255, 255, .5); position: relative; cursor: pointer; transition: background-color var(--t-ui) var(--kurve), border-color var(--t-ui) var(--kurve); flex: none; }
.schalter input::after { content: ''; position: absolute; top: 3px; left: 3px; width: 16px; height: 16px; border-radius: 50%; background: var(--weiss); transition: translate var(--t-ui) var(--kurve); }
.schalter input:checked { background: var(--signal); border-color: var(--signal); }
.schalter input:checked::after { translate: 20px 0; background: var(--schwarz); }
.schalter input:focus-visible { outline: 2px solid var(--weiss); outline-offset: 2px; }
.schalter small { display: block; font: 400 11px/1.3 'DM Mono', monospace; color: var(--meta); letter-spacing: .02em; text-transform: none; }
.laden { display: inline-flex; align-items: center; justify-content: space-between; gap: 14px; min-height: 52px; padding: 0 20px; border: 0; background: var(--signal); color: var(--schwarz); font-weight: 700; font-size: 16px; cursor: pointer; transition: scale var(--t-ui) var(--kurve), background-color var(--t-ui) var(--kurve); }
.laden:hover { background: #ff7440; }
.laden:active { scale: .97; }
.laden:focus-visible { outline: 2px solid var(--weiss); outline-offset: 3px; }
.laden svg { transition: translate var(--t-ui) var(--kurve); }
.laden:hover svg { translate: 0 2px; }

/* Leiste unten: Woche oder Team */
.leiste { padding: 0 var(--rand) clamp(22px, 3vw, 40px); }
.leiste-kopf { display: flex; justify-content: space-between; align-items: baseline; gap: 12px; padding: 12px 0; border-top: 1px solid var(--linie); }
.leiste-kopf h2 { font-weight: 700; font-stretch: 75%; font-size: clamp(1.25rem, 1rem + 1vw, 1.75rem); letter-spacing: -.01em; }
.reihe { display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); gap: 1px; background: var(--linie); border: 1px solid var(--linie); }
.reihe[data-art="team"] { grid-template-columns: repeat(4, minmax(0, 1fr)); }
.klein { position: relative; display: flex; flex-direction: column; gap: 10px; padding: 14px; background: var(--schwarz); color: var(--weiss); border: 0; text-align: left; cursor: pointer; transition: background-color var(--t-ui) var(--kurve); animation: rein var(--t-form) var(--kurve) both; }
.klein:nth-child(2) { animation-delay: 60ms; } .klein:nth-child(3) { animation-delay: 120ms; } .klein:nth-child(4) { animation-delay: 180ms; } .klein:nth-child(5) { animation-delay: 240ms; } .klein:nth-child(6) { animation-delay: 300ms; } .klein:nth-child(7) { animation-delay: 360ms; }
.klein:hover { background: #161616; }
.klein:focus-visible { outline: 2px solid var(--signal); outline-offset: -2px; }
.klein[aria-current="true"] { box-shadow: inset 0 0 0 1px var(--weiss); }
.klein[aria-current="true"]::after { content: ''; position: absolute; top: 12px; right: 12px; width: 6px; height: 6px; border-radius: 50%; background: var(--signal); }
.klein .bild { width: min(100%, 120px); aspect-ratio: 1; margin: 0 auto; color: var(--weiss); }
.klein .mono { display: block; }
.klein b { font-weight: 600; font-size: 15px; }
.reihe[data-art="team"] .klein { flex-direction: row; align-items: center; gap: 14px; background: var(--hell); color: var(--schwarz); }
.reihe[data-art="team"] .klein:hover { background: #ffffff; }
.reihe[data-art="team"] .klein .bild { width: 64px; margin: 0; flex: none; color: var(--schwarz); }
.reihe[data-art="team"] .klein[aria-current="true"] { box-shadow: inset 0 0 0 2px var(--signal); }

@keyframes rein { from { opacity: 0; translate: 0 12px; } }

/* Vorschau 960 × 720: nur die Bühne */
.vorschau body { overflow: hidden; height: 100svh; }
.vorschau .demo, .vorschau .leiste, .vorschau .kopf nav, .vorschau .regler { display: none; }
.vorschau .vorspann { font-size: 1rem; }
.vorschau .kopf { padding-block: 14px; }
.vorschau .buehne { max-height: calc(100svh - 92px); }
.vorschau h1 { font-size: 4.25rem; }

/* Breiten */
@media (max-width: 1100px) {
  .sektion { grid-template-columns: minmax(0, 6fr) minmax(0, 5fr); }
  .regeln span.erkl { display: none; }
  .demo .werte span:nth-child(n+3) { display: none; }
}
@media (max-width: 820px) {
  .sektion { grid-template-columns: minmax(0, 1fr); }
  .buehne { max-height: none; position: relative; top: auto; }
  .kopf nav a:not([aria-current]) { display: none; }
  .wortmarke span { display: none; }
  .reihe { grid-template-columns: repeat(4, minmax(0, 1fr)); }
  .reihe[data-art="team"] { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .demo .werte { display: none; }
}
@media (max-width: 520px) {
  .regler { grid-template-columns: minmax(0, 1fr); }
  .laden { width: 100%; }
  .reihe { grid-template-columns: repeat(4, minmax(0, 1fr)); }
  .klein { padding: 10px 8px; gap: 6px; }
  .klein .mono { font-size: 10px; }
  .reihe[data-art="team"] { grid-template-columns: minmax(0, 1fr); }
  .ecke { font-size: 10px; }
  .ecke.ol, .ecke.ul { left: 12px; } .ecke.or, .ecke.ur { right: 12px; }
  .ecke.ol, .ecke.or { top: 10px; } .ecke.ul, .ecke.ur { bottom: 10px; }
}
/* Touch: keine Hover-Gesten, größere Ziele */
@media (hover: none) {
  .klein:hover { background: var(--schwarz); }
  .reihe[data-art="team"] .klein:hover { background: var(--hell); }
  .laden:hover svg { translate: none; }
  input[type=range]::-webkit-slider-thumb { width: 28px; height: 28px; margin-top: -13px; }
  .eingabe, .knopf { min-height: 50px; }
}
/* Reduzierte Bewegung: statische Form des Tages, Wechsel ohne Dauer */
.reduziert *, .reduziert *::before, .reduziert *::after { transition-duration: .01ms !important; animation-duration: .01ms !important; animation-delay: 0ms !important; }
@media (forced-colors: active) { button, [role=button], [role=tab], [role=switch] { border: 1px solid ButtonText !important; } }
</style>
</head>
<body>
<div class="demo mono">
  <div class="gruppe">
    <span>Variante</span>
    <div class="varianten" role="group" aria-label="Anwendung des Zeichens">
      <button type="button" data-v="tag" aria-pressed="true">Tag und Woche</button>
      <button type="button" data-v="team" aria-pressed="false">Team-Karten</button>
    </div>
  </div>
  <p class="werte" data-werte></p>
</div>

<header class="kopf">
  <a class="wortmarke" href="#"><span data-mini aria-hidden="true"></span><b>CAMBO</b><span>Studio für Bewegung und Marke, Kempten</span></a>
  <nav aria-label="Hauptnavigation"><a href="#">Arbeiten</a><a href="#">Studio</a><a href="#zeichen" aria-current="page">Zeichen</a><a href="#">Kontakt</a></nav>
</header>

<main class="sektion" id="zeichen">
  <figure class="buehne" data-buehne>
    <div class="zeichen-gross" role="img" data-zeichen aria-label="Zeichen von Cambo"></div>
    <p class="ecke ol ziffern" data-ecke="ol"></p>
    <p class="ecke or" data-ecke="or"></p>
    <p class="ecke ul" data-ecke="ul"></p>
    <p class="ecke ur ziffern" data-ecke="ur"></p>
  </figure>

  <div class="text">
    <p class="mono augenbraue">Identität · Regelwerk</p>
    <h1>Jeden Tag ein anderes Zeichen.</h1>
    <p class="vorspann">Unser Zeichen ist kein Bild, sondern eine Regel. Ring, Öffnung und Punkt bleiben. Wo die Öffnung steht, wie oft der Ring geschnitten ist und wie viele Ringe er hat, entscheiden Uhrzeit, Datum und ein Wort.</p>

    <ol class="regeln" data-regeln>
      <li><span class="nr">01</span><p><b>Gerüst</b><span class="erkl">Ring, Öffnung, Punkt in Signalorange</span></p><output>fest</output></li>
      <li data-regel="zeit"><span class="nr">02</span><p><b>Uhrzeit</b><span class="erkl">Die Öffnung folgt der Sonne, ab 20 Uhr Nacht</span></p><output data-out="zeit"></output></li>
      <li data-regel="datum"><span class="nr">03</span><p><b>Datum</b><span class="erkl">Wochentag gibt die Zahl der Schnitte</span></p><output data-out="datum"></output></li>
      <li data-regel="saat"><span class="nr">04</span><p><b>Saat</b><span class="erkl">Ein Wort ergibt Ringe und Weite der Öffnung</span></p><output data-out="saat"></output></li>
      <li data-regel="zufall"><span class="nr">05</span><p><b>Zufall</b><span class="erkl">Einmal pro Besuch, der Punkt rutscht in der Öffnung</span></p><output data-out="zufall"></output></li>
    </ol>

    <div class="regler">
      <div class="feld voll">
        <label for="saat">Saat <span>Name, Projekt, Anlass</span></label>
        <input class="eingabe" id="saat" type="text" value="Cambo" maxlength="40" autocomplete="off" spellcheck="false" data-saat>
      </div>
      <div class="feld voll">
        <label for="zeit">Uhrzeit <output for="zeit" class="ziffern" data-zeit-text>12:00</output></label>
        <div class="zeitzeile">
          <input id="zeit" type="range" min="0" max="1439" step="1" value="720" data-zeit aria-describedby="zeit-h">
          <button type="button" class="knopf" data-jetzt aria-pressed="true">Jetzt</button>
        </div>
        <span class="unsichtbar" id="zeit-h">Uhrzeit in Kempten, Minuten seit Mitternacht. Jetzt folgt der echten Uhr.</span>
      </div>
      <div class="feld">
        <label for="datum">Datum</label>
        <input class="eingabe ziffern" id="datum" type="date" data-datum>
      </div>
      <div class="feld">
        <span class="titel">Zufall</span>
        <label class="schalter"><input type="checkbox" checked data-zufall-an><span>Pro Besuch<small data-zufall-quelle>prüfe random()</small></span></label>
      </div>
      <div class="feld voll">
        <button type="button" class="laden" data-laden><span>Diese Form als SVG laden</span><svg width="14" height="16" viewBox="0 0 14 16" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true"><path d="M7 1v11M2.5 7.5 7 12l4.5-4.5M1 15h12"/></svg></button>
      </div>
    </div>
  </div>
</main>

<section class="leiste" aria-labelledby="leiste-t">
  <div class="leiste-kopf"><h2 id="leiste-t" data-leiste-titel>Diese Woche, gleiche Uhrzeit</h2><p class="mono" data-leiste-meta>Tippen setzt das Datum</p></div>
  <div class="reihe" data-reihe></div>
</section>

<script>
(() => {
  const html = document.documentElement;
  const reduziert = () => html.classList.contains('reduziert');
  const $ = (s, r = document) => r.querySelector(s);
  const $$ = (s, r = document) => [...r.querySelectorAll(s)];
  const T = { form: 1200, ui: 300 };   // ms · Kurve cubic-bezier(0.16, 1, 0.3, 1)
  const expoOut = t => t >= 1 ? 1 : 1 - Math.pow(2, -10 * t);   // expo.out, Kurve wie --kurve

  /* ---------- Saat: FNV-1a → mulberry32 ---------- */
  function hash(text) {
    let h = 0x811c9dc5;
    for (const z of String(text).trim().toLowerCase()) { h ^= z.codePointAt(0); h = Math.imul(h, 0x01000193); }
    return h >>> 0;
  }
  function mulberry32(a) {
    return () => { a |= 0; a = a + 0x6D2B79F5 | 0; let t = Math.imul(a ^ a >>> 15, 1 | a); t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t; return ((t ^ t >>> 14) >>> 0) / 4294967296; };
  }

  /* ---------- Zufall: CSS random() oder JS mit Saat ---------- */
  const buehne = $('[data-buehne]');
  const cssZufall = !!(window.CSS && CSS.supports && CSS.supports('opacity', 'random(0, 1)'));
  let zufallWert = 0.5, zufallQuelle = '';
  if (cssZufall) {
    zufallWert = parseFloat(getComputedStyle(buehne).getPropertyValue('--zufall')) || 0.5;
    zufallQuelle = 'CSS random()';
  } else {
    let s = Number(sessionStorage.getItem('cambo-besuch'));
    if (!s) { s = Date.now() % 4294967296; try { sessionStorage.setItem('cambo-besuch', s); } catch (e) {} }
    zufallWert = mulberry32(s)();
    zufallQuelle = 'JS mulberry32 · Saat ' + s;
  }

  /* ---------- Regeln ---------- */
  const NACHT_AB = 20, TAG_AB = 6;
  const R = 100, r = 56, PUNKT = 15, MITTE = 78;
  // 06:00 rechts (Osten), 12:00 oben, 18:00 links, 00:00 unten
  const winkelVon = min => -((min / 60 - 6) / 24) * 360;
  function regeln({ minuten, datum, saat, zufall }) {
    const rnd = mulberry32(hash(saat || 'Cambo'));
    const gamma = 52 + rnd() * 24;                        // Öffnung 52–76°
    const ringe = 2 + Math.floor(rnd() * 4);              // 2–5 Ringe
    const gew = Array.from({ length: ringe }, () => .6 + rnd() * .8), summe = gew.reduce((a, b) => a + b, 0);
    let acc = 0;
    const trenner = Array.from({ length: 4 }, (_, i) => {
      if (i < ringe - 1) { acc += gew[i]; return { r: r + (R - r) * acc / summe, p: 1 }; }
      return { r: R - 1, p: 0 };
    });
    const wt = (datum.getDay() || 7);                     // Mo 1 … So 7
    const jt = Math.floor((datum - new Date(datum.getFullYear(), 0, 0)) / 864e5);
    const phi0 = jt / 366 * 360 + rnd() * 30;
    const theta = winkelVon(minuten);
    const schnitte = Array.from({ length: 7 }, (_, i) => ({ a: phi0 + Math.min(i, wt - 1) * 360 / wt, p: i < wt ? 1 : 0 }));
    const versatz = (zufall - .5) * gamma * .45;
    return { theta, gamma, trenner, schnitte, punkt: theta + versatz, nacht: minuten < TAG_AB * 60 || minuten >= NACHT_AB * 60, info: { ringe, wt, jt } };
  }

  /* ---------- Zeichnen (SVG mit Maske, damit der Export transparent bleibt) ---------- */
  const rad = g => g * Math.PI / 180;
  const pkt = (rr, g) => [+(rr * Math.cos(rad(g))).toFixed(2), +(rr * Math.sin(rad(g))).toFixed(2)];
  let maskeNr = 0;
  function zeichenSVG(p, { farbe = 'currentColor', titel = '', breite } = {}) {
    const id = 'cm' + (++maskeNr);
    const [x1, y1] = pkt(R + 12, p.theta - p.gamma / 2), [x2, y2] = pkt(R + 12, p.theta + p.gamma / 2);
    const keil = `M0 0L${x1} ${y1}A${R + 12} ${R + 12} 0 0 1 ${x2} ${y2}Z`;
    const ringe = p.trenner.filter(t => t.p > .01).map(t => `<circle r="${t.r.toFixed(2)}" fill="none" stroke="#000" stroke-width="${(2.6 * t.p).toFixed(2)}"/>`).join('');
    const schnitte = p.schnitte.filter(s => s.p > .01).map(s => { const [a, b] = pkt(r - 4, s.a), [c, d] = pkt(R + 4, s.a); return `<line x1="${a}" y1="${b}" x2="${c}" y2="${d}" stroke="#000" stroke-width="${(3.4 * s.p).toFixed(2)}"/>`; }).join('');
    const [px, py] = pkt(MITTE, p.punkt);
    const groesse = breite ? ` width="${breite}" height="${breite}"` : '';
    return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="-120 -120 240 240"${groesse}${titel ? '' : ' aria-hidden="true"'}>${titel ? `<title>${titel}</title>` : ''}` +
      `<defs><mask id="${id}" maskUnits="userSpaceOnUse" x="-120" y="-120" width="240" height="240"><rect x="-120" y="-120" width="240" height="240" fill="#fff"/><circle r="${r}" fill="#000"/><path d="${keil}" fill="#000"/>${ringe}${schnitte}</mask></defs>` +
      `<circle r="${R}" fill="${farbe}" mask="url(#${id})"/><circle cx="${px}" cy="${py}" r="${PUNKT}" fill="#ff5a1f"/></svg>`;
  }

  /* ---------- Zustand ---------- */
  const eingabe = { saat: $('[data-saat]'), zeit: $('[data-zeit]'), datum: $('[data-datum]'), zufall: $('[data-zufall-an]') };
  const berlin = new Intl.DateTimeFormat('de-DE', { timeZone: 'Europe/Berlin', year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', hourCycle: 'h23' });
  function jetztInKempten() {
    const t = Object.fromEntries(berlin.formatToParts(new Date()).map(x => [x.type, x.value]));
    return { minuten: +t.hour * 60 + +t.minute, iso: `${t.year}-${t.month}-${t.day}` };
  }
  let live = true;
  const zustand = { minuten: 720, datum: null, saat: 'Cambo', zufallAn: true };
  const isoZuDatum = iso => { const [j, m, d] = iso.split('-').map(Number); return new Date(j, m - 1, d, 12); };
  const datumZuIso = d => `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
  const hhmm = min => `${String(Math.floor(min / 60) % 24).padStart(2, '0')}:${String(Math.floor(min % 60)).padStart(2, '0')}`;
  const ziel = () => regeln({ minuten: zustand.minuten, datum: zustand.datum, saat: zustand.saat, zufall: zustand.zufallAn ? zufallWert : .5 });

  /* ---------- Tween zwischen zwei Parametersätzen ---------- */
  let aktuell = null, tween = null;
  const lerp = (a, b, t) => a + (b - a) * t;
  const lerpWinkel = (a, b, t) => { const d = ((b - a) % 360 + 540) % 360 - 180; return a + d * t; };
  function mischen(a, b, t) {
    return {
      theta: lerpWinkel(a.theta, b.theta, t), gamma: lerp(a.gamma, b.gamma, t), punkt: lerpWinkel(a.punkt, b.punkt, t), nacht: b.nacht, info: b.info,
      trenner: a.trenner.map((x, i) => ({ r: lerp(x.r, b.trenner[i].r, t), p: lerp(x.p, b.trenner[i].p, t) })),
      schnitte: a.schnitte.map((x, i) => ({ a: lerpWinkel(x.a, b.schnitte[i].a, t), p: lerp(x.p, b.schnitte[i].p, t) })),
    };
  }
  const gross = $('[data-zeichen]'), mini = $('[data-mini]');
  function malen(p) { gross.innerHTML = zeichenSVG(p); mini.innerHTML = zeichenSVG({ ...p, punkt: p.punkt }); }
  function zeigen(sofort) {
    const z = ziel();
    buehne.classList.toggle('nacht', z.nacht);
    texteSetzen(z);
    cancelAnimationFrame(tween);
    if (!aktuell || sofort || reduziert()) { aktuell = z; malen(z); return; }
    const von = aktuell, t0 = performance.now();
    const schritt = jetzt => {
      const t = Math.min(1, (jetzt - t0) / T.form);
      aktuell = mischen(von, z, expoOut(t)); malen(aktuell);
      if (t < 1) tween = requestAnimationFrame(schritt);
    };
    tween = requestAnimationFrame(schritt);
  }

  /* ---------- Texte, Ecken, Werte ---------- */
  const wtag = new Intl.DateTimeFormat('de-DE', { weekday: 'short' });
  const wtagLang = new Intl.DateTimeFormat('de-DE', { weekday: 'long' });
  const kurzDatum = new Intl.DateTimeFormat('de-DE', { day: 'numeric', month: 'numeric' });
  const RICHTUNG = ['Ost', 'Südost', 'Süd', 'Südwest', 'West', 'Nordwest', 'Nord', 'Nordost'];
  let letzte = {};
  function texteSetzen(z) {
    const zeit = hhmm(zustand.minuten), d = zustand.datum, saat = zustand.saat || 'Cambo';
    const out = {
      zeit: `${zeit} · Sonne ${RICHTUNG[Math.round((((zustand.minuten / 60 - 6) % 24 + 24) % 24) / 3) % 8]}`,
      datum: `${wtag.format(d).replace('.', '')} · ${z.info.wt} ${z.info.wt === 1 ? 'Schnitt' : 'Schnitte'}`,
      saat: `${z.info.ringe} Ringe · Öffnung ${Math.round(z.gamma)}°`,
      zufall: zustand.zufallAn ? `${zufallWert.toFixed(3).replace('.', ',')} · ${cssZufall ? 'CSS' : 'JS'}` : 'aus',
    };
    Object.entries(out).forEach(([k, v]) => {
      const el = $(`[data-out="${k}"]`);
      if (el.textContent !== v) { el.textContent = v; const li = el.closest('li'); if (letzte[k] !== undefined && !vorfuehrt) { li.classList.add('neu'); clearTimeout(li._u); li._u = setTimeout(() => li.classList.remove('neu'), 900); } }
      letzte[k] = v;
    });
    $('[data-zeit-text]').textContent = zeit;
    $('[data-ecke="ol"]').innerHTML = `Zeichen <b>${d.getFullYear()}-${String(z.info.jt).padStart(3, '0')}</b><br>${kurzDatum.format(d)} · ${zeit}`;
    $('[data-ecke="or"]').innerHTML = `<span class="sonne"></span>${z.nacht ? 'Nacht' : 'Tag'}<br>Kempten`;
    $('[data-ecke="ul"]').innerHTML = `Saat<br><b>${esc(saat.length > 22 ? saat.slice(0, 21) + '…' : saat)}</b>`;
    $('[data-ecke="ur"]').innerHTML = `Zufall<br>${zustand.zufallAn ? zufallWert.toFixed(3).replace('.', ',') : '–'}`;
    gross.setAttribute('aria-label', `Zeichen von Cambo für ${wtagLang.format(d)}, ${kurzDatum.format(d)}, ${zeit} Uhr, Saat „${saat}“: Ring mit ${z.info.ringe} Ringen und ${z.info.wt} Schnitten, Öffnung ${z.nacht ? 'bei Nacht' : 'bei Tag'}.`);
    $('[data-werte]').innerHTML = `<span>Form <b>${T.form} ms</b> expo.out</span><span>Tag/Nacht <b>800 ms</b></span><span>Öffnung <b>${Math.round(z.gamma)}°</b> · Ringe <b>${z.info.ringe}</b> · Schnitte <b>${z.info.wt}</b></span><span>Zufall <b>${cssZufall ? 'CSS random()' : 'JS mit Saat'}</b></span>`;
  }
  const esc = s => String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');

  /* ---------- Leiste: Woche oder Team ---------- */
  const TEAM = [['Vroni Kustermann', 'Gestaltung'], ['Severin Haug', 'Motion'], ['Ida Rasch', 'Code'], ['Lorenz Biechele', 'Produktion']];
  const reihe = $('[data-reihe]');
  function leisteBauen() {
    reihe.dataset.art = variante;
    if (variante === 'team') {
      $('[data-leiste-titel]').textContent = 'Visitenkarten, eine Saat je Person';
      $('[data-leiste-meta]').textContent = 'Tippen setzt die Saat';
      reihe.innerHTML = TEAM.map(([n, rolle]) => {
        const p = regeln({ minuten: zustand.minuten, datum: zustand.datum, saat: n, zufall: .5 });
        return `<button type="button" class="klein" data-person="${esc(n)}" aria-current="${n === zustand.saat}"><span class="bild" aria-hidden="true">${zeichenSVG(p)}</span><span><b>${esc(n)}</b><span class="mono">${rolle} · Cambo</span></span></button>`;
      }).join('');
    } else {
      $('[data-leiste-titel]').textContent = 'Diese Woche, gleiche Uhrzeit';
      $('[data-leiste-meta]').textContent = 'Tippen setzt das Datum';
      const d = zustand.datum, mo = new Date(d); mo.setDate(d.getDate() - ((d.getDay() || 7) - 1));
      reihe.innerHTML = Array.from({ length: 7 }, (_, i) => {
        const t = new Date(mo); t.setDate(mo.getDate() + i);
        const p = regeln({ minuten: zustand.minuten, datum: t, saat: zustand.saat, zufall: .5 });
        return `<button type="button" class="klein" data-tag="${datumZuIso(t)}" aria-current="${datumZuIso(t) === datumZuIso(d)}" aria-label="${wtagLang.format(t)}, ${kurzDatum.format(t)}: ${p.info.wt} Schnitte"><span class="bild" aria-hidden="true">${zeichenSVG(p)}</span><span class="mono ziffern">${wtag.format(t).replace('.', '')} ${kurzDatum.format(t)}</span></button>`;
      }).join('');
    }
  }
  reihe.addEventListener('click', e => {
    const k = e.target.closest('.klein'); if (!k) return;
    stopp();
    if (k.dataset.person) { eingabe.saat.value = zustand.saat = k.dataset.person; }
    if (k.dataset.tag) { eingabe.datum.value = k.dataset.tag; zustand.datum = isoZuDatum(k.dataset.tag); }
    $$('.klein', reihe).forEach(x => x.setAttribute('aria-current', x === k));
    zeigen();
  });

  /* ---------- Varianten ---------- */
  const qv = new URLSearchParams(location.search).get('v');
  let variante = qv === 'team' ? 'team' : 'tag';
  function varianteSetzen(v, url) {
    variante = v === 'team' ? 'team' : 'tag';
    $$('.varianten button').forEach(b => b.setAttribute('aria-pressed', b.dataset.v === variante));
    if (url) { const q = new URLSearchParams(location.search); q.set('v', variante); history.replaceState(null, '', '?' + q.toString() + location.hash); }
    eingabe.saat.value = zustand.saat = variante === 'team' ? TEAM[0][0] : 'Cambo';
    leisteBauen(); zeigen();
  }
  $$('.varianten button').forEach(b => b.addEventListener('click', () => { stopp(); varianteSetzen(b.dataset.v, true); }));

  /* ---------- Regler ---------- */
  let leisteUhr = null;
  const leisteSpaeter = () => { clearTimeout(leisteUhr); leisteUhr = setTimeout(leisteBauen, 260); };
  eingabe.saat.addEventListener('input', () => { stopp(); zustand.saat = eingabe.saat.value.trim() || 'Cambo'; zeigen(); if (variante === 'tag') leisteSpaeter(); else $$('.klein', reihe).forEach(x => x.setAttribute('aria-current', x.dataset.person === zustand.saat)); });
  eingabe.zeit.addEventListener('input', () => { stopp(); live = false; $('[data-jetzt]').setAttribute('aria-pressed', 'false'); zustand.minuten = +eingabe.zeit.value; zeigen(true); leisteSpaeter(); });
  eingabe.datum.addEventListener('change', () => { if (!eingabe.datum.value) return; stopp(); zustand.datum = isoZuDatum(eingabe.datum.value); live = false; $('[data-jetzt]').setAttribute('aria-pressed', 'false'); zeigen(); leisteBauen(); });
  eingabe.zufall.addEventListener('change', () => { stopp(); zustand.zufallAn = eingabe.zufall.checked; zeigen(); });
  $('[data-jetzt]').addEventListener('click', () => { stopp(); live = true; $('[data-jetzt]').setAttribute('aria-pressed', 'true'); jetztSetzen(); zeigen(); leisteBauen(); });
  function jetztSetzen() {
    const j = jetztInKempten();
    zustand.minuten = j.minuten; zustand.datum = isoZuDatum(j.iso);
    eingabe.zeit.value = j.minuten; eingabe.datum.value = j.iso;
  }
  // Live: einmal pro Minute nachziehen (nicht bei reduzierter Bewegung, dort bleibt die Form des Tages)
  setInterval(() => { if (live && !vorfuehrt && !reduziert()) { const alt = zustand.minuten; jetztSetzen(); if (alt !== zustand.minuten) zeigen(); } }, 30000);

  /* ---------- Download als SVG ---------- */
  $('[data-laden]').addEventListener('click', () => {
    const z = ziel(), farbe = z.nacht ? '#ffffff' : '#0b0b0b';
    const name = `cambo-zeichen-${datumZuIso(zustand.datum)}-${hhmm(zustand.minuten).replace(':', '')}-${(zustand.saat || 'cambo').toLowerCase().normalize('NFKD').replace(/[^\w]+/g, '-').replace(/^-|-$/g, '').slice(0, 30)}.svg`;
    const svg = '<?xml version="1.0" encoding="UTF-8"?>\n' + zeichenSVG(z, { farbe, breite: 1024, titel: `Cambo, ${datumZuIso(zustand.datum)} ${hhmm(zustand.minuten)}, Saat ${esc(zustand.saat)}` });
    const url = URL.createObjectURL(new Blob([svg], { type: 'image/svg+xml' }));
    const a = document.createElement('a'); a.href = url; a.download = name; document.body.append(a); a.click(); a.remove();
    setTimeout(() => URL.revokeObjectURL(url), 2000);
  });

  /* ---------- Vorführung ---------- */
  let lauf = 0, vorfuehrt = false, raf = null;
  const schlaf = ms => new Promise(r => setTimeout(r, ms));
  function stopp() { lauf++; vorfuehrt = false; cancelAnimationFrame(raf); html.classList.remove('vorfuehrung'); }
  function zeitraffer(ms, von, meinLauf) {
    // 24 Stunden in ms Millisekunden, Form folgt direkt (ohne Tween)
    return new Promise(res => {
      const t0 = performance.now();
      const schritt = jetzt => {
        if (meinLauf !== lauf) return res();
        const t = Math.min(1, (jetzt - t0) / ms);
        zustand.minuten = (von + t * 1440) % 1440; eingabe.zeit.value = Math.floor(zustand.minuten);
        zeigen(true);
        if (t < 1) raf = requestAnimationFrame(schritt); else res();
      };
      raf = requestAnimationFrame(schritt);
    });
  }
  async function tippen(text, meinLauf) {
    eingabe.saat.value = '';
    for (let i = 1; i <= text.length; i++) {
      if (meinLauf !== lauf) return;
      eingabe.saat.value = text.slice(0, i); zustand.saat = text.slice(0, i); zeigen();
      await schlaf(110);
    }
  }
  async function runde(meinLauf) {
    const ok = () => meinLauf === lauf;
    const start = jetztInKempten();
    zustand.datum = isoZuDatum(start.iso); eingabe.datum.value = start.iso;
    if (variante === 'team') {
      for (const [n] of TEAM) {
        if (!ok()) return;
        eingabe.saat.value = zustand.saat = n; zeigen();
        $$('.klein', reihe).forEach(x => x.setAttribute('aria-current', x.dataset.person === n));
        await schlaf(reduziert() ? 2200 : 1900);
      }
      if (!ok()) return;
      if (!reduziert()) await zeitraffer(5000, zustand.minuten, meinLauf);
      return;
    }
    eingabe.saat.value = zustand.saat = 'Cambo';
    if (!reduziert()) { await zeitraffer(6000, 6 * 60, meinLauf); if (!ok()) return; }
    else { zustand.minuten = 720; eingabe.zeit.value = 720; zeigen(true); }
    if (!reduziert()) { zustand.minuten = 14 * 60 + 30; eingabe.zeit.value = zustand.minuten; zeigen(); }
    await schlaf(900); if (!ok()) return;
    await tippen('Illerufer 2027', meinLauf); if (!ok()) return;
    await schlaf(1300); if (!ok()) return;
    await tippen('Cambo', meinLauf); if (!ok()) return;
    await schlaf(900);
    for (let i = 1; i <= 3; i++) {
      if (!ok()) return;
      const d = new Date(zustand.datum); d.setDate(d.getDate() + 1); zustand.datum = d; eingabe.datum.value = datumZuIso(d);
      $$('.klein', reihe).forEach(x => x.setAttribute('aria-current', x.dataset.tag === datumZuIso(d)));
      zeigen(); await schlaf(1500);
    }
  }
  async function vorfuehren() {
    stopp(); vorfuehrt = true; live = false; html.classList.add('vorfuehrung');
    const meinLauf = lauf;
    while (meinLauf === lauf) { await runde(meinLauf); if (meinLauf === lauf) await schlaf(600); }
  }
  function neustart() {
    stopp(); live = !reduziert(); $('[data-jetzt]').setAttribute('aria-pressed', live);
    jetztSetzen();
    if (reduziert()) { zustand.minuten = 720; eingabe.zeit.value = 720; }   // statische Form des Tages
    zustand.zufallAn = eingabe.zufall.checked = !reduziert();
    eingabe.saat.value = zustand.saat = variante === 'team' ? TEAM[0][0] : 'Cambo';
    aktuell = null; leisteBauen(); zeigen(true);
  }
  function pause() { stopp(); }
  function weiter() { vorfuehren(); }


  $('[data-zufall-quelle]').textContent = zufallQuelle;
  $$('.varianten button').forEach(b => b.setAttribute('aria-pressed', b.dataset.v === variante));
  neustart();
  // Die Detailseite setzt .reduziert evtl. erst nach dem Start: dann statische Form des Tages herstellen
  let warReduziert = reduziert();
  new MutationObserver(() => { if (reduziert() !== warReduziert) { warReduziert = reduziert(); neustart(); } }).observe(html, { attributes: true, attributeFilter: ['class'] });
})();
</script>
</body>
</html>