Zum Inhalt
← Navigation

Menü-Button-Morph: Burger zu X und Label-Tausch „Menu/Close“

Der Menü-Button zeigt seinen Zustand selbst: Linien drehen zum X, das Label rollt von „Menü“ zu „Schließen“.

Der Menü-Button trägt ein Textlabel („Menü“) neben zwei oder drei Linien. Beim Öffnen drehen die äußeren Linien zum X (+45° und −45°), eine mittlere Linie zieht sich zusammen. Gleichzeitig rollt das Label in einer einzeiligen Maske nach oben: „Menü“ fährt hinaus, „Schließen“ kommt von unten nach. Bei Hover rücken die Linien leicht auseinander. Optional steht neben „Schließen“ ein Kürzel-Hinweis [Esc].

Der Zustand wechselt genau dort, wo geklickt wurde. Das Wort sagt eindeutiger als jedes Icon, was der nächste Klick tut, und wirkt dabei editorial.

Wann einsetzen

Bei jeder Overlay- oder Drawer-Navigation, besonders wenn die Seite sonst wenig Bedienelemente zeigt.

Wann nicht

Wenn die Navigation dauerhaft sichtbar ist. Dann braucht es keinen Burger.

Varianten

  • Zwei Linien zum X
  • Drei Linien: Mittellinie verschwindet, dazu Kürzel-Hinweis „Schließen [Esc]“ (basement.studio)
  • Kreis-Button, der beim Öffnen wächst und zur Menüfläche wird
  • Label-Roll allein, ohne Linien
  • Lottie-Animation statt CSS

Umsetzung

<button aria-expanded="false" aria-controls="menue"><span class="wort"><span class="rolle"><span>Menü</span><span aria-hidden="true">Schließen</span></span></span><span class="striche" aria-hidden="true"><i></i><i></i></span></button>. .wort ist eine Zeile hoch (height: 1.25em; overflow: hidden), .rolle stapelt beide Wörter als Grid und verschiebt sich geöffnet um translateY(−1.25em). Beide Wörter teilen eine Spalte, so bleibt die Breite stabil. Linien absolut zentriert, geschlossen um ±Abstand versetzt, geöffnet translate 0 und rotate(±45deg). Die Kurve cubic-bezier(.65,.05,0,1) gilt für Linien (400 ms) und Label (450 ms). Den Zustand nur über aria-expanded steuern. Das zweite Wort ist aria-hidden, damit der Name nicht doppelt gelesen wird; der Name bleibt „Menü“, aria-expanded meldet den Zustand.

Robust umsetzen

Telefon. Trefferfläche mindestens 44 × 44 px. Das Label darf mobil entfallen, das Icon bleibt. Hover-Effekte nur unter @media (hover: hover).

Ohne Bewegung. Kein Roll, das Label wechselt sofort. Das X erscheint ohne Drehung. Die Menüfläche blendet nur kurz ein.

Barrierefrei. Echter <button> mit aria-expanded und aria-controls. Sichtbarer Fokusring. Escape schließt und setzt den Fokus zurück auf den Button. Die geschlossene Menüfläche ist inert.

Code

<!DOCTYPE html>
<!--
  Menü-Button-Morph: Burger zu X und Label-Tausch „Menu/Close“
  Fundus · Navigation · https://cecodesign.de/lab/fundus/navigation/menue-icon-und-label-morph/
  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>Menü-Button-Morph: Burger zu X und Label-Tausch · 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: 'Special Gothic'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/special-gothic:vf@latest/latin-standard-normal.woff2') format('woff2-variations'); font-weight: 400 700; font-stretch: 75% 125%; font-display: swap; }
@font-face { font-family: 'Atkinson Hyperlegible Next'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/atkinson-hyperlegible-next:vf@latest/latin-wght-normal.woff2') format('woff2-variations'); font-weight: 200 800; font-display: swap; }
@font-face { font-family: 'Geist Mono'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/geist-mono:vf@latest/latin-wght-normal.woff2') format('woff2-variations'); font-weight: 100 900; font-display: swap; }

:root {
  /* Rezept handwerk-werkstatt-klartext: Materialfarben, ein Signalton nur für Anfrage und Anruf */
  --grund: #efedea; --zement: #d3cdc2; --holz: #c3b4a5; --salbei: #bdc1a9; --tinte: #1a1a1a;
  --leise: rgba(26, 26, 26, .72); --linie: rgba(26, 26, 26, .16);
  --signal: #fb6100;
  /* Menü-Button: gemessene Kurve aus der Osmo Side Navigation */
  --kurve: cubic-bezier(0.65, 0.05, 0, 1);
  --icon: 400ms;             /* Linien drehen */
  --roll: 450ms;             /* Label rollt */
  --versatz: 50ms;           /* untere Linie folgt */
  --auf: 500ms;              /* Fläche öffnet */
  --zu: 280ms;               /* Fläche schließt, schneller */
  --zu-kurve: cubic-bezier(0.4, 0, 1, 1);
  --ui: 150ms cubic-bezier(0.4, 0, 0.2, 1);
  --knopf-gr: 15px;
  --rand: clamp(16px, 3.4vw, 48px);
}
* { box-sizing: border-box; margin: 0; padding: 0; }
html { background: var(--grund); color: var(--tinte); font-family: 'Atkinson Hyperlegible Next', system-ui, sans-serif; -webkit-font-smoothing: antialiased; }
body { min-height: 100svh; display: grid; grid-template-rows: auto 1fr auto; overflow-x: hidden; }
a { color: inherit; text-decoration: none; }
.gewerk { font: 500 13px/1.2 'Special Gothic', 'Arial Narrow', sans-serif; font-stretch: 75%; letter-spacing: .04em; text-transform: uppercase; font-variant-numeric: tabular-nums; }
.mono { font: 500 11px/1.5 'Geist Mono', ui-monospace, monospace; letter-spacing: .14em; text-transform: uppercase; color: var(--leise); }

/* ---------- Kopf ---------- */
.kopf {
  position: sticky; top: 0; z-index: 30;
  display: flex; align-items: center; gap: 24px;
  padding: 18px var(--rand); min-height: 84px;
}
.marke { display: flex; align-items: center; gap: 12px; margin-right: auto; }
.marke svg { width: 30px; height: 30px; flex: none; }
.marke b { display: block; font: 700 21px/1 'Special Gothic', sans-serif; font-stretch: 118%; letter-spacing: .02em; }
.marke small { display: block; margin-top: 4px; color: var(--leise); }
.kopf-info { color: var(--leise); }
.anruf {
  display: inline-flex; align-items: center; gap: 10px; min-height: 44px; padding: 0 18px 0 14px;
  background: var(--signal); color: var(--tinte); border-radius: 999px;
  font: 700 15px/1 'Special Gothic', sans-serif; letter-spacing: .01em; font-variant-numeric: tabular-nums;
  transition: background-color var(--ui), scale var(--ui);
}
.anruf svg { width: 18px; height: 18px; }
.anruf:active { scale: .96; }

/* ---------- Der Menü-Button ---------- */
.menueknopf {
  --a: 4px;                  /* halber Linienabstand, zwei Linien */
  --lw: 1.75em;              /* Linienlänge */
  display: inline-flex; align-items: center; gap: .8em;
  min-height: 44px; min-width: 44px; padding: 0 2px 0 10px;
  border: 0; background: none; color: var(--tinte); cursor: pointer;
  font: 600 var(--knopf-gr)/1 'Special Gothic', sans-serif; font-stretch: 75%; letter-spacing: .06em; text-transform: uppercase;
  border-radius: 8px; transition: color var(--icon) var(--kurve), scale var(--ui);
  -webkit-tap-highlight-color: transparent;
}
.menueknopf:active, .menueknopf.gedrueckt { scale: .96; }
.menueknopf:focus-visible { outline: 2px solid currentColor; outline-offset: 4px; }

/* Label: zwei Wörter untereinander in einer Maske, eine Zeile hoch */
.wort { display: block; height: 1.25em; overflow: hidden; }
.rolle { display: grid; grid-template-rows: 1.25em 1.25em; justify-items: end; align-items: center; transition: translate var(--roll) var(--kurve); }
.rolle span { display: inline-flex; align-items: center; gap: .5em; white-space: nowrap; }
.rolle kbd { display: none; font: 500 .62em/1 'Geist Mono', ui-monospace, monospace; letter-spacing: .06em; padding: .3em .45em .25em; border: 1px solid currentColor; border-radius: 4px; opacity: .7; }
.offen .rolle { translate: 0 -1.25em; }

/* Linien */
.striche { position: relative; display: block; width: var(--lw); height: 1.4em; flex: none; }
.striche i {
  position: absolute; left: 50%; top: 50%; width: var(--lw); height: 2px; margin: -1px 0 0 calc(var(--lw) / -2);
  background: currentColor; border-radius: 1px;
  transition: translate var(--icon) var(--kurve), rotate var(--icon) var(--kurve), scale var(--icon) var(--kurve), opacity var(--icon) var(--kurve);
}
.striche i:nth-child(1) { translate: 0 calc(var(--a) * -1); }
.striche i:nth-child(2) { scale: 0 1; opacity: 0; }
.striche i:nth-child(3) { translate: 0 var(--a); transition-delay: 0s, var(--versatz), 0s, 0s; }
@media (hover: hover) { .menueknopf:hover { --a: 5.5px; } }
.menueknopf.schwebt { --a: 5.5px; }
.offen .striche i:nth-child(1) { translate: 0 0; rotate: 45deg; }
.offen .striche i:nth-child(3) { translate: 0 0; rotate: -45deg; }

/* Variante: drei Linien, Mittellinie zieht sich zusammen, Esc-Hinweis */
[data-variante="drei"] .menueknopf { --a: 7px; }
[data-variante="drei"] .menueknopf.schwebt { --a: 8.5px; }
@media (hover: hover) { [data-variante="drei"] .menueknopf:hover { --a: 8.5px; } }
[data-variante="drei"] .striche i:nth-child(2) { scale: 1 1; opacity: 1; }
[data-variante="drei"].offen .striche i:nth-child(2) { scale: 0 1; opacity: 0; }
[data-variante="drei"] .rolle kbd { display: inline-block; }

/* Variante: Kreis, der zur Menüfläche wächst */
[data-variante="kreis"] .menueknopf { --lw: 1.2em; --a: 3.5px; gap: 1em; }
[data-variante="kreis"] .menueknopf.schwebt { --a: 5px; }
@media (hover: hover) { [data-variante="kreis"] .menueknopf:hover { --a: 5px; } }
[data-variante="kreis"] .striche { width: 52px; height: 52px; color: var(--grund); }
[data-variante="kreis"] .striche::before { content: ""; position: absolute; inset: 0; border-radius: 50%; background: var(--tinte); transition: scale var(--ui); }
[data-variante="kreis"].offen .menueknopf { color: var(--grund); }

/* ---------- Menüfläche ---------- */
.schleier { position: fixed; inset: 0; z-index: 10; background: rgba(26, 26, 26, .38); opacity: 0; pointer-events: none; transition: opacity var(--zu) var(--zu-kurve); }
.offen .schleier { opacity: 1; pointer-events: auto; transition: opacity var(--auf) var(--kurve); }
.menue {
  --p-grund: var(--salbei); --p-text: var(--tinte); --p-leise: rgba(26, 26, 26, .66); --p-linie: rgba(26, 26, 26, .2);
  position: fixed; top: 0; right: 0; z-index: 20; width: min(560px, 100vw); height: 100svh;
  background: var(--p-grund); color: var(--p-text);
  padding: 112px var(--rand) 28px clamp(24px, 3.4vw, 48px);
  display: flex; flex-direction: column; overflow-y: hidden;   /* zu: inert und ohne Scrollbereich, offen: scrollt */
  clip-path: inset(0 0 0 100%);
  transition: clip-path var(--zu) var(--zu-kurve);
}
.offen .menue { overflow-y: auto; clip-path: inset(0 0 0 0); transition: clip-path var(--auf) var(--kurve); }
[data-variante="kreis"] .menue { --p-grund: var(--tinte); --p-text: var(--grund); --p-leise: rgba(239, 237, 234, .62); --p-linie: rgba(239, 237, 234, .18); clip-path: circle(26px at var(--kx, 90%) var(--ky, 50px)); }
[data-variante="kreis"].offen .menue { clip-path: circle(var(--kr, 150%) at var(--kx, 90%) var(--ky, 50px)); }

.menue-kopf { display: flex; justify-content: space-between; color: var(--p-leise); padding-bottom: 14px; border-bottom: 1px solid var(--p-linie); }
.menue-liste { list-style: none; }
.menue-liste a {
  display: grid; grid-template-columns: 2.6em 1fr; column-gap: 8px; row-gap: 4px;
  padding: 16px 0 17px; border-bottom: 1px solid var(--p-linie);
}
.menue-liste .nr { grid-row: span 2; padding-top: .55em; color: var(--p-leise); }
.menue-liste .titel { font: 700 clamp(26px, 2.6vw, 34px)/1.05 'Special Gothic', sans-serif; letter-spacing: -.01em; }
.menue-liste .text { font-size: 15px; line-height: 1.45; color: var(--p-leise); }
.menue-liste a:focus-visible { outline: 2px solid currentColor; outline-offset: 2px; }
@media (hover: hover) { .menue-liste a .titel { transition: translate var(--ui); } .menue-liste a:hover .titel { translate: 6px 0; } }
.menue-fuss { margin-top: auto; padding-top: 28px; display: grid; grid-template-columns: 1fr 1fr; gap: 20px 24px; font-size: 15px; line-height: 1.5; }
.menue-fuss .gewerk { color: var(--p-leise); margin-bottom: 6px; }
.menue-cta { grid-column: 1 / -1; display: flex; gap: 10px; flex-wrap: wrap; }
.knopf {
  display: inline-flex; align-items: center; justify-content: center; min-height: 48px; padding: 0 22px; border-radius: 999px;
  font: 700 15px/1 'Special Gothic', sans-serif; letter-spacing: .01em; border: 1.5px solid currentColor;
  transition: scale var(--ui), background-color var(--ui);
}
.knopf.signal { background: var(--signal); border-color: var(--signal); color: var(--tinte); }
.knopf:active { scale: .96; }

/* Einträge: 8 px Versatz plus Deckkraft, gestaffelt */
.menue [data-stufe] { opacity: 0; translate: 0 8px; transition: opacity 150ms linear, translate 0s 150ms; }
.offen .menue [data-stufe] { opacity: 1; translate: 0 0; transition: opacity var(--icon) var(--kurve), translate var(--icon) var(--kurve); transition-delay: calc(140ms + var(--i, 0) * 40ms); }

/* ---------- Seite dahinter ---------- */
.held {
  display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1.05fr); gap: clamp(28px, 4vw, 64px); align-items: center;
  padding: clamp(12px, 3vw, 40px) var(--rand) clamp(28px, 4vw, 48px);
}
.held-text .gewerk { color: var(--leise); margin-bottom: 18px; }
h1 { font: 700 clamp(2.25rem, 1.5rem + 3vw, 3.5rem)/1.05 'Special Gothic', sans-serif; font-stretch: 100%; letter-spacing: -.01em; text-wrap: balance; max-width: 13em; }
.held-text p.lead { margin-top: 20px; font-size: 1.0625rem; line-height: 1.55; max-width: 34em; text-wrap: pretty; }
.held-cta { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 28px; }
.zahlen { display: grid; grid-template-columns: repeat(4, auto); justify-content: start; gap: 8px 36px; list-style: none; margin-top: clamp(28px, 4vw, 48px); padding-top: 18px; border-top: 1px solid var(--linie); }
.zahlen b { display: block; font: 700 28px/1 'Special Gothic', sans-serif; font-variant-numeric: tabular-nums; }
.zahlen span { display: block; margin-top: 6px; color: var(--leise); }
.held-bild { background: var(--zement); border-radius: 6px; padding: clamp(14px, 2vw, 26px); }
.held-bild svg { display: block; width: 100%; height: auto; }
.held-bild figcaption { display: flex; justify-content: space-between; gap: 12px; margin-top: 12px; padding-top: 10px; border-top: 1px solid var(--linie); color: var(--leise); }

/* ---------- Steuerung der Demo ---------- */
.steuer { display: flex; flex-wrap: wrap; justify-content: space-between; align-items: center; gap: 12px 24px; padding: 14px var(--rand) 18px; border-top: 1px solid var(--linie); }
.werte { max-width: 70ch; }
.werte b { font-weight: 500; color: var(--tinte); }
.varianten { display: flex; gap: 4px; flex-wrap: wrap; }
.varianten button { font: inherit; letter-spacing: inherit; text-transform: inherit; color: var(--leise); background: none; border: 1px solid var(--linie); border-radius: 999px; padding: 7px 12px 6px; cursor: pointer; transition: color var(--ui), border-color var(--ui), background-color var(--ui); }
.varianten button[aria-pressed="true"] { color: var(--grund); background: var(--tinte); border-color: var(--tinte); }
.varianten button:focus-visible { outline: 2px solid var(--tinte); outline-offset: 2px; }

/* Vorführung: gezeichneter Zeiger */
.zeiger { position: fixed; left: 0; top: 0; z-index: 40; width: 20px; height: 20px; pointer-events: none; display: none; transition: transform 700ms var(--kurve); }
.vorfuehrung .zeiger { display: block; }

/* Vorschau 960 × 720: nur die Bühne, Knopf größer */
.vorschau body { overflow: hidden; height: 100svh; }
.vorschau .steuer { display: none; }
:root.vorschau { --knopf-gr: 22px; }
.vorschau .kopf { min-height: 104px; }
.vorschau .menue { padding-top: 132px; }
.vorschau .menue-liste a { padding: 12px 0 13px; }
.vorschau .menue-fuss { padding-top: 20px; }
.vorschau .leiste { display: none; }

/* Untere Leiste (schmal): Anrufen und Anfrage immer erreichbar */
.leiste { display: none; }

/* ---------- Schmal und Touch ---------- */
@media (max-width: 1100px) { .kopf-info { display: none; } }
@media (max-width: 860px) {
  .held { grid-template-columns: 1fr; }
  .zahlen { grid-template-columns: repeat(2, auto); }
}
@media (max-width: 560px) {
  .kopf { gap: 10px; min-height: 72px; padding-top: 12px; padding-bottom: 12px; }
  .marke small { display: none; }
  .kopf .anruf { display: none; }
  .leiste { position: fixed; left: 0; right: 0; bottom: 0; z-index: 8; display: grid; grid-template-columns: 1fr 1fr; gap: 8px; padding: 10px 16px calc(10px + env(safe-area-inset-bottom)); background: var(--grund); border-top: 1px solid var(--linie); }
  .leiste .knopf { min-height: 48px; }
  body { padding-bottom: 70px; }
  .menue { padding-top: 92px; }
  .menue-fuss { grid-template-columns: 1fr; }
  .held-bild figcaption span:last-child { display: none; }
  .werte { font-size: 10px; }
}
@media (hover: none) {
  .menueknopf { padding-left: 14px; }
}

/* ---------- Reduzierte Bewegung: kein Roll, keine Drehung, nur kurzes Einblenden der Fläche ---------- */
.reduziert .rolle, .reduziert .striche i, .reduziert .menueknopf, .reduziert .menue [data-stufe], .reduziert .zeiger { transition: none !important; }
.reduziert .menue, .reduziert[data-variante] .menue { clip-path: none; opacity: 0; visibility: hidden; transition: opacity 150ms linear, visibility 0s 150ms; }
.reduziert.offen .menue { opacity: 1; visibility: visible; transition: opacity 150ms linear; }
.reduziert .menue [data-stufe] { opacity: 1; translate: none; }
.reduziert .schleier { transition: opacity 150ms linear; }
/* Kontrastmodus: Knöpfe mit Rahmen */
@media (forced-colors: active) { button, [role=button], [role=tab], [role=switch] { border: 1px solid ButtonText !important; } }
</style>
</head>
<body>
<header class="kopf">
  <a class="marke" href="#start" aria-label="Tischlerei Tönjes, Startseite">
    <svg viewBox="0 0 30 30" aria-hidden="true"><rect width="30" height="30" rx="4" fill="#1a1a1a"/><path d="M8 7h14l-3.5 7.5L22 23H8l3.5-8.5Z" fill="#c3b4a5"/></svg>
    <span><b>TÖNJES</b><small class="gewerk">Tischlerei · Oldenburg</small></span>
  </a>
  <p class="kopf-info gewerk">Meisterbetrieb seit 1987 · Werkstatt Donnerschwee</p>
  <a class="anruf" href="#anrufen" aria-label="Anrufen: 0441 998 76 0">
    <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M6.6 3.5 9 3l1.6 4.3-2 1.5a12 12 0 0 0 6.6 6.6l1.5-2L21 15l-.5 2.4A2.4 2.4 0 0 1 18 19.5 15.5 15.5 0 0 1 4.5 6 2.4 2.4 0 0 1 6.6 3.5Z" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round"/></svg>
    <span>0441 998 76 0</span>
  </a>
  <button class="menueknopf" type="button" aria-expanded="false" aria-controls="menue" data-knopf>
    <span class="wort"><span class="rolle"><span>Menü</span><span aria-hidden="true">Schließen <kbd>Esc</kbd></span></span></span>
    <span class="striche" aria-hidden="true"><i></i><i></i><i></i></span>
  </button>
</header>

<div class="schleier" data-schleier aria-hidden="true"></div>
<nav class="menue" id="menue" aria-label="Hauptmenü" data-menue inert>
  <div class="menue-kopf gewerk" data-stufe style="--i:0"><span>Seiten</span><span>05</span></div>
  <ol class="menue-liste">
    <li data-stufe style="--i:1"><a href="#leistungen"><span class="nr gewerk">01</span><span class="titel">Leistungen</span><span class="text">Einbauschränke, Küchen, Treppen, Holzfenster und Haustüren.</span></a></li>
    <li data-stufe style="--i:2"><a href="#referenzen"><span class="nr gewerk">02</span><span class="titel">Referenzen</span><span class="text">Über 600 Projekte zwischen Ammerland und Wesermarsch.</span></a></li>
    <li data-stufe style="--i:3"><a href="#ablauf"><span class="nr gewerk">03</span><span class="titel">So läuft ein Auftrag</span><span class="text">Aufmaß vor Ort, Zeichnung, Festpreis, Fertigung, Einbau.</span></a></li>
    <li data-stufe style="--i:4"><a href="#werkstatt"><span class="nr gewerk">04</span><span class="titel">Werkstatt &amp; Team</span><span class="text">14 Leute, zwei Meister, drei Azubis. Wir bilden aus.</span></a></li>
    <li data-stufe style="--i:5"><a href="#kontakt"><span class="nr gewerk">05</span><span class="titel">Kontakt</span><span class="text">Anfrage mit Foto und Maßen, Antwort binnen zwei Werktagen.</span></a></li>
  </ol>
  <div class="menue-fuss">
    <div data-stufe style="--i:6"><p class="gewerk">Werkstatt</p><p>Donnerschweer Straße 212<br>26123 Oldenburg</p></div>
    <div data-stufe style="--i:6"><p class="gewerk">Erreichbar</p><p>Mo–Fr 7–16 Uhr<br>Notdienst Türen: 24 h</p></div>
    <div class="menue-cta" data-stufe style="--i:7"><a class="knopf signal" href="#anrufen">Anrufen</a><a class="knopf" href="#anfrage">Anfrage stellen</a></div>
  </div>
</nav>

<main class="held" id="start">
  <div class="held-text">
    <p class="gewerk">Tischlerei · Innenausbau · Oldenburg und Ammerland</p>
    <h1>Möbel, die in Ihre Wände passen.</h1>
    <p class="lead">Wir messen auf, bauen in unserer Werkstatt in Donnerschwee und montieren selbst. Ein Ansprechpartner vom ersten Termin bis zur letzten Fuge.</p>
    <div class="held-cta"><a class="knopf signal" href="#anfrage">Anfrage stellen</a><a class="knopf" href="#anrufen">0441 998 76 0</a></div>
    <ul class="zahlen">
      <li><b>1987</b><span class="gewerk">gegründet</span></li>
      <li><b>14</b><span class="gewerk">Leute</span></li>
      <li><b>600+</b><span class="gewerk">Projekte</span></li>
      <li><b>40 km</b><span class="gewerk">Einzugsgebiet</span></li>
    </ul>
  </div>
  <figure class="held-bild">
    <svg viewBox="0 0 600 470" role="img" aria-label="Werkzeichnung: Einbauschrank unter einer Dachschräge, Ansicht">
      <!-- Raum: Wand mit Dachschräge -->
      <path d="M50 30H300L560 220V420H50Z" fill="#e6e2dc"/>
      <path d="M50 30H300L560 220V420" fill="none" stroke="#1a1a1a" stroke-width="1.6"/>
      <path d="M30 420H580" stroke="#1a1a1a" stroke-width="2.4"/>
      <!-- Hochschrank links, zwei Türen -->
      <g stroke="#1a1a1a" stroke-width="1.2">
        <rect x="70" y="50" width="70" height="355" fill="#bdc1a9"/>
        <rect x="142" y="50" width="70" height="355" fill="#bdc1a9"/>
        <path d="M132 210v40M150 210v40" stroke-width="2.2" stroke-linecap="round"/>
        <!-- offenes Regal in Eiche -->
        <rect x="214" y="50" width="86" height="355" fill="#c3b4a5"/>
        <path d="M214 135h86M214 220h86M214 305h86" />
      </g>
      <!-- Inhalt Regal -->
      <g fill="#1a1a1a" opacity=".78">
        <rect x="222" y="98" width="7" height="37"/><rect x="231" y="104" width="6" height="31"/><rect x="239" y="94" width="8" height="41"/>
        <rect x="270" y="112" width="22" height="23" rx="2" fill="#efedea" stroke="#1a1a1a"/>
        <rect x="222" y="196" width="34" height="24" fill="#efedea" stroke="#1a1a1a"/>
        <rect x="262" y="186" width="7" height="34"/><rect x="271" y="190" width="6" height="30"/>
        <circle cx="282" cy="292" r="12" fill="none" stroke="#1a1a1a"/>
        <rect x="222" y="268" width="40" height="37" fill="#efedea" stroke="#1a1a1a"/>
      </g>
      <!-- Schrank unter der Schräge: drei Türen, Oberkante folgt der Schräge -->
      <g stroke="#1a1a1a" stroke-width="1.2" fill="#bdc1a9">
        <path d="M302 52L380 109V318H302Z"/>
        <path d="M382 110L460 167V318H382Z"/>
        <path d="M462 168L540 225V318H462Z"/>
        <rect x="302" y="320" width="118" height="85"/>
        <rect x="422" y="320" width="118" height="85"/>
        <path d="M370 250v34M392 250v34M450 260v24M472 260v24M341 362h40M461 362h40" stroke-width="2.2" stroke-linecap="round"/>
      </g>
      <!-- Sockel -->
      <rect x="70" y="405" width="470" height="15" fill="#1a1a1a" opacity=".86"/>
      <!-- Bemaßung -->
      <g stroke="#1a1a1a" stroke-width="1" fill="none">
        <path d="M70 446H540M70 439v14M540 439v14M300 442v8"/>
        <path d="M28 50V405M21 50h14M21 405h14"/>
        <path d="M410 128a40 40 0 0 1 22 -8" opacity=".7"/>
      </g>
      <g font-family="'Special Gothic', 'Arial Narrow', sans-serif" font-size="13" font-weight="500" fill="#1a1a1a" letter-spacing=".5" style="font-stretch: 75%">
        <text x="185" y="466" text-anchor="middle">2.300</text>
        <text x="420" y="466" text-anchor="middle">2.380</text>
        <text x="16" y="230" text-anchor="middle" transform="rotate(-90 16 230)">2.360</text>
        <text x="440" y="116">36°</text>
      </g>
    </svg>
    <figcaption class="gewerk"><span>Einbauschrank Dachgeschoss, Bloherfelde</span><span>Ansicht M 1:25 · Eiche, Fronten Salbei lackiert</span></figcaption>
  </figure>
</main>

<div class="steuer">
  <p class="mono werte" data-werte></p>
  <div class="varianten mono" role="group" aria-label="Variante">
    <button type="button" data-v="zwei" aria-pressed="true">Zwei Linien</button>
    <button type="button" data-v="drei" aria-pressed="false">Drei Linien + Esc</button>
    <button type="button" data-v="kreis" aria-pressed="false">Kreis wird Fläche</button>
  </div>
</div>

<div class="leiste"><a class="knopf signal" href="#anrufen">Anrufen</a><a class="knopf" href="#anfrage">Anfrage</a></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="#1a1a1a" stroke="#efedea" stroke-width="1.2" stroke-linejoin="round"/></svg>

<script>
(() => {
  const html = document.documentElement;
  const reduziert = () => html.classList.contains('reduziert');
  const knopf = document.querySelector('[data-knopf]');
  const menue = document.querySelector('[data-menue]');
  const schleier = document.querySelector('[data-schleier]');
  const werte = document.querySelector('[data-werte]');
  const zeiger = document.querySelector('[data-zeiger]');
  const varianten = [...document.querySelectorAll('[data-v]')];

  const WERTE = {
    zwei:  'Kurve <b>cubic-bezier(.65,.05,0,1)</b> · Linien <b>400 ms</b>, ±45°, untere <b>+50 ms</b> · Label-Roll <b>450 ms</b>, 100 % Zeilenhöhe · Fläche auf <b>500 ms</b>, zu <b>280 ms</b> cubic-bezier(.4,0,1,1)',
    drei:  'Kurve <b>cubic-bezier(.65,.05,0,1)</b> · Mittellinie scaleX 1→0 <b>400 ms</b> · äußere ±45°, untere <b>+50 ms</b> · Label-Roll <b>450 ms</b> mit Esc-Hinweis · Fläche auf <b>500 ms</b>, zu <b>280 ms</b>',
    kreis: 'Kurve <b>cubic-bezier(.65,.05,0,1)</b> · Kreis <b>52 px</b> wächst per clip-path: circle() in <b>500 ms</b>, zu <b>280 ms</b> · Linien <b>400 ms</b>, ±45° · Label-Roll <b>450 ms</b>'
  };

  function variante(v) {
    if (html.classList.contains('offen')) umschalten(false);
    html.dataset.variante = v;
    varianten.forEach(b => b.setAttribute('aria-pressed', String(b.dataset.v === v)));
    werte.innerHTML = WERTE[v];
    kreisMessen();
  }
  varianten.forEach(b => b.addEventListener('click', () => variante(b.dataset.v)));

  // Kreis-Variante: Mittelpunkt des Knopfes relativ zur Menüfläche, Radius bis zur fernsten Ecke
  function kreisMessen() {
    const k = knopf.querySelector('.striche').getBoundingClientRect(), m = menue.getBoundingClientRect();
    const x = k.left + k.width / 2 - m.left, y = k.top + k.height / 2 - m.top;
    const r = Math.hypot(Math.max(x, m.width - x), Math.max(y, m.height - y));
    menue.style.setProperty('--kx', x + 'px');
    menue.style.setProperty('--ky', y + 'px');
    menue.style.setProperty('--kr', Math.ceil(r + 2) + 'px');
  }
  addEventListener('resize', kreisMessen);

  // Eine Quelle der Wahrheit: aria-expanded am Knopf, gespiegelt als .offen am <html>
  function umschalten(auf) {
    auf = auf ?? knopf.getAttribute('aria-expanded') !== 'true';
    if (auf) kreisMessen();
    knopf.setAttribute('aria-expanded', String(auf));
    html.classList.toggle('offen', auf);
    menue.inert = !auf;
  }
  knopf.addEventListener('click', () => { stoppen(); umschalten(); });
  schleier.addEventListener('click', () => umschalten(false));
  addEventListener('keydown', e => {
    if (e.key === 'Escape' && knopf.getAttribute('aria-expanded') === 'true') { umschalten(false); knopf.focus(); }
  });
  document.addEventListener('click', e => { const a = e.target.closest('a[href^="#"]'); if (a) e.preventDefault(); });

  // ---------- Vorführung: gedachter Zeiger öffnet und schließt, in der Vorschau durch alle Varianten ----------
  let lauf = 0, angehalten = false;
  const warten = ms => new Promise(r => setTimeout(r, ms));
  function zeigerAuf(x, y, sofort) {
    zeiger.style.transition = sofort ? 'none' : '';
    zeiger.style.transform = `translate(${x - 3}px, ${y - 2}px)`;
  }
  function knopfMitte() { const r = knopf.querySelector('.striche').getBoundingClientRect(); return [r.left + r.width * .35, r.top + r.height * .62]; }
  async function klick(id) {
    knopf.classList.add('gedrueckt'); await warten(150); if (id !== lauf) return;
    knopf.classList.remove('gedrueckt'); umschalten();
  }
  async function runde(id) {
    const r = reduziert();
    const reihe = html.classList.contains('vorschau') ? ['zwei', 'drei', 'kreis'] : [html.dataset.variante];
    let n = reihe.indexOf(html.dataset.variante); if (n < 0) n = 0;
    while (id === lauf) {
      if (reihe.length > 1) variante(reihe[n % reihe.length]);
      n++;
      zeigerAuf(innerWidth * .62, innerHeight * .78, true);
      await warten(500); if (id !== lauf) return;
      zeigerAuf(...knopfMitte());
      await warten(750); if (id !== lauf) return;
      knopf.classList.add('schwebt');
      await warten(r ? 200 : 550); if (id !== lauf) return;
      await klick(id); if (id !== lauf) return;
      await warten(700); if (id !== lauf) return;
      const erster = menue.querySelector('.menue-liste li:nth-child(2) .titel').getBoundingClientRect();
      knopf.classList.remove('schwebt');
      zeigerAuf(erster.left + 60, erster.top + 20);
      await warten(1500); if (id !== lauf) return;
      zeigerAuf(...knopfMitte());
      await warten(750); if (id !== lauf) return;
      knopf.classList.add('schwebt');
      await warten(350); if (id !== lauf) return;
      await klick(id); if (id !== lauf) return;
      await warten(500); if (id !== lauf) return;
      knopf.classList.remove('schwebt');
      zeigerAuf(innerWidth * .62, innerHeight * .78);
      await warten(900);
    }
  }
  function stoppen() {
    if (!html.classList.contains('vorfuehrung')) return;
    lauf++; html.classList.remove('vorfuehrung'); knopf.classList.remove('schwebt', 'gedrueckt');
  }
  function vorfuehren() {
    lauf++; angehalten = false;
    umschalten(false); knopf.classList.remove('schwebt', 'gedrueckt');
    html.classList.add('vorfuehrung');
    runde(lauf);
  }
  function pause() { if (html.classList.contains('vorfuehrung')) { lauf++; angehalten = true; } }
  function weiter() { if (angehalten) vorfuehren(); }
  function neustart() {
    lauf++; angehalten = false;
    html.classList.remove('vorfuehrung'); knopf.classList.remove('schwebt', 'gedrueckt');
    umschalten(false);
  }

  variante('zwei');

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