Tiefen-Taste (3D-Button)
Der Button hat sichtbare Tiefe: Hover hebt die Fläche an, Druck setzt sie fast auf. Als Stapel, harter Versatz, Keycap oder Soft-Taste mit Kontur.
Freundliche, ermutigende Marken mit einer klaren Hauptaktion: Fahrschule, Kurse und Coaching, Direktvertrieb.
Ein Button mit sichtbarer Tiefe. Eine Fläche liegt über einer Kante oder einem Schatten. Hover hebt sie etwas an, Druck setzt sie fast auf. Der Weg zwischen Ruhe, Hover und Druck ist die Rückmeldung: Man sieht, dass etwas nachgibt. Vier Spielarten mit derselben Mechanik: Stapel aus Schatten, Kante und Front; harter Versatzschatten im Brutalismus-Stil; Keycap mit dunklerer Unterkante; weiche Soft-Taste mit Kontur.
Tiefe macht die Handlung greifbar. Der Button sieht drückbar aus, bevor man ihn berührt, und der Druck antwortet sofort mit einem kurzen Weg nach unten. Das wirkt freundlich und ermutigend, gerade bei Zielgruppen, die zögern (Anmeldung, Probetraining, erste Buchung).
Wann einsetzen
Für die eine Hauptaktion einer Ansicht in freundlichen, lauten oder spielerischen Marken: Fahrschule, Kurse und Coaching, Direktvertrieb von Produkten (Stapel); Fitnessstudio, Kultur und Festival (Versatz); Tastenkürzel und Werkzeug-Oberflächen (Keycap).
Wann nicht
Nicht bei Luxus, Kanzlei, Praxis und Immobilien: Tiefe wirkt dort spielerisch. Nie mehrere Tiefen-Tasten nebeneinander (Spielzeug-Look); Nebenaktionen bleiben flach. Die Soft-Taste nie ohne Kontur und nie als einzige Zustandsanzeige: Neumorph hat zu wenig Kontrast, Buttons sind schwer als bedienbar zu erkennen. Nicht für Formularfelder, Reiter und Menüs.
Varianten
- stapel: drei Ebenen (Schatten, Kante, Front); Front in Ruhe -4 px, Hover -6 px mit Überschwinger, Druck -2 px (nach Josh W. Comeau)
- versatz-brutal: harter Versatzschatten ohne Unschärfe, dicker Rand; Druck schiebt die Fläche in den Schatten, Bewegung in Stufen
- keycap: Taste mit dunklerer Unterkante als Schatten; Druck nimmt die Unterkante weg und senkt die Fläche um denselben Weg
- soft: Neumorph-Zwilling mit Licht- und Schattenseite, nur mit 1 px Kontur und Text mindestens 4,5:1; Druck kehrt die Schatten nach innen
Umsetzung
Echter <button> mit drei Kindern: span.taste__schatten und span.taste__kante liegen absolut, span.taste__front trägt Text und Größe. Die Zustände laufen über Variablen am Button (--fy Front, --sy Schatten, --t/--k Dauer und Kurve): Ruhe -4 px, :hover -6 px (nur unter @media (hover: hover)), :active -2 px. Die Übergangsdauer steht am Zielzustand: hin 250 ms mit Überschwinger, Druck 34 ms, zurück 600 ms. Reduzierte Bewegung setzt nur die Variablen gleich. Varianten tauschen die Variablen (Versatz, Keycap, Soft) und blenden Schatten und Kante aus. Deaktiviert: aria-disabled statt disabled, damit der Button fokussierbar bleibt und seine Begründung (aria-describedby) vorgelesen wird; die Front liegt flach. Laden: data-zustand="laedt" hält die Taste gedrückt, Label wechselt, Status über role=status. Nur eine Tiefen-Taste je Ansicht, Nebenaktionen flach (in der Demo „Lieber anrufen“ als Link).
Robust umsetzen
Telefon. Ohne Hover kein Anheben: Die Taste ruht in ihrer Tiefe, der Druck wirkt über :active sofort. Trefferfläche mindestens 44 px hoch (Front plus Kante). Kein Tap-Highlight, damit der Druck die Rückmeldung bleibt.
Ohne Bewegung. Kein Weg: Front und Schatten bleiben stehen. Hover und Druck zeigen sich nur über Farbe (heller bzw. dunkler) in 80 ms linear.
Barrierefrei. Echter <button> mit sichtbarem Text, :focus-visible mit eigener Kontur (nie nur über Schatten). Text auf der Front mindestens 4,5:1. Die Tiefe ist Zusatz, nie die einzige Zustandsanzeige: Gedrückt-Zustände (aria-pressed) und Deaktiviert zusätzlich über Text oder Farbe. Im Kontrastmodus (forced-colors) fallen Schatten weg: Rahmen in ButtonText. Trefferfläche mindestens 44 × 44 px (WCAG 2.5.8 verlangt 24 px, 44 px nach Apple HIG).
Code
<!DOCTYPE html>
<!--
Tiefen-Taste (3D-Button)
Fundus · Buttons · https://cecodesign.de/lab/fundus/buttons/tiefen-taste-3d/
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>Tiefen-Taste (3D-Button) · 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: 'Bricolage Grotesque'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/bricolage-grotesque:vf@latest/latin-opsz-normal.woff2') format('woff2-variations'); font-weight: 200 800; font-display: swap; }
@font-face { font-family: 'Atkinson Hyperlegible Next'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/atkinson-hyperlegible-next:vf@latest/latin-wght-normal.woff2') format('woff2-variations'); font-weight: 200 800; font-display: swap; }
@font-face { font-family: 'Geist Mono'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/geist-mono:vf@latest/latin-wght-normal.woff2') format('woff2-variations'); font-weight: 100 900; font-display: swap; }
:root {
/* Rezept fahrschule-freundlich-buchbar: Creme, Navy für Vertrauen, Himmelblau, Kennzeichen-Gelb nur als Fläche */
--grund: #f8f8ec; --wechsel: #eef0d8; --karte: #ffffff;
--navy: #142b50; --nacht: #0b1a33; --himmel: #3fbadf; --gelb: #ffd60a;
--leise: #566175; --linie: rgba(20, 43, 80, .16); --fehler: #b3261e; --gut: #1f6b3a;
--einblenden: cubic-bezier(0.16, 1, 0.3, 1);
}
* { box-sizing: border-box; margin: 0; }
html { background: var(--grund); color: var(--navy); font: 400 1.0625rem/1.6 'Atkinson Hyperlegible Next', system-ui, sans-serif; -webkit-text-size-adjust: 100%; }
:root:has(:focus-visible) { scroll-behavior: auto; }
body { min-height: 100svh; overflow-x: hidden; }
:focus-visible { outline: 3px solid var(--navy); outline-offset: 4px; border-radius: 6px; }
.label { font: 500 11px/1.2 'Geist Mono', ui-monospace, monospace; letter-spacing: .14em; text-transform: uppercase; color: var(--leise); }
.nur-sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
/* ---------- Kopf ---------- */
.kopf { display: flex; align-items: center; justify-content: space-between; gap: 16px; flex-wrap: wrap; padding: 18px clamp(20px, 4vw, 56px); border-bottom: 1px solid var(--linie); }
.marke { display: flex; align-items: center; gap: 10px; color: var(--navy); text-decoration: none; }
.marke svg { width: 34px; height: 34px; flex: none; }
.marke b { font: 800 1.25rem/1 'Bricolage Grotesque', sans-serif; letter-spacing: -.02em; }
.marke small { display: block; font: 500 10px/1.2 'Geist Mono', monospace; letter-spacing: .14em; text-transform: uppercase; color: var(--leise); margin-top: 3px; }
.kopf nav { display: flex; gap: clamp(14px, 2.4vw, 28px); flex-wrap: wrap; }
.kopf nav a { color: var(--navy); text-decoration: none; font-weight: 600; font-size: .9375rem; padding: 10px 0; }
.kopf nav a:hover { text-decoration: underline; text-underline-offset: 4px; }
.kopf .tel { font: 500 .875rem/1 'Geist Mono', monospace; color: var(--navy); text-decoration: none; padding: 12px 0; }
/* ---------- Bühne ---------- */
.buehne { display: grid; grid-template-columns: minmax(0, 1.08fr) minmax(0, .92fr); gap: clamp(28px, 5vw, 72px); align-items: start; padding: clamp(28px, 5vw, 64px) clamp(20px, 4vw, 56px) clamp(40px, 6vw, 80px); max-width: 1240px; margin: 0 auto; }
.text .label { margin-bottom: 18px; }
h1 { font: 800 clamp(2.5rem, 1rem + 5.2vw, 5.25rem)/.98 'Bricolage Grotesque', sans-serif; font-variation-settings: 'opsz' 96; letter-spacing: -.02em; hyphens: auto; max-width: 11ch; }
h1 em { font-style: normal; position: relative; white-space: nowrap; z-index: 0; isolation: isolate; }
h1 em::after { content: ''; position: absolute; left: -.04em; right: -.06em; bottom: .04em; height: .3em; background: var(--gelb); z-index: -1; border-radius: 3px; }
.lead { margin-top: 22px; max-width: 46ch; font-size: 1.125rem; }
.klassen { margin-top: 30px; }
.klassen .label { margin-bottom: 10px; }
.chips { display: flex; flex-wrap: wrap; gap: 8px; }
.chip { min-height: 44px; min-width: 64px; padding: 0 16px; border: 2px solid var(--navy); border-radius: 999px; background: transparent; color: var(--navy); font: 700 1rem/1 'Atkinson Hyperlegible Next', sans-serif; cursor: pointer; transition: background-color 150ms cubic-bezier(0.4, 0, 0.2, 1), color 150ms cubic-bezier(0.4, 0, 0.2, 1); }
.chip:hover { background: rgba(20, 43, 80, .07); }
.chip[aria-pressed="true"] { background: var(--navy); color: #fff; }
.preise { display: grid; grid-template-columns: repeat(3, minmax(0, max-content)); gap: 6px 28px; margin-top: 20px; }
.preise dt { font: 500 11px/1.3 'Geist Mono', monospace; letter-spacing: .1em; text-transform: uppercase; color: var(--leise); }
.preise dd { font: 700 1.375rem/1.2 'Bricolage Grotesque', sans-serif; font-variant-numeric: tabular-nums; grid-row: 2; }
.preise-fuss { margin-top: 10px; font-size: .875rem; color: var(--leise); }
/* ---------- Buchungskarte ---------- */
.karte { background: var(--karte); border: 2px solid var(--navy); border-radius: 22px; padding: clamp(20px, 3vw, 32px); box-shadow: 0 1px 0 rgba(20,43,80,.04), 0 24px 48px -28px rgba(20, 43, 80, .35); }
.karte h2 { font: 800 clamp(1.5rem, 1rem + 1.4vw, 2rem)/1.05 'Bricolage Grotesque', sans-serif; letter-spacing: -.015em; }
.karte .unter { margin-top: 6px; color: var(--leise); font-size: .9375rem; }
fieldset { border: 0; padding: 0; margin: 20px 0 0; min-width: 0; }
legend { padding: 0; margin-bottom: 8px; }
.slots { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px; }
.slot { position: relative; }
.slot input { position: absolute; opacity: 0; inset: 0; margin: 0; cursor: pointer; }
.slot span { display: grid; gap: 2px; min-height: 64px; align-content: center; padding: 8px 10px; border: 1.5px solid rgba(20, 43, 80, .45); border-radius: 12px; text-align: center; transition: background-color 150ms cubic-bezier(0.4, 0, 0.2, 1), border-color 150ms cubic-bezier(0.4, 0, 0.2, 1); }
.slot b { font: 700 .8125rem/1.2 'Atkinson Hyperlegible Next', sans-serif; }
.slot time { font: 500 1.0625rem/1.2 'Geist Mono', monospace; font-variant-numeric: tabular-nums; }
.slot input:checked + span { background: var(--navy); border-color: var(--navy); color: #fff; }
.slot input:focus-visible + span { outline: 3px solid var(--navy); outline-offset: 3px; }
.feld { margin-top: 14px; display: grid; gap: 6px; min-width: 0; }
.feld label { font-weight: 700; font-size: .9375rem; }
.feld input { width: 100%; min-width: 0; min-height: 48px; padding: 0 14px; border: 1.5px solid rgba(20, 43, 80, .55); border-radius: 12px; font: inherit; color: var(--navy); background: #fff; }
.feld input:focus-visible { outline: 3px solid var(--navy); outline-offset: 2px; }
.feld[data-fehler] input { border-color: var(--fehler); border-width: 2px; }
.feld .fehler { display: none; color: var(--fehler); font-size: .875rem; font-weight: 600; }
.feld[data-fehler] .fehler { display: block; }
.felder2 { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; }
.aktion { margin-top: 24px; display: flex; align-items: center; gap: 14px 18px; flex-wrap: wrap; }
.aktion .neben { color: var(--navy); font-weight: 600; font-size: .9375rem; text-underline-offset: 4px; padding: 12px 0; }
.status { margin-top: 14px; min-height: 1.5em; font-size: .9375rem; }
.status[data-art="gut"] { color: var(--gut); font-weight: 700; }
.klein { margin-top: 6px; font-size: .8125rem; color: var(--leise); }
/* ---------- Kurse ---------- */
.kurse { background: var(--wechsel); padding: clamp(40px, 6vw, 80px) clamp(20px, 4vw, 56px); }
.kurse-innen { max-width: 1240px; margin: 0 auto; }
.kurse h2 { font: 800 clamp(1.75rem, 1rem + 2.4vw, 3rem)/1 'Bricolage Grotesque', sans-serif; letter-spacing: -.02em; margin-top: 10px; }
.kurs-liste { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(320px, 100%), 1fr)); gap: 18px; margin-top: 28px; }
.kurs { background: var(--grund); border-radius: 20px; padding: 24px; display: grid; gap: 10px; border: 1.5px solid rgba(20, 43, 80, .2); }
.kurs h3 { font: 800 1.375rem/1.1 'Bricolage Grotesque', sans-serif; }
.kurs .zeit { font: 500 .875rem/1.4 'Geist Mono', monospace; font-variant-numeric: tabular-nums; }
.kurs .frei { font-weight: 700; }
.kurs .frei[data-voll] { color: var(--leise); }
.kurs .taste { justify-self: start; margin-top: 8px; }
/* =====================================================================
Tiefen-Taste: ein <button> mit Schatten, Kante und Front.
Zustände über Variablen, damit reduzierte Bewegung nur Werte tauscht.
===================================================================== */
.taste {
--r: 14px;
--ruhe: -4px; --hoch: -6px; --tief: -2px; /* Front: Ruhe, Hover, Druck (Comeau) */
--s-ruhe: 2px; --s-hoch: 4px; --s-tief: 1px; /* Schatten */
--fy: var(--ruhe); --sy: var(--s-ruhe); --hell: 1;
--t-hin: 250ms; --k-hin: cubic-bezier(0.3, 0.7, 0.4, 1.5);
--t-druck: 34ms; --k-druck: linear;
--t-zurueck: 600ms; --k-zurueck: cubic-bezier(0.3, 0.7, 0.4, 1);
--t: var(--t-zurueck); --k: var(--k-zurueck);
position: relative; display: inline-block; border: 0; padding: 0; margin: 6px 0 0; background: transparent;
color: var(--navy); font: 700 1.0625rem/1.2 'Atkinson Hyperlegible Next', sans-serif; cursor: pointer;
border-radius: var(--r); outline-offset: 6px; -webkit-tap-highlight-color: transparent; user-select: none; -webkit-user-select: none;
filter: brightness(var(--hell)); transition: filter 250ms linear;
}
.taste__schatten, .taste__kante { position: absolute; inset: 0; border-radius: var(--r); }
.taste__schatten { background: rgba(20, 43, 80, .32); filter: blur(2px); transform: translateY(var(--sy)); transition: transform var(--t) var(--k); }
.taste__kante { background: linear-gradient(to left, var(--nacht) 0%, var(--navy) 8%, var(--navy) 92%, var(--nacht) 100%); }
.taste__front {
position: relative; display: flex; align-items: center; justify-content: center; gap: .6em;
min-height: 52px; padding: 0 26px; border-radius: var(--r);
background: var(--gelb); color: var(--navy); box-shadow: inset 0 0 0 2px var(--navy);
transform: translateY(var(--fy)); transition: transform var(--t) var(--k), background-color 80ms linear, box-shadow var(--t) var(--k);
}
.taste__front svg { width: 20px; height: 20px; flex: none; }
.taste__kbd { display: none; }
.taste__lade { display: none; width: 18px; height: 18px; border-radius: 50%; border: 2.5px solid currentColor; border-right-color: transparent; animation: drehen 700ms linear infinite; }
@keyframes drehen { to { rotate: 1turn; } }
@media (hover: hover) {
.taste:hover { --fy: var(--hoch); --sy: var(--s-hoch); --hell: 1.1; --t: var(--t-hin); --k: var(--k-hin); }
}
.taste.ist-hover { --fy: var(--hoch); --sy: var(--s-hoch); --hell: 1.1; --t: var(--t-hin); --k: var(--k-hin); }
.taste:active, .taste.ist-druck, .taste[data-zustand="laedt"] { --fy: var(--tief); --sy: var(--s-tief); --t: var(--t-druck); --k: var(--k-druck); }
/* Laden und Erfolg */
.taste[data-zustand="laedt"] { cursor: progress; }
.taste[data-zustand="laedt"] .taste__lade { display: block; }
.taste[data-zustand="laedt"] .taste__pfeil, .taste[data-zustand="gut"] .taste__pfeil { display: none; }
.taste__haken { display: none; }
.taste[data-zustand="gut"] .taste__haken { display: block; }
/* Deaktiviert: Front liegt flach auf der Kante, keine Tiefe, kein Weg */
.taste[aria-disabled="true"]:not([data-zustand]) { --fy: 0px; --sy: 0px; --hell: 1; cursor: not-allowed; }
.taste[aria-disabled="true"]:not([data-zustand]) .taste__front { background: #e3e4d4; color: #3f4a5e; box-shadow: inset 0 0 0 2px rgba(20, 43, 80, .4); }
.taste[aria-disabled="true"]:not([data-zustand]) .taste__schatten { opacity: 0; }
/* ---- Variante versatz-brutal: harter Versatz, Druck in den Schatten, Stufen ---- */
[data-variante="versatz-brutal"] .taste { --r: 6px; --ruhe: 0px; --hoch: -2px; --tief: 5px; --vx: 5px;
--t-hin: 160ms; --k-hin: steps(2, end); --t-druck: 0ms; --k-druck: steps(1, end); --t-zurueck: 160ms; --k-zurueck: steps(2, end); filter: none; margin-right: 6px; margin-bottom: 6px; }
[data-variante="versatz-brutal"] .taste__schatten, [data-variante="versatz-brutal"] .taste__kante { display: none; }
[data-variante="versatz-brutal"] .taste__front { transform: translate(var(--fy), var(--fy)); box-shadow: inset 0 0 0 3px var(--navy), calc(5px - var(--fy)) calc(5px - var(--fy)) 0 0 var(--navy); }
[data-variante="versatz-brutal"] .taste[aria-disabled="true"]:not([data-zustand]) .taste__front { transform: translate(5px, 5px); box-shadow: inset 0 0 0 3px rgba(20, 43, 80, .4); }
@media (hover: hover) { [data-variante="versatz-brutal"] .taste:hover .taste__front { background: #ffe14d; } }
[data-variante="versatz-brutal"] .taste.ist-hover .taste__front { background: #ffe14d; }
/* ---- Variante keycap: dunklere Unterkante als Schatten, Druck nimmt sie weg ---- */
[data-variante="keycap"] .taste { --r: 12px; --ruhe: 0px; --hoch: -1px; --tief: 4px; --kante: 4px;
--t-hin: 180ms; --k-hin: cubic-bezier(0, 0, 0.58, 1); --t-druck: 100ms; --k-druck: cubic-bezier(0, 0, 0.58, 1); --t-zurueck: 180ms; --k-zurueck: cubic-bezier(0, 0, 0.58, 1); filter: none; margin-bottom: 4px; }
[data-variante="keycap"] .taste__schatten, [data-variante="keycap"] .taste__kante { display: none; }
[data-variante="keycap"] .taste__front { background: linear-gradient(180deg, #21406f 0%, var(--navy) 70%); color: #fff;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, .22), 0 calc(4px - var(--fy)) 0 0 var(--nacht), 0 calc(7px - var(--fy)) 10px -3px rgba(20, 43, 80, .45); }
[data-variante="keycap"] .taste__kbd { display: inline-grid; place-items: center; min-width: 26px; height: 22px; padding: 0 6px; border-radius: 5px; background: rgba(255, 255, 255, .14); box-shadow: inset 0 -1px 0 rgba(0, 0, 0, .35); font: 500 12px/1 'Geist Mono', monospace; }
@media (hover: hover) { [data-variante="keycap"] .taste:hover .taste__front { background: linear-gradient(180deg, #2a4d82 0%, #1a3762 70%); } }
[data-variante="keycap"] .taste.ist-hover .taste__front { background: linear-gradient(180deg, #2a4d82 0%, #1a3762 70%); }
[data-variante="keycap"] .taste[aria-disabled="true"]:not([data-zustand]) .taste__front { background: #d6d9dd; color: #2f3a4e; box-shadow: inset 0 0 0 1px rgba(20, 43, 80, .35); transform: translateY(4px); }
/* ---- Variante soft: Neumorph nur mit Kontur 1 px und vollem Textkontrast ---- */
[data-variante="soft"] .taste { --r: 16px; --ruhe: 0px; --hoch: -1px; --tief: 1px;
--t-hin: 180ms; --k-hin: cubic-bezier(0, 0, 0.58, 1); --t-druck: 100ms; --k-druck: cubic-bezier(0, 0, 0.58, 1); --t-zurueck: 180ms; --k-zurueck: cubic-bezier(0, 0, 0.58, 1); filter: none; }
[data-variante="soft"] .taste__schatten, [data-variante="soft"] .taste__kante { display: none; }
[data-variante="soft"] .taste__front { background: var(--soft-grund, var(--grund)); color: var(--navy);
box-shadow: inset 0 0 0 1px var(--leise), -6px -6px 14px rgba(255, 255, 255, .95), 7px 7px 16px rgba(20, 43, 80, .2); }
@media (hover: hover) { [data-variante="soft"] .taste:hover .taste__front { box-shadow: inset 0 0 0 1px var(--navy), -8px -8px 18px #fff, 9px 9px 20px rgba(20, 43, 80, .24); } }
[data-variante="soft"] .taste.ist-hover .taste__front { box-shadow: inset 0 0 0 1px var(--navy), -8px -8px 18px #fff, 9px 9px 20px rgba(20, 43, 80, .24); }
[data-variante="soft"] .taste:active .taste__front, [data-variante="soft"] .taste.ist-druck .taste__front, [data-variante="soft"] .taste[data-zustand="laedt"] .taste__front {
box-shadow: inset 0 0 0 1px var(--navy), inset 5px 5px 10px rgba(20, 43, 80, .2), inset -5px -5px 10px rgba(255, 255, 255, .9); }
[data-variante="soft"] .taste__front::before { content: ''; width: 8px; height: 8px; border-radius: 50%; background: var(--gelb); box-shadow: 0 0 0 1.5px var(--navy); }
[data-variante="soft"] .taste[data-zustand] .taste__front::before { display: none; }
[data-variante="soft"] .taste[aria-disabled="true"]:not([data-zustand]) .taste__front { box-shadow: inset 0 0 0 1px rgba(20, 43, 80, .4); background: transparent; color: #3f4a5e; }
[data-variante="soft"] .taste[aria-disabled="true"]:not([data-zustand]) .taste__front::before { background: transparent; }
[data-variante="soft"] .karte { background: var(--grund); }
/* ---------- Simulierter Zeiger (Vorführung) ---------- */
.zeiger { position: fixed; left: 0; top: 0; z-index: 20; width: 22px; height: 22px; pointer-events: none; display: none; transition: transform 700ms var(--einblenden); }
.vorfuehrung .zeiger { display: block; }
/* ---------- Pult ---------- */
.pult { position: fixed; right: 16px; bottom: 16px; z-index: 30; display: grid; gap: 8px; padding: 12px 14px; max-width: min(440px, calc(100vw - 32px)); background: rgba(11, 26, 51, .94); color: #f3f2ee; border-radius: 14px; box-shadow: 0 12px 30px -12px rgba(11, 26, 51, .5); }
.pult .label { color: rgba(243, 242, 238, .72); }
.pult__knoepfe { display: flex; flex-wrap: wrap; gap: 6px; }
.pult__knoepfe button { min-height: 32px; padding: 0 11px; border-radius: 999px; border: 1px solid rgba(243, 242, 238, .3); background: transparent; color: #f3f2ee; font: 500 11px/1 'Geist Mono', monospace; letter-spacing: .08em; text-transform: uppercase; cursor: pointer; }
.pult__knoepfe button[aria-pressed="true"] { background: #f3f2ee; color: var(--nacht); }
.pult__knoepfe button:focus-visible { outline: 2px solid var(--himmel); outline-offset: 2px; }
.pult__werte { font: 400 11px/1.5 'Geist Mono', monospace; color: rgba(243, 242, 238, .78); }
/* ---------- Vorschau 960 × 720 ---------- */
.vorschau body { overflow: hidden; }
.vorschau .kurse, .vorschau .kopf nav { display: none; }
/* ---------- Telefon ---------- */
@media (max-width: 880px) {
.buehne { grid-template-columns: minmax(0, 1fr); }
h1 { max-width: 14ch; }
}
@media (max-width: 520px) {
.kopf nav { display: none; }
.slots { grid-template-columns: minmax(0, 1fr); }
.slot span { min-height: 48px; grid-auto-flow: column; justify-content: space-between; text-align: left; padding: 0 14px; }
.felder2 { grid-template-columns: minmax(0, 1fr); }
.preise { grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 6px 14px; }
.preise dd { font-size: 1.125rem; }
.taste { width: 100%; }
}
@media (max-width: 360px) { .chip { min-width: 0; flex: 1 1 auto; } }
/* ---------- Reduzierte Bewegung: kein Weg, nur Farbe ---------- */
:root.reduziert .taste { --hoch: var(--ruhe); --tief: var(--ruhe); --s-hoch: var(--s-ruhe); --s-tief: var(--s-ruhe); --t-hin: 80ms; --t-druck: 80ms; --t-zurueck: 80ms; --k-hin: linear; --k-druck: linear; --k-zurueck: linear; }
:root.reduziert .taste:active .taste__front, :root.reduziert .taste.ist-druck .taste__front { filter: brightness(.9); }
:root.reduziert .taste__lade { animation: none; border-right-color: currentColor; opacity: .5; }
:root.reduziert .zeiger { transition: none; }
:root.reduziert * { scroll-behavior: auto !important; }
/* Windows-Kontrastmodus: Schatten fallen weg, also Rahmen */
@media (forced-colors: active) {
button, [role=button], [role=tab], [role=switch], input, select, textarea { border: 1px solid ButtonText !important; }
.taste__schatten, .taste__kante { display: none; }
.taste__front { transform: none !important; border: 2px solid ButtonText; }
.taste[aria-disabled="true"] .taste__front { border-color: GrayText; color: GrayText; }
.slot span { border: 1px solid ButtonText; }
.slot input:checked + span { outline: 3px solid Highlight; }
}
</style>
</head>
<body data-variante="stapel">
<header class="kopf">
<a class="marke" href="#" aria-label="Fahrschule Traunufer, Startseite">
<svg viewBox="0 0 34 34" aria-hidden="true"><rect x="1" y="1" width="32" height="32" rx="9" fill="#142b50"/><path d="M8 27c4-6 14-5 15-11 .8-4.6-3-7-7-7" fill="none" stroke="#ffd60a" stroke-width="4" stroke-linecap="round"/><path d="M8 27c4-6 14-5 15-11 .8-4.6-3-7-7-7" fill="none" stroke="#142b50" stroke-width="1" stroke-dasharray="2 2.4"/></svg>
<span><b>Traunufer</b><small>Fahrschule · Wels</small></span>
</a>
<nav aria-label="Hauptmenü"><a href="#">Klassen</a><a href="#">Preise</a><a href="#kurse">Intensivkurse</a><a href="#">Kontakt</a></nav>
<a class="tel" href="#">07242 418 60</a>
</header>
<main>
<section class="buehne" aria-labelledby="titel">
<div class="text">
<p class="label">Fahrschule in Wels · Theorie am Abend</p>
<h1 id="titel">Führerschein in Wels. <em>Ohne Bammel.</em></h1>
<p class="lead">Wir holen dich zur ersten Fahrstunde zu Hause oder an der Schule ab. Ruhige Fahrlehrer:innen, feste Ansprechperson, Prüfung in drei Monaten.</p>
<div class="klassen" role="group" aria-labelledby="klassen-titel">
<p class="label" id="klassen-titel">Welche Klasse?</p>
<div class="chips">
<button class="chip" type="button" aria-pressed="true" data-klasse="B">B</button>
<button class="chip" type="button" aria-pressed="false" data-klasse="L17">L17</button>
<button class="chip" type="button" aria-pressed="false" data-klasse="A">A</button>
<button class="chip" type="button" aria-pressed="false" data-klasse="BE">BE</button>
</div>
<dl class="preise" aria-live="polite">
<dt>Grundbetrag</dt><dd data-p="grund">390 €</dd>
<dt>Fahrstunde 50 min</dt><dd data-p="stunde">62 €</dd>
<dt>Theorie</dt><dd data-p="theorie">12 Abende</dd>
</dl>
<p class="preise-fuss">Alle Preise inkl. MwSt., vollständiger Preisaushang im Büro.</p>
</div>
</div>
<form class="karte" novalidate aria-labelledby="buchen-titel">
<h2 id="buchen-titel">Erste Fahrstunde buchen</h2>
<p class="unter">50 Minuten, Abholung in Wels inklusive.</p>
<fieldset>
<legend class="label">Freie Termine diese Woche</legend>
<div class="slots">
<label class="slot"><input type="radio" name="termin" value="Dienstag, 3. November, 16:30"><span><b>Di 3.11.</b><time>16:30</time></span></label>
<label class="slot"><input type="radio" name="termin" value="Mittwoch, 4. November, 15:00"><span><b>Mi 4.11.</b><time>15:00</time></span></label>
<label class="slot"><input type="radio" name="termin" value="Donnerstag, 5. November, 17:15" checked><span><b>Do 5.11.</b><time>17:15</time></span></label>
</div>
</fieldset>
<div class="felder2">
<div class="feld" id="feld-name">
<label for="name">Vorname</label>
<input id="name" name="name" autocomplete="given-name" aria-describedby="name-fehler">
<p class="fehler" id="name-fehler">Bitte gib deinen Vornamen an.</p>
</div>
<div class="feld">
<label for="tel">Telefon <span style="font-weight:400">(für die SMS)</span></label>
<input id="tel" name="tel" type="tel" autocomplete="tel" inputmode="tel">
</div>
</div>
<div class="aktion">
<button class="taste" type="submit" id="buchen">
<span class="taste__schatten" aria-hidden="true"></span>
<span class="taste__kante" aria-hidden="true"></span>
<span class="taste__front">
<span class="taste__lade" aria-hidden="true"></span>
<span class="taste__text">Fahrstunde buchen</span>
<svg class="taste__pfeil" viewBox="0 0 20 20" aria-hidden="true"><path d="M3 10h13M11 4.5 16.5 10 11 15.5" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/></svg>
<svg class="taste__haken" viewBox="0 0 20 20" aria-hidden="true"><path d="m4 10.5 4 4 8-9" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"/></svg>
<kbd class="taste__kbd" aria-hidden="true">↵</kbd>
</span>
</button>
<a class="neben" href="#">Lieber anrufen</a>
</div>
<p class="status" id="status" role="status" aria-live="polite"></p>
<p class="klein">Kostenlos verschieben bis 24 Stunden vorher.</p>
</form>
</section>
<section class="kurse" id="kurse" aria-labelledby="kurse-titel">
<div class="kurse-innen">
<p class="label">Intensivkurse in den Ferien</p>
<h2 id="kurse-titel">Theorie in fünf Tagen.</h2>
<div class="kurs-liste">
<article class="kurs">
<p class="zeit">28.–30.12.2026 · 9:00–13:00</p>
<h3>Theorie kompakt, Winter</h3>
<p class="frei" data-voll id="voll-1">Ausgebucht. Nächster Kurs im Februar.</p>
<button class="taste" type="button" aria-disabled="true" aria-describedby="voll-1">
<span class="taste__schatten" aria-hidden="true"></span><span class="taste__kante" aria-hidden="true"></span>
<span class="taste__front"><span class="taste__text">Ausgebucht</span></span>
</button>
</article>
<article class="kurs">
<p class="zeit">15.–19.02.2027 · 9:00–13:00</p>
<h3>Theorie kompakt, Semesterferien</h3>
<p class="frei">Noch 4 Plätze frei.</p>
<button class="taste" type="button" id="platz">
<span class="taste__schatten" aria-hidden="true"></span><span class="taste__kante" aria-hidden="true"></span>
<span class="taste__front"><span class="taste__lade" aria-hidden="true"></span><span class="taste__text">Platz sichern</span>
<svg class="taste__pfeil" viewBox="0 0 20 20" aria-hidden="true"><path d="M3 10h13M11 4.5 16.5 10 11 15.5" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/></svg>
<svg class="taste__haken" viewBox="0 0 20 20" aria-hidden="true"><path d="m4 10.5 4 4 8-9" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"/></svg></span>
</button>
</article>
</div>
</div>
</section>
</main>
<div class="pult">
<p class="label" id="pult-titel">Variante</p>
<div class="pult__knoepfe" role="group" aria-labelledby="pult-titel">
<button type="button" data-v="stapel" aria-pressed="true">Stapel</button>
<button type="button" data-v="versatz-brutal" aria-pressed="false">Versatz</button>
<button type="button" data-v="keycap" aria-pressed="false">Keycap</button>
<button type="button" data-v="soft" aria-pressed="false">Soft</button>
</div>
<p class="pult__werte" data-werte></p>
</div>
<svg class="zeiger" data-zeiger viewBox="0 0 22 22" aria-hidden="true"><path d="M3 2 18 11l-7 1.5-3.4 6.5Z" fill="#142b50" stroke="#fff" stroke-width="1.4" stroke-linejoin="round"/></svg>
<script>
(() => {
const html = document.documentElement, body = document.body;
const reduziert = () => html.classList.contains('reduziert');
const VARIANTEN = ['stapel', 'versatz-brutal', 'keycap', 'soft'];
const WERTE = {
'stapel': 'Front −4 / −6 / −2 px · Hover 250 ms cubic-bezier(.3,.7,.4,1.5) · Druck 34 ms · zurück 600 ms',
'versatz-brutal': 'Rand 3 px · Versatz 5 px · Hover −2 px · Druck +5 px · steps(2) à 80 ms, Druck 0 ms',
'keycap': 'Unterkante 4 px · Druck 100 ms, zurück 180 ms cubic-bezier(0,0,.58,1)',
'soft': 'Kontur 1 px #566175 · Schatten ±6 px / 14 px · Druck innen · 100 / 180 ms'
};
/* ---------- Variante aus der URL ---------- */
const knoepfe = [...document.querySelectorAll('.pult__knoepfe button')];
const werteFeld = document.querySelector('[data-werte]');
const ausUrl = new URLSearchParams(location.search).get('v');
let aktuell = VARIANTEN.includes(ausUrl) ? ausUrl : 'stapel';
function variante(v, url) {
aktuell = v; body.dataset.variante = v; html.dataset.v = v;
knoepfe.forEach(k => k.setAttribute('aria-pressed', String(k.dataset.v === v)));
werteFeld.textContent = WERTE[v];
if (url) { 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)));
variante(aktuell);
/* ---------- Klassenwahl ---------- */
const PREISE = {
B: ['390 €', '62 €', '12 Abende'], L17: ['420 €', '62 €', '12 Abende'],
A: ['360 €', '68 €', '6 Abende'], BE: ['240 €', '78 €', '2 Abende']
};
const chips = [...document.querySelectorAll('.chip')];
function klasse(k) {
chips.forEach(c => c.setAttribute('aria-pressed', String(c.dataset.klasse === k)));
const [g, s, t] = PREISE[k];
document.querySelector('[data-p="grund"]').textContent = g;
document.querySelector('[data-p="stunde"]').textContent = s;
document.querySelector('[data-p="theorie"]').textContent = t;
}
chips.forEach(c => c.addEventListener('click', () => klasse(c.dataset.klasse)));
/* ---------- Buchung: Fehler, Laden, Erfolg ---------- */
const form = document.querySelector('form.karte');
const taste = document.getElementById('buchen');
const text = taste.querySelector('.taste__text');
const status = document.getElementById('status');
const feldName = document.getElementById('feld-name');
const name = document.getElementById('name');
let uhrBuchung = null;
function tasteZurueck(t, label) {
t.removeAttribute('data-zustand'); t.removeAttribute('aria-disabled');
t.querySelector('.taste__text').textContent = label;
}
function buchen(e) {
if (e) e.preventDefault();
if (taste.dataset.zustand) return;
if (!name.value.trim()) {
feldName.setAttribute('data-fehler', ''); name.setAttribute('aria-invalid', 'true');
status.textContent = ''; if (e) name.focus(); return false;
}
feldName.removeAttribute('data-fehler'); name.removeAttribute('aria-invalid');
const termin = form.querySelector('input[name="termin"]:checked').value;
taste.dataset.zustand = 'laedt'; taste.setAttribute('aria-disabled', 'true');
text.textContent = 'Wird gebucht …'; status.removeAttribute('data-art'); status.textContent = 'Termin wird gebucht.';
clearTimeout(uhrBuchung);
uhrBuchung = setTimeout(() => {
taste.dataset.zustand = 'gut';
text.textContent = 'Gebucht';
status.dataset.art = 'gut';
status.textContent = `Gebucht: ${termin}. Die Bestätigung kommt per SMS.`;
}, 1400);
return true;
}
form.addEventListener('submit', buchen);
name.addEventListener('input', () => { if (name.value.trim()) { feldName.removeAttribute('data-fehler'); name.removeAttribute('aria-invalid'); } });
taste.addEventListener('click', e => { if (taste.dataset.zustand === 'gut') { e.preventDefault(); } });
const platz = document.getElementById('platz');
platz.addEventListener('click', () => {
if (platz.dataset.zustand) return;
platz.dataset.zustand = 'laedt'; platz.setAttribute('aria-disabled', 'true');
platz.querySelector('.taste__text').textContent = 'Wird reserviert …';
setTimeout(() => { platz.dataset.zustand = 'gut'; platz.querySelector('.taste__text').textContent = 'Platz reserviert'; }, 1200);
});
document.querySelectorAll('.taste[aria-disabled="true"]').forEach(t => t.addEventListener('click', e => e.preventDefault()));
document.querySelectorAll('a[href="#"]').forEach(a => a.addEventListener('click', e => e.preventDefault()));
/* ---------- Vorführung: gedachter Zeiger fährt hin, hebt, drückt, bucht ---------- */
const zeiger = document.querySelector('[data-zeiger]');
let uhren = [], laeuft = false;
const spaeter = (fn, ms) => uhren.push(setTimeout(fn, ms));
function stopp() { uhren.forEach(clearTimeout); uhren = []; clearTimeout(uhrBuchung); }
function zeigerAuf(el, dx = .62, dy = .55) {
const r = el.getBoundingClientRect();
zeiger.style.transform = `translate(${r.left + r.width * dx}px, ${r.top + r.height * dy}px)`;
}
function grundzustand() {
stopp();
[taste, platz].forEach(t => t.classList.remove('ist-hover', 'ist-druck'));
tasteZurueck(taste, 'Fahrstunde buchen'); tasteZurueck(platz, 'Platz sichern');
feldName.removeAttribute('data-fehler'); name.removeAttribute('aria-invalid');
status.textContent = ''; status.removeAttribute('data-art');
form.reset(); klasse('B');
}
function neustart() { laeuft = false; html.classList.remove('vorfuehrung'); grundzustand(); variante(aktuell); }
function runde() {
grundzustand();
const ziel = taste.querySelector('.taste__front');
zeiger.style.transition = 'none';
zeiger.style.transform = `translate(${innerWidth * .5}px, ${innerHeight + 30}px)`;
zeiger.getBoundingClientRect(); zeiger.style.transition = '';
name.value = 'Lena';
spaeter(() => zeigerAuf(ziel), 200);
spaeter(() => taste.classList.add('ist-hover'), 900);
spaeter(() => taste.classList.add('ist-druck'), 1900);
spaeter(() => { taste.classList.remove('ist-druck'); buchen(); }, 2150);
spaeter(() => { zeiger.style.transform = `translate(${innerWidth - 60}px, ${innerHeight - 40}px)`; taste.classList.remove('ist-hover'); }, 4200);
spaeter(runde, 5600);
}
function vorfuehren() { stopp(); laeuft = true; html.classList.add('vorfuehrung'); runde(); }
function pause() { stopp(); laeuft = false; }
function weiter() { if (!laeuft && html.classList.contains('vorfuehrung')) vorfuehren(); }
})();
</script>
</body>
</html>