Zum Inhalt
← Hero & Section-Layouts

Zahlen-/Fakten-Sektion als Typo-Ereignis

Wenige echte Kennzahlen als Riesenziffern: Beim Scrollen rollen sie wie ein Zählwerk auf ihren Wert, zählen hoch oder kippen auf.

Shop und Marke mit wenigen starken Zahlen, die Sektion darf laut sein (d2c, Handwerk): Riesenziffern mit Walze, Zählen oder Kippen am Scroll.

Statt drei Icon-Kärtchen mit „100+ Kunden“ stehen zwei bis vier Kennzahlen als riesige Ziffern im Raster, oft in einer sehr schmalen oder sehr fetten Display-Schrift. Jede Zahl hat eine kurze, präzise Beschriftung, dazu klein Quelle und Zeitraum. Beim Einscrollen zählen die Zahlen hoch, rollen wie ein mechanisches Zählwerk oder kippen auf. Varianten koppeln die Zahlen an Sticky-Seiten (Zahl steht, Erklärung scrollt), an Mini-Diagramme oder als Jahreszahl an die Scroll-Timeline einer Firmengeschichte.

Zahlen sind konkrete Beweise. Groß gesetzt werden sie zum grafischen Element, das die Sektion trägt. Das Zählen erzeugt einen kurzen Moment der Spannung, bis der Wert steht.

Wann einsetzen

Wenn es echte, belastbare Zahlen gibt: Jahre, Projekte, Bewertungen, Garantie- und Rückgabefristen, Ergebnisse aus Fallstudien. Im Shop als Vertrauens-Sektion, bei Firmen als Fakten-Block.

Wann nicht

Bei Eitelkeitszahlen ohne Aussage („1000 Tassen Kaffee“) und bei nur einer Zahl. Wo Zurückhaltung gefragt ist (Kanzlei, Medizin), stehen die Zahlen besser einfach da, ohne Hochzählen.

Varianten

  • Ziffern-Walze (Odometer): jede Stelle rollt als Streifen auf ihre Ziffer
  • Hochzählen beim Eintritt oder an den Scroll gekoppelt
  • Aufkippen: Zahl oder Karte dreht von rotateX -92° auf 0
  • Jahreszahl-Zähler an der Scroll-Timeline
  • Zahlen im Bento-Raster
  • Produktvergleichs-Statistik
  • Ruhige Tabelle: Zahlen stehen ohne Zählen in einer echten Tabelle, nur die Haarlinien zeichnen sich (Kanzlei, Medizin)
  • Mit Mini-Diagramm: Zahl zählt einmal, Balken, Linie oder Punkte bauen sich im selben Takt auf (Verein, Marktzahlen)

Umsetzung

Der Endwert steht als Text im DOM (visuell versteckt), die Animation läuft nur in einem aria-hidden-Duplikat. Ziffern mit font-variant-numeric: tabular-nums, bei Schriften mit Mediävalziffern zusätzlich lining-nums. Walze: jede Stelle ist ein Fenster (overflow: hidden, 1em hoch) mit einem Streifen 0–9; translateY bringt die Zielziffer ins Fenster, von rechts gestaffelt. Hochzählen: Wert × Fortschritt runden, mit führenden Nullen in fester Stellenzahl, damit nichts springt. Scroll-Kopplung: Fortschritt aus der Position der Kachel, linear, mit kurzer Glättung (GSAP: scrollTrigger { scrub: 0.6 }). CSS-only geht mit @property --n { syntax: '<integer>'; inherits: false; initial-value: 0 } plus counter-reset: n var(--n) und content: counter(n); der Wert steht dann aber nicht im DOM.

Robust umsetzen

Telefon. Zahlen untereinander, die Größe bleibt dominant (etwa 26–28vw). Jede Kachel bekommt ihren eigenen Scroll-Bereich statt eines gemeinsamen Staggers.

Ohne Bewegung. Endwert sofort anzeigen, ohne Walze, Zählen oder Kippen.

Barrierefrei. Der Endwert muss als Text im DOM stehen, nicht nur per CSS-Counter. Die animierte Kopie ist aria-hidden. Während des Zählens kein aria-live, sonst liest der Screenreader jeden Zwischenwert vor.

Code

<!DOCTYPE html>
<!--
  Zahlen-/Fakten-Sektion als Typo-Ereignis
  Fundus · Hero & Section-Layouts · https://cecodesign.de/lab/fundus/hero-layouts/zahlen-fakten-section/
  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>Zahlen-/Fakten-Sektion als Typo-Ereignis · 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: 'Young Serif'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/young-serif@latest/latin-400-normal.woff2') format('woff2'); font-weight: 400; font-display: swap; }
@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: 'Newsreader'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/newsreader:vf@latest/latin-opsz-normal.woff2') format('woff2-variations'); font-weight: 200 800; font-display: swap; }
@font-face { font-family: 'Newsreader'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/newsreader:vf@latest/latin-opsz-italic.woff2') format('woff2-variations'); font-weight: 200 800; font-style: italic; font-display: swap; }
@font-face { font-family: 'Schibsted Grotesk'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/schibsted-grotesk:vf@latest/latin-wght-normal.woff2') format('woff2-variations'); font-weight: 400 900; font-display: swap; }
@font-face { font-family: 'Big Shoulders'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/big-shoulders:vf@latest/latin-wght-normal.woff2') format('woff2-variations'); font-weight: 100 900; font-display: swap; }
@font-face { font-family: 'Archivo'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/archivo:vf@latest/latin-full-normal.woff2') format('woff2-variations'); font-weight: 100 900; font-stretch: 62% 125%; font-display: swap; }
@font-face { font-family: 'Geist Mono'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/geist-mono:vf@latest/latin-wght-normal.woff2') format('woff2-variations'); font-weight: 100 900; font-display: swap; }

/* Welt: Rezept „d2c-produkt-zuerst“ – Klüver, Taschen aus alten Segeln, Flensburg */
:root {
  --grund: #f5f4f1;          /* Allbirds natural-white-25 */
  --kachel: #ece9e2;         /* Produktkachel */
  --tinte: #212121;
  --leise: #686868;
  --linie: rgba(33, 33, 33, .14);
  --nacht: #03143b;          /* dunkle Kachel (Allbirds midnight) */
  --hell: #f5f4f1;
  --signal: #d8432b;         /* Segelnummer-Rot, Marken-Akzent */
  --gruen: #10714e;
  --ui: cubic-bezier(0, 0, 0.2, 1);  /* Rezept: UI-Ausklang */
  --serif: 'Young Serif', Georgia, serif;
  --sans: 'Instrument Sans', system-ui, sans-serif;
  --mono: 'Geist Mono', ui-monospace, monospace;
  --rand: clamp(16px, 3.4vw, 48px);
}
* { box-sizing: border-box; margin: 0; padding: 0; }
html { background: var(--grund); color: var(--tinte); font-family: var(--sans); font-size: 16px; -webkit-font-smoothing: antialiased; }
body { background: var(--grund); overflow-x: hidden; }
a { color: inherit; text-decoration: none; }
button { font: inherit; color: inherit; }
.sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
.label { font: 500 .75rem/1.3 var(--sans); letter-spacing: .06em; text-transform: uppercase; }
.mono { font: 500 11px/1.4 var(--mono); letter-spacing: .14em; text-transform: uppercase; color: var(--leise); }

/* ---------- Shop-Kopf ---------- */
.leiste { background: var(--tinte); color: var(--hell); text-align: center; padding: 8px var(--rand); font-size: .8125rem; }
.kopf { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 16px; padding: 18px var(--rand); border-bottom: 1px solid var(--linie); }
.kopf nav { display: flex; gap: clamp(14px, 2vw, 28px); font-weight: 500; font-size: .9375rem; }
.kopf nav a, .kopf__rechts a { transition: color .15s var(--ui); }
.kopf nav a:hover, .kopf__rechts a:hover { color: var(--leise); }
.wortmarke { font: 400 1.75rem/1 var(--serif); letter-spacing: -.01em; display: flex; align-items: center; gap: 8px; }
.wortmarke svg { width: 22px; height: 26px; }
.kopf__rechts { display: flex; justify-content: flex-end; gap: 20px; font-weight: 500; font-size: .9375rem; }
.korb b { display: inline-grid; place-items: center; min-width: 20px; height: 20px; margin-left: 6px; border-radius: 99px; background: var(--tinte); color: var(--hell); font-size: .75rem; font-variant-numeric: tabular-nums; }

/* ---------- Einstieg (Produkt zuerst, kompakt) ---------- */
.einstieg { display: grid; grid-template-columns: 1fr minmax(320px, 460px); gap: clamp(24px, 4vw, 64px); align-items: end; padding: clamp(28px, 3.6vw, 52px) var(--rand) clamp(28px, 3.6vw, 48px); }
.einstieg h1 { font: 400 clamp(2.25rem, 1.4rem + 3.6vw, 4.5rem)/1.02 var(--serif); letter-spacing: -.01em; max-width: 15ch; margin: 14px 0 16px; text-wrap: balance; }
.einstieg p.text { font-size: 1.0625rem; line-height: 1.55; max-width: 52ch; color: #3b3b3b; }
.einstieg .label { color: var(--leise); }
.knopf { display: inline-flex; align-items: center; justify-content: center; height: 44px; padding: 0 20px; border-radius: 99px; background: var(--tinte); color: var(--hell); font-weight: 600; font-size: .9375rem; transition: background-color .15s var(--ui); }
.knopf:hover { background: #000; }
.produkt { background: var(--kachel); border-radius: 6px; padding: 12px; display: grid; grid-template-columns: 132px 1fr; gap: 16px; align-items: center; }
.produkt__bild { aspect-ratio: 1; display: grid; place-items: center; position: relative; background: #e4e0d7; border-radius: 4px; }
.produkt__bild svg { width: 82%; height: auto; }
.produkt__bild .neu { position: absolute; top: 6px; left: 6px; background: #d43f27;   /* Signal-Rot minimal dunkler: weiße Schrift 4,6:1 */ color: #fff; border-radius: 99px; padding: 3px 8px; font-size: .6875rem; }
.produkt__info { display: grid; gap: 6px; padding-right: 6px; }
.produkt__zeile { display: flex; justify-content: space-between; align-items: baseline; gap: 12px; font-weight: 600; font-size: 1rem; }
.produkt__zeile span:last-child { font-variant-numeric: tabular-nums; }
.produkt__unter { display: flex; justify-content: space-between; align-items: center; color: var(--leise); font-size: .8125rem; margin-bottom: 6px; }
.swatches { display: flex; gap: 6px; }
.swatches i { width: 14px; height: 14px; border-radius: 50%; box-shadow: inset 0 0 0 1px rgba(0,0,0,.15); }
.lager { display: inline-flex; align-items: center; gap: 6px; }
.lager::before { content: ''; width: 7px; height: 7px; border-radius: 50%; background: var(--gruen); }

/* ---------- Die Zahlen-Sektion ---------- */
.zahlen { padding: clamp(24px, 3vw, 44px) var(--rand) clamp(40px, 6vw, 80px); border-top: 1px solid var(--linie); }
.zahlen__kopf { display: flex; justify-content: space-between; align-items: end; gap: 16px 32px; flex-wrap: wrap; margin-bottom: clamp(18px, 2.4vw, 32px); }
.zahlen__kopf h2 { font: 600 clamp(1.5rem, 1.1rem + 1.4vw, 2.25rem)/1.1 var(--sans); letter-spacing: -.015em; margin-top: 8px; }
.zahlen__kopf .label { color: var(--leise); }
.modi { display: flex; gap: 4px; }
.modi button { font: inherit; letter-spacing: inherit; text-transform: inherit; color: var(--leise); background: none; border: 1px solid var(--linie); border-radius: 999px; height: 30px; padding: 0 12px; cursor: pointer; transition: background-color .15s var(--ui), color .15s var(--ui), border-color .15s var(--ui); }
.modi button:hover { color: var(--tinte); border-color: rgba(33,33,33,.4); }
.modi button[aria-pressed='true'] { background: var(--tinte); color: var(--hell); border-color: var(--tinte); }
.modi button:focus-visible, .knopf:focus-visible, a:focus-visible { outline: 2px solid var(--signal); outline-offset: 3px; }

.raster { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; }
.kachel { position: relative; background: var(--kachel); border-radius: 6px; padding: clamp(16px, 1.8vw, 26px); display: grid; align-content: space-between; gap: 18px; overflow: hidden; min-width: 0; }
.kachel__kopf { position: relative; display: flex; justify-content: space-between; gap: 12px; padding-bottom: 12px; box-shadow: inset 0 -1px 0 color-mix(in srgb, currentColor 18%, transparent); }
.kachel__kopf::after { content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 1px; background: currentColor; transform: scaleX(var(--l, 1)); transform-origin: 0 50%; }
.kachel__kopf .label { font-weight: 600; }
.kachel__kopf .nr { font: 500 11px/1.3 var(--mono); letter-spacing: .14em; color: var(--leise); }
.kachel__text { font-size: .9375rem; line-height: 1.5; max-width: 34ch; }
.quelle { font-size: .75rem; color: var(--leise); margin-top: 8px; font-variant-numeric: tabular-nums; }

.gross { grid-column: 1 / -1; background: var(--nacht); color: var(--hell); grid-template-columns: 1fr minmax(220px, 300px); grid-template-rows: auto 1fr; column-gap: 32px; }
.gross .kachel__kopf { grid-column: 1 / -1; }
.gross .kachel__kopf .nr, .gross .quelle { color: rgba(245, 244, 241, .6); }
.gross .kachel__text { align-self: end; color: rgba(245, 244, 241, .86); position: relative; }
.gross .zahl { font-size: clamp(96px, min(23vw, 40svh), 340px); align-self: end; margin-bottom: -.04em; }
.gross .segel { position: absolute; right: -2%; top: -8%; height: 128%; width: auto; pointer-events: none; }

/* Riesenziffern: Young Serif setzt Mediävalziffern als Standard, hier Versal- und Tabellenziffern (lnum, tnum), Walze pro Stelle */
.zahl { font: 400 clamp(72px, 8.4vw, 136px)/1 var(--serif); letter-spacing: 0; display: flex; align-items: flex-start; font-variant-numeric: lining-nums tabular-nums; perspective: 900px; perspective-origin: 50% 100%; white-space: nowrap; }
.ziffern { display: inline-flex; transform-origin: 50% 100%; backface-visibility: hidden; will-change: transform; }
.stelle { display: inline-block; width: .63em; height: 1em; overflow: hidden; text-align: center; }
.streifen { display: block; will-change: transform; }
.streifen span { display: block; height: 1em; line-height: 1em; }
.zeichen { display: inline-block; height: 1em; line-height: 1em; width: .34em; text-align: center; }
.stelle, .zeichen { transition: opacity .15s var(--ui); }
.stelle.leer, .zeichen.leer { opacity: .18; }
.einheit { font-size: .26em; line-height: 1; margin: .32em 0 0 .16em; color: var(--signal); }
.gross .einheit { color: #ff8a6b; }

.werte { display: flex; flex-wrap: wrap; gap: 6px 18px; margin-top: 16px; }
.werte b { color: var(--tinte); font-weight: 500; }

/* Reduzierte Bewegung: Endwert sofort, keine Walzen, kein Kippen */
.reduziert .streifen, .reduziert .ziffern { transition: none !important; }
:root.reduziert * { animation-duration: .01ms !important; }

/* Vorschau 960 × 720: nur die Bühne */
.vorschau body { overflow: hidden; }
.vorschau .leiste, .vorschau .kopf, .vorschau .einstieg { display: none; }
.vorschau .zahlen { border-top: 0; min-height: 100svh; padding-top: 28px; padding-bottom: 24px; }
.vorschau .gross .zahl { font-size: 22vw; }

@media (max-width: 900px) {
  .kopf nav { display: none; }
  .kopf { grid-template-columns: 1fr auto; }
  .kopf__rechts .konto { display: none; }
  .einstieg { grid-template-columns: minmax(0, 1fr); }
  .einstieg > * { min-width: 0; }
  .gross { grid-template-columns: 1fr; }
  .gross .kachel__text { max-width: 40ch; }
}
@media (max-width: 640px) {
  .raster { grid-template-columns: 1fr; }
  .gross .zahl { font-size: 28vw; }
  .zahl { font-size: 26vw; }
  .gross .segel { height: 70%; top: -6%; right: -14%; opacity: .7; }
  .produkt { grid-template-columns: 104px 1fr; }
  .zahlen__kopf .modi { width: 100%; }
}
@media (hover: none) { .modi button { height: 36px; padding: 0 14px; } }
@media (forced-colors: active) { button, [role=button], [role=tab], [role=switch] { border: 1px solid ButtonText !important; } }
/* ===================== Varianten (Regel 13: ?v=typo | tabelle | diagramm) ===================== */
.welt[hidden] { display: none !important; }
.vpult { position: fixed; left: 14px; bottom: 14px; z-index: 30; display: flex; align-items: center; gap: 2px; padding: 4px; border-radius: 999px; background: rgba(18, 18, 20, .86); color: #f3f2ee; -webkit-backdrop-filter: blur(10px); backdrop-filter: blur(10px); box-shadow: 0 6px 24px rgba(0, 0, 0, .16); font: 500 10.5px/1 'Geist Mono', ui-monospace, monospace; letter-spacing: .14em; text-transform: uppercase; }
.vpult > span { padding: 0 10px 0 10px; color: rgba(243, 242, 238, .55); }
.vpult button { font: inherit; letter-spacing: inherit; text-transform: inherit; height: 28px; padding: 0 12px; border: 0; border-radius: 999px; background: none; color: rgba(243, 242, 238, .78); cursor: pointer; transition: background-color .15s cubic-bezier(0, 0, 0.2, 1), color .15s cubic-bezier(0, 0, 0.2, 1); }
.vpult button:hover { color: #fff; }
.vpult button[aria-pressed='true'] { background: #f3f2ee; color: #121214; }
.vpult button:focus-visible { outline: 2px solid #ff8a6b; outline-offset: 2px; }
@media (hover: none) { .vpult button { height: 36px; padding: 0 14px; } }
@media (max-width: 640px) { .vpult { left: 8px; right: 8px; bottom: 8px; justify-content: center; flex-wrap: wrap; border-radius: 18px; } }

/* ---------- Variante „Ruhige Tabelle“: Welt Rezept kanzlei-beratung-souveraen ---------- */
html[data-v='tabelle'], html[data-v='tabelle'] body { background: #f9f8f8; color: #212529; }
.welt-k { --k-marke: #1e1e50; --k-text: #212529; --k-meta: #7e6e61; --k-linie: #d2cdc7; --k-flaeche: #efebeb; --k-fokus: #00bee6; --k-kurve: cubic-bezier(0.4, 0, 0.22, 1);
  font-family: 'Schibsted Grotesk', system-ui, sans-serif; color: var(--k-text); }
.k-kopf { display: flex; justify-content: space-between; align-items: center; gap: 20px; padding: 22px var(--rand); border-bottom: 1px solid var(--k-linie); }
.k-marke { display: grid; gap: 4px; color: var(--k-marke); }
.k-marke b { font: 400 25px/1 'Newsreader', Georgia, serif; letter-spacing: -.01em; }
.k-marke b i { font-style: italic; }
.k-marke span { font-size: .8125rem; color: var(--k-meta); }
.k-kopf nav { display: flex; gap: 28px; font-weight: 500; font-size: .9375rem; }
.k-kopf nav a { background: linear-gradient(currentColor, currentColor) 0 100% / 0 1px no-repeat; padding-bottom: 3px; transition: background-size 125ms var(--k-kurve); }
.k-kopf nav a:hover { background-size: 100% 1px; }
.k-zahlen { display: grid; grid-template-columns: minmax(260px, 4fr) 8fr; gap: 32px clamp(32px, 6vw, 112px); padding: clamp(40px, 7vw, 104px) var(--rand) clamp(48px, 7vw, 96px); max-width: 1440px; }
.k-intro { align-self: start; position: sticky; top: 32px; display: grid; gap: 18px; }
.k-label { font-size: .8125rem; font-weight: 500; color: var(--k-meta); letter-spacing: .02em; }
.k-intro h2 { font: 400 clamp(1.625rem, 1.2rem + 1.6vw, 2.5rem)/1.12 'Newsreader', Georgia, serif; letter-spacing: -.01em; color: var(--k-marke); text-wrap: balance; }
.k-intro h2 em { font-style: italic; }
.k-intro p.text { font-size: 1.0625rem; line-height: 1.6; max-width: 40ch; }
.k-intro a { justify-self: start; font-weight: 500; color: var(--k-marke); border-bottom: 1px solid currentColor; padding-bottom: 2px; transition: color 125ms var(--k-kurve); }
.k-intro a:hover { color: #3a3a86; }
.k-intro a:focus-visible, .k-kopf a:focus-visible { outline: 2px solid var(--k-fokus); outline-offset: 3px; }
.k-tabelle { width: 100%; border-collapse: collapse; }
.k-tabelle tbody tr { position: relative; }
/* Haarlinie je Zeile: zeichnet sich einmal von links (scaleX), sonst keine Bewegung */
.k-tabelle tbody th::before { content: ''; position: absolute; left: 0; right: 0; top: 0; height: 1px; background: var(--k-linie); transform-origin: 0 50%; transition: transform 500ms var(--k-kurve); transition-delay: calc(var(--r) * 80ms); }
.k-tabelle tbody tr:last-child th::after { content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 1px; background: var(--k-linie); }
.k-tabelle th, .k-tabelle td { text-align: left; vertical-align: baseline; padding: 22px 0 24px; }
.k-tabelle th { font-weight: 500; font-size: .9375rem; width: 27%; padding-right: 24px; }
.k-tabelle .k-wert { font: 400 clamp(44px, 4.4vw, 72px)/1 'Newsreader', Georgia, serif; letter-spacing: -.015em; color: var(--k-marke); width: 27%; padding-right: 24px; white-space: nowrap; font-variant-numeric: lining-nums tabular-nums; }
.k-wert small { font: 400 .34em/1 'Schibsted Grotesk', sans-serif; letter-spacing: 0; margin-left: .3em; color: var(--k-text); vertical-align: .2em; }
.k-tabelle td:last-child { font-size: .9375rem; line-height: 1.55; color: #4a4f55; max-width: 38ch; }
.k-tabelle td { transition: opacity 250ms var(--k-kurve); transition-delay: calc(var(--r) * 80ms + 160ms); }
.js .welt-k:not(.da) .k-tabelle tbody th::before { transform: scaleX(0); }
.js .welt-k:not(.da) .k-tabelle td { opacity: 0; }
.k-quelle { grid-column: 2; font-size: .8125rem; color: var(--k-meta); margin-top: -12px; }
.k-werte { grid-column: 1 / -1; }
.reduziert .k-tabelle tbody th::before, .reduziert .k-tabelle td { transition: none !important; }
.reduziert .welt-k .k-tabelle tbody th::before { transform: none; }
.reduziert .welt-k .k-tabelle td { opacity: 1; }

/* ---------- Variante „Mit Mini-Diagramm“: Welt Rezept verein-nonprofit-wirkung ---------- */
html[data-v='diagramm'], html[data-v='diagramm'] body { background: #fff; color: #222520; }
.welt-v { --v-marke: #002e6d; --v-flaeche: #edf5fa; --v-grau: #dfdfd7; --v-text: #222520; --v-gelb: #ffca0a; --v-kurve: cubic-bezier(0.645, 0.045, 0.355, 1); --v-dauer: 1400ms;
  font-family: 'Archivo', system-ui, sans-serif; color: var(--v-text); }
.v-kopf { display: flex; justify-content: space-between; align-items: center; gap: 20px; padding: 16px var(--rand); }
.v-marke { display: flex; align-items: center; gap: 10px; font: 800 26px/1 'Big Shoulders', sans-serif; letter-spacing: .01em; text-transform: uppercase; color: var(--v-marke); }
.v-marke svg { width: 30px; height: 22px; }
.v-kopf nav { display: flex; gap: 26px; font-weight: 500; font-size: .9375rem; }
.v-kopf nav a:hover { color: var(--v-marke); text-decoration: underline; text-underline-offset: 4px; }
.v-spenden { display: inline-flex; align-items: center; height: 42px; padding: 0 20px; border-radius: 999px; background: var(--v-gelb); color: var(--v-text); font-weight: 600; transition: background-color 150ms var(--v-kurve); }
.v-spenden:hover { background: #ffd84a; }
.v-wirkung { padding: clamp(28px, 4vw, 56px) var(--rand) clamp(48px, 6vw, 88px); }
.v-titelzeile { display: flex; justify-content: space-between; align-items: end; gap: 16px 40px; flex-wrap: wrap; margin-bottom: clamp(20px, 2.6vw, 36px); }
.v-label { font: 600 .8125rem/1.2 'Archivo', sans-serif; letter-spacing: .08em; text-transform: uppercase; color: var(--v-marke); }
.v-titelzeile h2 { font: 800 clamp(2.75rem, 1.6rem + 5vw, 6rem)/.95 'Big Shoulders', sans-serif; text-transform: uppercase; letter-spacing: .005em; color: var(--v-marke); margin-top: 10px; max-width: 14ch; }
.v-titelzeile p { font-size: 1.0625rem; line-height: 1.6; max-width: 40ch; }
.v-raster { display: grid; grid-template-columns: 1.25fr 1fr 1fr; grid-template-rows: auto auto; gap: 10px; }
.v-kachel { position: relative; display: grid; grid-template-rows: auto auto 1fr auto; gap: 10px; padding: clamp(18px, 2vw, 28px); border-radius: 4px; background: var(--v-flaeche); min-width: 0; color: var(--v-marke); }
.v-kachel.v-gross { grid-row: span 2; background: var(--v-marke); color: #fff; }
.v-kachel.v-breit { grid-column: span 2; }
.v-kachel h3 { font: 600 .8125rem/1.3 'Archivo', sans-serif; letter-spacing: .08em; text-transform: uppercase; display: flex; justify-content: space-between; gap: 12px; }
.v-kachel h3 span { font-weight: 500; opacity: .7; }
.v-zahl { font: 800 clamp(3rem, 2rem + 4vw, 5.5rem)/.9 'Big Shoulders', sans-serif; font-variant-numeric: tabular-nums lining-nums; display: flex; align-items: baseline; gap: .08em; white-space: nowrap; }
.v-gross .v-zahl { font-size: clamp(6rem, 3rem + 9vw, 11rem); }
.v-einheit { font-size: .42em; font-weight: 700; }
.v-text { font-size: .9375rem; line-height: 1.5; max-width: 34ch; color: var(--v-text); }
.v-gross .v-text { color: rgba(255, 255, 255, .86); }
.v-quelle { font-size: .75rem; color: rgba(34, 37, 32, .68); font-variant-numeric: tabular-nums; }
.v-gross .v-quelle { color: rgba(255, 255, 255, .6); }
.v-diagramm { align-self: end; width: 100%; height: auto; display: block; overflow: visible; }
.v-diagramm text { font: 500 11px 'Archivo', sans-serif; fill: currentColor; font-variant-numeric: tabular-nums; }
/* Diagramme bauen sich einmal auf, sobald die Kachel im Bild ist (gleiche Kurve und Dauer wie das Zählen) */
.v-balken rect { transform-box: fill-box; transform-origin: 50% 100%; transition: transform var(--v-dauer) var(--v-kurve); transition-delay: calc(var(--i) * 60ms); }
.v-linie { stroke-dasharray: 1; transition: stroke-dashoffset var(--v-dauer) var(--v-kurve); }
.v-flaeche-pfad { transition: opacity var(--v-dauer) var(--v-kurve); }
.v-punkt { transition: opacity 300ms var(--v-kurve) calc(var(--v-dauer) - 200ms); }
.v-stapel rect { transform-box: fill-box; transform-origin: 0 50%; transition: transform var(--v-dauer) var(--v-kurve); transition-delay: calc(var(--i) * 120ms); }
.v-kreise circle.voll { transition: fill-opacity 200ms var(--v-kurve); transition-delay: calc(var(--i) * 24ms); }
.js .welt-v .v-kachel:not(.da) .v-balken rect { transform: scaleY(0); }
.js .welt-v .v-kachel:not(.da) .v-linie { stroke-dashoffset: 1; }
.js .welt-v .v-kachel:not(.da) :is(.v-flaeche-pfad, .v-punkt) { opacity: 0; }
.js .welt-v .v-kachel:not(.da) .v-stapel rect { transform: scaleX(0); }
.js .welt-v .v-kachel:not(.da) .v-kreise circle.voll { fill-opacity: 0; }
.v-legende { display: flex; flex-wrap: wrap; gap: 4px 14px; font-size: .75rem; }
.v-legende i { display: inline-block; width: 9px; height: 9px; border-radius: 2px; margin-right: 6px; vertical-align: -1px; }
.reduziert .welt-v :is(.v-balken rect, .v-linie, .v-flaeche-pfad, .v-punkt, .v-stapel rect, .v-kreise circle) { transition: none !important; transform: none !important; stroke-dashoffset: 0 !important; opacity: 1 !important; }
.reduziert .welt-v .v-kreise circle.voll { fill-opacity: 1 !important; }
.welt-v a:focus-visible { outline: 2px solid var(--v-marke); outline-offset: 3px; }

.vorschau .k-kopf, .vorschau .v-kopf, .vorschau .vpult { display: none; }
.vorschau .k-zahlen { padding-top: 36px; padding-bottom: 20px; min-height: 100svh; align-content: center; }
.vorschau .k-tabelle th, .vorschau .k-tabelle td { padding: 16px 0 18px; }
.vorschau .v-wirkung { padding-top: 22px; padding-bottom: 18px; }
.vorschau .v-titelzeile { margin-bottom: 14px; }
.vorschau .v-titelzeile h2 { font-size: 52px; max-width: none; }
.vorschau .v-gross .v-zahl { font-size: 128px; }
.vorschau .v-zahl { font-size: 72px; }
.vorschau .k-intro p.text { font-size: 1rem; }
.vorschau .v-titelzeile p { display: none; }
.vorschau .v-text { display: none; }

@media (max-width: 1000px) { .k-kopf nav, .v-kopf nav { display: none; } }
@media (max-width: 840px) {
  .k-zahlen { grid-template-columns: 1fr; }
  .k-intro { position: static; }
  .k-quelle { grid-column: 1; }
  .v-raster { grid-template-columns: 1fr 1fr; }
  .v-kachel.v-gross { grid-row: auto; grid-column: 1 / -1; }
  .v-kachel.v-breit { grid-column: 1 / -1; }
}
@media (max-width: 640px) {
  .k-tabelle, .k-tabelle tbody, .k-tabelle tr { display: block; }
  .k-tabelle tbody tr { display: grid; grid-template-columns: 1fr auto; column-gap: 16px; padding: 18px 0 20px; }
  .k-tabelle th, .k-tabelle td { display: block; padding: 0; width: auto !important; }
  .k-tabelle th { align-self: baseline; }
  .k-tabelle .k-wert { grid-column: 2; grid-row: 1; padding: 0; text-align: right; font-size: 44px; }
  .k-tabelle td:last-child { grid-column: 1 / -1; margin-top: 10px; }
  .v-raster { grid-template-columns: 1fr; }
  .v-gross .v-zahl { font-size: 30vw; }
  .v-zahl { font-size: 22vw; }
  .v-kopf .v-spenden { height: 38px; padding: 0 16px; }
}
</style>
</head>
<body>
<div class="welt" data-welt="typo">
<p class="leiste">Kostenloser Versand ab 80 € · Rückgabe 30 Tage</p>
<header class="kopf">
  <a class="wortmarke" href="#" aria-label="Klüver, Startseite">
    <svg viewBox="0 0 22 26" aria-hidden="true"><path d="M3 24 L12 2 L12 24 Z" fill="#212121"/><path d="M14 8 L20 24 L14 24 Z" fill="#d8432b"/></svg>Klüver
  </a>
  <nav aria-label="Hauptnavigation"><a href="#">Taschen</a><a href="#">Seesäcke</a><a href="#">Kleinkram</a><a href="#">Werkstatt</a></nav>
  <div class="kopf__rechts"><a href="#" class="konto">Konto</a><a href="#" class="korb">Korb<b>0</b></a></div>
</header>

<section class="einstieg" aria-labelledby="claim">
  <div>
    <p class="label">Genäht am Hafen, Flensburg</p>
    <h1 id="claim">Jede Tasche war vorher ein Segel.</h1>
    <p class="text">Wir holen ausgemusterte Segel aus Vereinen und Werften rund um die Förde und nähen daraus Taschen. Jede trägt die Nummer ihres Segels.</p>
  </div>
  <article class="produkt" aria-label="Hafentasche No. 1284">
    <div class="produkt__bild">
      <span class="neu label">Neu</span>
      <svg viewBox="0 0 400 420" role="img" aria-label="Hafentasche aus weißem Segeltuch mit roter Segelnummer">
        <path d="M128 150 C128 62 168 34 200 34 C232 34 272 62 272 150" fill="none" stroke="#03143b" stroke-width="13" stroke-linecap="round"/>
        <path d="M150 150 C150 84 176 60 200 60 C224 60 250 84 250 150" fill="none" stroke="#03143b" stroke-opacity=".35" stroke-width="5"/>
        <path d="M70 140 L330 140 L352 400 L48 400 Z" fill="#faf8f3" stroke="#cbc4b6" stroke-width="2"/>
        <path d="M70 140 L330 140 L333 176 L67 176 Z" fill="#03143b"/>
        <path d="M58 270 L342 228" stroke="#d6cfc1" stroke-width="2"/>
        <path d="M54 330 L346 300" stroke="#d6cfc1" stroke-width="2"/>
        <path d="M58 274 L342 232" stroke="#e5dfd3" stroke-width="1" stroke-dasharray="4 5"/>
        <g transform="rotate(-8 200 300)"><text x="200" y="318" text-anchor="middle" font-family="Instrument Sans, sans-serif" font-weight="700" font-size="58" fill="#d8432b" letter-spacing="2">GER 1284</text></g>
        <path d="M290 360 L318 316 L318 360 Z" fill="#212121" opacity=".85"/>
        <circle cx="96" cy="160" r="5" fill="#f5f4f1"/><circle cx="304" cy="160" r="5" fill="#f5f4f1"/>
      </svg>
    </div>
    <div class="produkt__info">
      <div class="produkt__zeile"><span>Hafentasche No. 1284</span><span>129,00 €</span></div>
      <div class="produkt__unter"><span class="swatches" aria-label="3 Farben"><i style="background:#faf8f3"></i><i style="background:#03143b"></i><i style="background:#d8432b"></i></span><span class="lager">Auf Lager</span></div>
      <a class="knopf" href="#">In den Korb</a>
    </div>
  </article>
</section>

<section class="zahlen" aria-labelledby="zahlen-titel" data-buehne="typo">
  <div class="zahlen__kopf">
    <div>
      <p class="label">Warum Klüver</p>
      <h2 id="zahlen-titel">Zahlen aus dem Werkstattbuch</h2>
    </div>
    <div class="modi mono" role="group" aria-label="Bewegung">
      <button type="button" data-variante="walze" aria-pressed="true">Walze</button>
      <button type="button" data-variante="zaehlen" aria-pressed="false">Zählen</button>
      <button type="button" data-variante="kippen" aria-pressed="false">Kippen</button>
    </div>
  </div>

  <div class="raster">
    <article class="kachel gross" data-zahl data-wert="1284" data-anzeige="1.284">
      <svg class="segel" viewBox="0 0 300 420" aria-hidden="true">
        <g fill="none" stroke="#f5f4f1" stroke-opacity=".14" stroke-width="1.5">
          <path d="M150 6 L282 410 L30 410 Z"/>
          <path d="M150 6 L150 410"/>
          <path d="M138 52 L162 50 M123 100 L177 96 M108 150 L193 144 M92 200 L209 192 M76 250 L225 240 M60 300 L242 288 M45 350 L258 336"/>
        </g>
      </svg>
      <p class="kachel__kopf"><span class="label">Segel gerettet</span><span class="nr">01 / 04</span></p>
      <p class="zahl"><span class="sr">1.284 Segel</span><span class="ziffern" aria-hidden="true"></span></p>
      <div class="kachel__text">
        <p>Ausgemusterte Regatta- und Fahrtensegel aus 41 Vereinen und Werften zwischen Flensburg und Kappeln. Aus jedem Segel entstehen 6 bis 14 Taschen.</p>
        <p class="quelle">Werkstattbuch, 06/2019 bis 10/2026</p>
      </div>
    </article>
    <article class="kachel" data-zahl data-wert="48" data-anzeige="4,8" data-einheit="/5">
      <p class="kachel__kopf"><span class="label">Bewertung</span><span class="nr">02</span></p>
      <p class="zahl"><span class="sr">4,8 von 5 Sternen</span><span class="ziffern" aria-hidden="true"></span></p>
      <div class="kachel__text"><p>Durchschnitt aus 2.961 Bewertungen nach verifiziertem Kauf.</p><p class="quelle">Shop-Bewertungen, Stand 31.10.2026</p></div>
    </article>
    <article class="kachel" data-zahl data-wert="30" data-anzeige="30" data-einheit="Tage">
      <p class="kachel__kopf"><span class="label">Rückgabe</span><span class="nr">03</span></p>
      <p class="zahl"><span class="sr">30 Tage</span><span class="ziffern" aria-hidden="true"></span></p>
      <div class="kachel__text"><p>Kostenlos. Das Rücksendeetikett liegt schon im Paket.</p><p class="quelle">Ab Zustellung, ohne Begründung</p></div>
    </article>
    <article class="kachel" data-zahl data-wert="10" data-anzeige="10" data-einheit="Jahre">
      <p class="kachel__kopf"><span class="label">Garantie auf Nähte</span><span class="nr">04</span></p>
      <p class="zahl"><span class="sr">10 Jahre</span><span class="ziffern" aria-hidden="true"></span></p>
      <div class="kachel__text"><p>Reißt eine Naht, nähen wir sie in der Werkstatt am Hafen neu. Porto zahlen wir.</p><p class="quelle">Ab Kaufdatum, Beleg genügt</p></div>
    </article>
  </div>

  <div class="werte mono" aria-live="off" data-werte></div>
</section>
</div>

<!-- Variante „Ruhige Tabelle“: Kanzlei, Zahlen stehen einfach da -->
<div class="welt welt-k" data-welt="tabelle" hidden>
  <header class="k-kopf">
    <a class="k-marke" href="#" aria-label="Hollmann &amp; Ritter, Startseite"><b>Hollmann <i>&amp;</i> Ritter</b><span>Rechtsanwälte · Steuerberater · Notare</span></a>
    <nav aria-label="Hauptnavigation"><a href="#">Kompetenzen</a><a href="#">Köpfe</a><a href="#">Karriere</a><a href="#">Kontakt</a></nav>
  </header>
  <section class="k-zahlen" aria-labelledby="k-titel" data-buehne="tabelle">
    <div class="k-intro">
      <p class="k-label">Die Kanzlei</p>
      <h2 id="k-titel">Seit 1987 in Niedersachsen. <em>Vier Standorte, ein Team.</em></h2>
      <p class="text">Wir beraten Familienunternehmen, Mittelstand und öffentliche Auftraggeber im Wirtschafts-, Arbeits- und Steuerrecht. Jedes Mandat führt eine Partnerin oder ein Partner persönlich.</p>
      <a href="#">Unsere Köpfe kennenlernen</a>
    </div>
    <table class="k-tabelle">
      <caption class="sr">Hollmann &amp; Ritter in Zahlen, Stand 1. Oktober 2026</caption>
      <thead class="sr"><tr><th scope="col">Kennzahl</th><th scope="col">Wert</th><th scope="col">Erläuterung</th></tr></thead>
      <tbody>
        <tr style="--r:0"><th scope="row">Gegründet</th><td class="k-wert">1987</td><td>In Hannover von Dr. Ingrid Hollmann und Peter Ritter, als Kanzlei für den Mittelstand.</td></tr>
        <tr style="--r:1"><th scope="row">Standorte</th><td class="k-wert">4</td><td>Hannover, Braunschweig, Göttingen und Celle. Jeder Standort mit eigenem Sekretariat.</td></tr>
        <tr style="--r:2"><th scope="row">Berufsträger</th><td class="k-wert">63</td><td>Davon 21 Partnerinnen und Partner, 9 Steuerberater und 4 Notare.</td></tr>
        <tr style="--r:3"><th scope="row">Fachanwaltstitel</th><td class="k-wert">38</td><td>In 11 Rechtsgebieten, die meisten im Arbeits-, Steuer- und Handelsrecht.</td></tr>
        <tr style="--r:4"><th scope="row">Neue Mandate 2025</th><td class="k-wert">1.420</td><td>Ohne Erstgespräche und Beratungen unter einer Stunde.</td></tr>
      </tbody>
    </table>
    <p class="k-quelle">Stand 1. Oktober 2026 · eigene Angaben</p>
    <div class="werte mono k-werte" data-werte-k></div>
  </section>
</div>

<!-- Variante „Mit Mini-Diagramm“: Verein, Wirkung in Zahlen -->
<div class="welt welt-v" data-welt="diagramm" hidden>
  <header class="v-kopf">
    <a class="v-marke" href="#" aria-label="Uferpaten e. V., Startseite"><svg viewBox="0 0 30 22" aria-hidden="true"><path d="M1 7q3.5-4 7 0t7 0 7 0 7 0M1 14q3.5-4 7 0t7 0 7 0 7 0" fill="none" stroke="currentColor" stroke-width="2.6" stroke-linecap="round"/><path d="M1 21h28" stroke="#ffca0a" stroke-width="2.6" stroke-linecap="round"/></svg>Uferpaten</a>
    <nav aria-label="Hauptnavigation"><a href="#">Mitmachen</a><a href="#">Einsätze</a><a href="#">Wirkung</a><a href="#">Verein</a></nav>
    <a class="v-spenden" href="#">Spenden</a>
  </header>
  <section class="v-wirkung" aria-labelledby="v-titel" data-buehne="diagramm">
    <div class="v-titelzeile">
      <div><p class="v-label">Wirkung 2020 bis 2026</p><h2 id="v-titel">Was eure Spenden aus dem Fluss holen</h2></div>
      <p>Seit 2020 räumen Freiwillige an Elbe, Mulde und Saale auf. Jede Zahl hier stammt aus unseren Einsatzprotokollen und dem Jahresbericht.</p>
    </div>
    <div class="v-raster">
      <article class="v-kachel v-gross" data-vz data-wert="18.4" data-dez="1">
        <h3>Müll aus dem Wasser <span>2020–2026</span></h3>
        <p class="v-zahl"><span class="sr">18,4 Tonnen</span><span aria-hidden="true" data-lauf>18,4</span><span class="v-einheit" aria-hidden="true">t</span></p>
        <p class="v-text">Gewogen am Sammelpunkt, vor der Trennung. Jedes Jahr mehr, weil mehr Ufer betreut werden.</p>
        <svg class="v-diagramm v-balken" viewBox="0 0 320 168" role="img" aria-label="Müll je Jahr in Tonnen: 2020 0,9; 2021 1,6; 2022 2,2; 2023 2,8; 2024 3,1; 2025 3,6; 2026 4,2">
          <g fill="currentColor">
            <rect x="4" y="114" width="32" height="26" fill-opacity=".34" style="--i:0"/>
            <rect x="49" y="94.3" width="32" height="45.7" fill-opacity=".34" style="--i:1"/>
            <rect x="94" y="77.1" width="32" height="62.9" fill-opacity=".34" style="--i:2"/>
            <rect x="139" y="60" width="32" height="80" fill-opacity=".34" style="--i:3"/>
            <rect x="184" y="51.4" width="32" height="88.6" fill-opacity=".34" style="--i:4"/>
            <rect x="229" y="37.1" width="32" height="102.9" fill-opacity=".34" style="--i:5"/>
            <rect x="274" y="20" width="32" height="120" style="--i:6"/>
          </g>
          <text x="290" y="12" text-anchor="middle">4,2 t</text>
          <g text-anchor="middle" fill-opacity=".7"><text x="20" y="160">20</text><text x="65" y="160">21</text><text x="110" y="160">22</text><text x="155" y="160">23</text><text x="200" y="160">24</text><text x="245" y="160">25</text><text x="290" y="160">26</text></g>
        </svg>
        <p class="v-quelle">Einsatzprotokolle, Stand 30.09.2026</p>
      </article>
      <article class="v-kachel" data-vz data-wert="2140" data-dez="0">
        <h3>Freiwillige <span>gesamt</span></h3>
        <p class="v-zahl"><span class="sr">2.140 Freiwillige</span><span aria-hidden="true" data-lauf>2.140</span></p>
        <svg class="v-diagramm" viewBox="0 0 300 96" role="img" aria-label="Freiwillige insgesamt: 2020 120; 2021 410; 2022 760; 2023 1.120; 2024 1.490; 2025 1.830; 2026 2.140">
          <path class="v-flaeche-pfad" d="M10 86 L10 80.1 L56.7 70.6 L103.3 59.1 L150 47.3 L196.7 35.2 L243.3 24.1 L290 13.9 L290 86 Z" fill="currentColor" fill-opacity=".08"/>
          <path class="v-linie" pathLength="1" d="M10 80.1 L56.7 70.6 L103.3 59.1 L150 47.3 L196.7 35.2 L243.3 24.1 L290 13.9" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linejoin="round" stroke-linecap="round"/>
          <circle class="v-punkt" cx="290" cy="13.9" r="4.5" fill="currentColor"/>
          <g fill-opacity=".7"><text x="10" y="96">2020</text><text x="290" y="96" text-anchor="end">2026</text></g>
        </svg>
        <p class="v-quelle">Mindestens ein Einsatz, ohne Schulklassen</p>
      </article>
      <article class="v-kachel" data-vz data-wert="86" data-dez="0">
        <h3>Direkt in Einsätze <span>2025</span></h3>
        <p class="v-zahl"><span class="sr">86 Prozent</span><span aria-hidden="true" data-lauf>86</span><span class="v-einheit" aria-hidden="true">%</span></p>
        <svg class="v-diagramm v-stapel" viewBox="0 0 300 18" role="img" aria-label="Verwendung der Spenden 2025: 86 Prozent Einsätze, 9 Prozent Verwaltung, 5 Prozent Spendenwerbung">
          <rect x="0" y="0" width="256" height="18" rx="2" fill="currentColor" style="--i:0"/>
          <rect x="259" y="0" width="24" height="18" rx="2" fill="currentColor" fill-opacity=".45" style="--i:1"/>
          <rect x="286" y="0" width="14" height="18" rx="2" fill="currentColor" fill-opacity=".2" style="--i:2"/>
        </svg>
        <p class="v-legende"><span><i style="background:#002e6d"></i>Einsätze 86 %</span><span><i style="background:rgba(0,46,109,.45)"></i>Verwaltung 9 %</span><span><i style="background:rgba(0,46,109,.2)"></i>Werbung 5 %</span></p>
        <p class="v-quelle">Jahresbericht 2025, geprüft</p>
      </article>
      <article class="v-kachel v-breit" data-vz data-wert="37" data-dez="0">
        <h3>Ufer mit fester Patenschaft <span>Ziel 2028: 50 km</span></h3>
        <p class="v-zahl"><span class="sr">37 Kilometer</span><span aria-hidden="true" data-lauf>37</span><span class="v-einheit" aria-hidden="true">km</span></p>
        <svg class="v-diagramm v-kreise" viewBox="0 0 694 30" role="img" aria-label="37 von 50 geplanten Uferkilometern haben eine feste Patenschaft" data-kreise></svg>
        <p class="v-quelle">Ein Punkt je Kilometer · Stand 30.09.2026</p>
      </article>
    </div>
    <div class="werte mono" data-werte-v></div>
  </section>
</div>

<div class="vpult" role="group" aria-label="Variante">
  <span>Variante</span>
  <button type="button" data-v-wahl="typo" aria-pressed="true">Typo-Ereignis</button>
  <button type="button" data-v-wahl="tabelle" aria-pressed="false">Ruhige Tabelle</button>
  <button type="button" data-v-wahl="diagramm" aria-pressed="false">Mini-Diagramm</button>
</div>

<script>
var STEUER = {};   // je Variante ein Steuerobjekt { start, stopp, neustart, vorfuehren, pause, weiter }
/* ---------- Grundvariante „Typo-Ereignis“ (d2c, Klüver) ---------- */
(() => {
  const html = document.documentElement;
  const reduziert = () => html.classList.contains('reduziert');
  const vorschau = () => html.classList.contains('vorschau');
  const welt = document.querySelector('[data-welt="typo"]');
  const aktiv = () => !welt.hidden;

  /* Werte (siehe bau.werte in meta.json) */
  const W = {
    stagger: 0.6,        // je Kachel, Anteil der Kacheldauer (Trionn: stagger each .6)
    glaettung: 0.6,      // s bis ~95 % aufgeholt (scrub .6)
    ziffernStagger: 0.08,// Walze: Stellen gestaffelt von rechts
    extraUmlauf: 10,     // Walze: Zielziffer + eine volle Umdrehung
    winkel: -92,         // Kippen: rotateX -92° → 0
    bereich: [1.0, 0.4], // Scroll: Kacheloberkante von 100 % bis 40 % der Viewport-Höhe
    dauerVorf: 1100      // ms je Kachel in der Vorführung
  };
  const ANZEIGE = {
    walze:   [['Scroll', 'scrub, linear'], ['Glättung', '0,6 s'], ['Stagger', '0,6 je Kachel'], ['Stellen', 'von rechts, 0,08'], ['Weg', 'Ziffer + 10']],
    zaehlen: [['Scroll', 'scrub, linear'], ['Glättung', '0,6 s'], ['Stagger', '0,6 je Kachel'], ['Ziffern', 'lnum tnum'], ['Führende Nullen', '18 %']],
    kippen:  [['Scroll', 'scrub, linear'], ['Glättung', '0,6 s'], ['Stagger', '0,6 je Kachel'], ['rotateX', '−92° → 0'], ['Perspektive', '900 px']]
  };

  const kacheln = [...welt.querySelectorAll('[data-zahl]')].map(el => {
    const ziel = el.querySelector('.ziffern');
    const text = el.dataset.anzeige;
    const stellen = [], zeichen = [];
    for (const ch of text) {
      if (/\d/.test(ch)) {
        const s = document.createElement('span'); s.className = 'stelle';
        const st = document.createElement('span'); st.className = 'streifen';
        st.innerHTML = Array.from({ length: 20 }, (_, i) => `<span>${i % 10}</span>`).join('');
        s.appendChild(st); ziel.appendChild(s);
        stellen.push({ el: s, streifen: st, ziel: +ch });
      } else {
        const z = document.createElement('span'); z.className = 'zeichen'; z.textContent = ch;
        ziel.appendChild(z); zeichen.push({ el: z, vorStelle: stellen.length });
      }
    }
    if (el.dataset.einheit) {
      const e = document.createElement('span'); e.className = 'einheit'; e.textContent = el.dataset.einheit; e.setAttribute('aria-hidden', 'true');
      ziel.appendChild(e);                                       // kippt mit der Zahl
    }
    const komma = text.indexOf(','), ganz = komma < 0 ? stellen.length : text.slice(0, komma).replace(/\D/g, '').length;
    return { el, ziffern: ziel, stellen, zeichen, ganz, wert: +el.dataset.wert, ist: 0, soll: 0, reihe: 0, inReihe: 1 };
  });

  let variante = 'walze';
  const clamp = v => Math.min(1, Math.max(0, v));

  function zeichnen(k) {
    const l = k.ist, n = k.stellen.length;
    k.el.style.setProperty('--l', l.toFixed(4));
    if (variante === 'walze') {
      const S = W.ziffernStagger, rest = 1 - (n - 1) * S;
      k.stellen.forEach((s, i) => {
        const j = n - 1 - i;                                  // von rechts
        const lj = clamp((l - j * S) / rest);
        setze(s, lj * (s.ziel + W.extraUmlauf));
        s.el.classList.remove('leer');
      });
      k.zeichen.forEach(z => z.el.classList.remove('leer'));
      k.ziffern.style.transform = '';
    } else if (variante === 'zaehlen') {
      const v = Math.round(k.wert * l);
      const t = String(v).padStart(n, '0');
      const fuehrend = n - String(v).length;                   // Anzahl führender Nullen
      k.stellen.forEach((s, i) => { setze(s, +t[i]); s.el.classList.toggle('leer', i < fuehrend && i < k.ganz - 1); });
      k.zeichen.forEach(z => z.el.classList.toggle('leer', fuehrend > 0 && z.vorStelle <= fuehrend && z.vorStelle < k.ganz));
      k.ziffern.style.transform = '';
    } else {
      k.stellen.forEach(s => { setze(s, s.ziel); s.el.classList.remove('leer'); });
      k.zeichen.forEach(z => z.el.classList.remove('leer'));
      k.ziffern.style.transform = `rotateX(${((1 - l) * W.winkel).toFixed(2)}deg)`;
    }
  }
  function setze(s, pos) { s.streifen.style.transform = `translate3d(0, ${(-pos * 5).toFixed(3)}%, 0)`; } // 20 Zellen → 5 % je Zelle

  /* Reihen erkennen: Kacheln mit gleicher Oberkante teilen sich den Stagger */
  function reihen() {
    const tops = kacheln.map(k => Math.round(k.el.offsetTop));
    kacheln.forEach((k, i) => {
      const gleiche = kacheln.filter((_, j) => Math.abs(tops[j] - tops[i]) < 4);
      k.inReihe = gleiche.length; k.reihe = gleiche.indexOf(k);
    });
  }

  /* Scroll → Soll-Fortschritt je Kachel (scrub, linear) */
  function sollAusScroll() {
    const vh = innerHeight, y = scrollY;
    const maxY = Math.max(0, document.documentElement.scrollHeight - vh);
    kacheln.forEach(k => {
      const top = k.el.getBoundingClientRect().top;
      /* Bereich an die Seite anpassen: Kacheln, die schon beim Laden sichtbar sind, starten bei 0,
         und das Ende muss mit dem letzten möglichen Scroll erreichbar sein */
      const anfang = Math.min(vh * W.bereich[0], top + y);
      const ende = Math.min(anfang - 1, Math.max(vh * W.bereich[1], top + y - maxY));
      const roh = clamp((anfang - top) / (anfang - ende));
      const gesamt = 1 + W.stagger * (k.inReihe - 1);
      k.soll = clamp(roh * gesamt - W.stagger * k.reihe);
    });
  }

  /* Vorführung: Zeit statt Scroll, Kacheln nacheinander, Varianten im Wechsel */
  let modus = 'scroll', t0 = 0, einmal = false, uhr = null, zuletzt = 0, pausiert = false, pausiertAb = 0;
  const REIHE = ['walze', 'zaehlen', 'kippen'];
  const LAUF = () => W.dauerVorf * (1 + W.stagger * (kacheln.length - 1));
  const HALTEN = 2200;
  function sollAusZeit(jetzt) {
    const t = (jetzt - t0) / W.dauerVorf;
    kacheln.forEach((k, i) => { k.soll = clamp(t - W.stagger * i); });
    if (jetzt - t0 > LAUF() + (einmal ? 0 : HALTEN)) {
      if (einmal) { modus = 'scroll'; einmal = false; }
      else { setzeVariante(REIHE[(REIHE.indexOf(variante) + 1) % REIHE.length]); nullen(); t0 = jetzt; }
    }
  }
  function nullen() { kacheln.forEach(k => { k.ist = 0; k.soll = 0; zeichnen(k); }); }

  function schritt(jetzt) {
    const dt = Math.min(0.1, (jetzt - (zuletzt || jetzt)) / 1000); zuletzt = jetzt;
    if (!aktiv()) { uhr = null; return; }
    if (reduziert()) { kacheln.forEach(k => { k.ist = 1; zeichnen(k); }); uhr = null; return; }
    if (modus === 'zeit') sollAusZeit(jetzt); else sollAusScroll();
    const a = 1 - Math.exp(-dt / (W.glaettung / 3));               // ~95 % nach 0,6 s
    kacheln.forEach(k => {
      const neu = Math.abs(k.soll - k.ist) < 0.0005 ? k.soll : k.ist + (k.soll - k.ist) * a;
      if (neu !== k.ist || k.erst !== true) { k.ist = neu; k.erst = true; zeichnen(k); }
    });
    uhr = requestAnimationFrame(schritt);
  }
  function starten() { if (!uhr && !pausiert) { zuletzt = 0; uhr = requestAnimationFrame(schritt); } }
  function stoppen() { cancelAnimationFrame(uhr); uhr = null; }

  function setzeVariante(neu) {
    variante = neu;
    welt.querySelectorAll('[data-variante]').forEach(b => b.setAttribute('aria-pressed', String(b.dataset.variante === neu)));
    welt.querySelector('[data-werte]').innerHTML = ANZEIGE[neu].map(([a, b]) => `<span>${a} <b>${b}</b></span>`).join('');
    kacheln.forEach(k => { k.erst = false; zeichnen(k); });
  }

  welt.querySelectorAll('[data-variante]').forEach(b => b.addEventListener('click', () => {
    setzeVariante(b.dataset.variante);
    if (reduziert()) { starten(); return; }
    nullen(); modus = 'zeit'; einmal = true; t0 = performance.now(); starten();   // einmal abspielen, dann wieder Scroll
  }));
  ['wheel', 'touchstart', 'keydown'].forEach(ev => addEventListener(ev, () => { if (modus === 'zeit' && !einmal && !vorschau()) modus = 'scroll'; }, { passive: true }));
  addEventListener('resize', reihen);
  document.fonts && document.fonts.ready.then(reihen);

  setzeVariante('walze');

  function neustart() {
    stoppen(); pausiert = false; modus = 'scroll'; einmal = false;
    scrollTo(0, 0); setzeVariante(variante); nullen(); reihen(); starten();
  }
  function vorfuehren() {
    stoppen(); pausiert = false;
    if (!vorschau()) scrollTo(0, welt.querySelector('[data-buehne]').offsetTop);   // nur im eigenen Fenster scrollen
    reihen(); setzeVariante('walze'); nullen();
    modus = 'zeit'; einmal = false; t0 = performance.now(); starten();
  }
  function pause() { if (!pausiert) pausiertAb = performance.now(); pausiert = true; stoppen(); }
  function weiter() { if (pausiert && modus === 'zeit') t0 += performance.now() - pausiertAb; pausiert = false; starten(); }

  function start() { pausiert = false; modus = 'scroll'; einmal = false; reihen(); setzeVariante(variante); starten(); }
  function stopp() { stoppen(); modus = 'scroll'; einmal = false; }
  STEUER.typo = { start, stopp, neustart, vorfuehren, pause, weiter };
})();

/* ---------- Variante „Ruhige Tabelle“ (Kanzlei): keine Zählung, nur die Haarlinien zeichnen sich einmal ---------- */
(() => {
  const html = document.documentElement;
  const reduziert = () => html.classList.contains('reduziert');
  const welt = document.querySelector('[data-welt="tabelle"]');
  const tabelle = welt.querySelector('.k-tabelle');
  const werte = welt.querySelector('[data-werte-k]');
  const W = { linie_ms: 500, versatz_ms: 80, wert_ms: 250, wert_nach_ms: 160, halten_ms: 3200 };
  let io = null, uhr = null, vorf = false, pausiert = false;
  function zeigeWerte() {
    werte.innerHTML = reduziert()
      ? '<span>Reduziert <b>alles steht sofort</b></span><span>Ziffern <b>Newsreader, lnum tnum</b></span>'
      : '<span>Zählen <b>keines</b></span><span>Haarlinie <b>scaleX 0 → 1 · 500 ms</b></span><span>Kurve <b>cubic-bezier(0.4, 0, 0.22, 1)</b></span><span>Versatz <b>80 ms je Zeile</b></span><span>Wert <b>opacity 250 ms, +160 ms</b></span><span>Ziffern <b>Newsreader 400, lnum tnum</b></span>';
  }
  function zeigen() { welt.classList.add('da'); }
  function verbergen() { welt.classList.remove('da'); void welt.offsetWidth; }
  function beobachten() {
    if (io) io.disconnect();
    io = new IntersectionObserver(e => { if (e.some(x => x.isIntersecting)) { zeigen(); io.disconnect(); } }, { threshold: 0.25 });
    io.observe(tabelle);
  }
  function runde() {
    if (!vorf || pausiert) return;
    verbergen();
    uhr = setTimeout(() => { zeigen(); uhr = setTimeout(runde, (reduziert() ? 0 : 5 * W.versatz_ms + W.linie_ms) + W.halten_ms); }, 400);
  }
  function start() { zeigeWerte(); verbergen(); if (reduziert()) zeigen(); else beobachten(); }
  function stopp() { vorf = false; clearTimeout(uhr); if (io) io.disconnect(); }
  function neustart() { stopp(); scrollTo(0, 0); start(); }
  function vorfuehren() { stopp(); vorf = true; pausiert = false; zeigeWerte(); runde(); }
  function pause() { pausiert = true; clearTimeout(uhr); }
  function weiter() { if (pausiert) { pausiert = false; if (vorf) runde(); } }
  STEUER.tabelle = { start, stopp, neustart, vorfuehren, pause, weiter };
})();

/* ---------- Variante „Mit Mini-Diagramm“ (Verein): einmal zählen, Diagramm baut sich im selben Takt auf ---------- */
(() => {
  const html = document.documentElement;
  const reduziert = () => html.classList.contains('reduziert');
  const welt = document.querySelector('[data-welt="diagramm"]');
  const werte = welt.querySelector('[data-werte-v]');
  const W = { dauer_ms: 1400, kurve: [0.645, 0.045, 0.355, 1], halten_ms: 2600, versatz_ms: 150 };
  // Punkte für die Kilometer: 50 geplant, 37 betreut
  const kreise = welt.querySelector('[data-kreise]');
  kreise.innerHTML = Array.from({ length: 50 }, (_, i) => `<circle cx="${(7 + i * 13.6).toFixed(1)}" cy="15" r="5.4" class="${i < 37 ? 'voll' : ''}" style="--i:${i}" fill="currentColor" ${i < 37 ? '' : 'fill-opacity="0" stroke="currentColor" stroke-opacity=".35" stroke-width="1.2"'}/>`).join('');
  // cubic-bezier wie in CSS, damit Zahl und Diagramm dieselbe Kurve haben
  function bezier([x1, y1, x2, y2]) {
    const f = (t, a, b) => 3 * a * t * (1 - t) ** 2 + 3 * b * t * t * (1 - t) + t ** 3;
    return x => { let lo = 0, hi = 1, t = x; for (let i = 0; i < 24; i++) { t = (lo + hi) / 2; if (f(t, x1, x2) < x) lo = t; else hi = t; } return f(t, y1, y2); };
  }
  const kurve = bezier(W.kurve);
  const zahl = new Intl.NumberFormat('de-DE');
  const kacheln = [...welt.querySelectorAll('[data-vz]')].map((el, i) => ({ el, i, lauf: el.querySelector('[data-lauf]'), wert: +el.dataset.wert, dez: +el.dataset.dez, t0: null }));
  const text = (k, p) => zahl.format(+(k.wert * p).toFixed(k.dez)).replace(/^(\d+)$/, m => k.dez ? m + ',' + '0'.repeat(k.dez) : m);
  let io = null, raf = null, uhr = null, vorf = false, pausiert = false;
  function zeigeWerte() {
    werte.innerHTML = reduziert()
      ? '<span>Reduziert <b>Endwerte und fertige Diagramme sofort</b></span>'
      : '<span>Zählen <b>einmal, 1400 ms</b></span><span>Kurve <b>cubic-bezier(0.645, 0.045, 0.355, 1)</b></span><span>Diagramm <b>gleiche Kurve und Dauer</b></span><span>Balken <b>scaleY, +60 ms je Balken</b></span><span>Linie <b>stroke-dashoffset 1 → 0</b></span><span>Punkte <b>+24 ms</b></span><span>Ziffern <b>Big Shoulders 800, tnum</b></span>';
  }
  function malen(jetzt) {
    let offen = false;
    kacheln.forEach(k => {
      if (k.t0 === null) return;
      const p = Math.min(1, Math.max(0, (jetzt - k.t0) / W.dauer_ms));
      k.lauf.textContent = text(k, kurve(p));
      if (p < 1) offen = true;
    });
    raf = offen ? requestAnimationFrame(malen) : null;
  }
  function los(k, verz = 0) {
    if (reduziert()) { k.el.classList.add('da'); k.lauf.textContent = text(k, 1); return; }
    k.t0 = performance.now() + verz;
    k.el.style.setProperty('transition-delay', '0ms');
    setTimeout(() => k.el.classList.add('da'), verz);
    if (!raf) raf = requestAnimationFrame(malen);
  }
  function nullen() { kacheln.forEach(k => { k.t0 = null; k.el.classList.remove('da'); k.lauf.textContent = text(k, reduziert() ? 1 : 0); }); void welt.offsetWidth; if (reduziert()) kacheln.forEach(k => k.el.classList.add('da')); }
  function beobachten() {
    if (io) io.disconnect();
    // Zählen nur, wenn die Kachel im Bild ist, und nur einmal
    io = new IntersectionObserver(e => e.forEach(x => { if (x.isIntersecting) { const k = kacheln.find(k => k.el === x.target); io.unobserve(x.target); los(k); } }), { threshold: 0.45 });
    kacheln.forEach(k => io.observe(k.el));
  }
  function runde() {
    if (!vorf || pausiert) return;
    nullen();
    if (reduziert()) { uhr = setTimeout(runde, W.halten_ms); return; }
    kacheln.forEach(k => los(k, 300 + k.i * W.versatz_ms));
    uhr = setTimeout(runde, 300 + 3 * W.versatz_ms + W.dauer_ms + W.halten_ms);
  }
  function start() { zeigeWerte(); nullen(); if (!reduziert()) beobachten(); }
  function stopp() { vorf = false; clearTimeout(uhr); if (io) io.disconnect(); cancelAnimationFrame(raf); raf = null; }
  function neustart() { stopp(); scrollTo(0, 0); start(); }
  function vorfuehren() { stopp(); vorf = true; pausiert = false; zeigeWerte(); runde(); }
  function pause() { pausiert = true; clearTimeout(uhr); }
  function weiter() { if (pausiert) { pausiert = false; if (vorf) runde(); } }
  STEUER.diagramm = { start, stopp, neustart, vorfuehren, pause, weiter };
})();

/* ---------- Varianten-Umschalter (Regel 13): ?v=typo | tabelle | diagramm ---------- */
(() => {
  const html = document.documentElement;
  const IDS = ['typo', 'tabelle', 'diagramm'];
  let v = new URLSearchParams(location.search).get('v');
  if (!IDS.includes(v)) v = IDS[0];
  const knoepfe = [...document.querySelectorAll('[data-v-wahl]')];
  function setze(neu, url) {
    if (STEUER[v]) STEUER[v].stopp();
    v = neu;
    html.dataset.v = neu;
    document.querySelectorAll('[data-welt]').forEach(w => { w.hidden = w.dataset.welt !== neu; });
    knoepfe.forEach(b => b.setAttribute('aria-pressed', String(b.dataset.vWahl === neu)));
    if (url) {
      const q = new URLSearchParams(location.search); q.set('v', neu);
      history.replaceState(null, '', location.pathname + '?' + q.toString().replace(/=(?=&|$)/g, '') + location.hash);
    }
    STEUER[neu].start();
  }
  let vorf = false;   // läuft gerade die Vorführung? Dann zeigt sie nach dem Umschalten die neue Variante
  knoepfe.forEach(b => b.addEventListener('click', () => { setze(b.dataset.vWahl, true); scrollTo(0, 0); if (vorf) STEUER[v].vorfuehren(); }));
  setze(v, false);
  // Schalter „reduzierte Bewegung“ der Detailseite: Variante neu aufbauen
  new MutationObserver(m => { if (m.some(x => x.oldValue && x.oldValue.includes('reduziert') !== html.classList.contains('reduziert'))) STEUER[v].start(); })
    .observe(html, { attributes: true, attributeFilter: ['class'], attributeOldValue: true });

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