Fehlerseite und Leerzustände
404-Seite, Suche ohne Treffer und leere Bereiche mit Haltung: klare Überschrift ohne Schuld, „Meintest du“ aus Adresse oder Tippfehler, Filter-Hinweise mit Trefferzahl, Leerzustand erst nach dem Laden und mit erstem Schritt.
Jede Website: eigene Vorlage für tote Links, umbenannte Kurse, Produkte oder Leistungen; mit „Meintest du“ aus der Adresse.
Drei Zustände, die jede Seite hat und die oft vergessen werden: die 404-Seite, die Suche ohne Treffer und leere Bereiche wie eine leere Merkliste oder „noch kein Kurs“. Jeder Zustand sagt klar, was los ist, ohne Schuld beim Besucher, und bietet einen echten Ausweg: Suche mit Vorschlägen, ein „Meintest du“, beliebte Ziele, Kontakt. Leere Bereiche erklären, wie sie sich füllen, und zeigen den ersten Schritt.
Ein Fehler oder eine leere Fläche ist ein Moment, in dem Besucher abspringen. Eine Seite mit Haltung und einem passenden Vorschlag macht daraus einen Umweg statt einer Sackgasse. Baymard nennt Seiten ohne Treffer ohne Weiterweg eine unnötige Sackgasse; laut NN/g sollen leere Zustände den Systemstatus zeigen, etwas lehren und direkt zur nächsten Aufgabe führen.
Wann einsetzen
Auf jeder Website: 404-Seite als eigene Vorlage. Bei Suche und Filtern (Kurse, Produkte, Termine, Stellen). In Kundenbereichen mit Merkliste, Warenkorb, Buchungen oder Lernfortschritt.
Wann nicht
Witzige Fehlerseiten ohne Ausweg. „Hoppla“-Sprache bei ernsten Themen (Praxis, Kanzlei, Behörde). Leerzustände, die während des Ladens erscheinen: erst laden, dann „leer“ melden.
Varianten
- 404-Seite mit Suche, „Meintest du“ aus der falschen Adresse und beliebten Zielen
- Suche ohne Treffer mit Schreibkorrektur, Filter-Hinweisen und beliebten Themen
- Leerer Bereich (Merkliste, Meine Kurse) mit Lernhinweis und erstem Schritt
- Fehler beim Laden mit „Erneut versuchen“ (Netzwerk, Server)
Umsetzung
404: Der Server liefert Status 404 (nicht 200 mit Fehlertext) und einen eigenen title. Aus dem Pfad der falschen Adresse Wörter bilden und mit Kurs-Titeln abgleichen; der beste Treffer wird als „Meintest du“ angeboten, bei sicherem Treffer besser serverseitig umleiten (301). Suche mit role=combobox, aria-expanded und Vorschlagsliste (role=listbox), Pfeiltasten und Enter. Keine Treffer: Wörter mit Levenshtein-Abstand ≤ 2 gegen den Wortschatz der Kurse prüfen und „Meintest du“ mit Trefferzahl anbieten; für jeden aktiven Filter die Zahl „ohne diesen Filter“ zeigen. Trefferzahl in aria-live=polite. Leerer Bereich: erst Skeleton mit aria-busy, dann Leerzustand mit Erklärung, Umrissen der künftigen Inhalte und einem Hauptknopf. Füllt sich der Bereich (Herz bei einem Vorschlag), wechselt er ohne Seitenwechsel. Texte in Haltung der Marke, aber ohne Schuld beim Besucher.
Robust umsetzen
Telefon. Eine Spalte. Suchfeld oben und groß, Vorschläge als Liste mit großen Zeilen. Beliebte Ziele als Liste statt Kacheln. Die Leerzustände stehen untereinander, der Hauptknopf in voller Breite.
Ohne Bewegung. Zustände wechseln ohne Einblenden und ohne Feder, Skeleton ohne Schimmer.
Barrierefrei. Eine h1 je Zustand, die sagt, was passiert ist. Trefferzahl und Zustandswechsel per aria-live=polite. Suche als Combobox mit Pfeiltasten, Enter und Esc. Skeleton mit aria-busy und unsichtbarem „Lädt“. Illustrationen sind dekorativ (aria-hidden). Keine Farbe als einziges Signal, keine roten Warntexte. Fokus nach dem Wechsel auf die neue Überschrift.
Code
<!DOCTYPE html>
<!--
Fehlerseite und Leerzustände
Fundus · Sektionen & Funktionen · https://cecodesign.de/lab/fundus/sektionen/fehlerseite-leerzustand/
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>Fehlerseite und Leerzustände · 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: 'Bricolage Grotesque'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/bricolage-grotesque:vf@latest/latin-standard-normal.woff2') format('woff2-variations'); font-weight: 200 800; font-stretch: 75% 100%; font-display: swap; }
@font-face { font-family: 'Onest'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/onest:vf@latest/latin-wght-normal.woff2') format('woff2-variations'); font-weight: 100 900; font-display: swap; }
@font-face { font-family: 'Shantell Sans'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/shantell-sans:vf@latest/latin-full-normal.woff2') format('woff2-variations'); font-weight: 300 800; font-display: swap; }
@font-face { font-family: 'Geist Mono'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/geist-mono:vf@latest/latin-wght-normal.woff2') format('woff2-variations'); font-weight: 100 900; font-display: swap; }
/* Rezept „Online-Kurs & Coaching: klarer Lernweg“: heller kühler Grund, fast schwarze Tinte, ein Markenton, Grün nur für Erfolg */
:root {
--grund: #f3f5f9; --karte: #ffffff; --tinte: #141414; --leise: #5d6372; --linie: #dfe3ec; --linie-stark: #c9cfdc;
--marke: #4f5fe0; --marke-hell: #e6e9ff; --erfolg: #10a86c; --gelb: #f7c325;
--kurve: cubic-bezier(0.22, 1, 0.36, 1); /* easeOutQuint, gemessen bei Brilliant */
--feder: cubic-bezier(0.34, 1.56, 0.64, 1); /* nur für Erfolgsmomente */
--ui: 150ms; --wechsel: 300ms; --ein: 500ms;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
html { background: var(--grund); color: var(--tinte); font: 400 17px/1.6 'Onest', system-ui, sans-serif; -webkit-font-smoothing: antialiased; }
body { min-height: 100svh; display: flex; flex-direction: column; }
button, input { font: inherit; color: inherit; }
button { background: none; border: 0; cursor: pointer; }
a { color: inherit; }
h1, h2, h3 { font-family: 'Bricolage Grotesque', system-ui, sans-serif; font-optical-sizing: auto; letter-spacing: -.02em; }
.notiz { font-family: 'Shantell Sans', cursive; font-variation-settings: 'INFM' 40, 'BNCE' 0; font-size: .9375rem; color: var(--marke); }
.label { font-size: 13px; font-weight: 600; letter-spacing: .06em; text-transform: uppercase; color: var(--leise); }
:focus-visible { outline: 3px solid var(--marke); outline-offset: 3px; border-radius: 6px; }
.unsichtbar { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
/* ── Kopf ─────────────────────────────────────────── */
.kopf { display: flex; align-items: center; justify-content: space-between; gap: 20px; padding: 18px clamp(18px, 4vw, 48px); background: var(--grund); }
.logo { display: flex; align-items: center; gap: 10px; text-decoration: none; font: 700 20px/1 'Bricolage Grotesque', sans-serif; letter-spacing: -.02em; }
.logo svg { width: 30px; height: 30px; }
.nav { display: flex; align-items: center; gap: 6px; list-style: none; font-size: 15px; }
.nav a { display: inline-flex; align-items: center; gap: 8px; padding: 8px 12px; border-radius: 999px; text-decoration: none; transition: background-color var(--ui) var(--kurve); }
.nav a:hover { background: var(--karte); }
.nav a[aria-current] { background: var(--karte); font-weight: 600; box-shadow: inset 0 0 0 1px var(--linie); }
.zahl { min-width: 22px; height: 22px; padding: 0 6px; border-radius: 11px; display: inline-grid; place-items: center; font-size: 12px; font-weight: 700; background: var(--linie); color: var(--leise); font-variant-numeric: tabular-nums; transition: background-color var(--wechsel) var(--kurve), color var(--wechsel) var(--kurve); }
.zahl.voll { background: var(--marke); color: #fff; }
main { flex: 1; width: min(100% - 2 * clamp(18px, 4vw, 48px), 1160px); margin: 0 auto; padding: clamp(24px, 5vh, 56px) 0 72px; }
.zustand { display: none; }
body[data-v="404"] [data-zustand="404"], body[data-v="keine-treffer"] [data-zustand="keine-treffer"], body[data-v="leer"] [data-zustand="leer"] { display: block; }
[data-ein] { animation: ein var(--ein) var(--kurve) both; animation-delay: calc(var(--i, 0) * 60ms); }
@keyframes ein { from { opacity: 0; translate: 0 12px; } }
/* Knöpfe */
.knopf { display: inline-flex; align-items: center; justify-content: center; gap: 10px; padding: 13px 20px; border-radius: 12px; font-weight: 600; font-size: 16px; text-decoration: none; background: var(--marke); color: #fff; transition: background-color var(--ui) var(--kurve), translate var(--ui) var(--kurve); }
.knopf:hover { background: #3f4fd0; }
.knopf:active { translate: 0 1px; }
.knopf.zweit { background: var(--karte); color: var(--tinte); box-shadow: inset 0 0 0 1.5px var(--linie-stark); }
.knopf.zweit:hover { background: #fff; box-shadow: inset 0 0 0 1.5px var(--tinte); }
/* ── Zustand 404 ──────────────────────────────────── */
.fehler-raster { display: grid; grid-template-columns: minmax(0, 7fr) minmax(0, 5fr); gap: clamp(28px, 5vw, 72px); align-items: start; }
.fehler-raster h1 { font-size: clamp(2.25rem, 1.4rem + 3.4vw, 4.25rem); line-height: 1.05; font-weight: 700; font-variation-settings: 'opsz' 96; margin: 14px 0 18px; text-wrap: balance; }
.fehler-raster .lead { font-size: 1.125rem; color: var(--leise); max-width: 52ch; }
.adresse { display: flex; flex-wrap: wrap; gap: 4px 10px; align-items: baseline; margin-top: 20px; font-size: 14px; color: var(--leise); }
.adresse code { font: 500 13.5px/1.4 'Geist Mono', ui-monospace, monospace; color: var(--tinte); background: var(--karte); padding: 3px 8px; border-radius: 6px; box-shadow: inset 0 0 0 1px var(--linie); word-break: break-all; }
.meintest { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 6px 20px; align-items: center; margin-top: 28px; padding: 18px 20px; background: var(--karte); border-radius: 16px; box-shadow: inset 0 0 0 1px var(--linie); }
.meintest .label { grid-column: 1 / -1; color: var(--marke); }
.meintest h2 { font-size: 1.375rem; font-weight: 650; line-height: 1.2; }
.meintest p { font-size: 15px; color: var(--leise); }
.meintest .knopf { grid-row: 2 / span 2; grid-column: 2; }
/* Suche als Combobox */
.suchfeld { position: relative; margin-top: 30px; max-width: 560px; }
.suchfeld label { display: block; font-weight: 600; margin-bottom: 8px; }
.eingabe { display: flex; align-items: center; gap: 10px; padding: 0 16px; height: 56px; background: var(--karte); border-radius: 14px; box-shadow: inset 0 0 0 1.5px var(--linie-stark); transition: box-shadow var(--ui) var(--kurve); }
.eingabe:focus-within { box-shadow: inset 0 0 0 2px var(--marke), 0 0 0 4px var(--marke-hell); }
.eingabe svg { width: 20px; height: 20px; flex: none; color: var(--leise); }
.eingabe input { flex: 1; min-width: 0; border: 0; background: none; outline: none; font-size: 17px; height: 100%; }
.vorschlaege { position: absolute; z-index: 5; left: 0; right: 0; top: calc(100% + 6px); list-style: none; background: var(--karte); border-radius: 14px; box-shadow: 0 18px 40px -16px rgba(20, 20, 40, .28), inset 0 0 0 1px var(--linie); padding: 6px; display: none; }
.vorschlaege.offen { display: block; animation: ein var(--wechsel) var(--kurve) both; }
.vorschlaege li { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 2px 14px; padding: 10px 12px; border-radius: 10px; cursor: pointer; }
.vorschlaege li small { grid-column: 1; font-size: 13px; color: var(--leise); }
.vorschlaege li em { grid-row: 1 / span 2; grid-column: 2; align-self: center; font-style: normal; font-size: 14px; font-weight: 600; font-variant-numeric: tabular-nums; }
.vorschlaege li[aria-selected="true"] { background: var(--marke-hell); }
.vorschlaege mark { background: none; color: var(--marke); font-weight: 700; }
.vorschlaege .leer-zeile { color: var(--leise); cursor: default; }
.ziele { margin-top: 40px; }
.ziele ul { list-style: none; display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; margin-top: 12px; }
.ziele a { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 0 12px; padding: 14px 16px; border-radius: 14px; background: var(--karte); text-decoration: none; box-shadow: inset 0 0 0 1px var(--linie); transition: box-shadow var(--ui) var(--kurve), translate var(--ui) var(--kurve); }
.ziele a:hover { box-shadow: inset 0 0 0 1.5px var(--tinte); translate: 0 -2px; }
.ziele b { font-weight: 600; }
.ziele small { grid-column: 1; font-size: 13.5px; color: var(--leise); }
.ziele a::after { content: '→'; grid-row: 1 / span 2; grid-column: 2; align-self: center; color: var(--marke); }
.bild-404 { position: relative; }
.bild-404 svg { display: block; width: 100%; height: auto; }
.bild-404 .weg { stroke-dasharray: 6 9; animation: laufen 1.6s linear 3; } /* 3 × 1,6 s = 4,8 s, dann still (WCAG 2.2.2) */
.vorfuehrung .bild-404 .weg { animation-iteration-count: infinite; } /* Galerie-Vorschau: läuft weiter, pause() hält an */
@keyframes laufen { to { stroke-dashoffset: -30; } }
/* ── Zustand keine Treffer ────────────────────────── */
.suchkopf h1 { font-size: clamp(1.75rem, 1.2rem + 2vw, 2.75rem); font-weight: 650; line-height: 1.1; }
.sucheingabe { display: flex; gap: 10px; margin-top: 18px; max-width: 720px; }
.sucheingabe .eingabe { flex: 1; }
.filter { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin-top: 16px; }
.chip { display: inline-flex; align-items: center; gap: 8px; padding: 7px 14px; border-radius: 999px; font-size: 14.5px; background: var(--karte); box-shadow: inset 0 0 0 1px var(--linie-stark); transition: background-color var(--ui) var(--kurve), color var(--ui) var(--kurve), box-shadow var(--ui) var(--kurve); }
.chip:hover { box-shadow: inset 0 0 0 1.5px var(--tinte); }
.chip[aria-pressed="true"] { background: var(--tinte); color: #fff; box-shadow: none; }
.chip[aria-pressed="true"]::after { content: '×'; font-size: 16px; line-height: 1; opacity: .7; }
.treffer-zeile { margin: 26px 0 14px; display: flex; justify-content: space-between; gap: 16px; font-size: 15px; color: var(--leise); border-bottom: 1px solid var(--linie); padding-bottom: 10px; }
.treffer-zeile b { color: var(--tinte); font-variant-numeric: tabular-nums; }
.karten { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 16px; list-style: none; }
.kurs { position: relative; display: grid; gap: 8px; align-content: start; padding: 18px 18px 16px; background: var(--karte); border-radius: 16px; box-shadow: inset 0 0 0 1px var(--linie); }
.kurs .modul { font-family: 'Shantell Sans', cursive; font-size: 14px; color: var(--leise); font-variation-settings: 'INFM' 40, 'BNCE' 0; }
.kurs h3 { font-size: 1.1875rem; font-weight: 650; line-height: 1.2; padding-right: 36px; }
.kurs .meta { font-size: 14px; color: var(--leise); }
.kurs .preis { font-weight: 700; font-variant-numeric: tabular-nums; margin-top: 4px; }
.kurs .start { justify-self: start; font-size: 12.5px; font-weight: 700; padding: 3px 9px; border-radius: 999px; background: var(--gelb); }
.herz { position: absolute; top: 12px; right: 12px; width: 40px; height: 40px; display: grid; place-items: center; border-radius: 50%; transition: background-color var(--ui) var(--kurve); }
.herz:hover { background: var(--grund); }
.herz svg { width: 22px; height: 22px; transition: scale 450ms var(--feder); }
.herz path { fill: none; stroke: var(--tinte); stroke-width: 1.8; transition: fill var(--ui) var(--kurve), stroke var(--ui) var(--kurve); }
.herz[aria-pressed="true"] path { fill: var(--marke); stroke: var(--marke); }
.herz[aria-pressed="true"] svg { scale: 1.12; }
.herz.puls svg { animation: puls 450ms var(--feder); }
@keyframes puls { 40% { scale: 1.35; } }
.leer-panel { display: grid; grid-template-columns: 180px minmax(0, 1fr); gap: 28px 40px; padding: clamp(22px, 3vw, 36px); background: var(--karte); border-radius: 20px; box-shadow: inset 0 0 0 1px var(--linie); }
.leer-panel svg { width: 100%; height: auto; }
.leer-panel h2 { font-size: clamp(1.5rem, 1.1rem + 1.2vw, 2rem); font-weight: 650; line-height: 1.15; }
.leer-panel p { color: var(--leise); margin-top: 8px; max-width: 54ch; }
.wege { display: grid; gap: 10px; margin-top: 20px; justify-items: start; }
.weg-knopf { display: inline-flex; align-items: baseline; gap: 10px; padding: 10px 14px; border-radius: 12px; background: var(--marke-hell); color: var(--tinte); text-align: left; transition: background-color var(--ui) var(--kurve); }
.weg-knopf:hover { background: #d6dbff; }
.weg-knopf b { color: var(--marke); }
.weg-knopf small { color: var(--leise); font-size: 14px; font-variant-numeric: tabular-nums; }
.themen { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 22px; align-items: center; }
.themen > span { font-size: 14px; color: var(--leise); margin-right: 4px; }
.kontakt-zeile { margin-top: 22px; font-size: 15px; color: var(--leise); }
.kontakt-zeile a { color: var(--tinte); font-weight: 600; }
.beliebt { margin-top: 36px; }
.beliebt h2 { font-size: 1.25rem; font-weight: 650; margin-bottom: 14px; }
/* ── Zustand leer (Mein Bereich) ──────────────────── */
.bereich-kopf h1 { font-size: clamp(2rem, 1.3rem + 2.6vw, 3.25rem); font-weight: 700; line-height: 1.05; }
.bereich-kopf p { color: var(--leise); margin-top: 6px; }
.panels { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 18px; margin-top: 28px; }
.panel { display: grid; align-content: start; gap: 14px; padding: 22px; background: var(--karte); border-radius: 20px; box-shadow: inset 0 0 0 1px var(--linie); min-height: 300px; }
.panel-kopf { display: flex; justify-content: space-between; align-items: baseline; }
.panel-kopf h2 { font-size: 1.25rem; font-weight: 650; }
.panel-kopf span { font-size: 14px; color: var(--leise); font-variant-numeric: tabular-nums; }
.geist { display: grid; gap: 10px; }
.geist i { display: block; height: 58px; border-radius: 12px; border: 1.5px dashed var(--linie-stark); background: linear-gradient(#eef1f6, #eef1f6) 16px 15px / 42% 9px no-repeat, linear-gradient(#f1f3f7, #f1f3f7) 16px 32px / 26% 8px no-repeat, linear-gradient(var(--erfolg), var(--erfolg)) calc(100% - 16px) 50% / 36px 6px no-repeat; }
.geist i + i { background-size: 34% 9px, 20% 8px, 0 0; }
.geist i + i { opacity: .55; }
.leer-text { display: grid; gap: 6px; justify-items: start; }
.leer-text h3 { font-size: 1.125rem; font-weight: 650; }
.leer-text p { color: var(--leise); font-size: 15.5px; max-width: 46ch; }
.leer-text .knopf { margin-top: 10px; }
.skelett { display: grid; gap: 10px; }
.skelett i { display: block; height: 58px; border-radius: 12px; background: linear-gradient(90deg, #eef1f6 0%, #f7f8fb 40%, #eef1f6 80%) 0 0 / 300% 100%; animation: schimmer 1.4s linear infinite; }
.skelett b { display: block; width: 46%; height: 14px; border-radius: 7px; background: #eef1f6; }
@keyframes schimmer { to { background-position: -150% 0; } }
.merkliste { list-style: none; display: grid; gap: 8px; }
.merkliste li { display: grid; grid-template-columns: 6px minmax(0, 1fr) auto; gap: 14px; align-items: center; padding: 12px 12px 12px 0; border-radius: 12px; box-shadow: inset 0 0 0 1px var(--linie); animation: ein var(--wechsel) var(--kurve) both; }
.merkliste li::before { content: ''; align-self: stretch; border-radius: 0 4px 4px 0; background: var(--marke); }
.merkliste b { font-weight: 600; line-height: 1.25; }
.merkliste small { display: block; font-size: 13.5px; color: var(--leise); }
.merkliste button { font-size: 14px; color: var(--leise); text-decoration: underline; text-underline-offset: 3px; padding: 6px; }
.panel[aria-busy="true"] .leer-text, .panel[aria-busy="true"] .geist, .panel[aria-busy="true"] .merkliste { display: none; }
.panel:not([aria-busy="true"]) .skelett { display: none; }
.panel.gefuellt .leer-text, .panel.gefuellt .geist { display: none; }
.vorschlag-kopf { display: flex; justify-content: space-between; align-items: baseline; margin: 36px 0 14px; }
.vorschlag-kopf h2 { font-size: 1.25rem; font-weight: 650; }
/* gedachter Finger der Vorführung */
.finger { position: fixed; z-index: 50; left: 0; top: 0; width: 28px; height: 28px; margin: -14px 0 0 -14px; border-radius: 50%; background: rgba(79, 95, 224, .22); box-shadow: 0 0 0 2px var(--marke); opacity: 0; pointer-events: none; transition: transform 700ms var(--kurve), opacity var(--wechsel) var(--kurve); }
.vorfuehrung .finger { opacity: 1; }
.finger.tippt { animation: tippen 300ms var(--kurve); }
@keyframes tippen { 50% { scale: .7; } }
.fuss { padding: 22px clamp(18px, 4vw, 48px); border-top: 1px solid var(--linie); display: flex; flex-wrap: wrap; justify-content: space-between; gap: 8px 24px; font-size: 14px; color: var(--leise); }
/* ── Pult ─────────────────────────────────────────── */
.pult { position: fixed; right: 14px; bottom: 14px; z-index: 40; display: flex; flex-wrap: wrap; align-items: center; gap: 8px 12px; padding: 8px 10px 8px 12px; background: rgba(20, 20, 20, .92); color: #eceef4; border-radius: 10px; max-width: min(560px, calc(100vw - 28px)); }
.pult, .pult button { font: 500 10.5px/1 'Geist Mono', ui-monospace, monospace; letter-spacing: .14em; text-transform: uppercase; }
.pult .gruppe { display: flex; gap: 2px; }
.pult button { color: rgba(236, 238, 244, .65); padding: 7px 9px; border-radius: 6px; }
.pult button[aria-pressed="true"] { background: #eceef4; color: #141414; }
.pult .werte { text-transform: none; letter-spacing: .02em; color: rgba(236, 238, 244, .55); }
/* Reduzierte Bewegung */
:root.reduziert *, :root.reduziert *::before, :root.reduziert *::after { transition-duration: .01ms !important; animation-duration: .01ms !important; animation-iteration-count: 1 !important; }
:root.reduziert .skelett i { animation: none; }
:root.reduziert .bild-404 .weg { animation: none; }
/* Vorschau: nur die Bühne */
.vorschau body { overflow: hidden; }
.vorschau .fuss, .vorschau .nav li:not(.immer) { display: none; }
.vorschau main { padding-top: 18px; width: calc(100% - 64px); }
.vorschau .fehler-raster { grid-template-columns: minmax(0, 7fr) minmax(0, 4fr); gap: 32px; }
.vorschau .fehler-raster h1 { font-size: 46px; margin: 8px 0 12px; }
.vorschau .fehler-raster .lead { font-size: 16px; }
.vorschau .meintest { margin-top: 18px; }
.vorschau .suchfeld { margin-top: 18px; }
.vorschau .ziele { display: none; }
.vorschau .beliebt { display: none; }
.vorschau .leer-panel { grid-template-columns: 130px minmax(0, 1fr); }
.vorschau .vorschlag-kopf { margin-top: 22px; }
.vorschau .panel { min-height: 0; }
/* Telefon */
@media (max-width: 900px) {
.fehler-raster { grid-template-columns: minmax(0, 1fr); }
.bild-404 { max-width: 320px; }
.karten { grid-template-columns: 1fr; }
.panels { grid-template-columns: 1fr; }
.leer-panel { grid-template-columns: 1fr; }
.leer-panel > svg { max-width: 150px; }
}
@media (max-width: 640px) {
.nav li:not(.immer) { display: none; }
.ziele ul { grid-template-columns: 1fr; }
.meintest { grid-template-columns: 1fr; }
.meintest .knopf { grid-row: auto; grid-column: 1; justify-self: start; margin-top: 8px; }
.sucheingabe { flex-direction: column; }
.leer-text .knopf { width: 100%; }
main { padding-bottom: 96px; }
}
@media (hover: none) {
.vorschlaege li { padding: 14px 12px; }
.chip { padding: 10px 16px; }
}
@media (forced-colors: active) {
button, [role=button], [role=tab], [role=switch] { border: 1px solid ButtonText !important; }
}
</style>
</head>
<body data-v="404">
<header class="kopf">
<a class="logo" href="#start"><svg viewBox="0 0 30 30" aria-hidden="true"><rect width="30" height="30" rx="9" fill="#141414"/><path d="M7 21c4-9 12-12 16-12" fill="none" stroke="#f7c325" stroke-width="3" stroke-linecap="round"/><circle cx="23" cy="9" r="3" fill="#10a86c"/></svg>Kursbogen</a>
<nav aria-label="Hauptnavigation"><ul class="nav">
<li><a href="#kurse" data-nav="kurse">Kurse</a></li>
<li><a href="#termine">Termine</a></li>
<li><a href="#ueber">Über Svenja</a></li>
<li class="immer"><a href="#merkliste" data-nav="leer">Merkliste <span class="zahl" data-zahl aria-label="0 gemerkt">0</span></a></li>
</ul></nav>
</header>
<main id="inhalt">
<!-- Zustand 1: Seite nicht gefunden (Server liefert Status 404) -->
<section class="zustand" data-zustand="404" aria-labelledby="h-404">
<div class="fehler-raster">
<div>
<p class="label" data-ein>Fehler 404 · Seite nicht gefunden</p>
<h1 id="h-404" tabindex="-1" data-ein style="--i:1">Diese Seite gibt es nicht mehr.</h1>
<p class="lead" data-ein style="--i:2">Vielleicht haben wir den Kurs umbenannt, oder in der Adresse fehlt ein Zeichen. Hast du die Adresse abgetippt, prüf sie bitte noch einmal.</p>
<p class="adresse" data-ein style="--i:3"><span>Aufgerufen:</span><code data-adresse>kursbogen.de/kurse/buchhaltung-einsteiger-2025</code></p>
<div class="meintest" data-ein style="--i:4">
<span class="label">Meintest du</span>
<h2 data-meintest-titel>Buchhaltung für Einsteiger</h2>
<p data-meintest-meta>Nächster Start 12.01.2027 · 6 Wochen · live</p>
<a class="knopf" href="#kurs">Zum Kurs <span aria-hidden="true">→</span></a>
</div>
<div class="suchfeld" data-ein style="--i:5">
<label for="such-404">Oder such direkt nach deinem Thema</label>
<div class="eingabe">
<svg viewBox="0 0 24 24" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="11" cy="11" r="7"/><path d="M20 20l-4-4"/></svg>
<input id="such-404" type="search" autocomplete="off" placeholder="z. B. Excel, Angebot, Steuern" role="combobox" aria-expanded="false" aria-controls="liste-404" aria-autocomplete="list" data-such404>
</div>
<ul class="vorschlaege" id="liste-404" role="listbox" aria-label="Vorschläge" data-liste404></ul>
</div>
<nav class="ziele" aria-labelledby="h-ziele" data-ein style="--i:6">
<h2 id="h-ziele" class="label">Beliebte Ziele</h2>
<ul>
<li><a href="#kurse"><b>Alle Kurse</b><small data-anzahl-kurse>8 Kurse für Selbstständige</small></a></li>
<li><a href="#termine"><b>Nächste Starttermine</b><small>ab 12.01.2027, live und online</small></a></li>
<li><a href="#mini"><b>Kostenloser Mini-Kurs</b><small>Dein erstes Angebot in 30 Minuten</small></a></li>
<li><a href="#kontakt"><b>Frag Svenja direkt</b><small>Antwort werktags in 24 Stunden</small></a></li>
</ul>
</nav>
</div>
<div class="bild-404" aria-hidden="true" data-ein style="--i:2">
<svg viewBox="0 0 420 460">
<rect x="0" y="0" width="420" height="460" rx="28" fill="#e6e9ff"/>
<path d="M300 128 C 360 150, 390 220, 352 266" fill="none" stroke="#141414" stroke-width="2.2" stroke-dasharray="1 9" stroke-linecap="round" opacity=".55"/>
<path class="weg" d="M60 410 C 120 380, 70 320, 150 300 S 260 280, 232 214 S 262 150, 286 134" fill="none" stroke="#4f5fe0" stroke-width="5" stroke-linecap="round"/>
<!-- Weg bricht ab -->
<path d="M292 116 l8 10 l-6 6 l9 11" fill="none" stroke="#141414" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M314 104 l8 10 l-6 6 l9 11" fill="none" stroke="#141414" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" opacity=".35"/>
<g font-family="Bricolage Grotesque, sans-serif" font-weight="700" font-size="15" text-anchor="middle">
<circle cx="60" cy="410" r="16" fill="#141414"/><text x="60" y="415" fill="#fff">1</text>
<circle cx="150" cy="300" r="16" fill="#141414"/><text x="150" y="305" fill="#fff">2</text>
<circle cx="232" cy="214" r="16" fill="#141414"/><text x="232" y="219" fill="#fff">3</text>
</g>
<!-- Schild -->
<g transform="translate(250 300)">
<path d="M20 0 V 110" stroke="#141414" stroke-width="4" stroke-linecap="round"/>
<path d="M-6 12 H 92 L 110 30 L 92 48 H -6 Z" fill="#f7c325" stroke="#141414" stroke-width="2.5" stroke-linejoin="round"/>
<text x="6" y="36" font-family="Shantell Sans, cursive" font-size="17" fill="#141414">Umweg ↗</text>
</g>
<text x="40" y="70" font-family="Shantell Sans, cursive" font-size="20" fill="#4f5fe0">Kleiner Umweg,</text>
<text x="40" y="96" font-family="Shantell Sans, cursive" font-size="20" fill="#4f5fe0">gleiches Ziel.</text>
<circle cx="352" cy="266" r="30" fill="#10a86c"/>
<path d="M339 266 l8 8 l17 -17" fill="none" stroke="#fff" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</div>
</div>
</section>
<!-- Zustand 2: Suche ohne Treffer -->
<section class="zustand" data-zustand="keine-treffer" aria-labelledby="h-suche">
<div class="suchkopf" data-ein>
<h1 id="h-suche" tabindex="-1">Kurse durchsuchen</h1>
<form class="sucheingabe" role="search" data-suchform>
<label class="unsichtbar" for="such-seite">Suchbegriff</label>
<div class="eingabe">
<svg viewBox="0 0 24 24" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="11" cy="11" r="7"/><path d="M20 20l-4-4"/></svg>
<input id="such-seite" type="search" autocomplete="off" value="Exel Pivot" data-suche>
</div>
<button class="knopf" type="submit">Suchen</button>
</form>
<div class="filter" role="group" aria-label="Format filtern" data-filter>
<span class="label">Format</span>
<button type="button" class="chip" data-format="live" aria-pressed="false">Live</button>
<button type="button" class="chip" data-format="selbst" aria-pressed="false">Selbstlernkurs</button>
<button type="button" class="chip" data-format="mini" aria-pressed="false">Mini-Kurs</button>
</div>
</div>
<p class="treffer-zeile"><span aria-live="polite" data-trefferzahl></span><span>sortiert nach nächstem Start</span></p>
<ul class="karten" data-ergebnisse></ul>
<div class="leer-panel" data-leerpanel hidden>
<svg viewBox="0 0 180 160" aria-hidden="true">
<rect x="20" y="22" width="112" height="128" rx="14" fill="#fff" stroke="#c9cfdc" stroke-width="2"/>
<path d="M40 52h62M40 72h44M40 92h54" stroke="#dfe3ec" stroke-width="8" stroke-linecap="round"/>
<circle cx="114" cy="96" r="30" fill="#e6e9ff" stroke="#141414" stroke-width="4"/>
<path d="M136 118l22 22" stroke="#141414" stroke-width="7" stroke-linecap="round"/>
<text x="104" y="104" font-family="Shantell Sans, cursive" font-size="24" fill="#4f5fe0">?</text>
</svg>
<div>
<h2 data-leer-titel tabindex="-1">Keine Kurse zu „Exel Pivot“.</h2>
<p data-leer-text>Das liegt oft an einem Tippfehler oder an einem Filter. Hier sind Wege, die weiterführen:</p>
<div class="wege" data-wege></div>
<div class="themen"><span>Beliebte Themen</span><button type="button" class="chip" data-thema="Excel">Excel</button><button type="button" class="chip" data-thema="Buchhaltung">Buchhaltung</button><button type="button" class="chip" data-thema="Preise">Preise</button><button type="button" class="chip" data-thema="Website">Website</button></div>
<p class="kontakt-zeile">Du findest nichts Passendes? <a href="#kontakt">Schreib Svenja</a>, sie antwortet werktags in 24 Stunden.</p>
</div>
</div>
<div class="beliebt" data-beliebt hidden>
<h2>Gerade beliebt</h2>
<ul class="karten" data-beliebt-liste></ul>
</div>
</section>
<!-- Zustand 3: leerer Bereich -->
<section class="zustand" data-zustand="leer" aria-labelledby="h-bereich">
<div class="bereich-kopf" data-ein>
<h1 id="h-bereich" tabindex="-1">Mein Bereich</h1>
<p>Hallo Jana, schön, dass du da bist.</p>
</div>
<div class="panels">
<section class="panel" aria-labelledby="h-meine" aria-busy="true" data-panel="kurse" data-ein style="--i:1">
<div class="panel-kopf"><h2 id="h-meine">Meine Kurse</h2><span>0 gebucht</span></div>
<div class="skelett"><b></b><i></i><i></i><span class="unsichtbar">Lädt …</span></div>
<div class="geist" aria-hidden="true"><i></i><i></i></div>
<div class="leer-text">
<h3>Du hast noch keinen Kurs gebucht.</h3>
<p>Hier siehst du später deine Module, deinen Fortschritt und die nächsten Live-Termine. Fang klein an: Der Mini-Kurs dauert 30 Minuten und kostet nichts.</p>
<a class="knopf" href="#mini">Mini-Kurs starten</a>
</div>
</section>
<section class="panel" aria-labelledby="h-merk" aria-busy="true" data-panel="merk" data-ein style="--i:2">
<div class="panel-kopf"><h2 id="h-merk">Merkliste</h2><span aria-live="polite" data-merkzahl>0 Kurse</span></div>
<div class="skelett"><b></b><i></i><i></i><span class="unsichtbar">Lädt …</span></div>
<div class="geist" aria-hidden="true"><i></i><i></i></div>
<div class="leer-text">
<h3>Noch nichts gemerkt.</h3>
<p>Tipp bei einem Kurs auf das Herz, dann landet er hier. So vergleichst du Termine und Preise in Ruhe.</p>
<a class="knopf zweit" href="#kurse">Kurse ansehen</a>
</div>
<ul class="merkliste" data-merkliste></ul>
</section>
</div>
<div class="vorschlag-kopf"><h2>Vorschläge für dich</h2><span class="notiz">passend zu „Selbstständig seit 2026“</span></div>
<ul class="karten" data-vorschlaege></ul>
</section>
</main>
<footer class="fuss"><span>Kursbogen · Svenja Döhler · Coburg</span><span>Online-Kurse für Selbstständige · Impressum · Datenschutz</span></footer>
<span class="finger" data-finger aria-hidden="true"></span>
<div class="pult">
<div class="gruppe" role="group" aria-label="Zustand der Demo">
<button type="button" data-variante="404" aria-pressed="true">404</button>
<button type="button" data-variante="keine-treffer" aria-pressed="false">Keine Treffer</button>
<button type="button" data-variante="leer" aria-pressed="false">Leer</button>
</div>
<span class="werte" data-werte></span>
</div>
<script>
(() => {
const html = document.documentElement, body = document.body;
const reduziert = () => html.classList.contains('reduziert');
const $ = s => document.querySelector(s), $$ = s => [...document.querySelectorAll(s)];
/* ── Kurse (Datenbasis für Suche, Vorschläge, Merkliste) ── */
const KURSE = [
{ id: 'buch', titel: 'Buchhaltung für Einsteiger', tags: 'buchhaltung steuern euer finanzen belege einsteiger', format: 'live', dauer: '6 Wochen', start: '2027-01-12', preis: 349, modul: '6 Module · 45 min' },
{ id: 'pivot', titel: 'Excel für Selbstständige: Pivot und Diagramme', tags: 'excel pivot tabellen diagramme auswertung', format: 'selbst', dauer: '4 Wochen', preis: 189, modul: '8 Lektionen' },
{ id: 'excel', titel: 'Excel-Grundlagen in fünf Abenden', tags: 'excel tabellen formeln grundlagen', format: 'live', dauer: '5 Abende', start: '2027-02-03', preis: 149, modul: '5 Abende · 90 min' },
{ id: 'angebot', titel: 'Dein erstes Angebot in 30 Minuten', tags: 'angebot preise kunden', format: 'mini', dauer: '30 Minuten', preis: 0, modul: 'Mini-Kurs' },
{ id: 'preise', titel: 'Preise finden, die tragen', tags: 'preise kalkulation stundensatz angebot', format: 'live', dauer: '3 Wochen', start: '2027-01-20', preis: 229, modul: '3 Module · 60 min' },
{ id: 'website', titel: 'Eigene Website in sechs Wochen', tags: 'website homepage webseite online', format: 'live', dauer: '6 Wochen', start: '2027-01-26', preis: 449, modul: '6 Module · 60 min' },
{ id: 'steuer', titel: 'Steuern für Kleinunternehmer', tags: 'steuern umsatzsteuer kleinunternehmer finanzamt', format: 'selbst', dauer: '2 Wochen', preis: 129, modul: '6 Lektionen' },
{ id: 'linkedin', titel: 'Sichtbar werden ohne Krampf', tags: 'marketing linkedin sichtbarkeit kunden', format: 'selbst', dauer: '2 Wochen', preis: 99, modul: '5 Lektionen' },
];
const FORMAT = { live: 'live', selbst: 'Selbstlernkurs', mini: 'Mini-Kurs' };
const FILTERNAME = { live: 'Live', selbst: 'Selbstlernkurs', mini: 'Mini-Kurs' };
const datum = iso => new Intl.DateTimeFormat('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' }).format(new Date(iso + 'T12:00:00'));
const preis = p => p ? new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR', maximumFractionDigits: 0 }).format(p) : 'kostenlos';
const norm = s => s.toLowerCase().replace(/[^a-zäöüß0-9 ]+/g, ' ').trim();
const text = k => norm(k.titel + ' ' + k.tags);
const WORTSCHATZ = new Map();
KURSE.forEach(k => (k.titel + ' ' + k.tags).split(/[^A-Za-zÄÖÜäöüß0-9-]+/).forEach(w => { if (w.length > 2 && !WORTSCHATZ.has(w.toLowerCase())) WORTSCHATZ.set(w.toLowerCase(), /^[a-zäöü]/.test(w) && w === w.toLowerCase() ? w[0].toUpperCase() + w.slice(1) : w); }));
$('[data-anzahl-kurse]').textContent = `${KURSE.length} Kurse für Selbstständige`;
function abstand(a, b) { // Levenshtein
const d = Array.from({ length: a.length + 1 }, (_, i) => [i]);
for (let j = 1; j <= b.length; j++) d[0][j] = j;
for (let i = 1; i <= a.length; i++) for (let j = 1; j <= b.length; j++) d[i][j] = Math.min(d[i - 1][j] + 1, d[i][j - 1] + 1, d[i - 1][j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
return d[a.length][b.length];
}
function suche(q, formate = []) {
const t = norm(q).split(/\s+/).filter(x => x.length > 1);
return KURSE.filter(k => (!formate.length || formate.includes(k.format)) && t.every(w => text(k).split(' ').some(x => x.startsWith(w))));
}
function korrektur(q) { // jedes unbekannte Wort durch das nächste bekannte ersetzen (Abstand ≤ 2)
let geaendert = false;
const neu = norm(q).split(/\s+/).filter(Boolean).map(w => {
if ([...WORTSCHATZ.keys()].some(x => x.startsWith(w))) return w;
let best = null, bd = 3;
WORTSCHATZ.forEach((anzeige, x) => { const dd = abstand(w, x); if (dd < bd && w.length > 3) { bd = dd; best = anzeige; } });
if (best) { geaendert = true; return best; }
return w;
});
return geaendert ? neu.map(w => w[0].toUpperCase() + w.slice(1)).join(' ') : null;
}
function karte(k, mitHerz = true) {
const gemerkt = merk.has(k.id);
return `<li class="kurs" data-kurs="${k.id}">
<span class="modul">${k.modul}</span>
<h3>${k.titel}</h3>
<span class="meta">${k.dauer} · ${FORMAT[k.format]}</span>
${k.start ? `<span class="start">Start ${datum(k.start)}</span>` : ''}
<span class="preis">${preis(k.preis)}</span>
${mitHerz ? `<button type="button" class="herz" data-herz="${k.id}" aria-pressed="${gemerkt}" aria-label="${k.titel} merken"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 20s-7.5-4.6-7.5-10A4.3 4.3 0 0 1 12 7.2 4.3 4.3 0 0 1 19.5 10c0 5.4-7.5 10-7.5 10z"/></svg></button>` : ''}
</li>`;
}
/* ── 404: Vorschlag aus der falschen Adresse, Suche als Combobox ── */
function ausAdresse(pfad) {
const w = norm(pfad.replace(/[\/\-_.]/g, ' ')).split(' ').filter(x => x.length > 3 && !/^\d+$/.test(x) && !['kurse', 'kursbogen'].includes(x));
let best = null, punkte = 0;
KURSE.forEach(k => { const p = w.filter(x => text(k).includes(x)).length; if (p > punkte) { punkte = p; best = k; } });
return best;
}
const vorschlag = ausAdresse($('[data-adresse]').textContent);
if (vorschlag) {
$('[data-meintest-titel]').textContent = vorschlag.titel;
$('[data-meintest-meta]').textContent = [vorschlag.start ? 'Nächster Start ' + datum(vorschlag.start) : '', vorschlag.dauer, FORMAT[vorschlag.format]].filter(Boolean).join(' · ');
}
const in404 = $('[data-such404]'), liste404 = $('[data-liste404]');
let auswahl = -1, treffer404 = [];
function zeige404() {
const q = in404.value.trim();
if (!q) { liste404.classList.remove('offen'); in404.setAttribute('aria-expanded', 'false'); return; }
treffer404 = suche(q).slice(0, 4);
const hl = s => { let r = s; norm(q).split(/\s+/).filter(x => x.length > 1).forEach(w => { r = r.replace(new RegExp('(' + w.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + ')', 'ig'), '<mark>$1</mark>'); }); return r; };
const korr = !treffer404.length && korrektur(q);
liste404.innerHTML = treffer404.length
? treffer404.map((k, i) => `<li role="option" id="opt-${i}" aria-selected="${i === auswahl}" data-i="${i}"><span>${hl(k.titel)}</span><small>${k.dauer} · ${FORMAT[k.format]}</small><em>${preis(k.preis)}</em></li>`).join('')
: `<li class="leer-zeile" role="option" aria-disabled="true">Keine Kurse zu „${q}“${korr ? `. Meintest du <b>${korr}</b>?` : '.'}</li>`;
liste404.classList.add('offen'); in404.setAttribute('aria-expanded', 'true');
in404.setAttribute('aria-activedescendant', auswahl >= 0 ? 'opt-' + auswahl : '');
}
in404.addEventListener('input', () => { auswahl = -1; zeige404(); });
in404.addEventListener('keydown', e => {
if (e.key === 'ArrowDown' || e.key === 'ArrowUp') { e.preventDefault(); if (!treffer404.length) return; auswahl = (auswahl + (e.key === 'ArrowDown' ? 1 : -1) + treffer404.length) % treffer404.length; zeige404(); }
else if (e.key === 'Escape') { liste404.classList.remove('offen'); in404.setAttribute('aria-expanded', 'false'); }
else if (e.key === 'Enter') { e.preventDefault(); zurSuche(in404.value); }
});
liste404.addEventListener('click', e => { const li = e.target.closest('[data-i]'); if (li) zurSuche(treffer404[+li.dataset.i].titel); });
in404.addEventListener('blur', () => setTimeout(() => liste404.classList.remove('offen'), 150));
function zurSuche(q) { $('[data-suche]').value = q; setzeVariante('keine-treffer', true); }
/* ── Suche ohne Treffer ─────────────────────── */
const inSuche = $('[data-suche]');
const formate = () => $$('[data-format][aria-pressed="true"]').map(b => b.dataset.format);
function sucheZeigen() {
const q = inSuche.value.trim(), f = formate(), r = q ? suche(q, f) : KURSE.filter(k => !f.length || f.includes(k.format));
$('[data-trefferzahl]').innerHTML = `<b>${r.length}</b> ${r.length === 1 ? 'Kurs' : 'Kurse'}${q ? ` für „${q}“` : ''}`;
$('[data-ergebnisse]').innerHTML = r.map(k => karte(k)).join('');
const leer = !r.length;
$('[data-leerpanel]').hidden = !leer; $('[data-beliebt]').hidden = !leer;
if (!leer) return;
$('[data-leer-titel]').textContent = `Keine Kurse zu „${q}“${f.length ? ' mit diesem Filter' : ''}.`;
const wege = [];
const korr = korrektur(q);
if (korr) { const n = suche(korr, f).length, n0 = suche(korr).length; if (n || n0) wege.push(`<button type="button" class="weg-knopf" data-setze="${korr}"${!n ? ' data-ohnefilter' : ''}><span>Meintest du <b>${korr}</b>?</span><small>${n || n0} ${(n || n0) === 1 ? 'Kurs' : 'Kurse'}${!n ? ' ohne Filter' : ''}</small></button>`); }
f.forEach(x => { const n = suche(korr || q, f.filter(y => y !== x)).length; if (n) wege.push(`<button type="button" class="weg-knopf" data-filter-weg="${x}"><span>Ohne Filter „${FILTERNAME[x]}“</span><small>${n} ${n === 1 ? 'Kurs' : 'Kurse'}</small></button>`); });
if (!wege.length) wege.push(`<a class="weg-knopf" href="#kurse"><span>Alle <b>${KURSE.length} Kurse</b> ansehen</span></a>`);
$('[data-wege]').innerHTML = wege.join('');
$('[data-beliebt-liste]').innerHTML = ['angebot', 'buch', 'preise'].map(id => karte(KURSE.find(k => k.id === id))).join('');
}
$('[data-suchform]').addEventListener('submit', e => { e.preventDefault(); stoppen(); sucheZeigen(); });
inSuche.addEventListener('input', () => { sucheZeigen(); });
$('[data-filter]').addEventListener('click', e => { const b = e.target.closest('[data-format]'); if (!b) return; stoppen(); b.setAttribute('aria-pressed', String(b.getAttribute('aria-pressed') !== 'true')); sucheZeigen(); });
document.addEventListener('click', e => {
const s = e.target.closest('[data-setze]'), fw = e.target.closest('[data-filter-weg]'), th = e.target.closest('[data-thema]');
if (s) { if (s.hasAttribute('data-ohnefilter')) $$('[data-format]').forEach(b => b.setAttribute('aria-pressed', 'false')); inSuche.value = s.dataset.setze; sucheZeigen(); }
if (fw) { $(`[data-format="${fw.dataset.filterWeg}"]`).setAttribute('aria-pressed', 'false'); sucheZeigen(); }
if (th) { inSuche.value = th.dataset.thema; sucheZeigen(); }
if ((s || fw || th) && e.isTrusted) stoppen();
});
/* ── Leerer Bereich: erst laden, dann leer, Herz füllt die Merkliste ── */
const merk = new Set();
function merkZeigen() {
const n = merk.size, z = $('[data-zahl]');
z.textContent = n; z.classList.toggle('voll', n > 0); z.setAttribute('aria-label', n + ' gemerkt');
$('[data-merkzahl]').textContent = n + (n === 1 ? ' Kurs' : ' Kurse');
const p = $('[data-panel="merk"]');
p.classList.toggle('gefuellt', n > 0);
$('[data-merkliste]').innerHTML = [...merk].map(id => { const k = KURSE.find(x => x.id === id); return `<li><span><b>${k.titel}</b><small>${k.start ? 'Start ' + datum(k.start) + ' · ' : ''}${preis(k.preis)}</small></span><button type="button" data-entfernen="${id}" aria-label="${k.titel} entfernen">Entfernen</button></li>`; }).join('');
$$('[data-herz]').forEach(b => { const an = merk.has(b.dataset.herz); b.setAttribute('aria-pressed', String(an)); b.setAttribute('aria-label', (an ? 'Gemerkt: ' : 'Merken: ') + KURSE.find(x => x.id === b.dataset.herz).titel); });
}
function umschaltenMerk(id) {
if (merk.has(id)) merk.delete(id); else merk.add(id);
merkZeigen();
const b = $(`[data-vorschlaege] [data-herz="${id}"]`) || $(`[data-herz="${id}"]`);
if (b && merk.has(id) && !reduziert()) { b.classList.remove('puls'); void b.offsetWidth; b.classList.add('puls'); }
}
document.addEventListener('click', e => {
const h = e.target.closest('[data-herz]'), x = e.target.closest('[data-entfernen]');
if (h) { if (e.isTrusted) stoppen(); umschaltenMerk(h.dataset.herz); }
if (x) { if (e.isTrusted) stoppen(); merk.delete(x.dataset.entfernen); merkZeigen(); }
});
$('[data-vorschlaege]').innerHTML = ['angebot', 'preise', 'steuer'].map(id => karte(KURSE.find(k => k.id === id))).join('');
let ladeUhr = 0;
function laden() {
clearTimeout(ladeUhr);
$$('[data-panel]').forEach(p => p.setAttribute('aria-busy', 'true'));
ladeUhr = setTimeout(() => $$('[data-panel]').forEach(p => p.setAttribute('aria-busy', 'false')), reduziert() ? 300 : 900);
}
/* ── Varianten (Regel 13) ─────────────────── */
const VARIANTEN = ['404', 'keine-treffer', 'leer'];
const WERTE = {
'404': 'easeOutQuint · Einblenden 500 ms, 12 px, Versatz 60 ms · UI 150 ms',
'keine-treffer': 'Levenshtein ≤ 2 · Zahl ohne Filter · easeOutQuint 300 ms',
'leer': 'Laden 900 ms, dann leer · Feder 450 ms nur beim Merken',
};
function setzeVariante(v, url, fokus) {
v = VARIANTEN.includes(v) ? v : '404';
body.dataset.v = v;
$$('[data-variante]').forEach(b => b.setAttribute('aria-pressed', String(b.dataset.variante === v)));
$('[data-werte]').textContent = WERTE[v];
$$('[data-nav]').forEach(a => a.toggleAttribute('aria-current', (a.dataset.nav === 'kurse' && v === 'keine-treffer') || (a.dataset.nav === 'leer' && v === 'leer')));
document.title = { '404': 'Seite nicht gefunden', 'keine-treffer': 'Suche', 'leer': 'Mein Bereich' }[v] + ' · Kursbogen';
// Einblende-Animation neu starten
$$(`[data-zustand="${v}"] [data-ein]`).forEach(el => { el.style.animation = 'none'; void el.offsetWidth; el.style.animation = ''; });
if (v === 'keine-treffer') sucheZeigen();
if (v === 'leer') laden();
if (fokus) { const h = $(`[data-zustand="${v}"] h1`); h && h.focus({ preventScroll: true }); }
if (url) { const q = new URLSearchParams(location.search); q.set('v', v); history.replaceState(null, '', '?' + q.toString().replace(/=(?=&|$)/g, '')); }
}
$$('[data-variante]').forEach(b => b.addEventListener('click', () => { stoppen(); setzeVariante(b.dataset.variante, true); }));
document.addEventListener('click', e => { const a = e.target.closest('a[href^="#"]'); if (a) e.preventDefault(); });
/* ── Vorführung: gedachter Finger tippt, Text wird getippt ── */
const finger = $('[data-finger]');
let uhren = [];
const nach = (ms, f) => uhren.push(setTimeout(f, ms));
function zeigeAuf(el, dx = 0, dy = 0) { if (!el) return; const r = el.getBoundingClientRect(); finger.style.transform = `translate(${r.left + Math.min(r.width / 2, 60) + dx}px, ${r.top + r.height / 2 + dy}px)`; }
function tippe(el) { zeigeAuf(el); finger.classList.remove('tippt'); void finger.offsetWidth; finger.classList.add('tippt'); }
function schreibe(input, wort, start, schritt = 110, danach) {
for (let i = 0; i <= wort.length; i++) nach(start + i * schritt, () => { input.value = wort.slice(0, i); input.dispatchEvent(new Event('input')); });
return start + wort.length * schritt;
}
function runde() {
const v = body.dataset.v;
if (v === '404') {
setzeVariante('404');
in404.value = ''; zeige404();
nach(600, () => zeigeAuf(in404));
const t = schreibe(in404, 'excel', 1200, 140);
nach(t + 500, () => { auswahl = 0; zeige404(); });
nach(t + 1200, () => { auswahl = 1; zeige404(); });
nach(t + 2400, () => { in404.value = ''; zeige404(); zeigeAuf($('.meintest .knopf')); });
nach(t + 3400, () => tippe($('.meintest .knopf')));
nach(t + 4600, runde);
} else if (v === 'keine-treffer') {
$$('[data-format]').forEach(b => b.setAttribute('aria-pressed', 'false'));
inSuche.value = ''; sucheZeigen();
nach(400, () => zeigeAuf(inSuche));
const t = schreibe(inSuche, 'Exel Pivot', 800, 110);
nach(t + 900, () => zeigeAuf($('[data-setze]')));
nach(t + 1900, () => { const b = $('[data-setze]'); tippe(b); b && b.click(); });
nach(t + 3400, () => tippe($('[data-format="live"]')));
nach(t + 3500, () => $('[data-format="live"]').click());
nach(t + 5000, () => zeigeAuf($('[data-filter-weg]') || $('[data-setze]')));
nach(t + 5800, () => { const b = $('[data-filter-weg]'); if (b) { tippe(b); b.click(); } });
nach(t + 7400, () => { inSuche.value = 'Exel Pivot'; sucheZeigen(); });
nach(t + 8400, runde);
} else {
merk.clear(); merkZeigen(); laden();
nach(1600, () => zeigeAuf($('[data-vorschlaege] [data-herz="preise"]')));
nach(2500, () => { const b = $('[data-vorschlaege] [data-herz="preise"]'); tippe(b); umschaltenMerk('preise'); });
nach(3700, () => { const b = $('[data-vorschlaege] [data-herz="angebot"]'); zeigeAuf(b); });
nach(4500, () => { const b = $('[data-vorschlaege] [data-herz="angebot"]'); tippe(b); umschaltenMerk('angebot'); });
nach(6400, () => zeigeAuf($('[data-entfernen]')));
nach(7200, () => { tippe($('[data-entfernen]')); merk.clear(); merkZeigen(); });
nach(8800, runde);
}
}
function stoppen() { uhren.forEach(clearTimeout); uhren = []; html.classList.remove('vorfuehrung'); }
function neustart() { stoppen(); merk.clear(); merkZeigen(); inSuche.value = 'Exel Pivot'; $$('[data-format]').forEach(b => b.setAttribute('aria-pressed', 'false')); in404.value = ''; zeige404(); setzeVariante(body.dataset.v); }
function vorfuehren() { stoppen(); html.classList.add('vorfuehrung'); finger.style.transition = 'none'; finger.style.transform = `translate(${innerWidth * .6}px, ${innerHeight * .7}px)`; void finger.offsetWidth; finger.style.transition = ''; runde(); }
function pause() { uhren.forEach(clearTimeout); uhren = []; }
function weiter() { if (html.classList.contains('vorfuehrung') && !uhren.length) runde(); }
merkZeigen();
setzeVariante(new URLSearchParams(location.search).get('v'));
})();
</script>
</body>
</html>