Zum Inhalt
← Animationsstile

Animationsstil „Studio-Signatur“ (asymmetrisches Gleiten)

Studio-Startseite, auf der Button, Slider, Akkordeon und Menü dieselbe eigene Kurve cubic-bezier(0.625, 0.05, 0, 1) mit krummen Dauern teilen; umschaltbar auf zwei weitere gemessene Hauskurven.

Eine eigene, asymmetrische inOut-Kurve als Markenzeichen für alle UI-Übergänge. Sie fährt kurz und zögernd an und läuft dann schnell, lang und sehr weich aus. Osmo nutzt cubic-bezier(0.625, 0.05, 0, 1) für Buttons, Schatten, Bildunterschriften und Slides. Dazu kommt eine kleine Dauer-Skala mit mittleren Werten zwischen 0,25 und 0,75 s. Kurve und Skala gelten konsequent für Hover, Menüs, Akkordeons und Slider-Navigation.

Die Kurve stammt nicht aus der Standardbibliothek. Darum wirkt jede Bewegung bewusst gestaltet und hochwertig. Weil alle Komponenten dieselbe Kurve teilen, entsteht Wiedererkennung wie bei einer Hausschrift.

Wann einsetzen

Agentur- und Portfolio-Websites, Marken mit Premiumanspruch und Komponentenbibliotheken. Als Standard-Ease für die ganze Oberfläche.

Wann nicht

Nicht für lange Einblendungen beim Scrollen, dort passt expo.out besser. Nicht für Bewegungen, die sofort reagieren müssen, etwa Drag-Feedback.

Varianten

  • Osmo-Skala: cubic-bezier(0.625, 0.05, 0, 1) mit 250 / 400 / 475 / 525 / 725 ms je nach Komponente
  • Ein Token: cubic-bezier(0.65, 0.05, 0, 1) mit einer Dauer von 750 ms für alles (Lando Norris)
  • Knapp: cubic-bezier(0, 0, 0, 1) mit 200 und 300 ms (Studio Dumbar), Gegenstück cubic-bezier(1, 0, 1, 1) für Ausgänge
  • In GSAP dieselbe Kurve per CustomEase

Umsetzung

CSS-Token im :root: --ease-signature: cubic-bezier(0.625, 0.05, 0, 1); --dur-ui-xs: .25s; --dur-ui-s: .4s; --dur-ui-m: .525s; --dur-ui-l: .725s; für Buttons .475s. Jede Transition nutzt nur diese Variablen. Bildunterschriften folgen dem Bild mit 300 ms Verzögerung, Gruppen mit 30 bis 50 ms Versatz. GSAP: CustomEase.create('signature', '0.625, 0.05, 0, 1'). Die krummen Dauern (475 / 525 / 725 ms) sind ein bewusstes Merkmal.

Robust umsetzen

Telefon. Kurve und Dauern bleiben gleich. Hover-Zustände auf :active und Fokus übertragen.

Ohne Bewegung. Farb-, Schatten- und Opazitätswechsel bleiben mit derselben Kurve. Verschiebungen, Rollen und clip-path-Fahrten entfallen.

Barrierefrei. Fokuszustände sofort sichtbar machen; die Outline nie animiert ein- oder ausblenden. Akkordeon mit aria-expanded und Region, Menü mit aria-expanded und Escape zum Schließen, Slider-Wechsel über eine aria-live-Region ansagen.

Code

<!DOCTYPE html>
<!--
  Animationsstil „Studio-Signatur“ (asymmetrisches Gleiten)
  Fundus · Animationsstile · https://cecodesign.de/lab/fundus/animationsstile/token-studio-signatur/
  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>Animationsstil „Studio-Signatur“ (asymmetrisches Gleiten) · 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: 'Instrument Serif'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/instrument-serif@latest/latin-400-italic.woff2') format('woff2'); font-weight: 400; font-style: italic; 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 {
  /* Die Token des Musters: eine Kurve, eine Dauer-Skala. JS tauscht sie je Variante. */
  --ease-signature: cubic-bezier(0.625, 0.05, 0, 1);
  --dur-ui-xs: .25s;    /* Schatten, Farben */
  --dur-ui-s: .4s;      /* Opazität, Unterstreichung */
  --dur-ui-btn: .475s;  /* Buttons */
  --dur-ui-m: .525s;    /* Bildunterschriften, Akkordeon */
  --dur-ui-l: .725s;    /* Slider, Menü */
  --versatz: 40ms;      /* Gruppen */
  --nachlauf: .3s;      /* Bildunterschrift folgt dem Bild */

  --grund: #ebe9e3; --papier: #f6f5f1; --text: #141414; --leise: rgba(20, 20, 20, .64); --linie: rgba(20, 20, 20, .14);
  --akzent: #e2461d; --nacht: #141414; --nacht-text: #f1efe9;
  --karte: clamp(228px, 26vw, 400px); --luecke: clamp(14px, 1.6vw, 22px);
}
* { box-sizing: border-box; margin: 0; padding: 0; }
html { background: var(--grund); color: var(--text); font-family: 'Instrument Sans', system-ui, sans-serif; -webkit-font-smoothing: antialiased; }
body { background: var(--grund); overflow-x: hidden; }
img { display: block; }
button { font: inherit; color: inherit; background: none; border: 0; cursor: pointer; }
a { color: inherit; text-decoration: none; }
ul { list-style: none; }
.mono { font: 500 11px/1.3 '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; }
/* Fokus sofort sichtbar, nie animiert */
:focus-visible { outline: 2px solid var(--akzent); outline-offset: 3px; transition: none; }

/* ---------- Bühne ---------- */
.buehne { position: relative; min-height: 100svh; display: grid; grid-template-rows: auto 1fr auto; gap: clamp(20px, 3.4vw, 44px); padding: clamp(18px, 3vw, 38px); overflow: hidden; }

/* Kopf */
.kopf { position: relative; z-index: 8; display: flex; align-items: center; justify-content: space-between; gap: 16px; }
.marke { transition: color var(--dur-ui-s) var(--ease-signature); display: flex; align-items: baseline; gap: 10px; font-weight: 600; font-size: 17px; letter-spacing: -.01em; }
.marke i { font: italic 400 19px/1 'Instrument Serif', Georgia, serif; color: var(--leise); }
.nav { display: flex; align-items: center; gap: clamp(18px, 2.4vw, 34px); }
.nav a { position: relative; font-size: 14px; padding: 6px 0; }
.nav a::after { content: ''; position: absolute; left: 0; right: 0; bottom: 2px; height: 1px; background: currentColor; transform: scaleX(0); transform-origin: 100% 50%; transition: transform var(--dur-ui-s) var(--ease-signature); }
.nav a:hover::after, .nav a.an::after { transform: scaleX(1); transform-origin: 0 50%; }
.menue-knopf { display: inline-flex; align-items: center; gap: 10px; height: 40px; padding: 0 16px 0 18px; border-radius: 999px; background: var(--text); color: var(--papier); font-size: 14px; position: relative; z-index: 7; transition: background-color var(--dur-ui-xs) var(--ease-signature), color var(--dur-ui-xs) var(--ease-signature); }
.menue-knopf .striche { position: relative; width: 16px; height: 10px; }
.menue-knopf .striche i { position: absolute; left: 0; right: 0; height: 1.5px; background: currentColor; transition: transform var(--dur-ui-l) var(--ease-signature); }
.menue-knopf .striche i:first-child { top: 1px; }
.menue-knopf .striche i:last-child { bottom: 1px; }
.menue-knopf .wort { display: grid; height: 1.2em; overflow: hidden; line-height: 1.2; }
.menue-knopf .wort span { grid-area: 1 / 1; transition: transform var(--dur-ui-l) var(--ease-signature); }
.menue-knopf .wort span + span { transform: translateY(110%); }
.offen .menue-knopf { background: var(--papier); color: var(--text); }
.offen .menue-knopf .striche i:first-child { transform: translateY(3.5px) rotate(45deg); }
.offen .menue-knopf .striche i:last-child { transform: translateY(-3.5px) rotate(-45deg); }
.offen .menue-knopf .wort span:first-child { transform: translateY(-110%); }
.offen .menue-knopf .wort span + span { transform: none; }

/* Inhalt */
.feld { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1.12fr); gap: clamp(24px, 4vw, 64px); align-items: start; min-height: 0; }
.text { display: grid; gap: clamp(16px, 2vw, 24px); align-content: start; padding-top: clamp(4px, 1.4vw, 20px); }
.rubrik { display: flex; gap: 10px; align-items: center; }
.rubrik::before { content: ''; width: 7px; height: 7px; border-radius: 50%; background: var(--akzent); }
h1 { font-weight: 560; font-size: clamp(40px, 5.6vw, 86px); line-height: .96; letter-spacing: -.045em; max-width: 9.5em; }
h1 em { font: italic 400 1.08em/1 'Instrument Serif', Georgia, serif; letter-spacing: -.02em; }
.vorspann { font-size: clamp(15px, 1.15vw, 17px); line-height: 1.5; color: var(--leise); max-width: 34ch; }
.knoepfe { display: flex; flex-wrap: wrap; gap: 10px; }

/* Button: Fläche fährt von unten ein, Beschriftung rollt */
.knopf { position: relative; display: inline-flex; align-items: center; gap: 12px; height: 48px; padding: 0 22px; border-radius: 999px; overflow: hidden; font-size: 15px; isolation: isolate; }
.knopf .flaeche { position: absolute; inset: 0; z-index: -1; border-radius: inherit; transform: translateY(101%); transition: transform var(--dur-ui-btn) var(--ease-signature); }
.knopf .rolle { display: grid; height: 1.25em; overflow: hidden; line-height: 1.25; }
.knopf .rolle span { grid-area: 1 / 1; transition: transform var(--dur-ui-btn) var(--ease-signature); }
.knopf .rolle span + span { transform: translateY(100%); }
.knopf svg { width: 14px; height: 14px; fill: none; stroke: currentColor; stroke-width: 1.6; transition: transform var(--dur-ui-btn) var(--ease-signature); }
.knopf:hover .flaeche, .knopf.an .flaeche { transform: none; }
.knopf:hover .rolle span:first-child, .knopf.an .rolle span:first-child { transform: translateY(-100%); }
.knopf:hover .rolle span + span, .knopf.an .rolle span + span { transform: none; }
.knopf:hover svg, .knopf.an svg { transform: translateX(4px) rotate(-45deg); }
.knopf.voll { background: var(--text); color: var(--papier); transition: color var(--dur-ui-xs) var(--ease-signature); }
.knopf.voll .flaeche { background: var(--akzent); }
.knopf.rand { box-shadow: inset 0 0 0 1px var(--linie); transition: color var(--dur-ui-btn) var(--ease-signature); }
.knopf.rand .flaeche { background: var(--text); }
.knopf.rand:hover, .knopf.rand.an { color: var(--papier); }

/* Akkordeon */
.leistungen { border-top: 1px solid var(--linie); max-width: 470px; }
.leistungen li { border-bottom: 1px solid var(--linie); }
.leistungen button { width: 100%; display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 13px 0; text-align: left; font-size: 15px; font-weight: 500; transition: color var(--dur-ui-xs) var(--ease-signature); }
.leistungen button span { display: flex; gap: 14px; align-items: baseline; }
.leistungen button small { font: 500 10px/1 'Geist Mono', ui-monospace, monospace; letter-spacing: .14em; color: var(--leise); }
.leistungen button:hover, .leistungen button.an { color: var(--akzent); }
.plus { position: relative; width: 12px; height: 12px; flex: none; }
.plus::before, .plus::after { content: ''; position: absolute; left: 0; right: 0; top: 50%; height: 1.5px; margin-top: -.75px; background: currentColor; transition: transform var(--dur-ui-m) var(--ease-signature); }
.plus::after { transform: rotate(90deg); }
[aria-expanded='true'] .plus::after { transform: rotate(0); }
[aria-expanded='true'] .plus::before { transform: rotate(180deg); }
.klappe { display: grid; grid-template-rows: 0fr; transition: grid-template-rows var(--dur-ui-m) var(--ease-signature); }
.klappe > div { overflow: hidden; }
.klappe p { font-size: 14px; line-height: 1.5; color: var(--leise); padding: 0 0 14px 34px; max-width: 40ch; opacity: 0; transform: translateY(-8px); transition: opacity var(--dur-ui-m) var(--ease-signature), transform var(--dur-ui-m) var(--ease-signature); }
.auf .klappe { grid-template-rows: 1fr; }
.auf .klappe p { opacity: 1; transform: none; }

/* Slider */
.schau { display: grid; gap: 16px; min-width: 0; }
.schau-kopf { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
.zaehler { display: flex; align-items: center; gap: 12px; }
.zaehler b { color: var(--text); font-weight: 500; display: inline-grid; height: 1.3em; overflow: hidden; }
.zaehler b span { grid-area: 1 / 1; transition: transform var(--dur-ui-l) var(--ease-signature), opacity var(--dur-ui-s) var(--ease-signature); }
.balken { width: 72px; height: 1px; background: var(--linie); position: relative; }
.balken i { position: absolute; inset: 0; background: var(--text); transform-origin: 0 50%; transform: scaleX(.2); transition: transform var(--dur-ui-l) var(--ease-signature); }
.pfeile { display: flex; gap: 8px; }
.pfeil { width: 44px; height: 44px; border-radius: 50%; background: var(--papier); display: grid; place-items: center; box-shadow: 0 1px 2px rgba(20, 20, 20, .08); transition: box-shadow var(--dur-ui-xs) var(--ease-signature), background-color var(--dur-ui-xs) var(--ease-signature), color var(--dur-ui-xs) var(--ease-signature); }
.pfeil svg { width: 16px; height: 16px; fill: none; stroke: currentColor; stroke-width: 1.5; }
.pfeil:hover, .pfeil.an { box-shadow: 0 12px 26px -10px rgba(20, 20, 20, .45); background: var(--text); color: var(--papier); }
.fenster { overflow: hidden; margin-right: calc(-1 * clamp(18px, 3vw, 38px)); touch-action: pan-y; user-select: none; }
.spur { display: flex; gap: var(--luecke); transition: transform var(--dur-ui-l) var(--ease-signature); will-change: transform; }
.karte { flex: none; width: var(--karte); display: grid; gap: 12px; opacity: .38; transition: opacity var(--dur-ui-s) var(--ease-signature); }
.karte.aktiv { opacity: 1; }
.bild { aspect-ratio: 4 / 5; overflow: hidden; border-radius: 4px; background: #d9d6ce; box-shadow: 0 2px 6px -2px rgba(20, 20, 20, .14); transition: box-shadow var(--dur-ui-xs) var(--ease-signature); }
.karte.aktiv .bild { box-shadow: 0 22px 36px -30px rgba(20, 20, 20, .5); }
.bild img { width: 100%; height: 100%; object-fit: cover; transform: scale(1.08); transition: transform var(--dur-ui-l) var(--ease-signature); -webkit-user-drag: none; }
.karte.aktiv .bild img { transform: none; }
.unterschrift { display: flex; justify-content: space-between; align-items: baseline; gap: 12px; }
.unterschrift > * { opacity: 0; transform: translateY(16px); transition: opacity var(--dur-ui-m) var(--ease-signature), transform var(--dur-ui-m) var(--ease-signature); }
.unterschrift strong { font-weight: 560; font-size: 17px; line-height: 1.15; letter-spacing: -.01em; white-space: nowrap; }
.unterschrift strong em { display: block; font: italic 400 18px/1.15 'Instrument Serif', Georgia, serif; color: var(--leise); }
.karte.aktiv .unterschrift > * { opacity: 1; transform: none; transition-delay: var(--nachlauf); }
.karte.aktiv .unterschrift > * + * { transition-delay: calc(var(--nachlauf) + var(--versatz)); }

/* Menü-Overlay */
.menue { position: absolute; inset: 0; z-index: 6; background: var(--nacht); color: var(--nacht-text); display: grid; grid-template-columns: minmax(0, 1fr) auto; grid-template-rows: auto auto; align-content: start; gap: clamp(28px, 4vw, 48px) clamp(24px, 4vw, 64px); padding: clamp(96px, 12vw, 130px) clamp(18px, 3vw, 38px) clamp(18px, 3vw, 38px); clip-path: inset(0 0 100% 0); visibility: hidden; transition: clip-path var(--dur-ui-l) var(--ease-signature), visibility 0s var(--dur-ui-l); }
.offen .menue { clip-path: inset(0 0 0 0); visibility: visible; transition: clip-path var(--dur-ui-l) var(--ease-signature), visibility 0s; }
.menue ul { align-self: start; display: grid; gap: 2px; }
.menue li { overflow: hidden; }
.menue li a { display: flex; align-items: baseline; gap: 18px; font-weight: 560; font-size: clamp(40px, 6.4vw, 92px); line-height: 1.02; letter-spacing: -.045em; transform: translateY(105%); transition: transform var(--dur-ui-l) var(--ease-signature), color var(--dur-ui-xs) var(--ease-signature); }
.menue li a small { font: 500 11px/1 'Geist Mono', ui-monospace, monospace; letter-spacing: .14em; color: rgba(241, 239, 233, .5); transform: translateY(-1.6em); }
.menue li a:hover { color: var(--akzent); }
.offen .menue li a { transform: none; transition-delay: calc(var(--i) * var(--versatz) + 120ms), 0s; }
.menue-fuss { grid-column: 1; display: flex; justify-content: space-between; gap: 16px; flex-wrap: wrap; color: rgba(241, 239, 233, .5); opacity: 0; transition: opacity var(--dur-ui-s) var(--ease-signature); }
.offen .menue-fuss { opacity: 1; transition-delay: var(--nachlauf); }
.menue-fuss b { color: var(--nacht-text); font-weight: 500; }
.menue-neu { grid-column: 2; grid-row: 1 / span 2; width: clamp(200px, 22vw, 300px); display: grid; gap: 12px; align-self: start; }
.menue-neu .rahmen { aspect-ratio: 4 / 5; overflow: hidden; border-radius: 4px; clip-path: inset(100% 0 0 0); transition: clip-path var(--dur-ui-l) var(--ease-signature); }
.menue-neu img { width: 100%; height: 100%; object-fit: cover; transform: scale(1.15); transition: transform var(--dur-ui-l) var(--ease-signature); }
.menue-neu p { color: rgba(241, 239, 233, .5); opacity: 0; transform: translateY(16px); transition: opacity var(--dur-ui-m) var(--ease-signature), transform var(--dur-ui-m) var(--ease-signature); }
.menue-neu p b { display: block; margin-top: 6px; font: italic 400 22px/1.15 'Instrument Serif', Georgia, serif; letter-spacing: 0; text-transform: none; color: var(--nacht-text); }
.offen .menue-neu .rahmen { clip-path: inset(0 0 0 0); transition-delay: 160ms; }
.offen .menue-neu img { transform: none; transition-delay: 160ms; }
.offen .menue-neu p { opacity: 1; transform: none; transition-delay: calc(160ms + var(--nachlauf)); }
.offen .modi button { color: rgba(241, 239, 233, .6); }
.offen .marke { color: var(--nacht-text); }
.offen .marke i { color: rgba(241, 239, 233, .5); }
.offen .nav a { visibility: hidden; }

/* Fuß: Varianten, Werte, Kurve */
.fuss { position: relative; z-index: 7; display: flex; align-items: center; justify-content: space-between; gap: 14px 28px; flex-wrap: wrap; padding-top: 14px; border-top: 1px solid var(--linie); transition: border-color var(--dur-ui-xs) var(--ease-signature); }
.modi { display: flex; gap: 4px; flex-wrap: wrap; }
.modi button { letter-spacing: inherit; text-transform: inherit; font-size: 11px; color: var(--leise); border: 1px solid var(--linie); border-radius: 999px; height: 30px; padding: 0 12px; transition: color .15s, background-color .15s, border-color .15s; }
.modi button:hover { color: var(--text); border-color: rgba(20, 20, 20, .4); }
.modi button[aria-pressed='true'] { background: var(--text); color: var(--papier); border-color: var(--text); }
.werte-zeile { flex: 1 1 380px; display: flex; align-items: center; justify-content: flex-end; gap: 14px; min-width: 0; }
.werte { display: flex; flex-wrap: wrap; gap: 5px 14px; justify-content: flex-end; }
.werte b { color: var(--text); font-weight: 500; }
.werte span { white-space: nowrap; }
.werte .jetzt b { color: #b93918; }   /* Akzent abgedunkelt für 11-px-Text (4,7:1) */
.kurvenbild { width: 46px; height: 30px; overflow: visible; flex: none; }
.kurvenbild path { fill: none; stroke: var(--text); stroke-width: 1.25; vector-effect: non-scaling-stroke; }
.kurvenbild .achse { stroke: var(--linie); }
.kurvenbild circle { fill: var(--akzent); }
.offen .fuss { border-color: rgba(241, 239, 233, .16); }
.offen .fuss .mono, .offen .werte b { color: rgba(241, 239, 233, .6); }
.offen .werte b { color: var(--nacht-text); }
.offen .kurvenbild path { stroke: var(--nacht-text); }
.offen .modi button { border-color: rgba(241, 239, 233, .2); }
.offen .modi button[aria-pressed='true'] { background: var(--nacht-text); color: var(--nacht); }

/* Gedachter Zeiger der Vorführung, gleitet mit der Signaturkurve */
.zeiger { position: absolute; left: 0; top: 0; z-index: 9; width: 22px; height: 22px; margin: -11px 0 0 -11px; border-radius: 50%; background: rgba(226, 70, 29, .9); box-shadow: 0 0 0 6px rgba(226, 70, 29, .18); pointer-events: none; display: none; transition: transform var(--dur-ui-l) var(--ease-signature), box-shadow var(--dur-ui-xs) var(--ease-signature); }
.vorfuehrung .zeiger { display: block; }
.zeiger.drueck { box-shadow: 0 0 0 14px rgba(226, 70, 29, .22); }

/* Vorschau: nur die Bühne */
.vorschau body { overflow: hidden; }
.vorschau .buehne { height: 100svh; }

@media (max-width: 1100px) and (min-width: 761px) {
  .nav a:not(:last-child) { display: none; }
  .vorspann { display: none; }
}
@media (max-height: 760px) and (min-width: 761px) {
  .vorspann { display: none; }
  .leistungen button { padding: 11px 0; }
}
@media (max-width: 760px) {
  :root { --karte: 68vw; }
  .nav a { display: none; }
  .feld { grid-template-columns: 1fr; gap: 34px; }
  h1 { font-size: clamp(40px, 12vw, 60px); }
  .schau { order: -1; }
  .fenster { margin-right: calc(-1 * clamp(18px, 3vw, 38px)); }
  .buehne { min-height: 0; grid-template-rows: auto auto auto; }
  .werte-zeile { margin-left: 0; justify-content: space-between; width: 100%; align-items: flex-start; }
  .werte { justify-content: flex-start; letter-spacing: .06em; }
  .marke i { display: none; }
  .menue { position: fixed; padding-top: 92px; grid-template-columns: 1fr; grid-template-rows: auto 1fr; align-content: stretch; }
  .menue-fuss { align-self: end; flex-direction: column; gap: 8px; }
  .menue-neu { display: none; }
}
@media (hover: none) {
  .knopf:active .flaeche { transform: none; }
  .knopf:active .rolle span:first-child { transform: translateY(-100%); }
  .knopf:active .rolle span + span { transform: none; }
  .knopf.rand:active { color: var(--papier); }
  .pfeil:hover { box-shadow: 0 1px 2px rgba(20, 20, 20, .08); background: var(--papier); color: var(--text); }
  .pfeil:active { box-shadow: 0 12px 26px -10px rgba(20, 20, 20, .45); background: var(--text); color: var(--papier); }
  .leistungen button:hover { color: inherit; }
}

/* Reduzierte Bewegung: Farben, Schatten und Opazität bleiben, Wege entfallen */
:root.reduziert .spur, :root.reduziert .bild img, :root.reduziert .balken i,
:root.reduziert .menue-knopf .striche i, :root.reduziert .menue-knopf .wort span, :root.reduziert .plus::before, :root.reduziert .plus::after,
:root.reduziert .knopf svg, :root.reduziert .zeiger { transition-property: none; }
:root.reduziert .bild img { transform: none; }
:root.reduziert .knopf .flaeche { transform: none; opacity: 0; transition: opacity var(--dur-ui-btn) var(--ease-signature); }
:root.reduziert .knopf:hover .flaeche, :root.reduziert .knopf.an .flaeche { opacity: 1; }
:root.reduziert .knopf .rolle span, :root.reduziert .knopf:hover .rolle span, :root.reduziert .knopf.an .rolle span { transform: none; transition: none; }
:root.reduziert .knopf .rolle span + span { visibility: hidden; }
:root.reduziert .knopf:hover svg, :root.reduziert .knopf.an svg { transform: none; }
:root.reduziert .unterschrift > *, :root.reduziert .klappe p, :root.reduziert .menue li a { transform: none; }
:root.reduziert .klappe { transition: none; }
:root.reduziert .menue { clip-path: none; opacity: 0; transition: opacity var(--dur-ui-s) var(--ease-signature), visibility 0s var(--dur-ui-s); }
:root.reduziert .offen .menue { opacity: 1; transition: opacity var(--dur-ui-s) var(--ease-signature), visibility 0s; }
:root.reduziert .menue-neu .rahmen { clip-path: none; transition: none; }
:root.reduziert .menue-neu img, :root.reduziert .menue-neu p { transform: none; transition-property: opacity; }
:root.reduziert .zaehler b span { transition: opacity var(--dur-ui-s) var(--ease-signature); }
@media (forced-colors: active) { button, [role=button], [role=tab], [role=switch] { border: 1px solid ButtonText !important; } }
</style>
</head>
<body>
<div class="buehne" data-buehne>
  <header class="kopf">
    <a class="marke" href="#">Kessel &amp; Wiehl <i>Gestaltung</i></a>
    <nav class="nav" aria-label="Hauptnavigation">
      <a href="#" data-ziel="nav">Arbeiten</a>
      <a href="#">Studio</a>
      <a href="#">Journal</a>
      <button class="menue-knopf" type="button" aria-expanded="false" aria-controls="menue" data-menue-knopf>
        <span class="wort"><span>Menü</span><span>Schließen</span></span>
        <span class="striche" aria-hidden="true"><i></i><i></i></span>
      </button>
    </nav>
  </header>

  <main class="feld">
    <div class="text">
      <p class="mono rubrik">Studio für Marken und Räume · Leipzig</p>
      <h1>Gestaltung, die man <em>wieder&shy;erkennt.</em></h1>
      <p class="vorspann">Wir entwickeln Erscheinungsbilder, Websites und Leitsysteme für Betriebe, die lange bleiben wollen. Jede Bewegung folgt bei uns derselben Kurve.</p>
      <div class="knoepfe">
        <a class="knopf voll" href="#" data-ziel="knopf"><span class="flaeche" aria-hidden="true"></span><span class="rolle"><span>Projekt anfragen</span><span aria-hidden="true">Projekt anfragen</span></span><svg viewBox="0 0 14 14" aria-hidden="true"><path d="M2 7h10M8 3l4 4-4 4"/></svg></a>
        <a class="knopf rand" href="#" data-ziel="knopf2"><span class="flaeche" aria-hidden="true"></span><span class="rolle"><span>Arbeitsweise</span><span aria-hidden="true">Arbeitsweise</span></span></a>
      </div>
      <ul class="leistungen" data-akkordeon>
        <li><button type="button" aria-expanded="false" aria-controls="k1" id="b1"><span><small>01</small>Erscheinungsbild</span><i class="plus" aria-hidden="true"></i></button>
          <div class="klappe" id="k1" role="region" aria-labelledby="b1"><div><p>Wortmarke, Farben, Schrift und ein Regelwerk, das auch die Druckerei versteht.</p></div></div></li>
        <li><button type="button" aria-expanded="false" aria-controls="k2" id="b2"><span><small>02</small>Websites</span><i class="plus" aria-hidden="true"></i></button>
          <div class="klappe" id="k2" role="region" aria-labelledby="b2"><div><p>Schnelle Seiten mit einer eigenen Bewegungssprache. Gebaut, gepflegt, gemessen.</p></div></div></li>
        <li><button type="button" aria-expanded="false" aria-controls="k3" id="b3"><span><small>03</small>Leitsysteme</span><i class="plus" aria-hidden="true"></i></button>
          <div class="klappe" id="k3" role="region" aria-labelledby="b3"><div><p>Beschilderung für Cafés, Praxen und Werkhallen, vom Plan bis zur Montage.</p></div></div></li>
      </ul>
    </div>

    <section class="schau" aria-roledescription="Karussell" aria-label="Ausgewählte Arbeiten">
      <div class="schau-kopf">
        <p class="mono zaehler"><b data-ziffer aria-hidden="true"></b><span>/ 05</span><span class="balken" aria-hidden="true"><i data-balken></i></span></p>
        <div class="pfeile">
          <button class="pfeil" type="button" data-richtung="-1" aria-label="Vorherige Arbeit"><svg viewBox="0 0 16 16" aria-hidden="true"><path d="M10 3 5 8l5 5"/></svg></button>
          <button class="pfeil" type="button" data-richtung="1" aria-label="Nächste Arbeit"><svg viewBox="0 0 16 16" aria-hidden="true"><path d="m6 3 5 5-5 5"/></svg></button>
        </div>
      </div>
      <div class="fenster" data-fenster><div class="spur" data-spur></div></div>
      <p class="sr" aria-live="polite" data-ansage></p>
    </section>
  </main>

  <footer class="fuss">
    <div class="modi mono" role="group" aria-label="Hauskurve">
      <button type="button" data-modus="osmo" aria-pressed="true">Fünf Stufen</button>
      <button type="button" data-modus="lando" aria-pressed="false">Ein Token</button>
      <button type="button" data-modus="dumbar" aria-pressed="false">Knapp</button>
    </div>
    <div class="werte-zeile">
      <p class="werte mono" data-werte></p>
      <svg class="kurvenbild" viewBox="0 0 1 1" preserveAspectRatio="none" aria-hidden="true">
        <path class="achse" d="M0 1 H1"/>
        <path data-kurve d="M0 1 C0.625 0.95 0 0 1 0"/>
        <circle data-punkt cx="1" cy="0" r=".08"/>
      </svg>
    </div>
  </footer>

  <div class="menue" id="menue" data-menue>
    <ul>
      <li><a href="#" style="--i:0">Arbeiten <small>24</small></a></li>
      <li><a href="#" style="--i:1">Studio <small>08</small></a></li>
      <li><a href="#" style="--i:2">Journal <small>13</small></a></li>
      <li><a href="#" style="--i:3">Kontakt</a></li>
    </ul>
    <figure class="menue-neu">
      <div class="rahmen"><img src="bilder/keramik-stillleben-hoch.jpg" alt="Keramik-Stillleben: helle Flasche, schwarze Kugelvase, gelber Becher und Schalen auf Sockeln vor Terrakotta" width="800" height="1000" loading="lazy"></div>
      <p class="mono">Neu im Journal<b>Ein Stillleben in Terrakotta</b></p>
    </figure>
    <div class="menue-fuss mono"><span>Karl-Heine-Straße 41 · <b>04229 Leipzig</b></span><span>hallo@kessel-wiehl.de</span></div>
  </div>

  <div class="zeiger" data-zeiger aria-hidden="true"></div>
</div>

<script>
(() => {
  const html = document.documentElement;
  const reduziert = () => html.classList.contains('reduziert');
  const $ = s => document.querySelector(s);

  // Drei gemessene Hauskurven. Dauern in ms: xs Schatten/Farbe, s Opazität, btn Button, m Unterschrift/Akkordeon, l Slider/Menü
  const MODI = {
    osmo:   { name: 'Fünf Stufen', bez: [0.625, 0.05, 0, 1], dauer: { xs: 250, s: 400, btn: 475, m: 525, l: 725 }, versatz: 40, nachlauf: 300 },
    lando:  { name: 'Ein Token',  bez: [0.65, 0.05, 0, 1],  dauer: { xs: 750, s: 750, btn: 750, m: 750, l: 750 }, versatz: 40, nachlauf: 300 },
    dumbar: { name: 'Knapp',      bez: [0, 0, 0, 1],        dauer: { xs: 200, s: 200, btn: 300, m: 300, l: 300 }, versatz: 30, nachlauf: 150 },
  };
  const NAMEN = { xs: 'Schatten', s: 'Opazität', btn: 'Button', m: 'Akkordeon', l: 'Navigation' };
  const ARBEITEN = [
    { bild: 'bilder/marke-roestkontor-hoch.jpg', titel: 'Röstkontor', art: 'Erscheinungsbild', jahr: '2025', alt: 'Wortbildmarke Röstkontor: Kaffeebohne im Kreis über cremefarbenen Versalien auf Ziegelrot' },
    { bild: 'bilder/marke-eselsohr-hoch.jpg', titel: 'Eselsohr', art: 'Wortbildmarke', jahr: '2024', alt: 'Wortbildmarke Eselsohr: Blatt mit rosa Eselsohr über kursivem Schriftzug auf Anthrazit' },
    { bild: 'bilder/flakon-hoch.jpg', titel: 'Ambra 9', art: 'Packaging', jahr: '2025', alt: 'Bernsteinfarbener Parfumflakon auf weißem Podest vor kräftigem Kobaltblau' },
    { bild: 'bilder/magazin-doppelseite-hoch.jpg', titel: 'Leise Dinge', art: 'Editorial', jahr: '2023', alt: 'Aufgeschlagenes Magazin mit der Überschrift „Die Ruhe der Dinge“ und zweispaltigem Text' },
    { bild: 'bilder/marke-krume-hoch.jpg', titel: 'Krume', art: 'Logo', jahr: '2026', alt: 'Wortmarke krume in fetten dunkelbraunen Kleinbuchstaben unter einem Brotlaib auf Senfgelb' },
  ];

  const buehne = $('[data-buehne]'), spur = $('[data-spur]'), fenster = $('[data-fenster]');
  const ziffer = $('[data-ziffer]'), balken = $('[data-balken]'), ansage = $('[data-ansage]');
  const werte = $('[data-werte]'), kurvePfad = $('[data-kurve]'), punkt = $('[data-punkt]');
  const menueKnopf = $('[data-menue-knopf]'), zeiger = $('[data-zeiger]');
  const knoepfeModus = [...document.querySelectorAll('[data-modus]')];
  const akkordeon = [...document.querySelectorAll('[data-akkordeon] button')];

  // Karten und Zifferband aufbauen
  const karten = ARBEITEN.map((a, i) => {
    const k = document.createElement('figure');
    k.className = 'karte';
    k.innerHTML = `<div class="bild"><img src="${a.bild}" alt="${a.alt}" width="800" height="1000" ${i > 1 ? 'loading="lazy"' : ''}></div>` +
      `<figcaption class="unterschrift"><strong>${a.titel}<em>${a.art}</em></strong><span class="mono">${a.jahr}</span></figcaption>`;
    spur.append(k);
    const z = document.createElement('span'); z.textContent = String(i + 1).padStart(2, '0');
    ziffer.append(z);
    return k;
  });
  const ziffern = [...ziffer.children];

  let modus = 'osmo', aktiv = 0;

  // Bezier lösen, damit der Punkt im Kurvenbild genau der CSS-Kurve folgt
  function bezier([x1, y1, x2, y2]) {
    const bx = t => 3 * x1 * t * (1 - t) ** 2 + 3 * x2 * t * t * (1 - t) + t ** 3;
    const by = t => 3 * y1 * t * (1 - t) ** 2 + 3 * y2 * t * t * (1 - t) + t ** 3;
    return x => { let lo = 0, hi = 1, t = x; for (let i = 0; i < 24; i++) { t = (lo + hi) / 2; if (bx(t) < x) lo = t; else hi = t; } return by(t); };
  }
  let punktLauf = 0;
  function zeigeBewegung(stufe) {
    const m = MODI[modus], ms = m.dauer[stufe];
    const jetzt = werte.querySelector('.jetzt');
    if (jetzt) jetzt.innerHTML = `${NAMEN[stufe]} <b>${ms} ms</b>`;
    if (reduziert()) return;
    const e = bezier(m.bez), t0 = performance.now(), lauf = ++punktLauf;
    const schritt = now => {
      if (lauf !== punktLauf) return;
      const x = Math.min(1, (now - t0) / ms);
      punkt.setAttribute('cx', x.toFixed(3)); punkt.setAttribute('cy', (1 - e(x)).toFixed(3));
      if (x < 1) requestAnimationFrame(schritt);
    };
    requestAnimationFrame(schritt);
  }

  function setzeModus(name) {
    modus = name;
    const m = MODI[name], r = html.style;
    r.setProperty('--ease-signature', `cubic-bezier(${m.bez.join(', ')})`);
    r.setProperty('--dur-ui-xs', m.dauer.xs + 'ms');
    r.setProperty('--dur-ui-s', m.dauer.s + 'ms');
    r.setProperty('--dur-ui-btn', m.dauer.btn + 'ms');
    r.setProperty('--dur-ui-m', m.dauer.m + 'ms');
    r.setProperty('--dur-ui-l', m.dauer.l + 'ms');
    r.setProperty('--versatz', m.versatz + 'ms');
    r.setProperty('--nachlauf', m.nachlauf + 'ms');
    knoepfeModus.forEach(b => b.setAttribute('aria-pressed', String(b.dataset.modus === name)));
    kurvePfad.setAttribute('d', `M0 1 C${m.bez[0]} ${1 - m.bez[1]} ${m.bez[2]} ${1 - m.bez[3]} 1 0`);
    const d = m.dauer;
    const skala = name === 'lando' ? `<span>Dauer <b>${d.l} ms für alles</b></span>`
      : `<span>Skala <b>${[...new Set([d.xs, d.s, d.btn, d.m, d.l])].join(' / ')} ms</b></span>`;
    werte.innerHTML = `<span>Kurve <b>cubic-bezier(${m.bez.join(', ')})</b></span>${skala}` +
      (reduziert() ? `<span>Reduziert <b>nur Farbe und Opazität</b></span>`
        : `<span>Nachlauf <b>${m.nachlauf} ms</b></span><span class="jetzt">Navigation <b>${d.l} ms</b></span>`);
  }

  function zeigeArbeit(i, stumm) {
    aktiv = (i + karten.length) % karten.length;
    const breite = karten[0].getBoundingClientRect().width + parseFloat(getComputedStyle(spur).columnGap || 0);
    spur.style.transform = `translateX(${-aktiv * breite}px)`;
    karten.forEach((k, n) => k.classList.toggle('aktiv', n === aktiv));
    ziffern.forEach((z, n) => {
      z.style.transform = `translateY(${(n - aktiv) * 100}%)`;
      z.style.opacity = n === aktiv ? 1 : 0;
    });
    balken.style.transform = `scaleX(${(aktiv + 1) / karten.length})`;
    if (!stumm) { ansage.textContent = `${ARBEITEN[aktiv].titel}, ${aktiv + 1} von ${karten.length}`; zeigeBewegung('l'); }
  }

  function klappe(btn, auf) {
    const offen = auf ?? btn.getAttribute('aria-expanded') !== 'true';
    akkordeon.forEach(b => { if (b !== btn) { b.setAttribute('aria-expanded', 'false'); b.parentElement.classList.remove('auf'); } });
    btn.setAttribute('aria-expanded', String(offen));
    btn.parentElement.classList.toggle('auf', offen);
    zeigeBewegung('m');
  }

  function menue(auf) {
    const offen = auf ?? !buehne.classList.contains('offen');
    buehne.classList.toggle('offen', offen);
    menueKnopf.setAttribute('aria-expanded', String(offen));
    zeigeBewegung('l');
  }

  // Bedienung
  document.querySelectorAll('[data-richtung]').forEach(b => b.addEventListener('click', () => { nutzer(); zeigeArbeit(aktiv + +b.dataset.richtung); }));
  akkordeon.forEach(b => b.addEventListener('click', () => { nutzer(); klappe(b); }));
  menueKnopf.addEventListener('click', () => { nutzer(); menue(); });
  knoepfeModus.forEach(b => b.addEventListener('click', () => { nutzer(); setzeModus(b.dataset.modus); }));
  document.querySelectorAll('a[href="#"]').forEach(a => a.addEventListener('click', e => e.preventDefault()));
  document.querySelectorAll('.knopf').forEach(k => k.addEventListener('pointerenter', () => zeigeBewegung('btn')));
  document.querySelectorAll('.pfeil').forEach(k => k.addEventListener('pointerenter', () => zeigeBewegung('xs')));
  addEventListener('keydown', e => {
    if (e.key === 'Escape' && buehne.classList.contains('offen')) { menue(false); menueKnopf.focus(); }
    if (e.target.closest('input, textarea')) return;
    if (e.key === 'ArrowRight' && e.target.closest('.schau')) zeigeArbeit(aktiv + 1);
    if (e.key === 'ArrowLeft' && e.target.closest('.schau')) zeigeArbeit(aktiv - 1);
  });
  // Wischen auf dem Slider (Touch und Maus)
  let start = null;
  fenster.addEventListener('pointerdown', e => { start = e.clientX; });
  addEventListener('pointerup', e => {
    if (start === null) return;
    const dx = e.clientX - start; start = null;
    if (Math.abs(dx) > 40) { nutzer(); zeigeArbeit(aktiv + (dx < 0 ? 1 : -1)); }
  });
  addEventListener('resize', () => zeigeArbeit(aktiv, true));

  // ---------- Vorführung: gedachter Zeiger fährt die Komponenten ab ----------
  const REIHE = ['osmo', 'lando', 'dumbar'];
  let uhren = [], runde = 0, laeuft = false;
  const nach = (ms, f) => uhren.push(setTimeout(f, ms));
  function stopp() { uhren.forEach(clearTimeout); uhren = []; laeuft = false; }
  function zeigerZu(sel) {
    const el = $(sel); if (!el) return;
    const r = el.getBoundingClientRect(), b = buehne.getBoundingClientRect();
    zeiger.style.transform = `translate(${r.left - b.left + r.width * .62}px, ${r.top - b.top + r.height * .58}px)`;
  }
  const an = (sel, ja) => { const el = $(sel); if (el) el.classList.toggle('an', ja); };
  const druck = () => { zeiger.classList.add('drueck'); setTimeout(() => zeiger.classList.remove('drueck'), 160); };
  function nutzer() { if (laeuft) { stopp(); html.classList.remove('vorfuehrung'); document.querySelectorAll('.an').forEach(e => e.classList.remove('an')); } }

  function rundeSpielen() {
    setzeModus(REIHE[runde % REIHE.length]);
    const d = MODI[modus].dauer;
    let t = 0;
    const schritt = (ms, f) => { t += ms; nach(t, f); };
    schritt(80, () => zeigerZu('[data-richtung="1"]'));
    schritt(650, () => { an('[data-richtung="1"]', true); zeigeBewegung('xs'); });
    schritt(260, () => { druck(); zeigeArbeit(aktiv + 1); });
    schritt(Math.max(1100, d.l + MODI[modus].nachlauf + 400), () => { druck(); zeigeArbeit(aktiv + 1); });
    schritt(Math.max(1100, d.l + 500), () => { an('[data-richtung="1"]', false); zeigerZu('[data-ziel="knopf"]'); });
    schritt(700, () => { an('[data-ziel="knopf"]', true); zeigeBewegung('btn'); });
    schritt(Math.max(1000, d.btn + 500), () => { an('[data-ziel="knopf"]', false); zeigerZu('#b2'); });
    schritt(700, () => { druck(); an('#b2', true); klappe($('#b2'), true); });
    schritt(Math.max(1100, d.m + 700), () => { an('#b2', false); klappe($('#b2'), false); zeigerZu('[data-menue-knopf]'); });
    schritt(760, () => { druck(); menue(true); });
    schritt(Math.max(1500, d.l + 900), () => { druck(); menue(false); });
    schritt(Math.max(900, d.l + 300), () => { runde++; rundeSpielen(); });
  }

  function grundzustand() {
    stopp();
    document.querySelectorAll('.an').forEach(e => e.classList.remove('an'));
    buehne.classList.remove('offen'); menueKnopf.setAttribute('aria-expanded', 'false');
    akkordeon.forEach(b => { b.setAttribute('aria-expanded', 'false'); b.parentElement.classList.remove('auf'); });
    zeigeArbeit(0, true);
  }
  function neustart() { html.classList.remove('vorfuehrung'); grundzustand(); setzeModus(modus); zeigeArbeit(1); }
  function vorfuehren() {
    grundzustand();
    html.classList.add('vorfuehrung');
    runde = 0; laeuft = true;
    const b = buehne.getBoundingClientRect();
    zeiger.style.transition = 'none';
    zeiger.style.transform = `translate(${b.width * .5}px, ${Math.min(b.height, innerHeight) * .7}px)`;
    zeiger.offsetWidth; zeiger.style.transition = '';
    rundeSpielen();
  }
  function pause() { stopp(); }
  function weiter() { if (html.classList.contains('vorfuehrung') && !laeuft) { laeuft = true; rundeSpielen(); } }

  setzeModus(modus);
  zeigeArbeit(0, true);
  document.fonts.ready.then(() => zeigeArbeit(aktiv, true));
  // Schalter „reduziert“ der Detailseite und Rahmen-Skript setzen die Klasse später
  let warReduziert = reduziert();
  new MutationObserver(() => { if (reduziert() !== warReduziert) { warReduziert = reduziert(); setzeModus(modus); } }).observe(html, { attributes: true, attributeFilter: ['class'] });

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