Zum Inhalt
← Animationsstile

Animationsstil „Tech präzise“ (Produkt-UI)

Produkt-UI einer Saarbrücker Beleg-Software: Panel, Reiter, Buchen und Toast laufen exakt auf den Carbon-Token, ein Protokoll zeigt jede Bewegung mit Dauer und Kurve.

Bewegungssprache von Software- und SaaS-Seiten: sehr kurze Dauern von 70 bis 240 ms für die Bedienung, 400 bis 700 ms für Einblendungen. Bewegt werden Deckkraft und ein kleiner Weg von 4 bis 8 px, optional ein Blur von 4 px auf 0. Die Kurven starten knapp und laufen klar aus (Carbon productive). Austritte sind schneller als Eintritte. Nichts federt, nichts dreht sich.

Die Oberfläche reagiert so schnell wie die Software selbst. Bewegung erklärt nur, woher ein Element kommt und wohin es geht, ohne Aufmerksamkeit zu ziehen. Das wirkt effizient und verlässlich.

Wann einsetzen

SaaS, Tools, Dashboards, Dokumentationen, technische Produktseiten, Formulare.

Wann nicht

Emotionale Kampagnen, Mode, Kunst.

Varianten

  • Productive (Carbon): kurz und funktional, für die ganze Bedienung
  • Expressive (Carbon): länger und weicher, nur für Hero-Momente
  • Mit Blur: Einblendungen zusätzlich von blur(4px) auf 0

Umsetzung

Token direkt aus Carbon: --ease-productive: cubic-bezier(0.2, 0, 0.38, 0.9), --ease-enter: cubic-bezier(0, 0, 0.38, 0.9), --ease-exit: cubic-bezier(0.2, 0, 1, 0.9), --ease-expressive: cubic-bezier(0.4, 0.14, 0.3, 1). Dauern fast-01 70, fast-02 110, moderate-01 150, moderate-02 240, slow-01 400, slow-02 700 ms. Zuordnung: Hover und Farbe 70, Reiter 110, Austritte 150, Panels 240, Einblendungen 400, Hero 700. Hover per CSS-Transition, Panels und Toasts per Web Animations API.

Robust umsetzen

Telefon. Gleiche Token. Seitenpanels werden zu Sheets von unten, der Weg wechselt von x auf y.

Ohne Bewegung. Translation und Blur entfallen, die Deckkraft bleibt (höchstens 150 ms).

Barrierefrei. Zustandswechsel zusätzlich per ARIA melden: aria-selected für Zeilen und Reiter, aria-busy beim Speichern, role="status" für Toasts. Reiter per Pfeiltasten, Panel per Escape schließen.

Code

<!DOCTYPE html>
<!--
  Animationsstil „Tech präzise“ (Produkt-UI)
  Fundus · Animationsstile · https://cecodesign.de/lab/fundus/animationsstile/token-tech-praezise/
  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>Animationsstil „Tech präzise“ (Produkt-UI) · 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; }

:root {
  /* Rezept „SaaS & KI“: Graustufen tragen alles, ein Markenton für Aktion und Fokus */
  --grund: #08090a; --panel: #161718; --flaeche: #0f1011; --linie: #242728;
  --text: #f7f8f8; --text-2: #c2c7ca; --leise: #8a8f98;
  --marke: #7b70ff;
  --gruen: #59d499; --blau: #57c1ff; --gelb: #ffc533; --rot: #ff6161;

  /* Carbon-Motion-Token */
  --ease-productive: cubic-bezier(0.2, 0, 0.38, 0.9);
  --ease-enter: cubic-bezier(0, 0, 0.38, 0.9);
  --ease-exit: cubic-bezier(0.2, 0, 1, 0.9);
  --ease-expressive: cubic-bezier(0.4, 0.14, 0.3, 1);
  --ease-expressive-enter: cubic-bezier(0, 0, 0.3, 1);
  --ease-expressive-exit: cubic-bezier(0.4, 0.14, 1, 1);
  --dur-fast-01: 70ms;
  --dur-fast-02: 110ms;
  --dur-moderate-01: 150ms;
  --dur-moderate-02: 240ms;
  --dur-slow-01: 400ms;
  --dur-slow-02: 700ms;
  --tab-dauer: var(--dur-fast-02);
  --tab-kurve: var(--ease-productive);
}
* { box-sizing: border-box; margin: 0; }
html { background: var(--grund); color: var(--text); font-family: 'Geist', system-ui, sans-serif; font-size: 16px; -webkit-font-smoothing: antialiased; overflow-x: clip; }
body { min-height: 100svh; overflow-x: clip; }
a { color: inherit; text-decoration: none; }
button { font: inherit; color: inherit; background: none; border: 0; padding: 0; cursor: pointer; }
:focus-visible { outline: 2px solid var(--marke); outline-offset: 2px; }
.mono { font: 450 12px/1.3 'Geist Mono', ui-monospace, monospace; letter-spacing: .02em; font-variant-numeric: tabular-nums; }
.versal { text-transform: uppercase; letter-spacing: .08em; }

/* ——— Seite ——— */
.seite { max-width: 1200px; margin-inline: auto; padding: 0 clamp(16px, 3.2vw, 40px) clamp(28px, 4vw, 56px); }
.kopf { display: flex; align-items: center; justify-content: space-between; gap: 20px; height: 60px; border-bottom: 1px solid var(--linie); }
.marke { display: flex; align-items: center; gap: 9px; font-weight: 600; font-size: 17px; letter-spacing: -.02em; }
.marke svg { width: 20px; height: 20px; }
.nav { display: flex; gap: 26px; list-style: none; padding: 0; font-size: 14px; color: var(--text-2); }
.nav a { transition: color var(--dur-fast-01) var(--ease-productive); }
.nav a:hover { color: var(--text); }
.kopf-rechts { display: flex; align-items: center; gap: 10px; font-size: 14px; }
.knopf { display: inline-flex; align-items: center; justify-content: center; gap: 8px; height: 36px; padding: 0 14px; border-radius: 8px; font-size: 14px; font-weight: 500; white-space: nowrap; transition: background-color var(--dur-fast-01) var(--ease-productive), box-shadow var(--dur-fast-01) var(--ease-productive); }
.knopf.primaer { background: #6458f0; color: #fff; }   /* Markenton abgedunkelt: Weiß 5:1 */
.knopf.primaer:hover { background: #6a5ff4; }
.knopf.zweit { box-shadow: inset 0 0 0 1px var(--linie); color: var(--text); background: var(--panel); }
.knopf.zweit:hover { box-shadow: inset 0 0 0 1px #3a3e41; }
.knopf.klein { height: 30px; padding: 0 11px; font-size: 13px; border-radius: 7px; }
.knopf kbd { font: 450 11px/1 'Geist Mono', monospace; padding: 3px 5px; border-radius: 4px; background: rgba(255, 255, 255, .16); }

.held { display: grid; grid-template-columns: minmax(0, 7fr) minmax(0, 5fr); gap: 20px clamp(24px, 4vw, 64px); align-items: end; padding-block: clamp(28px, 3.4vw, 44px) clamp(24px, 3vw, 36px); }
.neu { display: inline-flex; align-items: center; gap: 8px; height: 26px; padding: 0 10px 0 4px; margin-bottom: 18px; border-radius: 999px; box-shadow: inset 0 0 0 1px var(--linie); color: var(--text-2); font-size: 13px; }
.neu b { font: 500 11px/1 'Geist Mono', monospace; padding: 4px 7px; border-radius: 999px; background: rgba(123, 112, 255, .18); color: #b3acff; }
h1 { font-weight: 580; font-size: clamp(2.5rem, 1.6rem + 3.6vw, 4.5rem); line-height: 1.0; letter-spacing: -.022em; text-wrap: balance; max-width: 15ch; }
.unter { font-size: 1.0625rem; line-height: 1.6; letter-spacing: -.006em; color: var(--text-2); max-width: 46ch; }
.ctas { display: flex; gap: 10px; margin-top: 22px; flex-wrap: wrap; }
.ctas .knopf { height: 42px; padding: 0 18px; font-size: 15px; }

/* ——— Produkt-UI ——— */
.app { position: relative; height: 492px; display: grid; grid-template-columns: 200px minmax(0, 1fr); grid-template-rows: 44px minmax(0, 1fr); border-radius: 12px; overflow: hidden; background: var(--flaeche); box-shadow: inset 0 0 0 1px var(--linie), 0 30px 80px -30px rgba(0, 0, 0, .8); }
.app-kopf { grid-column: 1 / -1; display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 0 14px; border-bottom: 1px solid var(--linie); font-size: 13px; color: var(--leise); }
.pfad { display: flex; align-items: center; gap: 8px; min-width: 0; white-space: nowrap; overflow: hidden; }
.pfad b { color: var(--text); font-weight: 500; }
.suche { display: inline-flex; align-items: center; gap: 10px; height: 28px; padding: 0 8px 0 10px; border-radius: 7px; box-shadow: inset 0 0 0 1px var(--linie); color: var(--leise); }
.suche kbd { font: 450 11px/1 'Geist Mono', monospace; padding: 3px 5px; border-radius: 4px; background: var(--panel); color: var(--text-2); }
.seitenleiste { border-right: 1px solid var(--linie); padding: 12px 8px; display: grid; align-content: start; gap: 2px; font-size: 13px; color: var(--text-2); }
.seitenleiste p { padding: 14px 8px 6px; color: var(--leise); font-size: 11px; }
.seitenleiste a { display: flex; align-items: center; justify-content: space-between; height: 30px; padding: 0 8px; border-radius: 6px; transition: background-color var(--dur-fast-01) var(--ease-productive); }
.seitenleiste a:hover { background: var(--panel); }
.seitenleiste a[aria-current] { background: var(--panel); color: var(--text); }
.seitenleiste a span:last-child { font: 450 11px/1 'Geist Mono', monospace; color: var(--leise); }
.seitenleiste i { display: inline-block; width: 8px; height: 8px; border-radius: 2px; margin-right: 8px; background: var(--leise); }

.haupt { position: relative; min-width: 0; overflow: hidden; display: grid; grid-template-rows: auto minmax(0, 1fr) auto; }
.tabelle-kopf, .zeile { display: grid; grid-template-columns: 78px minmax(0, 1.6fr) 64px 100px 112px; align-items: center; gap: 12px; padding: 0 16px 0 18px; }
.tabelle-kopf { height: 34px; border-bottom: 1px solid var(--linie); color: var(--leise); font-size: 11px; }
.tabelle-kopf .rechts, .zeile .betrag { text-align: right; }
.zeilen { list-style: none; padding: 0; overflow: hidden; }
.zeile { position: relative; width: 100%; height: 42px; border-bottom: 1px solid rgba(36, 39, 40, .7); font-size: 13.5px; text-align: left; color: var(--text-2); transition: background-color var(--dur-fast-01) var(--ease-productive); }
.zeile::before { content: ''; position: absolute; left: 0; top: 8px; bottom: 8px; width: 2px; border-radius: 2px; background: var(--marke); opacity: 0; transition: opacity var(--dur-fast-02) var(--ease-productive); }
.zeile:hover, .zeile.zeiger-auf { background: rgba(255, 255, 255, .035); }
.zeile[aria-selected="true"] { background: rgba(123, 112, 255, .08); color: var(--text); }
.zeile[aria-selected="true"]::before { opacity: 1; }
.zeile .nr { font: 450 12px/1 'Geist Mono', monospace; color: var(--leise); }
.zeile .wer { color: var(--text); font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.zeile .datum { font: 450 12px/1 'Geist Mono', monospace; color: var(--leise); }
.zeile .betrag { font: 450 13px/1 'Geist Mono', monospace; color: var(--text); }
.status { justify-self: start; display: inline-flex; align-items: center; gap: 6px; height: 22px; padding: 0 8px; border-radius: 999px; font-size: 12px; font-weight: 500; color: var(--farbe); background: color-mix(in srgb, var(--farbe) 15%, transparent); transition: color var(--dur-moderate-01) var(--ease-productive), background-color var(--dur-moderate-01) var(--ease-productive); }
.status::before { content: ''; width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
.status[data-s="neu"] { --farbe: var(--blau); }
.status[data-s="ok"] { --farbe: var(--gruen); }
.status[data-s="frage"] { --farbe: var(--gelb); }
.status[data-s="fehler"] { --farbe: var(--rot); }
.status[data-s="gebucht"] { --farbe: var(--text-2); }
.protokoll { display: flex; align-items: center; gap: 14px; height: 34px; padding: 0 16px 0 18px; border-top: 1px solid var(--linie); color: var(--leise); white-space: nowrap; overflow: hidden; }
.protokoll .eintrag { display: inline-flex; gap: 8px; align-items: center; }
.protokoll .eintrag::before { content: ''; width: 6px; height: 6px; border-radius: 50%; background: var(--linie); }
.protokoll .eintrag:nth-child(2) { color: var(--text-2); }
.protokoll .eintrag:nth-child(2)::before { background: var(--marke); }
.protokoll .eintrag:nth-child(n+4) { opacity: .6; }
.protokoll b { font-weight: 450; color: var(--text); }

/* Seitenpanel (Drawer) */
.panel { position: absolute; z-index: 3; top: 0; right: 0; bottom: 34px; width: min(360px, 100%); display: grid; grid-template-rows: auto auto auto minmax(0, 1fr) auto; background: var(--panel); border-left: 1px solid var(--linie); box-shadow: -24px 0 48px -24px rgba(0, 0, 0, .7); }
.panel[hidden] { display: none; }
.panel-kopf { display: flex; justify-content: space-between; align-items: center; padding: 12px 14px 0 16px; color: var(--leise); }
.schliessen { display: grid; place-items: center; width: 28px; height: 28px; border-radius: 6px; color: var(--leise); transition: background-color var(--dur-fast-01) var(--ease-productive), color var(--dur-fast-01) var(--ease-productive); }
.schliessen:hover { background: rgba(255, 255, 255, .06); color: var(--text); }
.schliessen svg { width: 14px; height: 14px; }
.panel-titel { padding: 6px 16px 0; }
.panel-titel h3 { font-weight: 560; font-size: 17px; letter-spacing: -.012em; }
.panel-titel .summe { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; margin-top: 10px; }
.panel-titel .summe b { font: 500 24px/1 'Geist', sans-serif; letter-spacing: -.02em; font-variant-numeric: tabular-nums; }
.panel-titel .summe span { color: var(--leise); }
.reiter { position: relative; display: flex; gap: 4px; margin: 14px 16px 0; border-bottom: 1px solid var(--linie); }
.reiter button { height: 34px; padding: 0 8px; font-size: 13px; color: var(--leise); transition: color var(--dur-fast-01) var(--ease-productive); }
.reiter button[aria-selected="true"] { color: var(--text); }
.reiter .strich { position: absolute; bottom: -1px; left: 0; height: 2px; width: 0; background: var(--text); border-radius: 2px; transition: translate var(--tab-dauer) var(--tab-kurve), width var(--tab-dauer) var(--tab-kurve); }
.reiter-inhalt { overflow: hidden; padding: 12px 16px; }
.pruefung { list-style: none; padding: 0; display: grid; gap: 2px; }
.pruefung li { display: grid; grid-template-columns: 18px minmax(0, 1fr); gap: 10px; align-items: start; padding: 8px 0; font-size: 13px; line-height: 1.4; color: var(--text-2); border-bottom: 1px solid rgba(36, 39, 40, .7); }
.pruefung li small { display: block; color: var(--leise); font-size: 12px; }
.pruefung svg { width: 16px; height: 16px; margin-top: 1px; }
.konto { display: grid; gap: 8px; }
.konto div { display: flex; justify-content: space-between; gap: 12px; padding: 9px 10px; border-radius: 8px; background: var(--flaeche); box-shadow: inset 0 0 0 1px var(--linie); font-size: 13px; }
.konto div span:first-child { color: var(--leise); }
.konto div span:last-child { text-align: right; }
.sicherheit { position: relative; height: 4px; border-radius: 2px; background: var(--linie); margin-top: 4px; overflow: hidden; }
.sicherheit i { position: absolute; inset: 0; background: var(--gruen); transform-origin: left; }
.panel-fuss { display: flex; justify-content: flex-end; gap: 8px; padding: 12px 16px; border-top: 1px solid var(--linie); }
.buchen { position: relative; overflow: hidden; }
.buchen .fortschritt { position: absolute; left: 0; bottom: 0; height: 2px; width: 100%; background: #fff; transform-origin: left; transform: scaleX(0); }

/* Toast */
.toast { position: absolute; z-index: 4; left: 50%; bottom: 48px; translate: -50% 0; display: flex; align-items: center; gap: 14px; height: 40px; padding: 0 8px 0 14px; border-radius: 10px; background: #1f2122; box-shadow: inset 0 0 0 1px #2e3133, 0 12px 32px -8px rgba(0, 0, 0, .7); font-size: 13px; white-space: nowrap; }
.toast[hidden] { display: none; }
.toast svg { width: 15px; height: 15px; color: var(--gruen); }
.toast button { height: 26px; padding: 0 8px; border-radius: 6px; color: var(--text-2); font-size: 12.5px; transition: background-color var(--dur-fast-01) var(--ease-productive); }
.toast button:hover { background: rgba(255, 255, 255, .06); }

/* Gedachter Zeiger der Vorführung */
.zeiger { position: fixed; left: 0; top: 0; z-index: 20; width: 18px; height: 22px; pointer-events: none; display: none; filter: drop-shadow(0 2px 4px rgba(0, 0, 0, .5)); }
.vorfuehrung .zeiger { display: block; }

/* ——— Pult: Varianten und Token (im Fluss) ——— */
.pult { display: grid; grid-template-columns: auto minmax(0, 1fr); align-items: center; gap: 14px 28px; margin-top: 18px; padding-top: 16px; border-top: 1px solid var(--linie); color: var(--leise); }
.varianten { display: flex; gap: 2px; padding: 3px; border-radius: 9px; box-shadow: inset 0 0 0 1px var(--linie); }
.varianten button { height: 30px; padding: 0 12px; border-radius: 6px; color: var(--leise); transition: color var(--dur-fast-01) var(--ease-productive), background-color var(--dur-fast-01) var(--ease-productive); }
.varianten button:hover { color: var(--text); }
.varianten button[aria-pressed="true"] { background: var(--panel); color: var(--text); box-shadow: inset 0 0 0 1px #2e3133; }
.token { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 4px 16px; }
.token span { transition: color var(--dur-moderate-01) var(--ease-productive); }
.token b { font-weight: 450; color: var(--text-2); }
.token span.an, .token span.an b { color: #b3acff; }

/* ——— Vorschau: nur die Bühne ——— */
.vorschau body { overflow: hidden; height: 100svh; }
.vorschau .pult, .vorschau .unter { display: none; }
.vorschau .held { padding-block: 22px 20px; }
.vorschau .neu { display: none; }
/* Vorschau: die App füllt den Rest der Bühne, damit Panel und Protokoll sichtbar bleiben */
.vorschau .seite { height: 100svh; display: flex; flex-direction: column; padding-bottom: 18px; }
.vorschau .app { flex: 1; height: auto; min-height: 0; }

/* ——— Reduzierte Bewegung: keine Translation, kein Blur, Deckkraft ≤ 150 ms ——— */
:root.reduziert *, :root.reduziert *::before, :root.reduziert *::after { transition-duration: .01ms !important; animation-duration: .01ms !important; }

/* ——— Breiten ——— */
@media (max-width: 1020px) {
  .nav { display: none; }
  .app { grid-template-columns: 0 minmax(0, 1fr); }
  .seitenleiste { visibility: hidden; }
}
@media (max-width: 760px) {
  .held { grid-template-columns: 1fr; }
  .pult { grid-template-columns: 1fr; }
  .token { justify-content: flex-start; }
}
@media (max-width: 640px) {
  .kopf-rechts .anmelden { display: none; }
  .app { height: 540px; grid-template-rows: 44px minmax(0, 1fr); }
  .pfad .weg, .suche span { display: none; }
  .tabelle-kopf, .zeile { grid-template-columns: minmax(0, 1fr) 84px 96px; padding: 0 12px 0 14px; gap: 10px; }
  .tabelle-kopf .nr, .tabelle-kopf .datum, .zeile .datum { display: none; }
  .zeile { height: 52px; }
  .zeile .nr { display: none; }
  .zeile .wer small { display: block; margin-top: 3px; font: 450 11px/1 'Geist Mono', monospace; color: var(--leise); }
  /* Panel wird zum Sheet von unten */
  .panel { top: auto; left: 0; width: 100%; height: 86%; border-left: 0; border-top: 1px solid var(--linie); border-radius: 12px 12px 0 0; box-shadow: 0 -24px 48px -24px rgba(0, 0, 0, .7); }
  .protokoll .eintrag:nth-child(n+3) { display: none; }
  .toast { bottom: 44px; }
  .varianten { width: 100%; display: grid; grid-template-columns: repeat(3, 1fr); }
}
@media (min-width: 641px) { .zeile .wer small { display: none; } }
/* 320 px: Lieferant und Belegnummer brechen um statt abgeschnitten zu werden */
@media (max-width: 360px) {
  .tabelle-kopf, .zeile { grid-template-columns: minmax(0, 1fr) auto auto; gap: 8px; }
  .zeile { height: auto; min-height: 52px; padding-block: 7px; }
  .zeile .wer { white-space: normal; overflow: visible; text-overflow: clip; line-height: 1.2; overflow-wrap: anywhere; }
  .zeile .wer small { line-height: 1.25; }
  .status { padding: 0 7px; font-size: 11.5px; }
}
@media (forced-colors: active) {
  button, [role=button], [role=tab], [role=switch] { border: 1px solid ButtonText !important; }
}
/* Touch: kein Hover-Zustand, Ziele größer */
@media (hover: none) {
  .zeile:hover { background: none; }
  .schliessen { width: 36px; height: 36px; }
  .varianten button { height: 38px; }
}
</style>
</head>
<body>
<div class="seite">
  <header class="kopf">
    <a class="marke" href="#" aria-label="Klarbeleg, Startseite">
      <svg viewBox="0 0 20 20" aria-hidden="true"><rect x="3" y="2" width="14" height="16" rx="2.5" fill="none" stroke="#f7f8f8" stroke-width="1.6"/><path d="M6.5 10.2l2.4 2.4 4.6-5" fill="none" stroke="#7b70ff" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg>
      Klarbeleg
    </a>
    <ul class="nav">
      <li><a href="#">Produkt</a></li>
      <li><a href="#">Für Kanzleien</a></li>
      <li><a href="#">Preise</a></li>
      <li><a href="#">Sicherheit</a></li>
      <li><a href="#">Changelog</a></li>
    </ul>
    <div class="kopf-rechts">
      <a class="anmelden" href="#">Anmelden</a>
      <a class="knopf primaer klein" href="#">Kostenlos testen</a>
    </div>
  </header>

  <section class="held">
    <div data-held>
      <a class="neu" href="#"><b>Neu</b>Skonto-Erkennung im DATEV-Export</a>
      <h1>Jeder Beleg geprüft, bevor er gebucht wird.</h1>
    </div>
    <div data-held>
      <p class="unter">Klarbeleg liest Rechnungen, prüft die Pflichtangaben nach § 14 UStG und schlägt das Konto vor. Entwickelt in Saarbrücken, gehostet in Frankfurt.</p>
      <div class="ctas"><a class="knopf primaer" href="#">Kostenlos testen</a><a class="knopf zweit" href="#">Demo buchen</a></div>
    </div>
  </section>

  <div class="app" data-app role="region" aria-label="Produktansicht Klarbeleg (Beispiel)">
    <div class="app-kopf">
      <div class="pfad"><span class="weg">Kanzlei Weiler &amp; Hahn</span><span class="weg">/</span><b>Bäckerei Ruppert GmbH</b><span>/</span><span>Belege Oktober</span></div>
      <span class="suche"><span>Suchen</span><kbd>⌘K</kbd></span>
    </div>
    <nav class="seitenleiste" aria-label="App-Navigation (Beispiel)">
      <a href="#" aria-current="page"><span><i style="background: var(--blau)"></i>Eingang</span><span>12</span></a>
      <a href="#"><span><i style="background: var(--gelb)"></i>Rückfragen</span><span>2</span></a>
      <a href="#"><span><i style="background: var(--gruen)"></i>Geprüft</span><span>31</span></a>
      <a href="#"><span><i></i>Gebucht</span><span>148</span></a>
      <p class="mono versal">Mandanten</p>
      <a href="#"><span>Bäckerei Ruppert</span><span>12</span></a>
      <a href="#"><span>Praxis Dr. Seelbach</span><span>7</span></a>
      <a href="#"><span>Elektro Ahne KG</span><span>4</span></a>
    </nav>
    <div class="haupt">
      <div class="tabelle-kopf mono versal"><span class="nr">Beleg</span><span>Lieferant</span><span class="datum">Datum</span><span class="rechts">Betrag</span><span>Status</span></div>
      <div class="zeilen" role="listbox" aria-label="Belege" data-zeilen></div>
      <p class="protokoll mono" data-protokoll aria-hidden="true"><span>Bewegung</span></p>

      <aside class="panel" data-panel hidden aria-label="Belegdetails">
        <div class="panel-kopf mono"><span data-p-nr>RE-0412</span><button class="schliessen" type="button" data-schliessen aria-label="Details schließen"><svg viewBox="0 0 14 14" aria-hidden="true"><path d="M3 3l8 8M11 3l-8 8" stroke="currentColor" stroke-width="1.5"/></svg></button></div>
        <div class="panel-titel"><h3 data-p-wer>Mühle Rothfels KG</h3><div class="summe"><b data-p-betrag>1.842,60 €</b><span class="mono" data-p-ust>netto 1.722,06 € · USt 7 %</span></div></div>
        <div class="reiter" role="tablist" aria-label="Ansicht" data-reiter>
          <button type="button" role="tab" aria-selected="true" data-tab="pruefung">Prüfung</button>
          <button type="button" role="tab" aria-selected="false" data-tab="buchung">Buchung</button>
          <button type="button" role="tab" aria-selected="false" data-tab="verlauf">Verlauf</button>
          <span class="strich" data-strich aria-hidden="true"></span>
        </div>
        <div class="reiter-inhalt" role="tabpanel" data-inhalt></div>
        <div class="panel-fuss"><button class="knopf zweit klein" type="button">Rückfrage</button><button class="knopf primaer klein buchen" type="button" data-buchen>Buchen <kbd>↵</kbd><span class="fortschritt" data-fortschritt></span></button></div>
      </aside>

      <div class="toast" data-toast hidden role="status"><svg viewBox="0 0 16 16" aria-hidden="true"><circle cx="8" cy="8" r="7" fill="none" stroke="currentColor" stroke-width="1.4"/><path d="M5 8.2l2 2 4-4.4" fill="none" stroke="currentColor" stroke-width="1.6"/></svg><span data-toast-text>RE-0412 auf 3300 gebucht</span><button type="button">Rückgängig</button></div>
    </div>
  </div>

  <div class="pult mono">
    <div class="varianten" role="group" aria-label="Variante" data-varianten>
      <button type="button" data-variante="productive" aria-pressed="true">Productive</button>
      <button type="button" data-variante="expressive" aria-pressed="false">Expressive</button>
      <button type="button" data-variante="blur" aria-pressed="false">Mit Blur</button>
    </div>
    <p class="token" data-token></p>
  </div>
</div>
<svg class="zeiger" data-zeiger viewBox="0 0 18 22" aria-hidden="true"><path d="M1.5 1.5v17l4.6-4.4 3 6.4 3-1.4-3-6.2h6.4z" fill="#fff" stroke="#08090a" stroke-width="1.2" stroke-linejoin="round"/></svg>

<script>
(() => {
  const html = document.documentElement;
  const reduziert = () => html.classList.contains('reduziert');
  const $ = (s, el = document) => el.querySelector(s);
  const $$ = (s, el = document) => [...el.querySelectorAll(s)];
  const css = getComputedStyle(html);
  const tok = n => css.getPropertyValue(n).trim();
  const ms = n => parseFloat(tok(n));

  const K = {
    productive: tok('--ease-productive'), enter: tok('--ease-enter'), exit: tok('--ease-exit'),
    expressive: tok('--ease-expressive'), xEnter: tok('--ease-expressive-enter'), xExit: tok('--ease-expressive-exit'),
  };
  const D = { f1: ms('--dur-fast-01'), f2: ms('--dur-fast-02'), m1: ms('--dur-moderate-01'), m2: ms('--dur-moderate-02'), s1: ms('--dur-slow-01'), s2: ms('--dur-slow-02') };

  // Welche Token jede Variante wofür benutzt
  const VARIANTEN = {
    productive: { std: 'productive', ein: 'enter', aus: 'exit', tab: 'f2', inhalt: 'm1', panel: 'm2', panelAus: 'm1', liste: 'm2', toast: 'm2', toastAus: 'm1', held: 's1', stagger: 30, blur: 0 },
    expressive: { std: 'expressive', ein: 'xEnter', aus: 'xExit', tab: 'm1', inhalt: 'm2', panel: 's1', panelAus: 'm2', liste: 's1', toast: 's1', toastAus: 'm2', held: 's2', stagger: 40, blur: 0 },
    blur:       { std: 'productive', ein: 'enter', aus: 'exit', tab: 'f2', inhalt: 'm1', panel: 'm2', panelAus: 'm1', liste: 'm2', toast: 'm2', toastAus: 'm1', held: 's1', stagger: 30, blur: 4 },
  };
  const TOKEN_NAME = { f1: 'fast-01', f2: 'fast-02', m1: 'moderate-01', m2: 'moderate-02', s1: 'slow-01', s2: 'slow-02' };
  const KURVE_NAME = { productive: 'productive', enter: 'entrance', exit: 'exit', expressive: 'expressive', xEnter: 'expr. entrance', xExit: 'expr. exit' };
  let v = VARIANTEN.productive, varName = 'productive';

  /* ——— Daten ——— */
  const BELEGE = [
    { nr: 'RE-0412', wer: 'Mühle Rothfels KG', datum: '28.10.', betrag: '1.842,60 €', netto: 'netto 1.722,06 € · USt 7 %', s: 'neu', konto: '3300 Wareneingang 7 % VSt', kred: '70412 Mühle Rothfels KG', sicher: .98,
      checks: [['ok', 'Pflichtangaben nach § 14 UStG vollständig', '11 von 11 Feldern erkannt'], ['ok', 'USt-IdNr. des Lieferanten gültig', 'Abfrage BZSt, 28.10. 07:13'], ['ok', 'Betrag passt zur Bestellung B-2291', '40 Sack Weizenmehl Type 550'], ['frage', 'Skonto 2 % bis 11.11. möglich', '36,85 € sparen, Zahlung vormerken']] },
    { nr: 'RE-0411', wer: 'Energie Saar-Nord', datum: '27.10.', betrag: '612,00 €', netto: 'netto 514,29 € · USt 19 %', s: 'ok', konto: '4240 Gas, Strom, Wasser', kred: '70388 Energie Saar-Nord', sicher: .99,
      checks: [['ok', 'Pflichtangaben vollständig', '11 von 11 Feldern erkannt'], ['ok', 'Abschlag wie im Vormonat', 'Abweichung 0,0 %'], ['ok', 'Lastschrift erkannt', 'Keine Zahlung nötig']] },
    { nr: 'RE-0410', wer: 'Kübler Verpackung', datum: '27.10.', betrag: '389,45 €', netto: 'netto 327,27 € · USt 19 %', s: 'fehler', konto: '3400 Wareneingang 19 % VSt', kred: '70251 Kübler Verpackung', sicher: .71,
      checks: [['ok', 'Pflichtangaben vollständig', '10 von 11 Feldern erkannt'], ['fehler', 'Menge weicht vom Lieferschein ab', '500 Tüten berechnet, 400 geliefert'], ['frage', 'Leistungsdatum fehlt', 'Lieferschein 24.10. als Ersatz?']] },
    { nr: 'RE-0409', wer: 'Autohaus Bliesen', datum: '24.10.', betrag: '1.204,30 €', netto: 'netto 1.012,02 € · USt 19 %', s: 'frage', konto: '4540 Kfz-Reparaturen', kred: '70177 Autohaus Bliesen', sicher: .88,
      checks: [['ok', 'Pflichtangaben vollständig', '11 von 11 Feldern erkannt'], ['frage', 'Fahrzeug nicht zugeordnet', 'Kennzeichen SB-RU 214 oder SB-RU 88?']] },
    { nr: 'RE-0408', wer: 'Druckerei Saarfeld', datum: '23.10.', betrag: '268,80 €', netto: 'netto 225,88 € · USt 19 %', s: 'ok', konto: '4600 Werbekosten', kred: '70302 Druckerei Saarfeld', sicher: .97,
      checks: [['ok', 'Pflichtangaben vollständig', '11 von 11 Feldern erkannt'], ['ok', 'Angebot A-118 angenommen', '2.000 Flyer Adventsaktion']] },
    { nr: 'RE-0407', wer: 'Spedition Fechingen', datum: '22.10.', betrag: '975,00 €', netto: 'netto 819,33 € · USt 19 %', s: 'neu', konto: '4730 Ausgangsfrachten', kred: '70415 Spedition Fechingen', sicher: .93,
      checks: [['ok', 'Pflichtangaben vollständig', '11 von 11 Feldern erkannt'], ['ok', 'Touren mit Lieferliste abgeglichen', '13 von 13 Touren']] },
    { nr: 'RE-0406', wer: 'Büro Wendel', datum: '21.10.', betrag: '74,90 €', netto: 'netto 62,94 € · USt 19 %', s: 'ok', konto: '4930 Bürobedarf', kred: '70120 Büro Wendel', sicher: .99,
      checks: [['ok', 'Kleinbetrag unter 250 €', 'vereinfachte Rechnung zulässig']] },
    { nr: 'RE-0405', wer: 'Getränkehandel Ensheim', datum: '20.10.', betrag: '436,10 €', netto: 'netto 366,47 € · USt 19 %', s: 'ok', konto: '4650 Bewirtungskosten', kred: '70233 Getränkehandel Ensheim', sicher: .9,
      checks: [['ok', 'Pflichtangaben vollständig', '11 von 11 Feldern erkannt'], ['frage', 'Leergut verrechnet', 'Pfand 48,00 € separat buchen?']] },
    { nr: 'RE-0404', wer: 'Reinigung Blank & Söhne', datum: '20.10.', betrag: '142,80 €', netto: 'netto 120,00 € · USt 19 %', s: 'ok', konto: '4250 Reinigung', kred: '70119 Reinigung Blank & Söhne', sicher: .99,
      checks: [['ok', 'Dauerauftrag Oktober', 'wie Vormonat']] },
  ];
  const STATUS_TEXT = { neu: 'Zu prüfen', ok: 'Geprüft', frage: 'Rückfrage', fehler: 'Abweichung', gebucht: 'Gebucht' };
  const ICON = {
    ok: '<svg viewBox="0 0 16 16" aria-hidden="true"><circle cx="8" cy="8" r="7" fill="none" stroke="#59d499" stroke-width="1.4"/><path d="M5 8.2l2 2 4-4.4" fill="none" stroke="#59d499" stroke-width="1.6"/></svg>',
    frage: '<svg viewBox="0 0 16 16" aria-hidden="true"><circle cx="8" cy="8" r="7" fill="none" stroke="#ffc533" stroke-width="1.4"/><path d="M8 4.5v4.2M8 10.8v.8" stroke="#ffc533" stroke-width="1.6"/></svg>',
    fehler: '<svg viewBox="0 0 16 16" aria-hidden="true"><circle cx="8" cy="8" r="7" fill="none" stroke="#ff6161" stroke-width="1.4"/><path d="M5.6 5.6l4.8 4.8M10.4 5.6l-4.8 4.8" stroke="#ff6161" stroke-width="1.6"/></svg>',
  };

  const zeilenBox = $('[data-zeilen]'), panel = $('[data-panel]'), inhalt = $('[data-inhalt]');
  const reiter = $$('[data-tab]'), strich = $('[data-strich]'), toast = $('[data-toast]');
  const protokollEl = $('[data-protokoll]'), tokenEl = $('[data-token]'), zeiger = $('[data-zeiger]');
  let auswahl = -1, tab = 'pruefung', laufend = [];

  function zeilenAufbauen() {
    zeilenBox.innerHTML = BELEGE.map((b, i) => `<button type="button" class="zeile" role="option" aria-selected="false" data-i="${i}">
      <span class="nr">${b.nr}</span><span class="wer">${b.wer}<small>${b.nr} · ${b.datum}</small></span><span class="datum">${b.datum}</span>
      <span class="betrag">${b.betrag}</span><span class="status" data-s="${b.s}">${STATUS_TEXT[b.s]}</span></button>`).join('');
  }

  /* ——— Token-Anzeige und Bewegungsprotokoll ——— */
  const TOKENS = [['productive', '(0.2, 0, 0.38, 0.9)'], ['enter', '(0, 0, 0.38, 0.9)'], ['exit', '(0.2, 0, 1, 0.9)'], ['expressive', '(0.4, 0.14, 0.3, 1)'], ['xEnter', '(0, 0, 0.3, 1)'], ['xExit', '(0.4, 0.14, 1, 1)'],
    ['f1', '70'], ['f2', '110'], ['m1', '150'], ['m2', '240'], ['s1', '400'], ['s2', '700']];
  tokenEl.innerHTML = TOKENS.map(([k, w]) => `<span data-t="${k}">${KURVE_NAME[k] || TOKEN_NAME[k]} <b>${w}</b></span>`).join('');
  const tokenUhr = {};
  function leuchte(...keys) {
    keys.forEach(k => { const el = tokenEl.querySelector(`[data-t="${k}"]`); if (!el) return; el.classList.add('an'); clearTimeout(tokenUhr[k]); tokenUhr[k] = setTimeout(() => el.classList.remove('an'), 900); });
  }
  const eintraege = [];
  function protokoll(was, d, kurve, weg) {
    leuchte(d, kurve);
    const dauer = reduziert() ? '≤150 ms' : `${D[d]} ms`;
    eintraege.unshift(`<span class="eintrag">${was} <b>${dauer}</b> ${reduziert() ? 'linear' : KURVE_NAME[kurve]}${weg && !reduziert() ? ' · ' + weg : ''}</span>`);
    eintraege.length = Math.min(eintraege.length, 3);
    protokollEl.innerHTML = '<span>Bewegung</span>' + eintraege.join('');
  }

  /* ——— Grundbaustein: Deckkraft + kleiner Weg (+ optional Blur) ——— */
  function merke(a) { laufend.push(a); a.finished.then(() => { laufend = laufend.filter(x => x !== a); }, () => {}); return a; }
  function ein(el, { d, kurve, x = 0, y = 0, verzug = 0 }) {
    if (reduziert()) return merke(el.animate([{ opacity: 0 }, { opacity: 1 }], { duration: D.m1, easing: 'linear', delay: verzug, fill: 'backwards' }));
    const von = { opacity: 0, transform: `translate(${x}px, ${y}px)` }, nach = { opacity: 1, transform: 'translate(0, 0)' };
    if (v.blur) { von.filter = `blur(${v.blur}px)`; nach.filter = 'blur(0px)'; }
    return merke(el.animate([von, nach], { duration: D[d], easing: K[kurve], delay: verzug, fill: 'backwards' }));
  }
  function aus(el, { d, kurve, x = 0, y = 0 }) {
    if (reduziert()) return merke(el.animate([{ opacity: 1 }, { opacity: 0 }], { duration: 100, easing: 'linear', fill: 'forwards' }));
    return merke(el.animate([{ opacity: 1, transform: 'translate(0, 0)' }, { opacity: 0, transform: `translate(${x}px, ${y}px)` }], { duration: D[d], easing: K[kurve], fill: 'forwards' }));
  }
  const schmal = matchMedia('(max-width: 640px)');

  /* ——— Panel ——— */
  function zeigeTab(name, mitBewegung) {
    tab = name;
    reiter.forEach(b => b.setAttribute('aria-selected', String(b.dataset.tab === name)));
    const aktiv = reiter.find(b => b.dataset.tab === name);
    strich.style.width = aktiv.offsetWidth + 'px';
    strich.style.translate = aktiv.offsetLeft + 'px 0';
    const b = BELEGE[auswahl];
    if (name === 'pruefung') {
      inhalt.innerHTML = `<ul class="pruefung">${b.checks.map(([s, t, k]) => `<li>${ICON[s]}<span>${t}<small>${k}</small></span></li>`).join('')}</ul>`;
    } else if (name === 'buchung') {
      inhalt.innerHTML = `<div class="konto"><div><span>Konto (SKR 03)</span><span>${b.konto}</span></div><div><span>Gegenkonto</span><span>${b.kred}</span></div>
        <div style="display:block"><span style="display:flex;justify-content:space-between"><span style="color:var(--leise)">Sicherheit Vorschlag</span><span class="mono">${Math.round(b.sicher * 100)} %</span></span><span class="sicherheit"><i style="transform:scaleX(${b.sicher})"></i></span></div></div>`;
    } else {
      inhalt.innerHTML = `<ul class="pruefung"><li><span class="mono">07:12</span><span>Eingang per E-Mail<small>rechnungen@ruppert-baeckerei.de</small></span></li><li><span class="mono">07:12</span><span>Texterkennung<small>1 Seite, 1,4 s</small></span></li><li><span class="mono">07:13</span><span>Prüfung abgeschlossen<small>${b.checks.length} Regeln</small></span></li></ul>`;
      $$('.pruefung li', inhalt).forEach(li => li.style.gridTemplateColumns = '42px minmax(0, 1fr)');
    }
    if (!mitBewegung) return;
    // Tab-Wechsel: Strich gleitet (CSS), Inhalt blendet mit 4 px ein
    protokoll('Reiter', v.tab, v.std, 'x');
    $$('li, .konto > div', inhalt).forEach((el, i) => ein(el, { d: v.inhalt, kurve: v.ein, y: 4, verzug: i * v.stagger }));
  }

  function oeffne(i) {
    const warOffen = !panel.hidden;
    $$('.zeile').forEach(z => z.setAttribute('aria-selected', String(+z.dataset.i === i)));
    auswahl = i;
    const b = BELEGE[i];
    $('[data-p-nr]').textContent = b.nr;
    $('[data-p-wer]').textContent = b.wer;
    $('[data-p-betrag]').textContent = b.betrag;
    $('[data-p-ust]').textContent = b.netto;
    panel.hidden = false;
    zeigeTab('pruefung', false);
    if (!warOffen) {
      ein(panel, schmal.matches ? { d: v.panel, kurve: v.ein, y: 8 } : { d: v.panel, kurve: v.ein, x: 8 });
      protokoll('Panel rein', v.panel, v.ein, schmal.matches ? 'y 8 → 0' : 'x 8 → 0');
    }
    // Prüfpunkte gestaffelt: Deckkraft + 4 px
    $$('li', inhalt).forEach((el, n) => ein(el, { d: v.liste, kurve: v.ein, y: 4, verzug: (warOffen ? 0 : 60) + n * v.stagger }));
    if (warOffen) protokoll('Inhalt', v.liste, v.ein, `y 4 → 0, ${v.stagger} ms Versatz`);
  }
  async function schliesse() {
    if (panel.hidden) return;
    $$('.zeile').forEach(z => z.setAttribute('aria-selected', 'false'));
    protokoll('Panel raus', v.panelAus, v.aus, schmal.matches ? 'y 0 → 8' : 'x 0 → 8');
    const a = aus(panel, schmal.matches ? { d: v.panelAus, kurve: v.aus, y: 8 } : { d: v.panelAus, kurve: v.aus, x: 8 });
    await a.finished.catch(() => {});
    if (a.playState === 'finished') { panel.hidden = true; a.cancel(); }
    auswahl = -1;
  }

  /* ——— Buchen: aria-busy, Fortschritt, Status, Toast ——— */
  let toastUhr = null;
  async function buchen() {
    if (auswahl < 0) return;
    const knopf = $('[data-buchen]'), balken = $('[data-fortschritt]');
    knopf.setAttribute('aria-busy', 'true');
    protokoll('Druck', 'f1', 'productive');
    const lauf = merke(balken.animate([{ transform: 'scaleX(0)' }, { transform: 'scaleX(1)' }], { duration: reduziert() ? 1 : 520, easing: 'linear' }));
    await lauf.finished.catch(() => {});
    knopf.removeAttribute('aria-busy');
    const b = BELEGE[auswahl];
    b.s = 'gebucht';
    const pill = $(`.zeile[data-i="${auswahl}"] .status`);
    pill.dataset.s = 'gebucht'; pill.textContent = STATUS_TEXT.gebucht;
    $('[data-toast-text]').textContent = `${b.nr} auf ${b.konto.split(' ')[0]} gebucht`;
    zeigeToast();
  }
  function zeigeToast() {
    clearTimeout(toastUhr);
    toast.getAnimations().forEach(a => a.cancel());
    toast.hidden = false;
    ein(toast, { d: v.toast, kurve: v.ein, y: 8 });
    protokoll('Toast rein', v.toast, v.ein, 'y 8 → 0');
    toastUhr = setTimeout(versteckeToast, 2600);
  }
  async function versteckeToast() {
    if (toast.hidden) return;
    protokoll('Toast raus', v.toastAus, v.aus, 'y 0 → 4');
    const a = aus(toast, { d: v.toastAus, kurve: v.aus, y: 4 });
    await a.finished.catch(() => {});
    if (a.playState === 'finished') { toast.hidden = true; a.cancel(); }
  }

  /* ——— Ereignisse ——— */
  zeilenBox.addEventListener('click', e => { const z = e.target.closest('.zeile'); if (!z) return; stopp(); oeffne(+z.dataset.i); });
  $('[data-schliessen]').addEventListener('click', () => { stopp(); schliesse(); });
  reiter.forEach(b => b.addEventListener('click', () => { stopp(); zeigeTab(b.dataset.tab, true); }));
  reiter.forEach(b => b.addEventListener('keydown', e => {
    if (e.key !== 'ArrowRight' && e.key !== 'ArrowLeft') return;
    const i = reiter.indexOf(b), n = reiter[(i + (e.key === 'ArrowRight' ? 1 : reiter.length - 1)) % reiter.length];
    n.focus(); n.click();
  }));
  $('[data-buchen]').addEventListener('click', () => { stopp(); buchen(); });
  addEventListener('keydown', e => { if (e.key === 'Escape') { stopp(); schliesse(); } });

  function setzeVariante(name) {
    varName = name; v = VARIANTEN[name];
    $$('[data-variante]').forEach(b => b.setAttribute('aria-pressed', String(b.dataset.variante === name)));
    html.style.setProperty('--tab-dauer', D[v.tab] + 'ms');
    html.style.setProperty('--tab-kurve', K[v.std]);
  }
  $$('[data-variante]').forEach(b => b.addEventListener('click', () => { stopp(); setzeVariante(b.dataset.variante); auftakt(); }));

  /* ——— Auftakt: Hero-Einblendung mit 8 px ——— */
  function auftakt() {
    laufend.forEach(a => a.cancel()); laufend = [];
    clearTimeout(toastUhr); toast.hidden = true; panel.hidden = true; auswahl = -1;
    BELEGE[0].s = 'neu'; BELEGE[5].s = 'neu';
    zeilenAufbauen();
    eintraege.length = 0; protokollEl.innerHTML = '<span>Bewegung</span>';
    const teile = [...$$('[data-held]'), $('[data-app]')];
    teile.forEach((el, i) => ein(el, { d: v.held, kurve: v.ein, y: 8, verzug: i * v.stagger * 2 }));
    $$('.zeile').forEach((z, i) => ein(z, { d: v.liste, kurve: v.ein, y: 4, verzug: 200 + i * v.stagger }));
    protokoll('Hero', v.held, v.ein, 'y 8 → 0');
  }

  /* ——— Vorführung mit gedachtem Zeiger ——— */
  let lauf = 0, pausiert = false, vorf = false;
  const warte = (t, nr) => new Promise((ok, nein) => {
    const ende = performance.now() + t;
    const tick = () => { if (nr !== lauf) return nein(); if (pausiert) return setTimeout(tick, 100); if (performance.now() >= ende) return ok(); setTimeout(tick, Math.min(60, ende - performance.now())); };
    tick();
  });
  let zx = 0, zy = 0;
  function zeigerZu(el, nr, dx = .5, dy = .55) {
    const r = el.getBoundingClientRect();
    const x = r.left + r.width * dx, y = r.top + r.height * dy;
    zeiger.animate([{ transform: `translate(${zx}px, ${zy}px)` }, { transform: `translate(${x}px, ${y}px)` }], { duration: reduziert() ? 1 : D.s1, easing: K.productive, fill: 'forwards' });
    zx = x; zy = y;
    $$('.zeiger-auf').forEach(z => z.classList.remove('zeiger-auf'));
    if (el.classList.contains('zeile')) el.classList.add('zeiger-auf');
    return warte(D.s1 + 120, nr);
  }
  async function schleife(nr) {
    const folge = ['productive', 'expressive', 'blur'];
    let runde = 0;
    const app = $('[data-app]').getBoundingClientRect();
    zx = app.left + app.width * .3; zy = app.bottom - 40;
    while (nr === lauf) {
      setzeVariante(folge[runde % 3]);
      auftakt();
      await warte(900, nr);
      await zeigerZu($('.zeile[data-i="0"]'), nr, .4);
      oeffne(0);
      await warte(1300, nr);
      await zeigerZu(reiter[1], nr);
      zeigeTab('buchung', true);
      await warte(900, nr);
      await zeigerZu($('[data-buchen]'), nr);
      buchen();
      await warte(1500, nr);
      await zeigerZu($('.zeile[data-i="2"]'), nr, .4);
      oeffne(2);
      await warte(1500, nr);
      await zeigerZu($('[data-schliessen]'), nr);
      schliesse();
      await warte(1300, nr);
      runde++;
    }
  }
  function stopp() { if (!vorf) return; vorf = false; lauf++; html.classList.remove('vorfuehrung'); $$('.zeiger-auf').forEach(z => z.classList.remove('zeiger-auf')); }
  function vorfuehren() {
    stopp(); pausiert = false; vorf = true; lauf++;
    html.classList.add('vorfuehrung');
    schleife(lauf).catch(() => {});
  }
  function neustart() { stopp(); pausiert = false; scrollTo(0, 0); auftakt(); }
  function pause() { pausiert = true; document.getAnimations().forEach(a => a.pause()); }
  function weiter() { pausiert = false; document.getAnimations().forEach(a => { if (a.playState === 'paused') a.play(); }); }

  zeilenAufbauen();
  (document.fonts ? document.fonts.ready : Promise.resolve()).then(() => { if (!vorf) auftakt(); });

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