Kompakte, expandierende Insel-Navigation
Die Pille mit Logo und „Menü“ wächst selbst zur Karte: erst die Form, dann der Inhalt, der Hintergrund verschwimmt.
Geschlossen ist die Navigation eine kleine, abgerundete Insel mit Logo und „Menü“. Beim Öffnen wächst genau dieses Element in Breite und Höhe zur Karte, statt ein separates Overlay einzublenden. Die Ecken bleiben rund. Der Seitenhintergrund wird weichgezeichnet und leicht abgedunkelt. Links, Teaser und CTA blenden erst ein, wenn die Form fast steht. In der zweistufigen Form wächst zuerst die Breite, dann die Höhe. Beim Schließen verschwindet zuerst der Inhalt, dann zieht sich die Karte zurück.
Objektkonstanz: Man sieht, dass das Menü aus dem Knopf herauswächst. Das ist räumlich verständlich und wirkt hochwertig, ähnlich wie Apples Dynamic Island. Anders als beim Vollbild-Overlay bleibt die Seite als Kontext sichtbar.
Wann einsetzen
Marken-, Studio- und Produktseiten mit 4 bis 8 Links plus Teaser. Wenn der Hero nicht ganz verdeckt werden soll.
Wann nicht
Bei sehr vielen Links oder Unterebenen. Und wenn keine Zeit für sorgfältiges Timing bleibt: Schlecht getimt wirkt der Morph billig.
Varianten
- Ecke: Die Insel sitzt oben rechts und öffnet zur Karte nach links unten (Codrops-Kuration „Compact Navigation“, z. B. VNTNR, Vendredi Society).
- Mitte: Eine zentrierte Leiste wächst symmetrisch in Breite und Höhe (Osmo „Centered Scaling Navigation Bar“).
- Zweistufig: Erst wird die Pille breit, dann klappt die Karte nach unten auf (Osmo „Two-step Scaling Navigation“).
- Dynamic-Island-Stil: Die Insel wechselt ihren Inhalt je nach Scroll-Kontext.
Umsetzung
Ein Container mit festem Anker (top/right) in Zielgröße. Geschlossen schneidet clip-path: inset(0 var(--r) var(--b) var(--l) round 26px) nur die Pille heraus, offen ist inset(0 round 22px). Die Kanten als @property registrieren, dann lassen sich Breite und Höhe getrennt per CSS-Transition timen. clip-path beschneidet auch die Klickfläche, die geschlossene Karte blockiert also nichts. Die Kopfzeile folgt den Kanten per padding, so bleiben Logo und Knopf immer sichtbar und verzerren nicht. Kein scale auf Textcontainer. Inhalt separat mit Verzögerung und Stagger einblenden. Schatten per filter: drop-shadow auf einer Hülle, weil box-shadow mitgeschnitten wird. Hintergrund: eigenes Element mit backdrop-filter: blur(8–16px) und Opacity-Fade. Alternativen: GSAP Flip (messen, Klasse wechseln, Flip.from) oder eine same-document View Transition mit view-transition-name auf dem Container. backdrop-filter auf großen Flächen ist auf schwachen Geräten teuer.
Robust umsetzen
Telefon. Die Karte wächst auf fast volle Breite (16 px Rand) und höchstens 85svh Höhe, innen scrollbar. Auf schwachen Geräten den Blur durch reine Abdunklung ersetzen.
Ohne Bewegung. Kein Morph: Die Karte erscheint an Zielgröße per kurzem Fade. Der Blur bleibt.
Barrierefrei. Knopf mit aria-expanded und aria-controls. Die Karte als Disclosure oder Popover (popover=auto bringt Light-Dismiss und Escape mit). Beim Öffnen Fokus auf den ersten Link, beim Schließen zurück auf den Knopf. Escape und Klick auf den Hintergrund schließen. Die übrige Seite ist inert, solange die Karte offen ist. Den Zustand nicht nur über die Form zeigen: Das Label wechselt zu „Schließen“.
Code
<!DOCTYPE html>
<!--
Kompakte, expandierende Insel-Navigation
Fundus · Navigation · https://cecodesign.de/lab/fundus/navigation/kompakte-expandierende-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>Kompakte, expandierende Insel-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: '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; }
/* Die vier Maße der Insel als registrierte Properties: so lassen sich Breite (--l/--r)
und Höhe (--b) getrennt animieren, und der Kopf der Insel kann ihnen folgen. */
@property --l { syntax: '<length-percentage>'; inherits: true; initial-value: 0px; }
@property --r { syntax: '<length-percentage>'; inherits: true; initial-value: 0px; }
@property --b { syntax: '<length-percentage>'; inherits: true; initial-value: 0px; }
@property --rund { syntax: '<length>'; inherits: true; initial-value: 26px; }
:root {
/* Hafenrösterei: kühles Nebelgrau als Shop-Grund, Fördeblau, Bojen-Orange, Seegrün */
--grund: #eceeed; --kachel: #dfe3e2; --tinte: #15181b; --leise: rgba(21, 24, 27, .66); --linie: rgba(21, 24, 27, .14);
--marine: #1c3553; --nacht: #0f1d2e; --orange: #ee5a2c; --seegruen: #1f6b55; --rosa: #d99a86; --weiss: #f4f5f3;
--insel: var(--nacht); --hell: #f1f3f2; --hell-leise: rgba(241, 243, 242, .54); --hell-linie: rgba(241, 243, 242, .12);
--akzent: var(--orange);
--kurve: cubic-bezier(0.16, 1, 0.3, 1); /* expo.out: Morph */
--inhalt: cubic-bezier(0.33, 1, 0.68, 1); /* power2.out: Inhalt */
--ui: 150ms;
--pille-b: 252px; --pille-h: 52px; /* geschlossene Insel */
--karte-b: min(640px, 100vw - 32px); /* offene Karte */
--rand: 20px;
}
* { 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; }
body { min-height: 100svh; overflow-x: hidden; }
button { font: inherit; color: inherit; background: none; border: 0; padding: 0; cursor: pointer; }
a { color: inherit; text-decoration: none; }
svg { display: block; }
.label { font: 600 11px/1.3 'Instrument Sans', system-ui, sans-serif; letter-spacing: .08em; text-transform: uppercase; }
.mono { font: 500 11px/1.35 'Geist Mono', ui-monospace, monospace; letter-spacing: .14em; text-transform: uppercase; }
.leise { color: var(--leise); }
.ziffern { font-variant-numeric: tabular-nums; }
.svg-vorrat { position: absolute; width: 0; height: 0; overflow: hidden; }
/* ---------- Kaffeetüten (eigene SVG-Illustration, Farben je Röstung) ---------- */
.sack { width: 100%; height: auto; overflow: visible; filter: drop-shadow(0 18px 18px rgba(8, 16, 26, .22)); }
.sack__name { font: 400 17px/1 'Young Serif', Georgia, serif; fill: var(--motiv); text-anchor: middle; }
.sack__info { font: 600 6.6px/1 'Instrument Sans', sans-serif; letter-spacing: .12em; fill: var(--motiv); text-anchor: middle; opacity: .8; }
.bohne { position: absolute; width: var(--g, 18px); rotate: var(--w, 0deg); }
/* ---------- Die Seite dahinter: Shop-Startseite ---------- */
.seite { max-width: 1440px; margin: 0 auto; padding: 0 clamp(16px, 3.4vw, 48px) 120px; }
.oben { height: 92px; display: flex; align-items: center; justify-content: space-between; gap: 24px; }
.oben__links { display: flex; align-items: center; gap: 10px; }
.oben__links i { width: 6px; height: 6px; border-radius: 50%; background: var(--seegruen); }
.oben__rechts { display: flex; align-items: center; gap: 8px; opacity: 0; transition: opacity var(--ui) linear; }
.oben__rechts b { min-width: 20px; height: 20px; display: inline-grid; place-items: center; border-radius: 10px; background: var(--tinte); color: var(--grund); font-size: 11px; letter-spacing: 0; }
[data-variante="mitte"] .oben__rechts { opacity: 1; }
[data-variante="mitte"] .oben__links .lang { display: none; }
.held { display: grid; grid-template-columns: minmax(0, 5fr) minmax(0, 6fr); gap: clamp(24px, 4vw, 64px); align-items: stretch; padding-top: clamp(8px, 3vh, 40px); }
.held__text { display: flex; flex-direction: column; justify-content: center; padding-bottom: 8px; }
.held__titel { margin-top: 18px; font: 400 clamp(40px, 1.4rem + 3.6vw, 78px)/1.03 'Young Serif', Georgia, serif; letter-spacing: -.01em; text-wrap: balance; }
.held__titel span { color: var(--marine); }
.held__lead { margin-top: 20px; font-size: clamp(15px, 1.15vw, 17px); line-height: 1.55; color: var(--leise); max-width: 42ch; text-wrap: pretty; }
.knoepfe { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 26px; }
.btn { height: 48px; display: inline-flex; align-items: center; gap: 10px; padding: 0 22px; border-radius: 999px; font-size: 15px; font-weight: 600; letter-spacing: -.005em; transition: background var(--ui) linear, color var(--ui) linear; }
.btn--voll { background: var(--tinte); color: var(--grund); }
.btn--voll:hover { background: var(--marine); }
.btn--rand { box-shadow: inset 0 0 0 1.5px var(--tinte); }
.btn--rand:hover { background: var(--tinte); color: var(--grund); }
.vertrauen { list-style: none; padding: 18px 0 0; margin-top: 28px; border-top: 1px solid var(--linie); display: grid; grid-template-columns: repeat(3, minmax(0, auto)); justify-content: start; gap: 8px 28px; font-size: 13.5px; line-height: 1.35; }
.vertrauen li { display: grid; gap: 2px; }
.vertrauen b { font-weight: 600; }
.vertrauen span { color: var(--leise); }
.vertrauen .sterne { color: #b74522; letter-spacing: .06em; }
/* Kategorie-Bento: drei große Einstiege statt Slider */
.bento { display: grid; grid-template-columns: minmax(0, 1.25fr) minmax(0, 1fr); grid-template-rows: 1fr 1fr; gap: 12px; height: min(600px, calc(100svh - 120px)); min-height: 440px; }
.kachel { position: relative; overflow: hidden; border-radius: 18px; display: block; isolation: isolate; }
.kachel__kopf { position: absolute; z-index: 2; left: 18px; right: 18px; top: 16px; display: flex; justify-content: space-between; align-items: center; gap: 10px; }
.kachel__fuss { position: absolute; z-index: 2; left: 18px; right: 18px; bottom: 16px; display: flex; justify-content: space-between; align-items: flex-end; gap: 12px; }
.kachel__name { font-size: 17px; font-weight: 600; letter-spacing: -.015em; line-height: 1.2; }
.kachel__name small { display: block; font-size: 13px; font-weight: 500; opacity: .7; letter-spacing: 0; margin-top: 2px; }
.kachel__preis { font-size: 17px; font-weight: 600; white-space: nowrap; }
.marke-pill { height: 24px; display: inline-flex; align-items: center; padding: 0 10px; border-radius: 12px; background: var(--weiss); color: var(--tinte); }
.pfeil { width: 34px; height: 34px; border-radius: 50%; display: grid; place-items: center; font-size: 15px; transition: translate 400ms var(--kurve); }
.kachel:hover .pfeil { translate: 3px 0; }
.kachel--espresso { grid-row: span 2; background: var(--marine); color: var(--hell); }
.kachel--espresso .pfeil { background: rgba(241, 243, 242, .14); }
.kachel--espresso .buehne { position: absolute; inset: 14% 14% 18%; display: grid; place-items: center; }
.kachel--espresso .sack { width: min(66%, 300px); rotate: -4deg; }
.kachel--espresso::before { content: ""; position: absolute; left: 50%; top: 52%; width: 78%; aspect-ratio: 1; translate: -50% -50%; border-radius: 50%; background: radial-gradient(closest-side, rgba(255, 255, 255, .12), transparent); z-index: -1; }
.wellen { position: absolute; left: 0; right: 0; bottom: 0; height: 34%; z-index: -1; opacity: .22; }
.kachel--abo { background: var(--orange); color: var(--nacht); }
.kachel--abo .abo__text { position: absolute; z-index: 2; left: 18px; top: 44px; display: grid; gap: 10px; }
.kachel--abo .abo__wort { font: 400 clamp(46px, 5.4vw, 76px)/.9 'Young Serif', Georgia, serif; letter-spacing: -.01em; }
.kachel--abo .abo__unter { font-size: 14px; font-weight: 600; line-height: 1.35; letter-spacing: -.005em; }
.kachel--abo .abo__tueten { position: absolute; right: 4%; bottom: -16%; width: 50%; height: 92%; }
.kachel--abo .abo__tueten .sack { position: absolute; width: 58%; }
.kachel--abo .abo__tueten .sack:first-child { right: 34%; bottom: 0; rotate: -9deg; }
.kachel--abo .abo__tueten .sack:last-child { right: 0; bottom: 6%; rotate: 6deg; }
.kachel--abo .pfeil { background: rgba(15, 29, 46, .12); }
.rhythmus { display: flex; gap: 4px; margin-top: 6px; }
.rhythmus i { width: 8px; height: 8px; border-radius: 50%; border: 1.5px solid currentColor; opacity: .55; }
.rhythmus i:nth-child(odd) { background: currentColor; opacity: 1; }
.kachel--filter { background: var(--kachel); color: var(--tinte); }
.kachel--filter .sack { position: absolute; width: 40%; right: 10%; top: 13%; rotate: 5deg; }
.kachel--filter .pfeil { background: rgba(21, 24, 27, .08); }
/* Bestseller: einheitliche Kacheln, Preis mit Tabellenziffern und Grundpreis */
.sortiment { margin-top: clamp(64px, 11vh, 128px); }
.sortiment__kopf { display: flex; justify-content: space-between; align-items: baseline; gap: 16px; padding-bottom: 16px; margin-bottom: 20px; border-bottom: 1px solid var(--linie); }
.sortiment__kopf h2 { font-size: clamp(24px, 1.1rem + 1.4vw, 36px); font-weight: 600; letter-spacing: -.015em; }
.produkte { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: clamp(12px, 1.6vw, 20px); }
.produkt { display: grid; gap: 12px; align-content: start; }
.produkt__bild { position: relative; aspect-ratio: 4 / 5; border-radius: 14px; background: var(--kachel); display: grid; place-items: center; overflow: hidden; }
.produkt__bild .sack { width: 54%; transition: translate 400ms var(--kurve); }
.produkt:hover .produkt__bild .sack { translate: 0 -6px; }
.produkt__bild .marke-pill { position: absolute; left: 12px; top: 12px; }
.produkt__zeile { display: flex; justify-content: space-between; gap: 12px; font-size: 16px; font-weight: 600; letter-spacing: -.01em; }
.produkt__noten { font-size: 14px; color: var(--leise); line-height: 1.4; min-height: 2.8em; }
.produkt__unten { display: flex; justify-content: space-between; align-items: center; gap: 8px; font-size: 13px; color: var(--leise); }
.mahlgrad { display: flex; gap: 4px; }
.mahlgrad span { height: 24px; display: inline-flex; align-items: center; padding: 0 9px; border-radius: 12px; border: 1px solid var(--linie); font-size: 12px; color: var(--tinte); }
.mahlgrad span:first-child { border-color: var(--tinte); }
/* ---------- Schleier: Blur und Abdunklung hinter der offenen Karte ---------- */
.schleier {
position: fixed; inset: 0; z-index: 20; pointer-events: none; opacity: 0;
background: rgba(15, 29, 46, .3); /* Abdunklung 30 % */
-webkit-backdrop-filter: blur(12px); backdrop-filter: blur(12px);
transition: opacity 350ms var(--kurve) 100ms;
}
.schleier.an { opacity: 1; pointer-events: auto; transition: opacity 600ms var(--kurve); }
/* ---------- Die Insel ---------- */
.huelle { pointer-events: none; position: fixed; z-index: 30; top: var(--rand); right: var(--rand); width: var(--karte-b); filter: drop-shadow(0 14px 32px rgba(15, 29, 46, .24)); }
[data-variante="mitte"] .huelle { right: auto; left: 50%; translate: -50% 0; }
.insel {
position: relative; color: var(--hell); pointer-events: auto;
max-height: calc(100svh - 2 * var(--rand)); display: grid; grid-template-rows: auto minmax(0, 1fr);
/* geschlossen: nur die Pille oben rechts ist sichtbar */
--l: calc(100% - var(--pille-b)); --r: 0px; --b: calc(100% - var(--pille-h)); --rund: 26px;
clip-path: inset(0 var(--r) var(--b) var(--l) round var(--rund));
/* Schließen: Inhalt geht zuerst (150 ms), dann 350 ms Morph */
--d-breite: 350ms; --v-breite: 100ms; --d-hoehe: 350ms; --v-hoehe: 100ms;
transition: --l var(--d-breite) var(--kurve) var(--v-breite), --r var(--d-breite) var(--kurve) var(--v-breite),
--b var(--d-hoehe) var(--kurve) var(--v-hoehe), --rund var(--d-hoehe) var(--kurve) var(--v-hoehe);
}
.insel::before { content: ""; position: absolute; inset: 0; background: var(--insel); z-index: -1; }
[data-variante="mitte"] .insel { --l: calc(50% - var(--pille-b) / 2 - 32px); --r: calc(50% - var(--pille-b) / 2 - 32px); }
/* Öffnen: 600 ms expo.out, Breite und Höhe gemeinsam */
.insel.offen { --l: 0px; --r: 0px; --b: 0px; --rund: 22px; --d-breite: 600ms; --v-breite: 0ms; --d-hoehe: 600ms; --v-hoehe: 0ms; }
/* Zweistufig: erst Breite (400 ms), dann Höhe (600 ms ab 250 ms); zu in umgekehrter Folge */
[data-variante="zweistufig"] .insel { --d-hoehe: 300ms; --v-hoehe: 100ms; --d-breite: 300ms; --v-breite: 250ms; }
[data-variante="zweistufig"] .insel.offen { --d-breite: 400ms; --v-breite: 0ms; --d-hoehe: 600ms; --v-hoehe: 250ms; }
/* Kopf: folgt den Kanten der Insel, Logo und Knopf bleiben immer in der sichtbaren Fläche */
.insel__kopf { height: var(--pille-h); display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 0 calc(var(--r) + 6px) 0 calc(var(--l) + 16px); position: relative; }
.insel__kopf::after { content: ""; position: absolute; left: 20px; right: 20px; bottom: 0; height: 1px; background: var(--hell-linie); opacity: 0; transition: opacity var(--ui) linear; }
.offen .insel__kopf::after { opacity: 1; transition: opacity 300ms var(--inhalt) 240ms; }
.marke { display: flex; align-items: center; gap: 8px; white-space: nowrap; }
.marke svg { width: 24px; height: 24px; flex: none; }
.marke__name { font: 400 17px/1 'Young Serif', Georgia, serif; letter-spacing: -.005em; }
.marke__ort { font-size: 11px; font-weight: 600; letter-spacing: .08em; text-transform: uppercase; color: var(--hell-leise); margin-left: -1px; }
.knopf { height: 40px; display: flex; align-items: center; justify-content: flex-end; gap: 10px; padding: 0 14px 0 16px; border-radius: 999px; background: rgba(241, 243, 242, .1); outline: none; overflow: hidden;
width: var(--kb-zu, auto); transition: background var(--ui) linear, width 350ms var(--kurve); }
.offen .knopf { width: var(--kb-offen, auto); transition: background var(--ui) linear, width 600ms var(--kurve); }
.knopf:hover, .knopf:focus-visible { background: rgba(241, 243, 242, .18); }
.knopf:focus-visible { box-shadow: 0 0 0 2px var(--akzent); }
.knopf__text { display: grid; color: var(--hell); }
.knopf__text span { grid-area: 1 / 1; justify-self: end; white-space: nowrap; transition: opacity var(--ui) linear; }
.knopf__text span + span, .offen .knopf__text span:first-child { opacity: 0; }
.offen .knopf__text span + span { opacity: 1; }
.knopf__icon { width: 14px; height: 14px; position: relative; }
.knopf__icon::before, .knopf__icon::after { content: ""; position: absolute; left: 0; right: 0; top: 50%; height: 1.5px; margin-top: -.75px; background: currentColor; border-radius: 1px; transition: translate 400ms var(--kurve), rotate 400ms var(--kurve); }
.knopf__icon::before { translate: 0 -3px; }
.knopf__icon::after { translate: 0 3px; }
.offen .knopf__icon::before { translate: 0 0; rotate: 45deg; }
.offen .knopf__icon::after { translate: 0 0; rotate: -45deg; }
/* Inhalt der Karte: kommt nach dem Morph, Stagger 40 ms, 12 px von unten */
.insel__inhalt { overflow: auto; overscroll-behavior: contain; display: grid; grid-template-columns: minmax(0, 1fr) 188px; gap: 22px 32px; padding: 22px 20px 20px 20px; visibility: hidden; transition: visibility 0s linear 450ms; }
.offen .insel__inhalt { visibility: visible; transition: none; }
.stufe { opacity: 0; translate: 0 12px; transition: opacity var(--ui) linear, translate 0s linear var(--ui); }
.offen .stufe { opacity: 1; translate: 0 0; transition: opacity 300ms var(--inhalt) calc(var(--start, 240ms) + var(--i) * 40ms), translate 300ms var(--inhalt) calc(var(--start, 240ms) + var(--i) * 40ms); }
[data-variante="zweistufig"] .insel { --start: 450ms; }
.menue { list-style: none; padding: 0; display: grid; align-content: start; }
.menue a { display: grid; grid-template-columns: 2.4em auto minmax(0, 1fr); align-items: baseline; column-gap: 10px; padding: 3px 0; outline: none; }
.menue .label { color: var(--hell-leise); font-variant-numeric: tabular-nums; }
.menue__name { font-size: clamp(30px, 4vw, 40px); font-weight: 600; letter-spacing: -.035em; line-height: 1.1; transition: opacity var(--ui) linear, translate 400ms var(--kurve); }
.menue__zahl { align-self: start; margin-top: .7em; color: var(--hell-leise); transition: translate 400ms var(--kurve); }
.menue__zahl--neu { color: var(--nacht); background: var(--orange); border-radius: 9px; padding: 3px 7px 2px; margin-top: .55em; justify-self: start; }
.menue a[aria-current="page"] .menue__name::after { content: ""; display: inline-block; width: 7px; height: 7px; margin-left: 10px; border-radius: 50%; background: var(--akzent); vertical-align: .35em; }
@media (hover: hover) {
.menue:hover .menue__name, .menue.zeigt .menue__name { opacity: .38; }
.menue a:hover .menue__name, .menue a.zeigt .menue__name, .menue a:focus-visible .menue__name { opacity: 1; translate: 8px 0; }
.menue a:hover .menue__zahl, .menue a.zeigt .menue__zahl { translate: 8px 0; }
}
.menue a:focus-visible .menue__name { text-decoration: underline; text-decoration-thickness: 1.5px; text-underline-offset: .14em; }
.teaser { display: grid; align-content: start; gap: 10px; }
.teaser__bild { position: relative; aspect-ratio: 4 / 5; border-radius: 12px; background: var(--rosa); display: grid; place-items: center; overflow: hidden; }
.teaser__bild .sack { width: 62%; rotate: 4deg; filter: drop-shadow(0 12px 14px rgba(15, 29, 46, .28)); }
.teaser .label { color: var(--hell-leise); }
.teaser__titel { font: 400 17px/1.22 'Young Serif', Georgia, serif; text-wrap: balance; }
.teaser__noten { font-size: 13px; line-height: 1.4; color: var(--hell-leise); }
.insel__fuss { grid-column: 1 / -1; display: flex; justify-content: space-between; align-items: center; gap: 12px 20px; flex-wrap: wrap; padding-top: 16px; border-top: 1px solid var(--hell-linie); }
.insel__fuss p { display: flex; align-items: center; gap: 8px; font-size: 13.5px; color: var(--hell-leise); }
.insel__fuss p i { width: 7px; height: 7px; border-radius: 50%; background: #4fbf8f; flex: none; }
.insel__fuss p b { color: var(--hell); font-weight: 600; }
.cta { height: 44px; display: inline-flex; align-items: center; gap: 10px; padding: 0 20px; border-radius: 999px; background: var(--hell); color: var(--nacht); font-size: 15px; font-weight: 600; letter-spacing: -.005em; transition: background var(--ui) linear; }
.cta:hover, .cta:focus-visible { background: #fff; outline: none; }
.cta:focus-visible { box-shadow: 0 0 0 2px var(--akzent); }
/* ---------- Demo-Leiste: Variante und Werte ---------- */
.leiste { position: fixed; left: 0; right: 0; bottom: 0; z-index: 10; display: flex; justify-content: space-between; align-items: center; gap: 8px 24px; flex-wrap: wrap; padding: 12px clamp(16px, 3.4vw, 48px); background: rgba(236, 238, 237, .9); -webkit-backdrop-filter: blur(10px); backdrop-filter: blur(10px); border-top: 1px solid var(--linie); color: var(--leise); }
.leiste b { color: var(--tinte); font-weight: 500; }
.modi { display: flex; gap: 4px; align-items: center; }
.modi button { font: inherit; letter-spacing: inherit; text-transform: inherit; color: var(--leise); border: 1px solid var(--linie); border-radius: 999px; height: 28px; padding: 0 12px; }
.modi button[aria-pressed="true"] { background: var(--tinte); color: var(--grund); border-color: var(--tinte); }
.modi button:focus-visible { outline: 2px solid var(--akzent); outline-offset: 2px; }
/* Vorführung: gezeichneter Zeiger */
.zeiger { position: fixed; left: 0; top: 0; z-index: 40; width: 18px; height: 18px; pointer-events: none; display: none; transition: transform 900ms var(--kurve), scale var(--ui) var(--kurve); }
.vorfuehrung .zeiger { display: block; }
.zeiger.drueckt { scale: .8; }
/* Galerie-Vorschau: nur die Bühne */
.vorschau body { overflow: hidden; }
.vorschau .leiste, .vorschau .sortiment { display: none; }
/* ---------- Tablet und Telefon ---------- */
@media (max-width: 1100px) {
.leiste .werte__lang { display: none; }
}
@media (max-width: 1020px) {
.vertrauen { grid-template-columns: 1fr 1fr; }
.vertrauen li:last-child { display: none; }
}
@media (max-width: 760px) {
:root { --rand: 16px; --pille-b: 236px; }
.oben { height: 84px; }
.oben__links, .oben__rechts { display: none; }
.held { grid-template-columns: 1fr; padding-top: 0; }
.held__titel { margin-top: 14px; }
.vertrauen { grid-template-columns: 1fr 1fr; gap: 8px 16px; font-size: 13px; }
.bento { height: auto; min-height: 0; grid-template-columns: 1fr 1fr; grid-template-rows: 340px 210px; }
.kachel--espresso { grid-row: auto; grid-column: 1 / -1; }
.kachel--espresso .sack { width: min(46%, 200px); }
.kachel--abo .abo__text { left: 14px; top: 40px; gap: 6px; }
.kachel--abo .abo__wort { font-size: 40px; }
.kachel--abo .abo__unter { font-size: 12.5px; }
.kachel--abo .abo__tueten { width: 62%; right: -4%; bottom: -34%; }
.kachel__kopf, .kachel__fuss { left: 14px; right: 14px; }
.kachel--filter .kachel__fuss .pfeil { display: none; }
.kachel--filter .sack { width: 50%; right: 8%; top: 18%; }
.kachel__name, .kachel__preis { font-size: 15px; }
.produkte { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }
.produkt__zeile, .produkt__unten { flex-wrap: wrap; }
.produkt:nth-child(n+3) { display: none; }
.insel { max-height: min(85svh, 100svh - 2 * var(--rand)); }
.insel__inhalt { grid-template-columns: 1fr; gap: 20px; }
.teaser { grid-template-columns: 76px minmax(0, 1fr); align-items: center; gap: 4px 14px; }
.teaser__bild { grid-row: span 3; aspect-ratio: 1; border-radius: 10px; }
.teaser__bild .sack { width: 54%; }
.teaser__titel { font-size: 15px; }
.fuss__lang { display: none; }
.leiste { padding-block: 10px; }
.leiste .werte__lang { display: none; }
}
/* Touch ohne Hover: keine Hover-Dämpfung, und auf kleinen Geräten nur Abdunklung statt Blur */
@media (hover: none) {
.menue a { padding: 6px 0; }
}
@media (hover: none) and (max-width: 760px) {
.schleier { -webkit-backdrop-filter: none; backdrop-filter: none; background: rgba(15, 29, 46, .4); }
}
/* ---------- Reduzierte Bewegung: kein Morph, Karte erscheint per Fade an Zielgröße ---------- */
.reduziert .insel, .reduziert .insel.offen { transition: none; }
.reduziert .stufe, .reduziert .offen .stufe { translate: none; transition: opacity var(--ui) linear; }
.reduziert .insel__kopf::after, .reduziert .knopf, .reduziert .knopf__icon::before, .reduziert .knopf__icon::after, .reduziert .menue__name, .reduziert .pfeil, .reduziert .produkt__bild .sack { transition: none; }
.reduziert .menue a .menue__name { translate: none !important; }
.reduziert .schleier, .reduziert .schleier.an { transition: opacity var(--ui) linear; }
.reduziert .zeiger { transition: none; }
@media (forced-colors: active) {
button, [role=button], [role=tab], [role=switch] { border: 1px solid ButtonText !important; }
.knopf:focus-visible, .cta:focus-visible { outline: 2px solid Highlight; outline-offset: 2px; }
}
</style>
</head>
<body data-variante="ecke">
<!-- Eigene Illustrationen: Tüte, Bohne, Wellenmuster, Bildmarke -->
<svg class="svg-vorrat" aria-hidden="true" focusable="false">
<defs>
<linearGradient id="volumen" x1="0" x2="1" y1="0" y2="0">
<stop offset="0" stop-color="#000" stop-opacity=".22"/>
<stop offset=".16" stop-color="#fff" stop-opacity=".1"/>
<stop offset=".45" stop-color="#fff" stop-opacity="0"/>
<stop offset=".82" stop-color="#000" stop-opacity=".05"/>
<stop offset="1" stop-color="#000" stop-opacity=".24"/>
</linearGradient>
<pattern id="falz" width="3" height="20" patternUnits="userSpaceOnUse"><rect width="1.4" height="20" fill="#000" fill-opacity=".14"/></pattern>
<symbol id="sack" viewBox="0 0 200 280">
<path d="M36 14H164L168 44 176 250Q176 266 160 266H40Q24 266 24 250L32 44Z" style="fill: var(--sack)"/>
<path d="M36 14H164L168 44 176 250Q176 266 160 266H40Q24 266 24 250L32 44Z" fill="url(#volumen)"/>
<rect x="37" y="18" width="126" height="18" fill="url(#falz)"/>
<path d="M33 40H167" stroke="#000" stroke-opacity=".12" stroke-width="1.2"/>
<path d="M28 236Q100 248 172 236" fill="none" stroke="#000" stroke-opacity=".12" stroke-width="1.2"/>
<circle cx="100" cy="64" r="7.5" fill="none" style="stroke: var(--motiv)" stroke-opacity=".35" stroke-width="1.5"/>
<circle cx="100" cy="64" r="2.4" style="fill: var(--motiv)" fill-opacity=".35"/>
<rect x="50" y="90" width="100" height="134" rx="3" style="fill: var(--etikett)"/>
<text x="100" y="106" text-anchor="middle" style="font: 600 6.4px 'Instrument Sans', sans-serif; letter-spacing: .2em; fill: var(--motiv)">HAFENRÖSTEREI</text>
<path d="M60 113H140" style="stroke: var(--motiv)" stroke-opacity=".35" stroke-width=".8"/>
<g fill="none" style="stroke: var(--motiv)" stroke-width="1.8" stroke-linecap="round">
<path d="M62 182q6.5-5 13 0t13 0 13 0 13 0 13 0 13 0"/>
<path d="M62 191q6.5-5 13 0t13 0 13 0 13 0 13 0 13 0" stroke-opacity=".6"/>
<path d="M62 200q6.5-5 13 0t13 0 13 0 13 0 13 0 13 0" stroke-opacity=".3"/>
</g>
<text x="100" y="216" text-anchor="middle" style="font: 600 5.6px 'Instrument Sans', sans-serif; letter-spacing: .18em; fill: var(--motiv); fill-opacity: .7">250 G · GERÖSTET IN KIEL</text>
</symbol>
<symbol id="bohne" viewBox="0 0 20 28">
<ellipse cx="10" cy="14" rx="9" ry="13" style="fill: var(--bohne, #3b2418)"/>
<path d="M10.5 2.5C6 8 15 18 9.5 25.5" fill="none" stroke="#000" stroke-opacity=".4" stroke-width="1.8" stroke-linecap="round"/>
</symbol>
<symbol id="bildmarke" viewBox="0 0 24 24">
<circle cx="12" cy="12" r="12" fill="#ee5a2c"/>
<path d="M5 11.5q1.75-1.6 3.5 0t3.5 0 3.5 0 3.5 0M5 15.5q1.75-1.6 3.5 0t3.5 0 3.5 0 3.5 0" fill="none" stroke="#0f1d2e" stroke-width="1.6" stroke-linecap="round"/>
<path d="M12 4.5v4.6" stroke="#0f1d2e" stroke-width="1.6" stroke-linecap="round"/>
</symbol>
</defs>
</svg>
<div class="seite" id="seite">
<header class="oben">
<p class="label oben__links leise"><i aria-hidden="true"></i><span>Gratisversand ab 35 €<span class="lang"> · Geröstet jeden Dienstag</span></span></p>
<p class="label oben__rechts leise" aria-hidden="true">Warenkorb <b class="ziffern">2</b></p>
</header>
<main class="held">
<div class="held__text">
<p class="label leise">Spezialitätenrösterei · Kiel · seit 2016</p>
<h1 class="held__titel">Frisch geröstet an der <span>Kieler Förde</span>.</h1>
<p class="held__lead">Wir rösten dienstags in kleinen Chargen und verschicken noch am selben Tag. Ganze Bohne oder gemahlen, einzeln oder im Abo, jederzeit pausierbar.</p>
<div class="knoepfe">
<a class="btn btn--voll" href="#kaffee">Kaffee entdecken <span aria-hidden="true">→</span></a>
<a class="btn btn--rand" href="#abo">Abo ab 11,90 €</a>
</div>
<ul class="vertrauen">
<li><b><span class="sterne" aria-hidden="true">★★★★★</span> 4,9 von 5</b><span class="ziffern">1.284 Bewertungen</span></li>
<li><b>Versand am Rösttag</b><span>bis 14 Uhr bestellt</span></li>
<li><b>Röstdatum auf jeder Tüte</b><span>nie älter als 7 Tage</span></li>
</ul>
</div>
<div class="bento">
<a class="kachel kachel--espresso" href="#espresso">
<div class="kachel__kopf"><span class="label marke-pill">Bestseller</span><span class="label" style="opacity:.7">Espresso</span></div>
<svg class="wellen" viewBox="0 0 400 120" preserveAspectRatio="none" aria-hidden="true"><g fill="none" stroke="#f1f3f2" stroke-width="1.4"><path d="M0 20q25-14 50 0t50 0 50 0 50 0 50 0 50 0 50 0 50 0"/><path d="M0 44q25-14 50 0t50 0 50 0 50 0 50 0 50 0 50 0 50 0"/><path d="M0 68q25-14 50 0t50 0 50 0 50 0 50 0 50 0 50 0 50 0"/><path d="M0 92q25-14 50 0t50 0 50 0 50 0 50 0 50 0 50 0 50 0"/><path d="M0 116q25-14 50 0t50 0 50 0 50 0 50 0 50 0 50 0 50 0"/></g></svg>
<div class="buehne">
<svg class="sack" viewBox="0 0 200 280" role="img" aria-label="Tüte Förde Espresso" style="--sack:#f4f5f3; --etikett:#ee5a2c; --motiv:#0f1d2e"><use href="#sack"/><text class="sack__name" x="100" y="144">Förde</text><text class="sack__info" x="100" y="159">ESPRESSO · BRASILIEN</text></svg>
</div>
<svg class="bohne" style="left:14%; top:30%; --g:16px; --w:-30deg" viewBox="0 0 20 28" aria-hidden="true"><use href="#bohne"/></svg>
<svg class="bohne" style="right:15%; top:24%; --g:13px; --w:40deg" viewBox="0 0 20 28" aria-hidden="true"><use href="#bohne"/></svg>
<svg class="bohne" style="right:20%; bottom:26%; --g:19px; --w:110deg" viewBox="0 0 20 28" aria-hidden="true"><use href="#bohne"/></svg>
<div class="kachel__fuss">
<p class="kachel__name">Förde Espresso<small>Kakao, Haselnuss, kräftig</small></p>
<p class="kachel__preis ziffern">12,90 €</p>
</div>
</a>
<a class="kachel kachel--abo" href="#abo">
<div class="kachel__kopf"><span class="label">Kaffee-Abo</span><span class="label">−10 %</span></div>
<div class="abo__text"><p class="abo__wort">Abo</p><p class="abo__unter">Alle 2 oder 4 Wochen,<br>ab 11,90 € je Tüte</p></div>
<div class="abo__tueten" aria-hidden="true">
<svg class="sack" viewBox="0 0 200 280" style="--sack:#1c3553; --etikett:#f4f5f3; --motiv:#1c3553"><use href="#sack"/><text class="sack__name" x="100" y="144">Schleuse</text><text class="sack__info" x="100" y="159">ENTKOFFEINIERT</text></svg>
<svg class="sack" viewBox="0 0 200 280" style="--sack:#f4f5f3; --etikett:#1f6b55; --motiv:#f4f5f3"><use href="#sack"/><text class="sack__name" x="100" y="144">Holtenau</text><text class="sack__info" x="100" y="159">FILTER · KOLUMBIEN</text></svg>
</div>
<div class="kachel__fuss">
<div class="rhythmus" aria-hidden="true"><i></i><i></i><i></i><i></i><i></i></div>
</div>
</a>
<a class="kachel kachel--filter" href="#filter">
<div class="kachel__kopf"><span class="label">Filterkaffee</span></div>
<svg class="sack" viewBox="0 0 200 280" role="img" aria-label="Tüte Holtenau Filter" style="--sack:#1f6b55; --etikett:#f4f5f3; --motiv:#0f1d2e"><use href="#sack"/><text class="sack__name" x="100" y="144">Holtenau</text><text class="sack__info" x="100" y="159">FILTER · KOLUMBIEN</text></svg>
<div class="kachel__fuss">
<p class="kachel__name">6 Sorten<small>ab 10,50 €</small></p>
<span class="pfeil" aria-hidden="true">→</span>
</div>
</a>
</div>
</main>
<section class="sortiment" aria-label="Bestseller">
<div class="sortiment__kopf"><h2>Unsere Bestseller</h2><a class="label leise" href="#alle">Alle 14 Kaffees →</a></div>
<div class="produkte">
<a class="produkt" href="#foerde">
<div class="produkt__bild"><span class="label marke-pill">Bestseller</span><svg class="sack" viewBox="0 0 200 280" aria-hidden="true" style="--sack:#f4f5f3; --etikett:#ee5a2c; --motiv:#0f1d2e"><use href="#sack"/><text class="sack__name" x="100" y="144">Förde</text><text class="sack__info" x="100" y="159">ESPRESSO · BRASILIEN</text></svg></div>
<p class="produkt__zeile"><span>Förde Espresso</span><span class="ziffern">12,90 €</span></p>
<p class="produkt__noten">Kakao, Haselnuss, kräftig. Für Siebträger und Mokka.</p>
<div class="produkt__unten"><span class="mahlgrad"><span>Bohne</span><span>Gemahlen</span></span><span class="ziffern">51,60 €/kg</span></div>
</a>
<a class="produkt" href="#holtenau">
<div class="produkt__bild"><svg class="sack" viewBox="0 0 200 280" aria-hidden="true" style="--sack:#1f6b55; --etikett:#f4f5f3; --motiv:#0f1d2e"><use href="#sack"/><text class="sack__name" x="100" y="144">Holtenau</text><text class="sack__info" x="100" y="159">FILTER · KOLUMBIEN</text></svg></div>
<p class="produkt__zeile"><span>Holtenau Filter</span><span class="ziffern">13,50 €</span></p>
<p class="produkt__noten">Rote Äpfel, Karamell, mild. Für Handfilter und Kanne.</p>
<div class="produkt__unten"><span class="mahlgrad"><span>Bohne</span><span>Gemahlen</span></span><span class="ziffern">54,00 €/kg</span></div>
</a>
<a class="produkt" href="#schleuse">
<div class="produkt__bild"><svg class="sack" viewBox="0 0 200 280" aria-hidden="true" style="--sack:#1c3553; --etikett:#f4f5f3; --motiv:#1c3553"><use href="#sack"/><text class="sack__name" x="100" y="144">Schleuse</text><text class="sack__info" x="100" y="159">ENTKOFFEINIERT</text></svg></div>
<p class="produkt__zeile"><span>Schleuse Decaf</span><span class="ziffern">11,90 €</span></p>
<p class="produkt__noten">Milchschokolade, Mandel. Schonend mit Wasser entkoffeiniert.</p>
<div class="produkt__unten"><span class="mahlgrad"><span>Bohne</span><span>Gemahlen</span></span><span class="ziffern">47,60 €/kg</span></div>
</a>
<a class="produkt" href="#guji">
<div class="produkt__bild"><span class="label marke-pill">Saison</span><svg class="sack" viewBox="0 0 200 280" aria-hidden="true" style="--sack:#d99a86; --etikett:#f4f5f3; --motiv:#0f1d2e"><use href="#sack"/><text class="sack__name" x="100" y="144">Guji</text><text class="sack__info" x="100" y="159">FILTER · ÄTHIOPIEN</text></svg></div>
<p class="produkt__zeile"><span>Guji Hambela</span><span class="ziffern">16,90 €</span></p>
<p class="produkt__noten">Bergamotte, Pfirsich, Schwarztee. Nur bis Januar.</p>
<div class="produkt__unten"><span class="mahlgrad"><span>Bohne</span><span>Gemahlen</span></span><span class="ziffern">67,60 €/kg</span></div>
</a>
</div>
</section>
</div>
<div class="schleier" data-schleier aria-hidden="true"></div>
<div class="huelle">
<nav class="insel" data-insel aria-label="Hauptmenü">
<div class="insel__kopf">
<a class="marke" href="#start"><svg viewBox="0 0 24 24" aria-hidden="true"><use href="#bildmarke"/></svg><span class="marke__name">Hafenrösterei</span><span class="marke__ort">Kiel</span></a>
<button class="knopf" type="button" data-knopf aria-expanded="false" aria-controls="insel-inhalt">
<span class="knopf__text label"><span>Menü</span><span aria-hidden="true">Schließen</span></span>
<span class="knopf__icon" aria-hidden="true"></span>
</button>
</div>
<div class="insel__inhalt" id="insel-inhalt">
<ul class="menue" data-menue>
<li class="stufe" style="--i:0"><a href="#kaffee" aria-current="page"><span class="label">01</span><span class="menue__name">Kaffee</span><span class="label menue__zahl">14</span></a></li>
<li class="stufe" style="--i:1"><a href="#abo"><span class="label">02</span><span class="menue__name">Abo</span><span class="label menue__zahl menue__zahl--neu">−10 %</span></a></li>
<li class="stufe" style="--i:2"><a href="#zubehoer"><span class="label">03</span><span class="menue__name">Zubehör</span></a></li>
<li class="stufe" style="--i:3"><a href="#roesterei"><span class="label">04</span><span class="menue__name">Rösterei</span></a></li>
<li class="stufe" style="--i:4"><a href="#kurse"><span class="label">05</span><span class="menue__name">Kurse</span><span class="label menue__zahl">3 Termine</span></a></li>
</ul>
<a class="teaser stufe" style="--i:5" href="#guji">
<span class="teaser__bild"><svg class="sack" viewBox="0 0 200 280" aria-hidden="true" style="--sack:#f4f5f3; --etikett:#0f1d2e; --motiv:#f4f5f3"><use href="#sack"/><text class="sack__name" x="100" y="144">Guji</text><text class="sack__info" x="100" y="159">FILTER · ÄTHIOPIEN</text></svg></span>
<span class="label">Neu · Saisonkaffee</span>
<span class="teaser__titel">Guji Hambela aus Äthiopien</span>
<span class="teaser__noten">Bergamotte, Pfirsich, Schwarztee · 16,90 €</span>
</a>
<div class="insel__fuss stufe" style="--i:6">
<p><i aria-hidden="true"></i><span><b>Nächste Röstung Di., 03.11.</b><span class="fuss__lang"> · Versand am selben Tag</span></span></p>
<a class="cta" href="#abo">Abo starten <span aria-hidden="true">→</span></a>
</div>
</div>
</nav>
</div>
<div class="leiste mono">
<div class="modi" role="group" aria-label="Variante">
<button type="button" data-modus="ecke" aria-pressed="true">Ecke</button>
<button type="button" data-modus="mitte" aria-pressed="false">Mitte</button>
<button type="button" data-modus="zweistufig" aria-pressed="false">Zweistufig</button>
</div>
<p 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="#0f1d2e" stroke="#f1f3f2" stroke-width="1.2" stroke-linejoin="round"/></svg>
<script>
(() => {
const html = document.documentElement;
const reduziert = () => html.classList.contains('reduziert');
const insel = document.querySelector('[data-insel]');
const knopf = document.querySelector('[data-knopf]');
const menue = document.querySelector('[data-menue]');
const schleier = document.querySelector('[data-schleier]');
const seite = document.getElementById('seite');
const leiste = document.querySelector('.leiste');
const werte = document.querySelector('[data-werte]');
const zeiger = document.querySelector('[data-zeiger]');
const modi = [...document.querySelectorAll('[data-modus]')];
const marke = document.querySelector('.marke');
// Pille so breit wie Logo + Knopf; der Knopf wechselt seine Breite mit dem Text
function messen() {
const [zu, auf] = knopf.querySelectorAll('.knopf__text span');
const rest = 16 + 14 + 10 + 14; // Innenabstand, Lücke, Icon
const kbZu = Math.ceil(zu.getBoundingClientRect().width + rest), kbAuf = Math.ceil(auf.getBoundingClientRect().width + rest);
insel.style.setProperty('--kb-zu', kbZu + 'px');
insel.style.setProperty('--kb-offen', kbAuf + 'px');
insel.style.setProperty('--pille-b', Math.ceil(16 + marke.getBoundingClientRect().width + 16 + kbZu + 6) + 'px');
}
messen();
document.fonts.ready.then(messen);
const WERTE = {
ecke: ['Morph 600 ms expo.out', ' · Inhalt 300 ms power2.out ab 240 ms · Stagger 40 ms · 12 px · zu 150 + 350 ms · Blur 12 px · 30 %'],
mitte: ['Morph 600 ms expo.out, zentriert', ' · Inhalt 300 ms power2.out ab 240 ms · Stagger 40 ms · 12 px · zu 150 + 350 ms · Blur 12 px · 30 %'],
zweistufig: ['Breite 400 ms, Höhe 600 ms ab 250 ms', ' · expo.out · Inhalt 300 ms ab 450 ms · Stagger 40 ms · 12 px · zu Höhe 300, Breite 300 ab 250 ms'],
};
function zeigeWerte() {
const m = document.body.dataset.variante;
if (reduziert()) { werte.innerHTML = '<b>Reduziert: kein Morph</b> · Karte per Fade 150 ms · Blur bleibt'; return; }
werte.innerHTML = `<b>${WERTE[m][0]}</b><span class="werte__lang">${WERTE[m][1]}</span>`;
}
let offen = false;
function setzen(an, fokus = true) {
if (an === offen) return;
offen = an;
const umschalten = () => {
insel.classList.toggle('offen', an);
schleier.classList.toggle('an', an);
knopf.setAttribute('aria-expanded', an);
seite.inert = an; leiste.inert = an;
};
if (reduziert()) {
// Kein Morph: Zustand sofort wechseln, Karte bzw. Pille an Zielgröße einblenden
umschalten();
insel.animate([{ opacity: 0 }, { opacity: 1 }], { duration: 150, easing: 'linear' });
} else umschalten();
if (fokus) {
if (an) setTimeout(() => { if (offen) menue.querySelector('a').focus({ preventScroll: true }); }, reduziert() ? 160 : 300);
else knopf.focus({ preventScroll: true });
}
}
knopf.addEventListener('click', () => setzen(!offen));
schleier.addEventListener('click', () => setzen(false));
addEventListener('keydown', e => { if (e.key === 'Escape' && offen) setzen(false); });
insel.addEventListener('click', e => { const a = e.target.closest('a'); if (a) { e.preventDefault(); if (a.closest('.insel__inhalt')) setzen(false); } });
document.querySelectorAll('.seite a').forEach(a => a.addEventListener('click', e => e.preventDefault()));
function variante(m) {
document.body.dataset.variante = m;
modi.forEach(b => b.setAttribute('aria-pressed', b.dataset.modus === m));
zeigeWerte();
}
modi.forEach(b => b.addEventListener('click', () => { const war = offen; setzen(false, false); setTimeout(() => { variante(b.dataset.modus); if (war) setzen(true, false); }, war ? 500 : 0); }));
zeigeWerte();
addEventListener('load', zeigeWerte); // ?reduziert setzt die Klasse erst später
// ---------- Vorführung: ein gedachter Zeiger öffnet, liest, schließt; jede Runde eine Variante ----------
const FOLGE = ['ecke', 'mitte', 'zweistufig'];
let runde = 0, uhren = [], laeuft = false;
const spaeter = (ms, f) => uhren.push(setTimeout(f, ms));
const stoppen = () => { uhren.forEach(clearTimeout); uhren = []; };
function zeigerAuf(x, y) { zeiger.style.transform = `translate(${x - 2}px, ${y - 1}px)`; }
function aufElement(el, fx = .5, fy = .5) { const r = el.getBoundingClientRect(); zeigerAuf(r.left + r.width * fx, r.top + r.height * fy); }
function druecken() { zeiger.classList.add('drueckt'); setTimeout(() => zeiger.classList.remove('drueckt'), 160); }
function zeigt(a) { menue.classList.toggle('zeigt', !!a); menue.querySelectorAll('a').forEach(x => x.classList.toggle('zeigt', x === a)); }
function eineRunde() {
const m = FOLGE[runde % FOLGE.length]; runde++;
variante(m);
zeigt(null);
zeigerAuf(innerWidth * .42, innerHeight * .62);
spaeter(500, () => aufElement(knopf, .4, .55));
spaeter(1250, () => { druecken(); setzen(true, false); });
const links = [...menue.querySelectorAll('a')];
[0, 1, 3, 2].forEach((n, k) => spaeter(2100 + k * 520, () => { aufElement(links[n].querySelector('.menue__name'), .35, .55); spaeter(260, () => zeigt(links[n])); }));
spaeter(4500, () => { zeigt(null); aufElement(knopf, .4, .55); });
spaeter(5200, () => { druecken(); setzen(false, false); });
spaeter(6400, eineRunde);
}
function vorfuehren() {
stoppen(); laeuft = true;
html.classList.add('vorfuehrung');
setzen(false, false);
spaeter(offen ? 500 : 0, eineRunde);
}
function pause() { stoppen(); laeuft = false; }
function weiter() { if (!laeuft && html.classList.contains('vorfuehrung')) { runde = Math.max(0, runde - 1); vorfuehren(); } }
function neustart() { stoppen(); laeuft = false; html.classList.remove('vorfuehrung'); zeigt(null); setzen(false, false); runde = 0; zeigeWerte(); }
})();
</script>
</body>
</html>