Gleitender Indikator / Segment-Schalter mit Pille
Pille, Unterstrich und Segment-Schalter gleiten per CSS Anchor Positioning zum Ziel, 250 ms mit der bei COLLINS gemessenen Kurve.
Ein einziges Element markiert den aktiven oder gehoverten Punkt: ein Unterstrich, ein Punkt oder eine Hintergrund-Pille. Beim Wechsel gleitet es von der alten zur neuen Position und passt seine Breite an, statt zu springen. Typische Formen sind die Hover-Pille in der Navigation, der wandernde Tab-Unterstrich und der Segment-Schalter, dessen Pille zur gewählten Option gleitet. 2026 geht das ohne JavaScript per CSS Anchor Positioning: Das Ziel bekommt anchor-name, das Pseudo-Element des Containers richtet sich per anchor() daran aus und transitioniert seine inset-Werte.
Kontinuität: Das Auge folgt einem Objekt, so wird der Zustandswechsel verständlich. Ein kleines Detail mit großem Qualitätsgefühl. Besonders stark wirkt es zusammen mit animierten Preisen in einem Preis-Umschalter.
Wann einsetzen
Tabs, Pill-Navigationen, Filterleisten, Segment-Schalter, Sprach- und Theme-Schalter, Ansichtsumschalter (Raster oder Liste), Preis-Umschalter (monatlich oder jährlich), Einstellungen.
Wann nicht
Bei Mehrseiten-Navigation mit vollem Neuladen fehlt die alte Position. Dann nur mit Cross-Document View Transition oder gar nicht. Bei mehr als 4 bis 5 Optionen, dann besser Dropdown oder scrollbare Tabs. Wenn die Optionen sehr unterschiedlich lang sind und umbrechen.
Varianten
- Hover-Pille: folgt dem Zeiger über die Links und kehrt beim Verlassen zum aktiven Punkt zurück (COLLINS)
- Tab-Unterstrich, der beim Wechsel wandert (Chrome-Anleitung, Cursor)
- Segment-Schalter: Pille ankert am Label der gewählten Radio-Option
- Gleiten oder Dehnen: beide Kanten gleichzeitig oder die führende Kante zuerst
- Punkt oder Balken in vertikalen Leisten (Stripe: Höhe per anchor-size)
- Glas-Indikator mit Glassmorphism
- Preis-Umschalter mit animierten Zahlen (Motion AnimateNumber)
- Framework-Varianten: Motion layoutId (Shared Layout), Radix NavigationMenu.Indicator mit CSS-Variablen, Base UI oder Radix Toggle Group mit Motion
Umsetzung
Tab-Unterstrich: .tabs { position: relative } .tabs [aria-selected=true] { anchor-name: --tab } .tabs::after { content: ''; position: absolute; position-anchor: --tab; left: anchor(left); right: anchor(right); top: calc(anchor(bottom) - 2px); height: 2px } @media (prefers-reduced-motion: no-preference) { .tabs::after { transition: left .25s cubic-bezier(.215,.61,.355,1), right .25s cubic-bezier(.215,.61,.355,1) } }. Nicht position-area verwenden, das ist nicht transitionierbar.
Hover-Pille: a:hover, a:focus-visible { anchor-name: --pille }; den aktiven Punkt mit .nav:has(a:hover) a[aria-current] { anchor-name: none } freigeben. Pille 0,5rem breiter als der Eintrag: left: calc(anchor(left) - .25rem). Hover-Regeln in @media (hover: hover) legen.
Segment-Schalter: <fieldset> mit <legend> und Radios, label:has(:checked) { anchor-name: --seg }, Pille als ::before der Spur mit z-index: -1 und isolation: isolate. Die Radios behalten die native Pfeiltasten-Steuerung.
Dehnen: JS setzt data-richtung, die folgende Kante bekommt ein transition-delay. Die Richtung aus der DOM-Reihenfolge bestimmen und vorher kein Layout lesen, sonst startet die Transition noch mit der alten Richtung.
Fallback: @supports not (position-anchor: --a) misst JS die Position (getBoundingClientRect oder offsetLeft/offsetWidth) und schreibt CSS-Variablen, dieselben Transitions greifen. Ohne JS: statischer border-bottom bzw. Hintergrund am gewählten Label.
Robust umsetzen
Telefon. Funktioniert bei Tap identisch. Die Hover-Pille entfällt, die Pille sitzt am aktiven Punkt und gleitet beim Antippen.
Ohne Bewegung. Transition entfernen, der Indikator springt. Die Chrome-Anleitung sieht das bereits so vor.
Barrierefrei. Der Indikator ist dekorativ. Den Zustand zwingend über aria-current=page bzw. aria-selected abbilden und nicht nur über Farbe oder Pille zeigen, etwa mit einem zusätzlichen Punkt. Segment-Schalter als Radiogruppe mit fieldset und legend (oder role=radiogroup). Tabs mit role=tablist, tab und tabpanel und Pfeiltasten-Steuerung.
Code
<!DOCTYPE html>
<!--
Gleitender Indikator / Segment-Schalter mit Pille
Fundus · Navigation · https://cecodesign.de/lab/fundus/navigation/gleitender-indikator/
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>Gleitender Indikator / Segment-Schalter mit Pille · Fundus-Demo</title>
<script>document.documentElement.classList.add('js');if(matchMedia('(prefers-reduced-motion: reduce)').matches)document.documentElement.classList.add('reduziert')</script>
<style>
@font-face { font-family: 'Geist'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/geist:vf@latest/latin-wght-normal.woff2') format('woff2-variations'); font-weight: 100 900; font-display: swap; }
@font-face { font-family: 'Geist Mono'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/geist-mono:vf@latest/latin-wght-normal.woff2') format('woff2-variations'); font-weight: 100 900; font-display: swap; }
:root {
/* Kadenz: Graustufen tragen alles, ein Markenton für Aktion und aktive Zustände */
--papier: #fafafa; --karte: #ffffff; --tinte: #121316; --leise: #676b73; --linie: rgba(18, 19, 22, .1);
--flaeche: #efeff1; --akzent: #4b45d9; --akzent-hell: #e7e6fb; --warn: #d9902a;
/* Gemessen bei COLLINS: Position 250 ms mit easeOutCubic, Deckkraft 500 ms */
--kurve: cubic-bezier(0.215, 0.61, 0.355, 1);
--dauer: 250ms;
--dauer-deckkraft: 500ms;
--versatz: 0ms; /* Variante „Dehnen“: die folgende Kante startet später */
--schatten: 0 12px 31px #0000001f;
}
.dehnen { --versatz: 120ms; }
* { box-sizing: border-box; margin: 0; }
html { background: var(--papier); color: var(--tinte); font-family: 'Geist', system-ui, sans-serif; letter-spacing: -.006em; -webkit-font-smoothing: antialiased; font-feature-settings: "tnum" 1; }
body { min-height: 100svh; }
button, input { font: inherit; color: inherit; }
.mono { font: 500 11px/1.35 'Geist Mono', ui-monospace, monospace; letter-spacing: .14em; text-transform: uppercase; color: var(--leise); }
.sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
.seite { max-width: 1200px; margin: 0 auto; padding: clamp(16px, 2.6vw, 28px) clamp(16px, 3.4vw, 40px) 120px; display: grid; gap: clamp(28px, 5.4vh, 60px); }
/* ---------- Kopf mit Hover-Pille ---------- */
.kopf { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 16px; }
.marke { display: inline-flex; align-items: center; gap: 9px; font-size: 18px; font-weight: 600; letter-spacing: -.03em; color: inherit; text-decoration: none; white-space: nowrap; }
.marke svg { width: 22px; height: 22px; flex: none; }
.kopf__ort { justify-self: end; display: flex; align-items: center; gap: 18px; font-size: 14px; font-weight: 500; }
.kopf__ort a { color: inherit; text-decoration: none; white-space: nowrap; }
.kopf__ort .anmelden { color: var(--leise); }
.knopf { display: inline-flex; align-items: center; height: 34px; padding: 0 14px; border-radius: 8px; background: var(--akzent); color: #fff !important; box-shadow: inset 0 1px 0 rgba(255, 255, 255, .18), 0 1px 2px rgba(18, 19, 22, .16); }
.nav__liste { list-style: none; padding: 4px; display: flex; gap: 2px; position: relative; isolation: isolate; border-radius: 999px; background: rgba(18, 19, 22, .045); }
.nav__liste a { display: flex; align-items: center; gap: 7px; padding: 8px 15px; border-radius: 999px; font-size: 14.5px; font-weight: 500; color: var(--leise); text-decoration: none; outline: none; transition: color var(--dauer) var(--kurve); }
.nav__liste a[aria-current="page"] { color: var(--tinte); }
/* Zustand nicht nur über die Pille: der aktuelle Punkt trägt einen Punkt im Markenton */
.nav__liste a::before { content: ""; width: 5px; height: 5px; border-radius: 50%; background: var(--akzent); margin-left: -12px; opacity: 0; transition: opacity var(--dauer) var(--kurve), margin var(--dauer) var(--kurve); }
.nav__liste a[aria-current="page"]::before { margin-left: 0; opacity: 1; }
/* Die Pille: ein einziges Element am Container, verankert am Ziel */
.nav__liste a[aria-current="page"] { anchor-name: --nav-pille; }
.nav__liste:has(a.zeigt) a[aria-current="page"] { anchor-name: none; }
.nav__liste a.zeigt { anchor-name: --nav-pille; color: var(--tinte); }
@media (hover: hover) {
.nav__liste:has(a:hover, a:focus-visible) a[aria-current="page"] { anchor-name: none; }
.nav__liste a:is(:hover, :focus-visible) { anchor-name: --nav-pille; color: var(--tinte); }
}
.nav__liste::before {
content: ""; position: absolute; z-index: -1; border-radius: 999px; background: var(--karte); box-shadow: var(--schatten);
position-anchor: --nav-pille;
top: anchor(top); bottom: anchor(bottom);
left: calc(anchor(left) - .25rem); right: calc(anchor(right) - .25rem); /* Pille 0,5rem breiter als der Eintrag */
}
/* ---------- Einleitung ---------- */
.intro { display: grid; gap: 18px; }
.neu { justify-self: start; display: inline-flex; align-items: center; gap: 10px; padding: 4px 12px 4px 4px; border: 1px solid var(--linie); border-radius: 999px; background: var(--karte); font-size: 13px; color: var(--leise); text-decoration: none; }
.neu b { font: 500 10.5px/1 'Geist Mono', monospace; letter-spacing: .1em; text-transform: uppercase; color: var(--akzent); background: var(--akzent-hell); padding: 5px 8px; border-radius: 999px; }
.neu span { color: var(--tinte); }
.titel { font-size: clamp(2.25rem, 1.6rem + 3.6vw, 4.5rem); font-weight: 580; letter-spacing: -.035em; line-height: 1.02; }
.titel span { display: block; color: #898d95; }
.titel b { display: block; font-weight: inherit; }
.intro__unten { display: grid; gap: 18px; }
.intro__text { max-width: 58ch; font-size: clamp(15px, 1.2vw, 17px); line-height: 1.6; text-wrap: pretty; color: var(--leise); }
.intro__zeile { display: flex; justify-content: space-between; align-items: center; gap: 16px; flex-wrap: wrap; }
.arbeiten { display: grid; gap: clamp(22px, 3.4vh, 32px); }
/* ---------- Steuerleiste: Tabs mit Unterstrich + Segment-Schalter ---------- */
.steuer { display: flex; justify-content: space-between; align-items: end; gap: 20px 32px; flex-wrap: wrap; border-bottom: 1px solid var(--linie); }
.tabs { display: flex; gap: clamp(18px, 2.6vw, 34px); position: relative; }
.tabs button { background: none; border: 0; padding: 0 0 14px; cursor: pointer; font-size: clamp(16px, 1.8vw, 21px); font-weight: 520; letter-spacing: -.018em; color: var(--leise); white-space: nowrap; transition: color var(--dauer) var(--kurve); outline: none; }
.tabs button sup { font: 500 max(10px, .5em)/1 'Geist Mono', monospace; letter-spacing: .06em; margin-left: 4px; vertical-align: .6em; }
.tabs button[aria-selected="true"] { color: var(--tinte); anchor-name: --tab; }
.tabs button:focus-visible { text-decoration: underline dotted; text-underline-offset: 5px; }
.tabs::after {
content: ""; position: absolute; height: 2px; background: var(--akzent); border-radius: 2px;
position-anchor: --tab;
top: calc(anchor(bottom) - 1.5px); left: anchor(left); right: anchor(right);
}
.seg { border: 0; padding: 0; margin: 0 0 10px; display: flex; align-items: center; gap: 12px; }
.seg legend { float: left; }
.seg__spur { display: flex; position: relative; isolation: isolate; padding: 3px; border-radius: 999px; border: 1px solid var(--linie); background: var(--karte); }
.seg label { position: relative; display: block; cursor: pointer; }
.seg input { position: absolute; inset: 0; opacity: 0; margin: 0; cursor: pointer; }
.seg span { display: block; padding: 6px 14px; font: 500 11px/1.3 'Geist Mono', monospace; letter-spacing: .14em; text-transform: uppercase; color: var(--leise); transition: color var(--dauer) var(--kurve); }
.seg label:has(:checked) { anchor-name: --seg; }
.seg label:has(:checked) span { color: var(--karte); }
.seg label:has(:focus-visible) span { text-decoration: underline; text-underline-offset: 3px; }
.seg__spur::before {
content: ""; position: absolute; z-index: -1; border-radius: 999px; background: var(--tinte);
position-anchor: --seg;
top: anchor(top); bottom: anchor(bottom); left: anchor(left); right: anchor(right);
}
/* ---------- Bewegung: eine Kurve für alle drei Indikatoren ---------- */
.nav__liste::before, .tabs::after, .seg__spur::before {
transition: left var(--dauer) var(--kurve), right var(--dauer) var(--kurve),
top var(--dauer) var(--kurve), bottom var(--dauer) var(--kurve),
opacity var(--dauer-deckkraft) var(--kurve);
}
/* Dehnen: die führende Kante läuft sofort, die folgende mit Versatz (Richtung setzt JS) */
[data-richtung="rechts"]::before, [data-richtung="rechts"]::after {
transition: right var(--dauer) var(--kurve), left var(--dauer) var(--kurve) var(--versatz),
top var(--dauer) var(--kurve), bottom var(--dauer) var(--kurve), opacity var(--dauer-deckkraft) var(--kurve);
}
[data-richtung="links"]::before, [data-richtung="links"]::after {
transition: left var(--dauer) var(--kurve), right var(--dauer) var(--kurve) var(--versatz),
top var(--dauer) var(--kurve), bottom var(--dauer) var(--kurve), opacity var(--dauer-deckkraft) var(--kurve);
}
/* Ohne Anchor Positioning: JS misst und schreibt --l/--r/--t/--b, dieselben Transitions greifen */
@supports not (position-anchor: --a) {
.nav__liste::before, .tabs::after, .seg__spur::before { left: var(--l, 0); right: var(--r, 100%); top: var(--t, 0); bottom: var(--b, 0); }
.tabs::after { top: auto; bottom: -1px; }
.nav__liste::before { left: calc(var(--l, 0px) - .25rem); right: calc(var(--r, 100%) - .25rem); }
}
/* ---------- Funktionen: Karten und Liste ---------- */
.werke { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: clamp(28px, 3vw, 40px) clamp(12px, 1.6vw, 20px); }
.werk { display: grid; grid-template-columns: 1fr auto; gap: 5px 12px; align-content: start; }
.werk[hidden] { display: none; }
.werk__bild { grid-column: 1 / -1; aspect-ratio: 3 / 2; overflow: hidden; border-radius: 10px; background: var(--flaeche); margin-bottom: 10px; box-shadow: inset 0 0 0 1px var(--linie); }
.werk__bild svg { display: block; width: 100%; height: 100%; }
.werk__nr { display: none; }
.werk__name { font-size: 16.5px; font-weight: 560; letter-spacing: -.014em; line-height: 1.25; }
.werk__jahr { justify-self: end; align-self: baseline; font-size: 10px; color: var(--akzent); }
.werk__art { grid-column: 1 / -1; font-size: 14px; line-height: 1.45; color: var(--leise); }
.werke[data-ansicht="liste"] { grid-template-columns: 1fr; gap: 0; border-top: 1px solid var(--linie); }
.werke[data-ansicht="liste"] .werk { grid-template-columns: 3.4em minmax(0, 1fr) minmax(0, .9fr) 4.4em 60px; align-items: center; gap: 16px; padding: 12px 0; border-bottom: 1px solid var(--linie); }
.werke[data-ansicht="liste"] .werk__nr { display: block; grid-column: 1; grid-row: 1; }
.werke[data-ansicht="liste"] .werk__name { grid-column: 2; grid-row: 1; font-size: clamp(19px, 2.2vw, 26px); letter-spacing: -.022em; }
.werke[data-ansicht="liste"] .werk__art { grid-column: 3; grid-row: 1; }
.werke[data-ansicht="liste"] .werk__jahr { grid-column: 4; grid-row: 1; align-self: center; justify-self: start; }
.werke[data-ansicht="liste"] .werk__bild { grid-column: 5; grid-row: 1; width: 60px; margin: 0; border-radius: 5px; }
/* Produkt-Ausschnitte als SVG: Graustufen, ein Markenton, Statusfarbe nur im UI */
.ui text { font-family: 'Geist', system-ui, sans-serif; }
.ui .m { font-family: 'Geist Mono', ui-monospace, monospace; letter-spacing: .04em; }
.ui .bg { fill: var(--flaeche); }
.ui .fe { fill: #fff; stroke: #e1e1e5; }
.ui .g1 { fill: #ececef; } .ui .g2 { fill: #d5d6db; } .ui .g3 { fill: #a9acb3; }
.ui .ak { fill: var(--akzent); } .ui .akh { fill: var(--akzent-hell); }
.ui .tx { fill: var(--tinte); } .ui .tl { fill: #868a92; } .ui .ws { fill: #fff; }
.ui .wa { fill: var(--warn); }
/* ---------- Demo-Leiste: Variante und Werte ---------- */
.leiste { position: fixed; left: 0; right: 0; bottom: 0; z-index: 10; display: flex; justify-content: space-between; align-items: center; gap: 10px 24px; flex-wrap: wrap; padding: 12px clamp(16px, 3.4vw, 40px); background: rgba(250, 250, 250, .88); backdrop-filter: blur(10px); border-top: 1px solid var(--linie); }
.leiste b { color: var(--tinte); font-weight: 500; }
.modi { display: flex; gap: 4px; align-items: center; }
.modi button { font: inherit; letter-spacing: inherit; text-transform: inherit; color: var(--leise); background: var(--karte); border: 1px solid var(--linie); border-radius: 999px; height: 28px; padding: 0 12px; cursor: pointer; }
.modi button[aria-pressed="true"] { background: var(--tinte); color: var(--papier); border-color: var(--tinte); }
/* Vorführung: gezeichneter Zeiger */
.zeiger { position: fixed; left: 0; top: 0; z-index: 20; width: 18px; height: 18px; pointer-events: none; display: none; transition: scale .15s var(--kurve); }
.vorfuehrung .zeiger { display: block; }
.zeiger.drueckt { scale: .8; }
.vorschau body { overflow: hidden; }
.vorschau .leiste { display: none; }
/* Reduzierte Bewegung: Indikatoren springen, Inhalte wechseln ohne Einflug */
.reduziert .nav__liste::before, .reduziert .tabs::after, .reduziert .seg__spur::before,
.reduziert .nav__liste a, .reduziert .nav__liste a::before, .reduziert .tabs button, .reduziert .seg span { transition: none !important; }
@media (max-width: 860px) {
.werke { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.kopf__ort { display: none; }
.kopf { grid-template-columns: 1fr auto; }
}
@media (max-width: 640px) {
.kopf { grid-template-columns: 1fr; gap: 14px; }
.nav__liste { justify-self: start; }
.nav__liste a { padding: 8px 11px; font-size: 14px; }
.steuer { border-bottom: 0; }
.tabs { width: 100%; justify-content: space-between; gap: 10px; border-bottom: 1px solid var(--linie); }
.tabs button { font-size: 14.5px; }
.neu { font-size: 12px; }
.seg { margin: 0; }
.werk__name { font-size: 15px; }
.werk__art { font-size: 13px; }
.werke[data-ansicht="liste"] .werk { grid-template-columns: 2.4em minmax(0, 1fr) 54px; gap: 12px; }
.werke[data-ansicht="liste"] .werk__art, .werke[data-ansicht="liste"] .werk__jahr { display: none; }
.werke[data-ansicht="liste"] .werk__bild { grid-column: 3; width: 54px; }
.leiste { padding-block: 10px; }
.leiste .werte__lang { display: none; }
}
/* 320 px (WCAG 1.4.10): Pille und Tabs passen ohne Querscrollen */
@media (max-width: 360px) {
.nav__liste a { padding: 7px 8px; font-size: 13px; gap: 5px; }
.tabs { flex-wrap: wrap; justify-content: flex-start; column-gap: 16px; row-gap: 6px; }
}
@media (forced-colors: active) {
button, [role=button], [role=tab], [role=switch] { border: 1px solid ButtonText !important; }
}
</style>
</head>
<body>
<svg width="0" height="0" style="position:absolute" aria-hidden="true"><defs>
<filter id="ui-schatten" x="-30%" y="-30%" width="160%" height="180%"><feDropShadow dx="0" dy="4" stdDeviation="4" flood-color="#121316" flood-opacity=".16"/></filter>
</defs></svg>
<div class="seite">
<header class="kopf">
<a class="marke" href="#start" aria-label="Kadenz Startseite">
<svg viewBox="0 0 22 22" aria-hidden="true"><rect width="22" height="22" rx="6" fill="#4b45d9"/><rect x="5" y="5.5" width="8" height="2.6" rx="1.3" fill="#fff"/><rect x="7.5" y="9.7" width="9.5" height="2.6" rx="1.3" fill="#fff" opacity=".75"/><rect x="5" y="13.9" width="6" height="2.6" rx="1.3" fill="#fff" opacity=".5"/></svg>
Kadenz
</a>
<nav aria-label="Hauptnavigation">
<ul class="nav__liste" data-indikator="nav">
<li><a href="#funktionen" aria-current="page">Funktionen</a></li>
<li><a href="#preise">Preise</a></li>
<li><a href="#changelog">Changelog</a></li>
<li><a href="#kunden">Kunden</a></li>
</ul>
</nav>
<div class="kopf__ort"><a class="anmelden" href="#anmelden">Anmelden</a><a class="knopf" href="#testen">Kostenlos testen</a></div>
</header>
<main class="intro">
<a class="neu" href="#changelog"><b>Neu · 4.2</b><span>Schichttausch per Push-Freigabe</span></a>
<div class="intro__unten">
<h1 class="titel"><b>Dienstpläne in Minuten.</b> <span>Nicht am Sonntagabend.</span></h1>
<p class="intro__text">Kadenz plant Schichten nach Verfügbarkeit, Qualifikation und Arbeitszeitgesetz. Für Pflege, Gastronomie und Handel ab fünf Mitarbeitenden.</p>
</div>
</main>
<section class="arbeiten" aria-label="Funktionen">
<div class="intro__zeile"><p class="mono">Funktionen in der Reihenfolge der Woche</p><p class="mono" data-zaehler>08 Funktionen</p></div>
<div class="steuer">
<div class="tabs" role="tablist" aria-label="Funktionen filtern" data-indikator="tabs">
<button type="button" role="tab" id="tab-alle" aria-selected="true" aria-controls="werke" data-filter="alle">Alle<sup>08</sup></button>
<button type="button" role="tab" id="tab-planen" aria-selected="false" aria-controls="werke" data-filter="planen" tabindex="-1">Planen<sup>03</sup></button>
<button type="button" role="tab" id="tab-team" aria-selected="false" aria-controls="werke" data-filter="team" tabindex="-1">Team<sup>03</sup></button>
<button type="button" role="tab" id="tab-lohn" aria-selected="false" aria-controls="werke" data-filter="lohn" tabindex="-1">Zeit & Lohn<sup>02</sup></button>
</div>
<fieldset class="seg">
<legend class="mono">Ansicht</legend>
<div class="seg__spur" data-indikator="seg">
<label><input type="radio" name="ansicht" value="raster" checked><span>Karten</span></label>
<label><input type="radio" name="ansicht" value="liste"><span>Liste</span></label>
</div>
</fieldset>
</div>
<div class="werke" id="werke" role="tabpanel" aria-labelledby="tab-alle" data-ansicht="raster">
<article class="werk" data-art="planen">
<figure class="werk__bild"><svg class="ui" viewBox="0 0 300 200" role="img" aria-label="Wochenplan, eine Schicht wird auf Freitag gezogen">
<rect class="bg" width="300" height="200"/>
<rect class="fe" x="24.5" y="24.5" width="251" height="190" rx="8"/>
<text class="tx" x="38" y="46" font-size="10" font-weight="600">KW 46</text>
<text class="tl m" x="262" y="46" font-size="6.5" text-anchor="end">KÜCHE · 4 PERSONEN</text>
<g class="tl m" font-size="6" text-anchor="middle"><text x="84" y="64">MO</text><text x="111" y="64">DI</text><text x="138" y="64">MI</text><text x="165" y="64">DO</text><text x="192" y="64">FR</text><text x="219" y="64">SA</text><text x="246" y="64">SO</text></g>
<rect class="g1" x="34" y="70" width="232" height="1"/>
<circle class="g2" cx="47" cy="87" r="7"/><circle class="g3" cx="47" cy="111" r="7"/><circle class="akh" cx="47" cy="135" r="7"/><circle class="g2" cx="47" cy="159" r="7"/>
<g class="g1"><rect x="73" y="78" width="23" height="18" rx="3"/><rect x="100" y="78" width="23" height="18" rx="3"/><rect x="127" y="78" width="23" height="18" rx="3"/><rect x="181" y="78" width="23" height="18" rx="3"/>
<rect x="208" y="102" width="23" height="18" rx="3"/>
<rect x="73" y="126" width="23" height="18" rx="3"/><rect x="127" y="126" width="23" height="18" rx="3"/><rect x="154" y="126" width="23" height="18" rx="3"/><rect x="235" y="126" width="23" height="18" rx="3"/>
<rect x="73" y="150" width="23" height="18" rx="3"/><rect x="100" y="150" width="23" height="18" rx="3"/></g>
<rect class="akh" x="100" y="102" width="23" height="18" rx="3"/><rect class="akh" x="154" y="102" width="23" height="18" rx="3"/>
<g fill="#4b45d9" class="m" font-size="5.6" text-anchor="middle"><text x="111.5" y="113">14–22</text><text x="165.5" y="113">14–22</text></g>
<rect x="181.5" y="150.5" width="22" height="17" rx="3" fill="none" stroke="#4b45d9" stroke-width="1" stroke-dasharray="2.5 2"/>
<g transform="translate(187 142) rotate(-5)" filter="url(#ui-schatten)"><rect class="ak" width="25" height="19" rx="3"/><text class="ws m" x="12.5" y="12.4" font-size="5.8" text-anchor="middle">06–14</text></g>
<path d="M207 156.5 217.5 162.6l-4.7 1-2.3 4.4Z" fill="#121316" stroke="#fff" stroke-width="1" stroke-linejoin="round"/>
</svg></figure>
<span class="mono werk__nr">01</span><h2 class="werk__name">Wochenplan per Ziehen</h2><p class="mono werk__jahr">Start</p><p class="werk__art">Planen · Schichten verschieben statt neu tippen</p>
</article>
<article class="werk" data-art="planen">
<figure class="werk__bild"><svg class="ui" viewBox="0 0 300 200" role="img" aria-label="Offene Schichten mit Besetzungsvorschlag">
<rect class="bg" width="300" height="200"/>
<rect class="fe" x="24.5" y="24.5" width="251" height="190" rx="8"/>
<text class="tx" x="38" y="46" font-size="10" font-weight="600">Offene Schichten</text>
<rect class="ak" x="198" y="35" width="64" height="16" rx="8"/><text class="ws" x="230" y="45.6" font-size="7" font-weight="500" text-anchor="middle">Vorschlag</text>
<text class="tl m" x="38" y="67" font-size="6">38 / 42 BESETZT</text><text class="tl m" x="262" y="67" font-size="6" text-anchor="end">KW 46</text>
<rect class="g1" x="38" y="72" width="224" height="4" rx="2"/><rect class="ak" x="38" y="72" width="203" height="4" rx="2"/>
<g><rect x="34" y="88" width="232" height="26" rx="6" fill="#f6f6f8"/><text class="tx m" x="44" y="104" font-size="6.6">SA 06:00–14:00</text><text class="tl" x="128" y="104" font-size="7.5">Service</text><circle class="g2" cx="224" cy="101" r="7"/><circle class="ak" cx="248" cy="101" r="7"/><path d="m244.6 101.2 2.3 2.3 4.4-4.6" fill="none" stroke="#fff" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/></g>
<g><rect x="34" y="120" width="232" height="26" rx="6" fill="#f6f6f8"/><text class="tx m" x="44" y="136" font-size="6.6">SA 14:00–22:00</text><text class="tl" x="128" y="136" font-size="7.5">Küche</text><circle class="g3" cx="224" cy="133" r="7"/><circle class="ak" cx="248" cy="133" r="7"/><path d="m244.6 133.2 2.3 2.3 4.4-4.6" fill="none" stroke="#fff" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/></g>
<g><rect x="34" y="152" width="232" height="26" rx="6" fill="#f6f6f8"/><text class="tx m" x="44" y="168" font-size="6.6">SO 06:00–14:00</text><text class="tl" x="128" y="168" font-size="7.5">Service</text><circle cx="224" cy="165" r="6.5" fill="none" stroke="#a9acb3" stroke-dasharray="2 2"/><circle cx="248" cy="165" r="6.5" fill="none" stroke="#4b45d9"/></g>
</svg></figure>
<span class="mono werk__nr">02</span><h2 class="werk__name">Automatische Besetzung</h2><p class="mono werk__jahr">Pro</p><p class="werk__art">Planen · füllt offene Schichten nach Qualifikation</p>
</article>
<article class="werk" data-art="planen">
<figure class="werk__bild"><svg class="ui" viewBox="0 0 300 200" role="img" aria-label="Warnung: Ruhezeit unter elf Stunden">
<rect class="bg" width="300" height="200"/>
<rect class="fe" x="24.5" y="24.5" width="251" height="190" rx="8"/>
<text class="tx" x="38" y="46" font-size="10" font-weight="600">Ruhezeit-Prüfung</text>
<text class="tl m" x="262" y="46" font-size="6.5" text-anchor="end">ARBZG § 5</text>
<rect class="g1" x="38" y="70" width="224" height="16" rx="4"/>
<rect class="g2" x="53" y="70" width="67" height="16" rx="4"/>
<rect x="194" y="70.5" width="60" height="15" rx="4" fill="#fbeedb" stroke="#d9902a"/>
<g class="tl m" font-size="5.8"><text x="53" y="98">DO 14</text><text x="120" y="98" text-anchor="end">23</text><text x="194" y="98">FR 08</text><text x="254" y="98" text-anchor="end">16</text></g>
<path d="M121 108v5h72v-5" fill="none" stroke="#d9902a" stroke-width="1.2"/>
<text class="wa m" x="157" y="124" font-size="8" font-weight="600" text-anchor="middle">9 h</text>
<rect x="34.5" y="138.5" width="231" height="44" rx="7" fill="#fdf7ee" stroke="#f0dcb8"/>
<circle class="wa" cx="50" cy="160" r="4"/>
<text class="tx" x="62" y="157" font-size="8" font-weight="600">Ruhezeit unter 11 Stunden</text>
<text class="tl" x="62" y="169" font-size="7">Frühdienst am Freitag auf 10:00 verschieben?</text>
</svg></figure>
<span class="mono werk__nr">03</span><h2 class="werk__name">Ruhezeit-Prüfung</h2><p class="mono werk__jahr">Start</p><p class="werk__art">Planen · warnt vor Verstößen, bevor der Plan raus ist</p>
</article>
<article class="werk" data-art="team">
<figure class="werk__bild"><svg class="ui" viewBox="0 0 300 200" role="img" aria-label="Schichttausch zwischen zwei Mitarbeitenden">
<rect class="bg" width="300" height="200"/>
<rect class="fe" x="28.5" y="34.5" width="104" height="108" rx="9"/>
<circle class="g2" cx="50" cy="58" r="10"/><text class="tx" x="66" y="57" font-size="8.5" font-weight="600">Lena</text><text class="tl m" x="66" y="67" font-size="5.6">SERVICE</text>
<rect class="g1" x="40" y="84" width="80" height="22" rx="5"/><text class="tx m" x="80" y="97.6" font-size="7" text-anchor="middle">FR 14–22</text>
<text class="tl" x="40" y="128" font-size="7">gibt ab</text>
<rect class="fe" x="167.5" y="34.5" width="104" height="108" rx="9"/>
<circle class="g3" cx="189" cy="58" r="10"/><text class="tx" x="205" y="57" font-size="8.5" font-weight="600">Jonas</text><text class="tl m" x="205" y="67" font-size="5.6">SERVICE</text>
<rect class="akh" x="179" y="84" width="80" height="22" rx="5"/><text class="m" fill="#4b45d9" x="219" y="97.6" font-size="7" text-anchor="middle">FR 14–22</text>
<text class="tl" x="179" y="128" font-size="7">übernimmt</text>
<g filter="url(#ui-schatten)"><circle class="ak" cx="150" cy="95" r="13"/></g>
<path d="M143.5 91h12.5l-3-3M156.5 99H144l3 3" fill="none" stroke="#fff" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<rect class="tx" x="28" y="156" width="244" height="26" rx="7"/><text class="ws" x="150" y="172" font-size="8" font-weight="500" text-anchor="middle">Tausch freigeben</text>
</svg></figure>
<span class="mono werk__nr">04</span><h2 class="werk__name">Schichttausch</h2><p class="mono werk__jahr">Team</p><p class="werk__art">Team · tauschen ohne Rundmail und Rückruf</p>
</article>
<article class="werk" data-art="team">
<figure class="werk__bild"><svg class="ui" viewBox="0 0 300 200" role="img" aria-label="Kalender mit Verfügbarkeiten im November">
<rect class="bg" width="300" height="200"/>
<rect class="fe" x="24.5" y="24.5" width="251" height="190" rx="8"/>
<text class="tx" x="38" y="46" font-size="10" font-weight="600">November</text>
<g class="m" font-size="5.6"><rect class="akh" x="166" y="40" width="7" height="7" rx="2"/><text class="tl" x="176" y="46">FREI</text><circle class="ak" cx="206" cy="43.5" r="2.4"/><text class="tl" x="212" y="46">WUNSCH</text><rect class="g1" x="243" y="40" width="7" height="7" rx="2"/><text class="tl" x="253" y="46">–</text></g>
<text class="tl m" x="45" y="69" font-size="6" opacity=".45">26</text><text class="tl m" x="77" y="69" font-size="6" opacity=".45">27</text><text class="tl m" x="109" y="69" font-size="6" opacity=".45">28</text><text class="tl m" x="141" y="69" font-size="6" opacity=".45">29</text><text class="tl m" x="173" y="69" font-size="6" opacity=".45">30</text><text class="tl m" x="205" y="69" font-size="6" opacity=".45">31</text><rect class="g1" x="233" y="60" width="30" height="21" rx="4" stroke="var(--akzent)" stroke-width="1.2"/><text class="tl m" x="237" y="69" font-size="6">1</text><rect class="akh" x="41" y="84" width="30" height="21" rx="4"/><text class="tx m" x="45" y="93" font-size="6">2</text><rect class="akh" x="73" y="84" width="30" height="21" rx="4"/><text class="tx m" x="77" y="93" font-size="6">3</text><rect class="akh" x="105" y="84" width="30" height="21" rx="4"/><text class="tx m" x="109" y="93" font-size="6">4</text><rect class="g1" x="137" y="84" width="30" height="21" rx="4"/><text class="tl m" x="141" y="93" font-size="6">5</text><rect class="akh" x="169" y="84" width="30" height="21" rx="4"/><text class="tx m" x="173" y="93" font-size="6">6</text><circle class="ak" cx="193" cy="99" r="2.4"/><rect class="g1" x="201" y="84" width="30" height="21" rx="4"/><text class="tl m" x="205" y="93" font-size="6">7</text><rect class="g1" x="233" y="84" width="30" height="21" rx="4"/><text class="tl m" x="237" y="93" font-size="6">8</text><rect class="akh" x="41" y="108" width="30" height="21" rx="4"/><text class="tx m" x="45" y="117" font-size="6">9</text><rect class="akh" x="73" y="108" width="30" height="21" rx="4"/><text class="tx m" x="77" y="117" font-size="6">10</text><rect class="akh" x="105" y="108" width="30" height="21" rx="4"/><text class="tx m" x="109" y="117" font-size="6">11</text><rect class="akh" x="137" y="108" width="30" height="21" rx="4"/><text class="tx m" x="141" y="117" font-size="6">12</text><rect class="akh" x="169" y="108" width="30" height="21" rx="4"/><text class="tx m" x="173" y="117" font-size="6">13</text><circle class="ak" cx="193" cy="123" r="2.4"/><rect class="g1" x="201" y="108" width="30" height="21" rx="4"/><text class="tl m" x="205" y="117" font-size="6">14</text><rect class="g1" x="233" y="108" width="30" height="21" rx="4"/><text class="tl m" x="237" y="117" font-size="6">15</text><rect class="akh" x="41" y="132" width="30" height="21" rx="4"/><text class="tx m" x="45" y="141" font-size="6">16</text><rect class="akh" x="73" y="132" width="30" height="21" rx="4"/><text class="tx m" x="77" y="141" font-size="6">17</text><rect class="akh" x="105" y="132" width="30" height="21" rx="4"/><text class="tx m" x="109" y="141" font-size="6">18</text><rect class="g1" x="137" y="132" width="30" height="21" rx="4"/><text class="tl m" x="141" y="141" font-size="6">19</text><rect class="akh" x="169" y="132" width="30" height="21" rx="4"/><text class="tx m" x="173" y="141" font-size="6">20</text><circle class="ak" cx="193" cy="147" r="2.4"/><rect class="g1" x="201" y="132" width="30" height="21" rx="4"/><text class="tl m" x="205" y="141" font-size="6">21</text><rect class="g1" x="233" y="132" width="30" height="21" rx="4"/><text class="tl m" x="237" y="141" font-size="6">22</text><rect class="akh" x="41" y="156" width="30" height="21" rx="4"/><text class="tx m" x="45" y="165" font-size="6">23</text><rect class="akh" x="73" y="156" width="30" height="21" rx="4"/><text class="tx m" x="77" y="165" font-size="6">24</text><rect class="akh" x="105" y="156" width="30" height="21" rx="4"/><text class="tx m" x="109" y="165" font-size="6">25</text><rect class="akh" x="137" y="156" width="30" height="21" rx="4"/><text class="tx m" x="141" y="165" font-size="6">26</text><rect class="akh" x="169" y="156" width="30" height="21" rx="4"/><text class="tx m" x="173" y="165" font-size="6">27</text><circle class="ak" cx="193" cy="171" r="2.4"/><rect class="g1" x="201" y="156" width="30" height="21" rx="4"/><text class="tl m" x="205" y="165" font-size="6">28</text><rect class="g1" x="233" y="156" width="30" height="21" rx="4"/><text class="tl m" x="237" y="165" font-size="6">29</text>
</svg></figure>
<span class="mono werk__nr">05</span><h2 class="werk__name">Verfügbarkeiten</h2><p class="mono werk__jahr">Start</p><p class="werk__art">Team · Wünsche bis zum 20. des Vormonats</p>
</article>
<article class="werk" data-art="team">
<figure class="werk__bild"><svg class="ui" viewBox="0 0 300 200" role="img" aria-label="Telefon mit Benachrichtigung über den neuen Wochenplan">
<rect class="bg" width="300" height="200"/>
<rect class="tx" x="102" y="24" width="96" height="196" rx="17"/>
<rect x="106" y="28" width="88" height="190" rx="13" fill="#f4f4f6"/>
<rect class="tx" x="137" y="34" width="26" height="7" rx="3.5"/>
<text class="tx" x="150" y="70" font-size="15" font-weight="600" text-anchor="middle">06:12</text>
<text class="tl" x="150" y="80" font-size="5.6" text-anchor="middle">Sonntag, 1. November</text>
<g filter="url(#ui-schatten)"><rect class="fe" x="60.5" y="94.5" width="179" height="50" rx="10"/></g>
<rect class="ak" x="71" y="105" width="16" height="16" rx="4.5"/><rect class="ws" x="74.6" y="108.6" width="5.8" height="1.9" rx=".95"/><rect class="ws" x="76.4" y="112.1" width="6.9" height="1.9" rx=".95" opacity=".75"/><rect class="ws" x="74.6" y="115.6" width="4.4" height="1.9" rx=".95" opacity=".5"/>
<text class="tl m" x="94" y="109" font-size="5.6">LUMIO</text><text class="tl" x="229" y="109" font-size="6" text-anchor="end">jetzt</text>
<text class="tx" x="94" y="122" font-size="8" font-weight="600">Dein Plan für KW 46 ist da</text>
<text class="tl" x="94" y="134" font-size="7">3 Schichten · 24 Stunden</text>
<rect x="72.5" y="150.5" width="155" height="32" rx="9" fill="#fbfbfc" stroke="#e1e1e5"/>
<text class="tx" x="84" y="164" font-size="7" font-weight="600">Lena möchte tauschen</text>
<text class="tl m" x="84" y="174" font-size="5.6">FR 14–22 · SERVICE</text>
</svg></figure>
<span class="mono werk__nr">06</span><h2 class="werk__name">Team-App mit Push</h2><p class="mono werk__jahr">Team</p><p class="werk__art">Team · der Plan landet auf jedem Telefon</p>
</article>
<article class="werk" data-art="lohn">
<figure class="werk__bild"><svg class="ui" viewBox="0 0 300 200" role="img" aria-label="Stempeluhr mit Knopf Einstempeln">
<rect class="bg" width="300" height="200"/>
<rect class="fe" x="24.5" y="24.5" width="251" height="190" rx="8"/>
<text class="tl m" x="38" y="46" font-size="6.2">FRÜHDIENST · STATION 2</text>
<text class="tx m" x="36" y="96" font-size="38" font-weight="500" letter-spacing="-1.5">07:58</text>
<text class="tl" x="38" y="113" font-size="7.5">Schichtbeginn 08:00 · Pflege</text>
<rect class="ak" x="38" y="130" width="104" height="28" rx="14"/><text class="ws" x="90" y="147.2" font-size="8.5" font-weight="500" text-anchor="middle">Einstempeln</text>
<circle cx="164" cy="144" r="3" fill="#2f9e6a"/><text class="tl" x="172" y="146.5" font-size="7">Standort bestätigt</text>
<circle cx="230" cy="80" r="23" fill="none" stroke="#ececef" stroke-width="5"/>
<circle cx="230" cy="80" r="23" fill="none" stroke="#4b45d9" stroke-width="5" stroke-linecap="round" stroke-dasharray="118 144.5" transform="rotate(-90 230 80)"/>
<text class="tx m" x="230" y="80" font-size="8" font-weight="600" text-anchor="middle">31,5</text>
<text class="tl m" x="230" y="89" font-size="5.2" text-anchor="middle">/ 38,5 H</text>
</svg></figure>
<span class="mono werk__nr">07</span><h2 class="werk__name">Stempeluhr</h2><p class="mono werk__jahr">Team</p><p class="werk__art">Zeit & Lohn · per App oder Tablet am Eingang</p>
</article>
<article class="werk" data-art="lohn">
<figure class="werk__bild"><svg class="ui" viewBox="0 0 300 200" role="img" aria-label="Lohnexport mit Stunden und Zuschlägen für Oktober">
<rect class="bg" width="300" height="200"/>
<rect class="fe" x="24.5" y="24.5" width="251" height="190" rx="8"/>
<text class="tx" x="38" y="46" font-size="10" font-weight="600">Oktober 2026</text>
<rect x="216.5" y="35.5" width="46" height="15" rx="4" fill="#fff" stroke="#d5d6db"/><text class="tx m" x="239.5" y="45.4" font-size="5.8" text-anchor="middle">EXPORT</text>
<g class="tl m" font-size="5.6"><text x="38" y="67">NAME</text><text x="166" y="67" text-anchor="end">STD.</text><text x="212" y="67" text-anchor="end">ZUSCHL.</text><text x="262" y="67" text-anchor="end">GESAMT</text></g>
<rect class="g1" x="38" y="72" width="224" height="1"/>
<g font-size="7"><text class="tx" x="38" y="87">A. Krüger</text><text class="tx" x="38" y="105">M. Yilmaz</text><text class="tx" x="38" y="123">S. Becker</text><text class="tx" x="38" y="141">J. Nowak</text></g>
<g class="tx m" font-size="6.8" text-anchor="end">
<text x="166" y="87">162,5</text><text x="212" y="87">148,20 €</text><text x="262" y="87">2.910,70 €</text>
<text x="166" y="105">138,0</text><text x="212" y="105">96,40 €</text><text x="262" y="105">2.373,40 €</text>
<text x="166" y="123">96,5</text><text x="212" y="123">42,10 €</text><text x="262" y="123">1.489,60 €</text>
<text x="166" y="141">120,0</text><text x="212" y="141">211,80 €</text><text x="262" y="141">2.131,80 €</text>
</g>
<g class="g1"><rect x="38" y="92" width="224" height="1"/><rect x="38" y="110" width="224" height="1"/><rect x="38" y="128" width="224" height="1"/></g>
<rect class="g2" x="38" y="148" width="224" height="1"/>
<text class="tx" x="38" y="163" font-size="7" font-weight="600">Summe</text>
<g class="tx m" font-size="6.8" font-weight="600" text-anchor="end"><text x="166" y="163">517,0</text><text x="212" y="163">498,50 €</text><text fill="#4b45d9" x="262" y="163">8.905,50 €</text></g>
</svg></figure>
<span class="mono werk__nr">08</span><h2 class="werk__name">Lohnexport</h2><p class="mono werk__jahr">Pro</p><p class="werk__art">Zeit & Lohn · Stunden und Zuschläge für die Lohnbuchhaltung</p>
</article>
</div>
</section>
</div>
<div class="leiste mono">
<p data-werte><b>250 ms</b> · cubic-bezier(.215,.61,.355,1)<span class="werte__lang"> · Pille +0,5rem · Schatten 0 12px 31px #0000001f</span> · <span data-technik>Anchor Positioning</span></p>
<div class="modi" role="group" aria-label="Bewegung des Indikators">
<button type="button" data-modus="gleiten" aria-pressed="true">Gleiten</button>
<button type="button" data-modus="dehnen" aria-pressed="false">Dehnen</button>
</div>
</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="#121316" stroke="#ffffff" stroke-width="1.2" stroke-linejoin="round"/></svg>
<script>
(() => {
const html = document.documentElement;
const reduziert = () => html.classList.contains('reduziert');
const ANKER = CSS.supports('position-anchor: --a');
const KANN_HOVER = matchMedia('(hover: hover)');
const KURVE = 'cubic-bezier(0.215, 0.61, 0.355, 1)';
const nav = document.querySelector('[data-indikator="nav"]');
const tabs = document.querySelector('[data-indikator="tabs"]');
const spur = document.querySelector('[data-indikator="seg"]');
const navLinks = [...nav.querySelectorAll('a')];
const tabKnoepfe = [...tabs.querySelectorAll('[role="tab"]')];
const radios = [...spur.querySelectorAll('input')];
const werke = document.getElementById('werke');
const alleWerke = [...werke.querySelectorAll('.werk')];
const zaehler = document.querySelector('[data-zaehler]');
// Welches Element trägt gerade den Indikator? (dieselbe Logik wie im CSS)
const ziele = {
nav: () => nav.querySelector('a.zeigt') || (KANN_HOVER.matches && nav.querySelector('a:hover, a:focus-visible')) || nav.querySelector('[aria-current="page"]'),
tabs: () => tabs.querySelector('[aria-selected="true"]'),
seg: () => spur.querySelector('input:checked').parentElement,
};
const reihen = { nav: navLinks, tabs: tabKnoepfe, seg: [...spur.querySelectorAll('label')] };
const letztes = new Map();
// Richtung für „Dehnen“ setzen; ohne Anchor Positioning zusätzlich messen
function nachfuehren(box) {
const ziel = ziele[box.dataset.indikator]();
const vorher = letztes.get(box);
// Reihenfolge im DOM statt Messung: kein Layout lesen, bevor die Richtung steht
// (sonst startet die Transition noch mit der alten Richtung)
if (vorher && vorher !== ziel) {
const reihe = reihen[box.dataset.indikator];
box.dataset.richtung = reihe.indexOf(ziel) > reihe.indexOf(vorher) ? 'rechts' : 'links';
}
letztes.set(box, ziel);
if (!ANKER) {
const c = box.getBoundingClientRect(), r = ziel.getBoundingClientRect();
box.style.setProperty('--l', (r.left - c.left) + 'px');
box.style.setProperty('--r', (c.right - r.right) + 'px');
box.style.setProperty('--t', (r.top - c.top) + 'px');
box.style.setProperty('--b', (c.bottom - r.bottom) + 'px');
}
}
const alle = () => [nav, tabs, spur].forEach(nachfuehren);
// Navigation: Hover-Pille folgt dem Zeiger, kehrt beim Verlassen zum aktuellen Punkt zurück
navLinks.forEach(a => {
a.addEventListener('pointerover', () => nachfuehren(nav));
a.addEventListener('focus', () => requestAnimationFrame(() => nachfuehren(nav)));
a.addEventListener('blur', () => requestAnimationFrame(() => nachfuehren(nav)));
a.addEventListener('click', e => { // Demo: Seitenwechsel als Zustand (SPA)
e.preventDefault();
navLinks.forEach(x => x.toggleAttribute('aria-current', x === a));
a.setAttribute('aria-current', 'page');
nachfuehren(nav);
});
});
nav.addEventListener('pointerleave', () => nachfuehren(nav));
// Tabs: Unterstrich wandert, Funktionen werden gefiltert
function waehleTab(knopf, fokus) {
if (knopf.getAttribute('aria-selected') === 'true') return;
tabKnoepfe.forEach(t => { const an = t === knopf; t.setAttribute('aria-selected', String(an)); t.tabIndex = an ? 0 : -1; });
werke.setAttribute('aria-labelledby', knopf.id);
nachfuehren(tabs);
if (fokus) knopf.focus();
filtern(knopf.dataset.filter);
}
tabKnoepfe.forEach((t, i) => {
t.addEventListener('click', () => waehleTab(t));
t.addEventListener('keydown', e => {
const n = { ArrowRight: 1, ArrowLeft: -1 }[e.key];
if (e.key === 'Home') { e.preventDefault(); waehleTab(tabKnoepfe[0], true); }
else if (e.key === 'End') { e.preventDefault(); waehleTab(tabKnoepfe.at(-1), true); }
else if (n) { e.preventDefault(); waehleTab(tabKnoepfe[(i + n + tabKnoepfe.length) % tabKnoepfe.length], true); }
});
});
// Segment-Schalter: native Radios, Pille gleitet zum gewählten Label
radios.forEach(r => r.addEventListener('change', () => {
nachfuehren(spur);
werke.dataset.ansicht = r.value;
einblenden();
}));
function filtern(art) {
let n = 0;
alleWerke.forEach(w => { const zeigen = art === 'alle' || w.dataset.art === art; w.hidden = !zeigen; if (zeigen) n++; });
zaehler.textContent = String(n).padStart(2, '0') + (n === 1 ? ' Funktion' : ' Funktionen');
einblenden();
}
// Inhalte kommen weich nach: 500 ms (gemessene Deckkraft-Dauer), 40 ms Versatz
function einblenden() {
if (reduziert()) return;
alleWerke.filter(w => !w.hidden).forEach((w, i) => w.animate(
[{ opacity: 0, transform: 'translateY(10px)' }, { opacity: 1, transform: 'none' }],
{ duration: 500, delay: i * 40, easing: KURVE, fill: 'backwards' }));
}
// Variante: Gleiten (gemessen) oder Dehnen (führende Kante zuerst)
const modusKnoepfe = [...document.querySelectorAll('[data-modus]')];
let gewaehlt = 'gleiten';
function modus(m) {
html.classList.toggle('dehnen', m === 'dehnen');
modusKnoepfe.forEach(b => b.setAttribute('aria-pressed', String(b.dataset.modus === m)));
document.querySelector('[data-werte] b').textContent = reduziert() ? 'Reduziert: springt ohne Transition' : m === 'dehnen' ? '250 ms, folgende Kante +120 ms' : '250 ms';
}
addEventListener('load', () => modus(gewaehlt)); // ?reduziert setzt die Klasse erst nach diesem Skript
modusKnoepfe.forEach(b => b.addEventListener('click', () => { gewaehlt = b.dataset.modus; modus(gewaehlt); }));
if (!ANKER) document.querySelector('[data-technik]').textContent = 'JS-Messung (kein Anchor Positioning)';
alle();
if (!ANKER) { new ResizeObserver(alle).observe(document.body); document.fonts.ready.then(alle); }
// ---------- Vorführung: ein gedachter Zeiger fährt Navigation, Tabs und Schalter ab ----------
const zeiger = document.querySelector('[data-zeiger]');
const mitte = el => { const r = el.getBoundingClientRect(); return { x: r.left + r.width * 0.52, y: r.top + r.height * 0.58 }; };
const draussen = () => ({ x: innerWidth * 0.78, y: Math.min(innerHeight * 0.36, mitte(document.querySelector('.titel')).y) });
const PLAN = [ // [Zeitpunkt ms, Ziel, Aktion]
[400, () => navLinks[1]], [1150, () => navLinks[2]], [1900, () => navLinks[3]], [2600, () => navLinks[1]],
[3400, draussen],
[4300, () => tabKnoepfe[1], 'klick'], [5300, () => tabKnoepfe[3], 'klick'], [6300, () => tabKnoepfe[2], 'klick'],
[7400, () => spur.querySelectorAll('label')[1], 'klick'], [8600, () => spur.querySelectorAll('label')[0], 'klick'],
[9600, () => tabKnoepfe[0], 'klick'], [10600, draussen],
];
const RUNDE = 11400, WEG = 520;
let uhr = null, t0 = 0, angehalten = 0, schritt = -1, runde = 0, pos = { x: 0, y: 0 }, von = { x: 0, y: 0 };
const sanft = p => p < .5 ? 4 * p * p * p : 1 - Math.pow(-2 * p + 2, 3) / 2;
const punkt = f => { const v = f(); return v instanceof Element ? mitte(v) : v; };
function hoverAn(x, y) {
const el = document.elementFromPoint(x, y);
const a = el && el.closest('.nav__liste a');
navLinks.forEach(l => l.classList.toggle('zeigt', l === a));
nachfuehren(nav);
}
function rahmen(t) {
const zeit = (t - t0) % RUNDE, r = Math.floor((t - t0) / RUNDE);
if (r !== runde) { runde = r; schritt = -1; modus(r % 2 ? 'dehnen' : 'gleiten'); }
// nächsten Wegpunkt bestimmen
let i = -1; PLAN.forEach((p, k) => { if (zeit >= p[0] - WEG) i = k; });
if (i >= 0) {
const [ankunft, ziel, aktion] = PLAN[i];
if (i !== schritt) { schritt = i; von = { ...pos }; PLAN[i].fertig = false; }
const p = Math.min(1, Math.max(0, (zeit - (ankunft - WEG)) / WEG));
const z = punkt(ziel);
pos = { x: von.x + (z.x - von.x) * sanft(p), y: von.y + (z.y - von.y) * sanft(p) };
if (p >= 1 && !PLAN[i].fertig) {
PLAN[i].fertig = true;
if (aktion === 'klick') {
const el = ziel(); zeiger.classList.add('drueckt'); setTimeout(() => zeiger.classList.remove('drueckt'), 160);
(el.querySelector('input') || el).click();
}
}
}
zeiger.style.transform = `translate(${pos.x - 2}px, ${pos.y - 1}px)`;
hoverAn(pos.x, pos.y);
uhr = requestAnimationFrame(rahmen);
}
function grundzustand() {
navLinks.forEach(l => l.classList.remove('zeigt'));
waehleTab(tabKnoepfe[0]);
if (!radios[0].checked) radios[0].click();
alle();
}
function vorfuehren() {
pause(); grundzustand();
html.classList.add('vorfuehrung');
pos = draussen(); von = { ...pos }; schritt = -1; runde = 0; modus('gleiten');
t0 = performance.now();
uhr = requestAnimationFrame(rahmen);
}
function pause() { if (uhr) angehalten = performance.now() - t0; cancelAnimationFrame(uhr); uhr = null; }
function weiter() { if (uhr || !html.classList.contains('vorfuehrung')) return; t0 = performance.now() - angehalten; uhr = requestAnimationFrame(rahmen); }
function neustart() { pause(); html.classList.remove('vorfuehrung'); grundzustand(); modus(gewaehlt); }
})();
</script>
</body>
</html>