Zum Inhalt
← Navigation

Gestapelte bzw. mehrstufige Drawer-Navigation

Jede Menüebene eines Leinen-Shops schiebt sich als eigene Karte über die vorige; die zurückgetretenen Karten bleiben als 16-px-Kanten sichtbar und zeigen, wie tief man ist.

Ein seitlicher Drawer zeigt die erste Ebene. Ein Klick auf einen Bereich mit Unterpunkten schiebt eine zweite Karte leicht versetzt über die erste, statt ein Akkordeon aufzuklappen. Die erste Karte skaliert minimal zurück und dunkelt ab. Ein Zurück-Button oder ein Wisch führt eine Ebene zurück. Die gestapelten Kanten bleiben sichtbar und zeigen die Tiefe.

Eine tiefe Informationsarchitektur wird räumlich verständlich, ohne lange Akkordeonlisten.

Wann einsetzen

Shops, Institutionen und große Agenturen mit vielen Leistungsseiten, vor allem mobil.

Wann nicht

Bei flachen Navigationen und bei mehr als drei Ebenen.

Varianten

  • Gestapelter Drawer mit sichtbaren Kanten
  • Mehrstufige Navigation mit Slide
  • Akkordeon als Fallback

Umsetzung

Jede Ebene ist ein eigenes Panel, absolut im Drawer; die aktuelle Tiefe steht als Datenattribut. Neue Panels laufen von translateX(100 %) auf 0, das darunterliegende geht auf scale(0.96) und dunkelt ab. Beim Wechsel bekommt die Überschrift der neuen Ebene den Fokus, der Zurück-Button steht an erster Stelle. Fallstrick: Links der verdeckten Ebenen inert setzen.

Robust umsetzen

Telefon. Hauptanwendungsfall.

Ohne Bewegung. Panels ohne Slide austauschen.

Barrierefrei. Den Ebenenwechsel ansagen, indem die Überschrift den Fokus bekommt. Zurück per Escape oder Alt+Pfeil links. Untermenü-Auslöser sind Buttons mit aria-expanded.

Code

<!DOCTYPE html>
<!--
  Gestapelte bzw. mehrstufige Drawer-Navigation
  Fundus · Navigation · https://cecodesign.de/lab/fundus/navigation/gestapelte-drawer-navigation/
  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>Gestapelte bzw. mehrstufige Drawer-Navigation · 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: '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: '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; }

/* Sparrenlinnen, Bielefeld · Rezept „Onlineshop & Produktmarke: Produkt zuerst“:
   warme Neutraltöne als Bühne, Grotesk für Navigation, Preise und Produktnamen, Young Serif nur für Claims */
:root {
  --grund: #f5f4f1; --kachel: #ece9e2; --tinte: #212121; --leise: #666666; --linie: rgba(33, 33, 33, .12); --gruen: #10714e; --pink: #eb5c9d;
  --panel: #fdfcfa; --schublade: #dcd7cc;
  --vor: cubic-bezier(0.25, 1, 0.5, 1);   /* power3.out: Ebene schiebt sich drüber, 400 ms (Messwert 350–450) */
  --auf: cubic-bezier(0, 0, 0.2, 1);      /* Rezept: Öffnen mit Ausklang, 300 ms */
  --zu: cubic-bezier(0.4, 0, 1, 1);       /* Rezept: Schließen mit Anlauf, schneller */
  --versatz: 16px;                         /* sichtbare Stapelkante (Messwert 8–16 px) */
  --gx: clamp(16px, 3vw, 40px);
}
* { box-sizing: border-box; margin: 0; }
html { background: var(--grund); color: var(--tinte); font-family: 'Instrument Sans', system-ui, sans-serif; -webkit-font-smoothing: antialiased; }
html, body { overflow-x: clip; }
button { font: inherit; color: inherit; background: none; border: 0; cursor: pointer; padding: 0; text-align: left; }
a { color: inherit; text-decoration: none; }
ul { list-style: none; padding: 0; }
.serif { font-family: 'Young Serif', Georgia, serif; font-weight: 400; }
.badge { display: inline-flex; align-items: center; height: 20px; padding: 0 7px; border-radius: 4px; font: 600 .75rem/1 'Instrument Sans', sans-serif; letter-spacing: .06em; text-transform: uppercase; }
.preis { font-variant-numeric: tabular-nums; }

/* ── Seite ── */
.ansage { height: 34px; display: flex; align-items: center; justify-content: center; gap: 28px; background: var(--tinte); color: #f5f4f1; font-size: 13px; }
.ansage span + span::before { content: '·'; margin-right: 28px; opacity: .5; }
.kopf { position: sticky; top: 0; z-index: 4; display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; height: 64px; padding: 0 var(--gx); background: var(--grund); border-bottom: 1px solid var(--linie); }
.menueknopf { display: inline-flex; align-items: center; gap: 10px; height: 40px; padding: 0 14px 0 10px; margin-left: -10px; border-radius: 8px; font-weight: 500; font-size: 15px; transition: background-color 150ms var(--auf); justify-self: start; }
.menueknopf:hover { background: var(--kachel); }
.menueknopf svg { width: 20px; height: 20px; }
.marke { font: 400 25px/1 'Young Serif', Georgia, serif; letter-spacing: -.01em; }
.werkzeuge { display: flex; justify-content: flex-end; gap: 6px; }
.werkzeuge a { position: relative; width: 40px; height: 40px; border-radius: 8px; display: grid; place-items: center; transition: background-color 150ms var(--auf); }
.werkzeuge a:hover { background: var(--kachel); }
.werkzeuge svg { width: 21px; height: 21px; fill: none; stroke: currentColor; stroke-width: 1.6; stroke-linecap: round; stroke-linejoin: round; }
.werkzeuge .zahl { position: absolute; top: 5px; right: 4px; min-width: 16px; height: 16px; padding: 0 4px; border-radius: 8px; background: var(--tinte); color: #fff; font-size: 10.5px; font-weight: 600; display: grid; place-items: center; }

.held { display: grid; grid-template-columns: minmax(0, 1.2fr) minmax(0, 1fr); gap: 24px 64px; align-items: end; padding: clamp(28px, 5vh, 64px) var(--gx) clamp(22px, 3.4vh, 36px); }
.held h1 { font: 400 clamp(2.25rem, 1.4rem + 3.6vw, 4.5rem)/1.05 'Young Serif', Georgia, serif; letter-spacing: -.01em; text-wrap: balance; }
.held .neben { display: grid; gap: 18px; justify-items: start; padding-bottom: 6px; }
.held p { font-size: 1rem; line-height: 1.55; color: var(--leise); max-width: 46ch; }
.held .ctas { display: flex; flex-wrap: wrap; gap: 10px; }
.knopf { display: inline-flex; align-items: center; height: 46px; padding: 0 22px; border-radius: 999px; font-weight: 600; font-size: 15px; transition: background-color 150ms var(--auf), border-color 150ms var(--auf); }
.knopf.voll { background: var(--tinte); color: #fff; }
.knopf.voll:hover { background: #3a3a3a; }
.knopf.rand { border: 1px solid rgba(33,33,33,.3); }
.knopf.rand:hover { border-color: var(--tinte); }
.vertrauen { display: flex; flex-wrap: wrap; gap: 6px 18px; font-size: 13px; color: var(--leise); }
.vertrauen b { color: var(--tinte); font-weight: 600; }

.kacheln { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 12px; padding: 0 var(--gx) 40px; }
.kat { display: grid; gap: 10px; }
.kat .bild { position: relative; aspect-ratio: 4 / 5; border-radius: 6px; background: var(--kachel); overflow: hidden; }
.kat .bild svg { position: absolute; inset: 0; width: 100%; height: 100%; transition: transform 400ms var(--auf); }
.kat:hover .bild svg { transform: scale(1.03); }
.kat .bild .badge { position: absolute; left: 12px; top: 12px; background: #fff; }
.kat .zeile { display: flex; justify-content: space-between; gap: 12px; font-size: 1.0625rem; font-weight: 600; }
.kat .zeile span:last-child { font-weight: 500; color: var(--leise); font-size: .9375rem; }
.kat .farben { display: flex; gap: 5px; }
.kat .farben i { width: 14px; height: 14px; border-radius: 50%; border: 1px solid rgba(0,0,0,.12); }

/* ── Abdunklung und Schublade ── */
.dunkel { position: fixed; inset: 0; z-index: 20; background: rgba(33, 33, 33, .42); opacity: 0; visibility: hidden; transition: opacity 200ms var(--zu), visibility 0s linear 200ms; }
.offen .dunkel { opacity: 1; visibility: visible; transition: opacity 300ms var(--auf), visibility 0s; }
.schublade { position: fixed; top: 0; bottom: 0; left: 0; z-index: 21; width: min(440px, calc(100vw - 44px)); background: var(--schublade); transform: translateX(-102%); visibility: hidden;
             transition: transform 200ms var(--zu), visibility 0s linear 200ms; box-shadow: 24px 0 60px rgba(0,0,0,.18); overflow: hidden; }
.offen .schublade { transform: none; visibility: visible; transition: transform 300ms var(--auf), visibility 0s; }

/* Ebenen: jede Tiefe ist eine eigene Karte */
.ebene { position: absolute; top: 0; bottom: 0; right: 0; left: calc(var(--i) * var(--versatz)); display: grid; grid-template-rows: auto minmax(0, 1fr) auto; background: var(--panel);
         transform-origin: 0 50%; box-shadow: -10px 0 30px rgba(33, 33, 33, .1); border-left: 1px solid rgba(33,33,33,.06);
         transition: transform 400ms var(--vor), opacity 400ms var(--vor); }
.ebene:first-child { box-shadow: none; border-left: 0; }
.ebene + .ebene, .ebene[data-lage='unten'] { border-radius: 14px 0 0 14px; }
.ebene[data-lage='unten'] { overflow: hidden; }
.ebene::after { content: ''; position: absolute; inset: 0; background: #2a2723; opacity: 0; pointer-events: none; transition: opacity 400ms var(--vor); }
.ebene[data-lage='rechts'] { transform: translateX(calc(100% + 40px)); }
.ebene[data-lage='unten'] { transform: scale(calc(1 - var(--tiefe) * .04)); }
.ebene[data-lage='unten']::after { opacity: calc(.08 + var(--tiefe) * .1); }
.schublade[data-richtung='zurueck'] .ebene, .schublade[data-richtung='zurueck'] .ebene::after { transition-duration: 260ms; transition-timing-function: var(--zu); }
.schublade[data-richtung='zurueck'] .ebene[data-lage='oben'], .schublade[data-richtung='zurueck'] .ebene[data-lage='oben']::after { transition-timing-function: var(--vor); transition-duration: 340ms; }
/* Variante Slide: keine Kanten, die alte Ebene weicht nach links aus */
.schublade[data-variante='slide'] .ebene { left: 0; box-shadow: none; }
.schublade[data-variante='slide'] .ebene[data-lage='unten'] { transform: translateX(-28%); opacity: 0; }
.schublade[data-variante='slide'] .ebene[data-lage='unten']::after { opacity: 0; }
.schublade[data-variante='slide'] .ebene[data-lage='rechts'] { transform: translateX(100%); }

.e-kopf { display: grid; grid-template-columns: 1fr auto; align-items: center; gap: 8px; min-height: 64px; padding: 10px 14px 10px 22px; border-bottom: 1px solid var(--linie); }
.e-kopf .titel { display: flex; align-items: baseline; gap: 10px; font-size: 1.25rem; font-weight: 600; letter-spacing: -.015em; outline: none; }
.e-kopf .titel small { font-size: .8125rem; font-weight: 500; color: var(--leise); letter-spacing: 0; }
.zurueck { display: inline-flex; align-items: center; gap: 6px; height: 34px; padding: 0 10px 0 6px; margin-left: -8px; border-radius: 8px; font-size: .875rem; font-weight: 500; color: var(--leise); transition: background-color 150ms var(--auf), color 150ms var(--auf); grid-column: 1 / -1; justify-self: start; }
.zurueck:hover { background: var(--kachel); color: var(--tinte); }
.zurueck svg, .schliessen svg, .weiter svg { width: 18px; height: 18px; fill: none; stroke: currentColor; stroke-width: 1.7; stroke-linecap: round; stroke-linejoin: round; }
.e-kopf.tief { grid-template-rows: auto auto; padding-top: 10px; row-gap: 2px; }
.e-kopf.tief .titel { grid-row: 2; }
.e-kopf.tief .schliessen { grid-row: 1 / 3; grid-column: 2; }
.schliessen { width: 40px; height: 40px; border-radius: 8px; display: grid; place-items: center; transition: background-color 150ms var(--auf); }
.schliessen:hover { background: var(--kachel); }
.e-liste { overflow-y: auto; overscroll-behavior: contain; padding: 8px 10px 18px; }
.e-liste li > a, .e-liste li > button { width: 100%; display: flex; align-items: center; justify-content: space-between; gap: 12px; min-height: 52px; padding: 0 12px; border-radius: 8px; font-size: 1.0625rem; font-weight: 500; transition: background-color 150ms var(--auf); }
.e-liste li > a:hover, .e-liste li > button:hover { background: var(--kachel); }
.e-liste li > button .weiter { color: var(--leise); display: inline-flex; align-items: center; gap: 8px; font-size: .8125rem; font-weight: 500; }
.e-liste .klein > a { min-height: 42px; font-size: .9375rem; font-weight: 500; color: var(--leise); }
.e-liste .trenner { height: 1px; background: var(--linie); margin: 8px 12px; }
.e-liste .alle > a { font-weight: 600; text-decoration: underline; text-decoration-color: rgba(33,33,33,.25); text-underline-offset: 4px; }
.e-liste .badge.sale { background: var(--pink); color: var(--tinte); }
.e-liste .badge.neu { background: var(--tinte); color: #fff; }
.e-liste .badge.gratis { background: rgba(16,113,78,.1); color: var(--gruen); }
.e-liste .mitfarbe { flex-direction: column; align-items: flex-start; justify-content: center; gap: 6px; padding-top: 10px; padding-bottom: 10px; }
.e-liste .mitfarbe small { font-size: .8125rem; color: var(--leise); font-weight: 400; }
.e-liste .groessen { display: flex; flex-wrap: wrap; gap: 6px; padding: 6px 12px 10px; }
.e-liste .groessen a { width: auto; min-height: 0; font-weight: 500; height: 34px; padding: 0 12px; border-radius: 999px; border: 1px solid rgba(33,33,33,.2); display: inline-flex; align-items: center; font-size: .875rem; font-variant-numeric: tabular-nums; transition: border-color 150ms var(--auf); }
.e-liste .groessen a:hover { border-color: var(--tinte); }
.e-liste .unterzeile { padding: 14px 12px 4px; font: 600 .75rem/1 'Instrument Sans', sans-serif; letter-spacing: .06em; text-transform: uppercase; color: var(--leise); }
.e-liste li > a.tipp-karte { display: grid; min-height: 0; font-size: inherit; font-weight: inherit; }
.tipp-karte { display: grid; grid-template-columns: 92px 1fr; gap: 14px; align-items: center; margin: 10px 2px 2px; padding: 10px; border-radius: 10px; background: var(--kachel); }
.tipp-karte .bild { width: 92px; aspect-ratio: 4 / 5; border-radius: 6px; overflow: hidden; background: #e2ddd3; }
.tipp-karte .bild svg { width: 100%; height: 100%; display: block; }
.tipp-karte b { color: var(--tinte); display: block; font-size: 1rem; font-weight: 600; }
.tipp-karte span { display: block; font-size: .875rem; color: var(--leise); margin-top: 3px; }
.tipp-karte .preis { color: var(--tinte); font-weight: 600; margin-top: 8px; }
.e-fuss { border-top: 1px solid var(--linie); padding: 12px 22px 16px; display: grid; gap: 10px; font-size: .875rem; }
.e-fuss .reihe { display: flex; flex-wrap: wrap; gap: 4px 18px; color: var(--leise); }
.e-fuss .reihe a { display: inline-block; padding-block: 4px; margin-block: -4px; }
.e-fuss .reihe a:hover { color: var(--tinte); }
.e-fuss .versand { display: flex; align-items: center; gap: 8px; color: var(--gruen); font-weight: 500; }
.e-fuss .versand i { width: 7px; height: 7px; border-radius: 50%; background: currentColor; }

/* Variante Akkordeon (Fallback) */
.akk { position: absolute; inset: 0; display: none; grid-template-rows: auto minmax(0, 1fr) auto; background: var(--panel); }
.schublade[data-variante='akkordeon'] .akk { display: grid; }
.schublade[data-variante='akkordeon'] .ebene { display: none; }
.akk .auf { display: grid; grid-template-rows: 0fr; transition: grid-template-rows 200ms var(--zu); }
.akk .auf > div { overflow: hidden; min-height: 0; }
.akk button[aria-expanded='true'] + .auf { grid-template-rows: 1fr; transition: grid-template-rows 300ms var(--auf); }
.akk button .weiter svg { transition: transform 200ms var(--zu); }
.akk button[aria-expanded='true'] .weiter svg { transform: rotate(90deg); transition: transform 300ms var(--auf); }
.akk .auf ul { padding-left: 14px; border-left: 1px solid var(--linie); margin: 2px 0 6px 12px; }
.akk .auf li > a, .akk .auf li > button { min-height: 44px; font-size: .9375rem; }

:focus-visible { outline: 2px solid var(--tinte); outline-offset: 2px; }
.e-liste :focus-visible { outline-offset: -2px; }

/* Vorführung: gezeichneter Zeiger, Druckpunkt */
.zeiger { position: fixed; left: 0; top: 0; z-index: 60; width: 20px; height: 20px; pointer-events: none; display: none; transition: transform 560ms var(--vor); }
.vorfuehrung .zeiger { display: block; }
.drueck { background: var(--kachel) !important; }

/* ── Pult: liegt fest unten rechts über der Seite ── */
.pult { position: fixed; right: 16px; bottom: 16px; z-index: 50; display: grid; gap: 8px; padding: 12px 14px; border-radius: 12px; background: rgba(253, 252, 250, .94); border: 1px solid var(--linie); box-shadow: 0 10px 30px rgba(0,0,0,.12); -webkit-backdrop-filter: blur(10px); backdrop-filter: blur(10px);
        font: 500 10.5px/1.4 'Geist Mono', ui-monospace, monospace; letter-spacing: .12em; text-transform: uppercase; color: var(--leise); max-width: 340px; }
.pult .modi { display: flex; flex-wrap: wrap; gap: 4px; }
.pult button { font: inherit; letter-spacing: inherit; text-transform: inherit; height: 26px; padding: 0 10px; border-radius: 999px; border: 1px solid var(--linie); color: var(--leise); transition: background-color 150ms var(--auf), color 150ms var(--auf); }
.pult button:hover { color: var(--tinte); }
.pult button[aria-pressed='true'] { background: var(--tinte); color: #fff; border-color: var(--tinte); }
.pult .werte { letter-spacing: .04em; text-transform: none; font-size: 11px; line-height: 1.5; }
.pult .werte b { color: var(--tinte); font-weight: 500; }

/* Reduzierte Bewegung: Ebenen tauschen ohne Slide, Schublade und Abdunklung nur kurz blenden */
.reduziert .ebene, .reduziert .ebene::after { transition: opacity 120ms linear !important; }
.reduziert .ebene[data-lage='rechts'] { opacity: 0; transform: none; }
.reduziert .schublade { transform: none; opacity: 0; transition: opacity 120ms linear, visibility 0s linear 120ms; }
.reduziert.offen .schublade { opacity: 1; transition: opacity 120ms linear, visibility 0s; }
.reduziert .akk .auf, .reduziert .akk button .weiter svg, .reduziert .zeiger, .reduziert .kat .bild svg { transition: none !important; }

/* Vorschau: Seite und Schublade, ohne Pult */
.vorschau body { overflow: hidden; }

@media (max-width: 1100px) {
  .kacheln { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .ansage span:nth-child(3) { display: none; }
}
@media (max-width: 760px) {
  .ansage { font-size: 12px; }
  .ansage span:nth-child(2) { display: none; }
  .kopf { height: 58px; }
  .menueknopf span { display: none; }
  .menueknopf { padding: 0 10px; }
  .marke { font-size: 22px; }
  .werkzeuge .weg { display: none; }
  .held { grid-template-columns: 1fr; gap: 16px; padding-top: 26px; }
  .held h1 { font-size: clamp(2.25rem, 10.5vw, 3rem); }
  .kacheln { gap: 10px; }
  .e-kopf { padding-left: 18px; }
}
@media (hover: none) {
  .menueknopf:hover, .werkzeuge a:hover, .e-liste li > a:hover, .e-liste li > button:hover, .zurueck:hover, .schliessen:hover { background: none; }
  .kat:hover .bild svg { transform: none; }
  .knopf.voll:hover { background: var(--tinte); }
}
@media (forced-colors: active) { button, [role=button], [role=tab], [role=switch] { border: 1px solid ButtonText !important; } }
</style>
</head>
<body>
<div class="seite" data-seite>
  <p class="ansage"><span>Gratisversand ab 69 €</span><span>60 Nächte Probe schlafen</span><span>Gewebt in Ostwestfalen</span></p>
  <header class="kopf">
    <button type="button" class="menueknopf" data-menue aria-expanded="false" aria-controls="schublade">
      <svg viewBox="0 0 20 20" aria-hidden="true"><path d="M3 6h14M3 10h14M3 14h9" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg><span>Menü</span>
    </button>
    <a class="marke" href="#">Sparrenlinnen</a>
    <div class="werkzeuge">
      <a href="#" aria-label="Suche"><svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="10.5" cy="10.5" r="6"/><path d="M15 15l5 5"/></svg></a>
      <a href="#" class="weg" aria-label="Merkliste"><svg viewBox="0 0 24 24" aria-hidden="true"><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></a>
      <a href="#" aria-label="Warenkorb, 2 Artikel"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 8h14l-1.2 11.2a1 1 0 0 1-1 .8H7.2a1 1 0 0 1-1-.8z"/><path d="M9 8V6.5a3 3 0 0 1 6 0V8"/></svg><span class="zahl">2</span></a>
    </div>
  </header>

  <main>
    <section class="held">
      <h1>Leinen, das mit jeder Wäsche weicher wird.</h1>
      <div class="neben">
        <p>Gewebt in Ostwestfalen, mit Steinen gewaschen, genäht in Bielefeld. Bettwäsche, Handtücher und Tischwäsche aus reinem Leinen, ohne Weichmacher und ohne Knitterausrüstung.</p>
        <div class="ctas"><a class="knopf voll" href="#">Bettwäsche entdecken</a><a class="knopf rand" href="#">Musterbox, gratis</a></div>
        <p class="vertrauen"><span><b>4,8 / 5</b> aus 2.314 Bewertungen</span><span>Oeko-Tex Standard 100</span><span>Rücksendung kostenlos</span></p>
      </div>
    </section>
    <section class="kacheln" aria-label="Kategorien">
      <a class="kat" href="#"><div class="bild" data-stapel="salbei"><span class="badge">Neu</span></div><span class="zeile"><span>Bettwäsche</span><span class="preis">ab 89 €</span></span><span class="farben" data-farben="salbei"></span></a>
      <a class="kat" href="#"><div class="bild" data-stapel="natur"></div><span class="zeile"><span>Handtücher</span><span class="preis">ab 24 €</span></span><span class="farben" data-farben="natur"></span></a>
      <a class="kat" href="#"><div class="bild" data-stapel="terrakotta"></div><span class="zeile"><span>Tisch &amp; Küche</span><span class="preis">ab 19 €</span></span><span class="farben" data-farben="terrakotta"></span></a>
      <a class="kat" href="#"><div class="bild" data-stapel="rauch"></div><span class="zeile"><span>Wohnen</span><span class="preis">ab 39 €</span></span><span class="farben" data-farben="rauch"></span></a>
    </section>
  </main>
</div>

<div class="dunkel" data-dunkel></div>
<nav class="schublade" id="schublade" data-schublade data-variante="stapel" aria-label="Hauptmenü" inert></nav>

<div class="pult">
  <div class="modi" role="group" aria-label="Variante"><button type="button" data-v="stapel" aria-pressed="true">Stapel</button><button type="button" data-v="slide" aria-pressed="false">Slide</button><button type="button" data-v="akkordeon" aria-pressed="false">Akkordeon</button></div>
  <p class="werte" data-werte></p>
</div>
<svg class="zeiger" data-zeiger viewBox="0 0 18 18" aria-hidden="true"><path d="M2 1.5 15.5 9.2l-6 1.3-2.9 5.6Z" fill="#212121" stroke="#fdfcfa" stroke-width="1.2" stroke-linejoin="round"/></svg>

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

  /* ── Bildwelt: gefaltete Leinenstapel als SVG (im echten Shop Produktfotos auf #ece9e2) ── */
  const FARBEN = {
    salbei: ['#9fab92', '#b9c2ad', '#e8e3d6', '#8c9a80'],
    natur: ['#d9cdb6', '#e9e1d0', '#c4b497', '#f3efe6'],
    terrakotta: ['#c47a5a', '#dcb79c', '#e9e1d2', '#a9634a'],
    rauch: ['#8d9aa6', '#b7c0c7', '#5f6b76', '#e6e3dc'],
  };
  const SWATCH = { salbei: ['#9fab92', '#e8e3d6', '#8d9aa6', '#c47a5a'], natur: ['#e9e1d0', '#ffffff', '#c4b497'], terrakotta: ['#c47a5a', '#e9e1d2', '#3f3f3d'], rauch: ['#8d9aa6', '#5f6b76', '#e6e3dc'] };
  let uid = 0;
  function stapel(name, banderole = true) {
    const f = FARBEN[name], id = 'p' + (uid++);
    const lagen = [[0, f[2], 8], [1, f[1], -6], [2, f[0], 4], [3, f[3], -3]];
    const B = 270, H = 44, x0 = 65, unten = 392;
    let s = `<svg viewBox="0 0 400 500" aria-hidden="true"><defs>
      <pattern id="${id}t" width="4" height="4" patternUnits="userSpaceOnUse"><path d="M0 .5h4M.5 0v4" stroke="#000" stroke-width=".5" opacity=".09"/></pattern>
      <linearGradient id="${id}g" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#fff" stop-opacity=".22"/><stop offset=".35" stop-color="#fff" stop-opacity="0"/><stop offset="1" stop-color="#000" stop-opacity=".16"/></linearGradient>
      <filter id="${id}s" x="-30%" y="-30%" width="160%" height="160%"><feGaussianBlur stdDeviation="10"/></filter></defs>
      <ellipse cx="200" cy="${unten + 12}" rx="170" ry="16" fill="rgba(60,50,35,.28)" filter="url(#${id}s)"/>`;
    lagen.forEach(([i, farbe, dx]) => {
      const y = unten - (i + 1) * H, x = x0 + dx;
      s += `<g><rect x="${x}" y="${y}" width="${B}" height="${H}" rx="14" fill="${farbe}"/>
        <rect x="${x}" y="${y}" width="${B}" height="${H}" rx="14" fill="url(#${id}t)"/>
        <rect x="${x}" y="${y}" width="${B}" height="${H}" rx="14" fill="url(#${id}g)"/>
        <path d="M${x + 16} ${y + H * .55} q${B / 2 - 16} 7 ${B - 32} 0" fill="none" stroke="rgba(0,0,0,.1)" stroke-width="1.4"/>
        <path d="M${x + 10} ${y + 3} h${B - 20}" stroke="rgba(255,255,255,.35)" stroke-width="2" stroke-linecap="round"/></g>`;
    });
    if (banderole) {
      const y = unten - 4 * H - 2;
      s += `<rect x="176" y="${y}" width="58" height="${4 * H + 2}" fill="#f4efe4" opacity=".96"/><rect x="176" y="${y}" width="58" height="${4 * H + 2}" fill="url(#${id}t)"/>
        <text x="205" y="${y + 2 * H + 1}" text-anchor="middle" dominant-baseline="middle" transform="rotate(-90 205 ${y + 2 * H + 1})" font-family="Young Serif, Georgia, serif" font-size="15" fill="#3b3833">Sparrenlinnen</text>`;
    }
    return s + '</svg>';
  }
  $$('[data-stapel]').forEach(el => el.insertAdjacentHTML('afterbegin', stapel(el.dataset.stapel)));
  $$('[data-farben]').forEach(el => { el.innerHTML = SWATCH[el.dataset.farben].map(c => `<i style="background:${c}"></i>`).join(''); });

  /* ── Menü als Daten: höchstens drei Ebenen ── */
  const E = {
    menue: { titel: 'Menü', items: [
      { t: 'Bettwäsche', zu: 'bettwaesche', hinweis: '6' }, { t: 'Bad', zu: 'bad', hinweis: '5' }, { t: 'Tisch & Küche', zu: 'tisch', hinweis: '5' },
      { t: 'Wohnen', zu: 'wohnen', hinweis: '4' }, { t: 'Bekleidung', zu: 'kleidung', hinweis: '4' }, { trenner: true },
      { t: 'Geschenke' }, { t: 'Musterbox', badge: ['gratis', 'Gratis'] }, { t: 'Sale', badge: ['sale', '−30 %'] }] },
    bettwaesche: { titel: 'Bettwäsche', eltern: 'menue', tipp: ['salbei', 'Neu: Halbleinen Salbei', 'Leinen und Baumwolle, glatter im Griff', 'ab 89 €'], items: [
      { t: 'Alle Bettwäsche', alle: true }, { t: 'Bettbezüge', zu: 'bettbezuege', hinweis: '3 Stoffe' }, { t: 'Kissenbezüge', zu: 'kissen', hinweis: '4 Größen' },
      { t: 'Spannbettlaken' }, { t: 'Bettwäsche-Sets', badge: ['neu', 'Neu'] }, { t: 'Babybettwäsche' }] },
    bettbezuege: { titel: 'Bettbezüge', eltern: 'bettwaesche', items: [
      { t: 'Alle Bettbezüge', alle: true },
      { t: 'Reinleinen, stonewashed', farbe: 'salbei', unter: '100 % Leinen · 165 g/m² · ab 129 €' },
      { t: 'Halbleinen', farbe: 'natur', unter: '50 % Leinen, 50 % Baumwolle · ab 89 €' },
      { t: 'Leinen-Satin', farbe: 'rauch', unter: 'Glänzend, kühl im Sommer · ab 149 €' },
      { unterzeile: 'Nach Größe' }, { groessen: ['135 × 200', '155 × 220', '200 × 200', '240 × 220'] }] },
    kissen: { titel: 'Kissenbezüge', eltern: 'bettwaesche', items: [
      { t: 'Alle Kissenbezüge', alle: true }, { t: '40 × 80 cm' }, { t: '80 × 80 cm' }, { t: '50 × 70 cm' }, { t: 'Nackenrolle 15 × 40 cm' }] },
    bad: { titel: 'Bad', eltern: 'menue', items: [{ t: 'Alles fürs Bad', alle: true }, { t: 'Handtücher' }, { t: 'Badetücher' }, { t: 'Saunatücher' }, { t: 'Bademäntel', badge: ['neu', 'Neu'] }, { t: 'Badematten' }] },
    tisch: { titel: 'Tisch & Küche', eltern: 'menue', items: [{ t: 'Alles für Tisch & Küche', alle: true }, { t: 'Tischdecken' }, { t: 'Servietten' }, { t: 'Tischläufer' }, { t: 'Geschirrtücher' }, { t: 'Schürzen' }] },
    wohnen: { titel: 'Wohnen', eltern: 'menue', items: [{ t: 'Alles fürs Wohnen', alle: true }, { t: 'Kissenhüllen' }, { t: 'Plaids' }, { t: 'Vorhänge' }, { t: 'Wäschesäcke' }] },
    kleidung: { titel: 'Bekleidung', eltern: 'menue', items: [{ t: 'Alle Bekleidung', alle: true }, { t: 'Hemden' }, { t: 'Kleider' }, { t: 'Hosen' }, { t: 'Schlafanzüge' }] },
  };
  const ICO = {
    rechts: '<svg viewBox="0 0 20 20" aria-hidden="true"><path d="M8 5l5 5-5 5"/></svg>',
    links: '<svg viewBox="0 0 20 20" aria-hidden="true"><path d="M12 5l-5 5 5 5"/></svg>',
    x: '<svg viewBox="0 0 20 20" aria-hidden="true"><path d="M5 5l10 10M15 5L5 15"/></svg>',
  };
  const fuss = `<div class="e-fuss"><span class="versand"><i></i>Bestellt bis 14 Uhr, versendet heute aus Bielefeld</span><div class="reihe"><a href="#">Hilfe &amp; Versand</a><a href="#">60 Tage Rückgabe</a><a href="#">Mein Konto</a></div></div>`;

  function eintrag(it, akk) {
    if (it.trenner) return '<li class="trenner" role="presentation"></li>';
    if (it.unterzeile) return `<li class="unterzeile">${it.unterzeile}</li>`;
    if (it.groessen) return `<li class="groessen">${it.groessen.map(g => `<a href="#">${g}</a>`).join('')}</li>`;
    const badge = it.badge ? `<span class="badge ${it.badge[0]}">${it.badge[1]}</span>` : '';
    if (it.zu && akk) return `<li><button type="button" aria-expanded="false" data-ziel="${it.zu}">${it.t}<span class="weiter">${ICO.rechts}</span></button><div class="auf"><div><ul>${E[it.zu].items.map(k => eintrag(k, true)).join('')}</ul></div></div></li>`;
    if (it.zu) return `<li><button type="button" aria-expanded="false" data-ziel="${it.zu}">${it.t}<span class="weiter">${it.hinweis ? `<span>${it.hinweis}</span>` : ''}${ICO.rechts}</span></button></li>`;
    if (it.farbe) return `<li><a href="#" class="mitfarbe">${it.t}<small>${it.unter}</small><span class="farben" style="display:flex;gap:5px">${SWATCH[it.farbe].map(c => `<i style="width:14px;height:14px;border-radius:50%;border:1px solid rgba(0,0,0,.12);background:${c}"></i>`).join('')}</span></a></li>`;
    return `<li class="${it.alle ? 'alle' : ''}"><a href="#">${it.t}${badge}</a></li>`;
  }

  /* ── Schublade mit gestapelten Ebenen ── */
  const schublade = $('[data-schublade]'), dunkel = $('[data-dunkel]'), menueknopf = $('[data-menue]'), zeiger = $('[data-zeiger]');
  let variante = 'stapel', pfad = [];

  function ebeneBauen(id, i) {
    const e = E[id], el = document.createElement('section');
    el.className = 'ebene'; el.id = 'ebene-' + id; el.dataset.id = id;
    el.style.setProperty('--i', i);
    el.setAttribute('aria-labelledby', 'titel-' + id);
    const eltern = e.eltern ? E[e.eltern].titel : '';
    el.innerHTML = `
      <div class="e-kopf${i ? ' tief' : ''}">
        ${i ? `<button type="button" class="zurueck" data-zurueck aria-label="Zurück zu ${eltern}">${ICO.links}${eltern}</button>` : ''}
        <h2 class="titel" id="titel-${id}" tabindex="-1">${e.titel}${i === 2 ? `<small>${E[E[e.eltern].eltern].titel} / ${eltern}</small>` : ''}</h2>
        <button type="button" class="schliessen" data-schliessen aria-label="Menü schließen">${ICO.x}</button>
      </div>
      <ul class="e-liste">${e.items.map(it => eintrag(it)).join('')}${e.tipp ? `<li><a class="tipp-karte" href="#"><span class="bild">${stapel(e.tipp[0], false)}</span><span><b>${e.tipp[1]}</b><span>${e.tipp[2]}</span><span class="preis">${e.tipp[3]}</span></span></a></li>` : ''}</ul>
      ${i === 0 ? fuss : ''}`;
    return el;
  }
  function akkBauen() {
    const el = document.createElement('div');
    el.className = 'akk';
    el.innerHTML = `<div class="e-kopf"><h2 class="titel" tabindex="-1">Menü</h2><button type="button" class="schliessen" data-schliessen aria-label="Menü schließen">${ICO.x}</button></div>
      <ul class="e-liste">${E.menue.items.map(it => eintrag(it, true)).join('')}</ul>${fuss}`;
    return el;
  }

  // Lage aller Ebenen: oben (aktiv), unten (zurückgetreten, --tiefe), rechts (außerhalb)
  function lage() {
    const ebenen = $$('.ebene'), n = pfad.length - 1;
    ebenen.forEach(el => {
      const i = pfad.indexOf(el.dataset.id);
      if (i < 0) { el.dataset.lage = 'rechts'; el.inert = true; return; }
      el.dataset.lage = i === n ? 'oben' : 'unten';
      el.style.setProperty('--tiefe', n - i);
      el.inert = i !== n;
    });
    $$('.ebene [data-ziel]').forEach(b => b.setAttribute('aria-expanded', String(pfad.includes(b.dataset.ziel))));
  }
  function aufbauen() {
    schublade.innerHTML = '';
    schublade.appendChild(ebeneBauen('menue', 0));
    schublade.appendChild(akkBauen());
    pfad = ['menue'];
    lage();
  }

  const oben = () => $('#ebene-' + pfad[pfad.length - 1]);
  const raus = new Set();
  function vor(id) {
    if (pfad.includes(id) || pfad.length >= 3) return;
    schublade.dataset.richtung = 'vor';
    const el = ebeneBauen(id, pfad.length);
    el.dataset.lage = 'rechts';
    schublade.appendChild(el);
    void el.offsetWidth;                       // Startlage festschreiben, dann schieben
    pfad.push(id);
    lage();
    el.querySelector('.titel').focus({ preventScroll: true });   // Ebenenwechsel ansagen
  }
  function zurueck() {
    if (pfad.length < 2) { schliessen(); return; }
    schublade.dataset.richtung = 'zurueck';
    const id = pfad.pop(), el = $('#ebene-' + id);
    lage();
    raus.add(el);
    const weg = () => { if (raus.has(el)) { raus.delete(el); el.remove(); } };
    if (reduziert()) setTimeout(weg, 130); else el.addEventListener('transitionend', e => { if (e.target === el && e.propertyName === 'transform') weg(); });
    setTimeout(weg, 600);
    const zurueckZu = $(`#ebene-${pfad[pfad.length - 1]} [data-ziel="${id}"]`);
    if (zurueckZu) zurueckZu.focus({ preventScroll: true });
  }
  function oeffnen() {
    html.classList.add('offen');
    schublade.inert = false;
    menueknopf.setAttribute('aria-expanded', 'true');
    $('[data-seite]').inert = true;
    setTimeout(() => { const t = variante === 'akkordeon' ? $('.akk .titel') : oben().querySelector('.titel'); t && t.focus({ preventScroll: true }); }, 30);
  }
  function schliessen() {
    html.classList.remove('offen');
    schublade.inert = true;
    menueknopf.setAttribute('aria-expanded', 'false');
    $('[data-seite]').inert = false;
    menueknopf.focus({ preventScroll: true });
    setTimeout(() => { if (!html.classList.contains('offen')) { aufbauen(); } }, 260);
  }

  schublade.addEventListener('click', e => {
    const z = e.target.closest('[data-ziel]');
    if (z && z.closest('.akk')) { z.setAttribute('aria-expanded', String(z.getAttribute('aria-expanded') !== 'true')); return; }
    if (z) { vor(z.dataset.ziel); return; }
    if (e.target.closest('[data-zurueck]')) { zurueck(); return; }
    if (e.target.closest('[data-schliessen]')) { schliessen(); return; }
    if (e.target.closest('a')) e.preventDefault();
  });
  menueknopf.addEventListener('click', () => html.classList.contains('offen') ? schliessen() : oeffnen());
  dunkel.addEventListener('click', schliessen);
  addEventListener('keydown', e => {
    if (!html.classList.contains('offen')) return;
    if (e.key === 'Escape') { e.preventDefault(); variante === 'akkordeon' ? schliessen() : zurueck(); }
    if (e.altKey && e.key === 'ArrowLeft' && variante !== 'akkordeon') { e.preventDefault(); zurueck(); }
  });
  // Wisch nach rechts führt eine Ebene zurück (Touch)
  let wx = null, wy = 0;
  schublade.addEventListener('touchstart', e => { wx = e.touches[0].clientX; wy = e.touches[0].clientY; }, { passive: true });
  schublade.addEventListener('touchend', e => {
    if (wx === null) return;
    const dx = e.changedTouches[0].clientX - wx, dy = Math.abs(e.changedTouches[0].clientY - wy);
    if (dx > 70 && dy < 40 && variante !== 'akkordeon') zurueck();
    wx = null;
  }, { passive: true });
  $$('.seite a').forEach(a => a.addEventListener('click', e => e.preventDefault()));

  /* ── Varianten und Werte ── */
  function werteZeigen() {
    const w = $('[data-werte]');
    if (reduziert()) { w.innerHTML = 'Reduziert: Ebenen tauschen ohne Slide, <b>120 ms</b> Überblenden, Stapelkante bleibt'; return; }
    w.innerHTML = variante === 'stapel'
      ? 'Vor <b>400 ms power3.out</b> · Kante <b>16 px</b> · darunter <b>scale 0.96</b>, Abdunklung <b>18 %</b> · zurück <b>260 ms</b> (0.4, 0, 1, 1)'
      : variante === 'slide'
      ? 'Vor <b>400 ms power3.out</b> · neue Ebene 100 % → 0 · alte <b>−28 %</b>, Deckkraft 0 · zurück <b>260 ms</b>'
      : 'Höhe <b>grid-template-rows 0fr → 1fr</b> · auf <b>300 ms</b> (0, 0, 0.2, 1) · zu <b>200 ms</b> (0.4, 0, 1, 1)';
  }
  function setzen(v) {
    variante = v; schublade.dataset.variante = v;
    $$('[data-v]').forEach(b => b.setAttribute('aria-pressed', String(b.dataset.v === v)));
    aufbauen(); werteZeigen();
  }
  $$('[data-v]').forEach(b => b.addEventListener('click', () => { if (vorfuehrung) stopp(); setzen(b.dataset.v); if (!html.classList.contains('offen')) oeffnen(); }));

  /* ── Vorführung: gedachter Zeiger öffnet, klickt zwei Ebenen tief, geht zurück, schließt ── */
  let vorfuehrung = false, uhren = [], runde = 0;
  const REIHE = ['stapel', 'slide', 'akkordeon'];
  function nach(ms, f) { uhren.push(setTimeout(f, ms)); }
  function zeigeAuf(el) {
    if (!el) return;
    const r = el.getBoundingClientRect();
    zeiger.style.transform = `translate(${r.left + Math.min(r.width * .35, 120)}px, ${r.top + r.height * .55}px)`;
  }
  function druecken(el, f) { if (!el) return; el.classList.add('drueck'); setTimeout(() => { el.classList.remove('drueck'); f(); }, 160); }
  function ablauf() {
    const akk = variante === 'akkordeon';
    const k = s => akk ? $(`.akk [data-ziel="${s}"]`) : oben().querySelector(`[data-ziel="${s}"]`);
    const schritte = [
      [() => menueknopf, el => druecken(el, oeffnen), 1100],
      [() => k('bettwaesche'), el => druecken(el, () => el.click()), 1300],
      [() => k('bettbezuege'), el => druecken(el, () => el.click()), 2000],
      akk ? [() => k('bettbezuege'), el => druecken(el, () => el.click()), 900] : [() => oben().querySelector('[data-zurueck]'), el => druecken(el, zurueck), 900],
      akk ? [() => k('bettwaesche'), el => druecken(el, () => el.click()), 900] : [() => oben().querySelector('[data-zurueck]'), el => druecken(el, zurueck), 900],
      [() => akk ? $('.akk [data-schliessen]') : oben().querySelector('[data-schliessen]'), el => druecken(el, schliessen), 1000],
    ];
    let t = 500;
    schritte.forEach(([finden, tun, warte]) => {
      nach(t, () => zeigeAuf(finden()));
      nach(t + 620, () => tun(finden()));
      t += 620 + warte;
    });
    nach(t, () => { runde++; setzen(REIHE[runde % REIHE.length]); ablauf(); });
  }
  function stopp() { uhren.forEach(clearTimeout); uhren = []; vorfuehrung = false; html.classList.remove('vorfuehrung'); }
  function vorfuehren() {
    stopp();
    vorfuehrung = true; runde = 0;
    html.classList.add('vorfuehrung');
    html.classList.remove('offen'); schublade.inert = true;
    setzen('stapel');
    zeiger.style.transition = 'none'; zeiger.style.transform = `translate(${innerWidth * .55}px, ${innerHeight * .6}px)`; void zeiger.offsetWidth; zeiger.style.transition = '';
    ablauf();
  }
  let gemerkt = null;
  function pause() { if (!vorfuehrung) return; uhren.forEach(clearTimeout); uhren = []; gemerkt = true; }
  function weiter() { if (vorfuehrung && gemerkt) { gemerkt = null; html.classList.remove('offen'); setzen(variante); ablauf(); } }
  function neustart() { stopp(); html.classList.remove('offen'); schublade.inert = true; menueknopf.setAttribute('aria-expanded', 'false'); $('[data-seite]').inert = false; setzen(variante); setTimeout(oeffnen, 350); }


  setzen('stapel');
  // Die Demo startet mit offener Schublade, damit die erste Ebene sofort zu sehen ist
  // und eine Ebene tief, damit die Stapelkante sofort zu sehen ist
  if (!new URLSearchParams(location.search).has('vorschau')) {
    setTimeout(() => { if (!vorfuehrung) oeffnen(); }, 400);
    setTimeout(() => { if (!vorfuehrung && html.classList.contains('offen') && pfad.length === 1) variante === 'akkordeon' ? $('.akk [data-ziel="bettwaesche"]').click() : vor('bettwaesche'); }, 1000);
  }
})();
</script>
</body>
</html>