Zum Inhalt
← Navigation

Gestaltetes Cookie-/Consent-Banner (DSGVO-konform)

Das Consent-Banner spricht die Sprache der Marke: Ablehnen und Akzeptieren stehen gleichwertig nebeneinander, Dienste laden erst nach der Zustimmung.

Das Consent-Banner ist Teil des Designsystems statt ein fremdes CMP-Overlay. Auf dem Desktop ist es eine kompakte Karte unten links oder rechts, auf dem Telefon ein Bottom-Sheet. Es nutzt Hausschrift, Markenfarben und Radius und blendet sanft ein. Die erste Ebene bietet gleichwertige Aktionen: Ablehnen und Akzeptieren sind gleich groß und gleich gut erreichbar, dazu kommen die Einstellungen. Die zweite Ebene ist ein Dialog mit Kategorien als Schalter: Notwendig ist fest an, Statistik, Marketing und externe Medien sind standardmäßig aus. Skripte wie Analytics oder Einbettungen laden erst nach der Zustimmung. Die Wahl lässt sich jederzeit über den Footer-Link „Cookie-Einstellungen“ ändern. Externe Medien wie Videos oder Karten stehen als Zwei-Klick-Platzhalter im Layout.

Das Banner ist oft das Erste, was Besucher sehen. Ist es gestaltet und ehrlich, wirkt es wie ein Teil der Marke statt wie ein Fremdkörper und schafft Vertrauen. Gleichwertige Knöpfe vermeiden Dark Patterns und senken das Abmahnrisiko.

Wann einsetzen

Auf jeder Website mit nicht notwendigen Cookies, Trackern oder eingebetteten Drittinhalten. Besonders lohnend bei Templates, weil es einmal sauber gebaut überall passt, und bei Studio-Websites, deren Detailtreue bewertet wird.

Wann nicht

Wenn keine einwilligungspflichtigen Dienste laufen, etwa bei cookieloser Statistik ohne Einwilligungspflicht (rechtlich prüfen). Dann braucht es kein Banner. Nie als Vollbild-Sperre (Cookie-Wall) ohne gleichwertige Alternative.

Varianten

  • Helle Karte mit drei gleichrangigen Knöpfen Einstellungen / Ablehnen / Akzeptieren und Einstellungsdialog (Manual)
  • Dunkle Karte unten links mit zwei gleichen Knöpfen, mobil volle Breite (Dogstudio)
  • Glas-Box mit Kategorie-Schaltern direkt auf der ersten Ebene (Bureau Borsche; dort fehlt ein ausdrückliches Ablehnen, in der Demo ergänzt)
  • Texte und Kategorien im CMS gepflegt (Edenspiekermann)
  • Standard-CMP wie Cookiebot per CSS an das Designsystem angepasst (Diesdas)
  • Zwei-Klick-Platzhalter für Video oder Karte im Layout

Umsetzung

Das Banner ist eine <section aria-label="Cookie-Hinweis">, nicht modal. Es steht früh im DOM, damit die Tastatur es schnell erreicht, und ist unten fixiert. Es blendet erst nach dem ersten Paint ein (opacity und translate, 300 ms, cubic-bezier(0.25, 0.46, 0.45, 0.94)), damit es den LCP nicht verschiebt. Ablehnen und Akzeptieren haben gleiche Größe, Farbe und Gewicht. „Einstellungen“ öffnet einen <dialog> mit showModal(). Die Kategorien sind echte Checkboxen mit role="switch"; nur „Notwendig“ ist gesetzt und deaktiviert. Gespeichert werden Entscheidung, Version und Zeitstempel in einem notwendigen Cookie oder in localStorage. Skripte stehen als <script type="text/plain" data-kategorie="statistik"> im HTML und werden nach Zustimmung als echtes Skript neu eingesetzt. Der Footer-Link „Cookie-Einstellungen“ öffnet den Dialog erneut. Als Open-Source-Basis eignet sich orestbida/cookieconsent, das sich komplett über CSS-Variablen gestalten lässt. Rechtlicher Rahmen (vor dem Livegang prüfen, keine Rechtsberatung): § 25 TDDDG (vormals TTDSG), Art. 6 und 7 DSGVO, EuGH Planet49 (keine vorausgewählten Häkchen) und die DSK-Orientierungshilfe Telemedien (Ablehnen auf der ersten Ebene gleichwertig). Stand Digital Omnibus (Heuking, 23.10.2026; vom Kunden prüfen lassen): Bis zum Inkrafttreten gilt § 25 TDDDG unverändert. Einwilligung braucht also alles, was für den gewünschten Dienst nicht unbedingt erforderlich ist. Der Rat hat am 21.10.2026 seine Position festgelegt, die des Parlaments steht noch aus; in Kraft frühestens 2027. Geplant sind Ausnahmen für aggregierte Reichweitenmessung (auch durch beauftragte Dienstleister, wenn die Daten aggregiert und anonymisiert sind) und für kontextbezogene Werbung, dazu eine Sperrfrist nach Ablehnung, in der nicht erneut gefragt werden darf (Kommission sechs, Ratstext vier Monate). Die Pflicht, Browsersignale zu beachten, hat der Rat gestrichen. Deshalb die Kategorien konfigurierbar bauen: Dienste mit Zweck in einer Liste pflegen (notwendig, Sicherheit, Reichweitenmessung, Werbung, externe Medien), Ablehnen mit einem Klick auf der ersten Ebene, Entscheidung mit Zeitstempel, Zweck und Version speichern und die Wartezeit bis zur nächsten Abfrage als Einstellung vorsehen. So lässt sich eine Kategorie später ohne Umbau aus der Einwilligung nehmen.

Robust umsetzen

Telefon. Bottom-Sheet in voller Breite, höchstens etwa 40 bis 45 % der Höhe. Die Knöpfe sind gleich breit und mit dem Daumen erreichbar. Inhalte dürfen nicht unbedienbar verdeckt werden; ein Abstand in Höhe des Banners am Seitenende hilft.

Ohne Bewegung. Banner und Dialog erscheinen sofort, ohne Gleiten und Blenden.

Barrierefrei. Das Banner ist per Tastatur früh erreichbar, entweder durch seine Stellung im DOM oder per Skip-Link. Die Einstellungen sind ein echter <dialog> mit Fokusfalle und Escape. Jeder Schalter hat ein Label. Der Kontrast liegt bei mindestens 4,5 : 1, und die Knöpfe unterscheiden sich nicht nur durch Farbe. Das Banner darf Fokus-Elemente der Seite nicht dauerhaft verdecken (WCAG 2.4.11), etwa durch scroll-padding-bottom in Höhe des Banners.

Code

<!DOCTYPE html>
<!--
  Gestaltetes Cookie-/Consent-Banner (DSGVO-konform)
  Fundus · Navigation · https://cecodesign.de/lab/fundus/navigation/consent-banner-gestaltet/
  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, viewport-fit=cover">
<title>Gestaltetes Cookie-/Consent-Banner (DSGVO-konform) · 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: 'Instrument Serif'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/instrument-serif@latest/latin-400-italic.woff2') format('woff2'); font-weight: 400; font-style: italic; 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 {
  --papier: #eeebe4; --karte: #faf8f3; --tinte: #141413; --leise: rgba(20, 20, 19, .64); --linie: rgba(20, 20, 19, .14);
  --akzent: #b0441f;                                   /* Ziegel, 4,9 : 1 auf Papier */
  --nacht: #0e101a; --hell: #f3f2ee; --hell-leise: rgba(243, 242, 238, .72);
  --kurve: cubic-bezier(0.25, 0.46, 0.45, 0.94);      /* gemessen bei Dogstudio */
  --dauer: 300ms;                                      /* Einblenden, gemessen */
  --ui: 150ms;                                         /* Schalter, Hover */
  --rand: clamp(20px, 4vw, 48px);
}
* { box-sizing: border-box; margin: 0; }
html { background: var(--papier); color: var(--tinte); font-family: 'Instrument Sans', system-ui, sans-serif; -webkit-font-smoothing: antialiased; scroll-padding-bottom: calc(var(--banner-h, 0px) + 24px); }
body { min-height: 100svh; overflow-x: hidden; padding-bottom: var(--banner-h, 0px); }   /* nichts bleibt dauerhaft unter dem Banner */
img { display: block; max-width: 100%; }
button { font: inherit; color: inherit; }
a { color: inherit; }
.mono { font: 500 11px/1.35 'Geist Mono', ui-monospace, monospace; letter-spacing: .14em; text-transform: uppercase; color: var(--leise); }
.sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
:focus-visible { outline: 2px solid var(--akzent); outline-offset: 3px; }

/* ---------- Fundus-Steuerung (dezent, gehört nicht zur Beispielseite) ---------- */
.steuerung { display: grid; gap: 10px; padding: 14px var(--rand); border-bottom: 1px solid var(--linie); }
.steuerung .zeile { display: flex; align-items: center; justify-content: space-between; gap: 12px 20px; flex-wrap: wrap; }
.modi { display: flex; gap: 4px; flex-wrap: wrap; }
.modi button, .neu { font: inherit; letter-spacing: inherit; text-transform: inherit; color: var(--leise); background: none; border: 1px solid var(--linie); border-radius: 999px; height: 30px; padding: 0 12px; cursor: pointer; transition: color var(--ui), background-color var(--ui), border-color var(--ui); }
.modi button:hover, .neu:hover { color: var(--tinte); border-color: rgba(20, 20, 19, .32); }
.modi button[aria-pressed='true'] { background: var(--tinte); color: var(--papier); border-color: var(--tinte); }
.werte { display: flex; flex-wrap: wrap; gap: 4px 18px; }
.werte b { color: var(--tinte); font-weight: 500; }
.werte .status b { color: var(--akzent); }

/* ---------- Beispielseite: Studio Feldmark, Kassel ---------- */
.seite { padding: 0 var(--rand); }
.nav { display: flex; align-items: center; justify-content: space-between; height: 76px; }
.marke { display: flex; align-items: center; gap: 10px; font-size: 21px; font-weight: 600; letter-spacing: -.03em; text-decoration: none; }
.marke svg { width: 22px; height: 22px; }
.marke em { font-family: 'Instrument Serif', Georgia, serif; font-weight: 400; letter-spacing: 0; color: var(--leise); }
.nav ul { display: flex; gap: clamp(16px, 2.4vw, 32px); list-style: none; padding: 0; }
.nav a { text-decoration: none; color: var(--tinte); }
.nav a:hover { color: var(--akzent); }

.held { display: grid; grid-template-columns: 1.15fr .85fr; gap: clamp(24px, 4vw, 64px); align-items: end; padding: clamp(16px, 3vh, 40px) 0 clamp(28px, 4vh, 48px); }
.held .ober { margin-bottom: clamp(14px, 2vh, 24px); }
.titel { font-size: clamp(44px, 6.6vw, 108px); font-weight: 600; line-height: .93; letter-spacing: -.05em; max-width: 9.5ch; }
.titel em { font-family: 'Instrument Serif', Georgia, serif; font-weight: 400; letter-spacing: -.02em; color: var(--akzent); padding-right: .04em; }
.unter { display: flex; gap: 28px; align-items: baseline; margin-top: clamp(16px, 2.4vh, 28px); }
.unter p:first-child { font-size: clamp(15px, 1.25vw, 18px); line-height: 1.45; color: var(--leise); max-width: 36ch; }

/* Zwei-Klick-Platzhalter: externes Video lädt erst nach Zustimmung */
.medium { position: relative; aspect-ratio: 16 / 11; border-radius: 14px; overflow: hidden; background: #d9d4ca; isolation: isolate; }
.medium img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; filter: grayscale(1) contrast(.9) blur(6px); scale: 1.08; transition: filter .6s var(--kurve), scale .6s var(--kurve); }
.medium::after { content: ''; position: absolute; inset: 0; background: linear-gradient(180deg, rgba(20, 20, 19, .25), rgba(20, 20, 19, .78)); transition: opacity .6s var(--kurve); z-index: 0; }
.sperre { position: absolute; inset: auto 0 0 0; z-index: 1; padding: clamp(16px, 2vw, 26px); color: var(--hell); display: grid; gap: 12px; transition: opacity var(--dauer) var(--kurve); }
.sperre .mono { color: var(--hell-leise); }
.sperre p.txt { font-size: 14px; line-height: 1.45; max-width: 38ch; color: var(--hell); }
.sperre .reihe { display: flex; align-items: center; gap: 16px; flex-wrap: wrap; }
.knopf-hell { height: 40px; padding: 0 16px; border-radius: 999px; border: 0; background: var(--hell); color: var(--tinte); font-weight: 500; font-size: 14px; cursor: pointer; transition: background-color var(--ui); }
.knopf-hell:hover, .knopf-hell.gedrueckt { background: #fff; }
.immer { display: flex; align-items: center; gap: 8px; font-size: 13px; color: var(--hell-leise); cursor: pointer; }
.immer input { width: 16px; height: 16px; accent-color: var(--hell); }
.film { position: absolute; inset: 0; z-index: 1; display: grid; place-items: center; opacity: 0; pointer-events: none; transition: opacity var(--dauer) var(--kurve); }
.film .spielen { width: 76px; height: 76px; border-radius: 50%; border: 0; background: var(--karte); display: grid; place-items: center; cursor: pointer; box-shadow: 0 18px 40px -14px rgba(20, 20, 19, .5); }
.film .spielen svg { width: 22px; height: 22px; margin-left: 4px; }
.film .unterzeile { position: absolute; left: 18px; right: 18px; bottom: 16px; display: flex; justify-content: space-between; color: var(--tinte); }
.film .unterzeile .mono { color: var(--tinte); background: rgba(250, 248, 243, .86); padding: 6px 9px; border-radius: 999px; }
.medium.frei img { filter: none; scale: 1; }
.medium.frei::after { opacity: 0; }
.medium.frei .sperre { opacity: 0; visibility: hidden; }
.medium.frei .film { opacity: 1; pointer-events: auto; }

.arbeiten { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; padding-bottom: 40px; }
.arbeit { display: grid; gap: 10px; text-decoration: none; }
.arbeit img { width: 100%; height: auto; aspect-ratio: 2 / 1; object-fit: cover; border-radius: 10px; }
.arbeit span { display: flex; justify-content: space-between; }
.fuss { display: flex; justify-content: space-between; gap: 16px; flex-wrap: wrap; padding: 28px 0 36px; border-top: 1px solid var(--linie); }
.fuss nav { display: flex; gap: 20px; flex-wrap: wrap; }
.fuss a, .fuss button { text-decoration: none; background: none; border: 0; padding: 0; cursor: pointer; font: inherit; letter-spacing: inherit; text-transform: inherit; color: var(--leise); }
.fuss a:hover, .fuss button:hover, .fuss button.gedrueckt { color: var(--tinte); }

/* ---------- Das Banner: erste Ebene, nicht modal ---------- */
.banner {
  position: fixed; z-index: 20; left: 50px; bottom: 50px;
  opacity: 0; translate: 0 12px; visibility: hidden;
  transition: opacity var(--dauer) var(--kurve), translate var(--dauer) var(--kurve), visibility 0s linear var(--dauer);
}
.banner.da { opacity: 1; translate: 0 0; visibility: visible; transition: opacity var(--dauer) var(--kurve), translate var(--dauer) var(--kurve), visibility 0s; }
.banner[hidden] { display: none; }
.banner h2 { font-size: 21px; font-weight: 600; letter-spacing: -.025em; line-height: 1.15; margin: 10px 0 8px; }
.banner h2 em { font-family: 'Instrument Serif', Georgia, serif; font-weight: 400; letter-spacing: 0; }
.banner p.txt { font-size: 14px; line-height: 1.5; }
.banner p.txt a, .banner .link { text-decoration: underline; text-underline-offset: .18em; text-decoration-thickness: 1px; background: none; border: 0; padding: 0; cursor: pointer; font: inherit; color: inherit; }
.banner .link.mono { font: 500 11px/1.35 'Geist Mono', ui-monospace, monospace; letter-spacing: .14em; color: var(--tinte); }
.banner .kopfzeile { display: flex; align-items: center; justify-content: space-between; }
.punkt { display: inline-block; width: 7px; height: 7px; border-radius: 50%; background: var(--akzent); margin-right: 8px; vertical-align: 1px; }
.aktionen { display: grid; gap: 8px; margin-top: 20px; }
/* Gleichwertig: gleiche Größe, gleiche Gestaltung, nur der Text unterscheidet */
.knopf { min-height: 44px; padding: 0 14px; border-radius: 999px; border: 1.5px solid currentColor; background: transparent; font-size: 14px; font-weight: 500; white-space: nowrap; cursor: pointer; transition: background-color var(--ui), color var(--ui), border-color var(--ui), scale var(--ui); }

/* Variante Karte (Prinzip Manual: drei gleiche Aktionen) */
.banner.karte { width: min(440px, calc(100vw - 100px)); padding: 28px; border-radius: 20px; background: var(--karte); border: 1px solid var(--linie); box-shadow: 0 30px 70px -28px rgba(20, 20, 19, .42); }
.banner.karte p.txt { color: var(--leise); }
.banner.karte .aktionen { grid-template-columns: repeat(3, 1fr); }
.banner.karte .knopf:hover, .banner.karte .knopf.gedrueckt { background: var(--tinte); color: var(--karte); border-color: var(--tinte); }

/* Variante Dunkel (Messwerte Dogstudio: 50 px Rand, max. 360 px, 40 px innen, Radius 20 px) */
.banner.dunkel { width: min(360px, calc(100vw - 100px)); padding: 40px; border-radius: 20px; background: var(--nacht); color: var(--hell); box-shadow: 0 30px 70px -28px rgba(14, 16, 26, .6); }
.banner.dunkel .mono { color: var(--hell-leise); }
.banner.dunkel p.txt { color: var(--hell-leise); }
.banner.dunkel .link { color: var(--hell); }
.banner.dunkel .aktionen { grid-template-columns: 1fr 1fr; margin-top: 24px; }
.banner.dunkel .knopf { background: var(--hell); color: var(--nacht); border-color: var(--hell); }
.banner.dunkel .knopf:hover, .banner.dunkel .knopf.gedrueckt { background: transparent; color: var(--hell); }
.banner.dunkel :focus-visible { outline-color: var(--hell); }

/* Variante Glas (Prinzip Bureau Borsche: Kategorien gleich auf der ersten Ebene) */
.banner.glas { left: 0; right: 0; margin-inline: auto; width: min(640px, calc(100vw - 100px)); padding: 28px; border-radius: 20px; background: rgba(250, 248, 243, .6); -webkit-backdrop-filter: blur(20px) saturate(1.5); backdrop-filter: blur(20px) saturate(1.5); border: 1px solid rgba(255, 255, 255, .7); box-shadow: 0 30px 70px -30px rgba(20, 20, 19, .45); }
.banner.glas p.txt { color: rgba(20, 20, 19, .74); max-width: 52ch; }
.banner.glas .aktionen { grid-template-columns: repeat(3, 1fr); }
.banner.glas .knopf:hover, .banner.glas .knopf.gedrueckt { background: var(--tinte); color: var(--karte); border-color: var(--tinte); }
.kats { display: grid; grid-template-columns: repeat(4, 1fr); gap: 6px; margin-top: 18px; }
.kat-klein { display: flex; flex-direction: column; justify-content: space-between; gap: 12px; padding: 12px; border-radius: 12px; background: rgba(250, 248, 243, .7); border: 1px solid rgba(20, 20, 19, .08); font-size: 13.5px; font-weight: 500; cursor: pointer; }

/* Schalter: echte Checkbox mit role="switch", Standard aus */
.schalter { appearance: none; -webkit-appearance: none; flex: none; width: 42px; height: 24px; border-radius: 999px; background: rgba(20, 20, 19, .2); position: relative; cursor: pointer; transition: background-color var(--ui) var(--kurve); }
.schalter::before { content: ''; position: absolute; top: 3px; left: 3px; width: 18px; height: 18px; border-radius: 50%; background: #fff; box-shadow: 0 1px 3px rgba(20, 20, 19, .3); transition: translate var(--ui) var(--kurve); }
.schalter:checked { background: var(--tinte); }
.schalter:checked::before { translate: 18px 0; }
.schalter:disabled { background: rgba(20, 20, 19, .42); cursor: not-allowed; }
.schalter.gedrueckt::before { scale: .85; }

/* ---------- Zweite Ebene: <dialog> mit showModal() (Manual: max. 32rem, Backdrop #1c1c1c66 + blur 4px) ---------- */
.dialog { width: calc(100% - 32px); max-width: 32rem; max-height: calc(100svh - 32px); margin: auto; padding: 0; border: 0; border-radius: 20px; background: var(--karte); color: var(--tinte); box-shadow: 0 40px 90px -30px rgba(20, 20, 19, .55); overflow: auto; }
.dialog::backdrop { background: rgba(28, 28, 28, .4); -webkit-backdrop-filter: blur(4px); backdrop-filter: blur(4px); }
.dialog[open] { animation: dlg-ein var(--dauer) var(--kurve); }
.dialog[open]::backdrop { animation: blende var(--dauer) var(--kurve); }
.dialog.geht { animation: dlg-aus 200ms var(--kurve) forwards; }
.dialog.geht::backdrop { animation: blende 200ms var(--kurve) reverse forwards; }
@keyframes dlg-ein { from { opacity: 0; translate: 0 12px; scale: .98; } }
@keyframes dlg-aus { to { opacity: 0; translate: 0 8px; scale: .98; } }
@keyframes blende { from { opacity: 0; } }
.dialog form { padding: 30px 30px 26px; }
.dialog .kopfzeile { display: flex; justify-content: space-between; align-items: start; gap: 16px; }
.dialog h2 { font-size: 24px; font-weight: 600; letter-spacing: -.03em; line-height: 1.1; margin-top: 10px; }
.dialog h2:focus { outline: none; }
.dialog h2 em { font-family: 'Instrument Serif', Georgia, serif; font-weight: 400; letter-spacing: 0; color: var(--akzent); }
.zu { width: 36px; height: 36px; flex: none; border-radius: 50%; border: 1px solid var(--linie); background: none; cursor: pointer; display: grid; place-items: center; transition: background-color var(--ui); }
.zu:hover { background: rgba(20, 20, 19, .06); }
.kat-liste { list-style: none; padding: 0; margin: 20px 0 22px; border-top: 1px solid var(--linie); }
.kat { display: flex; justify-content: space-between; align-items: center; gap: 20px; padding: 14px 0; border-bottom: 1px solid var(--linie); cursor: pointer; }
.kat b { display: block; font-size: 15px; font-weight: 600; letter-spacing: -.01em; }
.kat small { display: block; font-size: 13px; line-height: 1.45; color: var(--leise); margin-top: 3px; }
.kat .fest { white-space: nowrap; }
.dialog .aktionen { grid-template-columns: repeat(3, 1fr); margin-top: 0; }
.dialog .knopf:hover, .dialog .knopf.gedrueckt { background: var(--tinte); color: var(--karte); border-color: var(--tinte); }
.dialog .stand { margin-top: 14px; text-align: center; }

/* Rückmeldung nach der Entscheidung */
.toast { position: fixed; z-index: 15; right: 50px; bottom: 50px; max-width: 380px; padding: 16px 18px; border-radius: 14px; background: var(--tinte); color: var(--hell); display: grid; gap: 6px; opacity: 0; translate: 0 12px; pointer-events: none; transition: opacity var(--dauer) var(--kurve), translate var(--dauer) var(--kurve); }
.toast.da { opacity: 1; translate: 0 0; }
.toast p { font-size: 14px; line-height: 1.4; }
.toast .mono { color: var(--hell-leise); }

/* Gedachter Zeiger der Vorführung (als Popover, damit er über dem Dialog liegt) */
.zeiger { position: fixed; left: 0; top: 0; width: 20px; height: 20px; margin: 0; padding: 0; border: 0; background: none; overflow: visible; inset: 0 auto auto 0; pointer-events: none; z-index: 40; display: none; transition: transform .8s cubic-bezier(0.16, 1, 0.3, 1); }
.vorfuehrung .zeiger:popover-open, .vorfuehrung .zeiger.an { display: block; }

/* ---------- Vorschau: nur die Bühne ---------- */
.vorschau body { overflow: hidden; }
.vorschau .steuerung, .vorschau .fuss { display: none; }

/* ---------- Telefon: Bottom-Sheet in voller Breite ---------- */
@media (max-width: 760px) {
  .steuerung { gap: 8px; padding-block: 12px; }
  .steuerung .zeile > .mono:first-child, .werte .neben { display: none; }
  .nav ul li:not(:last-child) { display: none; }
  .held { grid-template-columns: 1fr; align-items: start; }
  .titel { font-size: clamp(44px, 13vw, 64px); }
  .unter { flex-direction: column; gap: 10px; }
  .arbeiten { grid-template-columns: 1fr 1fr; }
  .arbeit:nth-child(3) { display: none; }
  .banner, .banner.karte, .banner.dunkel, .banner.glas {
    left: 0; right: 0; bottom: 0; width: auto; margin: 0; max-height: 46svh; overflow-y: auto; overscroll-behavior: contain;
    padding: 22px 20px calc(18px + env(safe-area-inset-bottom)); border-radius: 20px 20px 0 0; translate: 0 100%; opacity: 1;
  }
  .banner.da { translate: 0 0; }
  .banner h2 { font-size: 19px; margin: 8px 0 6px; }
  .banner p.txt { font-size: 13.5px; }
  .aktionen { margin-top: 14px; }
  .banner.karte .aktionen, .banner.glas .aktionen { grid-template-columns: 1fr 1fr; }
  .banner.karte [data-aktion='einstellungen'], .banner.glas [data-aktion='auswahl'] { grid-column: 1 / -1; grid-row: 1; }
  .banner.dunkel .aktionen { margin-top: 16px; }
  .banner .lang { display: none; }
  .kats { grid-template-columns: 1fr 1fr; margin-top: 12px; }
  .kat-klein { flex-direction: row; align-items: center; padding: 9px 10px; gap: 8px; font-size: 13px; }
  .dialog form { padding: 24px 20px 20px; }
  .dialog .aktionen { grid-template-columns: 1fr 1fr; }
  .dialog [data-aktion='auswahl'] { grid-column: 1 / -1; grid-row: 1; }
  .toast { left: 16px; right: 16px; bottom: 16px; max-width: none; }
}
@media (hover: none) {
  .knopf { min-height: 48px; }
  .schalter { width: 48px; height: 28px; }
  .schalter::before { width: 22px; height: 22px; }
  .schalter:checked::before { translate: 20px 0; }
}

/* ---------- Reduzierte Bewegung: sofort da, nichts gleitet ---------- */
.reduziert .banner, .reduziert .banner.da, .reduziert .toast, .reduziert .medium img, .reduziert .medium::after, .reduziert .sperre, .reduziert .film, .reduziert .zeiger { transition: none; translate: 0 0; }
.reduziert .dialog[open], .reduziert .dialog[open]::backdrop, .reduziert .dialog.geht, .reduziert .dialog.geht::backdrop { animation: none; }
.reduziert .schalter, .reduziert .schalter::before { transition: none; }
</style>
</head>
<body>
<div class="steuerung">
  <div class="zeile">
    <p class="mono">Navigation · Consent-Banner</p>
    <div class="modi mono" role="group" aria-label="Variante">
      <button type="button" data-variante="karte" aria-pressed="true">Karte</button>
      <button type="button" data-variante="dunkel" aria-pressed="false">Dunkel</button>
      <button type="button" data-variante="glas" aria-pressed="false">Glas</button>
    </div>
    <span class="mono"><button type="button" class="neu" data-neu>Neu zeigen</button></span>
  </div>
  <p class="werte mono" data-werte aria-live="polite"></p>
</div>

<!-- Banner früh im DOM: per Tastatur gleich nach der Steuerung erreichbar, optisch unten fixiert -->
<section class="banner karte" data-banner aria-label="Cookie-Hinweis" hidden></section>

<div class="seite">
  <header class="nav">
    <a class="marke" href="#start" aria-label="Studio Feldmark, Startseite">
      <svg viewBox="0 0 22 22" aria-hidden="true"><rect x="1" y="1" width="20" height="20" rx="3" fill="none" stroke="currentColor" stroke-width="2"/><path d="M1 11h20M11 11v10" stroke="currentColor" stroke-width="2"/><circle cx="16" cy="16" r="2.2" fill="#b0441f"/></svg>
      <span>Feldmark <em>Studio</em></span>
    </a>
    <ul class="mono">
      <li><a href="#arbeiten">Arbeiten</a></li>
      <li><a href="#studio">Studio</a></li>
      <li><a href="#journal">Journal</a></li>
      <li><a href="#kontakt">Kontakt</a></li>
    </ul>
  </header>

  <main class="held" id="start">
    <div>
      <p class="mono ober">Gestaltung für Handwerk &amp; Orte · Kassel</p>
      <h1 class="titel">Marken, die man <em>anfassen</em> möchte.</h1>
      <div class="unter">
        <p>Erscheinungsbilder, Websites und Leitsysteme für Betriebe, die mit den Händen arbeiten. Seit 2014.</p>
        <p class="mono">24 Projekte</p>
      </div>
    </div>
    <figure class="medium" data-medium>
      <img src="bilder/keramik-stillleben.jpg" alt="Vasen und Schalen auf hellen Sockeln vor Terrakotta, Standbild aus dem Werkstattfilm" width="1200" height="800">
      <div class="sperre">
        <p class="mono">Externer Inhalt · Video · 2:14</p>
        <p class="txt">Werkstattfilm: Ein Tag in der Keramikwerkstatt Hain. Beim Abspielen überträgt Ihr Browser Daten an den Videoanbieter.</p>
        <div class="reihe">
          <button type="button" class="knopf-hell" data-video>Video laden</button>
          <label class="immer"><input type="checkbox" data-immer> Externe Medien immer erlauben</label>
        </div>
      </div>
      <div class="film" aria-hidden="true" data-film>
        <button type="button" class="spielen" aria-label="Werkstattfilm abspielen" tabindex="-1"><svg viewBox="0 0 22 22"><path d="M4 2.5 19 11 4 19.5Z" fill="#141413"/></svg></button>
        <div class="unterzeile"><span class="mono">Werkstattfilm</span><span class="mono">2:14</span></div>
      </div>
    </figure>
  </main>

  <section class="arbeiten" id="arbeiten" aria-label="Ausgewählte Arbeiten">
    <a class="arbeit" href="#krume"><img src="bilder/marke-krume.jpg" alt="Wortmarke der Backstube Krume in Dunkelbraun auf Senfgelb" width="1200" height="800" loading="lazy"><span class="mono"><span>Krume · Erscheinungsbild</span><span>2025</span></span></a>
    <a class="arbeit" href="#roestkontor"><img src="bilder/marke-roestkontor.jpg" alt="Wortmarke der Kaffeerösterei Röstkontor in Creme auf Ziegelrot" width="1200" height="800" loading="lazy"><span class="mono"><span>Rösterei · Verpackung</span><span>2024</span></span></a>
    <a class="arbeit" href="#eselsohr"><img src="bilder/marke-eselsohr.jpg" alt="Bildmarke der Buchhandlung Eselsohr auf Anthrazit" width="1200" height="800" loading="lazy"><span class="mono"><span>Eselsohr · Leitsystem</span><span>2023</span></span></a>
  </section>

  <footer class="fuss mono" id="kontakt">
    <span>© 2026 Studio Feldmark · Kassel</span>
    <nav aria-label="Rechtliches">
      <a href="#impressum">Impressum</a>
      <a href="#datenschutz">Datenschutz</a>
      <button type="button" data-einstellungen>Cookie-Einstellungen</button>
    </nav>
  </footer>
</div>

<!-- Zweite Ebene -->
<dialog class="dialog" data-dialog aria-labelledby="dlg-titel">
  <form method="dialog">
    <div class="kopfzeile">
      <div>
        <p class="mono"><span class="punkt"></span>Einstellungen</p>
        <h2 id="dlg-titel" tabindex="-1" autofocus>Welche Dienste dürfen <em>laden?</em></h2>
      </div>
      <button type="button" class="zu" data-zu aria-label="Schließen, nichts ändern"><svg width="12" height="12" viewBox="0 0 12 12" aria-hidden="true"><path d="M1 1l10 10M11 1 1 11" stroke="currentColor" stroke-width="1.6"/></svg></button>
    </div>
    <ul class="kat-liste">
      <li><label class="kat"><span><b>Notwendig</b><small>Sitzung, Sprache und diese Entscheidung. Ohne sie funktioniert die Seite nicht.</small></span><span class="mono fest">Immer an</span></label></li>
      <li><label class="kat"><span><b>Statistik</b><small>Zählt Besuche selbst gehostet und ohne Profilbildung.</small></span><input type="checkbox" role="switch" class="schalter" data-kat="statistik"></label></li>
      <li><label class="kat"><span><b>Marketing</b><small>Misst, ob unsere Anzeigen in sozialen Netzwerken ankommen.</small></span><input type="checkbox" role="switch" class="schalter" data-kat="marketing"></label></li>
      <li><label class="kat"><span><b>Externe Medien</b><small>Videos und Karten von Drittanbietern. Dabei wird Ihre IP-Adresse übertragen.</small></span><input type="checkbox" role="switch" class="schalter" data-kat="medien"></label></li>
    </ul>
    <div class="aktionen">
      <button type="button" class="knopf" data-aktion="ablehnen">Alle ablehnen</button>
      <button type="button" class="knopf" data-aktion="auswahl">Auswahl speichern</button>
      <button type="button" class="knopf" data-aktion="akzeptieren">Alle akzeptieren</button>
    </div>
    <p class="mono stand">Version 3 · Stand 10/2026 · <a href="#datenschutz">Datenschutz</a></p>
  </form>
</dialog>

<div class="toast" data-toast role="status" aria-live="polite"><p data-toast-text></p><span class="mono">Ändern im Footer · Cookie-Einstellungen</span></div>

<div class="zeiger" data-zeiger popover="manual" aria-hidden="true"><svg viewBox="0 0 20 20" width="20" height="20"><path d="M2.5 1.5 17 10l-6.6 1.4-3.2 6.1Z" fill="#141413" stroke="#faf8f3" stroke-width="1.3" stroke-linejoin="round"/></svg></div>

<!-- Dienste warten als text/plain und laufen erst nach Zustimmung -->
<script type="text/plain" data-kategorie="statistik">document.dispatchEvent(new CustomEvent('feldmark:dienst', { detail: 'statistik' }));</script>
<script type="text/plain" data-kategorie="marketing">document.dispatchEvent(new CustomEvent('feldmark:dienst', { detail: 'marketing' }));</script>
<script type="text/plain" data-kategorie="medien">document.dispatchEvent(new CustomEvent('feldmark:dienst', { detail: 'medien' }));</script>

<script>
(() => {
  const html = document.documentElement;
  const reduziert = () => html.classList.contains('reduziert');
  const $ = s => document.querySelector(s);
  const banner = $('[data-banner]'), dialog = $('[data-dialog]'), medium = $('[data-medium]');
  const toast = $('[data-toast]'), toastText = $('[data-toast-text]'), werte = $('[data-werte]'), zeiger = $('[data-zeiger]');
  const DAUER = 300, VERZOEGERUNG = 500, VERSION = 3, SCHLUESSEL = 'feldmark.einwilligung';
  const NAMEN = { notwendig: 'Notwendig', statistik: 'Statistik', marketing: 'Marketing', medien: 'Externe Medien' };
  const VARIANTEN = ['karte', 'dunkel', 'glas'];

  // Erste Ebene je Variante. Ablehnen und Akzeptieren stehen immer gleichwertig nebeneinander.
  const TEXT = 'Notwendige Cookies halten die Seite am Laufen. Mit Ihrer Zustimmung zählen wir Besuche und zeigen Videos externer Anbieter.';
  const VORLAGEN = {
    karte: `
      <p class="mono"><span class="punkt"></span>Cookies &amp; Dienste</p>
      <h2>Kurz zu Ihren <em>Daten.</em></h2>
      <p class="txt">${TEXT} <span class="lang">Widerruf jederzeit im Footer. <a href="#datenschutz">Datenschutz</a></span></p>
      <div class="aktionen">
        <button type="button" class="knopf" data-aktion="einstellungen">Einstellungen</button>
        <button type="button" class="knopf" data-aktion="ablehnen">Ablehnen</button>
        <button type="button" class="knopf" data-aktion="akzeptieren">Akzeptieren</button>
      </div>`,
    dunkel: `
      <p class="mono">Cookies</p>
      <h2>Dürfen wir <em>zählen?</em></h2>
      <p class="txt">Wir würden gern Besuche messen und Videos externer Anbieter zeigen. Notwendiges läuft immer. Details in den <button type="button" class="link" data-aktion="einstellungen">Einstellungen</button>.</p>
      <div class="aktionen">
        <button type="button" class="knopf" data-aktion="ablehnen">Ablehnen</button>
        <button type="button" class="knopf" data-aktion="akzeptieren">Akzeptieren</button>
      </div>`,
    glas: `
      <div class="kopfzeile"><p class="mono"><span class="punkt"></span>Cookie-Einstellungen</p><button type="button" class="link mono" data-aktion="einstellungen">Details</button></div>
      <h2>Sie entscheiden, was <em>geladen</em> wird.</h2>
      <p class="txt lang">${TEXT}</p>
      <div class="kats" role="group" aria-label="Kategorien">
        <label class="kat-klein">Notwendig <input type="checkbox" role="switch" class="schalter" checked disabled></label>
        <label class="kat-klein">Statistik <input type="checkbox" role="switch" class="schalter" data-kat="statistik"></label>
        <label class="kat-klein">Marketing <input type="checkbox" role="switch" class="schalter" data-kat="marketing"></label>
        <label class="kat-klein">Ext. Medien <input type="checkbox" role="switch" class="schalter" data-kat="medien"></label>
      </div>
      <div class="aktionen">
        <button type="button" class="knopf" data-aktion="ablehnen">Ablehnen</button>
        <button type="button" class="knopf" data-aktion="auswahl">Auswahl speichern</button>
        <button type="button" class="knopf" data-aktion="akzeptieren">Alle akzeptieren</button>
      </div>`,
  };
  const KURVE = 'cubic-bezier(.25,.46,.45,.94)';
  const WERTE = {
    karte:  [['Kurve', KURVE], ['Einblenden', '300 ms'], ['Rand', '50 px'], ['Breite', 'max. 440 px'], ['Radius', '20 px'], ['Dialog', 'max. 32rem · Blur 4 px'], ['Vorbild', 'Manual']],
    dunkel: [['Kurve', KURVE], ['Einblenden', '300 ms'], ['Rand', '50 px'], ['Breite', 'max. 360 px'], ['Innen', '40 px'], ['Radius', '20 px'], ['Vorbild', 'Dogstudio']],
    glas:   [['Kurve', KURVE], ['Einblenden', '300 ms'], ['Glas', 'blur 20 px · saturate 1.5'], ['Breite', 'max. 640 px'], ['Radius', '20 px'], ['Vorbild', 'Bureau Borsche, Ablehnen ergänzt']],
  };

  let variante = 'karte', entschieden = false, wahl = leer(), geladen = new Set(), toastUhr = null, zeigUhr = null, zeigMarke = 0;
  function leer() { return { statistik: false, marketing: false, medien: false }; }

  // ---------- Anzeige der Werte und des Status ----------
  function zeigeWerte() {
    const w = reduziert() ? [['Reduzierte Bewegung', 'sofort da, ohne Gleiten']] : WERTE[variante];
    const an = Object.keys(wahl).filter(k => wahl[k]).map(k => NAMEN[k].toLowerCase());
    const status = entschieden ? `v${VERSION} · notwendig${an.length ? ', ' + an.join(', ') : ''}` : 'offen';
    const dienste = ['notwendig', ...geladen].join(', ');
    werte.innerHTML = w.map(([k, v], i) => `<span${i > 1 ? ' class="neben"' : ''}>${k} <b>${v}</b></span>`).join('')
      + `<span class="status">Einwilligung <b>${status}</b></span><span>Geladen <b>${dienste}</b></span>`;
  }

  // ---------- Banner ----------
  function bannerBauen() {
    banner.className = 'banner ' + variante;
    banner.innerHTML = VORLAGEN[variante];
    banner.querySelectorAll('[data-kat]').forEach(s => { s.checked = wahl[s.dataset.kat]; });
  }
  function bannerZeigen() {
    clearTimeout(zeigUhr);
    banner.hidden = false;
    banner.getBoundingClientRect();           // Ausgangslage festschreiben, dann blendet die Klasse ein
    banner.classList.add('da');
  }
  function bannerZeigenNachPaint() {
    // erst nach dem ersten Paint, damit das Banner den LCP nicht verschiebt
    clearTimeout(zeigUhr);
    const m = ++zeigMarke;
    requestAnimationFrame(() => requestAnimationFrame(() => { if (m === zeigMarke) zeigUhr = setTimeout(bannerZeigen, reduziert() ? 0 : VERZOEGERUNG); }));
  }
  function bannerWeg() {
    banner.classList.remove('da');
    setTimeout(() => { if (!banner.classList.contains('da')) banner.hidden = true; }, reduziert() ? 0 : DAUER);
  }
  // Fokus-Elemente der Seite nicht verdecken (WCAG 2.4.11): Scroll-Abstand = Bannerhöhe
  new ResizeObserver(() => html.style.setProperty('--banner-h', banner.classList.contains('da') ? banner.offsetHeight + 'px' : '0px')).observe(banner);

  // ---------- Dialog ----------
  function dialogAuf() {
    dialog.querySelectorAll('[data-kat]').forEach(s => { s.checked = wahl[s.dataset.kat]; });
    // im Glas-Banner schon gesetzte Schalter übernehmen
    banner.querySelectorAll('[data-kat]').forEach(s => { dialog.querySelector(`[data-kat="${s.dataset.kat}"]`).checked = s.checked; });
    dialog.classList.remove('geht');
    if (!dialog.open) dialog.showModal();
    zeigerNachVorn();
  }
  function dialogZu() {
    if (!dialog.open) return;
    if (reduziert()) return dialog.close();
    dialog.classList.add('geht');
    setTimeout(() => { dialog.classList.remove('geht'); dialog.close(); }, 200);
  }
  dialog.addEventListener('cancel', e => { e.preventDefault(); dialogZu(); });   // Escape: animiert schließen, nichts ändern
  dialog.addEventListener('click', e => { if (e.target === dialog) dialogZu(); });

  // ---------- Entscheidung ----------
  function lies(bereich) {
    const w = leer();
    bereich.querySelectorAll('[data-kat]').forEach(s => { w[s.dataset.kat] = s.checked; });
    return w;
  }
  function entscheiden(art, quelle) {
    wahl = art === 'akzeptieren' ? { statistik: true, marketing: true, medien: true } : art === 'ablehnen' ? leer() : lies(quelle);
    entschieden = true;
    try { localStorage.setItem(SCHLUESSEL, JSON.stringify({ version: VERSION, zeit: new Date().toISOString(), kategorien: { notwendig: true, ...wahl } })); } catch (e) {}
    dienstAktivieren();
    medium.classList.toggle('frei', wahl.medien || medium.dataset.einmal === 'ja');
    bannerWeg(); dialogZu();
    const an = Object.keys(wahl).filter(k => wahl[k]);
    melden(an.length === 3 ? 'Gespeichert: alle Dienste erlaubt.' : an.length ? `Gespeichert: Notwendig, ${an.map(k => NAMEN[k]).join(', ')}.` : 'Gespeichert: nur notwendige Cookies.');
    zeigeWerte();
  }
  // text/plain-Skripte der erlaubten Kategorien als echtes Skript neu einsetzen
  function dienstAktivieren() {
    document.querySelectorAll('script[type="text/plain"][data-kategorie]').forEach(alt => {
      const k = alt.dataset.kategorie;
      if (!wahl[k] || geladen.has(k)) return;
      const neu = document.createElement('script');
      neu.textContent = alt.textContent;
      alt.after(neu); neu.remove();
    });
  }
  document.addEventListener('feldmark:dienst', e => { geladen.add(e.detail); zeigeWerte(); });

  function melden(text) {
    clearTimeout(toastUhr);
    toastText.textContent = text;
    toast.classList.add('da');
    toastUhr = setTimeout(() => toast.classList.remove('da'), 3200);
  }

  // Klicks auf beiden Ebenen
  function aktion(e, bereich) {
    const b = e.target.closest('[data-aktion]');
    if (!b) return;
    if (e.isTrusted) halt();
    const a = b.dataset.aktion;
    if (a === 'einstellungen') dialogAuf();
    else entscheiden(a, bereich);
  }
  banner.addEventListener('click', e => aktion(e, banner));
  dialog.addEventListener('click', e => aktion(e, dialog));
  $('[data-zu]').addEventListener('click', e => { if (e.isTrusted) halt(); dialogZu(); });
  $('[data-einstellungen]').addEventListener('click', e => { if (e.isTrusted) halt(); dialogAuf(); });
  banner.addEventListener('change', e => { if (e.isTrusted) halt(); });

  // Zwei-Klick-Platzhalter: einmal laden ohne Speichern, oder dauerhaft erlauben
  $('[data-video]').addEventListener('click', e => {
    if (e.isTrusted) halt();
    if ($('[data-immer]').checked) {
      wahl.medien = true; entschieden = true;
      try { localStorage.setItem(SCHLUESSEL, JSON.stringify({ version: VERSION, zeit: new Date().toISOString(), kategorien: { notwendig: true, ...wahl } })); } catch (er) {}
      dienstAktivieren(); bannerWeg(); melden('Gespeichert: Externe Medien erlaubt.');
    } else {
      medium.dataset.einmal = 'ja';
      melden('Video einmalig geladen. Nichts gespeichert.');
    }
    medium.classList.add('frei');
    zeigeWerte();
  });

  // Varianten-Umschalter und Neustart
  function setzeVariante(v) {
    variante = v;
    document.querySelectorAll('[data-variante]').forEach(b => b.setAttribute('aria-pressed', String(b.dataset.variante === v)));
  }
  function zuruecksetzen() {
    clearTimeout(toastUhr); clearTimeout(zeigUhr); zeigMarke++;
    if (dialog.open) { dialog.classList.remove('geht'); dialog.close(); }
    toast.classList.remove('da');
    entschieden = false; wahl = leer(); geladen = new Set();
    medium.classList.remove('frei'); delete medium.dataset.einmal; $('[data-immer]').checked = false;
    banner.classList.remove('da'); banner.hidden = true;
    bannerBauen(); zeigeWerte();
  }
  document.querySelector('.modi').addEventListener('click', e => {
    const b = e.target.closest('[data-variante]');
    if (!b) return;
    halt(); setzeVariante(b.dataset.variante); zuruecksetzen(); bannerZeigenNachPaint();
  });
  $('[data-neu]').addEventListener('click', () => neustart());

  // ---------- Vorführung: ein gedachter Zeiger trifft Entscheidungen ----------
  const ABBRUCH = Symbol('abbruch');
  let lauf = 0, pausiert = false, laeuft = false;
  function warte(ms, g) {
    return new Promise((ok, nein) => {
      let rest = ms, t0 = performance.now();
      const tick = () => {
        if (g !== lauf) return nein(ABBRUCH);
        const t = performance.now();
        if (!pausiert) rest -= t - t0;
        t0 = t;
        rest <= 0 ? ok() : setTimeout(tick, 40);
      };
      tick();
    });
  }
  function zeigerNachVorn() {
    if (!html.classList.contains('vorfuehrung') || !zeiger.showPopover) return;
    try { if (zeiger.matches(':popover-open')) zeiger.hidePopover(); zeiger.showPopover(); } catch (e) { zeiger.classList.add('an'); }
  }
  function zeigerAuf(x, y, sofort) {
    if (sofort) zeiger.style.transition = 'none';
    zeiger.style.transform = `translate(${x}px, ${y}px)`;
    if (sofort) { zeiger.getBoundingClientRect(); zeiger.style.transition = ''; }
  }
  async function tippe(el, g) {
    const r = el.getBoundingClientRect();
    zeigerAuf(r.left + r.width * .55, r.top + r.height * .55);
    await warte(reduziert() ? 450 : 850, g);
    el.classList.add('gedrueckt');
    await warte(170, g);
    el.classList.remove('gedrueckt');
    el.click();
  }
  async function schleife(g) {
    let i = VARIANTEN.indexOf(variante);
    const q = s => banner.querySelector(s), d = s => dialog.querySelector(s);
    try {
      for (;;) {
        const v = VARIANTEN[i++ % VARIANTEN.length];
        setzeVariante(v); zuruecksetzen();
        zeigerAuf(innerWidth * .78, innerHeight * .9, true);
        zeigerNachVorn();
        await warte(500, g);
        bannerZeigen();
        await warte(1200, g);
        if (v === 'karte') {
          await tippe(q('[data-aktion="einstellungen"]'), g);
          await warte(700, g);
          await tippe(d('[data-kat="statistik"]'), g);
          await warte(350, g);
          await tippe(d('[data-kat="medien"]'), g);
          await warte(450, g);
          await tippe(d('[data-aktion="auswahl"]'), g);
        } else if (v === 'dunkel') {
          await tippe(q('[data-aktion="ablehnen"]'), g);
          await warte(1100, g);
          await tippe($('[data-video]'), g);
        } else {
          await tippe(q('[data-kat="medien"]'), g);
          await warte(350, g);
          await tippe(q('[data-kat="statistik"]'), g);
          await warte(450, g);
          await tippe(q('[data-aktion="auswahl"]'), g);
        }
        await warte(400, g);
        zeigerAuf(innerWidth * .78, innerHeight * .9);
        await warte(2600, g);
      }
    } catch (e) { if (e !== ABBRUCH) throw e; }
  }
  function halt() {
    lauf++; laeuft = false; pausiert = false;
    html.classList.remove('vorfuehrung');
    try { if (zeiger.matches(':popover-open')) zeiger.hidePopover(); } catch (e) {}
    zeiger.classList.remove('an');
  }

  function neustart() { halt(); zuruecksetzen(); bannerZeigenNachPaint(); }
  function vorfuehren() {
    halt();
    html.classList.add('vorfuehrung');
    laeuft = true;
    schleife(lauf);
  }
  function pause() { pausiert = true; }
  function weiter() {
    if (laeuft && html.classList.contains('vorfuehrung')) pausiert = false;
    else vorfuehren();
  }

  setzeVariante(variante); zuruecksetzen(); bannerZeigenNachPaint();
  addEventListener('load', zeigeWerte);   // ?reduziert setzt die Klasse erst nach diesem Skript

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