Unterstreichungs-Varianten
Linien, die sich beim Hover einzeichnen: links rein, rechts raus, aus der Mitte oder als Wiederkehr, im Fließtext über Zeilenumbrüche.
Editoriale Seiten, Studios, Magazine: die Grundvariante für Navigation und Fließtext, ruhig und klar gerichtet.
Links tragen eine feine Linie. Beim Hover zeichnet sie sich ein, meist von links nach rechts. Beim Verlassen läuft sie nach rechts hinaus, weil transform-origin wechselt. Andere Varianten wachsen aus der Mitte, oder eine sichtbare Linie läuft hinaus und eine zweite kommt nach. Technisch geht das per Pseudo-Element, per Hintergrund-Verlauf oder nativ mit text-decoration-inset.
Der Fließtext bleibt ruhig, trotzdem gibt der Link klares Feedback. Die durchlaufende Richtung, links rein und rechts raus, wirkt bewusst gestaltet und nicht wie ein Browser-Standard.
Wann einsetzen
Links im Fließtext, Navigation, Footer und Inline-CTAs in editorialen Layouts.
Wann nicht
Pseudo-Elemente nicht für Links, die umbrechen können: Die Linie sitzt dann falsch. Dort Hintergrund-Verlauf oder text-decoration nehmen. Die Linie im Fließtext nie ganz entfernen, wenn Links sonst nur an der Farbe erkennbar wären.
Varianten
- Durchlauf: scaleX 0 → 1, rein mit origin links, raus mit origin rechts (Rejouice 600 ms, cubic-bezier(0.85, 0, 0.15, 1); Codrops 300 ms)
- Aus der Mitte: origin center, 2 px, 300 ms cubic-bezier(0.215, 0.61, 0.355, 1) (K72)
- Wiederkehr: sichtbare Linie läuft rechts hinaus (300 ms), zweite kommt von links (400 ms, 100 ms später) (Codrops)
- Zwei Linien gegenläufig oder Linie und Text verschieben sich gemeinsam (Codrops)
- Clip-Path teilt die Linie in der Mitte (Codrops)
- Ruhig: Linie halbtransparent, bei Hover und Fokus volle Farbe und dicker (color-mix)
- Kringel als SVG, per clip-path inset eingezeichnet (etwa 0,6 s)
- Hintergrund-Verlauf (background-size 0 → 100 %) für mehrzeilige Links
- Nativ: text-decoration-inset 0 100% → 0 0 (Chrome 154), dazu text-decoration-skip-spaces: all für wortweise Linien (Chrome 155); ohne Unterstützung steht die Linie
Umsetzung
Einzeilig: a { position: relative } a::after { content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 1px; background: currentColor; transform: scaleX(0); transform-origin: 100% 50%; transition: transform .6s cubic-bezier(.85,0,.15,1) } a:is(:hover, :focus-visible)::after { transform: scaleX(1); transform-origin: 0 50% }. Mehrzeilig: a > span { background: linear-gradient(currentColor, currentColor) no-repeat 100% 100% / 0% 1px; transition: background-size .6s cubic-bezier(.85,0,.15,1) } a:is(:hover, :focus-visible) > span { background-size: 100% 1px; background-position-x: 0 }. Die ruhige Grundlinie liegt als zweiter Verlauf auf dem a. Nativ und progressiv: a { text-decoration: underline 1px; text-underline-offset: .26em } und in @supports (text-decoration-inset: 0 100%) unter (hover: hover) und ohne reduzierte Bewegung text-decoration-inset von 0 100% auf 0 0 (Prozent beziehen sich auf die Breite der Box; Chrome 154, Stand 02.11.2026 nur Chrome). Für eine ruhige Grundlinie im Fließtext trägt das a die blasse Linie und ein inneres span die wachsende. text-decoration-skip-spaces: all (Chrome 155) unterstreicht Wort für Wort; nur setzen, wenn das gewollt ist, normale Links brauchen es nicht. Ohne Support bleibt die Linie stehen und wird bei Hover und Fokus kräftiger. Wie die Einrückung bei umbrochenen Links wirkt, beschreibt die Chrome-Doku nicht: im Zielbrowser prüfen.
Robust umsetzen
Telefon. Ohne Hover steht die Linie dauerhaft, gedämpft. Beim Antippen (:active) wird sie kurz kräftig und dicker.
Ohne Bewegung. Linie immer sichtbar. Hover und Fokus wechseln nur Farbe und Dicke, ohne Bewegung.
Barrierefrei. Links im Fließtext nicht nur über Farbe unterscheiden: Grundlinie behalten. Fokus zeigt dasselbe wie Hover (:focus-visible).
Code
<!DOCTYPE html>
<!--
Unterstreichungs-Varianten
Fundus · Hover · https://cecodesign.de/lab/fundus/hover/unterstreichung-varianten/
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>Unterstreichungs-Varianten · Fundus-Demo</title>
<script>document.documentElement.classList.add('js');if(matchMedia('(prefers-reduced-motion: reduce)').matches)document.documentElement.classList.add('reduziert')</script>
<style>
@font-face { font-family: 'Instrument Sans'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/instrument-sans:vf@latest/latin-wght-normal.woff2') format('woff2-variations'); font-weight: 400 700; font-display: swap; }
@font-face { font-family: 'Geist Mono'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/geist-mono:vf@latest/latin-wght-normal.woff2') format('woff2-variations'); font-weight: 100 900; font-display: swap; }
@font-face { font-family: 'Instrument Serif'; font-style: italic; src: url('https://cdn.jsdelivr.net/fontsource/fonts/instrument-serif@latest/latin-400-italic.woff2') format('woff2'); font-weight: 400; font-display: swap; }
:root {
--papier: #efece5; --tinte: #161513; --leise: rgba(22, 21, 19, .66); --haar: rgba(22, 21, 19, .14);
--grundlinie: rgba(22, 21, 19, .26); /* ruhige Grundlinie im Fließtext */
--akzent: #17818b; /* aktive Linie im Fließtext */
}
/* Die drei Varianten als Token-Sätze. ein = Hover/Fokus, aus = Verlassen. */
[data-variante="durchlauf"] { /* Rejouice: 600 ms circ.inOut, 1 px, links rein, rechts raus */
--dicke: 1px; --dauer-ein: .6s; --dauer-aus: .6s; --verz-ein: 0s;
--kurve-ein: cubic-bezier(0.85, 0, 0.15, 1); --kurve-aus: cubic-bezier(0.85, 0, 0.15, 1);
--ursprung-ein: 0%; --ursprung-aus: 100%;
}
[data-variante="mitte"] { /* K72: 300 ms, 2 px, wächst aus der Mitte */
--dicke: 2px; --dauer-ein: .3s; --dauer-aus: .3s; --verz-ein: 0s;
--kurve-ein: cubic-bezier(0.215, 0.61, 0.355, 1); --kurve-aus: cubic-bezier(0.215, 0.61, 0.355, 1);
--ursprung-ein: 50%; --ursprung-aus: 50%;
}
[data-variante="wiederkehr"] { /* Codrops: Linie läuft rechts hinaus (300 ms), neue kommt links (400 ms, +100 ms) */
--dicke: 1px; --dauer-ein: .4s; --dauer-aus: .3s; --verz-ein: .1s;
--kurve-ein: cubic-bezier(0.4, 1, 0.8, 1); --kurve-aus: cubic-bezier(0.7, 0, 0.2, 1);
--ursprung-ein: 0%; --ursprung-aus: 100%;
}
[data-variante="inset"] { /* Nativ: text-decoration-inset 0 100% → 0 0 (Chrome 154+), kein Pseudo-Element, kein Verlauf */
--dicke: 1px; --dauer-ein: .3s; --dauer-aus: .3s; --verz-ein: 0s;
--kurve-ein: cubic-bezier(0.215, 0.61, 0.355, 1); --kurve-aus: cubic-bezier(0.215, 0.61, 0.355, 1);
--ursprung-ein: 0%; --ursprung-aus: 0%;
}
* { box-sizing: border-box; margin: 0; }
html { background: var(--papier); color: var(--tinte); font-family: 'Instrument Sans', system-ui, sans-serif; -webkit-font-smoothing: antialiased; }
body { min-height: 100svh; display: grid; grid-template-columns: minmax(0, 1fr); grid-template-rows: auto 1fr auto auto; padding: clamp(18px, 3.4vw, 44px) clamp(16px, 4vw, 56px); gap: clamp(22px, 3.4vw, 44px); }
a { color: inherit; text-decoration: none; outline: none; }
.mono { font: 500 11px/1.3 'Geist Mono', ui-monospace, monospace; letter-spacing: .14em; text-transform: uppercase; color: var(--leise); }
em { font-family: 'Instrument Serif', Georgia, serif; font-style: italic; font-weight: 400; letter-spacing: -.01em; }
/* Kopf */
.kopf { display: flex; justify-content: space-between; align-items: baseline; gap: 16px 32px; flex-wrap: wrap; padding-bottom: 14px; border-bottom: 1px solid var(--haar); }
.marke { font-size: 17px; font-weight: 600; letter-spacing: -.02em; }
.marke span { font-weight: 400; color: var(--leise); }
.nav { display: flex; gap: clamp(16px, 2.6vw, 34px); list-style: none; padding: 0; font-size: 15px; }
.nav [aria-current] { color: var(--leise); }
/* ── Technik 1: Pseudo-Element mit scaleX (einzeilige Links: Navigation, CTA, Fuß) ── */
.u { position: relative; display: inline-block; padding-bottom: 3px; }
.u::before, .u::after {
content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: var(--dicke);
background: currentColor; pointer-events: none;
}
.u::before { display: none; }
.u::after {
transform: scaleX(0); transform-origin: var(--ursprung-aus) 50%;
transition: transform var(--dauer-aus) var(--kurve-aus);
}
.u:is(:hover, :focus-visible, .an)::after {
transform: scaleX(1); transform-origin: var(--ursprung-ein) 50%;
transition: transform var(--dauer-ein) var(--kurve-ein) var(--verz-ein);
}
/* Wiederkehr: sichtbare Linie läuft hinaus, die zweite kommt nach */
[data-variante="wiederkehr"] .u::before {
display: block; transform-origin: 100% 50%; transition: none;
}
[data-variante="wiederkehr"] .u::after { transition: none; }
[data-variante="wiederkehr"] .u:is(:hover, :focus-visible, .an)::before {
transform: scaleX(0); transition: transform var(--dauer-aus) var(--kurve-aus);
}
[data-variante="wiederkehr"] .u:is(:hover, :focus-visible, .an)::after {
transition: transform var(--dauer-ein) var(--kurve-ein) var(--verz-ein);
}
/* ── Technik 2: Hintergrund-Verlauf (Fließtext, auch über Zeilenumbrüche) ──
<a class="t"><span>…</span></a>: das a trägt die ruhige Grundlinie, das span die aktive Linie. */
.t, .t > span {
background-repeat: no-repeat;
background-position-y: calc(100% - .08em);
-webkit-box-decoration-break: slice; box-decoration-break: slice;
}
.t { background-image: linear-gradient(var(--grundlinie), var(--grundlinie)); background-size: 100% 1px; background-position-x: 0; }
.t > span {
background-image: linear-gradient(var(--akzent), var(--akzent));
background-size: 0% var(--dicke); background-position-x: var(--ursprung-aus);
transition: background-size var(--dauer-aus) var(--kurve-aus);
}
.t:is(:hover, :focus-visible, .an) { color: var(--akzent); transition: color .15s linear; }
.t:is(:hover, :focus-visible, .an) > span {
background-size: 100% var(--dicke); background-position-x: var(--ursprung-ein);
transition: background-size var(--dauer-ein) var(--kurve-ein) var(--verz-ein);
}
[data-variante="wiederkehr"] .t { background-image: linear-gradient(var(--tinte), var(--tinte)); background-position-x: 100%; transition: none; }
[data-variante="wiederkehr"] .t > span { transition: none; }
[data-variante="wiederkehr"] .t:is(:hover, :focus-visible, .an) { background-size: 0% 1px; transition: background-size var(--dauer-aus) var(--kurve-aus); }
[data-variante="wiederkehr"] .t:is(:hover, :focus-visible, .an) > span { transition: background-size var(--dauer-ein) var(--kurve-ein) var(--verz-ein); }
/* ── Technik 3 (Variante inset): native Unterstreichung ──
Rückfall ohne text-decoration-inset: die Linie steht ruhig, Hover und Fokus machen sie kräftig und 2 px dick.
Mit Unterstützung: Linie startet mit Länge 0 (Ende um 100 % der Breite eingerückt) und wächst auf 0 0.
Fließtext: das a trägt die ruhige Grundlinie, das span die wachsende Akzentlinie (zwei native Linien übereinander). */
[data-variante="inset"] .u::before, [data-variante="inset"] .u::after { display: none !important; }
[data-variante="inset"] :is(.t, .t > span) { background-image: none !important; }
[data-variante="inset"] :is(.u, .t) {
text-decoration-line: underline; text-decoration-thickness: var(--dicke); text-underline-offset: .26em;
text-decoration-color: var(--grundlinie);
transition: text-decoration-color .15s linear, color .15s linear;
}
[data-variante="inset"] :is(.u, .t):is(:hover, :focus-visible, .an, :active) { text-decoration-color: currentColor; text-decoration-thickness: 2px; }
@supports (text-decoration-inset: 0 100%) {
@media (hover: hover) {
html:not(.reduziert) [data-variante="inset"] .u { text-decoration-color: currentColor; text-decoration-inset: 0 100%; transition: text-decoration-inset var(--dauer-aus) var(--kurve-aus); }
html:not(.reduziert) [data-variante="inset"] .u:is(:hover, :focus-visible, .an) { text-decoration-inset: 0 0; text-decoration-thickness: var(--dicke); transition: text-decoration-inset var(--dauer-ein) var(--kurve-ein) var(--verz-ein); }
html:not(.reduziert) [data-variante="inset"] .t:is(:hover, :focus-visible, .an) { text-decoration-color: var(--grundlinie); text-decoration-thickness: var(--dicke); }
html:not(.reduziert) [data-variante="inset"] .t > span {
text-decoration-line: underline; text-decoration-thickness: var(--dicke); text-underline-offset: .26em; text-decoration-color: var(--akzent);
text-decoration-inset: 0 100%; transition: text-decoration-inset var(--dauer-aus) var(--kurve-aus);
}
html:not(.reduziert) [data-variante="inset"] .t:is(:hover, :focus-visible, .an) > span { text-decoration-inset: 0 0; transition: text-decoration-inset var(--dauer-ein) var(--kurve-ein) var(--verz-ein); }
}
}
/* Wortweise Linie in Navigation, CTA und Fuß (Chrome 155+); ohne Unterstützung läuft die Linie durch */
@supports (text-decoration-skip-spaces: all) {
[data-variante="inset"] .u { text-decoration-skip-spaces: all; }
}
/* Bühne */
.buehne { align-self: center; display: grid; grid-template-columns: minmax(0, 1.25fr) minmax(0, .75fr); gap: clamp(28px, 5vw, 80px); align-items: end; }
.text { display: grid; gap: clamp(16px, 2vw, 24px); max-width: 40em; }
h1 { font-size: clamp(34px, 4.6vw, 64px); font-weight: 500; letter-spacing: -.045em; line-height: .98; text-wrap: balance; }
h1 em { letter-spacing: -.02em; }
.text p:not(.mono) { font-size: clamp(16px, 1.45vw, 19px); line-height: 1.58; max-width: 34em; text-wrap: pretty; }
.cta { justify-self: start; font-size: clamp(17px, 1.5vw, 20px); font-weight: 500; margin-top: 4px; }
.cta b { font-weight: 400; display: inline-block; margin-left: .35em; }
figure { display: grid; gap: 10px; }
figure img { display: block; width: 100%; aspect-ratio: 4 / 5; max-height: 54svh; object-fit: cover; border-radius: 4px; }
/* Fuß */
.fuss { display: flex; justify-content: space-between; align-items: baseline; gap: 12px 28px; flex-wrap: wrap; padding-top: 14px; border-top: 1px solid var(--haar); font-size: 14px; }
.fuss ul { display: flex; gap: clamp(16px, 2.4vw, 28px); list-style: none; padding: 0; }
.fuss address { font-style: normal; color: var(--leise); }
/* Bedienung: Variante und verwendete Werte */
.bedienung { display: flex; justify-content: space-between; align-items: center; gap: 10px 24px; flex-wrap: wrap; }
.schalter { display: flex; flex-wrap: wrap; gap: 4px; align-items: center; }
.schalter .mono { margin-right: 8px; }
.schalter button {
font: 500 11px/1 'Geist Mono', ui-monospace, monospace; letter-spacing: .1em; text-transform: uppercase;
color: var(--leise); background: none; border: 1px solid var(--haar); border-radius: 99px; padding: 7px 11px; cursor: pointer;
transition: color .15s linear, border-color .15s linear, background-color .15s linear;
}
.schalter button:hover { color: var(--tinte); border-color: var(--leise); }
.schalter button[aria-pressed="true"] { color: var(--papier); background: var(--tinte); border-color: var(--tinte); }
.schalter button:focus-visible { outline: 2px solid var(--tinte); outline-offset: 2px; }
.werte { text-align: right; overflow-wrap: anywhere; }
/* Vorführung: gezeichneter Zeiger */
.zeiger { position: fixed; left: 0; top: 0; z-index: 6; width: 18px; height: 18px; pointer-events: none; display: none; }
.vorfuehrung .zeiger { display: block; }
/* Vorschau: nur die Bühne, Variante klein oben rechts */
.vorschau body { overflow: hidden; grid-template-rows: auto 1fr auto; padding: 40px 52px 36px; height: 100svh; }
.vorschau .bedienung { display: none; }
.vorschau figure img { max-height: 380px; }
/* Touch: kein Hover. Linie dauerhaft sichtbar, beim Tippen kurz kräftig und dicker */
@media (hover: none) {
.u::after { transform: scaleX(1); opacity: .32; transition: opacity .15s linear, height .15s linear; }
.u:active::after { opacity: 1; height: 2px; }
[data-variante="wiederkehr"] .u::before { display: none; }
.t > span { background-size: 100% var(--dicke); background-image: linear-gradient(transparent, transparent); }
.t:active > span { background-image: linear-gradient(var(--akzent), var(--akzent)); background-size: 100% 2px; }
}
/* Reduzierte Bewegung: Linie immer da, nur Kraft und Dicke wechseln */
.reduziert .u::before { display: none !important; }
.reduziert .u::after, .reduziert .u:is(:hover, :focus-visible, .an)::after { transform: scaleX(1); transition: opacity .15s linear; opacity: .32; }
.reduziert .u:is(:hover, :focus-visible, .an)::after { opacity: 1; height: 2px; }
.reduziert .t, .reduziert .t:is(:hover, :focus-visible, .an) { background-image: linear-gradient(var(--grundlinie), var(--grundlinie)); background-size: 100% 1px; transition: none; }
.reduziert .t > span { background-size: 100% 2px; background-image: none; transition: none; }
.reduziert .t:is(:hover, :focus-visible, .an) > span { background-image: linear-gradient(var(--akzent), var(--akzent)); }
@media (max-width: 720px) {
.buehne { grid-template-columns: 1fr; align-items: start; }
figure img { aspect-ratio: 3 / 2; max-height: none; }
.werte { text-align: left; }
.nav { gap: 16px; }
}
@media (max-width: 420px) {
.kopf { flex-direction: column; gap: 10px; }
.fuss address { width: 100%; }
}
@media (forced-colors: active) {
button, [role=button], [role=tab], [role=switch] { border: 1px solid ButtonText !important; }
}
</style>
</head>
<body data-variante="durchlauf">
<header class="kopf">
<a class="marke u" href="#start">Atelier Kron <span>Gestaltung</span></a>
<ul class="nav">
<li><a class="u" href="#arbeiten">Arbeiten</a></li>
<li><a class="u" href="#studio">Studio</a></li>
<li><a class="u" href="#journal" aria-current="page">Journal</a></li>
<li><a class="u" href="#kontakt">Kontakt</a></li>
</ul>
</header>
<main class="buehne">
<article class="text">
<p class="mono">Journal · Nr. 14 · Oktober 2026</p>
<h1>Eine Reportage, gesetzt mit viel <em>Luft</em>.</h1>
<p>Für die dritte Ausgabe von <a class="t" href="#heft"><span>Stille Post</span></a> haben wir die Reportage über eine Töpferwerkstatt neu gesetzt. Links das Bild, rechts viel Weißraum und eine leise Überschrift. Gedruckt hat die <a class="t" href="#druckerei"><span>Druckerei Hallerstedt</span></a> auf ungestrichenem Naturpapier.</p>
<p>Gesetzt ist das Heft in <a class="t" href="#schrift"><span>einer Antiqua aus den Zwanzigern, die wir um eigene Ziffern und Initialen erweitert haben</span></a>. Alle Doppelseiten liegen im <a class="t" href="#archiv"><span>Projektarchiv</span></a>.</p>
<a class="cta u" href="#projekt">Projekt ansehen<b aria-hidden="true">→</b></a>
</article>
<figure>
<img src="bilder/magazin-doppelseite-hoch.jpg" alt="Aufgeschlagenes Magazin: Reportage „Die Ruhe der Dinge“ mit Initiale, zweispaltigem Text und kleinem Landschaftsbild" width="800" height="1000">
<figcaption class="mono">Stille Post, Ausgabe 03 · Reportage</figcaption>
</figure>
</main>
<footer class="fuss">
<address>Gerberstraße 7 · 50676 Köln</address>
<ul>
<li><a class="u" href="#newsletter">Newsletter</a></li>
<li><a class="u" href="#presse">Presse</a></li>
<li><a class="u" href="#impressum">Impressum</a></li>
</ul>
</footer>
<div class="bedienung">
<div class="schalter" role="group" aria-label="Variante">
<span class="mono">Variante</span>
<button type="button" data-v="durchlauf" aria-pressed="true">Durchlauf</button>
<button type="button" data-v="mitte" aria-pressed="false">Mitte</button>
<button type="button" data-v="wiederkehr" aria-pressed="false">Wiederkehr</button>
<button type="button" data-v="inset" aria-pressed="false">Nativ inset</button>
</div>
<p class="mono werte" data-werte aria-live="polite"></p>
</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="#161513" stroke="#efece5" stroke-width="1.2" stroke-linejoin="round"/></svg>
<script>
(() => {
const html = document.documentElement;
const body = document.body;
const knoepfe = [...document.querySelectorAll('[data-v]')];
const werteFeld = document.querySelector('[data-werte]');
const zeiger = document.querySelector('[data-zeiger]');
const VARIANTEN = ['durchlauf', 'mitte', 'wiederkehr', 'inset'];
const RUNDE = ['durchlauf', 'mitte', 'wiederkehr']; // Galerie-Vorschau ohne ?v= wechselt durch diese drei
const INSET = !!(window.CSS && CSS.supports('text-decoration-inset', '0 100%'));
const LUECKE = !!(window.CSS && CSS.supports('text-decoration-skip-spaces', 'all'));
const WERTE = {
durchlauf: 'circ.inOut 0.85,0,0.15,1 · 600 ms · 1 px · links rein, rechts raus',
mitte: '0.215,0.61,0.355,1 · 300 ms · 2 px · aus der Mitte',
wiederkehr: 'raus 300 ms 0.7,0,0.2,1 · rein 400 ms +100 ms 0.4,1,0.8,1 · 1 px',
inset: 'text-decoration-inset 0 100% → 0 0 · 300 ms 0.215,0.61,0.355,1 · skip-spaces all · hier: '
+ (INSET ? 'inset ja' : 'inset nein, Linie steht') + ', ' + (LUECKE ? 'skip-spaces ja' : 'skip-spaces nein')
};
const ausUrl = new URLSearchParams(location.search).get('v');
const fest = VARIANTEN.includes(ausUrl);
let aktuell = fest ? ausUrl : 'durchlauf';
function variante(v, url) {
body.dataset.variante = v;
knoepfe.forEach(k => k.setAttribute('aria-pressed', String(k.dataset.v === v)));
werteFeld.textContent = WERTE[v];
if (url) { aktuell = v; const u = new URL(location.href); u.searchParams.set('v', v); history.replaceState(history.state, '', u); }
}
knoepfe.forEach(k => k.addEventListener('click', () => variante(k.dataset.v, true)));
document.addEventListener('click', e => { const a = e.target.closest('a'); if (a) e.preventDefault(); });
variante(aktuell);
// Vorführung: ein gedachter Zeiger fährt fünf Links an. Mit ?v= (oder nach Umschalten) bleibt die gewählte Variante, sonst pro Runde eine der ersten drei
const ziele = () => [
document.querySelector('.nav li:nth-child(3) a'),
document.querySelector('a[href="#druckerei"]'),
document.querySelector('a[href="#schrift"]'),
document.querySelector('.cta'),
document.querySelector('.fuss li:nth-child(1) a')
];
const HALTEN = 1300, FAHRT = 650;
let uhr = null, start = 0, runde = -1, aktiv = null;
const pos = { x: 0, y: 0 };
const kurve = t => t < .5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2; // nur für den Zeiger
function punkt(a) {
// Zielpunkt: auf der ersten Zeile des Links, etwas rechts vom Anfang
const r = a.getClientRects()[0] || a.getBoundingClientRect();
return { x: r.left + Math.min(r.width * 0.55, 90), y: r.top + r.height * 0.6 };
}
function setzeAn(a) {
if (a === aktiv) return;
if (aktiv) aktiv.classList.remove('an');
aktiv = a;
if (a) a.classList.add('an');
}
function vorfuehren() {
pause();
html.classList.add('vorfuehrung');
runde = -1;
const liste = ziele();
const schritt = HALTEN + FAHRT, rundeDauer = liste.length * schritt + 900;
const ruhe = { x: innerWidth * 0.72, y: innerHeight * 0.9 };
const tick = t => {
const z = t - start, r = Math.floor(z / rundeDauer), p = z % rundeDauer;
if (r !== runde) { runde = r; variante(fest || new URL(location.href).searchParams.has('v') ? aktuell : RUNDE[r % RUNDE.length]); setzeAn(null); }
const i = Math.floor(p / schritt), q = p % schritt;
let von, nach, f, ziel = null;
if (i >= liste.length) { von = punkt(liste[liste.length - 1]); nach = ruhe; f = Math.min(1, (p - liste.length * schritt) / FAHRT); }
else {
von = i === 0 ? ruhe : punkt(liste[i - 1]);
nach = punkt(liste[i]);
f = Math.min(1, q / FAHRT);
if (q >= FAHRT) ziel = liste[i];
}
const k = kurve(f);
pos.x = von.x + (nach.x - von.x) * k; pos.y = von.y + (nach.y - von.y) * k;
zeiger.style.transform = `translate(${pos.x - 2}px, ${pos.y - 1}px)`;
setzeAn(ziel);
uhr = requestAnimationFrame(tick);
};
start = performance.now() - FAHRT; // sofort am ersten Link
uhr = requestAnimationFrame(tick);
}
function pause() { cancelAnimationFrame(uhr); uhr = null; }
function neustart() { pause(); html.classList.remove('vorfuehrung'); setzeAn(null); variante(aktuell); }
})();
</script>
</body>
</html>