KI-Streaming-Text und Denk-Indikator mit Shimmer
Ein Archiv-Assistent zeigt mit wanderndem Glanz, woran er gerade arbeitet, und streamt seine Antwort Wort für Wort.
Studio, Agentur, SaaS oder Kursanbieter mit großem eigenem Bestand (Projekte, Kurse, Dokumentation), der sich per Frage durchsuchen lässt; zeigt Projektkarten unter dem Generierungs-Overlay oder Quellen-Chips.
Während ein KI-Modell arbeitet, zeigt ein Status-Label wie „Denkt nach“, „Durchsucht 212 Projekte“ oder „Schreibt die Antwort“ einen wandernden Glanz. Ein hellerer Streifen läuft endlos über den Text, als Verlauf (background-clip: text), als Maske oder als Pseudo-Element. Zwischenschritte wie Suchen, Quellen lesen oder Tool-Aufrufe erscheinen als einklappbare Zeilen mit eigenem Glanz. Abgeschlossene Schritte verlieren den Glanz und bekommen ein Häkchen. Sobald die Antwort eintrifft, erscheint sie wortweise: Jedes neue Wort blendet in 80 ms von opacity .4 auf 1, statt Zeichen für Zeichen getippt zu werden. Bereiche, die noch generiert werden (Bilder, Code-Vorschau), liegen unter einem pulsierenden Overlay.
Die Wartezeit wird sichtbar und ehrlich: Es passiert etwas, ohne dass ein erfundener Prozentbalken nötig ist. Der Glanz ist inzwischen die gelernte Bildsprache für „KI arbeitet“. Benannte Schritte zeigen, was gerade geschieht, und schaffen Vertrauen. Das wortweise Einblenden glättet ungleichmäßige Token-Pakete, der Text wirkt dadurch flüssig.
Wann einsetzen
Chat- und Assistenz-Oberflächen, KI-Suche im Header (siehe command-palette-und-ki-suche), Generatoren, Formular-Assistenten und Demo-Animationen auf Produktseiten von KI-Werkzeugen. Als Frag-uns-Feld auch für Praxis, Kanzlei oder Hotel, wenn die Seite genug eigene Inhalte hat (Leistungen, Kasse und privat, Anfahrt, Zeiten) und jemand die Antworten pflegt.
Wann nicht
Bei normalen Ladezuständen ohne KI, dort passen Skeleton oder Spinner. Nicht als Dauerdekor auf Headlines (siehe shimmer-gradient-text). Nicht bei Wartezeiten unter etwa 300 ms, sonst flackert das Label. Kein KI-Feld ohne sichtbaren KI-Hinweis beim Start und ohne Ausweg zum Menschen (Telefon, Kontakt). In Praxis und Kanzlei keine Antworten zu Diagnose oder Rechtsfall, dort immer an das Telefon verweisen.
Varianten
- Text-Shimmer per background-clip: text und background-position (Cursor 2,5 s, Bolt 1,5 s)
- Masken-Shimmer per mask-image und mask-position (Perplexity 1,2 s und 1,8 s)
- Shimmer als Pseudo-Element mit eigenem Blend-Mode und Pause im Zyklus (Gemini 2 s)
- Wortweises Fade-in der Stream-Tokens (Cursor: 80 ms, opacity .4 → 1)
- Pulsierendes Overlay über generierten Bereichen (v0)
- Schritt-Liste mit Status: aktiv mit Glanz, erledigt mit Häkchen und ohne Glanz
- Frag-uns-Feld für KMU: antwortet nur aus eigenen Seiten, Quellen-Links sind Pflicht, ohne Quelle das Telefon als Ausweg
Umsetzung
Status-Label: .glanzt { background: linear-gradient(90deg, var(--muted) 0 30%, var(--fg) 55%, var(--muted) 70% 100%); background-size: 200% 100%; background-clip: text; color: transparent; animation: shimmer 2.5s linear infinite } @keyframes shimmer { from { background-position: 200% 0 } to { background-position: -200% 0 } }. Masken-Variante: mask-image: linear-gradient(-70deg, #000 50%, #0005, #000 65%); mask-size: 300% 100%; mask-position von 100% nach 0. Streaming: eintreffende Tokens zu Wörtern puffern und jedes Wort als <span class=w> anhängen; .w { animation: word-in 80ms ease-out both } @keyframes word-in { from { opacity: .4 } }. Label erst nach etwa 300 ms Wartezeit zeigen. Fertige Schritte: Klasse entfernen, Icon tauschen. Beim Pseudo-Element mit mix-blend-mode darf kein Vorfahr einen eigenen Stacking-Kontext bilden (opacity-Animation mit fill forwards, isolation), sonst blendet der Streifen gegen Transparenz und wird als Fläche sichtbar. KI-Hinweis als eigene Zeile zwischen Kopf und Verlauf (.ki-hinweis: Kürzel „KI“ als Marke, ein fetter Satz „Sie schreiben mit einem KI-Assistenten.“, danach in einem Halbsatz, woraus er antwortet und wo seine Grenze ist). Frag-uns-Feld für KMU: Antworten nur aus den eigenen Seiten (Index der eigenen Seiten, keine offene Websuche); jede Antwort endet mit mindestens einem Quellen-Link auf die eigene Unterseite; findet der Assistent keine Quelle, erfindet er nichts, sondern zeigt die Telefonnummer als großen Knopf (tel:) und bei Gesundheitsthemen den Bereitschaftsdienst 116 117. Die Telefonnummer steht zusätzlich dauerhaft im KI-Hinweis.
Robust umsetzen
Telefon. Verhalten wie am Desktop. Lange Schritt-Listen standardmäßig einklappen, das Label mit Glanz reicht als Hinweis.
Ohne Bewegung. Glanz abschalten (Gemini und Bolt machen es vor) und den Status als Text zeigen, etwa „· läuft…“. Wörter ohne Fade anhängen. Overlays über generierten Bereichen statisch lassen.
Barrierefrei. Antwortbereich mit aria-live=polite und aria-busy=true während der Generierung, damit nicht jedes Wort einzeln angesagt wird. Status-Label mit role=status. Statuswechsel immer als Text, nicht nur über den Glanz. Den gedimmten Grundton mit mindestens 4,5 : 1 Kontrast halten. Einklappbare Schritt-Liste über einen Knopf mit aria-expanded und aria-controls. KI-Kennzeichnung (Art. 50 KI-VO, seit 02.08.2026): Dass hier eine KI antwortet, steht direkt beim Start der Unterhaltung sichtbar im Fenster, nicht erst in Impressum oder Datenschutz, etwa „Sie schreiben mit einem KI-Assistenten.“ (Formulierung nach IHK Halle; eine einmalige Anzeige genügt in der Regel). Der Hinweis ist normaler Text vor dem Verlauf, keine Live-Region, und bleibt beim Wechsel der Frage stehen. Wortlaut und Pflichten im Einzelfall vom Kunden prüfen lassen.
Code
<!DOCTYPE html>
<!--
KI-Streaming-Text und Denk-Indikator mit Shimmer
Fundus · Mikroanimationen · https://cecodesign.de/lab/fundus/mikroanimationen/ki-streaming-text-denkindikator/
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>KI-Streaming-Text und Denk-Indikator mit Shimmer · 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: 'Instrument Sans'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/instrument-sans:vf@latest/latin-wght-normal.woff2') format('woff2-variations'); font-weight: 400 700; 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; }
@font-face { font-family: 'Instrument Serif'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/instrument-serif@latest/latin-400-italic.woff2') format('woff2'); font-weight: 400; font-style: italic; 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; }
:root {
--papier: #efede7; --flaeche: #fbfaf7; --tinte: #161513;
--leise: #6b675f; /* Grundton des Glanzes: 4,9 : 1 auf --flaeche */
--linie: rgba(22, 21, 19, .12);
--akzent: #c8552b; /* Lichtstreifen der Overlay-Variante */
--marke: #161513; --blase: #e7e4dc; --hinweis: #efede7;
--raus: cubic-bezier(0, 0, 0.58, 1); /* ease-out: Wort-Fade, Einblenden */
--wort: 80ms; /* Cursor: streaming-word-fade */
--glanz-dauer: 2500ms;
}
/* Variante „fragen“: Frag-uns-Feld einer Physiotherapie (Rezept praxis: Petrol, Greige, Apricot; Hedvig Letters Serif + Google Sans Flex; Kurve tech-präzise) */
html[data-welt="fragen"] {
--papier: #eef2f1; --flaeche: #ffffff; --tinte: #363b3b;
--leise: #596666; /* 5,97 : 1 auf Weiß, 5,48 : 1 auf dem Hinweis */
--linie: rgba(0, 84, 80, .16);
--akzent: #ffc776; --marke: #005450; --blase: #e2e0de; --hinweis: #fff4e0;
--raus: cubic-bezier(0, 0, 0.38, 0.9); /* entrance productive */
font-family: 'Google Sans Flex', system-ui, sans-serif;
}
html:not([data-welt="fragen"]) .nur-fragen { display: none !important; }
html[data-welt="fragen"] .nur-archiv { display: none !important; }
* { box-sizing: border-box; margin: 0; }
html { background: var(--papier); color: var(--tinte); font-family: 'Instrument Sans', system-ui, sans-serif; -webkit-font-smoothing: antialiased; }
body { min-height: 100svh; padding: clamp(16px, 3.4vw, 44px); display: grid; grid-template-rows: auto 1fr auto; gap: clamp(20px, 3vw, 36px); }
button { font: inherit; color: inherit; background: none; border: 0; cursor: pointer; }
.mono { font: 500 11px/1.35 'Geist Mono', ui-monospace, monospace; letter-spacing: .14em; text-transform: uppercase; color: var(--leise); }
.kopf { display: flex; justify-content: space-between; gap: 16px; }
/* Bühne: links die Einleitung, rechts das Gespräch */
.buehne { display: grid; grid-template-columns: minmax(0, .85fr) minmax(0, 1.15fr); gap: clamp(28px, 6vw, 96px); align-items: center; max-width: 1240px; width: 100%; margin: 0 auto; }
.intro h1 { font-size: clamp(40px, 5.6vw, 84px); font-weight: 500; letter-spacing: -.045em; line-height: .95; margin: 18px 0 24px; }
.intro h1 em { font: italic 400 1.08em/1 'Instrument Serif', serif; letter-spacing: -.02em; }
.intro .lead { font-size: 17px; line-height: 1.55; color: var(--leise); max-width: 30em; }
.intro dl { display: grid; grid-template-columns: repeat(3, auto); justify-content: start; gap: 6px 36px; margin-top: 36px; padding-top: 18px; border-top: 1px solid var(--linie); }
.intro dt { grid-row: 2; }
.intro dd { font-size: 28px; font-weight: 500; letter-spacing: -.03em; }
html[data-welt="fragen"] .intro h1 { font-family: 'Hedvig Letters Serif', Georgia, serif; font-weight: 400; font-size: clamp(38px, 4.8vw, 72px); letter-spacing: -.01em; line-height: 1.1; color: var(--marke); }
html[data-welt="fragen"] .intro h1 em { font: inherit; font-style: normal; color: var(--tinte); background: linear-gradient(transparent 64%, rgba(255, 199, 118, .7) 64% 92%, transparent 92%); }
html[data-welt="fragen"] .intro .lead { font-size: 17.5px; }
html[data-welt="fragen"] .marke { background: var(--marke); }
.chat { background: var(--flaeche); border: 1px solid var(--linie); border-radius: 20px; box-shadow: 0 1px 0 rgba(255,255,255,.7) inset, 0 30px 60px -40px rgba(22,21,19,.35); display: grid; grid-template-rows: auto auto 1fr auto; height: min(664px, 80svh); min-height: 560px; overflow: hidden; }
.chat-kopf { display: flex; align-items: center; gap: 10px; padding: 16px 22px; border-bottom: 1px solid var(--linie); }
.chat-kopf strong { font-weight: 600; font-size: 15px; white-space: nowrap; }
.chat-kopf .mono { margin-left: auto; }
/* Bewegung anhalten (WCAG 2.2.2): Antwort und Glanz laufen beim Laden ohne Zutun */
.chat-kopf .halt { margin-left: 4px; height: 26px; padding: 0 10px; border: 1px solid var(--linie); border-radius: 999px; color: var(--leise); transition: color .15s var(--raus), border-color .15s var(--raus); }
.chat-kopf .halt:hover { color: var(--tinte); border-color: rgba(22,21,19,.35); }
.chat-kopf .halt[aria-pressed="true"] { color: var(--tinte); border-color: var(--tinte); }
.chat-kopf .halt:focus-visible { outline: 2px solid var(--tinte); outline-offset: 2px; }
.vorschau .chat-kopf .halt { display: none; }
.marke { width: 22px; height: 22px; border-radius: 50%; background: conic-gradient(from 200deg, var(--tinte) 0 62%, var(--akzent) 62% 76%, var(--tinte) 76%); position: relative; }
.marke::after { content: ''; position: absolute; inset: 6px; border-radius: 50%; background: var(--flaeche); }
/* KI-Hinweis nach Art. 50 KI-VO: direkt beim Start der Unterhaltung sichtbar, nicht erst im Impressum */
.ki-hinweis { display: flex; align-items: flex-start; gap: 11px; margin: 12px 22px 0; padding: 9px 14px 9px 9px; border-radius: 12px; background: var(--hinweis); font-size: 13.5px; line-height: 1.45; }
.ki-hinweis .ki { flex: none; margin-top: 1px; padding: 4px 7px 3px; border-radius: 6px; background: var(--marke); color: var(--flaeche); font: 600 10px/1.2 'Geist Mono', ui-monospace, monospace; letter-spacing: .12em; }
.ki-hinweis p { color: var(--leise); }
.ki-hinweis strong { color: var(--tinte); font-weight: 600; }
.ki-hinweis a { color: var(--marke); font-weight: 600; white-space: nowrap; text-decoration-thickness: 1px; text-underline-offset: 3px; }
.ki-hinweis a:focus-visible { outline: 2px solid var(--marke); outline-offset: 2px; border-radius: 3px; }
.verlauf { padding: 22px 22px 8px; overflow: hidden; display: flex; flex-direction: column; gap: 18px; }
.frage { align-self: flex-end; max-width: 80%; background: var(--blase); border-radius: 16px 16px 4px 16px; padding: 10px 15px; font-size: 16px; line-height: 1.4; opacity: 0; translate: 0 8px; transition: opacity .3s var(--raus), translate .3s var(--raus); }
.frage.da { opacity: 1; translate: none; }
.antwort { display: flex; flex-direction: column; gap: 12px; }
.status { display: flex; align-items: center; gap: 10px; min-height: 24px; opacity: 0; transition: opacity .15s var(--raus); }
.status.da { opacity: 1; }
.status button { display: inline-flex; align-items: center; gap: 8px; font-size: 15px; font-weight: 500; padding: 2px 0; }
.status button:focus-visible { outline: 2px solid var(--tinte); outline-offset: 3px; border-radius: 4px; }
.pfeil { width: 9px; height: 9px; border-right: 1.5px solid var(--leise); border-bottom: 1.5px solid var(--leise); rotate: -45deg; transition: rotate .15s var(--raus); }
.antwort.offen .pfeil { rotate: 45deg; }
.status .zeit { font-variant-numeric: tabular-nums; }
.status .haken { display: none; width: 15px; height: 15px; }
.antwort.fertig .status .haken { display: block; }
/* Schritt-Liste: klappt über grid-template-rows */
.schritte-huelle { display: grid; grid-template-rows: 0fr; transition: grid-template-rows .3s var(--raus); }
.antwort.offen .schritte-huelle { grid-template-rows: 1fr; }
.schritte { list-style: none; padding: 0 0 0 4px; overflow: hidden; min-height: 0; transition: visibility 0s .3s; }
.antwort:not(.offen) .schritte { visibility: hidden; }
.antwort.offen .schritte { transition: none; }
.schritt { display: grid; grid-template-columns: 18px 1fr auto; align-items: center; gap: 10px; padding: 5px 0 5px 10px; border-left: 1px solid var(--linie); font-size: 14px; color: var(--leise); animation: rein .3s var(--raus) backwards; }
.schritt .ico { width: 10px; height: 10px; border-radius: 50%; border: 1.5px solid var(--leise); justify-self: center; }
.schritt.erledigt .ico { border: 0; width: 14px; height: 14px; background: no-repeat center / 14px url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 14 14'%3E%3Cpath d='M3 7.4 5.8 10 11 4.4' fill='none' stroke='%236b675f' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"); }
.schritt > span:nth-child(2) { justify-self: start; }
.schritt .detail { font-size: 10px; }
@keyframes rein { from { opacity: 0; translate: 0 4px; } }
/* ── Der Glanz: drei Varianten, umgeschaltet über data-variante am <body> ── */
.glanzt { position: relative; }
/* 1 · Text-Verlauf (Cursor): background-clip: text, Streifen 30/55/70 %, 200 %, 2,5 s linear */
[data-variante="text"] .glanzt {
color: transparent;
background: linear-gradient(90deg, var(--leise) 0%, var(--leise) 30%, var(--tinte) 55%, var(--leise) 70%, var(--leise) 100%);
background-size: 200% 100%; -webkit-background-clip: text; background-clip: text;
animation: glanz-text var(--glanz-dauer) linear infinite;
}
@keyframes glanz-text { from { background-position: 200% 0; } to { background-position: -200% 0; } }
/* 2 · Maske (Perplexity): -70deg #000 50 %, #0005, #000 65 %, mask-size 300 %, 1,8 s linear */
[data-variante="maske"] .glanzt {
color: var(--tinte);
-webkit-mask-image: linear-gradient(-70deg, #000 50%, #0005, #000 65%); mask-image: linear-gradient(-70deg, #000 50%, #0005, #000 65%);
-webkit-mask-size: 300% 100%; mask-size: 300% 100%;
animation: glanz-maske var(--glanz-dauer) linear infinite;
}
@keyframes glanz-maske { from { -webkit-mask-position: 100% 0; mask-position: 100% 0; } to { -webkit-mask-position: 0 0; mask-position: 0 0; } }
/* 3 · Overlay (Gemini): Pseudo-Element mit mix-blend-mode, 2 s linear, Pause ab 70 % */
[data-variante="overlay"] .glanzt { color: var(--tinte); }
[data-variante="overlay"] .glanzt::before {
content: ''; position: absolute; inset: -2px -6px; pointer-events: none;
background: linear-gradient(105deg, transparent 40%, var(--akzent) 50%, transparent 60%);
background-size: 300% 100%; mix-blend-mode: lighten;
animation: glanz-overlay var(--glanz-dauer) linear infinite;
}
@keyframes glanz-overlay { 0% { background-position: 100% 0; } 70%, 100% { background-position: 0 0; } }
.status .glanzt { font-weight: 500; }
/* Antworttext: jedes Wort blendet in 80 ms von .4 auf 1 */
.text { font-size: 16.5px; line-height: 1.62; max-width: 36em; }
.text p + p { margin-top: .7em; }
.w { animation: wort var(--wort) var(--raus) both; }
@keyframes wort { from { opacity: .4; } }
/* Anhang: Projektkarten mit Generierungs-Overlay (v0), Quellen */
.anhang { display: flex; gap: 12px; flex-wrap: wrap; }
.karte { position: relative; width: 168px; border-radius: 10px; overflow: hidden; background: #e7e4dc; animation: rein .3s var(--raus) both; }
.karte img { display: block; width: 100%; aspect-ratio: 3 / 2; object-fit: cover; opacity: 0; transition: opacity .6s var(--raus); }
.karte figcaption { padding: 8px 10px 9px; font-size: 10px; background: var(--flaeche); border: 1px solid var(--linie); border-top: 0; border-radius: 0 0 10px 10px; }
.karte::before, .karte::after { content: ''; position: absolute; left: 0; right: 0; top: 0; aspect-ratio: 3 / 2; pointer-events: none; transition: opacity .6s var(--raus); }
.karte::before { animation: puls 2s ease-in-out infinite; }
.karte::after { background: linear-gradient(105deg, transparent 30%, rgba(255,255,255,.55) 50%, transparent 70%); animation: streifen 2s linear infinite; }
@keyframes puls { 0%, 100% { background: #80808026; } 50% { background: #80808014; } }
@keyframes streifen { from { translate: -100% 0; } to { translate: 100% 0; } }
.karte.geladen img { opacity: 1; }
.karte.geladen::before, .karte.geladen::after { opacity: 0; animation: none; }
.quelle { display: inline-flex; align-items: center; gap: 8px; padding: 7px 11px; border: 1px solid var(--linie); border-radius: 999px; font-size: 10px; animation: rein .3s var(--raus) both; }
.quelle b { font-weight: 600; color: var(--tinte); }
/* Frag-uns-Feld: Quellen sind Pflicht und führen auf die eigene Seite; ohne Quelle keine Antwort, sondern das Telefon */
.anhang .titel { flex-basis: 100%; margin-bottom: -4px; }
.quelle-link { display: inline-flex; align-items: center; gap: 8px; padding: 6px 13px 6px 6px; border: 1px solid var(--linie); border-radius: 999px; font-size: 14px; color: var(--tinte); text-decoration: none; animation: rein .3s var(--raus) both; transition: border-color .15s var(--raus), background-color .15s var(--raus); }
.quelle-link .nr { display: grid; place-items: center; width: 22px; height: 22px; border-radius: 50%; background: var(--marke); color: var(--flaeche); font: 600 10px/1 'Geist Mono', ui-monospace, monospace; }
.quelle-link .pfad { color: var(--leise); }
.quelle-link:hover { border-color: var(--marke); background: rgba(0, 84, 80, .05); }
.quelle-link:focus-visible, .anruf:focus-visible, .ausweg a:focus-visible { outline: 2px solid var(--marke); outline-offset: 2px; }
.anruf { display: flex; align-items: center; justify-content: space-between; gap: 12px 20px; flex-wrap: wrap; width: 100%; max-width: 440px; padding: 14px 18px; border-radius: 14px; background: var(--marke); color: #fff; text-decoration: none; animation: rein .3s var(--raus) both; }
.anruf span { font-size: 14px; }
.anruf b { font-size: 21px; font-weight: 600; letter-spacing: .01em; font-variant-numeric: tabular-nums; }
.ausweg { flex-basis: 100%; font-size: 13px; color: var(--leise); animation: rein .3s var(--raus) both; }
.ausweg a { color: var(--tinte); font-weight: 600; }
.vorschlaege { display: flex; gap: 8px; flex-wrap: wrap; padding: 14px 22px 18px; border-top: 1px solid var(--linie); }
.vorschlaege button { font-size: 13.5px; padding: 8px 13px; border: 1px solid var(--linie); border-radius: 999px; transition: background .15s var(--raus), border-color .15s var(--raus); }
.vorschlaege button:hover { border-color: rgba(22,21,19,.35); }
.vorschlaege button[aria-current="true"] { background: var(--marke); color: var(--flaeche); border-color: var(--marke); }
.vorschlaege button:focus-visible, .umschalter button:focus-visible { outline: 2px solid var(--tinte); outline-offset: 2px; }
/* Regler: Variante und verwendete Werte */
.regler { display: flex; justify-content: space-between; align-items: center; gap: 12px 24px; flex-wrap: wrap; max-width: 1240px; width: 100%; margin: 0 auto; }
.umschalter { display: flex; gap: 4px; flex-wrap: wrap; }
.pult-gruppen { display: flex; gap: 6px 22px; flex-wrap: wrap; align-items: center; }
.pult-gruppen .umschalter + .umschalter { padding-left: 22px; border-left: 1px solid var(--linie); }
.umschalter button { padding: 7px 10px; border-radius: 999px; }
.umschalter button[aria-pressed="true"] { color: var(--tinte); background: rgba(22,21,19,.07); }
.werte { text-align: right; text-transform: none; letter-spacing: .04em; }
/* Angehalten: alle CSS-Animationen stehen */
.angehalten *, .angehalten *::before, .angehalten *::after { animation-play-state: paused !important; }
/* Vorschau 960 × 720: nur das Gespräch, größer */
.vorschau body { overflow: hidden; height: 100svh; padding: 28px 40px; grid-template-rows: 1fr; }
.vorschau .kopf, .vorschau .intro, .vorschau .regler { display: none; }
.vorschau .buehne { grid-template-columns: 1fr; max-width: 820px; }
.vorschau .chat { height: 664px; min-height: 0; }
.vorschau .chat-kopf { padding: 18px 26px; }
.vorschau .ki-hinweis { margin: 14px 30px 0; font-size: 14.5px; }
.vorschau .verlauf { padding: 20px 30px 8px; gap: 18px; }
.vorschau .frage { font-size: 19px; }
.vorschau .status button { font-size: 20px; }
.vorschau .schritt { font-size: 17px; padding: 7px 0 7px 12px; }
.vorschau .text { font-size: 19px; }
.vorschau .karte { width: 176px; }
/* Reduzierte Bewegung: kein Glanz, Wörter ohne Fade, Overlay statisch */
.reduziert .glanzt { animation: none !important; color: var(--tinte) !important; background: none !important; -webkit-mask: none !important; mask: none !important; }
.reduziert .glanzt::before { display: none; }
.reduziert .status .glanzt::after { content: ' · läuft…'; color: var(--leise); font-weight: 400; }
.reduziert .w, .reduziert .schritt, .reduziert .karte, .reduziert .quelle, .reduziert .quelle-link, .reduziert .anruf, .reduziert .ausweg, .reduziert .karte::before, .reduziert .karte::after { animation: none; }
.reduziert .karte::before { background: #80808020; }
.reduziert .karte::after { display: none; }
.reduziert .frage, .reduziert .schritte-huelle, .reduziert .pfeil { transition: none; translate: none; }
.reduziert .karte img { transition: opacity .15s linear; }
@media (max-width: 860px) {
.buehne { grid-template-columns: 1fr; align-items: start; }
.intro h1 { margin: 12px 0 14px; }
.intro dl { display: none; }
.chat { height: auto; min-height: 560px; }
.werte { text-align: left; }
}
@media (max-width: 560px) {
body { padding: 16px; }
.kopf .mono:last-child { display: none; }
.intro .lead { font-size: 15.5px; }
.chat { min-height: 600px; border-radius: 16px; }
.chat-kopf, .verlauf { padding-left: 16px; padding-right: 16px; }
.ki-hinweis { margin: 12px 16px 0; font-size: 13px; }
.pult-gruppen .umschalter + .umschalter { padding-left: 0; border-left: 0; }
.vorschlaege { padding: 12px 16px 16px; flex-wrap: nowrap; overflow-x: auto; scrollbar-width: none; }
.vorschlaege button { flex: none; font-size: 13px; }
.chat-kopf .mono { font-size: 10px; letter-spacing: .1em; }
.frage { max-width: 90%; font-size: 15px; }
.text { font-size: 15.5px; }
.karte { width: calc(50% - 6px); }
}
@media (hover: none) { .vorschlaege button:hover { border-color: var(--linie); } }
@media (forced-colors: active) {
button, [role=button], [role=tab], [role=switch] { border: 1px solid ButtonText !important; }
.glanzt { color: CanvasText !important; -webkit-text-fill-color: CanvasText; background: none !important; -webkit-mask: none !important; mask: none !important; }
.glanzt::before { display: none; }
}
</style>
</head>
<body data-variante="text">
<header class="kopf"><p class="mono nur-archiv">Projektarchiv · Assistent</p><p class="mono nur-archiv">Markendesign seit 2016</p><p class="mono nur-fragen">Physiotherapie am Kastanienhof · Frag uns</p><p class="mono nur-fragen">Freiburg · Kasse und privat</p></header>
<main class="buehne">
<section class="intro nur-archiv">
<p class="mono">Fragen statt suchen</p>
<h1>Fragen Sie <em>unser Archiv.</em></h1>
<p class="lead">Der Assistent kennt unsere Projekte, Preise und Abläufe. Er zeigt, woran er gerade arbeitet, und antwortet mit Quellen.</p>
<dl><dd>212</dd><dt class="mono">Projekte</dt><dd>48</dd><dt class="mono">Fallstudien</dt><dd>2026</dd><dt class="mono">Preisliste</dt></dl>
</section>
<section class="intro nur-fragen">
<p class="mono">Kurze Fragen vor dem Termin</p>
<h1>Fragen Sie uns, <em>bevor Sie kommen.</em></h1>
<p class="lead">Das Frag-uns-Feld antwortet nur aus unseren eigenen Seiten und nennt jedes Mal die Seite, auf der es steht. Findet es dort nichts, verweist es ans Telefon.</p>
<dl><dd>14</dd><dt class="mono">Praxisseiten</dt><dd>0</dd><dt class="mono">Diagnosen</dt><dd>8–18</dd><dt class="mono">Telefon Mo–Fr</dt></dl>
</section>
<section class="chat" aria-label="Gespräch mit dem Archiv-Assistenten">
<div class="chat-kopf"><span class="marke" aria-hidden="true"></span><strong>Archiv-Assistent</strong><span class="mono" data-variante-name>Glanz · Text-Verlauf</span><button type="button" class="halt mono" data-halt aria-pressed="false" aria-label="Bewegung anhalten">Anhalten</button></div>
<div class="ki-hinweis nur-archiv"><span class="ki" aria-hidden="true">KI</span><p><strong>Sie schreiben mit einem KI-Assistenten.</strong> Er kennt unser Projektarchiv und die Preisliste 2026, kann sich aber irren. Verbindlich ist unser Angebot.</p></div>
<div class="ki-hinweis nur-fragen"><span class="ki" aria-hidden="true">KI</span><p><strong>Sie schreiben mit einem KI-Assistenten.</strong> Er antwortet nur aus den Seiten dieser Praxis und stellt keine Diagnosen. Lieber persönlich: <a href="tel:+497614702210">0761 470 22 10</a></p></div>
<div class="verlauf" data-verlauf></div>
<div class="vorschlaege" data-vorschlaege aria-label="Vorgeschlagene Fragen"></div>
</section>
</main>
<footer class="regler">
<div class="pult-gruppen">
<div class="umschalter mono" role="group" aria-label="Variante" data-welten>
<button type="button" data-welt="archiv" aria-pressed="true">Archiv-Assistent</button>
<button type="button" data-welt="fragen" aria-pressed="false">Frag-uns-Feld</button>
</div>
<div class="umschalter mono" role="group" aria-label="Shimmer-Variante" data-umschalter>
<button type="button" data-v="text" aria-pressed="true">Text-Verlauf</button>
<button type="button" data-v="maske" aria-pressed="false">Maske</button>
<button type="button" data-v="overlay" aria-pressed="false">Overlay</button>
</div>
</div>
<p class="mono werte" data-werte></p>
</footer>
<script>
(() => {
const html = document.documentElement, body = document.body;
const reduziert = () => html.classList.contains('reduziert');
const $ = s => document.querySelector(s);
const verlauf = $('[data-verlauf]'), vorschlaegeEl = $('[data-vorschlaege]'), werteEl = $('[data-werte]');
// Die drei gemessenen Varianten
const VARIANTEN = {
text: { name: 'Text-Verlauf', dauer: 2500, werte: 'background-clip: text · 90deg · Streifen 30 / 55 / 70 % · size 200 % · 200 % → −200 % · 2500 ms linear' },
maske: { name: 'Maske', dauer: 1800, werte: 'mask-image −70deg #000 50 %, #0005, #000 65 % · mask-size 300 % · 100 % → 0 · 1800 ms linear' },
overlay: { name: 'Overlay', dauer: 2000, werte: '::before · mix-blend-mode: lighten · 105deg · 100 % → 0 · 2000 ms linear, Pause ab 70 %' },
};
const WORT = 'Wort 80 ms ease-out .4 → 1 · Label nach 300 ms';
const ARCHIV = [
{
knopf: 'Gastronomie-Projekte?', frage: 'Habt ihr schon für Gastronomie gearbeitet?',
schritte: [
['Durchsucht 212 Projekte', 'Projektarchiv durchsuchen', '212 Projekte', 1100],
['Liest 4 Fallstudien', 'Fallstudien lesen', '4 Quellen', 1300],
],
text: 'Ja, Gastronomie gehört zu unseren Schwerpunkten. Für Krume, eine Backstube mit kleinem Café, haben wir die komplette Marke entwickelt, vom Logo mit dem Brotlaib bis zur Brottüte. Beim Röstkontor, einer Rösterei mit Ausschank, kamen Logo und Website aus einer Hand. Zwei Beispiele finden Sie hier:',
anhang: { karten: [['marke-krume', 'Krume · Marke · 2023'], ['marke-roestkontor', 'Röstkontor · Website · 2024']] },
},
{
knopf: 'Was kostet ein Logo?', frage: 'Was kostet ein Logo mit kleinem Markenauftritt?',
schritte: [
['Prüft die Preisliste 2026', 'Preisliste 2026 öffnen', '3 Pakete', 1000],
['Vergleicht 12 Projekte', 'Ähnliche Projekte vergleichen', '12 Treffer', 1400],
],
text: 'Das hängt vom Umfang ab. Das Paket „Start“ umfasst Logo, Farben, Schriften und eine Visitenkarte und liegt bei 1.900 € netto. Mit Briefpapier, Social-Media-Vorlagen und einem kurzen Markenhandbuch sind es 3.400 €.\nBei zwölf vergleichbaren Projekten dauerte es vier bis sieben Wochen. Das Erstgespräch ist kostenlos.',
anhang: { quellen: [['Preisliste', '2026'], ['Projekt', 'Eselsohr'], ['Projekt', 'Ocker 04']] },
},
{
knopf: 'Wie läuft ein Projekt ab?', frage: 'Wie läuft ein Projekt bei euch ab?',
schritte: [
['Öffnet den Leitfaden', 'Projektleitfaden lesen', 'Version 4', 1000],
['Sucht ein Beispiel', 'Passendes Beispiel suchen', 'Gefüge', 1200],
],
text: 'In vier Schritten. Im Erstgespräch klären wir Ziel, Zielgruppe und Budget. Danach zeigen wir zwei bis drei Richtungen als Skizzen. Die gewählte Richtung arbeiten wir aus, mit zwei Korrekturrunden. Am Ende bekommen Sie alle Dateien und ein kurzes Handbuch. So ist zum Beispiel das Editorial-Design für das Magazin Gefüge entstanden.',
anhang: { karten: [['magazin-doppelseite', 'Gefüge · Editorial · 2025']] },
},
];
// Variante „fragen“: antwortet nur aus eigenen Seiten. Ohne Quelle keine Antwort, sondern das Telefon.
const TELEFON = ['0761 470 22 10', 'tel:+497614702210'];
const PRAXIS = [
{
knopf: 'Brauche ich ein Rezept?', frage: 'Brauche ich für die Behandlung ein Rezept?',
schritte: [
['Durchsucht 14 Praxisseiten', 'Praxisseiten durchsuchen', '14 Seiten', 1000],
['Liest „Kasse und privat“', 'Seite „Kasse und privat“ lesen', '2 Quellen', 1200],
],
text: 'Nicht unbedingt. Mit einem Rezept Ihrer Ärztin rechnen wir direkt mit der Krankenkasse ab; bringen Sie es bitte zum ersten Termin mit. Ohne Rezept behandeln wir Sie als Selbstzahler, die Preise stehen auf der Seite für Selbstzahler.',
anhang: { links: [['Leistungen', 'Kasse und privat', '#kasse-und-privat'], ['Preise', 'Selbstzahler', '#selbstzahler']] },
},
{
knopf: 'Kann ich parken?', frage: 'Kann ich bei euch parken, und gibt es Stufen?',
schritte: [
['Durchsucht 14 Praxisseiten', 'Praxisseiten durchsuchen', '14 Seiten', 900],
['Liest „Anfahrt“', 'Seite „Anfahrt“ lesen', '2 Quellen', 1100],
],
text: 'Ja. Im Hof stehen zwei Parkplätze für Patienten, einer davon breiter für Rollstuhl und Rollator. Die Praxis liegt im Erdgeschoss, ohne Stufen. Von der Straßenbahn-Haltestelle Kastanienhof sind es etwa drei Minuten zu Fuß.',
anhang: { links: [['Praxis', 'Anfahrt', '#anfahrt'], ['Praxis', 'Barrierefreiheit', '#barrierefreiheit']] },
},
{
knopf: 'Ist es die Bandscheibe?', frage: 'Mir zieht es seit Tagen ins Bein. Ist das ein Bandscheibenvorfall?',
schritte: [
['Durchsucht 14 Praxisseiten', 'Praxisseiten durchsuchen', '14 Seiten', 1100],
['Prüft die Treffer', 'Passende Seite suchen', 'keine Quelle', 1200],
],
fertig: 'Keine Quelle · Telefon',
text: 'Das kann ich nicht beurteilen. Ich antworte nur mit dem, was auf unseren Seiten steht, und ersetze keine Untersuchung. Sprechen Sie bitte mit uns oder mit Ihrer Ärztin. Wir sind heute bis 18 Uhr erreichbar:',
anhang: { telefon: true },
},
];
const WELTEN = {
archiv: { fragen: ARCHIV, name: 'Archiv-Assistent', werte: '' },
fragen: { fragen: PRAXIS, name: 'Frag uns', werte: ' · nur eigene Seiten, jede Antwort mit mind. 1 Quellen-Link, sonst Telefon · Kurve cubic-bezier(0, 0, 0.38, 0.9)' },
};
const wq = new URLSearchParams(location.search).get('v');
let welt = WELTEN[wq] ? wq : 'archiv';
let FRAGEN = WELTEN[welt].fragen;
// ── Ablauf auf einer eigenen, anhaltbaren Uhr ──
let uhr = 0, plan = [], angehalten = false, letzte = performance.now(), zeitFeld = null, zeitStart = 0, zeitLaeuft = false;
function plane(t, fn) { plan.push({ t, fn }); }
function tick(now) {
const dt = Math.min(100, now - letzte); letzte = now;
if (!angehalten) {
uhr += dt;
while (plan.length && plan[0].t <= uhr) plan.shift().fn();
if (zeitLaeuft && zeitFeld) zeitFeld.textContent = sek(uhr - zeitStart);
}
requestAnimationFrame(tick);
}
requestAnimationFrame(tick);
const sek = ms => (Math.max(0, ms) / 1000).toFixed(1).replace('.', ',') + ' s';
// Token-Pakete: 1 bis 3 Wörter, 40 bis 130 ms Abstand, wie ungleichmäßige Server-Chunks (fester Startwert)
function pakete(woerter, seed) {
let s = seed, r = () => (s = (s * 16807) % 2147483647) / 2147483647;
const out = []; let i = 0;
while (i < woerter.length) { const n = 1 + Math.floor(r() * 3); out.push({ von: i, bis: Math.min(woerter.length, i + n), warte: 40 + Math.round(r() * 90) }); i += n; }
return out;
}
const el = (tag, cls, txt) => { const e = document.createElement(tag); if (cls) e.className = cls; if (txt != null) e.textContent = txt; return e; };
const schmal = () => matchMedia('(max-width: 560px)').matches;
let aktuelleFrage = 0, nachEnde = null;
function spiele(fi) {
const F = FRAGEN[fi]; aktuelleFrage = fi;
plan = []; uhr = 0; zeitLaeuft = false;
[...vorschlaegeEl.children].forEach((b, j) => b.setAttribute('aria-current', j === fi));
verlauf.replaceChildren();
const frage = el('div', 'frage', F.frage);
const antwort = el('div', 'antwort');
const status = el('div', 'status'); status.setAttribute('role', 'status');
const knopf = el('button'); knopf.type = 'button'; knopf.setAttribute('aria-expanded', 'false');
const label = el('span', 'glanzt', 'Denkt nach');
const haken = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); haken.setAttribute('class', 'haken'); haken.setAttribute('viewBox', '0 0 14 14'); haken.setAttribute('aria-hidden', 'true');
haken.innerHTML = '<path d="M3 7.4 5.8 10 11 4.4" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/>';
knopf.append(el('span', 'pfeil'), haken, label);
const zeit = el('span', 'mono zeit', '0,0 s');
status.append(knopf, zeit);
const huelle = el('div', 'schritte-huelle'), liste = el('ol', 'schritte'); liste.id = 'schritte-' + fi;
knopf.setAttribute('aria-controls', liste.id); huelle.append(liste);
const text = el('div', 'text'); text.setAttribute('aria-live', 'polite'); text.setAttribute('aria-busy', 'true');
const anhang = el('div', 'anhang');
antwort.append(status, huelle, text, anhang);
verlauf.append(frage, antwort);
const offen = an => { antwort.classList.toggle('offen', an); knopf.setAttribute('aria-expanded', an); };
knopf.addEventListener('click', () => offen(!antwort.classList.contains('offen')));
let t = 0;
plane(30, () => frage.classList.add('da'));
// Label erst nach 300 ms Wartezeit zeigen (sonst Flackern bei schnellen Antworten)
t = 330;
plane(t, () => { status.classList.add('da'); offen(!schmal()); zeitFeld = zeit; zeitStart = uhr; zeitLaeuft = true; });
t += 450;
F.schritte.concat([['Schreibt die Antwort', 'Antwort formulieren', '', 0]]).forEach(([lab, zeile, detail, dauer], k, alle) => {
const li = el('li', 'schritt');
const z = el('span', 'glanzt', zeile);
li.append(el('span', 'ico'), z, el('span', 'mono detail', detail));
plane(t, () => { liste.append(li); label.textContent = lab; });
if (k < alle.length - 1) { t += dauer; plane(t, () => { z.classList.remove('glanzt'); li.classList.add('erledigt'); }); }
});
// Stream startet: Liste klappt zu, die Wörter kommen in Paketen
t += 380;
const absaetze = F.text.split('\n').map(a => a.split(' '));
const woerter = []; absaetze.forEach((a, ai) => a.forEach((w, wi) => woerter.push({ w, neu: wi === 0, ai })));
plane(t, () => offen(false));
let ziel = null;
pakete(woerter, 7 + fi * 101).forEach(p => {
t += p.warte;
plane(t, () => {
for (let i = p.von; i < p.bis; i++) {
const W = woerter[i];
if (W.neu) { ziel = el('p'); text.append(ziel); } else ziel.append(' ');
ziel.append(el('span', 'w', W.w));
}
});
});
t += 160;
plane(t, () => {
zeitLaeuft = false; zeit.textContent = sek(uhr - zeitStart);
antwort.querySelectorAll('.glanzt').forEach(g => g.classList.remove('glanzt'));
const li = liste.lastElementChild; if (li) li.classList.add('erledigt');
label.textContent = F.fertig || ('Fertig · ' + liste.children.length + ' Schritte');
antwort.classList.add('fertig'); text.setAttribute('aria-busy', 'false');
});
// Anhang: Karten unter dem Generierungs-Overlay, dann das Bild
t += 120;
if (F.anhang.karten) {
plane(t, () => F.anhang.karten.forEach(([bild, unter]) => {
const f = el('figure', 'karte'), img = el('img'); img.src = 'bilder/' + bild + '.jpg'; img.alt = unter.split(' · ')[0];
f.append(img, el('figcaption', 'mono', unter)); anhang.append(f);
}));
t += reduziert() ? 400 : 1800;
plane(t, () => anhang.querySelectorAll('.karte').forEach((k, i) => setTimeout(() => k.classList.add('geladen'), i * 140)));
} else if (F.anhang.links) {
// Quellen sind Pflicht: jede Antwort verlinkt die eigene Seite, aus der sie stammt
plane(t, () => anhang.append(el('p', 'mono titel', 'Aus unseren Seiten')));
F.anhang.links.forEach(([pfad, seite, href], i) => plane(t + 90 + i * 90, () => {
const a = el('a', 'quelle-link'); a.href = href;
a.append(el('span', 'nr', String(i + 1)), el('span', 'pfad', pfad + ' ›'), ' ' + seite);
a.addEventListener('click', e => e.preventDefault()); // Demo: keine echte Unterseite
anhang.append(a);
}));
t += 700;
} else if (F.anhang.telefon) {
// Keine Quelle gefunden: keine erfundene Antwort, sondern der Weg zum Menschen
plane(t, () => {
const a = el('a', 'anruf'); a.href = TELEFON[1];
a.append(el('span', null, 'Praxis anrufen'), el('b', null, TELEFON[0]));
const p = el('p', 'ausweg', 'Bei starken Beschwerden außerhalb der Sprechzeit: ärztlicher Bereitschaftsdienst ');
const b = el('a', null, '116 117'); b.href = 'tel:116117'; p.append(b);
anhang.append(a, p);
});
t += 700;
} else {
F.anhang.quellen.forEach(([a, b], i) => plane(t + i * 90, () => { const q = el('span', 'quelle mono'); q.append(a + ' ', el('b', null, b)); anhang.append(q); }));
t += 600;
}
if (nachEnde) { const n = nachEnde; plane(t + 2600, n); }
return t;
}
// Vorschlags-Knöpfe
function knoepfe() {
vorschlaegeEl.replaceChildren();
FRAGEN.forEach((F, i) => {
const b = el('button', null, F.knopf); b.type = 'button';
b.addEventListener('click', () => { stopVorfuehrung(); weiter(); spiele(i); });
vorschlaegeEl.append(b);
});
}
knoepfe();
// Varianten-Umschalter
function variante(v) {
body.dataset.variante = v;
html.style.setProperty('--glanz-dauer', VARIANTEN[v].dauer + 'ms');
document.querySelectorAll('[data-umschalter] button').forEach(b => b.setAttribute('aria-pressed', b.dataset.v === v));
$('[data-variante-name]').textContent = 'Glanz · ' + VARIANTEN[v].name;
werteEl.textContent = VARIANTEN[v].werte + ' · ' + WORT + WELTEN[welt].werte;
}
document.querySelectorAll('[data-umschalter] button').forEach(b => b.addEventListener('click', () => variante(b.dataset.v)));
variante('text');
// Variante (Regel 13): Archiv-Assistent oder Frag-uns-Feld, steht als ?v= in der URL
function setzeWelt(w, abspielen) {
welt = w; FRAGEN = WELTEN[w].fragen; html.dataset.welt = w;
document.querySelectorAll('[data-welten] button').forEach(b => b.setAttribute('aria-pressed', b.dataset.welt === w));
$('.chat-kopf strong').textContent = WELTEN[w].name;
$('.chat').setAttribute('aria-label', w === 'fragen' ? 'Frag-uns-Feld der Praxis' : 'Gespräch mit dem Archiv-Assistenten');
const q = new URLSearchParams(location.search); q.set('v', w);
history.replaceState(null, '', location.pathname + '?' + q.toString() + location.hash);
knoepfe(); variante(body.dataset.variante);
if (abspielen) { stopVorfuehrung(); weiter(); spiele(0); }
}
document.querySelectorAll('[data-welten] button').forEach(b => b.addEventListener('click', () => setzeWelt(b.dataset.welt, true)));
html.dataset.welt = welt;
if (welt !== 'archiv') setzeWelt(welt, false);
// ── API ──
let runde = 0;
const reihe = ['text', 'maske', 'overlay'];
function stopVorfuehrung() { nachEnde = null; html.classList.remove('vorfuehrung'); }
function naechste() {
variante(reihe[(runde + Math.floor(runde / 3)) % 3]);
const fi = runde % 3; runde++;
spiele(fi);
}
function vorfuehren() {
angehalten = false; body.classList.remove('angehalten'); haltZeigen();
html.classList.add('vorfuehrung'); runde = 0;
nachEnde = naechste; naechste();
}
function neustart() { stopVorfuehrung(); angehalten = false; body.classList.remove('angehalten'); haltZeigen(); spiele(aktuelleFrage); }
const haltKnopf = $('[data-halt]');
const haltZeigen = () => { haltKnopf.setAttribute('aria-pressed', String(angehalten)); haltKnopf.textContent = angehalten ? 'Abspielen' : 'Anhalten'; haltKnopf.setAttribute('aria-label', angehalten ? 'Bewegung abspielen' : 'Bewegung anhalten'); };
function pause() { angehalten = true; body.classList.add('angehalten'); haltZeigen(); }
function weiter() { angehalten = false; body.classList.remove('angehalten'); haltZeigen(); }
haltKnopf.addEventListener('click', () => (angehalten ? weiter : pause)());
// Reduzierte Bewegung: beim Laden steht die erste Antwort gleich fertig da (kein Stream ohne Zutun); Klick auf eine Frage spielt normal
const sofort = () => { while (plan.length) { const p = plan.shift(); uhr = p.t; p.fn(); } };
addEventListener('load', () => { if (html.classList.contains('vorschau')) return; spiele(0); if (reduziert() && !html.classList.contains('vorfuehrung')) sofort(); });
})();
</script>
</body>
</html>