Mega-Menü mit morphendem gemeinsamem Viewport
Vier Menüs teilen sich eine Box: Sie morpht in 250 ms auf Breite und Höhe des neuen Inhalts, der Inhalt gleitet in Bewegungsrichtung.
Mehrere Hauptpunkte wie Produkt oder Ressourcen teilen sich einen einzigen Dropdown-Container, den Viewport. Beim Hover öffnet er nach kurzer Verzögerung. Wechselt man zum Nachbarn, schließt er nicht, sondern ändert Breite, Höhe und Lage fließend auf die Maße des neuen Inhalts. Der alte Inhalt gleitet in Bewegungsrichtung hinaus, der neue kommt von der Gegenseite und blendet ein. Ein kleiner Pfeil kann auf den aktiven Trigger zeigen. Die Inhalte sind typografisch ruhig: Gruppen mit kleinen Überschriften, Icons, Kurzbeschreibungen und oft ein Teaser mit Bild.
Der Viewport wirkt wie ein Objekt, das mitdenkt. Es gibt kein Flackern zwischen den Menüs. Das richtungsbewusste Gleiten zeigt die räumliche Ordnung der Menüpunkte.
Wann einsetzen
Produkt- und SaaS-Seiten, Agenturen mit mehreren Leistungsbereichen, Shops mit Kategorien. Lohnt sich ab etwa drei Triggern mit je fünf oder mehr Einträgen.
Wann nicht
Bei wenigen, flachen Links und bei Portfolios. Bei einer reinen Touch-Zielgruppe ohne Desktop-Anteil.
Varianten
- Stripe-Stil: Die Box folgt dem Trigger und morpht, die Inhalte gleiten richtungsabhängig
- Radix- bzw. Vercel-Stil: Der Viewport bleibt stehen, die Größe kommt über CSS-Variablen, die Richtung über data-motion (from-start, from-end, to-start, to-end), ein Pfeil wandert
- Vollbreites Mega-Menü mit Kategorienspalte links, die den Inhaltsbereich steuert (Meter)
- Produktkarten mit Mini-UI-Vorschau (SurferSEO)
- Directional Hover (Osmo Mega Navigation)
- Mobil: Drill-down-Panels statt Viewport
Umsetzung
Alle Inhalte vorrendern und absolut im Viewport stapeln (width: max-content). Den aktiven Inhalt messen (offsetWidth, offsetHeight) und als CSS-Variablen --w, --h und --x auf den Viewport schreiben. width, height und translate transitionieren, overflow: hidden schneidet den Inhalt während des Morphens zu. Beim ersten Öffnen die Transition kurz abschalten, damit die Box nicht aus der alten Größe wächst.
Richtung aus dem Index-Vergleich von alt und neu bestimmen und als data-motion setzen. Keyframes von-start, von-ende, zu-start und zu-ende mit translateX und opacity. Vor dem Setzen das Attribut entfernen und einmal Layout lesen, sonst startet die Animation bei schnellem Hin und Her nicht neu.
Hover-Intent: Öffnen nach 200 ms. Solange die Box offen ist oder bis 300 ms nach dem Schließen wechseln die Trigger ohne Verzögerung (Radix skipDelayDuration). Beim Verlassen eine kurze Gnadenfrist lassen, damit der Weg vom Trigger in die Box und diagonales Ziehen nicht schließen.
Inaktive Panels mit inert versehen. Pfeiltasten in onKeyDown behandeln, onKeyPress feuert dafür nicht. backdrop-filter auf der Box wirkt nicht, wenn ein Elternelement für das Öffnen opacity oder transform animiert; dann den Hintergrund fast deckend wählen oder den Blur auf ein Geschwisterelement legen.
Robust umsetzen
Telefon. Der Viewport entfällt. Ein Menü-Knopf öffnet ein Vollbild-Blatt mit Drill-down-Panels oder einem Akkordeon. Die Trigger sind Buttons, Antippen öffnet die Unterebene.
Ohne Bewegung. Kein Gleiten, nur Überblenden in 100 bis 150 ms. Größe und Lage wechseln sofort.
Barrierefrei. Trigger als <button aria-expanded> mit aria-controls, Links als echte <a>. Pfeiltasten wechseln zwischen den Triggern, Home und End springen an die Enden, ArrowDown führt in den Inhalt, Escape schließt und setzt den Fokus zurück auf den Trigger. Öffnen auch per Enter und Klick, nicht nur per Hover.
Code
<!DOCTYPE html>
<!--
Mega-Menü mit morphendem gemeinsamem Viewport
Fundus · Navigation · https://cecodesign.de/lab/fundus/navigation/mega-menue-morphing-viewport/
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>Mega-Menü mit morphendem gemeinsamem Viewport · 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: 'Newsreader'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/newsreader:vf@latest/latin-opsz-normal.woff2') format('woff2-variations'); font-weight: 200 800; font-style: normal; font-display: swap; }
@font-face { font-family: 'Newsreader'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/newsreader:vf@latest/latin-opsz-italic.woff2') format('woff2-variations'); font-weight: 200 800; font-style: italic; font-display: swap; }
@font-face { font-family: 'Schibsted Grotesk'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/schibsted-grotesk:vf@latest/latin-wght-normal.woff2') format('woff2-variations'); font-weight: 400 900; font-style: normal; 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 kanzlei-beratung-souveraen: ein tiefer Markenton auf warmen Grautönen, heller Akzent nur für Aktiv und Fokus */
--grund: #f9f8f8; --flaeche: #efebeb; --linie-hart: #d2cdc7;
--karte: rgba(255, 255, 255, .985); --karte-voll: #ffffff; --fuss: #f6f4f4;
--marke: #1e1e50; --tinte: #212529; --leise: #5d6068; --meta: #73645a; --linie: rgba(30, 30, 80, .11); --akzent: #00bee6;
--serif: 'Newsreader', Georgia, serif;
/* Variante Stripe (Standard): Box folgt dem Trigger. Werte aus dem Builder.io-Nachbau */
--groesse-dauer: 250ms;
--groesse-kurve: cubic-bezier(0.2, 0.8, 0.2, 1);
--inhalt-dauer: 300ms;
--inhalt-kurve: cubic-bezier(0, 0, 0.58, 1); /* ease-out */
--distanz: 96px;
/* Öffnen und Schließen der Box */
--auf-dauer: 200ms; --zu-dauer: 150ms; --auf-kurve: cubic-bezier(0.25, 0.1, 0.25, 1); /* ease */
/* Mobil: Drill-down */
--drill-dauer: 300ms;
}
/* Variante Radix: Viewport bleibt stehen, Pfeil wandert. Werte aus dem Radix-Beispiel */
[data-variante="radix"] { --groesse-dauer: 300ms; --groesse-kurve: cubic-bezier(0.25, 0.1, 0.25, 1); --inhalt-dauer: 250ms; --inhalt-kurve: cubic-bezier(0.25, 0.1, 0.25, 1); --distanz: 200px; }
* { box-sizing: border-box; margin: 0; }
html { background: var(--grund); color: var(--tinte); font-family: 'Schibsted Grotesk', system-ui, sans-serif; -webkit-font-smoothing: antialiased; }
body { min-height: 100svh; background: var(--grund); }
button { font: inherit; color: inherit; background: none; border: 0; padding: 0; cursor: pointer; }
a { color: inherit; text-decoration: none; }
ul { list-style: none; padding: 0; }
.mono { font: 500 11px/1.35 'Geist Mono', ui-monospace, monospace; letter-spacing: .14em; text-transform: uppercase; color: var(--leise); }
.label { font: 500 11.5px/1.35 'Schibsted Grotesk', system-ui, sans-serif; letter-spacing: .1em; text-transform: uppercase; color: var(--meta); font-variant-numeric: tabular-nums; }
:focus-visible { outline: 2px solid var(--marke); outline-offset: 3px; border-radius: 2px; }
/* ---------- Kopf ---------- */
.kopf { position: sticky; top: 0; z-index: 30; background: rgba(249, 248, 248, .88); backdrop-filter: blur(14px) saturate(1.2); border-bottom: 1px solid var(--linie); }
.kopf__innen { position: relative; max-width: 1360px; margin: 0 auto; height: 72px; padding: 0 clamp(16px, 3.4vw, 48px); display: flex; align-items: center; gap: 24px; }
.marke { display: flex; align-items: center; gap: 12px; white-space: nowrap; margin-right: auto; color: var(--marke); }
.marke b { font: 500 23px/1 var(--serif); letter-spacing: -.012em; }
.marke small { padding-left: 12px; border-left: 1px solid var(--linie-hart); font-size: 11px; line-height: 1.25; font-weight: 500; letter-spacing: .1em; text-transform: uppercase; color: var(--meta); }
.nav { align-self: stretch; display: flex; align-items: stretch; }
.nav > li { display: flex; align-items: center; }
.nav__t { position: relative; display: flex; align-items: center; gap: 7px; height: 72px; padding: 0 13px; font-size: 15px; font-weight: 500; color: var(--leise); transition: color 150ms; white-space: nowrap; }
.nav__t::after { content: ""; position: absolute; left: 13px; right: 13px; bottom: -1px; height: 2px; background: var(--akzent); scale: 0 1; transition: scale 150ms var(--auf-kurve); }
.nav__t svg { width: 10px; height: 10px; transition: rotate var(--groesse-dauer) var(--groesse-kurve); }
.nav__t[aria-expanded="true"] { color: var(--marke); }
.nav__t[aria-expanded="true"]::after { scale: 1 1; }
.nav__t[aria-expanded="true"] svg { rotate: 180deg; }
@media (hover: hover) { .nav__t:hover { color: var(--marke); } }
.portal { display: none; align-items: center; gap: 7px; font-size: 14px; font-weight: 500; color: var(--leise); white-space: nowrap; }
.portal svg { width: 14px; height: 14px; }
.cta { height: 40px; display: flex; align-items: center; gap: 10px; padding: 0 18px; border-radius: 2px; background: var(--marke); color: #fff; font-size: 14px; font-weight: 500; white-space: nowrap; }
.cta svg { width: 12px; height: 12px; }
/* Textabstände (1.4.12): Kopf-CTA darf zweizeilig werden statt die Zeile zu sprengen */
.kopf__innen > .cta { height: auto; min-height: 40px; padding-block: 6px; line-height: 1.2; white-space: normal; min-width: 0; }
.kopf__innen > .cta svg { flex: none; }
.marke { min-width: 0; } .marke b { white-space: normal; }
.menue-knopf { display: none; }
/* ---------- Der gemeinsame Viewport ---------- */
.huelle {
position: absolute; left: clamp(16px, 3.4vw, 48px); right: clamp(16px, 3.4vw, 48px); top: 100%; height: 0;
perspective: 1400px; z-index: 2;
}
.huelle__innen {
position: absolute; inset: 0 0 auto 0;
transform-origin: var(--ox, 50%) 0;
opacity: 0; visibility: hidden; transform: rotateX(-12deg) scale(.96);
transition: opacity var(--zu-dauer) var(--auf-kurve), transform var(--zu-dauer) var(--auf-kurve), visibility 0s var(--zu-dauer);
}
.huelle.offen .huelle__innen { opacity: 1; visibility: visible; transform: none; transition: opacity var(--auf-dauer) var(--auf-kurve), transform var(--auf-dauer) var(--auf-kurve), visibility 0s; }
.huelle.neu .huelle__innen { animation: einzeln-auf var(--auf-dauer) var(--auf-kurve) both; }
@keyframes einzeln-auf { from { opacity: 0; transform: rotateX(-12deg) scale(.96); } }
.vp {
position: absolute; top: 10px; left: 0;
width: var(--w, 400px); height: var(--h, 300px); translate: var(--x, 0) 0;
border-radius: 6px; overflow: hidden;
background: var(--karte); /* kein backdrop-filter: die Öffnen-Animation (opacity/transform am Elternteil) würde ihn wirkungslos machen */
box-shadow: 0 0 0 1px rgba(30, 30, 80, .09), 0 2px 4px rgba(30, 30, 80, .04), 0 30px 60px -22px rgba(30, 30, 80, .32);
transition: width var(--groesse-dauer) var(--groesse-kurve), height var(--groesse-dauer) var(--groesse-kurve), translate var(--groesse-dauer) var(--groesse-kurve);
}
.pfeil {
position: absolute; top: 3px; left: 0; width: 16px; height: 8px; translate: calc(var(--px, 0px) - 8px) 0;
background: var(--karte-voll); clip-path: polygon(50% 0, 100% 100%, 0 100%);
transition: translate var(--groesse-dauer) var(--groesse-kurve);
}
.sofort .vp, .sofort .pfeil { transition: none; }
/* Panels: alle vorgerendert und gestapelt, nur das aktive ist sichtbar */
.panel { position: absolute; top: 0; left: 0; width: max-content; visibility: hidden; }
.panel.an, .panel[data-motion^="zu"] { visibility: visible; }
.panel[data-motion] { animation-duration: var(--inhalt-dauer); animation-timing-function: var(--inhalt-kurve); animation-fill-mode: both; }
.panel[data-motion="von-start"] { animation-name: von-start; }
.panel[data-motion="von-ende"] { animation-name: von-ende; }
.panel[data-motion="zu-start"] { animation-name: zu-start; }
.panel[data-motion="zu-ende"] { animation-name: zu-ende; }
@keyframes von-start { from { opacity: 0; translate: calc(var(--distanz) * -1) 0; } }
@keyframes von-ende { from { opacity: 0; translate: var(--distanz) 0; } }
@keyframes zu-start { to { opacity: 0; translate: calc(var(--distanz) * -1) 0; } }
@keyframes zu-ende { to { opacity: 0; translate: var(--distanz) 0; } }
.panel__inhalt { padding: 26px 28px 24px; display: grid; gap: 28px; }
.panel__fuss { display: flex; justify-content: space-between; align-items: center; gap: 20px; padding: 14px 28px; background: var(--fuss); border-top: 1px solid var(--linie); font-size: 13.5px; color: var(--leise); }
.panel__fuss a { color: var(--marke); font-weight: 500; white-space: nowrap; }
.panel__fuss a::after { content: " →"; }
.gruppe .label { margin-bottom: 12px; padding-bottom: 10px; border-bottom: 1px solid var(--linie); }
.eintrag { display: grid; grid-template-columns: 36px 1fr; gap: 13px; align-items: center; padding: 8px 10px; margin: 0 -10px; border-radius: 4px; transition: background-color 150ms; }
.eintrag b { display: block; font-size: 15px; font-weight: 500; color: var(--marke); }
.eintrag small { display: block; font-size: 13px; line-height: 1.35; color: var(--leise); margin-top: 2px; }
.ico { width: 36px; height: 36px; border-radius: 3px; display: grid; place-items: center; background: var(--karte-voll); box-shadow: inset 0 0 0 1px var(--linie-hart); transition: background-color 150ms; }
.ico svg { width: 18px; height: 18px; fill: none; stroke: var(--marke); stroke-width: 1.4; stroke-linecap: round; stroke-linejoin: round; }
.eintrag:is(:hover, .zeigt) { background: rgba(30, 30, 80, .045); }
.eintrag:is(:hover, .zeigt) .ico { background: var(--marke); box-shadow: none; }
.eintrag:is(:hover, .zeigt) .ico svg { stroke: #fff; }
/* Rechtsgebiete: zwei Gruppen und ein Studien-Teaser */
.p-recht .panel__inhalt { grid-template-columns: 256px 256px 224px; }
.teaser { display: grid; align-content: start; gap: 6px; padding: 10px; margin: -10px; border-radius: 4px; transition: background-color 150ms; }
.teaser svg { display: block; width: 100%; height: auto; aspect-ratio: 16 / 10; border-radius: 2px; margin-bottom: 8px; }
.teaser b { font: 400 19px/1.18 var(--serif); color: var(--marke); text-wrap: balance; }
.teaser small { font-size: 13px; line-height: 1.4; color: var(--leise); }
@media (hover: hover) { .teaser:hover { background: rgba(30, 30, 80, .045); } }
/* Köpfe: vier Karten mit Monogramm-Tafeln */
.p-koepfe .panel__inhalt { grid-template-columns: 200px 200px; gap: 20px 18px; }
.karte { display: grid; gap: 2px; }
.bild { position: relative; display: grid; align-content: end; aspect-ratio: 3 / 2; padding: 12px 14px; margin-bottom: 9px; border-radius: 2px; overflow: hidden; transition: scale 400ms var(--groesse-kurve);
background: var(--flaeche); color: var(--marke); }
.bild::before { content: ""; position: absolute; inset: 0; background: repeating-linear-gradient(90deg, transparent 0 11px, rgba(30, 30, 80, .07) 11px 12px); }
.bild.dunkel { background: var(--marke); color: #fff; }
.bild.dunkel::before { background: repeating-linear-gradient(90deg, transparent 0 11px, rgba(255, 255, 255, .08) 11px 12px); }
.bild i { position: relative; font: italic 300 46px/.9 var(--serif); letter-spacing: -.02em; }
.bild em { position: absolute; top: 11px; right: 12px; font-style: normal; font-size: 10.5px; font-weight: 500; letter-spacing: .1em; text-transform: uppercase; opacity: .72; }
.karte b { font-size: 15px; font-weight: 500; color: var(--marke); }
.karte small { font-size: 13px; color: var(--leise); display: flex; justify-content: space-between; gap: 10px; }
@media (hover: hover) { .karte:hover .bild { scale: .97; } }
/* Kanzlei: schmal, Textlinks */
.p-kanzlei .panel__inhalt { grid-template-columns: 300px; gap: 2px; }
.zeile { display: flex; justify-content: space-between; align-items: baseline; gap: 16px; padding: 10px 10px; margin: 0 -10px; border-radius: 4px; transition: background-color 150ms; }
.zeile b { font: 400 19px/1.2 var(--serif); color: var(--marke); }
.zeile small { font-size: 13px; color: var(--leise); }
.zeile .zahl { font-size: 11px; font-weight: 600; line-height: 19px; min-width: 19px; text-align: center; border-radius: 999px; background: var(--akzent); color: var(--marke); }
@media (hover: hover) { .zeile:hover { background: rgba(30, 30, 80, .045); } }
/* Insights: Liste mit Rubrik und Datum */
.p-insights .panel__inhalt { grid-template-columns: 372px; gap: 4px; }
.p-insights .panel__inhalt > .label { padding-bottom: 8px; }
.artikel { display: grid; gap: 4px; padding: 10px; margin: 0 -10px; border-radius: 4px; transition: background-color 150ms; }
.artikel .label { color: var(--meta); }
.artikel b { font: 400 18px/1.24 var(--serif); color: var(--marke); text-wrap: pretty; }
@media (hover: hover) { .artikel:hover { background: rgba(30, 30, 80, .045); } }
/* ---------- Seite unter dem Menü ---------- */
.seite { grid-template-columns: minmax(0, 1fr); max-width: 1360px; margin: 0 auto; padding: clamp(36px, 8vh, 88px) clamp(16px, 3.4vw, 48px) 120px; display: grid; gap: clamp(36px, 7vh, 64px); }
.held { display: grid; gap: 24px; }
.held__oben { display: flex; justify-content: space-between; align-items: center; gap: 12px 24px; flex-wrap: wrap; }
.frei { display: flex; align-items: center; gap: 9px; font-size: 14px; font-weight: 500; color: var(--marke); padding: 7px 14px 7px 12px; border-radius: 2px; background: var(--flaeche); }
.frei span { width: 7px; height: 7px; border-radius: 50%; background: var(--akzent); }
.held h1 { font: 400 clamp(38px, 1.3rem + 3.6vw, 76px)/1.04 var(--serif); letter-spacing: -.018em; color: var(--marke); max-width: 21ch; text-wrap: balance; }
.held h1 em { font-style: italic; font-weight: 300; }
.held__unten { display: flex; justify-content: space-between; align-items: end; gap: 20px 48px; flex-wrap: wrap; padding-top: 22px; border-top: 1px solid var(--linie-hart); }
.held__unten p:not(.label) { max-width: 54ch; font-size: clamp(15px, 1.25vw, 17px); line-height: 1.6; color: var(--leise); text-wrap: pretty; }
.aktuell { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: clamp(12px, 1.6vw, 20px); }
.meldung { display: grid; grid-template-rows: auto 1fr auto; gap: 18px; min-height: clamp(220px, 21vw, 290px); padding: clamp(18px, 1.8vw, 26px); background: var(--flaeche); color: var(--marke); border-radius: 2px; }
.meldung p { display: flex; justify-content: space-between; gap: 12px; }
.meldung h2 { align-self: end; font: 400 clamp(19px, 1.1rem + .6vw, 26px)/1.18 var(--serif); letter-spacing: -.008em; text-wrap: balance; overflow-wrap: break-word; }
.meldung > span { font-size: 14px; font-weight: 500; padding-top: 14px; border-top: 1px solid var(--linie-hart); }
.meldung > span::after { content: " →"; }
.meldung.dunkel { background: var(--marke); color: #fff; }
.meldung.dunkel .label { color: rgba(255, 255, 255, .62); }
.meldung.dunkel > span { border-color: rgba(255, 255, 255, .2); }
/* ---------- Mobil: Drill-down im Vollbild-Blatt ---------- */
.blatt { position: fixed; inset: 0; z-index: 40; background: var(--grund); display: grid; grid-template-rows: 72px 1fr; opacity: 0; visibility: hidden; transition: opacity var(--auf-dauer) var(--auf-kurve), visibility 0s var(--auf-dauer); }
.blatt.auf { opacity: 1; visibility: visible; transition: opacity var(--auf-dauer) var(--auf-kurve), visibility 0s; }
.blatt__kopf { display: flex; align-items: center; justify-content: space-between; padding: 0 16px; border-bottom: 1px solid var(--linie); }
.ebenen { position: relative; overflow: hidden; }
.ebene { position: absolute; inset: 0; overflow-y: auto; padding: 12px 16px 120px; transition: translate var(--drill-dauer) var(--inhalt-kurve), opacity var(--drill-dauer) var(--inhalt-kurve), visibility 0s; }
.ebene[data-stufe="1"] { translate: 100% 0; visibility: hidden; transition: translate var(--drill-dauer) var(--inhalt-kurve), visibility 0s var(--drill-dauer); }
.ebene[data-stufe="1"].an { translate: 0 0; visibility: visible; transition: translate var(--drill-dauer) var(--inhalt-kurve), visibility 0s; }
.blatt.tief .ebene[data-stufe="0"] { translate: -30% 0; opacity: 0; visibility: hidden; transition: translate var(--drill-dauer) var(--inhalt-kurve), opacity var(--drill-dauer) var(--inhalt-kurve), visibility 0s var(--drill-dauer); }
.oben { display: flex; justify-content: space-between; align-items: center; width: 100%; padding: 17px 0; border-bottom: 1px solid var(--linie-hart); font: 400 30px/1.1 var(--serif); letter-spacing: -.012em; color: var(--marke); text-align: left; }
.oben svg { width: 16px; height: 16px; rotate: -90deg; color: var(--meta); }
.zurueck { display: flex; align-items: center; gap: 8px; height: 44px; margin-bottom: 8px; }
.zurueck svg { width: 12px; height: 12px; rotate: 90deg; }
.ebene h2 { font: 400 32px/1.1 var(--serif); letter-spacing: -.012em; color: var(--marke); margin-bottom: 18px; }
.ebene .panel__inhalt { padding: 0; grid-template-columns: 1fr; gap: 24px; }
.ebene .panel__fuss { margin-top: 24px; border-radius: 3px; border: 1px solid var(--linie); padding: 14px 16px; flex-wrap: wrap; }
.ebene .p-koepfe .panel__inhalt { grid-template-columns: 1fr 1fr; gap: 18px 12px; }
.ebene .teaser { grid-template-columns: 128px 1fr; column-gap: 14px; row-gap: 4px; align-items: start; margin: 0; padding: 12px; background: var(--flaeche); border-radius: 3px; }
.ebene .teaser svg { grid-row: span 3; margin: 0; }
.ebene .karte small { flex-direction: column; gap: 1px; }
.ebene .teaser b { font-size: 17px; }
.ebene .bild i { font-size: 38px; }
.ebene .cta { margin-top: 28px; justify-content: center; height: 48px; font-size: 15px; }
/* ---------- Demo-Leiste ---------- */
.leiste { position: fixed; left: 0; right: 0; bottom: 0; z-index: 20; display: flex; justify-content: space-between; align-items: center; gap: 10px 24px; flex-wrap: wrap; padding: 12px clamp(16px, 3.4vw, 48px); background: rgba(249, 248, 248, .92); backdrop-filter: blur(10px); border-top: 1px solid var(--linie); }
.leiste b { color: var(--marke); font-weight: 500; }
.modi { display: flex; gap: 4px; align-items: center; flex-wrap: wrap; }
.modi > span { margin-right: 6px; }
.modi button { font: inherit; letter-spacing: inherit; text-transform: inherit; color: var(--leise); border: 1px solid var(--linie-hart); border-radius: 2px; height: 28px; padding: 0 12px; }
.modi button[aria-pressed="true"] { background: var(--marke); color: #fff; border-color: var(--marke); }
.werte { text-align: right; }
.werte__mobil, .werte__red { display: none; }
.reduziert .werte__red { display: inline; }
.reduziert [data-werte] { display: none; }
/* Vorführung: gezeichneter Zeiger */
.zeiger { position: fixed; left: 0; top: 0; z-index: 50; width: 18px; height: 18px; pointer-events: none; display: none; transition: scale 150ms var(--groesse-kurve); }
.vorfuehrung .zeiger { display: block; }
.zeiger.drueckt { scale: .8; }
.vorschau body { overflow: hidden; }
.vorschau .leiste { display: none; }
/* ---------- Reduzierte Bewegung: kein Gleiten, kein Morphen, nur kurzes Überblenden ---------- */
.reduziert .vp, .reduziert .pfeil, .reduziert .nav__t svg, .reduziert .bild { transition: none; }
.reduziert .huelle__innen, .reduziert .huelle.offen .huelle__innen { transform: none; transition-duration: 120ms; }
.reduziert .huelle.neu .huelle__innen { animation: none; }
.reduziert .panel[data-motion] { animation-duration: 120ms; animation-timing-function: linear; }
.reduziert .panel[data-motion="von-start"], .reduziert .panel[data-motion="von-ende"] { animation-name: blende-ein; }
.reduziert .panel[data-motion="zu-start"], .reduziert .panel[data-motion="zu-ende"] { animation-name: blende-aus; }
@keyframes blende-ein { from { opacity: 0; } }
@keyframes blende-aus { to { opacity: 0; } }
.reduziert .ebene, .reduziert .ebene[data-stufe="1"], .reduziert .ebene[data-stufe="1"].an, .reduziert .blatt.tief .ebene[data-stufe="0"] { translate: none; transition: opacity 120ms linear, visibility 0s; }
.reduziert .ebene[data-stufe="1"] { opacity: 0; }
.reduziert .ebene[data-stufe="1"].an { opacity: 1; }
/* ---------- Schmal: Viewport entfällt, Menü-Knopf öffnet das Blatt ---------- */
@media (max-width: 860px) {
.nav, .kopf__innen > .cta, .huelle { display: none; }
.menue-knopf { display: flex; align-items: center; gap: 10px; height: 40px; padding: 0 16px; border-radius: 2px; border: 1px solid var(--linie-hart); font-size: 15px; font-weight: 500; color: var(--marke); }
.menue-knopf i { display: grid; gap: 4px; width: 16px; }
.menue-knopf i::before, .menue-knopf i::after { content: ""; height: 1.5px; background: currentColor; border-radius: 2px; }
.aktuell { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.meldung:nth-child(3) { display: none; }
.leiste .werte__lang, .leiste .modi { display: none; }
:root:not(.reduziert) .werte__mobil { display: inline; }
.werte { text-align: left; }
}
@media (max-width: 520px) {
.marke small { display: none; }
.aktuell { grid-template-columns: minmax(0, 1fr); gap: 10px; }
.meldung { min-height: 0; gap: 22px; padding: 18px; }
.meldung h2 { font-size: 21px; }
}
@media (min-width: 861px) { .blatt { display: none; } }
@media (min-width: 861px) and (max-width: 1100px) {
.kopf__innen { gap: 14px; }
.marke small { display: none; }
.nav__t { padding: 0 10px; font-size: 14.5px; }
.nav__t::after { left: 10px; right: 10px; }
.werte__lang { display: none; }
}
@media (min-width: 1320px) { .portal { display: flex; } }
/* Kontrastmodus: Knöpfe mit Rahmen */
@media (forced-colors: active) { button, [role=button], [role=tab], [role=switch] { border: 1px solid ButtonText !important; } }
</style>
</head>
<body data-variante="stripe">
<svg width="0" height="0" style="position:absolute" aria-hidden="true">
<symbol id="pfeil-runter" viewBox="0 0 10 10"><path d="M1.5 3.5 5 7l3.5-3.5" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></symbol>
<symbol id="pfeil-rechts" viewBox="0 0 12 12"><path d="M1 6h10M7 2l4 4-4 4" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/></symbol>
</svg>
<header class="kopf">
<div class="kopf__innen" data-kopf>
<a class="marke" href="#start" aria-label="Brandt Okafor Rechtsanwälte, Startseite">
<b>Brandt Okafor</b><small>Rechtsanwälte<br>Frankfurt</small>
</a>
<nav aria-label="Hauptnavigation">
<ul class="nav" data-nav>
<li><button class="nav__t" data-trigger aria-expanded="false" aria-controls="p-recht">Rechtsgebiete <svg aria-hidden="true"><use href="#pfeil-runter"/></svg></button></li>
<li><button class="nav__t" data-trigger aria-expanded="false" aria-controls="p-koepfe">Köpfe <svg aria-hidden="true"><use href="#pfeil-runter"/></svg></button></li>
<li><button class="nav__t" data-trigger aria-expanded="false" aria-controls="p-kanzlei">Kanzlei <svg aria-hidden="true"><use href="#pfeil-runter"/></svg></button></li>
<li><button class="nav__t" data-trigger aria-expanded="false" aria-controls="p-insights">Insights <svg aria-hidden="true"><use href="#pfeil-runter"/></svg></button></li>
<li><a class="nav__t" href="#kontakt" data-flach>Kontakt</a></li>
</ul>
</nav>
<a class="portal" href="#portal"><svg viewBox="0 0 14 14" aria-hidden="true"><rect x="2.5" y="6" width="9" height="6.5" rx="1" fill="none" stroke="currentColor" stroke-width="1.3"/><path d="M4.5 6V4.2a2.5 2.5 0 0 1 5 0V6" fill="none" stroke="currentColor" stroke-width="1.3"/></svg>Mandantenportal</a>
<a class="cta" href="#erstgespraech">Erstgespräch vereinbaren <svg aria-hidden="true"><use href="#pfeil-rechts"/></svg></a>
<button class="menue-knopf" data-menueknopf aria-expanded="false" aria-controls="blatt"><i aria-hidden="true"></i>Menü</button>
<div class="huelle" data-huelle>
<div class="huelle__innen">
<div class="pfeil" aria-hidden="true"></div>
<div class="vp" data-vp>
<div class="panel p-recht" id="p-recht" data-panel inert>
<div class="panel__inhalt">
<div class="gruppe">
<p class="label">Transaktionen und Beratung</p>
<ul>
<li><a class="eintrag" href="#gesellschaftsrecht"><span class="ico"><svg viewBox="0 0 18 18"><rect x="2.5" y="2.5" width="8.5" height="8.5"/><rect x="7" y="7" width="8.5" height="8.5"/></svg></span><span><b>Gesellschaftsrecht und M&A</b><small>Kauf, Verkauf, Nachfolge</small></span></a></li>
<li><a class="eintrag" href="#arbeitsrecht"><span class="ico"><svg viewBox="0 0 18 18"><circle cx="6.3" cy="6" r="2.3"/><circle cx="12" cy="6" r="2.3"/><path d="M2 15c.4-2.6 2.1-4.2 4.3-4.2s3.9 1.6 4.3 4.2M9.6 11.4c.7-.4 1.5-.6 2.4-.6 2.2 0 3.7 1.6 4 4.2"/></svg></span><span><b>Arbeitsrecht</b><small>Restrukturierung, Betriebsrat</small></span></a></li>
<li><a class="eintrag" href="#bankrecht"><span class="ico"><svg viewBox="0 0 18 18"><path d="M2.5 7 9 3l6.5 4M3.5 7.5v6M7 7.5v6M11 7.5v6M14.5 7.5v6M2.5 15.5h13"/></svg></span><span><b>Bank- und Finanzierungsrecht</b><small>Akquisitionsfinanzierung</small></span></a></li>
</ul>
</div>
<div class="gruppe">
<p class="label">Konflikt und Compliance</p>
<ul>
<li><a class="eintrag" href="#prozess"><span class="ico"><svg viewBox="0 0 18 18"><path d="M9 2.5v13M5 15.5h8M3.5 5h11M3.5 5 1.8 10a1.9 1.9 0 0 0 3.4 0L3.5 5ZM14.5 5l-1.7 5a1.9 1.9 0 0 0 3.4 0L14.5 5Z"/></svg></span><span><b>Prozessführung</b><small>Zivilgerichte und Schiedsverfahren</small></span></a></li>
<li><a class="eintrag" href="#compliance"><span class="ico"><svg viewBox="0 0 18 18"><path d="M9 2 3.5 4v4.6c0 3.3 2.3 5.8 5.5 7.4 3.2-1.6 5.5-4.1 5.5-7.4V4L9 2Z"/><path d="m6.6 9 1.7 1.7 3.2-3.4"/></svg></span><span><b>Compliance und Investigations</b><small>Interne Untersuchungen</small></span></a></li>
<li><a class="eintrag" href="#kartellrecht"><span class="ico"><svg viewBox="0 0 18 18"><circle cx="7" cy="7" r="4.3"/><circle cx="11" cy="7" r="4.3"/><circle cx="9" cy="11" r="4.3"/></svg></span><span><b>Kartellrecht</b><small>Fusionskontrolle, Schadensersatz</small></span></a></li>
</ul>
</div>
<a class="teaser" href="#studie">
<svg viewBox="0 0 224 140" preserveAspectRatio="xMidYMid slice" aria-hidden="true">
<rect width="224" height="140" fill="#1e1e50"/>
<g stroke="rgba(255,255,255,.09)" stroke-width="1"><path d="M0 28.5h224M0 56.5h224M0 84.5h224M0 112.5h224"/><path d="M28.5 0v140M56.5 0v140M84.5 0v140M112.5 0v140M140.5 0v140M168.5 0v140M196.5 0v140"/></g>
<path d="M14 108h28V96h28V100h28V80h28V70h28V54h28V40h28" fill="none" stroke="#00bee6" stroke-width="2"/>
<g fill="#1e1e50" stroke="#00bee6" stroke-width="1.6"><circle cx="70" cy="96" r="2.6"/><circle cx="126" cy="80" r="2.6"/><circle cx="182" cy="54" r="2.6"/></g>
<text x="14" y="24" fill="rgba(255,255,255,.66)" font-family="Schibsted Grotesk, sans-serif" font-size="8.5" font-weight="500" letter-spacing="1">STUDIE 03 · 2026</text>
<text x="210" y="130" fill="#fff" font-family="Newsreader, serif" font-size="30" font-weight="300" font-style="italic" text-anchor="end">214</text>
<text x="14" y="130" fill="rgba(255,255,255,.66)" font-family="Schibsted Grotesk, sans-serif" font-size="8.5" font-weight="500" letter-spacing="1">TRANSAKTIONEN</text>
</svg>
<span class="label">Studie</span>
<b>Nachfolge im Mittelstand Rhein-Main</b>
<small>Wie Familienunternehmen 2026 verkaufen, übergeben und finanzieren.</small>
</a>
</div>
<div class="panel__fuss"><span>Erstgespräch: 30 Minuten, vertraulich, Termin binnen 48 Stunden</span><a href="#termin">Termin anfragen</a></div>
</div>
<div class="panel p-koepfe" id="p-koepfe" data-panel inert>
<div class="panel__inhalt">
<a class="karte" href="#brandt"><span class="bild dunkel"><em>Partnerin</em><i>HB</i></span><b>Dr. Helene Brandt</b><small><span>M&A, Nachfolge</span><span>DE · EN</span></small></a>
<a class="karte" href="#okafor"><span class="bild"><em>Partner</em><i>CO</i></span><b>Chidi Okafor</b><small><span>Prozessführung</span><span>DE · EN · FR</span></small></a>
<a class="karte" href="#yilmaz"><span class="bild"><em>Counsel</em><i>MY</i></span><b>Dr. Merve Yılmaz</b><small><span>Arbeitsrecht</span><span>DE · EN · TR</span></small></a>
<a class="karte" href="#ehrhardt"><span class="bild dunkel"><em>Partner</em><i>JE</i></span><b>Jonas Ehrhardt</b><small><span>Kartellrecht</span><span>DE · EN</span></small></a>
</div>
<div class="panel__fuss"><span>38 Berufsträgerinnen und Berufsträger</span><a href="#koepfe">Alle Köpfe</a></div>
</div>
<div class="panel p-kanzlei" id="p-kanzlei" data-panel inert>
<div class="panel__inhalt">
<a class="zeile" href="#profil"><b>Profil</b><small>Gegründet 2009</small></a>
<a class="zeile" href="#arbeitsweise"><b>Arbeitsweise</b><small>Feste Teams</small></a>
<a class="zeile" href="#probono"><b>Pro bono</b><small>Seit 2015</small></a>
<a class="zeile" href="#karriere"><b>Karriere</b><span class="zahl" aria-label="3 offene Stellen">3</span></a>
</div>
<div class="panel__fuss"><span>Im Westend, nahe der Alten Oper</span><a href="#anfahrt">Anfahrt</a></div>
</div>
<div class="panel p-insights" id="p-insights" data-panel inert>
<div class="panel__inhalt">
<p class="label">Insights</p>
<a class="artikel" href="#hinschg"><span class="label">Arbeitsrecht · 14.10.2026</span><b>Hinweisgeberschutz: Was Unternehmen ab 50 Beschäftigten prüfen sollten</b></a>
<a class="artikel" href="#share-deal"><span class="label">Gesellschaftsrecht · 02.10.2026</span><b>Nachfolge per Share Deal: Fünf Fehler, die den Kaufpreis kosten</b></a>
<a class="artikel" href="#fusionskontrolle"><span class="label">Kartellrecht · 18.09.2026</span><b>Wann ein Zukauf beim Bundeskartellamt angemeldet werden muss</b></a>
</div>
<div class="panel__fuss"><span>Mandantenbrief, einmal im Monat</span><a href="#mandantenbrief">Abonnieren</a></div>
</div>
</div>
</div>
</div>
</div>
</header>
<main class="seite" id="start">
<section class="held">
<div class="held__oben"><p class="label">Wirtschaftskanzlei · Frankfurt am Main · seit 2009</p><p class="frei"><span aria-hidden="true"></span>Erstgespräch binnen 48 Stunden</p></div>
<h1>Rechtsrat für Unternehmen, die <em>Entscheidungen</em> treffen müssen.</h1>
<div class="held__unten">
<p>Wir beraten Familienunternehmen und Mittelstand bei Transaktionen, Konflikten und Compliance. Jedes Mandat führt eine Partnerin oder ein Partner persönlich, vom ersten Gespräch bis zum Abschluss.</p>
<p class="label">Aktuell ↓</p>
</div>
</section>
<section class="aktuell" aria-label="Aktuelle Mandate und Insights">
<a class="meldung dunkel" href="#mandat"><p class="label"><span>Mandat · M&A</span><span>21.10.2026</span></p><h2>Verkauf eines hessischen Familienunternehmens an einen strategischen Käufer</h2><span>Zum Mandat</span></a>
<a class="meldung" href="#hinschg"><p class="label"><span>Insight · Arbeitsrecht</span><span>14.10.2026</span></p><h2>Hinweisgeberschutz: Was Unternehmen jetzt prüfen sollten</h2><span>Weiterlesen</span></a>
<a class="meldung" href="#fruehstueck"><p class="label"><span>Veranstaltung</span><span>12.11.2026</span></p><h2>Frühstück im Westend: Nachfolge rechtzeitig regeln</h2><span>Platz anfragen</span></a>
</section>
</main>
<div class="blatt" id="blatt" data-blatt inert>
<div class="blatt__kopf">
<span class="marke"><b>Brandt Okafor</b><small>Rechtsanwälte<br>Frankfurt</small></span>
<button class="menue-knopf" data-schliessen style="display:flex">Schließen</button>
</div>
<div class="ebenen" data-ebenen>
<div class="ebene" data-stufe="0" data-start></div>
</div>
</div>
<div class="leiste mono">
<div class="modi" role="group" aria-label="Variante">
<span>Variante</span>
<button type="button" data-modus="stripe" aria-pressed="true">Stripe</button>
<button type="button" data-modus="radix" aria-pressed="false">Radix</button>
<button type="button" data-modus="einzeln" aria-pressed="false">Ohne Morph</button>
</div>
<p class="werte" aria-live="polite"><span class="werte__lang" data-werte></span><span class="werte__red">Reduziert: Überblenden <b>120 ms</b> linear, Größe springt</span><span class="werte__mobil">Unter 860 px: Drill-down · <b>300 ms</b> ease-out</span> <span class="werte__lang">· Box <b data-mass>zu</b></span></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="#1e1e50" stroke="#ffffff" stroke-width="1.2" stroke-linejoin="round"/></svg>
<script>
(() => {
const html = document.documentElement, body = document.body;
const reduziert = () => html.classList.contains('reduziert');
const schmal = matchMedia('(max-width: 860px)');
const $ = s => document.querySelector(s), $$ = s => [...document.querySelectorAll(s)];
const kopf = $('[data-kopf]'), nav = $('[data-nav]'), huelle = $('[data-huelle]'), vp = $('[data-vp]');
const trigger = $$('[data-trigger]'), panels = $$('[data-panel]'), flach = $$('[data-flach]');
const anzeigeWerte = $('[data-werte]'), anzeigeMass = $('[data-mass]');
// Hover-Intent wie Radix (beobachtet): Öffnen nach 200 ms, Wechsel ohne Verzögerung
// bis 300 ms nach dem Schließen. Gnadenfrist beim Verlassen: eigener Wert.
const OEFFNEN_MS = 200, SKIP_MS = 300, GNADE_MS = 150;
const WERTE = {
stripe: 'Größe <b>250 ms</b> cubic-bezier(.2,.8,.2,1) · Inhalt <b>300 ms</b> ease-out, <b>96 px</b> · Intent <b>200 ms</b>',
radix: 'Größe <b>300 ms</b> ease · Inhalt <b>250 ms</b> ease, <b>200 px</b> · Box fest, Pfeil wandert',
einzeln: 'Vergleich: Menüs öffnen einzeln, <b>200 ms</b>, ohne gemeinsame Box'
};
let variante = 'stripe', offen = false, aktiv = -1, zuletztZu = -1e9, perHover = false;
let oeffnenUhr = 0, schliessenUhr = 0;
// ---------- Messen und Setzen ----------
function geometrie(i, w) {
const box = huelle.getBoundingClientRect(), t = trigger[i].getBoundingClientRect();
const mitte = t.left + t.width / 2 - box.left;
let x = variante === 'radix' ? trigger[0].getBoundingClientRect().left - box.left : mitte - w / 2;
x = Math.max(0, Math.min(box.width - w, x));
return { x: Math.round(x), mitte: Math.round(mitte) };
}
function setzen(i) {
const p = panels[i];
const w = p.offsetWidth, h = p.offsetHeight, g = geometrie(i, w);
vp.style.setProperty('--w', w + 'px');
vp.style.setProperty('--h', h + 'px');
vp.style.setProperty('--x', g.x + 'px');
huelle.style.setProperty('--px', g.mitte + 'px');
huelle.style.setProperty('--ox', g.mitte + 'px');
anzeigeMass.textContent = `${w} × ${h}`;
}
function bewegung(p, wert) {
p.removeAttribute('data-motion');
if (!wert) return;
void p.offsetWidth; // Animation neu starten, auch bei schnellem Hin und Her
p.dataset.motion = wert;
}
panels.forEach(p => p.addEventListener('animationend', e => {
if (e.target === p && p.dataset.motion && p.dataset.motion.startsWith('zu')) p.removeAttribute('data-motion');
}));
// ---------- Zeigen und Schließen ----------
function zeige(i) {
clearTimeout(oeffnenUhr); clearTimeout(schliessenUhr);
if (offen && i === aktiv) return;
const vorher = offen ? aktiv : -1;
aktiv = i;
trigger.forEach((t, j) => t.setAttribute('aria-expanded', String(j === i)));
const neu = panels[i];
if (vorher < 0 || variante === 'einzeln') {
// Erstes Öffnen (bzw. Vergleichsmodus): Größe und Lage springen, kein Gleiten
huelle.classList.add('sofort');
panels.forEach(p => { bewegung(p, null); p.classList.toggle('an', p === neu); p.inert = p !== neu; });
setzen(i);
void vp.offsetWidth;
requestAnimationFrame(() => huelle.classList.remove('sofort'));
if (vorher >= 0) { huelle.classList.remove('neu'); void huelle.offsetWidth; huelle.classList.add('neu'); }
} else {
// Wechsel: Box morpht, Inhalte gleiten in Bewegungsrichtung
const alt = panels[vorher], nachRechts = i > vorher;
panels.forEach(p => { if (p !== alt && p !== neu) bewegung(p, null); });
alt.classList.remove('an'); alt.inert = true;
bewegung(alt, nachRechts ? 'zu-start' : 'zu-ende');
neu.classList.add('an'); neu.inert = false;
bewegung(neu, nachRechts ? 'von-ende' : 'von-start');
setzen(i);
}
huelle.classList.add('offen');
offen = true;
}
function schliessen() {
clearTimeout(oeffnenUhr); clearTimeout(schliessenUhr);
if (!offen) return;
offen = false; zuletztZu = performance.now(); perHover = false;
huelle.classList.remove('offen', 'neu');
trigger.forEach(t => t.setAttribute('aria-expanded', 'false'));
panels.forEach(p => { p.inert = true; });
anzeigeMass.textContent = 'zu';
}
const spaeterSchliessen = () => { clearTimeout(schliessenUhr); schliessenUhr = setTimeout(schliessen, GNADE_MS); };
// ---------- Maus: Hover-Intent ----------
function betreten(i) {
clearTimeout(schliessenUhr); clearTimeout(oeffnenUhr);
if (offen || performance.now() - zuletztZu < SKIP_MS) { perHover = true; zeige(i); }
else oeffnenUhr = setTimeout(() => { perHover = true; zeige(i); }, OEFFNEN_MS);
}
trigger.forEach((t, i) => {
t.addEventListener('pointerenter', e => { if (e.pointerType === 'mouse') betreten(i); });
t.addEventListener('pointerleave', e => { if (e.pointerType === 'mouse') clearTimeout(oeffnenUhr); });
t.addEventListener('click', () => {
// Nach Hover-Öffnen schließt ein Klick nicht sofort wieder (sonst flackert es)
if (offen && aktiv === i) { if (!perHover) schliessen(); perHover = false; }
else { perHover = false; zeige(i); }
});
});
flach.forEach(a => a.addEventListener('pointerenter', e => { if (e.pointerType === 'mouse' && offen) spaeterSchliessen(); }));
[nav, huelle].forEach(el => {
el.addEventListener('pointerenter', e => { if (e.pointerType === 'mouse') clearTimeout(schliessenUhr); });
el.addEventListener('pointerleave', e => { if (e.pointerType === 'mouse') { clearTimeout(oeffnenUhr); if (offen) spaeterSchliessen(); } });
});
document.addEventListener('pointerdown', e => { if (offen && !kopf.contains(e.target)) schliessen(); });
// ---------- Tastatur ----------
const links = i => [...panels[i].querySelectorAll('a')];
nav.addEventListener('keydown', e => {
const oben = [...nav.querySelectorAll('.nav__t')], k = oben.indexOf(document.activeElement);
if (k < 0) return;
let z = -1;
if (e.key === 'ArrowRight') z = (k + 1) % oben.length;
else if (e.key === 'ArrowLeft') z = (k - 1 + oben.length) % oben.length;
else if (e.key === 'Home') z = 0;
else if (e.key === 'End') z = oben.length - 1;
if (z >= 0) {
e.preventDefault(); oben[z].focus();
const ti = trigger.indexOf(oben[z]);
if (offen) ti >= 0 ? zeige(ti) : schliessen();
return;
}
const ti = trigger.indexOf(document.activeElement);
if (e.key === 'ArrowDown' && ti >= 0) { e.preventDefault(); perHover = false; zeige(ti); links(ti)[0].focus(); }
if (e.key === 'Escape' && offen) { e.preventDefault(); schliessen(); }
});
huelle.addEventListener('keydown', e => {
if (aktiv < 0) return;
const l = links(aktiv), k = l.indexOf(document.activeElement);
if (e.key === 'Escape') { e.preventDefault(); schliessen(); trigger[aktiv].focus(); }
else if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
e.preventDefault();
const n = k + (e.key === 'ArrowDown' ? 1 : -1);
if (n < 0) trigger[aktiv].focus(); else l[Math.min(n, l.length - 1)].focus();
}
});
kopf.addEventListener('focusout', e => { if (offen && !kopf.contains(e.relatedTarget) && e.relatedTarget) schliessen(); });
document.addEventListener('click', e => { const a = e.target.closest('a[href^="#"]'); if (a) e.preventDefault(); });
// ---------- Variante ----------
const modi = $$('[data-modus]');
function variantSetzen(m) {
variante = m; body.dataset.variante = m;
modi.forEach(b => b.setAttribute('aria-pressed', String(b.dataset.modus === m)));
anzeigeWerte.innerHTML = WERTE[m];
if (offen) { huelle.classList.add('sofort'); setzen(aktiv); void vp.offsetWidth; requestAnimationFrame(() => huelle.classList.remove('sofort')); }
}
modi.forEach(b => b.addEventListener('click', () => variantSetzen(b.dataset.modus)));
variantSetzen('stripe');
addEventListener('resize', () => { if (offen) { if (schmal.matches) schliessen(); else setzen(aktiv); } });
// ---------- Mobil: Drill-down aus denselben Inhalten ----------
const blatt = $('[data-blatt]'), ebenen = $('[data-ebenen]'), start = $('[data-start]');
const menueKnopf = $('[data-menueknopf]');
const ebeneListe = [];
trigger.forEach((t, i) => {
const name = t.textContent.trim();
const b = document.createElement('button');
b.className = 'oben'; b.type = 'button'; b.setAttribute('aria-expanded', 'false');
b.innerHTML = `${name}<svg aria-hidden="true"><use href="#pfeil-runter"/></svg>`;
start.append(b);
const eb = document.createElement('div');
eb.className = 'ebene'; eb.dataset.stufe = '1'; eb.inert = true;
eb.setAttribute('role', 'region'); eb.setAttribute('aria-label', name);
eb.innerHTML = `<button class="zurueck mono" type="button"><svg aria-hidden="true"><use href="#pfeil-runter"/></svg>Zurück</button><h2>${name}</h2>`;
const klon = document.createElement('div');
klon.className = panels[i].className.replace('panel', '').trim();
klon.innerHTML = panels[i].innerHTML;
eb.append(klon);
ebenen.append(eb); ebeneListe.push(eb);
b.addEventListener('click', () => tiefer(i));
eb.querySelector('.zurueck').addEventListener('click', () => hoch());
});
const kontakt = document.createElement('a');
kontakt.className = 'oben'; kontakt.href = '#kontakt'; kontakt.textContent = 'Kontakt';
const cta = $('.kopf__innen > .cta').cloneNode(true);
start.append(kontakt, cta);
let tiefIndex = -1;
function blattAuf(an) {
blatt.classList.toggle('auf', an); blatt.inert = !an;
menueKnopf.setAttribute('aria-expanded', String(an));
body.style.overflow = an ? 'hidden' : '';
if (!an) hoch(true);
}
function tiefer(i) {
tiefIndex = i; blatt.classList.add('tief');
ebeneListe.forEach((e, j) => { e.classList.toggle('an', j === i); e.inert = j !== i; });
start.inert = true;
start.querySelectorAll('.oben')[i].setAttribute('aria-expanded', 'true');
}
function hoch(still) {
const war = tiefIndex; tiefIndex = -1;
blatt.classList.remove('tief'); start.inert = false;
ebeneListe.forEach(e => { e.classList.remove('an'); e.inert = true; });
start.querySelectorAll('.oben').forEach(b => b.setAttribute('aria-expanded', 'false'));
if (!still && war >= 0) start.querySelectorAll('.oben')[war].focus({ preventScroll: true });
}
menueKnopf.addEventListener('click', () => blattAuf(true));
$('[data-schliessen]').addEventListener('click', () => { blattAuf(false); menueKnopf.focus(); });
blatt.addEventListener('keydown', e => { if (e.key === 'Escape') { if (tiefIndex >= 0) hoch(); else { blattAuf(false); menueKnopf.focus(); } } });
// ---------- Vorführung: gedachter Zeiger ----------
const zeiger = $('[data-zeiger]');
let lauf = 0, pos = { x: 0, y: 0 }, unterIndex = -1, imBereich = false, markiert = null;
const warte = (ms, n) => new Promise(r => setTimeout(r, ms)).then(() => { if (n !== lauf) throw 0; });
const mitteVon = (el, fx = .5, fy = .5) => { const r = el.getBoundingClientRect(); return { x: r.left + r.width * fx, y: r.top + r.height * fy }; };
const glatt = t => t < .5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
function zeigerMalen() { zeiger.style.transform = `translate(${pos.x - 2}px, ${pos.y - 1}px)`; }
// Wertet aus, was unter dem gedachten Zeiger liegt, und ruft dieselbe Logik wie die echte Maus
function abtasten() {
const el = document.elementFromPoint(pos.x, pos.y);
const t = el && el.closest('[data-trigger]'), i = trigger.indexOf(t);
const drin = !!(el && (nav.contains(el) || (offen && el.closest('.vp'))));
if (i !== unterIndex) { if (unterIndex >= 0) clearTimeout(oeffnenUhr); if (i >= 0) betreten(i); unterIndex = i; }
if (drin && !imBereich) clearTimeout(schliessenUhr);
if (!drin && imBereich && offen) spaeterSchliessen();
imBereich = drin;
const e = el && el.closest('.eintrag, .karte, .zeile, .artikel');
if (e !== markiert) { if (markiert) markiert.classList.remove('zeigt'); markiert = e; if (e) e.classList.add('zeigt'); }
}
function fahre(ziel, ms, n) {
const von = { ...pos };
return new Promise((ok, nein) => {
const t0 = performance.now();
const schritt = t => {
if (n !== lauf) return nein(0);
const p = Math.min(1, (t - t0) / ms), k = glatt(p);
pos.x = von.x + (ziel.x - von.x) * k; pos.y = von.y + (ziel.y - von.y) * k;
zeigerMalen(); abtasten();
p < 1 ? requestAnimationFrame(schritt) : ok();
};
requestAnimationFrame(schritt);
});
}
async function tippen(el, n) {
zeiger.classList.add('drueckt'); await warte(150, n); zeiger.classList.remove('drueckt'); el.click();
}
async function rundeDesktop(n) {
const t = trigger;
await fahre(t[0] && mitteVon(t[0], .62, .82), 900, n); await warte(1500, n);
const cd = panels[0].querySelector('a[href="#bankrecht"]');
await fahre(mitteVon(cd, .3, .5), 700, n); await warte(900, n);
await fahre(mitteVon(t[1], .66, .82), 650, n); await warte(1500, n);
await fahre(mitteVon(t[2], .66, .82), 500, n); await warte(1300, n);
await fahre(mitteVon(t[3], .66, .82), 500, n); await warte(1500, n);
await fahre(mitteVon(t[2], .4, .82), 500, n); await warte(1100, n);
await fahre(mitteVon(t[1], .4, .82), 500, n); await warte(1300, n);
await fahre({ x: innerWidth * .3, y: innerHeight * .78 }, 900, n); await warte(1200, n);
}
async function rundeMobil(n) {
await fahre(mitteVon(menueKnopf), 800, n); await tippen(menueKnopf, n); await warte(900, n);
const oben = start.querySelectorAll('.oben');
await fahre(mitteVon(oben[0], .3), 600, n); await tippen(oben[0], n); await warte(1800, n);
let z = ebeneListe[0].querySelector('.zurueck');
await fahre(mitteVon(z), 600, n); await tippen(z, n); await warte(700, n);
await fahre(mitteVon(oben[1], .3), 500, n); await tippen(oben[1], n); await warte(1800, n);
z = ebeneListe[1].querySelector('.zurueck');
await fahre(mitteVon(z), 600, n); await tippen(z, n); await warte(600, n);
const s = $('[data-schliessen]');
await fahre(mitteVon(s), 600, n); await tippen(s, n); await warte(1000, n);
}
async function vorfuehren() {
pause();
const n = lauf;
html.classList.add('vorfuehrung');
pos = { x: innerWidth * .62, y: innerHeight * .8 }; zeigerMalen();
try { for (;;) await (schmal.matches ? rundeMobil(n) : rundeDesktop(n)); } catch (e) { if (e !== 0) throw e; }
}
function pause() { lauf++; clearTimeout(oeffnenUhr); clearTimeout(schliessenUhr); }
function neustart() {
pause(); html.classList.remove('vorfuehrung');
schliessen(); blattAuf(false); unterIndex = -1; imBereich = false;
if (markiert) { markiert.classList.remove('zeigt'); markiert = null; }
}
function weiter() { if (html.classList.contains('vorfuehrung')) vorfuehren(); }
})();
</script>
</body>
</html>