Zum Inhalt
← Navigation

Dock-Navigation mit Vergrößerung

Das Modul-Dock einer Planungs-App wächst unter dem Zeiger mit Kosinus-Falloff, die Nachbarn weichen aus, ein Tooltip mit Tastenkürzel sitzt über dem größten Icon.

Eine Leiste mit Icons, meist unten mittig. Nähert sich der Zeiger, wächst das Icon darunter am stärksten, die Nachbarn mit zunehmendem Abstand schwächer (Falloff). Die Leiste wächst mit. Verlässt der Zeiger die Leiste, kehren alle weich zur Ausgangsgröße zurück. Über dem vergrößerten Icon steht ein Tooltip mit Label.

Verspielt, taktil und sofort vertraut, weil jeder das Dock von macOS kennt.

Wann einsetzen

Persönliche Portfolios, Spielwiesen, Tool-Sammlungen und Labs.

Wann nicht

Bei seriösen Marken. Auf Touch-Geräten, dort fehlt der Zeiger. Wenn Icons ohne Label unverständlich sind. 2026 wirkt der Effekt zunehmend wie ein Template.

Varianten

  • Icons mit Label-Tooltip
  • Abschnitts-Dock (Abschnitte statt Apps)
  • Glas-Dock mit backdrop-filter

Umsetzung

Bei pointermove für jedes Icon die waagerechte Distanz zum Zeiger berechnen: scale = 1 + (maxScale − 1) × max(0, 1 − d / radius), weicher mit Kosinus-Falloff. Die Werte per gsap.quickTo nachführen. Sollen die Nachbarn ausweichen, Breite und Höhe statt scale animieren. Fallstrick Layout-Thrashing: die Ruhe-Positionen einmal messen und nur bei resize neu.

Robust umsetzen

Telefon. Keine Vergrößerung. Statische Leiste mit Labels.

Ohne Bewegung. Keine Vergrößerung, nur Hover- und Fokus-Fläche.

Barrierefrei. Icons brauchen zugängliche Namen. Der Tooltip erscheint auch bei Tastaturfokus. Die Vergrößerung darf Trefferflächen nicht verschieben, während man klickt.

Code

<!DOCTYPE html>
<!--
  Dock-Navigation mit Vergrößerung
  Fundus · Navigation · https://cecodesign.de/lab/fundus/navigation/dock-vergroesserung/
  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>Dock-Navigation mit Vergrößerung · 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; }

/* Taktwerk, Karlsruhe · Rezept „SaaS & KI: das Produkt zeigt sich selbst“:
   das echte Interface ist der Hero, Graustufen plus ein Markenton, Geist in drei Gewichten, Mono für Labels und Zahlen */
:root {
  --grund: #08090a; --panel: #161718; --panel-2: #111213; --rand: #242728; --text: #f7f8f8; --text-2: #d0d6e0; --meta: #8a8f98;
  --akzent: #7170ff; --gelb: #ffc533; --rot: #ff6161; --gruen: #59d499; --blau: #57c1ff;
  --ui: cubic-bezier(0.25, 0.46, 0.45, 0.94);   /* Rezept: easeOutQuad, 160 ms */
  --pult: 52px;
  --gx: clamp(16px, 3vw, 40px);
}
* { 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; }
html, body { overflow-x: clip; }
body { min-height: 100svh; }
button { font: inherit; color: inherit; background: none; border: 0; cursor: pointer; }
a { color: inherit; text-decoration: none; }
.mono { font-family: 'Geist Mono', ui-monospace, monospace; font-variant-numeric: tabular-nums; }
.label { font: 500 11px/1.3 'Geist Mono', ui-monospace, monospace; letter-spacing: .14em; text-transform: uppercase; color: var(--meta); }

/* ── Seite: Kopf, Hero-Zeile, Produktfenster ── */
.seite { height: calc(100svh - var(--pult)); min-height: 640px; display: grid; grid-template-rows: auto auto minmax(0, 1fr); max-width: 1240px; margin: 0 auto; padding: 0 var(--gx); }
.kopf { display: flex; align-items: center; justify-content: space-between; height: 60px; }
.logo { display: inline-flex; align-items: center; gap: 9px; font-weight: 600; font-size: 15px; letter-spacing: -.02em; }
.logo svg { width: 20px; height: 20px; }
.kopf nav { display: flex; gap: 26px; font-size: 14px; color: var(--meta); }
.kopf nav a, .kopf .an { transition: color 160ms var(--ui); }
.kopf nav a:hover, .kopf .an:hover { color: var(--text); }
.kopf .rechts { display: flex; align-items: center; gap: 18px; font-size: 14px; }
.kopf .an { color: var(--meta); }
.btn { display: inline-flex; align-items: center; gap: 8px; height: 32px; padding: 0 14px; border-radius: 8px; font-size: 14px; font-weight: 500; transition: background-color 160ms var(--ui), border-color 160ms var(--ui); }
.btn.primaer { background: #5e5ce6; color: #fff; }   /* Akzent abgedunkelt: Weiß 5,1:1 */
.btn.primaer:hover { background: #6361f2; }
.btn.leise { border: 1px solid var(--rand); color: var(--text-2); }
.btn.leise:hover { border-color: #3a3d40; }

.hero { display: grid; grid-template-columns: minmax(0, 1.25fr) minmax(0, 1fr); align-items: end; gap: 24px 48px; padding: clamp(18px, 3.6vh, 44px) 0 clamp(18px, 3vh, 32px); }
.hero h1 { font-size: clamp(2.5rem, 1.6rem + 3.6vw, 4.5rem); font-weight: 560; letter-spacing: -.022em; line-height: 1; text-wrap: balance; }
.hero h1 span { color: var(--meta); }
.hero .neben { display: grid; gap: 18px; justify-items: start; padding-bottom: 4px; }
.hero p { font-size: 1.0625rem; line-height: 1.6; letter-spacing: -.006em; color: var(--text-2); max-width: 44ch; }
.hero .ctas { display: flex; gap: 10px; }

/* Produktfenster: echtes HTML-Interface */
.fenster { position: relative; min-height: 0; border: 1px solid var(--rand); border-bottom: 0; border-radius: 12px 12px 0 0; background: var(--panel-2); overflow: hidden; display: grid; grid-template-rows: auto minmax(0, 1fr); box-shadow: 0 -1px 0 rgba(255,255,255,.03) inset, 0 30px 80px rgba(0,0,0,.5); }
.leiste { display: flex; align-items: center; gap: 14px; height: 44px; padding: 0 14px; border-bottom: 1px solid var(--rand); font-size: 13px; color: var(--meta); }
.leiste .werk { display: inline-flex; align-items: center; gap: 8px; color: var(--text-2); font-weight: 500; }
.leiste .werk i { width: 18px; height: 18px; border-radius: 5px; background: #2b2c46; color: #b9b8ff; display: grid; place-items: center; font: 600 10px/1 'Geist', sans-serif; font-style: normal; }
.leiste .pfad { color: var(--meta); }
.leiste .pfad b { color: var(--text); font-weight: 500; }
.leiste .suche { margin-left: auto; display: inline-flex; align-items: center; gap: 10px; height: 26px; padding: 0 8px 0 10px; border: 1px solid var(--rand); border-radius: 6px; width: 220px; }
.leiste .suche kbd { margin-left: auto; font: 500 11px/1 'Geist Mono', monospace; color: var(--meta); }
.leiste .ich { width: 24px; height: 24px; border-radius: 50%; background: linear-gradient(135deg, #3b3d52, #24252b); color: var(--text-2); display: grid; place-items: center; font-size: 10px; font-weight: 600; }

.inhalt { position: relative; min-height: 0; overflow: hidden; padding: 20px 22px 0; }
.modul { display: grid; grid-template-rows: auto auto 1fr; gap: 16px; height: 100%; }
.modul[hidden] { display: none; }
.m-kopf { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
.m-kopf h2 { font-size: 1.25rem; font-weight: 560; letter-spacing: -.012em; }
.chip { display: inline-flex; align-items: center; gap: 6px; height: 22px; padding: 0 8px; border-radius: 6px; background: #1c1d1f; border: 1px solid var(--rand); font: 500 11.5px/1 'Geist Mono', monospace; color: var(--text-2); white-space: nowrap; }
.chip i { width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
.chip.gelb { color: var(--gelb); background: rgba(255,197,51,.08); border-color: rgba(255,197,51,.2); }
.chip.gruen { color: var(--gruen); background: rgba(89,212,153,.08); border-color: rgba(89,212,153,.2); }
.m-kopf .btn { margin-left: auto; height: 28px; font-size: 13px; }
.m-sub { color: var(--meta); font-size: 13.5px; }

/* Plantafel */
.tafel { display: grid; grid-template-columns: 170px repeat(5, minmax(0, 1fr)); grid-template-rows: auto; grid-auto-rows: minmax(46px, 1fr); border-top: 1px solid var(--rand); font-size: 12.5px; min-height: 0; }
.tafel > .tag { padding: 8px 10px; color: var(--meta); border-left: 1px solid var(--rand); font: 500 11px/1 'Geist Mono', monospace; letter-spacing: .04em; }
.tafel > .tag.heute { color: var(--text); }
.tafel > .ecke { padding: 8px 0; }
.tafel .masch { padding: 0 0; min-height: 46px; display: flex; flex-direction: column; justify-content: center; gap: 3px; border-top: 1px solid var(--rand); }
.tafel .masch b { font-weight: 500; color: var(--text-2); }
.tafel .masch small { color: var(--meta); font: 400 11px/1 'Geist Mono', monospace; }
.tafel .spur { grid-column: 2 / -1; position: relative; min-height: 46px; border-top: 1px solid var(--rand); background: repeating-linear-gradient(90deg, transparent 0 calc(20% - 1px), var(--rand) calc(20% - 1px) 20%); }
.balken { z-index: 1; position: absolute; top: 50%; height: 30px; margin-top: -15px; display: flex; align-items: center; gap: 6px; padding: 0 8px; border-radius: 6px; font-size: 11.5px; white-space: nowrap; overflow: hidden; background: #202124; border: 1px solid #2c2e31; color: var(--text-2); }
.balken.laeuft { background: rgba(113,112,255,.16); border-color: rgba(113,112,255,.45); color: #d6d5ff; }
.balken.eng { background: rgba(255,197,51,.1); border-color: rgba(255,197,51,.4); color: #ffe3a1; }
.balken.fertig { background: rgba(89,212,153,.08); border-color: rgba(89,212,153,.3); color: #b6ecd1; }
.balken .nr { font-family: 'Geist Mono', monospace; color: inherit; opacity: .75; flex: none; }
.balken .t { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.jetzt { position: absolute; top: 0; bottom: 0; width: 1px; background: var(--akzent); }
.jetzt.kopf-punkt::before { content: ''; position: absolute; top: -3px; left: -3px; width: 7px; height: 7px; border-radius: 50%; background: var(--akzent); }

/* Tabellen, Karten, Listen */
.tabelle { width: 100%; border-collapse: collapse; font-size: 13px; align-self: start; }
.tabelle th { text-align: left; font: 500 11px/1 'Geist Mono', monospace; letter-spacing: .06em; text-transform: uppercase; color: var(--meta); padding: 9px 10px; border-bottom: 1px solid var(--rand); }
.tabelle td { padding: 11px 10px; border-bottom: 1px solid #1b1c1e; color: var(--text-2); }
.tabelle td.mono, .tabelle td.zahl { font-family: 'Geist Mono', monospace; font-size: 12.5px; }
.tabelle td.zahl { text-align: right; }
.tabelle th.zahl { text-align: right; }
.karten { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; align-content: start; }
.karte { border: 1px solid var(--rand); border-radius: 10px; background: var(--panel); padding: 14px; display: grid; gap: 10px; }
.karte .oben { display: flex; justify-content: space-between; align-items: center; font-size: 13px; color: var(--text-2); font-weight: 500; }
.karte .wert { font: 500 26px/1 'Geist', sans-serif; letter-spacing: -.02em; font-variant-numeric: tabular-nums; }
.karte .wert small { font-size: 13px; color: var(--meta); font-weight: 400; margin-left: 4px; letter-spacing: 0; }
.karte .unten { font-size: 12px; color: var(--meta); display: flex; justify-content: space-between; gap: 8px; }
.punkt { width: 7px; height: 7px; border-radius: 50%; display: inline-block; }
.saeulen { display: flex; align-items: end; gap: 3px; height: 34px; }
.saeulen i { flex: 1; background: #2a2c30; border-radius: 2px; }
.saeulen i.h { background: rgba(113,112,255,.7); }
.schicht { display: grid; grid-template-columns: 150px repeat(7, minmax(0, 1fr)); gap: 4px; font-size: 12.5px; align-content: start; }
.schicht .kz { height: 30px; border-radius: 6px; display: grid; place-items: center; font: 500 11.5px/1 'Geist Mono', monospace; background: #1a1b1d; color: var(--meta); }
.schicht .kz.F { background: rgba(87,193,255,.1); color: #a9dcfb; }
.schicht .kz.S { background: rgba(113,112,255,.14); color: #c9c8ff; }
.schicht .kz.N { background: rgba(255,255,255,.06); color: var(--text-2); }
.schicht .wer { display: flex; align-items: center; gap: 8px; color: var(--text-2); }
.schicht .wer i { width: 20px; height: 20px; border-radius: 50%; background: #26272b; display: grid; place-items: center; font: 600 9px/1 'Geist', sans-serif; font-style: normal; color: var(--meta); }
.schicht .tg { font: 500 11px/1 'Geist Mono', monospace; color: var(--meta); text-align: center; padding: 6px 0; }
.diagramm { border: 1px solid var(--rand); border-radius: 10px; background: var(--panel); padding: 14px 16px; display: grid; gap: 12px; align-self: start; }
.diagramm .bars { display: flex; align-items: end; gap: 10px; height: 120px; border-bottom: 1px solid var(--rand); }
.diagramm .bars div { flex: 1; display: grid; gap: 6px; justify-items: center; align-self: end; }
.diagramm .bars i { width: 100%; border-radius: 4px 4px 0 0; background: #2a2c30; }
.diagramm .bars div:last-child i { background: var(--akzent); }
.diagramm .achse { display: flex; gap: 10px; }
.diagramm .achse span { flex: 1; text-align: center; font: 500 11px/1 'Geist Mono', monospace; color: var(--meta); }
.vorschlag { border: 1px solid rgba(113,112,255,.35); border-radius: 12px; background: linear-gradient(180deg, rgba(113,112,255,.08), rgba(113,112,255,.02)); padding: 16px; display: grid; gap: 12px; max-width: 720px; align-self: start; }
.vorschlag .wer { display: flex; align-items: center; gap: 8px; color: #c9c8ff; font-size: 13px; font-weight: 500; }
.vorschlag p { color: var(--text-2); font-size: 14.5px; line-height: 1.55; }
.vorschlag ul { list-style: none; padding: 0; display: grid; gap: 6px; font: 400 12.5px/1.4 'Geist Mono', monospace; color: var(--text-2); }
.vorschlag li { display: flex; gap: 10px; padding: 7px 10px; border-radius: 6px; background: rgba(0,0,0,.25); }
.vorschlag li b { font-weight: 500; color: var(--gruen); }
.vorschlag li b.minus { color: var(--rot); }
.vorschlag .ctas { display: flex; gap: 8px; }
.einst { display: grid; gap: 0; max-width: 760px; align-self: start; border: 1px solid var(--rand); border-radius: 10px; overflow: hidden; }
.einst div { display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 12px 14px; border-bottom: 1px solid var(--rand); background: var(--panel); font-size: 13.5px; }
.einst div:last-child { border-bottom: 0; }
.einst small { display: block; color: var(--meta); font-size: 12px; margin-top: 2px; }
.schalter { width: 30px; height: 18px; border-radius: 9px; background: #2a2c30; position: relative; flex: none; }
.schalter::after { content: ''; position: absolute; top: 2px; left: 2px; width: 14px; height: 14px; border-radius: 50%; background: #9aa0a8; }
.schalter.an { background: var(--akzent); }
.schalter.an::after { left: 14px; background: #fff; }

/* ── Das Dock ── */
.dock-halter { position: absolute; left: 0; right: 0; bottom: 14px; display: flex; justify-content: center; pointer-events: none; z-index: 5; }
.dock { --basis: 44px; position: relative; display: flex; align-items: flex-end; gap: 6px; height: calc(var(--basis) + 16px); padding: 8px 8px; border-radius: 18px; pointer-events: auto;
        background: #161718; border: 1px solid #2a2d2f; box-shadow: 0 1px 0 rgba(255,255,255,.05) inset, 0 18px 40px rgba(0,0,0,.55); }
.dock[data-material='glas'] { background: rgba(28, 29, 31, .42); border-color: rgba(255,255,255,.1); -webkit-backdrop-filter: blur(18px) saturate(1.5); backdrop-filter: blur(18px) saturate(1.5); }
.d-item { position: relative; width: var(--basis); height: var(--basis); flex: none; display: block; padding: 0; transform-origin: 50% 100%; outline: none; }
.d-kachel { position: absolute; inset: 0; border-radius: 24%; display: grid; place-items: center; background: linear-gradient(180deg, #26282b, #1a1b1d); border: 1px solid #303336; box-shadow: 0 1px 0 rgba(255,255,255,.07) inset; transition: background-color 160ms var(--ui), border-color 160ms var(--ui); }
.d-kachel svg { width: 52%; height: 52%; fill: none; stroke: var(--text-2); stroke-width: 1.6; stroke-linecap: round; stroke-linejoin: round; }
.d-item.ki .d-kachel { background: linear-gradient(180deg, #34336e, #222146); border-color: #45449a; }
.d-item.ki .d-kachel svg { stroke: #d9d8ff; }
.d-item[aria-current='page']::after { content: ''; position: absolute; left: 50%; bottom: -6px; width: 4px; height: 4px; margin-left: -2px; border-radius: 50%; background: var(--text-2); }
.d-item:focus-visible .d-kachel { border-color: var(--akzent); box-shadow: 0 0 0 2px rgba(113,112,255,.45); }
.d-item.drueck .d-kachel { filter: brightness(.75); }
.trenner { width: 1px; align-self: stretch; margin: 4px 3px; background: #2c2f31; flex: none; }
.d-name { display: none; }
.tipp { position: fixed; left: 0; top: 0; z-index: 30; pointer-events: none; display: flex; align-items: center; gap: 8px; height: 28px; padding: 0 10px; border-radius: 7px; background: #1f2023; border: 1px solid #34373a; color: var(--text); font-size: 12.5px; font-weight: 500; white-space: nowrap; opacity: 0; transform: translate(-50%, -100%); transition: opacity 120ms var(--ui); box-shadow: 0 8px 24px rgba(0,0,0,.5); }
.tipp.an { opacity: 1; }
.tipp kbd { font: 500 10.5px/1 'Geist Mono', monospace; color: var(--meta); display: inline-flex; gap: 3px; }
.tipp kbd span { border: 1px solid #3a3d40; border-radius: 4px; padding: 2px 4px; }

/* Vorführung: gezeichneter Zeiger */
.zeiger { position: fixed; left: 0; top: 0; z-index: 40; width: 18px; height: 18px; pointer-events: none; display: none; }
.vorfuehrung .zeiger { display: block; }

/* ── Pult: Varianten und Werte, liegt unter der Seite ── */
.pult { height: var(--pult); display: flex; align-items: center; justify-content: space-between; gap: 10px 28px; padding: 0 var(--gx); border-top: 1px solid var(--rand); white-space: nowrap; font: 500 10.5px/1.4 'Geist Mono', ui-monospace, monospace; letter-spacing: .14em; text-transform: uppercase; color: var(--meta); background: var(--grund); }
.modi { display: flex; align-items: center; gap: 4px; flex-shrink: 0; }
.modi > span { margin: 0 8px 0 14px; }
.modi > span:first-child { margin-left: 0; }
.modi button { font: inherit; letter-spacing: inherit; text-transform: inherit; color: var(--meta); border: 1px solid var(--rand); border-radius: 6px; height: 26px; padding: 0 10px; transition: background-color 160ms var(--ui), color 160ms var(--ui), border-color 160ms var(--ui); }
.modi button:hover { color: var(--text); border-color: #3a3d40; }
.modi button[aria-pressed='true'] { background: var(--text); border-color: var(--text); color: var(--grund); }
.modi button:focus-visible, .btn:focus-visible, .kopf a:focus-visible { outline: 2px solid var(--akzent); outline-offset: 2px; }
.werte { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 2px 14px; letter-spacing: .06em; min-width: 0; }
.werte b { color: var(--text-2); font-weight: 500; }

/* Reduzierte Bewegung: keine Vergrößerung, nur Hover-/Fokus-Fläche und Tooltip */
.reduziert .d-item:hover .d-kachel, .reduziert .d-item.nah .d-kachel { background: #2e3134; border-color: #474b4f; }
.reduziert .d-item.ki:hover .d-kachel, .reduziert .d-item.ki.nah .d-kachel { background: #3d3c80; }
.reduziert .tipp { transition: none; }

/* Vorschau: nur das Produktfenster */
.vorschau body { overflow: hidden; }
.vorschau .pult, .vorschau .hero, .vorschau .kopf { display: none; }
.vorschau .seite { height: 100svh; min-height: 0; padding-top: 22px; grid-template-rows: minmax(0, 1fr); }

@media (max-width: 1100px) {
  .kopf nav { display: none; }
  .tafel { grid-template-columns: 130px repeat(5, minmax(0, 1fr)); }
  .pult { flex-direction: column; align-items: flex-start; justify-content: center; gap: 6px; }
  :root { --pult: 76px; }
  .werte { justify-content: flex-start; }
  .vorschau .tafel { grid-template-columns: 150px repeat(5, minmax(0, 1fr)); }
}
@media (max-width: 760px) {
  :root { --pult: auto; }
  .seite { height: auto; min-height: 0; }
  .kopf .an { display: none; }
  .hero { grid-template-columns: 1fr; padding-top: 18px; }
  .hero h1 { font-size: clamp(2.4rem, 11vw, 3rem); }
  .fenster { height: 470px; }
  .hero p { font-size: .9375rem; }
  .leiste .ich { margin-left: auto; }
  .balken .nr { display: none; }
  .leiste .suche { display: none; }
  .leiste .pfad { display: none; }
  .inhalt { padding: 16px 14px 0; }
  .m-kopf .btn { display: none; }
  .tafel { grid-template-columns: 1fr; }
  .tafel > .tag, .tafel > .ecke { display: none; }
  .tafel .masch { height: auto; padding-top: 10px; border-top: 1px solid var(--rand); }
  .tafel .spur { grid-column: 1; min-height: 40px; border-top: 0; }
  .tafel { grid-auto-rows: auto; }
  .karten { grid-template-columns: 1fr 1fr; }
  .tabelle .weg { display: none; }
  .schicht { grid-template-columns: 96px repeat(5, minmax(0, 1fr)); }
  .schicht .weg { display: none; }
  .pult { height: auto; padding-top: 12px; padding-bottom: 14px; }
  .modi { flex-wrap: wrap; row-gap: 6px; }
  .werte { display: none; }
}
/* Touch: keine Vergrößerung, statische Leiste mit Beschriftung */
@media (hover: none) {
  .dock-halter { left: 10px; right: 10px; bottom: 10px; }
  .dock { --basis: 34px; width: 100%; height: auto; justify-content: space-between; padding: 8px 10px 6px; gap: 2px; }
  .d-item { width: auto; height: auto; flex: 1; display: grid; justify-items: center; gap: 5px; }
  .d-kachel { position: relative; inset: auto; width: var(--basis); height: var(--basis); }
  .d-name { display: block; font-size: 10px; color: var(--meta); white-space: nowrap; }
  .d-item[aria-current='page'] .d-name { color: var(--text); }
  .d-item[aria-current='page']::after { display: none; }
  .d-item.mehr, .trenner { display: none; }
  .tipp { display: none; }
  .btn.primaer:hover { background: #5e5ce6; }
  .modi button:hover { color: var(--meta); border-color: var(--rand); }
  .modi button[aria-pressed='true']:hover { color: var(--grund); border-color: var(--text); }
}
@media (forced-colors: active) {
  button, [role=button], [role=tab], [role=switch] { border: 1px solid ButtonText !important; }
}
</style>
</head>
<body>
<div class="seite">
  <header class="kopf">
    <a class="logo" href="#" aria-label="Taktwerk, Startseite">
      <svg viewBox="0 0 20 20" aria-hidden="true"><rect x="2" y="9" width="3" height="9" rx="1" fill="#f7f8f8"/><rect x="6.5" y="3" width="3" height="15" rx="1" fill="#f7f8f8"/><rect x="11" y="7" width="3" height="11" rx="1" fill="#7170ff"/><rect x="15.5" y="11" width="3" height="7" rx="1" fill="#f7f8f8"/></svg>
      Taktwerk
    </a>
    <nav aria-label="Hauptnavigation"><a href="#">Produkt</a><a href="#">Fertiger</a><a href="#">Preise</a><a href="#">Changelog</a><a href="#">Doku</a></nav>
    <div class="rechts"><a class="an" href="#">Anmelden</a><a class="btn primaer" href="#">Kostenlos testen</a></div>
  </header>

  <section class="hero">
    <h1>Fertigung planen, <span>bevor sie hakt.</span></h1>
    <div class="neben">
      <p>Taktwerk legt Aufträge, Maschinen und Schichten auf eine Tafel und meldet Engpässe, bevor sie die Woche kippen. Aus Karlsruhe, für Fertiger mit 20 bis 500 Leuten.</p>
      <div class="ctas"><a class="btn primaer" href="#">Kostenlos testen</a><a class="btn leise" href="#">Demo buchen</a></div>
    </div>
  </section>

  <div class="fenster" aria-label="Produktansicht von Taktwerk">
    <div class="leiste">
      <span class="werk"><i>RB</i>Rheinstetter Blechtechnik</span>
      <span class="pfad">Halle 2 / <b data-pfad>Plantafel</b></span>
      <span class="suche">Suchen<kbd>⌘K</kbd></span>
      <span class="ich">MK</span>
    </div>
    <div class="inhalt" data-inhalt></div>
    <div class="dock-halter">
      <nav class="dock" data-dock data-material="massiv" aria-label="Module"></nav>
    </div>
  </div>
</div>

<div class="pult">
  <div class="modi" role="group" aria-label="Varianten">
    <span>Wachsen</span>
    <button type="button" data-wachsen="breite" aria-pressed="true">Breite</button>
    <button type="button" data-wachsen="scale" aria-pressed="false">Nur scale</button>
    <span>Material</span>
    <button type="button" data-material="massiv" aria-pressed="true">Massiv</button>
    <button type="button" data-material="glas" aria-pressed="false">Glas</button>
  </div>
  <p class="werte" data-werte></p>
</div>

<div class="tipp" data-tipp role="tooltip" id="dock-tipp"></div>
<svg class="zeiger" data-zeiger viewBox="0 0 18 18" aria-hidden="true"><path d="M2 1.5 15.5 9.2l-6 1.3-2.9 5.6Z" fill="#f7f8f8" stroke="#08090a" stroke-width="1.2" stroke-linejoin="round"/></svg>

<script src="https://cdn.jsdelivr.net/npm/gsap@3.15.0/dist/gsap.min.js"></script>
<script>
(() => {
  const html = document.documentElement;
  const reduziert = () => html.classList.contains('reduziert');
  const touch = () => matchMedia('(hover: none)').matches;
  const $ = s => document.querySelector(s), $$ = s => [...document.querySelectorAll(s)];

  /* ── Module der App: Inhalt als Daten ── */
  const ICON = {
    tafel: '<path d="M4 6.5h9M8 12h11M6 17.5h7"/><path d="M3 3.5v17" opacity=".5"/>',
    auftraege: '<rect x="5" y="4" width="14" height="17" rx="2.5"/><path d="M9.5 3v2.4h5V3M9 11h6M9 14.5h6M9 18h3"/>',
    maschinen: '<circle cx="12" cy="12" r="3.2"/><path d="M12 3v3M12 18v3M3 12h3M18 12h3M5.6 5.6l2.1 2.1M16.3 16.3l2.1 2.1M5.6 18.4l2.1-2.1M16.3 7.7l2.1-2.1"/>',
    schichten: '<rect x="3.5" y="5" width="17" height="15" rx="2.5"/><path d="M3.5 10h17M8 3v4M16 3v4M8 14h2M12 14h2M8 17h2"/>',
    auswertung: '<path d="M5 20v-7M10 20V6M15 20v-9M20 20V9M3 20.5h18"/>',
    ki: '<path d="M11 3.5l1.9 5.1 5.1 1.9-5.1 1.9L11 17.5l-1.9-5.1L4 10.5l5.1-1.9z"/><path d="M18 15.5l.8 2.2 2.2.8-2.2.8L18 21.5l-.8-2.2-2.2-.8 2.2-.8z"/>',
    einstellungen: '<path d="M4 7h10M18 7h2M4 17h4M12 17h8"/><circle cx="16" cy="7" r="2"/><circle cx="10" cy="17" r="2"/>',
  };
  const MODULE = [
    { id: 'tafel', name: 'Plantafel', kurz: 'Tafel', taste: ['G', 'P'] },
    { id: 'auftraege', name: 'Aufträge', kurz: 'Aufträge', taste: ['G', 'A'] },
    { id: 'maschinen', name: 'Maschinen', kurz: 'Maschinen', taste: ['G', 'M'] },
    { id: 'schichten', name: 'Schichten', kurz: 'Schichten', taste: ['G', 'S'] },
    { id: 'auswertung', name: 'Auswertung', kurz: 'Zahlen', taste: ['G', 'W'] },
    { trenner: true },
    { id: 'ki', name: 'KI-Disponent', kurz: 'KI', taste: ['⌘', 'J'] },
    { id: 'einstellungen', name: 'Einstellungen', kurz: 'Mehr', taste: ['⌘', ','], mehr: true },
  ];
  const tage = ['Mo 2.', 'Di 3.', 'Mi 4.', 'Do 5.', 'Fr 6.'];
  const TAFEL = [
    ['Laser L1', '6 kW · Blech bis 20 mm', [[0, 1.6, 'fertig', 'A-24112', 'Seitenteile Kühlregal'], [1.7, 1.9, 'laeuft', 'A-24117', 'Gehäuse Schaltschrank'], [3.7, 1.2, '', 'A-24125', 'Konsolen Edelstahl']]],
    ['Abkantpresse A2', '170 t · 3 m', [[.3, 1.2, 'fertig', 'A-24109', 'Winkel'], [1.6, 2.1, 'laeuft', 'A-24117', 'Gehäuse abkanten'], [3.9, 1, '', 'A-24128', 'Blenden']]],
    ['Abkantpresse A3', '220 t · 4 m', [[0, .9, 'fertig', 'A-24104', 'Träger'], [1, 2.6, 'eng', 'A-24131', 'Rahmen Förderband · 112 %'], [3.7, 1.2, '', 'A-24133', 'Deckel']]],
    ['Schweißroboter S1', 'MAG · 2 Stationen', [[.9, 1.8, 'laeuft', 'A-24112', 'Kühlregal schweißen'], [2.9, 1.7, '', 'A-24117', 'Gehäuse']]],
    ['Pulverbeschichtung', 'RAL nach Auftrag', [[0, 1.2, 'fertig', 'A-24101', 'RAL 7016'], [2.2, 1.4, '', 'A-24112', 'RAL 9005'], [3.8, 1.1, '', 'A-24109', 'RAL 7035']]],
    ['Montage M1', '4 Plätze', [[1.4, 1.3, 'laeuft', 'A-24104', 'Träger montieren'], [3, 2, '', 'A-24117', 'Endmontage + Prüfung']]],
    ['Versand', 'Rampe Ost', [[.6, .7, 'fertig', 'A-24098', 'Spedition Kühn'], [2.6, .8, '', 'A-24104', 'Abholung'], [4.3, .7, '', 'A-24112', 'Lkw 7,5 t']]],
  ];
  const AUFTRAEGE = [
    ['A-24131', 'Fördertechnik Ettlingen', 'Rahmen Förderband', '40', '08.11.', ['gelb', 'Engpass A3']],
    ['A-24117', 'Elektro Wagner, Pforzheim', 'Gehäuse Schaltschrank', '120', '11.11.', ['', 'In Arbeit']],
    ['A-24112', 'Kälte Brenner, Durlach', 'Kühlregal Seitenteile', '64', '07.11.', ['', 'In Arbeit']],
    ['A-24125', 'Gastro Bau Süd', 'Konsolen Edelstahl', '300', '15.11.', ['', 'Geplant']],
    ['A-24104', 'Hafenkran Rhein', 'Träger geschweißt', '12', '05.11.', ['gruen', 'Termintreu']],
    ['A-24098', 'Landtechnik Kraichgau', 'Abdeckbleche', '80', '04.11.', ['gruen', 'Versandt']],
    ['A-24133', 'Fördertechnik Ettlingen', 'Deckel Antrieb', '40', '18.11.', ['', 'Geplant']],
  ];
  const MASCH = [
    ['Laser L1', 'gruen', 91, 'A-24117 · Gehäuse', [6, 8, 7, 9, 8, 9, 9]],
    ['Abkantpresse A2', 'gruen', 84, 'A-24117 · Abkanten', [7, 6, 8, 8, 7, 8, 8]],
    ['Abkantpresse A3', 'gelb', 112, 'A-24131 · überbucht Mi', [8, 9, 9, 10, 11, 11, 11]],
    ['Schweißroboter S1', 'gruen', 78, 'A-24112 · Kühlregal', [5, 7, 6, 7, 8, 7, 8]],
    ['Pulverbeschichtung', 'blau', 0, 'Wartung bis 14:00', [6, 6, 5, 0, 0, 4, 6]],
    ['Montage M1', 'gruen', 69, 'A-24104 · Träger', [5, 6, 6, 7, 6, 7, 7]],
  ];
  const SCHICHT = [['Melanie K.', 'MK', 'FFFFF--'], ['Jonas R.', 'JR', 'SSSSS--'], ['Aylin D.', 'AD', 'NNNN---'], ['Piotr W.', 'PW', 'FFF-SSS'], ['Sven H.', 'SH', '--SSSSS'], ['Ines B.', 'IB', 'FFFFF--'], ['Kerem T.', 'KT', 'NNNNN--']];
  const FARBE = { gruen: 'var(--gruen)', gelb: 'var(--gelb)', blau: 'var(--blau)' };

  function modulHTML(id) {
    if (id === 'tafel') return `
      <div class="m-kopf"><h2>Plantafel · KW 45</h2><span class="chip">Auslastung 87 %</span><span class="chip gelb"><i></i>1 Engpass</span><span class="btn leise">Automatisch planen</span></div>
      <p class="m-sub">7 Ressourcen · 18 Arbeitsgänge · Stand 10:42 Uhr</p>
      <div class="tafel"><div class="ecke"></div>${tage.map((t, i) => `<div class="tag${i === 2 ? ' heute' : ''}">${t}</div>`).join('')}
        ${TAFEL.map(([n, s, b], r) => `<div class="masch"><b>${n}</b><small>${s}</small></div><div class="spur">${b.map(([a, l, k, nr, t]) => `<span class="balken ${k}" style="left:calc(${a * 20}% + 3px);width:calc(${l * 20}% - 6px)"><span class="nr">${nr}</span><span class="t">${t}</span></span>`).join('')}${r === 0 ? '<span class="jetzt kopf-punkt" style="left:49%;top:-34px"></span>' : '<span class="jetzt" style="left:49%"></span>'}</div>`).join('')}
      </div>`;
    if (id === 'auftraege') return `
      <div class="m-kopf"><h2>Aufträge</h2><span class="chip">32 offen</span><span class="chip gruen"><i></i>Termintreue 96,4 %</span><span class="btn leise">Neuer Auftrag</span></div>
      <p class="m-sub">Sortiert nach Liefertermin · ERP-Abgleich vor 4 Min.</p>
      <table class="tabelle"><thead><tr><th>Nr.</th><th class="weg">Kunde</th><th>Teil</th><th class="zahl weg">Menge</th><th class="zahl">Termin</th><th>Status</th></tr></thead>
      <tbody>${AUFTRAEGE.map(([nr, k, t, m, d, [f, s]]) => `<tr><td class="mono">${nr}</td><td class="weg">${k}</td><td>${t}</td><td class="zahl weg">${m}</td><td class="zahl">${d}</td><td><span class="chip ${f}">${f ? '<i></i>' : ''}${s}</span></td></tr>`).join('')}</tbody></table>`;
    if (id === 'maschinen') return `
      <div class="m-kopf"><h2>Maschinen</h2><span class="chip">6 Ressourcen</span><span class="chip gelb"><i></i>A3 überbucht</span></div>
      <p class="m-sub">Auslastung der laufenden Woche · OPC UA live</p>
      <div class="karten">${MASCH.map(([n, f, w, a, s]) => `<div class="karte"><div class="oben">${n}<span class="punkt" style="background:${FARBE[f]}"></span></div><div class="wert">${w ? w + '<small>%</small>' : '—<small>Wartung</small>'}</div><div class="saeulen">${s.map((h, i) => `<i class="${i === 2 ? 'h' : ''}" style="height:${h * 9}%"></i>`).join('')}</div><div class="unten"><span>${a}</span></div></div>`).join('')}</div>`;
    if (id === 'schichten') return `
      <div class="m-kopf"><h2>Schichten · KW 45</h2><span class="chip">3-Schicht</span><span class="chip gruen"><i></i>Alle Plätze besetzt</span></div>
      <p class="m-sub">F Früh 6–14 · S Spät 14–22 · N Nacht 22–6</p>
      <div class="schicht"><span></span>${['Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa', 'So'].map((t, i) => `<span class="tg${i > 4 ? ' weg' : ''}">${t}</span>`).join('')}
        ${SCHICHT.map(([n, k, p]) => `<span class="wer"><i>${k}</i>${n}</span>${[...p].map((c, i) => `<span class="kz ${c === '-' ? '' : c}${i > 4 ? ' weg' : ''}">${c === '-' ? '·' : c}</span>`).join('')}`).join('')}</div>`;
    if (id === 'auswertung') return `
      <div class="m-kopf"><h2>Auswertung</h2><span class="chip">Letzte 6 Wochen</span></div>
      <p class="m-sub">Gefertigte Teile je Woche · Rüstzeit seit Einführung −18 %</p>
      <div class="karten">${[['Termintreue', '96,4', '%', '+3,1 Pkt.'], ['Durchlaufzeit', '3,2', 'Tage', '−0,6 Tage'], ['Rüstzeit je Auftrag', '41', 'Min.', '−18 %']].map(([n, w, e, d]) => `<div class="karte"><div class="oben">${n}</div><div class="wert">${w}<small>${e}</small></div><div class="unten"><span style="color:var(--gruen)">${d}</span><span>ggü. Vorquartal</span></div></div>`).join('')}</div>
      <div class="diagramm"><div class="bars">${[62, 70, 66, 78, 74, 88].map(h => `<div><i style="height:${h}px"></i></div>`).join('')}</div><div class="achse">${['KW 40', 'KW 41', 'KW 42', 'KW 43', 'KW 44', 'KW 45'].map(k => `<span>${k}</span>`).join('')}</div></div>`;
    if (id === 'ki') return `
      <div class="m-kopf"><h2>KI-Disponent</h2><span class="chip">Vorschlag 1 von 2</span></div>
      <p class="m-sub">Prüft die Tafel alle 15 Minuten auf Engpässe und schlägt Umplanungen vor. Nichts wird ohne Freigabe geändert.</p>
      <div class="vorschlag"><span class="wer">${'<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round">' + ICON.ki + '</svg>'}Abkantpresse A3 ist am Mittwoch zu 112 % belegt</span>
        <p>A-24131 (Rahmen Förderband) passt mit gleichem Werkzeug auf A2. Wenn die Konsolen A-24125 einen Tag später laufen, bleibt der Liefertermin 08.11. für Fördertechnik Ettlingen sicher.</p>
        <ul><li><b class="minus">−</b>A3 · Mi · A-24131 · 6,5 h</li><li><b>+</b>A2 · Mi · A-24131 · 6,5 h · gleiches Oberwerkzeug</li><li><b>+</b>Laser L1 · Fr · A-24125 rückt 1 Tag · Termin 15.11. bleibt</li></ul>
        <div class="ctas"><span class="btn primaer">Übernehmen</span><span class="btn leise">Verwerfen</span></div></div>`;
    return `
      <div class="m-kopf"><h2>Einstellungen</h2><span class="chip">Halle 2</span></div>
      <p class="m-sub">Gilt für alle Planer dieses Werks.</p>
      <div class="einst">${[['Schichtmodell', '3-Schicht, Mo bis Fr, Wochenende nach Bedarf', 1], ['Feiertage', 'Baden-Württemberg', 1], ['ERP-Abgleich', 'Alle 5 Minuten, Aufträge und Stammdaten', 1], ['KI-Disponent', 'Vorschläge anzeigen, nie selbst umplanen', 1], ['Wochenbericht per Mail', 'Freitag 14:00 an die Werkleitung', 0]].map(([n, s, a]) => `<div><span>${n}<small>${s}</small></span><i class="schalter${a ? ' an' : ''}"></i></div>`).join('')}</div>`;
  }

  /* ── Dock aufbauen ── */
  const dock = $('[data-dock]'), inhalt = $('[data-inhalt]'), tipp = $('[data-tipp]'), pfad = $('[data-pfad]'), zeiger = $('[data-zeiger]');
  dock.innerHTML = MODULE.map(m => m.trenner ? '<span class="trenner" aria-hidden="true"></span>'
    : `<button type="button" class="d-item${m.id === 'ki' ? ' ki' : ''}${m.mehr ? ' mehr' : ''}" data-id="${m.id}" aria-label="${m.name}" aria-describedby="dock-tipp"><span class="d-kachel"><svg viewBox="0 0 24 24" aria-hidden="true">${ICON[m.id]}</svg></span><span class="d-name" aria-hidden="true">${m.kurz}</span></button>`).join('');
  inhalt.innerHTML = MODULE.filter(m => !m.trenner).map(m => `<section class="modul" data-modul="${m.id}" hidden>${modulHTML(m.id)}</section>`).join('');
  const items = $$('.d-item');
  const daten = MODULE.filter(m => !m.trenner);

  function oeffnen(id) {
    items.forEach(b => b.dataset.id === id ? b.setAttribute('aria-current', 'page') : b.removeAttribute('aria-current'));
    $$('.modul').forEach(m => { m.hidden = m.dataset.modul !== id; });
    pfad.textContent = daten.find(m => m.id === id).name;
    const neu = $(`.modul[data-modul="${id}"]`);
    if (!reduziert() && window.gsap) gsap.fromTo(neu, { autoAlpha: 0, y: 6 }, { autoAlpha: 1, y: 0, duration: .18, ease: 'power1.out' });
  }

  /* ── Vergrößerung ──
     Messwerte: Max-Skalierung 1,6–2,0; Einflussradius 2–3 Icon-Breiten; quickTo 200–300 ms power3.out.
     Gewählt: 1,8 · 2,5 Breiten · 250 ms. Abstand wird zu den Ruhe-Mitten gemessen (kein Layout-Thrashing). */
  const MAX = 1.8, RADIUS = 2.5, DAUER = .25, KURVE = 'power3.out';
  let wachsen = 'breite', material = 'massiv';
  let basis = 44, abstand = 6, mitten = [], aktivIndex = -1, laufend = false;
  const qW = [], qH = [], qS = [];

  function quick() {
    items.forEach((el, i) => {
      qW[i] = gsap.quickTo(el, 'width', { duration: DAUER, ease: KURVE });
      qH[i] = gsap.quickTo(el, 'height', { duration: DAUER, ease: KURVE });
      qS[i] = gsap.quickTo(el, 'scale', { duration: DAUER, ease: KURVE });
    });
  }
  // Ruhe-Mitten relativ zur Dock-Mitte: einmal messen, bei resize neu
  function messen() {
    if (!window.gsap) return;
    gsap.killTweensOf(items);
    gsap.set(items, { clearProps: 'width,height,scale' });
    const cs = getComputedStyle(dock);
    basis = parseFloat(cs.getPropertyValue('--basis')) || 44;
    abstand = parseFloat(cs.columnGap) || 6;
    const d = dock.getBoundingClientRect(), m = d.left + d.width / 2;
    mitten = items.map(el => { const r = el.getBoundingClientRect(); return r.left + r.width / 2 - m; });
    quick();
  }
  function dockMitte() { const d = dock.getBoundingClientRect(); return d.left + d.width / 2; }
  function faktor(dx) {
    const R = RADIUS * (basis + abstand);
    return dx < R ? (Math.cos(Math.PI * dx / R) + 1) / 2 : 0;       // Kosinus-Falloff
  }
  function nachfuehren(x) {
    const rel = x - dockMitte();
    let bester = -1, min = Infinity;
    items.forEach((el, i) => {
      const dx = Math.abs(rel - mitten[i]), s = 1 + (MAX - 1) * faktor(dx);
      if (dx < min) { min = dx; bester = i; }
      if (wachsen === 'breite') { qW[i](basis * s); qH[i](basis * s); qS[i](1); }
      else { qW[i](basis); qH[i](basis); qS[i](s); }
    });
    zeigeTipp(min < (basis + abstand) * .75 ? bester : -1);
  }
  function ruhe() { items.forEach((el, i) => { qW[i] && (qW[i](basis), qH[i](basis), qS[i](1)); }); zeigeTipp(-1); }

  // Tooltip folgt dem wachsenden Icon (rAF, solange sichtbar)
  function zeigeTipp(i) {
    items.forEach((el, j) => el.classList.toggle('nah', j === i));
    if (i === aktivIndex) return;
    aktivIndex = i;
    if (i < 0) { tipp.classList.remove('an'); return; }
    const m = daten[i];
    tipp.innerHTML = `${m.name}<kbd>${m.taste.map(t => `<span>${t}</span>`).join('')}</kbd>`;
    tipp.classList.add('an');
    if (!laufend) { laufend = true; requestAnimationFrame(tippLage); }
  }
  function tippLage() {
    if (aktivIndex < 0) { laufend = false; return; }
    const r = items[aktivIndex].getBoundingClientRect();
    tipp.style.left = (r.left + r.width / 2) + 'px';
    tipp.style.top = (r.top - 10) + 'px';
    requestAnimationFrame(tippLage);
  }

  // Maus und Tastatur
  dock.addEventListener('pointermove', e => { if (e.pointerType !== 'mouse' || vorfuehrung) return; reduziert() || touch() ? zeigeTipp(items.indexOf(e.target.closest('.d-item'))) : nachfuehren(e.clientX); });
  dock.addEventListener('pointerleave', e => { if (e.pointerType === 'mouse' && !vorfuehrung) ruhe(); });
  items.forEach((b, i) => {
    b.addEventListener('click', () => oeffnen(b.dataset.id));
    b.addEventListener('focus', () => { if (b.matches(':focus-visible')) zeigeTipp(i); });   // Tooltip auch bei Fokus, ohne Vergrößerung
    b.addEventListener('blur', () => zeigeTipp(-1));
    b.addEventListener('keydown', e => {
      const n = e.key === 'ArrowRight' ? 1 : e.key === 'ArrowLeft' ? -1 : 0;
      if (n) { e.preventDefault(); items[(i + n + items.length) % items.length].focus(); }
    });
  });
  $$('a, .btn').forEach(a => a.addEventListener('click', e => e.preventDefault()));

  /* ── Varianten ── */
  function werteZeigen() {
    $('[data-werte]').innerHTML = reduziert()
      ? '<span>Reduziert: <b>keine Vergrößerung</b>, nur Hover-/Fokus-Fläche und Tooltip</span>'
      : `<span>Max <b>${MAX}×</b> · Radius <b>${RADIUS} Icons = ${Math.round(RADIUS * (basis + abstand))} px</b> · <b>Kosinus</b>-Falloff</span><span>quickTo <b>${DAUER * 1000} ms ${KURVE}</b> · ${wachsen === 'breite' ? '<b>width/height</b>, Nachbarn weichen aus' : '<b>scale</b>, Nachbarn bleiben'}</span>`;
  }
  $$('[data-wachsen]').forEach(b => b.addEventListener('click', () => { wachsen = b.dataset.wachsen; $$('[data-wachsen]').forEach(x => x.setAttribute('aria-pressed', String(x === b))); ruhe(); werteZeigen(); }));
  $$('[data-material]').forEach(b => b.addEventListener('click', () => { setzeMaterial(b.dataset.material); }));
  function setzeMaterial(m) { material = m; dock.dataset.material = m; $$('[data-material]').forEach(x => x.setAttribute('aria-pressed', String(x.dataset.material === m))); }

  /* ── Vorführung: gedachter Zeiger fährt das Dock ab und klickt zweimal ── */
  let uhr = null, start = 0, vorfuehrung = false, letzterKlick = -1, runde = 0;
  // [Zeit s, Position (Index der Ruhe-Mitte, auch Zwischenwerte), drin?, Klick?]
  const WEG = [[0, 3, 0], [.8, -.2, 1], [3, 6.2, 1], [4.2, 2, 1], [4.7, 2, 1, 1], [5.9, 5, 1], [6.4, 5, 1, 1], [7.6, 5, 0], [9, 3, 0]];
  const sanft = t => .5 - .5 * Math.cos(Math.PI * t);
  function stelle(p) {
    const i = Math.max(0, Math.min(mitten.length - 1, Math.floor(p))), j = Math.min(mitten.length - 1, i + 1), f = p - Math.floor(p);
    if (p < 0) return mitten[0] + p * (basis + abstand);
    if (p > mitten.length - 1) return mitten[mitten.length - 1] + (p - mitten.length + 1) * (basis + abstand);
    return mitten[i] + (mitten[j] - mitten[i]) * f;
  }
  function schritt(t) {
    const zeit = ((t - start) / 1000) % 9, rundeJetzt = Math.floor((t - start) / 9000);
    if (rundeJetzt !== runde) { runde = rundeJetzt; letzterKlick = -1; setzeMaterial(runde % 2 ? 'glas' : 'massiv'); }
    let k = 0; while (k < WEG.length - 2 && WEG[k + 1][0] <= zeit) k++;
    const [t0, p0, d0] = WEG[k], [t1, p1, d1] = WEG[k + 1], f = sanft(Math.min(1, (zeit - t0) / (t1 - t0)));
    const d = dock.getBoundingClientRect(), mx = d.left + d.width / 2;
    const x = mx + stelle(p0 + (p1 - p0) * f);
    const drin = d0 + (d1 - d0) * f;
    const yIn = d.bottom - 8 - basis / 2, yAus = d.top - 150;
    const y = yAus + (yIn - yAus) * drin;
    zeiger.style.transform = `translate(${x - 2}px, ${y - 1}px)`;
    if (drin > .85) { reduziert() ? zeigeTipp(nahster(x - mx)) : nachfuehren(x); } else if (aktivIndex >= 0 || drin < .85) ruhe();
    // Klick
    for (let n = 0; n < WEG.length; n++) if (WEG[n][3] && zeit >= WEG[n][0] && letzterKlick < n && zeit < WEG[n][0] + .5) {
      letzterKlick = n;
      const el = items[Math.round(WEG[n][1])];
      el.classList.add('drueck'); setTimeout(() => el.classList.remove('drueck'), 140);
      oeffnen(el.dataset.id);
    }
    uhr = requestAnimationFrame(schritt);
  }
  function nahster(rel) { let b = 0, m = Infinity; mitten.forEach((c, i) => { const d = Math.abs(rel - c); if (d < m) { m = d; b = i; } }); return b; }

  function vorfuehren() {
    pause();
    if (!window.gsap) return;
    html.classList.add('vorfuehrung');
    vorfuehrung = true; letzterKlick = -1; runde = 0;
    oeffnen('tafel'); setzeMaterial('massiv');
    messen();
    start = performance.now();
    uhr = requestAnimationFrame(schritt);
  }
  function pause() { cancelAnimationFrame(uhr); uhr = null; }
  function weiter() { if (!uhr && vorfuehrung) { start = performance.now() - ((performance.now() - start) % 9000); uhr = requestAnimationFrame(schritt); } }
  function neustart() { pause(); vorfuehrung = false; html.classList.remove('vorfuehrung'); ruhe(); oeffnen('tafel'); }


  oeffnen('tafel');
  if (!window.gsap) return;
  werteZeigen();
  document.fonts.ready.then(() => { messen(); werteZeigen(); });
  let rz; addEventListener('resize', () => { clearTimeout(rz); rz = setTimeout(() => { messen(); werteZeigen(); }, 120); });
})();
</script>
</body>
</html>