Zum Inhalt
← Scroll & Section-Animationen

Smooth Scroll mit Lenis

Mausrad-Scroll gleitet per lerp oder fester Dauer nach; scroll-gekoppelte Zeiger und Parallaxen laufen über denselben GSAP-Takt ohne Stufen.

Lenis glättet Mausrad- und Trackpad-Scroll. Die Seite läuft der Zielposition per Interpolation (lerp) oder mit fester Dauer und Easing hinterher, statt je Rastung zu springen. Scrollbar und Scroll-Position bleiben nativ, es gibt keinen verschobenen Content-Wrapper. Scroll-Animationen (ScrollTrigger, WebGL) hängen am selben RAF-Loop und laufen deshalb synchron. Touch scrollt standardmäßig nativ (syncTouch: false).

Parallaxe, Pins und WebGL-Szenen bewegen sich gleichmäßig statt in Stufen. Das wirkt ruhig und hochwertig. Auf Award-Seiten ist Lenis weiterhin Grundausstattung.

Wann einsetzen

Bei Seiten mit vielen scroll-gekoppelten Effekten oder mit WebGL, das synchron zum DOM laufen muss.

Wann nicht

Bei text- oder datenlastigen Seiten, Web-Apps und Shops mit vielen verschachtelten Scroll-Containern oder iframes (über iframes stoppt Lenis). Nicht als Selbstzweck: Ohne Scroll-Effekte bringt es nur Trägheit. Viele Branchen-Rezepte im Fundus schließen es deshalb aus, etwa Handwerk, Architektur und Gastro.

Varianten

  • Lenis Standard mit lerp 0.1
  • Lenis mit duration und easing statt lerp, z. B. 1.2 s und 1.001 − 2^(−10t) wie bei Wokine
  • Locomotive Scroll v5 (baut auf Lenis auf) mit data-scroll-speed
  • GSAP ScrollSmoother (seit 3.13 kostenlos) mit smooth 0.8, data-speed und data-lag
  • Lenis Snap (lenis/snap) statt CSS scroll-snap

Umsetzung

const lenis = new Lenis({ lerp: 0.1, anchors: true }); lenis.on('scroll', ScrollTrigger.update); gsap.ticker.add(t => lenis.raf(t * 1000)); gsap.ticker.lagSmoothing(0); Dazu lenis.css einbinden (u. a. html.lenis { height: auto }, .lenis-smooth { scroll-behavior: auto }). Vorgaben: lerp 0.1, smoothWheel true, syncTouch false, syncTouchLerp 0.075, wheelMultiplier 1, touchMultiplier 1, autoRaf false, allowNestedScroll false. Für den Betrieb mit fester Dauer duration und easing zusammen setzen, dann wird lerp ignoriert. ScrollTrigger mit scrub: true koppeln; ein Zahlenwert wie scrub: 1 glättet ein zweites Mal. Modals und eigene Scroll-Bereiche bekommen data-lenis-prevent oder die prevent-Funktion. Grenzen laut README: kein CSS scroll-snap, Safari auf 60 fps begrenzt (30 fps im Stromsparmodus), keine Wirkung über iframes, position: fixed kann auf Safari vor M1 ruckeln. Die Demo nutzt 1.3.26, das README verlinkt 1.3.27. Einschätzung 2026: weiterhin Standard bei Award-Seiten, der Trend geht zu höherem lerp (spürbar, aber nicht schwammig) und nativem Touch.

Robust umsetzen

Telefon. syncTouch auf false lassen, dann scrollt Touch nativ. syncTouch ist auf iOS vor Version 16 instabil.

Ohne Bewegung. Bei prefers-reduced-motion Lenis nicht starten oder lerp: 1 setzen. Lenis 1.3.26 prüft das zusätzlich selbst (Option respectReducedMotion, Vorgabe true) und scrollt dann ohne Glättung.

Barrierefrei. Die native Scrollbar bleibt, Suchen auf der Seite funktioniert. Ankerlinks mit anchors: true anspringen. Tab-Fokus prüfen: Bei Fokus-Sprüngen darf Lenis nicht eingreifen.

Code

<!DOCTYPE html>
<!--
  Smooth Scroll mit Lenis
  Fundus · Scroll & Section-Animationen · https://cecodesign.de/lab/fundus/scroll/smooth-scroll-lenis/
  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>Smooth Scroll mit Lenis · 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: 'Bodoni Moda'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/bodoni-moda:vf@latest/latin-opsz-normal.woff2') format('woff2-variations'); font-weight: 400 900; font-style: normal; font-display: swap; }
@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; }

/* Rezept luxus-manufaktur-stille: Anthrazit und Weiß im Wechsel, Messing nur als Linie */
:root {
  --weiss: #ffffff; --anthrazit: #23282f; --flaeche: #f4f4f2; --meta: #676c73; --linie: #d9dadb; --messing: #a88a5a;
  --anthrazit-2: #2c3239; --hell-leise: rgba(255, 255, 255, .62); --hell-linie: rgba(255, 255, 255, .14);
  --ui-kurve: cubic-bezier(0.1, 0.6, 0.4, 1);   /* Rezept-Token, nur für Bedienelemente */
  --ui: 200ms;
  --kopf: 64px;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
html { background: var(--weiss); color: var(--anthrazit); font: 400 15px/1.47 'Instrument Sans', system-ui, sans-serif; -webkit-font-smoothing: antialiased; text-rendering: optimizeLegibility; }
body { background: var(--weiss); }
a { color: inherit; text-decoration: none; }
img, svg { display: block; }

/* Lenis-Stylesheet (lenis/dist/lenis.css, inline) */
html.lenis, html.lenis body { height: auto; }
.lenis:not(.lenis-autoToggle).lenis-stopped { overflow: clip; }
.lenis [data-lenis-prevent] { overscroll-behavior: contain; }
.lenis.lenis-smooth iframe { pointer-events: none; }
.lenis.lenis-smooth { scroll-behavior: auto !important; }

.versal { font-size: .75rem; line-height: 1.3; font-weight: 500; letter-spacing: .08em; text-transform: uppercase; }
.leise { color: var(--meta); }
.serif { font-family: 'Bodoni Moda', 'Times New Roman', serif; font-optical-sizing: auto; font-weight: 400; letter-spacing: -.01em; }
.ziffern { font-variant-numeric: tabular-nums lining-nums; }

/* Kopf */
.kopf { position: fixed; inset: 0 0 auto; height: var(--kopf); z-index: 20; display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; padding: 0 clamp(16px, 3vw, 40px); background: var(--weiss); border-bottom: 1px solid var(--linie); }
.marke { display: flex; align-items: baseline; gap: 12px; }
.marke b { font: 500 21px/1 'Bodoni Moda', serif; letter-spacing: .2em; }
.marke span { color: var(--meta); }
.kopf nav { display: flex; gap: clamp(18px, 2.4vw, 36px); }
.kopf nav a { position: relative; padding: 6px 0; }
.kopf nav a::after { content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 1px; background: var(--messing); transform: scaleX(0); transform-origin: left; transition: transform var(--ui) var(--ui-kurve); }
.kopf nav a:hover::after, .kopf nav a:focus-visible::after { transform: scaleX(1); }
.knopf { justify-self: end; display: inline-flex; align-items: center; height: 40px; padding: 0 18px; border: 1px solid var(--anthrazit); color: var(--anthrazit); background: transparent; transition: background-color var(--ui) var(--ui-kurve), color var(--ui) var(--ui-kurve); }
.knopf:hover, .knopf:focus-visible { background: var(--anthrazit); color: var(--weiss); }
.knopf--voll { background: var(--anthrazit); color: var(--weiss); }
.knopf--voll:hover, .knopf--voll:focus-visible { background: #343b45; }
:focus-visible { outline: 1px solid var(--messing); outline-offset: 3px; }

/* A · Neuheit: Text links scrollt, Uhr rechts steht */
.strecke { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); padding-top: var(--kopf); }
.texte { padding: 0 clamp(20px, 4.5vw, 72px); }
.kapitel { min-height: 78svh; display: grid; align-content: center; gap: 22px; border-top: 1px solid var(--linie); padding: 10vh 0; }
.kapitel:first-child { min-height: calc(100svh - var(--kopf)); border-top: 0; padding-top: 6vh; }
.kapitel__kopf { display: flex; justify-content: space-between; gap: 16px; }
h1.serif { font-size: clamp(2.75rem, 5.6vw, 5.5rem); line-height: 1.04; max-width: 9ch; }
h1 em, h2 em { font-style: normal; color: var(--messing); }
.lead { max-width: 34ch; font-size: 1.0625rem; line-height: 1.5; color: #4a5058; text-wrap: pretty; }
.aktionen { display: flex; gap: 22px; align-items: center; flex-wrap: wrap; margin-top: 8px; }
.aktionen .knopf { justify-self: start; }
.verweis { border-bottom: 1px solid var(--linie); padding-bottom: 3px; transition: border-color var(--ui) var(--ui-kurve); }
.verweis:hover, .verweis:focus-visible { border-color: var(--messing); }
.zahl { display: flex; align-items: baseline; gap: 14px; }
.zahl b { font: 400 clamp(5rem, 10.5vw, 9.5rem)/.9 'Bodoni Moda', serif; font-optical-sizing: auto; letter-spacing: -.02em; font-variant-numeric: lining-nums; }
.zahl span { font: 400 clamp(1.25rem, 2vw, 1.75rem)/1 'Bodoni Moda', serif; }
.kapitel p:not(.versal) { max-width: 36ch; color: #4a5058; text-wrap: pretty; }

.produkt { position: sticky; top: var(--kopf); height: calc(100svh - var(--kopf)); padding: clamp(16px, 2vw, 28px); padding-left: 0; }
.produkt__flaeche { position: relative; height: 100%; background: var(--flaeche); display: grid; justify-items: center; align-content: start; padding-top: clamp(52px, 8vh, 80px); overflow: hidden; }
/* Uhr sitzt oben, damit das Messpult unten rechts nur das Band überdeckt */
.uhr { height: min(58%, 520px); width: auto; max-width: 92%; overflow: visible; }
.produkt__zeile { position: absolute; left: 20px; right: 20px; top: 18px; display: flex; justify-content: space-between; gap: 12px; }
.produkt__hinweis { position: absolute; left: 20px; bottom: 18px; }

/* B · Werkstatt (Anthrazit) */
.werkstatt { background: var(--anthrazit); color: var(--weiss); padding: 16vh clamp(20px, 4.5vw, 72px) 18vh; }
.werkstatt__kopf { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 32px; align-items: end; margin-bottom: 9vh; }
h2.serif { font-size: clamp(2.25rem, 4.4vw, 4.25rem); line-height: 1.05; max-width: 12ch; }
.werkstatt__kopf p:not(.versal) { max-width: 40ch; color: var(--hell-leise); justify-self: end; }
.werkstatt .leise { color: rgba(255, 255, 255, .5); }
.schritte { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: clamp(14px, 2.2vw, 32px); }
.schritt:nth-child(2) { margin-top: 10vh; }
.schritt:nth-child(3) { margin-top: 4vh; }
.rahmen { aspect-ratio: 4 / 5; background: var(--anthrazit-2); overflow: hidden; position: relative; }
.rahmen svg { position: absolute; inset: -6% -6%; width: 112%; height: 112%; }
.schritt figcaption { display: flex; justify-content: space-between; gap: 12px; padding-top: 14px; border-top: 1px solid var(--hell-linie); margin-top: 14px; }

/* C · Termin */
.termin { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 40px; padding: 16vh clamp(20px, 4.5vw, 72px) 10vh; }
.termin p:not(.versal) { max-width: 38ch; color: #4a5058; margin: 22px 0 28px; }
.termin dl { display: grid; grid-template-columns: auto 1fr; gap: 14px 28px; align-self: end; border-top: 1px solid var(--linie); padding-top: 22px; }
.termin dt { color: var(--meta); }
.fuss { display: flex; justify-content: space-between; gap: 16px; flex-wrap: wrap; padding: 22px clamp(20px, 4.5vw, 72px) 120px; border-top: 1px solid var(--linie); color: var(--meta); }

/* Messpult der Demo */
.pult { position: fixed; right: calc(clamp(16px, 2vw, 28px) + 14px); bottom: calc(clamp(16px, 2vw, 28px) + 14px); z-index: 30; width: 304px; padding: 14px; background: rgba(255, 255, 255, .97); border: 1px solid var(--linie); box-shadow: 0 8px 30px rgba(35, 40, 47, .08); display: grid; gap: 11px; }
.mono { font: 500 10.5px/1.45 'Geist Mono', ui-monospace, monospace; letter-spacing: .14em; text-transform: uppercase; color: var(--meta); }
.mono b { color: var(--anthrazit); font-weight: 500; }
.pult__kopf { display: flex; justify-content: space-between; gap: 10px; }
.modi { display: grid; grid-template-columns: repeat(3, 1fr); border: 1px solid var(--linie); }
.modi button { font: inherit; letter-spacing: .06em; text-transform: none; color: var(--meta); background: none; border: 0; height: 30px; cursor: pointer; transition: background-color 150ms var(--ui-kurve), color 150ms var(--ui-kurve); }
.modi button + button { border-left: 1px solid var(--linie); }
.modi button[aria-pressed='true'] { background: var(--anthrazit); color: var(--weiss); }
.modi button:disabled { opacity: .35; cursor: not-allowed; }
.kurve { width: 100%; height: 64px; background: var(--flaeche); }
.legende { display: flex; justify-content: space-between; gap: 8px; }
.legende i { display: inline-block; width: 12px; height: 0; border-top: 1.5px solid var(--messing); vertical-align: middle; margin-right: 6px; }
.legende i.ziel { border-top: 1px dashed var(--meta); }
.pult__werte { border-top: 1px solid var(--linie); padding-top: 10px; text-transform: none; letter-spacing: .04em; }

/* Vorschau: nur die Bühne */
.vorschau body { overflow: hidden; }
.vorschau .kopf nav { display: none; }
.vorschau .kopf { grid-template-columns: 1fr auto; }
.vorschau .pult { width: 340px; gap: 12px; }
.vorschau .kurve { height: 92px; }
.vorschau .produkt__flaeche { align-content: center; padding-top: 0; }
.vorschau .uhr { height: 68%; align-self: center; }
.reduziert .kurve, .reduziert .legende { display: none; }
.vorschau .mono { font-size: 11.5px; }

/* Reduzierte Bewegung: kein Lenis, keine Parallaxe (JS), UI ohne Übergänge */
:root.reduziert * { transition-duration: .01ms !important; animation-duration: .01ms !important; }

@media (max-width: 900px) {
  .kopf { grid-template-columns: 1fr auto; --kopf: 56px; }
  .kopf nav { display: none; }
}
@media (max-width: 760px) {
  :root { --kopf: 56px; }
  .kopf { height: 56px; }
  .marke b { font-size: 18px; }
  .marke span { display: none; }
  .knopf { height: 36px; padding: 0 14px; }
  .strecke { grid-template-columns: 1fr; }
  .produkt { order: -1; z-index: 5; height: 44svh; padding: 0; background: var(--weiss); }
  .produkt__flaeche { align-content: center; padding: 30px 0 6px; }
  .uhr { height: calc(44svh - 40px); }
  .produkt__zeile { left: 14px; right: 14px; top: 10px; }
  .produkt__hinweis { display: none; }
  .kapitel { min-height: 62svh; padding: 8vh 0; }
  .kapitel:first-child { min-height: 0; align-content: start; padding: 36px 0 12vh; }
  .werkstatt__kopf, .termin { grid-template-columns: 1fr; }
  .werkstatt__kopf p:not(.versal) { justify-self: start; }
  .schritte { grid-template-columns: 1fr; gap: 40px; }
  .schritt:nth-child(n) { margin-top: 0; }
  .rahmen { aspect-ratio: 4 / 3; }
  .pult { left: 10px; right: 10px; bottom: 10px; width: auto; padding: 10px 12px; gap: 8px; }
  .fuss { padding-bottom: 150px; }
}
@media (hover: none) {
  /* Touch scrollt nativ (syncTouch: false): Kurve und Legende entfallen, ein Hinweis ersetzt sie */
  .kurve, .legende, .pult__kopf { display: none; }
}
/* Kontrastmodus: Knöpfe mit Rahmen */
@media (forced-colors: active) { button, [role=button], [role=tab], [role=switch] { border: 1px solid ButtonText !important; } }
</style>
</head>
<body>
<header class="kopf">
  <a class="marke" href="#oben" aria-label="Brehme Uhren, Erfurt, zum Anfang"><b>BREHME</b><span class="versal">Uhren · Erfurt</span></a>
  <nav class="versal" aria-label="Hauptnavigation">
    <a href="#kaliber">Zeitmesser</a><a href="#werkstatt">Manufaktur</a><a href="#termin">Service</a>
  </nav>
  <a class="knopf versal" href="#termin">Termin vereinbaren</a>
</header>

<main id="oben">
  <section class="strecke" aria-label="Neuheit Domstufen 38">
    <div class="texte">
      <article class="kapitel">
        <p class="versal leise">Neuheit 2026 · Domstufen 38</p>
        <h1 class="serif">Zeiger, die <em>gleiten.</em></h1>
        <p class="lead">Handaufzug, Kaliber B-31, Gehäuse aus Edelstahl. Gebaut, reguliert und geprüft in unserer Werkstatt in der Erfurter Altstadt.</p>
        <div class="aktionen versal">
          <a class="knopf knopf--voll" href="#termin">Termin in der Werkstatt</a>
          <a class="verweis" href="#kaliber">Das Kaliber</a>
        </div>
      </article>
      <article class="kapitel" id="kaliber">
        <div class="kapitel__kopf versal leise"><span>Gangreserve</span><span class="ziffern">02 / 04</span></div>
        <div class="zahl"><b>72</b><span>Stunden</span></div>
        <p>Drei Tage, ohne aufzuziehen. Zwei Federhäuser arbeiten in Serie und geben die Kraft gleichmäßig ab.</p>
      </article>
      <article class="kapitel">
        <div class="kapitel__kopf versal leise"><span>Halbschwingungen</span><span class="ziffern">03 / 04</span></div>
        <div class="zahl"><b class="ziffern">21.600</b><span>A/h</span></div>
        <p>Sechs Schläge pro Sekunde. Der kleine Sekundenzeiger bei sechs Uhr zeigt jeden einzelnen.</p>
      </article>
      <article class="kapitel">
        <div class="kapitel__kopf versal leise"><span>Durchmesser</span><span class="ziffern">04 / 04</span></div>
        <div class="zahl"><b class="ziffern">38,5</b><span>mm</span></div>
        <p>9,4 Millimeter hoch, mit Glasboden. Das Zifferblatt ist in neun Durchgängen von Hand bedruckt.</p>
      </article>
    </div>

    <figure class="produkt" aria-label="Domstufen 38, Zeiger folgen dem Scrollen">
      <div class="produkt__flaeche">
        <div class="produkt__zeile versal leise"><span>Domstufen 38 · Ref. B31-038</span><span class="ziffern">4.850 €</span></div>
        <svg class="uhr" viewBox="0 0 400 560" role="img" aria-label="Armbanduhr Domstufen 38 mit weißem Zifferblatt">
          <defs>
            <linearGradient id="stahl" x1="0" y1="0" x2="1" y2="1">
              <stop offset="0" stop-color="#f7f8f9"/><stop offset=".45" stop-color="#c3c6ca"/><stop offset=".7" stop-color="#eceef0"/><stop offset="1" stop-color="#a9adb2"/>
            </linearGradient>
            <linearGradient id="stahl2" x1="1" y1="0" x2="0" y2="1">
              <stop offset="0" stop-color="#f4f5f6"/><stop offset=".5" stop-color="#d2d5d8"/><stop offset="1" stop-color="#f0f1f2"/>
            </linearGradient>
            <radialGradient id="blatt" cx=".42" cy=".36" r=".75">
              <stop offset="0" stop-color="#ffffff"/><stop offset="1" stop-color="#eeede8"/>
            </radialGradient>
            <linearGradient id="leder" x1="0" y1="0" x2="1" y2="0">
              <stop offset="0" stop-color="#1d2127"/><stop offset=".5" stop-color="#2d333b"/><stop offset="1" stop-color="#1d2127"/>
            </linearGradient>
            <filter id="schatten" x="-30%" y="-30%" width="160%" height="160%"><feDropShadow dx="0" dy="10" stdDeviation="12" flood-color="#23282f" flood-opacity=".16"/></filter>
          </defs>
          <!-- Band -->
          <path d="M138 0 H262 L272 175 H128 Z" fill="url(#leder)"/>
          <path d="M128 385 H272 L262 560 H138 Z" fill="url(#leder)"/>
          <path d="M147 0 L139 170 M253 0 L261 170 M139 390 L147 560 M261 390 L253 560" stroke="#4d545d" stroke-width="1" stroke-dasharray="4 4" fill="none"/>
          <!-- Hörner und Gehäuse -->
          <g filter="url(#schatten)">
            <path d="M136 150 L150 120 H166 L162 165 Z M264 150 L250 120 H234 L238 165 Z M136 410 L150 440 H166 L162 395 Z M264 410 L250 440 H234 L238 395 Z" fill="url(#stahl)" stroke="#9a9ea3" stroke-width=".6"/>
            <rect x="326" y="266" width="18" height="28" rx="3" fill="url(#stahl)" stroke="#9a9ea3" stroke-width=".6"/>
            <circle cx="200" cy="280" r="134" fill="url(#stahl)" stroke="#9a9ea3" stroke-width=".7"/>
          </g>
          <path d="M330 270 V290 M334 270 V290 M338 270 V290" stroke="#9a9ea3" stroke-width=".6"/>
          <circle cx="200" cy="280" r="124" fill="url(#stahl2)"/>
          <circle cx="200" cy="280" r="117" fill="url(#blatt)" stroke="#c9cbce" stroke-width=".5"/>
          <g id="skala"></g>
          <text x="200" y="238" text-anchor="middle" font-family="Bodoni Moda, serif" font-size="13" letter-spacing="3.4" fill="#23282f">BREHME</text>
          <text x="200" y="249" text-anchor="middle" font-family="Instrument Sans, sans-serif" font-size="5.4" letter-spacing="2.4" fill="#8b9097">ERFURT</text>
          <text x="200" y="307" text-anchor="middle" font-family="Instrument Sans, sans-serif" font-size="5.2" letter-spacing="1.8" fill="#8b9097">HANDAUFZUG</text>
          <!-- Kleine Sekunde bei 6 Uhr -->
          <circle cx="200" cy="340" r="24" fill="#f6f5f1" stroke="#d9dadb" stroke-width=".6"/>
          <g id="skala-klein"></g>
          <g id="sekunde"><line x1="200" y1="346" x2="200" y2="319" stroke="#a88a5a" stroke-width=".9"/><circle cx="200" cy="340" r="1.8" fill="#a88a5a"/></g>
          <!-- Zeiger (Dauphine), an den Scroll gekoppelt -->
          <g id="stunde"><path d="M200 208 L206.5 280 L200 292 L193.5 280 Z" fill="#23282f"/><path d="M200 208 L206.5 280 L200 292 Z" fill="#3e4652"/></g>
          <g id="minute"><path d="M200 172 L205 280 L200 294 L195 280 Z" fill="#23282f"/><path d="M200 172 L205 280 L200 294 Z" fill="#3e4652"/></g>
          <circle cx="200" cy="280" r="5" fill="#23282f"/><circle cx="200" cy="280" r="1.7" fill="#a88a5a"/>
        </svg>
        <p class="produkt__hinweis versal leise">Die Zeiger folgen dem Scroll</p>
      </div>
    </figure>
  </section>

  <section class="werkstatt" id="werkstatt" aria-labelledby="werkstatt-titel">
    <div class="werkstatt__kopf">
      <div>
        <p class="versal leise" style="margin-bottom:18px">Manufaktur</p>
        <h2 class="serif" id="werkstatt-titel">Drei Wochen für <em>ein</em> Werk.</h2>
      </div>
      <p>Sieben Uhrmacherinnen und Uhrmacher, ein Raum mit Nordlicht. Jedes Kaliber B-31 geht durch dieselben Hände, vom Fräsen der Platine bis zur letzten Regulierung.</p>
    </div>
    <div class="schritte">
      <figure class="schritt"><div class="rahmen"><svg viewBox="0 0 400 500" data-zeichnung="platine" data-tempo="1" aria-hidden="true"></svg></div><figcaption class="versal"><span>01 · Platine fräsen</span><span class="leise ziffern">6 h</span></figcaption></figure>
      <figure class="schritt"><div class="rahmen"><svg viewBox="0 0 400 500" data-zeichnung="raeder" data-tempo="-1" aria-hidden="true"></svg></div><figcaption class="versal"><span>02 · Räderwerk montieren</span><span class="leise ziffern">2 Tage</span></figcaption></figure>
      <figure class="schritt"><div class="rahmen"><svg viewBox="0 0 400 500" data-zeichnung="blatt" data-tempo="1" aria-hidden="true"></svg></div><figcaption class="versal"><span>03 · Zifferblatt drucken</span><span class="leise ziffern">9 Durchgänge</span></figcaption></figure>
    </div>
  </section>

  <section class="termin" id="termin" aria-labelledby="termin-titel">
    <div>
      <p class="versal leise">Service</p>
      <h2 class="serif" id="termin-titel" style="margin-top:18px">Termin in der Werkstatt.</h2>
      <p>Wir zeigen Ihnen die Domstufen 38 am Werktisch, nehmen Maß für das Band und beantworten Fragen zu Revision und Pflege. Ein Termin dauert etwa eine Stunde.</p>
      <a class="knopf knopf--voll versal" href="#termin">Termin anfragen</a>
    </div>
    <dl class="versal">
      <dt>Werkstatt</dt><dd>Allerheiligenstraße 12, 99084 Erfurt</dd>
      <dt>Geöffnet</dt><dd class="ziffern">Di bis Fr 10 bis 18 Uhr, Sa 10 bis 14 Uhr</dd>
      <dt>Revision</dt><dd class="ziffern">alle 5 Jahre, ab 420 €</dd>
      <dt>Garantie</dt><dd class="ziffern">5 Jahre auf das Kaliber B-31</dd>
    </dl>
  </section>
  <footer class="fuss versal"><span>Brehme Uhren · Erfurt</span><span>Mitglied der Handwerkskammer Erfurt</span></footer>
</main>

<aside class="pult" aria-label="Demo-Steuerung: Smooth Scroll">
  <div class="pult__kopf mono"><span><b>Smooth Scroll</b></span><span>Lenis 1.3.26</span></div>
  <div class="modi mono" role="group" aria-label="Variante">
    <button type="button" data-variante="nativ" aria-pressed="false">Nativ</button>
    <button type="button" data-variante="lerp" aria-pressed="true">lerp 0.1</button>
    <button type="button" data-variante="dauer" aria-pressed="false">duration 1.2</button>
  </div>
  <canvas class="kurve" width="274" height="64" aria-hidden="true"></canvas>
  <div class="legende mono"><span><i class="ziel"></i>Ziel</span><span><i></i>Ist</span><span data-live>Δ 0 px</span></div>
  <p class="pult__werte mono" data-werte></p>
</aside>

<script src="https://cdn.jsdelivr.net/npm/lenis@1.3.26/dist/lenis.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.15.0/dist/gsap.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.15.0/dist/ScrollTrigger.min.js"></script>
<script>
(() => {
  const html = document.documentElement;
  const reduziert = () => html.classList.contains('reduziert');
  const touch = matchMedia('(hover: none)').matches;
  const NS = 'http://www.w3.org/2000/svg';
  gsap.registerPlugin(ScrollTrigger);

  /* ---------- Zeichnungen (eigene SVG-Linien) ---------- */
  const el = (tag, attr, eltern) => { const n = document.createElementNS(NS, tag); for (const k in attr) n.setAttribute(k, attr[k]); eltern && eltern.appendChild(n); return n; };
  const pol = (cx, cy, r, w) => [cx + r * Math.sin(w), cy - r * Math.cos(w)];

  // Minutenskala und Indizes
  const skala = document.getElementById('skala');
  for (let i = 0; i < 60; i++) {
    const w = i / 60 * Math.PI * 2, [x1, y1] = pol(200, 280, 111, w), [x2, y2] = pol(200, 280, i % 5 ? 106.5 : 104, w);
    el('line', { x1, y1, x2, y2, stroke: '#8b9097', 'stroke-width': i % 5 ? .55 : .9 }, skala);
  }
  for (let h = 0; h < 12; h++) {
    if (h === 6) continue;
    const g = el('g', { transform: `rotate(${h * 30} 200 280)` }, skala);
    const breit = h === 0 ? [-5.2, 1.6] : [-1.7];
    breit.forEach(dx => el('rect', { x: 200 + dx, y: 280 - 99, width: 3.4, height: h === 0 ? 22 : 18, fill: '#a88a5a', stroke: '#8a6f45', 'stroke-width': .4 }, g));
  }
  const klein = document.getElementById('skala-klein');
  for (let i = 0; i < 12; i++) {
    const w = i / 12 * Math.PI * 2, [x1, y1] = pol(200, 340, 22, w), [x2, y2] = pol(200, 340, i % 3 ? 20 : 18.5, w);
    el('line', { x1, y1, x2, y2, stroke: '#8b9097', 'stroke-width': .5 }, klein);
  }

  // Werkstatt-Zeichnungen in Messing und Grau
  const zahnrad = (cx, cy, r, z) => {
    let d = '';
    for (let i = 0; i < z; i++) {
      const a = i / z * Math.PI * 2, s = Math.PI / z;
      const p = [[r, a - s * .55], [r + 7, a - s * .3], [r + 7, a + s * .3], [r, a + s * .55], [r, a + s * 1.45]];
      p.forEach(([rr, w], k) => { const [x, y] = pol(cx, cy, rr, w); d += (i === 0 && k === 0 ? 'M' : 'L') + x.toFixed(1) + ' ' + y.toFixed(1); });
    }
    return d + 'Z';
  };
  const zeichnen = {
    platine(s) {
      el('circle', { cx: 200, cy: 250, r: 150, fill: 'none', stroke: '#a88a5a', 'stroke-width': 1 }, s);
      el('circle', { cx: 200, cy: 250, r: 142, fill: 'none', stroke: 'rgba(255,255,255,.18)', 'stroke-width': .6 }, s);
      [[200, 250, 16], [150, 180, 9], [262, 196, 11], [236, 318, 8], [140, 300, 12], [290, 270, 6], [180, 340, 5], [212, 150, 7]].forEach(([x, y, r]) => {
        el('circle', { cx: x, cy: y, r, fill: 'none', stroke: 'rgba(255,255,255,.55)', 'stroke-width': .8 }, s);
        el('circle', { cx: x, cy: y, r: r * .38, fill: '#a88a5a', opacity: .75 }, s);
      });
      for (let i = 0; i < 3; i++) { const [x, y] = pol(200, 250, 128, i * 2.1 + .4); el('circle', { cx: x, cy: y, r: 5, fill: 'none', stroke: '#a88a5a', 'stroke-width': .8 }, s); el('line', { x1: x - 3.5, y1: y, x2: x + 3.5, y2: y, stroke: '#a88a5a', 'stroke-width': .8 }, s); }
      el('path', { d: 'M120 210 C160 160 250 150 300 205', fill: 'none', stroke: 'rgba(255,255,255,.28)', 'stroke-width': .6, 'stroke-dasharray': '3 4' }, s);
    },
    raeder(s) {
      [[170, 220, 70, 48, '#a88a5a'], [262, 300, 42, 30, 'rgba(255,255,255,.6)'], [150, 345, 30, 22, 'rgba(255,255,255,.45)'], [278, 168, 26, 20, '#a88a5a']].forEach(([x, y, r, z, f]) => {
        el('path', { d: zahnrad(x, y, r, z), fill: 'none', stroke: f, 'stroke-width': .9 }, s);
        el('circle', { cx: x, cy: y, r: r * .62, fill: 'none', stroke: f, 'stroke-width': .5, opacity: .7 }, s);
        for (let k = 0; k < 5; k++) { const [x2, y2] = pol(x, y, r * .62, k / 5 * Math.PI * 2); el('line', { x1: x, y1: y, x2, y2, stroke: f, 'stroke-width': .5, opacity: .7 }, s); }
        el('circle', { cx: x, cy: y, r: 4, fill: f }, s);
      });
    },
    blatt(s) {
      el('circle', { cx: 200, cy: 250, r: 140, fill: 'none', stroke: 'rgba(255,255,255,.22)', 'stroke-width': .7 }, s);
      for (let i = 0; i < 60; i++) {
        if (i > 38) break;     // Druck noch nicht fertig: Durchgang 7 von 9
        const w = i / 60 * Math.PI * 2, [x1, y1] = pol(200, 250, 132, w), [x2, y2] = pol(200, 250, i % 5 ? 126 : 120, w);
        el('line', { x1, y1, x2, y2, stroke: i % 5 ? 'rgba(255,255,255,.55)' : '#a88a5a', 'stroke-width': i % 5 ? .6 : 1.4 }, s);
      }
      el('text', { x: 200, y: 222, 'text-anchor': 'middle', 'font-family': 'Bodoni Moda, serif', 'font-size': 22, 'letter-spacing': 6, fill: 'rgba(255,255,255,.85)' }, s).textContent = 'BREHME';
      el('text', { x: 200, y: 330, 'text-anchor': 'middle', 'font-family': 'Instrument Sans, sans-serif', 'font-size': 9, 'letter-spacing': 3, fill: '#a88a5a' }, s).textContent = 'DURCHGANG 7 / 9';
      el('path', { d: 'M60 470 H340', stroke: 'rgba(255,255,255,.18)', 'stroke-width': .6 }, s);
    }
  };
  document.querySelectorAll('[data-zeichnung]').forEach(s => zeichnen[s.dataset.zeichnung](s));

  /* ---------- Lenis, an ScrollTrigger und den GSAP-Ticker gekoppelt ---------- */
  const EXPO = t => Math.min(1, 1.001 - Math.pow(2, -10 * t));   // gemessen bei Wokine
  const VARIANTEN = {
    nativ: { werte: 'Lenis aus · der Browser scrollt selbst, das Mausrad springt je Rastung' },
    lerp:  { werte: 'lerp 0.1 · smoothWheel true · syncTouch false · anchors true · Zeiger per ScrollTrigger scrub: true', opt: { lerp: 0.1 } },
    dauer: { werte: 'duration 1.2 s · easing 1.001 − 2^(−10t) · lerp wird ignoriert · Messwert Wokine', opt: { duration: 1.2, easing: EXPO } }
  };
  let lenis = null, variante = 'lerp';

  function lenisStarten(v) {
    if (lenis) { lenis.destroy(); lenis = null; }
    variante = v;
    if (v !== 'nativ' && !reduziert() && typeof Lenis !== 'undefined') {
      lenis = new Lenis({ ...VARIANTEN[v].opt, smoothWheel: true, syncTouch: false, wheelMultiplier: 1, anchors: true, autoRaf: false });
      lenis.on('scroll', ScrollTrigger.update);
    }
    document.querySelectorAll('[data-variante]').forEach(b => b.setAttribute('aria-pressed', String(b.dataset.variante === variante)));
    document.querySelector('[data-werte]').textContent = reduziert()
      ? 'Reduzierte Bewegung: Lenis startet nicht, die Seite scrollt nativ'
      : touch && v !== 'nativ' ? VARIANTEN[v].werte.split(' · ')[0] + ' · Touch scrollt nativ (syncTouch: false)' : VARIANTEN[v].werte;
    proben.length = 0;
  }
  gsap.ticker.add(t => { if (lenis) lenis.raf(t * 1000); messen(t); });
  gsap.ticker.lagSmoothing(0);

  // Zeiger: direkt an den Scroll gekoppelt (scrub: true), damit jede Glättung allein von Lenis kommt
  gsap.set('#minute', { rotation: 48, svgOrigin: '200 280' });
  gsap.set('#stunde', { rotation: 304, svgOrigin: '200 280' });
  const zeigerZeit = gsap.timeline({ scrollTrigger: { trigger: '.strecke', start: 'top top', end: 'bottom bottom', scrub: true } })
    .to('#minute', { rotation: 48 + 720, ease: 'none' }, 0)
    .to('#stunde', { rotation: 304 + 60, ease: 'none' }, 0);

  // Werkstatt: Linienzeichnungen verschieben sich höchstens 4vh gegen den Rahmen (Rezeptregel)
  const parallaxen = [...document.querySelectorAll('[data-tempo]')].map(s => gsap.fromTo(s, { y: () => +s.dataset.tempo * innerHeight * .02 }, {
    y: () => -s.dataset.tempo * innerHeight * .02, ease: 'none',
    scrollTrigger: { trigger: s.closest('.schritt'), start: 'top bottom', end: 'bottom top', scrub: true, invalidateOnRefresh: true }
  }));
  function bewegungPruefen() { parallaxen.forEach(p => { p.scrollTrigger.enable(); if (reduziert()) { p.scrollTrigger.disable(); gsap.set(p.targets(), { y: 0 }); } }); }

  // Kleine Sekunde: 21.600 A/h = sechs Schritte pro Sekunde
  const sekunde = document.getElementById('sekunde');
  let letzteSek = -1;
  function sekundeStellen() {
    const d = new Date(), s = d.getSeconds() + Math.floor(d.getMilliseconds() / 1000 * 6) / 6;
    if (s !== letzteSek) { letzteSek = s; sekunde.setAttribute('transform', `rotate(${s * 6} 200 340)`); }
  }

  /* ---------- Messkurve: Ziel (Mausrad) gegen Ist (gerendert) ---------- */
  const leinwand = document.querySelector('.kurve'), ctx = leinwand.getContext('2d');
  const live = document.querySelector('[data-live]');
  const proben = [], FENSTER = 2.4;
  let bild = 0;
  function groesse() { const r = devicePixelRatio || 1, b = leinwand.clientWidth || 274, h = leinwand.clientHeight || 64; leinwand.width = b * r; leinwand.height = h * r; ctx.setTransform(r, 0, 0, r, 0, 0); }
  function messen(t) {
    if (!reduziert()) sekundeStellen();
    const ist = scrollY, ziel = lenis ? lenis.targetScroll : scrollY;
    proben.push([t, ziel, ist]);
    while (proben.length && proben[0][0] < t - FENSTER) proben.shift();
    if (++bild % 2 || !leinwand.clientWidth) return;
    const b = leinwand.clientWidth, h = leinwand.clientHeight;
    ctx.clearRect(0, 0, b, h);
    let lo = Infinity, hi = -Infinity;
    proben.forEach(p => { lo = Math.min(lo, p[1], p[2]); hi = Math.max(hi, p[1], p[2]); });
    if (hi - lo < 240) { const m = (hi + lo) / 2; lo = m - 120; hi = m + 120; }
    const X = tt => (1 - (t - tt) / FENSTER) * b, Y = v => 6 + (v - lo) / (hi - lo) * (h - 12);
    ctx.strokeStyle = '#e1e2e3'; ctx.lineWidth = 1;
    for (let i = 1; i < 4; i++) { ctx.beginPath(); ctx.moveTo(i * b / 4, 0); ctx.lineTo(i * b / 4, h); ctx.stroke(); }
    const linie = (k, farbe, breite, strich) => {
      ctx.beginPath(); ctx.setLineDash(strich); ctx.strokeStyle = farbe; ctx.lineWidth = breite;
      proben.forEach((p, i) => { const x = X(p[0]), y = Y(p[k]); if (!i) ctx.moveTo(x, y); else { if (k === 1) ctx.lineTo(x, Y(proben[i - 1][k])); ctx.lineTo(x, y); } });
      ctx.stroke(); ctx.setLineDash([]);
    };
    linie(1, '#8b9097', 1, [3, 3]);
    linie(2, '#a88a5a', 1.75, []);
    if (bild % 6 === 0) live.textContent = 'Δ ' + Math.round(Math.abs(ziel - ist)) + ' px';
  }

  /* ---------- Vorführung: Mausrad-Rastungen ohne Nutzer ---------- */
  let uhr = null, laeuft = false;
  const RASTUNG = 100, TAKT = 130, BUENDEL = 3, PAUSE = 720;
  const ende = () => { const s = document.querySelector('.werkstatt'); return Math.min(document.documentElement.scrollHeight - innerHeight, s.offsetTop + innerHeight * .35); };
  function rastung() {
    if (lenis) window.dispatchEvent(new WheelEvent('wheel', { deltaY: RASTUNG, deltaMode: 0, cancelable: true, bubbles: true }));
    else scrollBy(0, RASTUNG);
  }
  function nachOben(sanft) {
    if (lenis) lenis.scrollTo(0, sanft ? { duration: 1.6, easing: EXPO, force: true } : { immediate: true, force: true });
    else scrollTo(0, 0);
  }
  function schritt(n) {
    if (!laeuft) return;
    const ziel = lenis ? lenis.targetScroll : scrollY;
    if (ziel >= ende() - 2) {
      nachOben(true);
      // Galerie-Vorschau: jede Runde mit der anderen Lenis-Variante
      if (html.classList.contains('vorschau') && lenis) uhr = setTimeout(() => { lenisStarten(variante === 'lerp' ? 'dauer' : 'lerp'); schritt(0); }, 2200);
      else uhr = setTimeout(() => schritt(0), lenis ? 2200 : 1400);
      return;
    }
    rastung();
    uhr = setTimeout(() => schritt(n + 1), (n + 1) % BUENDEL ? TAKT : PAUSE);
  }
  function pause() { laeuft = false; clearTimeout(uhr); html.classList.remove('vorfuehrung'); }
  function neustart() { pause(); nachOben(false); proben.length = 0; }
  function vorfuehren() { neustart(); laeuft = true; html.classList.add('vorfuehrung'); uhr = setTimeout(() => schritt(0), 700); }
  function weiter() { if (!laeuft) { laeuft = true; html.classList.add('vorfuehrung'); schritt(0); } }

  // Echte Eingaben beenden die Vorführung (außer in der Galerie-Vorschau)
  ['wheel', 'touchstart', 'keydown', 'pointerdown'].forEach(typ => addEventListener(typ, e => {
    if (e.isTrusted && laeuft && !html.classList.contains('vorschau') && !(e.target instanceof Element && e.target.closest('.pult'))) pause();
  }, { passive: true }));

  document.querySelectorAll('[data-variante]').forEach(b => {
    if (b.dataset.variante !== 'nativ' && reduziert()) { b.disabled = true; b.title = 'Bei reduzierter Bewegung startet Lenis nicht'; }
    b.addEventListener('click', () => { const war = laeuft; pause(); lenisStarten(b.dataset.variante); if (war) weiter(); });
  });

  // Der Schalter der Detailseite setzt .reduziert nachträglich: Lenis dann abbauen
  let warReduziert = reduziert();
  new MutationObserver(() => {
    if (reduziert() === warReduziert) return;      // Lenis setzt selbst Klassen am <html>, die hier nichts auslösen dürfen
    warReduziert = reduziert();
    pause();
    lenisStarten(warReduziert ? 'nativ' : 'lerp');
    document.querySelectorAll('[data-variante]').forEach(b => { b.disabled = warReduziert && b.dataset.variante !== 'nativ'; });
    bewegungPruefen();
  }).observe(html, { attributes: true, attributeFilter: ['class'] });

  groesse(); addEventListener('resize', groesse);
  lenisStarten(reduziert() ? 'nativ' : 'lerp');
  bewegungPruefen();
  if (reduziert()) sekundeStellen();
  document.fonts && document.fonts.ready.then(() => ScrollTrigger.refresh());

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