Zum Inhalt
← Glas & Material

Progressiver Verlaufs-Blur

Inhalt taucht stufenlos unscharf unter Kopf und Warenkorb-Leiste: zehn gestapelte Backdrop-Ebenen mit wachsendem Blur und versetzten Masken.

Über der Kante eines Scroll-Bereichs liegt ein Stapel absolut positionierter Ebenen, etwa unter dem Sticky-Header oder über der Bottom-Bar. Jede Ebene hat backdrop-filter: blur() mit steigendem Radius. Jede Ebene bekommt eine eigene mask-image-Maske (linear-gradient): Je stärker ihr Blur, desto früher läuft sie aus. So wird der Inhalt zur Kante hin stufenlos unschärfer, statt hart abgeschnitten zu werden. Oft liegt zusätzlich ein leichter Farbverlauf in die Seitenfarbe darüber.

Der Verlauf imitiert optische Tiefenunschärfe. Kanten wirken weich und hochwertig, wie man es von Apple und visionOS kennt. Der Inhalt scheint unter das Interface zu tauchen, statt an einer harten Linie zu enden.

Wann einsetzen

Unter Sticky-Headern. Über Bottom-Navigation und Warenkorb-Leisten. An den Rändern horizontaler Karussells. Über der Unterkante von Bildern mit Bildunterschrift. Radial um ein Element, das im Fokus stehen soll.

Wann nicht

Bei langen Listen auf schwachen Mobilgeräten, denn jede Backdrop-Ebene kostet Rechenzeit. Über Text, der noch gelesen werden muss. Über einfarbigem Inhalt: Dort sieht man den Blur nicht, ein Farbverlauf reicht.

Varianten

  • Gestuft an der Kante (oben, unten oder beide): mehrere Ebenen mit steigendem Blur, bei Clerk 1 bis 10 px
  • Einlagig: eine Ebene mit festem Blur und weicher Maske, bei Raycast blur(12px) mit Maske 50 → 75 %. Günstiger, wirkt aber eher wie ein Überblenden als wie wachsende Unschärfe
  • Radial: Vignetten-Blur um ein Fokuselement
  • Shader-Variante mit echtem variablem Blur in WebGL

Umsetzung

Ebenen per Schleife erzeugen. Bei Clerk ist jede Maske bis 40 % deckend und läuft dann aus; das Maskenende rückt in 5-%-Schritten von 100 % auf 55 %, der Blur steigt dabei 1, 1, 2, 2, 3, 4, 5, 6, 8, 10 px. Alternativ zeigt jede Maske nur ein Band (Stop i/n bis (i+2)/n), die Bänder überlappen um eine Stufe. Weniger Stufen zeigen sichtbare Bänder, mehr Stufen kosten mehr Leistung. Die Ebenen nicht in ein Element mit opacity unter 1, filter oder mask legen: Das wird zur Backdrop-Wurzel, und die Ebenen sehen den Inhalt dahinter nicht mehr. Deshalb den Stapel nicht per opacity ein- und ausblenden. Ohne backdrop-filter bleibt nur der Farbverlauf, ein eigener Fallback ist nicht nötig.

Robust umsetzen

Telefon. Ebenenzahl auf 3 bis 4 senken oder nur einen Farbverlauf zeigen. Die Demo nutzt auf Touch-Geräten 4 Ebenen mit 1, 3, 6 und 10 px.

Ohne Bewegung. Der Effekt ist statisch, eine Anpassung ist nicht nötig.

Barrierefrei. Rein dekorativ: aria-hidden="true" und pointer-events: none. Kopf und Leiste über der Kante brauchen trotzdem genug Kontrast, deshalb einen Farbverlauf in die Seitenfarbe darunterlegen.

Code

<!DOCTYPE html>
<!--
  Progressiver Verlaufs-Blur
  Fundus · Glas & Material · https://cecodesign.de/lab/fundus/glas-material/progressive-blur/
  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>Progressiver Verlaufs-Blur · 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: 'Young Serif'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/young-serif@latest/latin-400-normal.woff2') format('woff2'); font-weight: 400; 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; }

/* Welt: Blaufeld, Granola-Manufaktur aus Ulm (Rezept d2c-produkt-zuerst) */
:root {
  --grund: #f5f4f1; --kachel: #ece9e2; --tinte: #212121; --leise: #6b6b6b; --linie: rgb(33 33 33 / .12);
  --gruen: #10714e; --blau: #1f5fad; --ocker: #d9962b; --sale: #c93c7f;
  --auf: cubic-bezier(0, 0, 0.2, 1);    /* Öffnen, Hover */
  --zu: cubic-bezier(0.4, 0, 1, 1);     /* Schließen */
  --kopf: 76px;
  --rand: clamp(16px, 3.4vw, 48px);
}
* { box-sizing: border-box; margin: 0; padding: 0; }
html { background: var(--grund); color: var(--tinte); -webkit-font-smoothing: antialiased; }
body { font: 400 16px/1.55 'Instrument Sans', system-ui, sans-serif; overflow-x: hidden; }
a { color: inherit; text-decoration: none; }
button { font: inherit; color: inherit; background: none; border: 0; cursor: pointer; }
svg { display: block; }
.serif { font-family: 'Young Serif', Georgia, serif; font-weight: 400; }
.mono { font: 500 11px/1.4 'Geist Mono', ui-monospace, monospace; letter-spacing: .14em; text-transform: uppercase; }
.etikett { font-size: .75rem; font-weight: 600; letter-spacing: .06em; text-transform: uppercase; }
:focus-visible { outline: 2px solid var(--blau); outline-offset: 3px; border-radius: 6px; }

/* ---------- Kopf (transparent, liegt über dem Blur) ---------- */
.kopf { position: fixed; inset: 0 0 auto; height: var(--kopf); z-index: 30; display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; padding: 0 var(--rand); }
.marke { display: flex; align-items: baseline; gap: 8px; font-size: 26px; letter-spacing: -.01em; line-height: 1; }
.marke small { font: 500 .6875rem/1 'Instrument Sans', sans-serif; letter-spacing: .06em; text-transform: uppercase; color: var(--leise); }
.nav { display: flex; gap: 28px; font-weight: 500; font-size: .9375rem; }
.nav a { position: relative; padding: 6px 0; }
.nav a::after { content: ''; position: absolute; left: 0; right: 0; bottom: 2px; height: 1.5px; background: currentColor; transform: scaleX(0); transform-origin: left; transition: transform .2s var(--auf); }
.nav a:hover::after { transform: scaleX(1); }
.icons { display: flex; justify-content: flex-end; gap: 6px; }
.icons button { width: 40px; height: 40px; display: grid; place-items: center; border-radius: 50%; position: relative; transition: background-color .15s var(--auf); }
.icons button:hover { background: rgb(33 33 33 / .06); }
.icons svg { width: 21px; height: 21px; }
.zaehler { position: absolute; top: 4px; right: 3px; min-width: 17px; height: 17px; border-radius: 9px; background: var(--tinte); color: var(--grund); font-size: 10.5px; font-weight: 600; display: grid; place-items: center; font-variant-numeric: tabular-nums; }
.nur-mobil { display: none !important; }

/* ---------- Der Effekt: gestapelte Backdrop-Ebenen mit Masken ---------- */
.satz { display: none; }
body[data-variante="gestuft"] .satz--gestuft,
body[data-variante="einlagig"] .satz--einlagig,
body[data-variante="radial"] .satz--radial { display: block; }
.kante { position: fixed; left: 0; right: 0; z-index: 25; pointer-events: none; }
.kante--oben { top: 0; height: var(--h-oben, 168px); }
.kante--unten { bottom: 0; height: var(--h-unten, 150px); }
.fokus { position: fixed; inset: 0; z-index: 24; pointer-events: none; }
.ebene { position: absolute; inset: 0; }
.fokus .ebene { -webkit-mask-image: radial-gradient(circle at var(--fx, 50%) var(--fy, 50%), transparent var(--r0), #000 var(--r1)); mask-image: radial-gradient(circle at var(--fx, 50%) var(--fy, 50%), transparent var(--r0), #000 var(--r1)); }
/* Farbverlauf in die Seitenfarbe: hält Kopf und Leiste lesbar */
.toenung { position: absolute; inset: 0; }
.kante--oben .toenung { background: linear-gradient(to bottom, rgb(245 244 241 / .8), rgb(245 244 241 / .5) 45%, rgb(245 244 241 / 0)); }
.kante--unten .toenung { background: linear-gradient(to top, rgb(245 244 241 / .55), rgb(245 244 241 / 0) 70%); }
.fokus .toenung { background: radial-gradient(circle at var(--fx, 50%) var(--fy, 50%), rgb(245 244 241 / 0) var(--rt0), rgb(245 244 241 / .28) var(--rt1)); }
/* Lernhilfe: jede Ebene bekommt einen eigenen Farbton */
body.ebenen-zeigen .ebene { background: hsl(var(--ton) 90% 52% / .16); }

/* ---------- Bühne / Hero ---------- */
.hero { display: grid; grid-template-columns: minmax(0, 5fr) minmax(0, 7fr); min-height: min(100svh, 980px); }
.hero__text { align-self: center; padding: calc(var(--kopf) + 40px) clamp(24px, 4vw, 72px) 150px var(--rand); display: grid; gap: 22px; justify-items: start; }
.hero__text .etikett { color: var(--gruen); display: flex; align-items: center; gap: 8px; }
.hero__text .etikett::before { content: ''; width: 7px; height: 7px; border-radius: 50%; background: currentColor; }
h1 { font-family: 'Young Serif', Georgia, serif; font-weight: 400; font-size: clamp(2.25rem, 1.4rem + 3.6vw, 4.5rem); line-height: 1.05; letter-spacing: -.01em; max-width: 9ch; }
.hero__text p.lead { font-size: 1.0625rem; max-width: 36ch; color: #3d3d3d; }
.knoepfe { display: flex; gap: 10px; flex-wrap: wrap; margin-top: 4px; }
.knopf { height: 50px; padding: 0 24px; border-radius: 999px; display: inline-flex; align-items: center; gap: 10px; font-weight: 600; font-size: .9375rem; transition: background-color .15s var(--auf), transform .15s var(--auf); }
.knopf--voll { background: var(--tinte); color: var(--grund); }
.knopf--voll:hover { background: #000; }
.knopf--rand { box-shadow: inset 0 0 0 1.5px var(--tinte); }
.knopf--rand:hover { background: rgb(33 33 33 / .05); }
.knopf:active { transform: scale(.97); }
.vertrauen { display: flex; flex-wrap: wrap; gap: 6px 18px; font-size: .8125rem; color: var(--leise); margin-top: 10px; }
.vertrauen span { display: inline-flex; align-items: center; gap: 6px; }
.vertrauen b { color: var(--tinte); font-weight: 600; }
.vertrauen svg { width: 15px; height: 15px; }
.hero__bild { position: relative; background: var(--blau); overflow: hidden; min-height: 520px; }
.hero__bild > svg { position: absolute; inset: 0; width: 100%; height: 100%; }
.preischip { position: absolute; left: clamp(16px, 4%, 40px); top: 50%; z-index: 2; background: var(--grund); border-radius: 14px; padding: 12px 16px; display: grid; gap: 2px; box-shadow: 0 10px 30px rgb(10 30 70 / .25); }
.preischip b { font-weight: 600; font-size: .9375rem; }
.preischip span { font-size: .8125rem; color: var(--leise); font-variant-numeric: tabular-nums; }

/* ---------- Bestseller ---------- */
.abschnitt { padding: clamp(56px, 8vw, 112px) var(--rand) 0; }
.abschnitt__kopf { display: flex; justify-content: space-between; align-items: end; gap: 16px; margin-bottom: 28px; }
h2 { font-weight: 600; font-size: clamp(1.5rem, 1.1rem + 1.4vw, 2.25rem); letter-spacing: -.015em; line-height: 1.15; }
.abschnitt__kopf a { font-weight: 500; font-size: .9375rem; white-space: nowrap; border-bottom: 1.5px solid currentColor; padding-bottom: 2px; }
.raster { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 36px 20px; }
.kachel { position: relative; display: grid; gap: 14px; }
.kachel__bild { position: relative; aspect-ratio: 4 / 5; background: var(--kachel); border-radius: 14px; display: grid; place-items: center; overflow: hidden; }
.kachel__bild svg { width: 66%; height: auto; transition: transform .2s var(--auf); }
.kachel:hover .kachel__bild svg { transform: translateY(-6px) rotate(-2deg); }
.badge { position: absolute; top: 12px; left: 12px; height: 24px; padding: 0 10px; border-radius: 999px; background: var(--grund); display: inline-flex; align-items: center; font-size: .6875rem; font-weight: 600; letter-spacing: .06em; text-transform: uppercase; }
.badge--sale { background: var(--sale); color: #fff; }
.badge--neu { background: var(--blau); color: #fff; }
.merken { position: absolute; top: 8px; right: 8px; width: 36px; height: 36px; border-radius: 50%; display: grid; place-items: center; transition: background-color .15s var(--auf); }
.merken:hover { background: rgb(33 33 33 / .07); }
.merken svg { width: 19px; height: 19px; }
.kachel__text { display: grid; grid-template-columns: 1fr auto; gap: 2px 12px; font-size: 1rem; }
.kachel__text h3 { font-size: 1rem; font-weight: 600; line-height: 1.3; }
.kachel__text .preis { font-weight: 600; font-variant-numeric: tabular-nums; }
.kachel__text .info { grid-column: 1 / -1; font-size: .8125rem; color: var(--leise); font-variant-numeric: tabular-nums; }
.kachel__text .wertung { grid-column: 1 / -1; font-size: .8125rem; display: flex; align-items: center; gap: 4px; }
.kachel__text .wertung span { color: var(--leise); }
.kachel__text .wertung svg { width: 13px; height: 13px; }

/* ---------- Themenwelten ---------- */
.welten { display: grid; grid-template-columns: 1.25fr 1fr 1fr; gap: 20px; }
.welt { position: relative; border-radius: 18px; overflow: hidden; min-height: 400px; padding: 28px; display: flex; flex-direction: column; justify-content: space-between; color: #fff; }
.welt > svg { position: absolute; right: -40px; bottom: -40px; width: 320px; height: 320px; }
.welt h3 { font-family: 'Young Serif', Georgia, serif; font-weight: 400; font-size: clamp(1.75rem, 1.2rem + 1.4vw, 2.5rem); line-height: 1.08; letter-spacing: -.01em; max-width: 10ch; position: relative; }
.welt p { position: relative; font-size: .9375rem; max-width: 26ch; }
.welt .etikett { position: relative; opacity: .82; }
.welt--ocker { background: var(--ocker); color: #2a1a05; }
.welt--blau { background: var(--blau); }
.welt--beere { background: #b3305b; }

/* ---------- Warum ---------- */
.warum { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 20px; border-top: 1px solid var(--linie); padding-top: 32px; }
.warum h3 { font-size: 1.125rem; font-weight: 600; margin: 14px 0 6px; }
.warum p { color: #474747; max-width: 34ch; }
.warum .zahl { font-family: 'Young Serif', Georgia, serif; font-size: 2.75rem; line-height: 1; color: var(--blau); font-variant-numeric: tabular-nums; }

/* ---------- Fuß ---------- */
.fuss { margin-top: clamp(72px, 9vw, 128px); background: var(--tinte); color: var(--grund); padding: clamp(48px, 6vw, 88px) var(--rand) 180px; display: grid; grid-template-columns: 1.4fr 1fr 1fr 1fr; gap: 40px 24px; }
.fuss h2 { font-family: 'Young Serif', Georgia, serif; font-weight: 400; font-size: clamp(1.75rem, 1.2rem + 1.6vw, 2.75rem); letter-spacing: -.01em; }
.fuss p { color: rgb(245 244 241 / .66); font-size: .9375rem; margin-top: 10px; max-width: 40ch; }
.fuss form { display: flex; gap: 8px; margin-top: 20px; max-width: 420px; }
.fuss input { flex: 1; min-width: 0; height: 48px; border-radius: 999px; border: 1px solid rgb(245 244 241 / .3); background: none; color: inherit; font: inherit; padding: 0 18px; }
.fuss form button { height: 48px; padding: 0 20px; border-radius: 999px; background: var(--grund); color: var(--tinte); font-weight: 600; }
.fuss h4 { font-size: .75rem; font-weight: 600; letter-spacing: .06em; text-transform: uppercase; color: rgb(245 244 241 / .55); margin-bottom: 14px; }
.fuss ul { list-style: none; display: grid; gap: 8px; font-size: .9375rem; }
.fuss .klein { grid-column: 1 / -1; border-top: 1px solid rgb(245 244 241 / .14); padding-top: 22px; display: flex; flex-wrap: wrap; justify-content: space-between; gap: 10px; font-size: .8125rem; color: rgb(245 244 241 / .55); }

/* ---------- Unten: Warenkorb-Leiste und Fundus-Bedienung ---------- */
.korb { position: fixed; right: var(--rand); bottom: 22px; z-index: 30; display: grid; gap: 8px; justify-items: end; }
.korb__hinweis { font-size: .8125rem; display: flex; align-items: center; gap: 10px; font-variant-numeric: tabular-nums; }
.korb__hinweis i { width: 96px; height: 4px; border-radius: 2px; background: rgb(33 33 33 / .14); overflow: hidden; display: block; }
.korb__hinweis i::before { content: ''; display: block; height: 100%; width: 42%; background: var(--gruen); }
.korb__leiste { display: flex; align-items: center; gap: 14px; background: var(--tinte); color: var(--grund); border-radius: 999px; padding: 6px 6px 6px 20px; box-shadow: 0 14px 40px rgb(0 0 0 / .18); }
.korb__leiste svg { width: 19px; height: 19px; }
.korb__leiste span { font-size: .9375rem; font-variant-numeric: tabular-nums; white-space: nowrap; }
.korb__leiste span b { font-weight: 600; }
.korb__leiste a { height: 42px; padding: 0 18px; border-radius: 999px; background: var(--grund); color: var(--tinte); display: inline-flex; align-items: center; font-weight: 600; font-size: .875rem; }

.steuer { position: fixed; left: var(--rand); bottom: 22px; z-index: 30; display: grid; gap: 8px; max-width: min(780px, 56vw); color: var(--leise); }
.modi { display: flex; flex-wrap: wrap; gap: 4px; }
.modi button { color: var(--leise); background: rgb(245 244 241 / .9); border: 1px solid var(--linie); border-radius: 999px; height: 30px; padding: 0 12px; transition: background-color .15s var(--auf), color .15s var(--auf); }
.modi button[aria-pressed='true'] { background: var(--tinte); color: var(--grund); border-color: var(--tinte); }
.werte { color: #555; text-transform: none; letter-spacing: .02em; line-height: 1.5; background: rgb(245 244 241 / .82); border-radius: 8px; padding: 6px 10px; width: fit-content; }
.werte b { color: var(--tinte); font-weight: 500; }

.vorschau body { overflow: hidden; }
.vorschau .steuer { display: none; }

/* ---------- Telefon ---------- */
@media (max-width: 900px) {
  .nav { display: none; }
  .raster { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 28px 12px; }
  .welten { grid-template-columns: 1fr; }
  .welt { min-height: 300px; }
  .fuss { grid-template-columns: 1fr 1fr; }
  .fuss > div:first-child { grid-column: 1 / -1; }
}
@media (max-width: 640px) {
  :root { --kopf: 60px; }
  .kopf { grid-template-columns: auto 1fr auto; gap: 4px; padding: 0 10px 0 6px; }
  .nur-mobil { display: grid !important; }
  .marke { font-size: 22px; }
  .marke small { display: none; }
  .icons { gap: 0; }
  .icons .ohne-mobil { display: none; }
  .hero { grid-template-columns: 1fr; min-height: 0; }
  .hero__bild { order: -1; min-height: 0; height: 58svh; }
  .hero__text { padding: 32px var(--rand) 40px; gap: 18px; }
  .preischip { top: auto; bottom: 18px; }
  .warum { grid-template-columns: 1fr; gap: 28px; }
  .kachel__text { grid-template-columns: 1fr; }
  .kachel__text h3 { font-size: .9375rem; }
  .fuss { padding-bottom: 240px; }
  .korb { left: 12px; right: 12px; bottom: 12px; justify-items: stretch; }
  .korb__hinweis { display: none; }
  .korb__leiste { justify-content: space-between; }
  .steuer { left: 12px; right: 12px; bottom: 76px; max-width: none; }
  .werte { font-size: 10px; }
}
/* Kontrastmodus: Knöpfe mit Rahmen */
@media (forced-colors: active) { button, [role=button], [role=tab], [role=switch] { border: 1px solid ButtonText !important; } }
</style>
</head>
<body data-variante="gestuft">

<!-- Effekt-Ebenen (rein dekorativ) -->
<div class="satz satz--gestuft" aria-hidden="true">
  <div class="kante kante--oben" data-kante="oben"></div>
  <div class="kante kante--unten" data-kante="unten"></div>
</div>
<div class="satz satz--einlagig" aria-hidden="true">
  <div class="kante kante--oben" data-kante="oben"></div>
  <div class="kante kante--unten" data-kante="unten"></div>
</div>
<div class="satz satz--radial" aria-hidden="true">
  <div class="fokus" data-fokus></div>
</div>

<header class="kopf">
  <div style="display:flex;align-items:center;gap:2px">
    <div class="icons nur-mobil"><button type="button" aria-label="Menü"><svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6"><path d="M4 8h16M4 16h16"/></svg></button></div>
    <a class="marke serif" href="#">blaufeld <small>Granola · Ulm</small></a>
  </div>
  <nav class="nav" aria-label="Hauptnavigation">
    <a href="#">Granola</a><a href="#">Porridge</a><a href="#">Probiersets</a><a href="#">Abo</a><a href="#">Rösterei</a>
  </nav>
  <div class="icons">
    <button type="button" aria-label="Suche" class="ohne-mobil"><svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6"><circle cx="11" cy="11" r="6.5"/><path d="m16 16 4 4"/></svg></button>
    <button type="button" aria-label="Merkliste" class="ohne-mobil"><svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6"><path d="M12 20s-7-4.4-7-10a4 4 0 0 1 7-2.6A4 4 0 0 1 19 10c0 5.6-7 10-7 10Z"/></svg></button>
    <button type="button" aria-label="Warenkorb, 2 Artikel"><svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6"><path d="M5 8h14l-1.2 11.2a1 1 0 0 1-1 .8H7.2a1 1 0 0 1-1-.8L5 8Z"/><path d="M9 10V7a3 3 0 0 1 6 0v3"/></svg><span class="zaehler">2</span></button>
  </div>
</header>

<main>
  <section class="hero">
    <div class="hero__text">
      <p class="etikett">Frisch geröstet am Dienstag</p>
      <h1>Hafer, langsam geröstet.</h1>
      <p class="lead">Kleine Chargen aus unserer Rösterei in der Ulmer Weststadt. Hafer von der Alb, Honig aus dem Blautal, keine Aromen.</p>
      <div class="knoepfe">
        <a class="knopf knopf--voll" href="#bestseller">Bestseller entdecken</a>
        <a class="knopf knopf--rand" href="#">Probierset · 19,90 €</a>
      </div>
      <div class="vertrauen">
        <span><svg aria-hidden="true" viewBox="0 0 16 16"><path fill="#212121" d="m8 1.5 1.9 4 4.4.5-3.3 3 .9 4.3L8 11.1l-3.9 2.2.9-4.3-3.3-3 4.4-.5Z"/></svg><b>4,8</b> aus 2.140 Bewertungen</span>
        <span>Gratisversand ab 39 €</span>
        <span>30 Tage Rückgabe</span>
      </div>
    </div>
    <div class="hero__bild" data-hero>
      <div class="preischip"><b>Nr. 1 Dinkel &amp; Alb-Honig</b><span>350 g · 7,90 €</span></div>
    </div>
  </section>

  <section class="abschnitt" id="bestseller">
    <div class="abschnitt__kopf"><h2>Unsere Bestseller</h2><a href="#">Alle 14 Sorten</a></div>
    <div class="raster" data-raster></div>
  </section>

  <section class="abschnitt">
    <div class="abschnitt__kopf"><h2>Für jeden Morgen</h2><a href="#">Alle Themen</a></div>
    <div class="welten" data-welten></div>
  </section>

  <section class="abschnitt">
    <div class="warum">
      <div><p class="zahl">40 kg</p><h3>Kleine Chargen</h3><p>So viel passt in unseren Ofen. Wir rösten langsam bei 150 °C, nie in Fett ausgebacken.</p></div>
      <div><p class="zahl">2</p><h3>Imkereien im Blautal</h3><p>Der Honig kommt aus Blaubeuren und Gerhausen, der Hafer von Feldern bei Laichingen.</p></div>
      <div><p class="zahl">0 g</p><h3>Plastik im Beutel</h3><p>Papier mit Zellulose-Innenschicht. Leer gegessen? Ab in die Papiertonne.</p></div>
    </div>
  </section>
</main>

<footer class="fuss">
  <div>
    <h2>Röstdienstag im Postfach.</h2>
    <p>Neue Sorten zuerst, Rezepte aus der Rösterei und 10 % auf die erste Bestellung.</p>
    <form onsubmit="return false"><input type="email" placeholder="E-Mail-Adresse" aria-label="E-Mail-Adresse"><button type="submit">Anmelden</button></form>
  </div>
  <div><h4>Shop</h4><ul><li>Granola</li><li>Porridge</li><li>Probiersets</li><li>Abo mit 10 %</li></ul></div>
  <div><h4>Hilfe</h4><ul><li>Versand &amp; Rückgabe</li><li>Zahlungsarten</li><li>Allergene</li><li>Kontakt</li></ul></div>
  <div><h4>Blaufeld</h4><ul><li>Die Rösterei</li><li>Unsere Bauern</li><li>Großhandel</li><li>Jobs</li></ul></div>
  <p class="klein"><span>Blaufeld Granola GmbH · Söflinger Straße 112 · 89077 Ulm</span><span>Impressum · Datenschutz · AGB · Widerruf</span></p>
</footer>

<div class="korb">
  <p class="korb__hinweis">Noch <b>22,60 €</b> bis zum Gratisversand <i></i></p>
  <div class="korb__leiste">
    <svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6"><path d="M5 8h14l-1.2 11.2a1 1 0 0 1-1 .8H7.2a1 1 0 0 1-1-.8L5 8Z"/><path d="M9 10V7a3 3 0 0 1 6 0v3"/></svg>
    <span>2 Artikel · <b>16,40 €</b></span>
    <a href="#">Zur Kasse</a>
  </div>
</div>

<div class="steuer">
  <div class="modi mono" role="group" aria-label="Variante wählen">
    <button type="button" data-variante="gestuft" aria-pressed="true">Gestuft</button>
    <button type="button" data-variante="einlagig" aria-pressed="false">Einlagig</button>
    <button type="button" data-variante="radial" aria-pressed="false">Radial</button>
    <button type="button" data-ebenen aria-pressed="false">Ebenen zeigen</button>
  </div>
  <p class="werte mono" data-werte aria-live="polite"></p>
</div>

<script>
(() => {
  const html = document.documentElement, body = document.body;
  const reduziert = () => html.classList.contains('reduziert');
  const mobilMQ = matchMedia('(max-width: 640px), (hover: none)');

  /* ---------- Werte (Clerk und Raycast gemessen, Radial eigene Werte) ---------- */
  const WERTE = {
    gestuft:  { blur: [1, 1, 2, 2, 3, 4, 5, 6, 8, 10], blurMobil: [1, 3, 6, 10], deckend: 40, endeVon: 100, endeBis: 55 },
    einlagig: { blur: [12], deckend: 50, ende: 75 },
    radial:   { blur: [1, 2, 4, 8, 16], blurMobil: [2, 6, 14], r: 150, rMobil: 96, band: 64, bandMobil: 48 },
    hoehe:    { oben: 168, obenMobil: 132, unten: 150, untenMobil: 200 }
  };
  let variante = 'gestuft', mobil = mobilMQ.matches;

  function stapel(el, blurs, maske) {
    el.innerHTML = '';
    blurs.forEach((b, i) => {
      const d = document.createElement('div');
      d.className = 'ebene';
      d.style.cssText = `-webkit-backdrop-filter: blur(${b}px); backdrop-filter: blur(${b}px); --ton: ${Math.round(i * 300 / Math.max(1, blurs.length - 1))};` + maske(i, blurs.length);
      el.appendChild(d);
    });
    const t = document.createElement('div'); t.className = 'toenung'; el.appendChild(t);
  }
  const lin = (richtung, a, b) => { const g = `linear-gradient(${richtung}, #000 0%, #000 ${a}%, transparent ${b}%)`; return `-webkit-mask-image: ${g}; mask-image: ${g};`; };

  function bauen() {
    mobil = mobilMQ.matches;
    const H = WERTE.hoehe;
    body.style.setProperty('--h-oben', (mobil ? H.obenMobil : H.oben) + 'px');
    body.style.setProperty('--h-unten', (mobil ? H.untenMobil : H.unten) + 'px');
    // Gestuft (Clerk): deckend bis 40 %, Maskenende in gleichen Schritten 100 % → 55 %
    const g = WERTE.gestuft, gb = mobil ? g.blurMobil : g.blur;
    const ende = (i, n) => +(g.endeVon - i * (g.endeVon - g.endeBis) / Math.max(1, n - 1)).toFixed(1);
    document.querySelectorAll('.satz--gestuft .kante').forEach(k => {
      const r = k.dataset.kante === 'oben' ? 'to bottom' : 'to top';
      stapel(k, gb, (i, n) => lin(r, g.deckend, ende(i, n)));
    });
    // Einlagig (Raycast): eine Ebene, Maske #000 50 % → transparent 75 %
    const e = WERTE.einlagig;
    document.querySelectorAll('.satz--einlagig .kante').forEach(k => {
      const r = k.dataset.kante === 'oben' ? 'to bottom' : 'to top';
      stapel(k, e.blur, () => lin(r, e.deckend, e.ende));
    });
    // Radial: Bänder um den Fokus, jede Ebene beginnt ein Band weiter außen
    const ra = WERTE.radial, rb = mobil ? ra.blurMobil : ra.blur, R = mobil ? ra.rMobil : ra.r, B = mobil ? ra.bandMobil : ra.band;
    const f = document.querySelector('[data-fokus]');
    stapel(f, rb, i => `--r0: ${R + i * B}px; --r1: ${R + (i + 1) * B}px;`);
    f.style.setProperty('--rt0', R + 'px'); f.style.setProperty('--rt1', (R + rb.length * B) + 'px');
    werteZeigen();
  }

  function werteZeigen() {
    const el = document.querySelector('[data-werte]');
    const H = WERTE.hoehe, ho = mobil ? H.obenMobil : H.oben;
    if (variante === 'gestuft') {
      const b = mobil ? WERTE.gestuft.blurMobil : WERTE.gestuft.blur;
      el.innerHTML = `<b>${b.length} Ebenen</b> · blur ${b.join(' ')} px · Maske deckend bis 40 %, Ende 100 → 55 % · Zone ${ho} px · Clerk`;
    } else if (variante === 'einlagig') {
      el.innerHTML = `<b>1 Ebene</b> · blur 12 px · Maske #000 50 % → transparent 75 % · Zone ${ho} px · Raycast`;
    } else {
      const ra = WERTE.radial, b = mobil ? ra.blurMobil : ra.blur;
      el.innerHTML = `<b>${b.length} Ebenen radial</b> · blur ${b.join(' ')} px · Fokus ${mobil ? ra.rMobil : ra.r} px, Band ${mobil ? ra.bandMobil : ra.band} px · eigene Werte`;
    }
  }

  /* ---------- Bildwelt: Papierbeutel als SVG ---------- */
  const zufall = s => () => { s |= 0; s = s + 0x6D2B79F5 | 0; let t = Math.imul(s ^ s >>> 15, 1 | s); t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t; return ((t ^ t >>> 14) >>> 0) / 4294967296; };
  const SORTEN = [
    { nr: 'Nr. 1', name: 'Dinkel & Alb-Honig', z: ['Dinkel &', 'Alb-Honig'], preis: '7,90 €', info: '350 g · 22,57 €/kg', w: '4,8', n: '612', farbe: '#d9962b', dunkel: '#7a4c0c', hell: '#fff3dc', muster: 'zack', korn: ['#b07a3a', '#d6a35a', '#8a5626', '#e8c27a'], badge: ['Bestseller', ''] },
    { nr: 'Nr. 3', name: 'Kakao & Haselnuss', z: ['Kakao &', 'Haselnuss'], preis: '8,50 €', info: '350 g · 24,29 €/kg', w: '4,9', n: '488', farbe: '#6e3b2a', dunkel: '#f0cfb6', hell: '#fbe9dc', muster: 'punkte', korn: ['#4a2416', '#7b4a2c', '#2f160d', '#a3693c'] },
    { nr: 'Nr. 4', name: 'Apfel & Zimt', z: ['Apfel &', 'Zimt'], preis: '7,90 €', info: '350 g · 22,57 €/kg', w: '4,7', n: '301', farbe: '#4f7f3a', dunkel: '#d9eac3', hell: '#eef6e2', muster: 'rauten', korn: ['#b07a3a', '#d6a35a', '#cfd98a', '#8a5626'] },
    { nr: 'Nr. 6', name: 'Beere & Mohn', z: ['Beere &', 'Mohn'], preis: '8,90 €', info: '350 g · 25,43 €/kg', w: '4,8', n: '205', farbe: '#b3305b', dunkel: '#f7c9d8', hell: '#fde6ee', muster: 'streifen', korn: ['#b07a3a', '#d6a35a', '#8e1d3f', '#2d2a33'], badge: ['Neu', 'neu'] },
    { nr: 'Porridge', name: 'Porridge Alb-Hafer', z: ['Alb-Hafer', 'kernig'], preis: '5,90 €', info: '500 g · 11,80 €/kg', w: '4,9', n: '733', farbe: '#1f5fad', dunkel: '#cfe0f7', hell: '#e3edfa', muster: 'wellen', korn: ['#e9dcc0', '#d8c59d', '#f4ecd9', '#c4ad80'] },
    { nr: 'Nr. 2', name: 'Erdnuss & Meersalz', z: ['Erdnuss &', 'Meersalz'], preis: '8,50 €', info: '350 g · 24,29 €/kg', w: '4,6', n: '190', farbe: '#d4553a', dunkel: '#fbd6cb', hell: '#fde9e3', muster: 'dreiecke', korn: ['#b07a3a', '#c98b4a', '#8a5626', '#e8c27a'] },
    { nr: 'Nr. 5', name: 'Kokos & Limette', z: ['Kokos &', 'Limette'], preis: '8,90 €', info: '350 g · 25,43 €/kg', w: '4,7', n: '142', farbe: '#7f9a2c', dunkel: '#ecf3cf', hell: '#f4f8e3', muster: 'punkte', korn: ['#f6f1e4', '#d6a35a', '#b07a3a', '#e9e2cc'] },
    { nr: 'Set', name: 'Probierset Sechserlei', z: ['Sechserlei', 'Probierset'], preis: '19,90 €', info: '6 × 60 g · 55,28 €/kg', w: '4,9', n: '356', farbe: '#212121', dunkel: '#f5f4f1', hell: '#f5f4f1', muster: 'bunt', korn: ['#b07a3a', '#6e3b2a', '#b3305b', '#d9962b'], badge: ['−15 %', 'sale'], kraft: true }
  ];
  let uid = 0;
  function muster(s, x0, x1, y, rnd) {
    const c = s.dunkel, out = [];
    if (s.muster === 'zack') { let d = `M${x0} ${y + 3}`; for (let x = x0; x < x1; x += 8) d += ` L${x + 4} ${y - 3} L${Math.min(x + 8, x1)} ${y + 3}`; out.push(`<path d="${d}" fill="none" stroke="${c}" stroke-width="2.4" stroke-linejoin="round"/>`); }
    if (s.muster === 'punkte') for (let x = x0 + 4; x < x1; x += 9) out.push(`<circle cx="${x}" cy="${y}" r="2.4" fill="${c}"/>`);
    if (s.muster === 'rauten') for (let x = x0 + 5; x < x1 - 2; x += 11) out.push(`<path d="M${x} ${y - 4.5}l4 4.5-4 4.5-4-4.5Z" fill="${c}"/>`);
    if (s.muster === 'streifen') for (let x = x0 + 3; x < x1; x += 6) out.push(`<rect x="${x}" y="${y - 4}" width="2.4" height="8" fill="${c}"/>`);
    if (s.muster === 'wellen') { let d = `M${x0} ${y}`; for (let x = x0; x < x1; x += 12) d += ` q3 -5 6 0 t6 0`; out.push(`<path d="${d}" fill="none" stroke="${c}" stroke-width="2.2"/>`); }
    if (s.muster === 'dreiecke') for (let x = x0 + 2; x < x1 - 6; x += 10) out.push(`<path d="M${x} ${y + 4}h8l-4-8Z" fill="${c}"/>`);
    if (s.muster === 'bunt') { const f = ['#d9962b', '#6e3b2a', '#4f7f3a', '#b3305b', '#1f5fad', '#d4553a']; let k = 0; for (let x = x0 + 4; x < x1; x += 10) out.push(`<rect x="${x - 3}" y="${y - 3}" width="6" height="6" rx="1" fill="${f[k++ % 6]}"/>`); }
    return out.join('');
  }
  function beutel(s, seed, attr = '') {
    const k = ++uid, rnd = zufall(seed || k * 97);
    const papier = s.kraft ? '#c9a676' : '#f7f2e8';
    const koerper = 'M30 40 L170 40 L177 238 Q177 252 163 252 L37 252 Q23 252 23 238 Z';
    let korn = '';
    for (let i = 0; i < 46; i++) {
      const x = 66 + rnd() * 68, y = 220 + rnd() * 24, r = 1.6 + rnd() * 3.2;
      korn += `<ellipse cx="${x.toFixed(1)}" cy="${y.toFixed(1)}" rx="${r.toFixed(1)}" ry="${(r * (.7 + rnd() * .3)).toFixed(1)}" fill="${s.korn[i % 4]}" transform="rotate(${Math.round(rnd() * 180)} ${x.toFixed(1)} ${y.toFixed(1)})"/>`;
    }
    let zack = 'M30 18'; for (let x = 30; x < 170; x += 7) zack += ` l3.5 -4 l3.5 4`;
    const tinte = s.kraft ? '#2b2116' : '#212121';
    return `<svg viewBox="0 0 200 270" ${attr} role="img" aria-label="Beutel ${s.name}">
      <defs>
        <linearGradient id="sch${k}" x1="0" x2="1"><stop offset="0" stop-color="#000" stop-opacity=".13"/><stop offset=".16" stop-color="#fff" stop-opacity="0"/><stop offset=".46" stop-color="#fff" stop-opacity=".16"/><stop offset=".8" stop-color="#000" stop-opacity="0"/><stop offset="1" stop-color="#000" stop-opacity=".17"/></linearGradient>
        <radialGradient id="sh${k}"><stop offset="0" stop-color="#000" stop-opacity=".22"/><stop offset="1" stop-color="#000" stop-opacity="0"/></radialGradient>
        <clipPath id="cl${k}"><path d="${koerper}"/></clipPath>
        <clipPath id="fe${k}"><rect x="64" y="216" width="72" height="30" rx="15"/></clipPath>
      </defs>
      <ellipse cx="100" cy="256" rx="92" ry="10" fill="url(#sh${k})"/>
      <path d="${zack} V42 H30 Z" fill="${papier}"/>
      <path d="${zack} V42 H30 Z" fill="url(#sch${k})"/>
      <rect x="30" y="30" width="140" height="1.2" fill="#000" opacity=".12"/>
      <g clip-path="url(#cl${k})">
        <rect x="0" y="0" width="200" height="270" fill="${papier}"/>
        <text x="100" y="72" text-anchor="middle" font-family="Young Serif, Georgia, serif" font-size="17" fill="${tinte}">blaufeld</text>
        <text x="100" y="84" text-anchor="middle" font-family="Instrument Sans, sans-serif" font-size="5.6" font-weight="600" letter-spacing="1.1" fill="${tinte}" opacity=".7">GRANOLA-MANUFAKTUR · ULM</text>
        <rect x="23" y="96" width="154" height="110" fill="${s.farbe}"/>
        ${muster(s, 31, 169, 106, rnd)}${muster(s, 31, 169, 196, rnd)}
        <text x="100" y="132" text-anchor="middle" font-family="Instrument Sans, sans-serif" font-size="9" font-weight="600" letter-spacing="1.4" fill="${s.hell}" opacity=".85">${s.nr.toUpperCase()}</text>
        <text x="100" y="154" text-anchor="middle" font-family="Young Serif, Georgia, serif" font-size="17" fill="${s.hell}">${s.z[0]}</text>
        <text x="100" y="174" text-anchor="middle" font-family="Young Serif, Georgia, serif" font-size="17" fill="${s.hell}">${s.z[1]}</text>
        <g clip-path="url(#fe${k})"><rect x="64" y="216" width="72" height="30" fill="#a8743c"/>${korn}</g>
        <rect x="64" y="216" width="72" height="30" rx="15" fill="none" stroke="${papier}" stroke-width="3"/>
        <rect x="0" y="0" width="200" height="270" fill="url(#sch${k})"/>
        <path d="M34 44 L39 250 M166 44 L161 250" stroke="#000" stroke-opacity=".05" stroke-width="2"/>
      </g>
    </svg>`;
  }

  /* Hero: großes Etikett-Feld in Blautopf-Blau, Haferflocken, zwei Beutel */
  function heroBauen() {
    const el = document.querySelector('[data-hero]'), rnd = zufall(11);
    let rand = '', flocken = '';
    // Etikett-Rahmen wie auf den Beuteln: Rauten und Punkte an allen Kanten
    for (let x = 18; x < 1000; x += 36) { rand += `<path d="M${x} 10l12 14-12 14-12-14Z" fill="#d9962b"/><circle cx="${x + 18}" cy="24" r="4.5" fill="#f5f4f1"/>`; rand += `<path d="M${x} 962l12 14-12 14-12-14Z" fill="#d9962b"/><circle cx="${x + 18}" cy="976" r="4.5" fill="#f5f4f1"/>`; }
    for (let y = 84; y < 930; y += 36) { rand += `<circle cx="52" cy="${y}" r="4.5" fill="#f5f4f1"/><circle cx="948" cy="${y}" r="4.5" fill="#f5f4f1"/>`; }
    for (let i = 0; i < 46; i++) {
      const x = 60 + rnd() * 880, y = 60 + rnd() * 880, r = 9 + rnd() * 10, a = Math.round(rnd() * 180);
      flocken += `<g transform="rotate(${a} ${x.toFixed(0)} ${y.toFixed(0)})"><ellipse cx="${x.toFixed(0)}" cy="${y.toFixed(0)}" rx="${r.toFixed(1)}" ry="${(r * .66).toFixed(1)}" fill="#f1e4c6"/><ellipse cx="${x.toFixed(0)}" cy="${(y - r * .12).toFixed(0)}" rx="${(r * .62).toFixed(1)}" ry="${(r * .3).toFixed(1)}" fill="#fff8e8" opacity=".7"/></g>`;
    }
    const svg = `<svg viewBox="0 0 1000 1000" preserveAspectRatio="xMidYMid slice" aria-hidden="true">
      <rect width="1000" height="1000" fill="#1f5fad"/>
      <rect x="70" y="56" width="860" height="888" fill="none" stroke="#3f7bc4" stroke-width="2"/>
      ${rand}${flocken}
      <g transform="translate(560 250) rotate(9)">${beutel(SORTEN[3], 5, 'width="300" height="405"')}</g>
      <g transform="translate(300 230) rotate(-5)">${beutel(SORTEN[0], 3, 'width="380" height="513"')}</g>
    </svg>`;
    el.insertAdjacentHTML('afterbegin', svg);
  }

  const STERN = '<svg aria-hidden="true" viewBox="0 0 16 16"><path fill="currentColor" d="m8 1.5 1.9 4 4.4.5-3.3 3 .9 4.3L8 11.1l-3.9 2.2.9-4.3-3.3-3 4.4-.5Z"/></svg>';
  const HERZ = '<svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6"><path d="M12 20s-7-4.4-7-10a4 4 0 0 1 7-2.6A4 4 0 0 1 19 10c0 5.6-7 10-7 10Z"/></svg>';
  function rasterBauen() {
    document.querySelector('[data-raster]').innerHTML = SORTEN.map((s, i) => `
      <article class="kachel" data-k="${i}">
        <a class="kachel__bild" href="#" aria-label="${s.name} ansehen">${beutel(s, 40 + i)}${s.badge ? `<span class="badge ${s.badge[1] ? 'badge--' + s.badge[1] : ''}">${s.badge[0]}</span>` : ''}</a>
        <button class="merken" type="button" aria-label="${s.name} merken">${HERZ}</button>
        <div class="kachel__text"><h3>${s.name}</h3><p class="preis">${s.preis}</p><p class="info">${s.info}</p><p class="wertung">${STERN}${s.w} <span>(${s.n})</span></p></div>
      </article>`).join('');
    const schale = (farbe, seed) => { const r = zufall(seed); let k = ''; for (let i = 0; i < 70; i++) { const a = r() * Math.PI * 2, d = Math.sqrt(r()) * 104, x = 160 + Math.cos(a) * d, y = 160 + Math.sin(a) * d; k += `<circle cx="${x.toFixed(0)}" cy="${y.toFixed(0)}" r="${(4 + r() * 7).toFixed(1)}" fill="${['#b07a3a', '#d6a35a', '#8a5626', '#f1e4c6', farbe][i % 5]}"/>`; } return `<svg viewBox="0 0 320 320" aria-hidden="true"><circle cx="160" cy="160" r="150" fill="#f5f4f1"/><circle cx="160" cy="160" r="124" fill="#efe3c8"/>${k}</svg>`; };
    document.querySelector('[data-welten]').innerHTML = `
      <a class="welt welt--ocker" href="#">${schale('#7a4c0c', 3)}<span class="etikett">Themenwelt</span><div><h3>Frühstück in fünf Minuten</h3><p>Granola, Joghurt, Obst. Unsere Sets für Eilige ab 15,80 €.</p></div></a>
      <a class="welt welt--blau" href="#">${schale('#cfe0f7', 8)}<span class="etikett">Themenwelt</span><div><h3>Für Touren auf der Alb</h3><p>Riegel und Porridge im Beutel, leicht im Rucksack.</p></div></a>
      <a class="welt welt--beere" href="#">${schale('#8e1d3f', 13)}<span class="etikett">Themenwelt</span><div><h3>Ohne Zucker&shy;zusatz</h3><p>Nur die Süße aus Datteln und Apfel.</p></div></a>`;
  }

  /* ---------- Radial: Fokus folgt einer Kachel ---------- */
  const fokusEl = document.querySelector('[data-fokus]');
  let fokusK = 1, fokusVon = null, fokusT0 = 0, fokusPos = null, fokusUhr = null;
  const DAUER_FOKUS = 400;   // ms, Produktbild-Wechsel laut Rezept
  // cubic-bezier(0, 0, 0.2, 1) per Newton-Verfahren
  function kurve(t) {
    const X1 = 0, X2 = 0.2, Y1 = 0, Y2 = 1;
    const bx = u => 3 * (1 - u) * (1 - u) * u * X1 + 3 * (1 - u) * u * u * X2 + u * u * u;
    const by = u => 3 * (1 - u) * (1 - u) * u * Y1 + 3 * (1 - u) * u * u * Y2 + u * u * u;
    const dx = u => 3 * (1 - u) * (1 - u) * X1 + 6 * (1 - u) * u * (X2 - X1) + 3 * u * u * (1 - X2);
    let u = t;
    for (let i = 0; i < 8; i++) { const f = bx(u) - t, d = dx(u); if (Math.abs(f) < 1e-5 || d < 1e-6) break; u = Math.min(1, Math.max(0, u - f / d)); }
    return by(u);
  }
  function zielVon(k) {
    const b = document.querySelector(`.kachel[data-k="${k}"] .kachel__bild`);
    if (!b) return { x: innerWidth / 2, y: innerHeight / 2 };
    const r = b.getBoundingClientRect();
    return { x: r.left + r.width / 2, y: r.top + r.height / 2 };
  }
  function fokusSetzen(k) {
    if (k === fokusK && fokusPos) return;
    fokusVon = fokusPos ? { ...fokusPos } : null; fokusK = k; fokusT0 = performance.now();
  }
  function fokusSchritt(t) {
    const z = zielVon(fokusK);
    const p = !fokusVon || reduziert() ? 1 : Math.min(1, (t - fokusT0) / DAUER_FOKUS);
    const e = kurve(p);
    fokusPos = fokusVon ? { x: fokusVon.x + (z.x - fokusVon.x) * e, y: fokusVon.y + (z.y - fokusVon.y) * e } : z;
    if (p >= 1) fokusVon = null;
    fokusEl.style.setProperty('--fx', fokusPos.x.toFixed(1) + 'px');
    fokusEl.style.setProperty('--fy', fokusPos.y.toFixed(1) + 'px');
    fokusUhr = variante === 'radial' ? requestAnimationFrame(fokusSchritt) : null;
  }

  function varianteSetzen(v) {
    variante = v; body.dataset.variante = v;
    document.querySelectorAll('[data-variante]').forEach(b => b.tagName === 'BUTTON' && b.setAttribute('aria-pressed', String(b.dataset.variante === v)));
    werteZeigen();
    if (v === 'radial' && !fokusUhr) { fokusPos = null; fokusUhr = requestAnimationFrame(fokusSchritt); }
  }
  document.querySelectorAll('button[data-variante]').forEach(b => b.addEventListener('click', () => {
    const lief = !!uhr; varianteSetzen(b.dataset.variante); if (lief) vorfuehren();
  }));
  document.querySelector('[data-ebenen]').addEventListener('click', e => {
    const an = body.classList.toggle('ebenen-zeigen'); e.currentTarget.setAttribute('aria-pressed', String(an));
  });

  /* ---------- Bedienung: Hover (Desktop), Tippen (Touch) ---------- */
  function kachelEvents() {
    document.querySelectorAll('.kachel').forEach(k => {
      k.addEventListener('pointerenter', e => { if (e.pointerType === 'mouse' && variante === 'radial' && !lief) fokusSetzen(+k.dataset.k); });
      k.querySelector('.kachel__bild').addEventListener('click', e => { e.preventDefault(); if (variante === 'radial') { anhalten(); fokusSetzen(+k.dataset.k); } });
    });
    document.querySelectorAll('a[href="#"]').forEach(a => a.addEventListener('click', e => e.preventDefault()));
  }

  /* ---------- Vorführung: Selbst-Scroll durch den Shop ---------- */
  let uhr = null, warte = null, t0 = 0, lief = false;
  const maxY = () => document.documentElement.scrollHeight - innerHeight;
  function rollen(nach, dauer, fertig) {
    const von = scrollY, s = performance.now();
    if (reduziert() || !dauer) { scrollTo(0, nach); fertig && fertig(); return; }
    const schritt = t => { const p = Math.min(1, (t - s) / dauer), e = 0.5 - 0.5 * Math.cos(p * Math.PI); scrollTo(0, von + (nach - von) * e); if (p < 1) uhr = requestAnimationFrame(schritt); else fertig && fertig(); };
    uhr = requestAnimationFrame(schritt);
  }
  function vorfuehren() {
    pause(); lief = true; t0 = performance.now();
    if (variante === 'radial') {
      let i = 0;
      const folge = [1, 2, 3, 0, 4, 6, 7, 5];
      const naechste = () => {
        const k = folge[i++ % folge.length], b = document.querySelector(`.kachel[data-k="${k}"] .kachel__bild`);
        const r = b.getBoundingClientRect(), y = Math.max(0, Math.min(maxY(), scrollY + r.top + r.height / 2 - innerHeight / 2));
        fokusSetzen(k);
        rollen(y, Math.abs(y - scrollY) > 4 ? 700 : 0, () => {});
        warte = setTimeout(naechste, reduziert() ? 2400 : 1700);
      };
      uhr = requestAnimationFrame(naechste);
      return;
    }
    // Etappen: Rollen 1200 ms, Halten 1600 ms; die erste Stelle zeigt Überschrift und Kacheln im Blur
    const absTop = el => el.getBoundingClientRect().top + scrollY;
    const stellen = () => {
      const h2 = document.querySelector('#bestseller h2'), k4 = document.querySelector('.kachel[data-k="4"]'), w = document.querySelector('.welten'), wa = document.querySelector('.warum');
      return [absTop(h2) - (mobil ? 70 : 95), absTop(k4) - (mobil ? 90 : 120), absTop(w) - (mobil ? 50 : 70), absTop(wa) - 180, 0].map(y => Math.max(0, Math.min(maxY(), Math.round(y))));
    };
    let i = 0;
    const etappe = () => {
      const st = stellen(), ziel = st[i % st.length], halten = i === 0 ? 3000 : 1600;
      i++;
      rollen(ziel, reduziert() ? 0 : 1200, () => { warte = setTimeout(etappe, reduziert() ? 3000 : halten); });
    };
    scrollTo(0, stellen()[0]); i = 1;
    warte = setTimeout(etappe, 3000);
  }
  function pause() { cancelAnimationFrame(uhr); clearTimeout(warte); uhr = null; warte = null; }
  function anhalten() { pause(); lief = false; }
  function weiter() { if (lief || html.classList.contains('vorschau')) vorfuehren(); }
  function neustart() { anhalten(); scrollTo(0, 0); fokusSetzen(1); }
  ['wheel', 'touchstart', 'keydown'].forEach(n => addEventListener(n, () => { if (!html.classList.contains('vorschau')) anhalten(); }, { passive: true }));

  heroBauen(); rasterBauen(); kachelEvents(); bauen();
  mobilMQ.addEventListener('change', bauen);

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