Zum Inhalt
← Sektionen & Funktionen

Traueranzeigen und Gedenkseite mit Kondolenzbuch

Traueranzeigen mit Suche und Termin der Trauerfeier, dazu eine Gedenkseite mit .ics, Kondolenzbuch mit Freigabe, stillen Kerzen als Zahl und einem klaren Ende.

Übersicht auf der Seite des Bestattungshauses oder der Friedhofsverwaltung: Wer vom Tod erfährt, findet Namen und Termin der Trauerfeier ohne Anruf.

Eine Liste der aktuellen Traueranzeigen mit Name, Lebensdaten in Tabellenziffern, Ort und Termin der Trauerfeier, mit Suche nach Namen und Sortierung. Jede Anzeige führt zu einer Gedenkseite: Anzeige als Text und als Bild mit alt-Text, Trauerfeier mit Ort, Uhrzeit und .ics-Datei, „Spenden statt Blumen“ mit Empfänger und Verwendungszweck, ein Kondolenzbuch, dessen Einträge vor der Veröffentlichung gelesen werden, und stille Kerzen als Zahl. Jede Seite sagt, bis wann sie online bleibt. Auf Wunsch der Familie gibt es sie ohne Kondolenzbuch und ohne Suchmaschinen.

Die Seite des Bestattungshauses ist oft der Ort, an dem Bekannte vom Tod erfahren und den Termin der Trauerfeier finden. Eine ruhige Liste mit Suche beantwortet „Wann und wo ist die Feier?“ ohne Anruf bei der Familie. Das Kondolenzbuch gibt Menschen einen Ort für ein paar Worte; die Freischaltung schützt die Angehörigen vor unpassenden Einträgen. Die Kerze als Zahl ist ein stilles Zeichen ohne Kitsch.

Wann einsetzen

Bestattungshaus, Friedhofsverwaltung, Bestattungswald, Kirchengemeinde oder Verein, die Anzeigen im Auftrag der Familie veröffentlichen. Nur mit Zustimmung der Angehörigen, die die Bestattung beauftragt haben.

Wann nicht

Wenn niemand die Kondolenzen zeitnah liest: dann die Variante ohne Kondolenzbuch. Nicht als Werbefläche (keine Grabstein- oder Blumen-Angebote neben der Anzeige ohne Kennzeichnung). Keine Flamme, kein Flackern, keine Zähler, die hochlaufen.

Varianten

  • Liste: aktuelle Traueranzeigen mit Suche nach Namen und Sortierung nach Sterbedatum, Trauerfeier oder Name
  • Gedenkseite: eine Person mit Reitern Anzeige, Trauerfeier, Kondolenzen und Kerzen, Zähler als Text an den Reitern
  • Still: nur Anzeige und Trauerfeier, ohne Kondolenzbuch und Kerzen, für Familien, die keine öffentlichen Einträge möchten
  • Nicht in Suchmaschinen (noindex) und abgelaufene Seite mit Hinweis statt Inhalt

Umsetzung

Daten je Anzeige: Name, Geburtsname, Geburts- und Sterbedatum, Ort, Trauerfeier (Datum, Uhrzeit, Ort, Adresse), Anzeige als Text und als Bild mit alt-Text, Spendenempfänger, Ablaufdatum, Schalter noindex und kondolenzbuch. Liste: Suche normalisiert Umlaute und Groß- und Kleinschreibung, durchsucht Name und Geburtsname; Treffer als Text in einer role=status-Region; Sortierung als select. Gedenkseite: Reiter nach APG mit Zählern als Text („Kondolenzen (3)“). Trauerfeier als .ics über Blob (RFC 5545, DTSTART mit TZID Europe/Berlin, UID, DTSTAMP). Kondolenz: Name, Text, E-Mail (nur intern, nie angezeigt), Datenschutzhinweis am Formular; Fehlerliste über dem Formular; nach dem Senden steht der Eintrag nur für den Schreibenden sichtbar als „wartet auf Freigabe“, nach der Freigabe in der Liste. Kerze: optionaler Name, Zahl steigt um eins ohne Animation; eine Kerze je Gerät (localStorage). „Link kopieren“ über navigator.clipboard mit Rückfall. Ablauf: nach dem Datum liefert der Server statt der Seite einen Hinweis mit Kontakt (in der Demo als Zustand). noindex: <meta name="robots" content="noindex"> und Ausschluss aus der Sitemap.

Robust umsetzen

Telefon. Liste als Karten untereinander, Lebensdaten unter dem Namen. Reiter brechen um statt seitlich zu scrollen. Knöpfe 48 px, Formularfelder volle Breite.

Ohne Bewegung. Nichts bewegt sich von selbst. Reiterwechsel und Bestätigungen ohne Übergang.

Barrierefrei. Reiter mit Zählern als Text, Pfeiltasten, Pos1, Ende. Suchergebnis als Text in role=status. Formular mit sichtbaren Labels, Fehlerliste mit Sprunglinks zu den Feldern und aria-describedby an den Feldern (WCAG 3.3.1, 3.3.3). Anzeige als Bild mit vollständigem alt-Text und daneben als echter Text. Lebensdaten als <time datetime>. Keine Flamme, kein Flackern (WCAG 2.3.1), nichts bewegt sich von selbst.

Code

<!DOCTYPE html>
<!--
  Traueranzeigen und Gedenkseite mit Kondolenzbuch
  Fundus · Sektionen & Funktionen · https://cecodesign.de/lab/fundus/sektionen/gedenkseiten-kondolenz/
  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>Traueranzeigen und Gedenkseite mit Kondolenzbuch · 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: 'Source Serif 4'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/source-serif-4:vf@latest/latin-opsz-normal.woff2') format('woff'); font-weight: 200 900; font-display: swap; }
@font-face { font-family: 'Commissioner'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/commissioner:vf@latest/latin-full-normal.woff2') format('woff2-variations'); font-weight: 100 900; font-display: swap; }

:root {
  /* Rezept bestattung-wuerdig */
  --kalk: #f4f1eb; --papier: #fbfaf7; --text: #24221f; --sand: #e8e1d5; --schiefer: #2b2d2c;
  --moos: #3f5b48; --moos-tief: #34503d; --moos-hell: #8fb59a;
  --leise: #5a554d; --linie: #d3ccc0; --auf-schiefer: #f4f1eb; --auf-schiefer-leise: #c9c3b8;
  --fehler: #8a2f1d;
  --serif: 'Source Serif 4', Georgia, serif;
  --sans: 'Commissioner', system-ui, sans-serif;
  --auf-kurve: cubic-bezier(0.4, 0, 0.2, 1); --zu-kurve: cubic-bezier(0.4, 0, 1, 1);
  --auf-dauer: 300ms; --zu-dauer: 200ms; --farbe-dauer: 150ms;
}
* { box-sizing: border-box; margin: 0; }
html { background: var(--kalk); color: var(--text); font-family: var(--sans); font-variation-settings: 'FLAR' 0, 'VOLM' 0; -webkit-text-size-adjust: 100%; }
body { min-height: 100svh; font-size: 1.1875rem; line-height: 1.65; font-variant-numeric: tabular-nums; }
:focus-visible { outline: 3px solid var(--text); outline-offset: 3px; border-radius: 2px; }
.dunkel :focus-visible { outline-color: var(--auf-schiefer); }
a { color: inherit; text-decoration-thickness: 1px; text-underline-offset: .35ex; }
button, input, textarea, select { font: inherit; color: inherit; }
button { cursor: pointer; background: none; border: 0; }
h1, h2, h3 { font-family: var(--serif); font-weight: 400; font-optical-sizing: auto; text-wrap: balance; }
h1 { font-size: clamp(2.25rem, 1.6rem + 2.6vw, 3.5rem); line-height: 1.12; letter-spacing: -.01em; }
h2 { font-size: clamp(1.75rem, 1.4rem + 1.4vw, 2.5rem); line-height: 1.15; }
h3 { font-size: 1.375rem; font-weight: 600; line-height: 1.3; }
p { max-width: 62ch; text-wrap: pretty; }
.nur-sr { position: absolute !important; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap; }
.rubrik { font: 500 1rem/1.3 var(--sans); letter-spacing: .06em; text-transform: uppercase; color: var(--leise); }
.klein { font-size: 1rem; line-height: 1.5; color: var(--leise); }
.platzhalter { display: inline-block; padding: .35rem .6rem; border: 1px dashed #8a8174; color: var(--leise); font-size: 1rem; line-height: 1.45; }
.sprung a { position: absolute; left: -9999px; z-index: 60; padding: 10px 16px; background: var(--kalk); border: 1px solid var(--text); font-weight: 600; font-size: 1rem; }
.sprung a:focus { left: 12px; top: 8px; }

/* Knöpfe (button-zustaende, pfeil-icon-button Variante linie) */
.knopf { display: inline-flex; align-items: center; justify-content: center; gap: .55em; min-height: 48px; padding: 0 1.25rem; border-radius: 6px; font: 600 1.0625rem/1.2 var(--sans); text-decoration: none; transition: background-color var(--farbe-dauer) var(--auf-kurve), color var(--farbe-dauer) var(--auf-kurve); }
.knopf-moos { background: var(--moos); color: #fff; }
.knopf-moos:hover { background: var(--moos-tief); }
.knopf-rand { border: 1px solid currentColor; }
.knopf-rand:hover { background: rgba(36, 34, 31, .06); }
.knopf[aria-busy="true"] { pointer-events: none; }
.knopf[disabled] { cursor: default; background: var(--sand); color: var(--leise); border-color: var(--linie); }
.linie { display: inline-flex; align-items: center; gap: .45em; min-height: 44px; font-weight: 600; text-decoration: underline; text-decoration-thickness: 1px; text-underline-offset: .35ex; }
.linie .pfeil-schaft { opacity: 0; transition: opacity var(--farbe-dauer) var(--auf-kurve); }
.linie .pfeil-spitze { transition: transform var(--farbe-dauer) var(--auf-kurve); }
.linie:hover .pfeil-schaft, .linie:focus-visible .pfeil-schaft { opacity: 1; }
.linie:hover .pfeil-spitze, .linie:focus-visible .pfeil-spitze { transform: translateX(3px); }

/* Kopf: schmale Sofortzeile (aus trauerfall-sofortleiste) und Marke */
.sofort { background: var(--schiefer); color: var(--auf-schiefer); }
.sofort p { max-width: 1200px; margin: 0 auto; padding: 10px clamp(16px, 3vw, 40px); display: flex; flex-wrap: wrap; align-items: center; gap: 2px 14px; font-size: 1rem; }
.sofort b { font-weight: 500; letter-spacing: .06em; text-transform: uppercase; }
.sofort span { color: var(--auf-schiefer-leise); }
.sofort a { font-weight: 600; font-size: 1.125rem; text-decoration: none; min-height: 32px; display: inline-flex; align-items: center; }
.sofort a:hover { text-decoration: underline; }
.kopf { max-width: 1200px; margin: 0 auto; padding: 20px clamp(16px, 3vw, 40px); display: flex; flex-wrap: wrap; align-items: center; gap: 12px 32px; }
.marke { display: grid; text-decoration: none; margin-right: auto; }
.marke b { font: 400 1.75rem/1.1 var(--serif); }
.marke span { font-size: 1rem; color: var(--leise); line-height: 1.4; }
.navi ul { list-style: none; padding: 0; display: flex; flex-wrap: wrap; gap: 4px 22px; }
.navi a { display: inline-flex; align-items: center; min-height: 44px; font-size: 1rem; font-weight: 500; text-decoration: none; }
.navi a:hover, .navi a[aria-current] { text-decoration: underline; }
.huelle { max-width: 1200px; margin: 0 auto; padding: 0 clamp(16px, 3vw, 40px); }
.pfad { font-size: 1rem; color: var(--leise); padding: 8px 0 0; }
.pfad a { color: var(--leise); }

/* ---------- Liste ---------- */
.liste-kopf { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 24rem); gap: 20px clamp(24px, 4vw, 64px); align-items: end; padding: 18px 0 28px; }
.liste-kopf > div { display: grid; gap: 12px; }
.werkzeuge { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 15rem); gap: 12px 16px; align-items: end; padding: 18px 0; border-top: 1px solid var(--linie); border-bottom: 1px solid var(--linie); }
.werkzeuge label { display: block; font-size: 1rem; font-weight: 500; margin-bottom: 4px; }
.suchfeld { position: relative; }
.suchfeld input, .werkzeuge select { width: 100%; min-height: 48px; padding: 10px 44px 10px 12px; border: 1px solid #8a8174; border-radius: 6px; background: var(--papier); font-size: 1.0625rem; }
.werkzeuge select { padding-right: 12px; }
.suchfeld input::-webkit-search-cancel-button { display: none; }
.leeren { position: absolute; right: 2px; bottom: 2px; width: 44px; height: 44px; display: grid; place-items: center; border-radius: 4px; }
.leeren:hover { background: rgba(36, 34, 31, .06); }
.leeren[hidden] { display: none; }
.treffer { grid-column: 1 / -1; font-size: 1rem; font-weight: 500; color: var(--leise); min-height: 1.5em; }
.anzeigen { list-style: none; padding: 0; }
.anzeige { position: relative; display: grid; grid-template-columns: minmax(0, 1.25fr) minmax(0, .8fr) minmax(0, 1.2fr) 24px; gap: 8px clamp(16px, 3vw, 40px); align-items: start; padding: 22px 0; border-bottom: 1px solid var(--linie); transition: background-color var(--farbe-dauer) var(--auf-kurve); }
.anzeige:hover { background: rgba(232, 225, 213, .55); }
.anzeige h2 { font-size: clamp(1.375rem, 1.2rem + .6vw, 1.75rem); line-height: 1.2; }
.anzeige h2 a { text-decoration: none; }
.anzeige h2 a::after { content: ''; position: absolute; inset: 0; }
.anzeige:hover h2 a, .anzeige h2 a:focus-visible { text-decoration: underline; text-decoration-thickness: 1px; }
.anzeige h2 a:focus-visible { outline: none; }
.anzeige:has(h2 a:focus-visible) { outline: 3px solid var(--text); outline-offset: 2px; }
.anzeige .geb { font-size: 1rem; color: var(--leise); }
.daten { display: grid; grid-template-columns: auto auto; justify-content: start; gap: 0 10px; font-size: 1rem; line-height: 1.6; }
.daten dt { color: var(--leise); }
.daten dd { margin: 0; }
.feier-kurz { font-size: 1rem; line-height: 1.55; }
.feier-kurz b { display: block; font-weight: 600; font-size: 1.0625rem; }
.feier-kurz .still { color: var(--leise); }
.anzeige .pfeil { padding-top: 6px; color: var(--leise); }
.leer { padding: 48px 0 56px; display: grid; gap: 12px; }
.leer h2 { font-size: 1.75rem; }

/* ---------- Gedenkseite ---------- */
.gedenk-kopf { display: grid; grid-template-columns: minmax(0, 22rem) minmax(0, 1fr); gap: 24px clamp(24px, 4vw, 64px); align-items: center; padding: 20px 0 32px; }
.gedenk-kopf.ohne-bild { grid-template-columns: minmax(0, 1fr); }
.gedenk-kopf figure { margin: 0; }
.gedenk-kopf figure svg { display: block; width: 100%; height: auto; border-radius: 4px; }
.gedenk-kopf figcaption { font-size: 1rem; color: var(--leise); padding-top: 8px; }
.gedenk-text { display: grid; gap: 10px; align-content: center; }
.gedenk-text h1 { font-size: clamp(2.5rem, 1.7rem + 3vw, 4rem); font-weight: 350; line-height: 1.05; }
.gedenk-text .geb { font-family: var(--serif); font-size: 1.375rem; color: var(--leise); }
.lebensdaten { font-size: 1.25rem; letter-spacing: .01em; }
.kurzinfo { margin-top: 8px; padding: 14px 16px; background: var(--sand); border-radius: 6px; font-size: 1.0625rem; line-height: 1.55; }
.kurzinfo b { font-weight: 600; }

.reiter { display: flex; flex-wrap: wrap; gap: 8px; padding-bottom: 0; border-bottom: 1px solid var(--linie); }
.reiter [role="tab"] { min-height: 48px; padding: 8px 18px; font-size: 1.0625rem; font-weight: 500; color: var(--leise); border-bottom: 3px solid transparent; margin-bottom: -1px; transition: color var(--farbe-dauer) var(--auf-kurve), background-color var(--farbe-dauer) var(--auf-kurve); }
.reiter [role="tab"]:hover { color: var(--text); background: rgba(36, 34, 31, .05); }
.reiter [role="tab"][aria-selected="true"] { color: var(--text); font-weight: 600; border-bottom-color: var(--schiefer); }
[role="tabpanel"] { padding: 32px 0 40px; }
[role="tabpanel"]:focus-visible { outline-offset: -3px; }
.zwei { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 24rem); gap: 32px clamp(24px, 4vw, 64px); align-items: start; }

.blatt { background: var(--papier); border: 1px solid var(--linie); padding: clamp(24px, 4vw, 48px); text-align: center; display: grid; gap: 10px; justify-items: center; }
.blatt p { max-width: 40ch; }
.blatt .gross { font: 400 clamp(1.75rem, 1.4rem + 1.2vw, 2.25rem)/1.15 var(--serif); }
.blatt .ser { font-family: var(--serif); font-size: 1.25rem; }
.blatt .trenn { width: 48px; height: 1px; background: var(--linie); margin: 8px 0; }
.als-bild { display: grid; gap: 8px; }
.als-bild svg { display: block; width: 100%; height: auto; border: 1px solid var(--linie); background: var(--papier); }

.angaben { display: grid; grid-template-columns: minmax(0, 9rem) minmax(0, 1fr); gap: 10px 20px; margin: 0; }
.angaben dt { color: var(--leise); font-size: 1rem; padding-top: .15em; }
.angaben dd { margin: 0; }
.angaben dd b { font-weight: 600; }
.reihe { display: flex; flex-wrap: wrap; gap: 10px 14px; align-items: center; margin-top: 22px; }
.spende { background: var(--sand); border-radius: 8px; padding: 20px; display: grid; gap: 10px; }
.spende h3 { font-size: 1.25rem; }
.spende dl { display: grid; gap: 2px; margin: 0; }
.spende dt { font-size: 1rem; color: var(--leise); margin-top: 8px; }
.spende dd { margin: 0; display: flex; flex-wrap: wrap; align-items: center; gap: 4px 10px; }
.kopier { display: inline-flex; align-items: center; gap: .4em; min-height: 44px; padding: 0 .7rem; border: 1px solid #8a8174; border-radius: 6px; font-size: 1rem; font-weight: 500; transition: background-color var(--farbe-dauer) var(--auf-kurve); }
.kopier:hover { background: rgba(36, 34, 31, .06); }
.kopier[data-kopiert] { border-color: var(--moos); color: var(--moos); }
.beispiel { font-size: .875rem; font-weight: 600; letter-spacing: .06em; text-transform: uppercase; color: var(--leise); border: 1px solid var(--leise); border-radius: 3px; padding: 0 .35em; }

.eintraege { list-style: none; padding: 0; display: grid; gap: 0; }
.eintrag { padding: 18px 0 20px; border-bottom: 1px solid var(--linie); display: grid; gap: 6px; }
.eintrag:first-child { padding-top: 0; }
.eintrag header { display: flex; flex-wrap: wrap; align-items: baseline; gap: 2px 12px; }
.eintrag header b { font: 600 1.125rem/1.3 var(--serif); }
.eintrag time { font-size: 1rem; color: var(--leise); }
.eintrag.wartet { padding: 16px; border: 1px dashed #8a8174; border-radius: 6px; margin-bottom: 18px; }
.marke-wartet { font-size: 1rem; font-weight: 600; color: var(--leise); }
.formular { background: var(--sand); border-radius: 8px; padding: 20px; display: grid; gap: 10px; }
.formular h3 { font-size: 1.25rem; }
.formular label { display: block; font-size: 1rem; font-weight: 500; margin-top: 4px; }
.formular input, .formular textarea { display: block; width: 100%; min-height: 48px; margin-top: 4px; padding: 10px 12px; border: 1px solid #8a8174; border-radius: 6px; background: var(--papier); font-size: 1.0625rem; line-height: 1.5; }
.formular textarea { min-height: 9rem; resize: vertical; }
.formular [aria-invalid="true"] { border: 2px solid var(--fehler); }
.hilfe { font-size: 1rem; color: var(--leise); line-height: 1.45; margin-top: 4px; }
.feldfehler { color: var(--fehler); font-size: 1rem; font-weight: 500; margin-top: 4px; }
.fehlerliste { border: 2px solid var(--fehler); border-radius: 6px; padding: 12px 14px; background: var(--papier); }
.fehlerliste p { font-weight: 600; font-size: 1.0625rem; }
.fehlerliste ul { margin: 4px 0 0; padding-left: 1.1rem; }
.fehlerliste a { color: var(--fehler); font-weight: 500; font-size: 1rem; }
.formular .knopf { width: 100%; margin-top: 6px; }
.felder { display: grid; gap: 6px; }
.felder[hidden] { display: none; }
.bestaetigung:empty { display: none; }
.bestaetigung { display: grid; gap: 8px; }
.bestaetigung:focus { outline: none; }
.bestaetigung p:first-child { font: 400 1.375rem/1.35 var(--serif); }

.kerzen { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 24rem); gap: 32px clamp(24px, 4vw, 64px); align-items: start; }
.zahl-zeile .zahl-gross { font: 300 clamp(4rem, 3rem + 4vw, 6rem)/1 var(--serif); font-variant-numeric: tabular-nums lining-nums; }
.zahl-zeile { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 18px; }
.zahl-zeile span { font-family: var(--serif); font-size: 1.5rem; }
.namen { margin-top: 14px; font-size: 1.0625rem; }

.seite-info { border-top: 1px solid var(--linie); padding: 28px 0 48px; display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 20px clamp(24px, 4vw, 64px); }
.seite-info > div { display: grid; gap: 10px; align-content: start; }
.seite-info h2 { font-size: 1.375rem; font-weight: 600; }
.seite-info p { font-size: 1.0625rem; line-height: 1.6; }
.ausgefuehrt { padding: 16px; border: 1px solid var(--linie); border-radius: 6px; }

.still-abschnitt { padding: 32px 0; border-top: 1px solid var(--linie); }
.still-abschnitt h2 { font-size: 1.75rem; margin-bottom: 20px; }
.still-abschnitt .blatt { max-width: 46rem; }
.ohne-buch { background: var(--sand); border-radius: 8px; padding: 20px; display: grid; gap: 8px; }
.ohne-buch h3 { font-size: 1.25rem; }
.abgelaufen { padding: 48px 0 72px; display: grid; gap: 16px; max-width: 44rem; }

.fuss { background: var(--schiefer); color: var(--auf-schiefer); }
.fuss-innen { max-width: 1200px; margin: 0 auto; padding: 32px clamp(16px, 3vw, 40px); display: flex; flex-wrap: wrap; gap: 12px 40px; font-size: 1rem; }
.fuss-innen p { color: var(--auf-schiefer-leise); }
.fuss .platzhalter { border-color: #8f897f; color: var(--auf-schiefer-leise); margin-top: 6px; }

/* Demo-Pult */
.pult { position: fixed; right: 14px; bottom: 14px; z-index: 50; width: min(330px, calc(100vw - 28px)); display: grid; gap: 8px; padding: 12px 14px; background: rgba(43, 45, 44, .97); color: var(--auf-schiefer); border: 1px solid rgba(244, 241, 235, .3); border-radius: 8px; font: 500 .75rem/1.4 var(--sans); }
.pult .zeile { display: flex; flex-wrap: wrap; align-items: center; gap: 5px; }
.pult .zeile > span { width: 100%; letter-spacing: .06em; text-transform: uppercase; color: var(--auf-schiefer-leise); }
.pult button { min-height: 30px; padding: 0 9px; border: 1px solid rgba(244, 241, 235, .45); border-radius: 4px; font: inherit; color: var(--auf-schiefer); }
.pult button[aria-pressed="true"] { background: var(--auf-schiefer); color: var(--schiefer); }
.pult button[disabled] { opacity: .55; cursor: default; }
.pult .werte { color: var(--auf-schiefer-leise); }
.pult [hidden] { display: none; }

@media (max-width: 900px) {
  .liste-kopf, .zwei, .kerzen, .seite-info { grid-template-columns: minmax(0, 1fr); }
  .anzeige { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }
  .anzeige .kopfteil { grid-column: 1 / -1; }
  .anzeige .pfeil { display: none; }
  .navi { display: none; }
}
@media (max-width: 760px) {
  .gedenk-kopf { grid-template-columns: minmax(0, 1fr); }
  .gedenk-kopf figure { order: 2; max-width: 15rem; }
  .gedenk-kopf { padding-top: 12px; }
  .werkzeuge { grid-template-columns: minmax(0, 1fr); }
  .reiter [role="tab"] { flex: 1 1 9rem; padding: 8px 12px; border: 1px solid var(--linie); border-radius: 6px; margin: 0; }
  .reiter [role="tab"][aria-selected="true"] { border-color: var(--schiefer); box-shadow: inset 0 -3px 0 var(--schiefer); }
  .reiter { border: 0; }
}
@media (max-width: 560px) {
  .anzeige { grid-template-columns: minmax(0, 1fr); }
  .angaben { grid-template-columns: minmax(0, 1fr); gap: 0 0; }
  .angaben dd { margin-bottom: 10px; }
}

:root.reduziert *, :root.reduziert *::before, :root.reduziert *::after { transition-duration: .01ms !important; animation-duration: .01ms !important; }
@media (hover: none) {
  .linie .pfeil-schaft { opacity: 1; }
  .anzeige:hover { background: none; }
}
@media (forced-colors: active) {
  button, [role=button], [role=tab], [role=switch], input, select, textarea { border: 1px solid ButtonText !important; }
  .reiter [role="tab"][aria-selected="true"] { outline: 2px solid Highlight; }
  .anzeige:has(h2 a:focus-visible) { outline: 3px solid Highlight; }
}
.vorschau body { overflow: hidden; }
.vorschau .sprung { display: none !important; }
</style>
</head>
<body>
<div class="sprung"><a href="#inhalt">Zum Inhalt springen</a></div>
<div class="sofort dunkel"><p><b>Im Trauerfall</b> <span>Tag und Nacht</span> <a href="tel:+498999998120">089 99998 120</a></p></div>
<header class="kopf">
  <a class="marke" href="#"><b>Steinlinde</b><span>Bestattungshaus · München-Pasing</span></a>
  <nav class="navi" aria-label="Hauptmenü"><ul><li><a href="#">Im Trauerfall</a></li><li><a href="#">Bestattungsarten</a></li><li><a href="#">Kosten</a></li><li><a href="#">Vorsorge</a></li><li><a href="#" aria-current="page">Traueranzeigen</a></li><li><a href="#">Über uns</a></li></ul></nav>
</header>
<main id="inhalt" tabindex="-1"><div class="huelle" id="buehne"></div></main>
<footer class="fuss dunkel"><div class="fuss-innen">
  <p><b>Bestattungshaus Steinlinde</b><br>Steinlindenweg 4, 81241 München<br>Büro werktags 8 bis 18 Uhr: 089 99998 121</p>
  <p>Impressum · Datenschutz · Erklärung zur Barrierefreiheit<br><span class="platzhalter">Platzhalter: Pflichtangaben vom Kunden prüfen lassen</span></p>
  <p>Beispielseite: Alle Personen, Daten und Texte sind erfunden. Die Nummern stammen aus dem Bereich, den die Bundesnetzagentur für Medienproduktionen freihält.</p>
</div></footer>

<div class="pult" role="group" aria-label="Steuerung der Demo">
  <div class="zeile" role="group" aria-label="Variante"><span>Variante</span>
    <button type="button" data-v="liste" aria-pressed="true">Liste</button>
    <button type="button" data-v="gedenkseite" aria-pressed="false">Gedenkseite</button>
    <button type="button" data-v="still" aria-pressed="false">Still</button>
  </div>
  <div class="zeile" role="group" aria-label="Zustände"><span>Zustand</span>
    <button type="button" id="p-leer" aria-pressed="false">Liste leer</button>
    <button type="button" id="p-foto" aria-pressed="false">Ohne Foto</button>
    <button type="button" id="p-noindex" aria-pressed="false">Nicht in Suchmaschinen</button>
    <button type="button" id="p-ablauf" aria-pressed="false">Abgelaufen</button>
    <button type="button" id="p-frei" disabled>Eintrag freigeben</button>
  </div>
  <p class="werte">Reiter und Farben 150 ms · nichts bewegt sich von selbst · Kerzenzahl ohne Hochzählen · Kopier-Rückmeldung 2000 ms</p>
</div>

<script>
(() => {
  const h = document.documentElement;
  const reduziert = () => h.classList.contains('reduziert');
  const $ = (s, r = document) => r.querySelector(s);
  const $$ = (s, r = document) => [...r.querySelectorAll(s)];
  const q = new URLSearchParams(location.search);
  const VARIANTEN = ['liste', 'gedenkseite', 'still'];
  const esc = t => String(t).replace(/[&<>"]/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[c]));

  /* ---------- Datum: alles relativ zu heute (Europe/Berlin), Rechnung auf UTC-Mittag ---------- */
  const heuteTeile = Object.fromEntries(new Intl.DateTimeFormat('en-CA', { timeZone: 'Europe/Berlin', year: 'numeric', month: '2-digit', day: '2-digit' }).formatToParts(new Date()).map(p => [p.type, p.value]));
  const HEUTE = Date.UTC(+heuteTeile.year, +heuteTeile.month - 1, +heuteTeile.day, 12);
  const tag = n => new Date(HEUTE + n * 864e5);
  const F = o => new Intl.DateTimeFormat('de-DE', Object.assign({ timeZone: 'UTC' }, o));
  const kurz = d => F({ day: '2-digit', month: '2-digit', year: 'numeric' }).format(d);
  const lang = d => F({ day: 'numeric', month: 'long', year: 'numeric' }).format(d).replace(/ /g, '\u00a0');
  const wtLang = d => F({ weekday: 'long', day: 'numeric', month: 'long' }).format(d);
  const wtKurz = d => F({ weekday: 'short', day: '2-digit', month: '2-digit' }).format(d).replace(',', ',');
  const iso = d => d.toISOString().slice(0, 10);
  const geboren = a => new Date(Date.UTC(a[0], a[1] - 1, a[2], 12));

  /* ---------- Daten (alle Personen erfunden) ---------- */
  const PERSONEN = [
    { id: 'amannsberger', vorname: 'Hedwig', name: 'Amannsberger', geb: 'Lechner', geboren: [1937, 5, 3], tod: -7, aus: 'Pasing',
      feier: { tag: 4, zeit: '11.00', ort: 'Feierhalle im Bestattungshaus Steinlinde', adresse: 'Steinlindenweg 4, 81241 München', zugang: 'Ohne Stufen. Parkplätze im Hof.', danach: 'Die Urne wird danach im engsten Familienkreis beigesetzt.' },
      anzeige: { oben: 'In Liebe und Dankbarkeit nehmen wir Abschied von', satz: 'Sie hat uns mit ihrer Ruhe, ihrem Garten und ihrem Lachen beschenkt.', familie: ['In stiller Trauer', 'Thomas und Elke Amannsberger', 'Lena, Jakob und Paul', 'sowie alle Angehörigen'] },
      foto: true, anzeigeBild: true, kondolenzbuch: true,
      kondolenzen: [
        { name: 'Gerda und Hans Rieger', tag: -5, text: 'Liebe Elke, lieber Thomas, wir denken an euch. Eure Mutter hat uns als Erste in der Siedlung willkommen geheißen. Ihren Garten und ihren Zwetschgendatschi vergessen wir nicht.' },
        { name: 'Monika Sedlmeier', tag: -4, text: 'Ich habe Frau Amannsberger zwölf Jahre lang in der Stadtteilbücherei erlebt, immer mit einem freundlichen Wort. Mein herzliches Beileid.' },
        { name: 'Die Nachbarn aus dem Lindenweg', tag: -3, text: 'In stiller Anteilnahme.' }
      ],
      kerzen: { zahl: 41, namen: ['Gerda und Hans', 'Monika S.', 'Familie Ostler', 'die Turngruppe vom Mittwoch', 'Lisa', 'Ilse und Kurt', 'Anni', 'Rosi W.', 'die Nachbarn aus dem Lindenweg', 'Christa', 'Sabine und Tom', 'Familie Gruber'] },
      spende: { empfaenger: 'Förderverein Hospiz am Würmbogen e.\u00a0V.', iban: 'DE00 0000 0000 0000 0000 00', zweck: 'Spende Hedwig Amannsberger' } },
    { id: 'leutgeb', vorname: 'Konrad', name: 'Leutgeb', geboren: [1949, 9, 14], tod: -4, aus: 'Obermenzing',
      feier: { tag: 7, zeit: '14.00', ort: 'Ruhewald am Hochanger, Treffpunkt am Forsthaus', adresse: 'Parkplatz am Waldrand, Zufahrt ausgeschildert', zugang: 'Der Weg zum Baum ist eben und mit dem Rollstuhl befahrbar.', danach: 'Baumbestattung. Bitte festes Schuhwerk.' },
      anzeige: { oben: 'Still und leise ist er gegangen, so wie er gelebt hat.', satz: 'Wir vermissen dich.', familie: ['Gabriele Leutgeb', 'Martin und Sophie mit Familien'] },
      foto: false, anzeigeBild: false, kondolenzbuch: false, noindex: true },
    { id: 'pichlmair', vorname: 'Marianne', name: 'Pichlmair', geb: 'Strasser', geboren: [1942, 1, 22], tod: -9, aus: 'Pasing',
      feier: { tag: 1, zeit: '13.00', ort: 'Feierhalle im Bestattungshaus Steinlinde', adresse: 'Steinlindenweg 4, 81241 München', zugang: 'Ohne Stufen. Parkplätze im Hof.' },
      anzeige: { oben: 'Nach einem langen, erfüllten Leben ist sie friedlich eingeschlafen.', satz: '', familie: ['Georg Pichlmair', 'Andrea und Michael mit Familien'] },
      kondolenzbuch: true, kondolenzen: [{ name: 'Renate und Fritz Obermaier', tag: -6, text: 'Liebe Familie Pichlmair, in Gedanken sind wir bei Ihnen.' }], kerzen: { zahl: 17, namen: ['Renate und Fritz', 'Familie Wenger'] } },
    { id: 'scholler', vorname: 'Ferdinand', name: 'Scholler', geboren: [1958, 7, 7], tod: -12, aus: 'Laim', feier: { art: 'familie' },
      anzeige: { oben: 'Viel zu früh.', satz: '', familie: ['In Liebe', 'Claudia', 'Felix und Johanna'] },
      kondolenzbuch: true, kondolenzen: [], kerzen: { zahl: 63, namen: [] } },
    { id: 'brandhuber', vorname: 'Luise', name: 'Brandhuber', geb: 'Mayr', geboren: [1931, 11, 30], tod: -15, aus: 'Pasing',
      feier: { tag: -4, zeit: '10.00', ort: 'Aussegnungshalle am Friedhof Würmbogen', adresse: 'Am Würmbogen 12, 81241 München' },
      anzeige: { oben: 'Sie hat ein Leben lang für andere gesorgt. Jetzt darf sie ruhen.', satz: '', familie: ['Ihre Kinder, Enkel und Urenkel'] },
      kondolenzbuch: true, kondolenzen: [{ name: 'Pfarrbüro und Seniorenkreis', tag: -10, text: 'Wir danken für viele Jahre Mitarbeit und denken an die Familie.' }], kerzen: { zahl: 88, namen: ['der Seniorenkreis'] } },
    { id: 'zankl', vorname: 'Emil', name: 'Zankl', geboren: [1964, 4, 2], tod: -3, aus: 'Gräfelfing', feier: { art: 'folgt' },
      anzeige: { oben: 'Plötzlich und unerwartet.', satz: '', familie: ['Petra Zankl', 'Lukas'] },
      kondolenzbuch: true, kondolenzen: [], kerzen: { zahl: 9, namen: [] } },
    { id: 'haselsteiner', vorname: 'Ingrid', name: 'Haselsteiner', geb: 'Kerschl', geboren: [1950, 6, 15], tod: -6, aus: 'Aubing',
      feier: { tag: 5, zeit: '10.00', ort: 'Aussegnungshalle am Friedhof Würmbogen', adresse: 'Am Würmbogen 12, 81241 München' },
      anzeige: { oben: 'Danke für alles.', satz: '', familie: ['Werner Haselsteiner', 'Katrin und Stefan'] },
      kondolenzbuch: true, kondolenzen: [], kerzen: { zahl: 24, namen: [] } }
  ];
  const P = id => PERSONEN.find(p => p.id === id);
  const voll = p => p.vorname + ' ' + p.name;
  const LAUFZEIT_TAGE = 92;

  /* ---------- Zustand ---------- */
  const zustand = { v: VARIANTEN.includes(q.get('v')) ? q.get('v') : 'liste', p: q.get('p'), leer: false, ohneFoto: false, noindexHand: null, abgelaufen: false,
    suche: '', sort: 'tod', reiter: 0, extra: {} };
  if (!P(zustand.p)) zustand.p = zustand.v === 'still' ? 'leutgeb' : 'amannsberger';
  const ex = id => zustand.extra[id] || (zustand.extra[id] = { wartet: null, frei: [], kerze: false, kerzenName: '' });
  function kerzeGeladen(id) { try { return localStorage.getItem('steinlinde-kerze-' + id) === '1'; } catch (e) { return false; } }

  function feierInfo(p) {
    const f = p.feier;
    if (f.art === 'familie') return { status: 'familie', kurz: 'Im engsten Familienkreis', satz: 'Die Trauerfeier findet auf Wunsch der Familie im engsten Familienkreis statt.' };
    if (f.art === 'folgt') return { status: 'folgt', kurz: 'Termin folgt', satz: 'Der Termin der Trauerfeier folgt. Er steht hier, sobald die Familie ihn festgelegt hat.' };
    const d = tag(f.tag);
    if (f.tag < 0) return { status: 'vorbei', d, kurz: 'Hat am ' + kurz(d).slice(0, 6) + ' stattgefunden', satz: 'Die Trauerfeier hat am ' + wtLang(d) + ' stattgefunden.' };
    return { status: 'kommt', d, kurz: wtKurz(d) + ' · ' + f.zeit + ' Uhr', satz: 'Trauerfeier am ' + wtLang(d) + ', um ' + f.zeit + ' Uhr, ' + f.ort + '.' };
  }
  function kondoZahl(p) { return p.kondolenzen.length + ex(p.id).frei.length; }
  function kerzenZahl(p) { return p.kerzen.zahl + (ex(p.id).kerze ? 1 : 0); }
  const pfeil = '<svg width="20" height="12" viewBox="0 0 20 12" aria-hidden="true" focusable="false"><path class="pfeil-schaft" d="M1 6h16" stroke="currentColor" stroke-width="1.5"/><path class="pfeil-spitze" d="M12 1.5 16.5 6 12 10.5" fill="none" stroke="currentColor" stroke-width="1.5"/></svg>';
  const haken = '<svg width="16" height="16" viewBox="0 0 16 16" aria-hidden="true" focusable="false"><path d="M3 8.5 6.5 12 13 4.5" fill="none" stroke="currentColor" stroke-width="1.8"/></svg>';
  const kopierZeichen = '<svg width="16" height="16" viewBox="0 0 16 16" aria-hidden="true" focusable="false"><rect x="5" y="5" width="8.5" height="8.5" rx="1.5" fill="none" stroke="currentColor" stroke-width="1.4"/><path d="M3 10.5V3.5A1 1 0 0 1 4 2.5h6.5" fill="none" stroke="currentColor" stroke-width="1.4"/></svg>';

  /* ---------- Liste ---------- */
  const norm = s => s.toLowerCase().replace(/ß/g, 'ss').replace(/ä/g, 'a').replace(/ö/g, 'o').replace(/ü/g, 'u').normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/ae/g, 'a').replace(/oe/g, 'o').replace(/ue/g, 'u');
  function renderListe() {
    $('#buehne').innerHTML = `
      <nav class="pfad" aria-label="Sie sind hier"><a href="#">Start</a> / <span aria-current="page">Traueranzeigen</span></nav>
      <div class="liste-kopf"><div><h1 id="titel" tabindex="-1">Traueranzeigen</h1>
        <p>Hier veröffentlichen wir Traueranzeigen im Auftrag der Familien: wann und wo die Trauerfeier ist, und, wenn die Familie es möchte, ein Kondolenzbuch.</p></div>
        <p class="klein">Jede Seite bleibt drei Monate online. Eine Anzeige fehlt? Manche Familien veröffentlichen keine. Fragen Sie uns gern: <a href="tel:+498999998121" class="tel-nr">089 99998 121</a></p></div>
      <div class="werkzeuge" role="search">
        <div><label for="suche">Nach Namen suchen</label><div class="suchfeld"><input id="suche" type="search" autocomplete="off" spellcheck="false" aria-describedby="treffer" value="${esc(zustand.suche)}">
          <button class="leeren" type="button" id="leeren" aria-label="Suche leeren"${zustand.suche ? '' : ' hidden'}><svg width="16" height="16" viewBox="0 0 16 16" aria-hidden="true" focusable="false"><path d="M3 3l10 10M13 3 3 13" stroke="currentColor" stroke-width="1.6"/></svg></button></div></div>
        <div><label for="sort">Sortieren</label><select id="sort">
          <option value="tod">Neueste Anzeigen zuerst</option><option value="feier">Nächste Trauerfeier zuerst</option><option value="name">Name, A bis Z</option></select></div>
        <p class="treffer" id="treffer" role="status"></p>
      </div>
      <div id="ergebnis"></div>`;
    $('#sort').value = zustand.sort;
    $('#suche').addEventListener('input', e => { zustand.suche = e.target.value; $('#leeren').hidden = !zustand.suche; filtern(); });
    $('#leeren').addEventListener('click', () => { zustand.suche = ''; $('#suche').value = ''; $('#leeren').hidden = true; filtern(); $('#suche').focus(); });
    $('#sort').addEventListener('change', e => { zustand.sort = e.target.value; filtern(); });
    filtern();
  }
  function filtern() {
    const ziel = $('#ergebnis'); if (!ziel) return;
    const alle = zustand.leer ? [] : PERSONEN.slice();
    const s = norm(zustand.suche.trim());
    let liste = alle.filter(p => !s || norm(voll(p) + ' ' + (p.geb || '')).includes(s));
    const feierWert = p => { const f = feierInfo(p); return f.status === 'kommt' ? p.feier.tag : f.status === 'folgt' ? 500 : 1000 - (p.feier.tag || 0); };
    if (zustand.sort === 'tod') liste.sort((a, b) => b.tod - a.tod);
    if (zustand.sort === 'feier') liste.sort((a, b) => feierWert(a) - feierWert(b));
    if (zustand.sort === 'name') liste.sort((a, b) => a.name.localeCompare(b.name, 'de'));
    const t = $('#treffer');
    if (!alle.length) {
      t.textContent = '';
      ziel.innerHTML = `<div class="leer"><h2>Zurzeit keine Traueranzeigen</h2><p>Im Moment gibt es keine Anzeige, die eine Familie hier veröffentlichen möchte.</p><p>Suchen Sie den Termin einer Trauerfeier? Rufen Sie uns an: <a href="tel:+498999998121" class="tel-nr">089 99998 121</a>. Wir sagen Ihnen, was wir sagen dürfen.</p></div>`;
      return;
    }
    if (!liste.length) {
      t.textContent = 'Keine Traueranzeige für „' + zustand.suche.trim() + '“';
      ziel.innerHTML = `<div class="leer"><h2>Keine Anzeige gefunden</h2><p>Für „${esc(zustand.suche.trim())}“ gibt es hier keine Traueranzeige. Vielleicht ist der Name anders geschrieben, oder die Familie hat keine Anzeige veröffentlicht.</p><p><button class="knopf knopf-rand" type="button" id="alle">Alle Anzeigen zeigen</button></p></div>`;
      $('#alle').addEventListener('click', () => { zustand.suche = ''; $('#suche').value = ''; $('#leeren').hidden = true; filtern(); $('#suche').focus(); });
      return;
    }
    t.textContent = s ? liste.length + (liste.length === 1 ? ' Traueranzeige' : ' Traueranzeigen') + ' für „' + zustand.suche.trim() + '“' : liste.length + ' Traueranzeigen';
    ziel.innerHTML = `<ul class="anzeigen">${liste.map(p => {
      const f = feierInfo(p), g = geboren(p.geboren), d = tag(p.tod);
      return `<li class="anzeige"><div class="kopfteil"><h2><a href="?v=${p.kondolenzbuch ? 'gedenkseite' : 'still'}&amp;p=${p.id}" data-person="${p.id}">${esc(voll(p))}</a></h2>${p.geb ? `<p class="geb">geb. ${esc(p.geb)}</p>` : ''}</div>
        <dl class="daten"><dt>Geboren</dt><dd><time datetime="${iso(g)}">${kurz(g)}</time></dd><dt>Gestorben</dt><dd><time datetime="${iso(d)}">${kurz(d)}</time></dd><dt>aus</dt><dd>${esc(p.aus)}</dd></dl>
        <p class="feier-kurz">${f.status === 'kommt' ? `<b>${f.kurz}</b>${esc(p.feier.ort)}` : `<span class="still">${f.kurz}</span>`}</p>
        <span class="pfeil" aria-hidden="true">${pfeil}</span></li>`; }).join('')}</ul>`;
    $$('[data-person]', ziel).forEach(a => a.addEventListener('click', e => {
      e.preventDefault(); const p = P(a.dataset.person);
      zustand.p = p.id; zustand.reiter = 0; setzeVariante(p.kondolenzbuch ? 'gedenkseite' : 'still', true); $('#titel').focus();
    }));
  }

  /* ---------- Gedenkseite ---------- */
  function fotoSvg(p) {
    return `<svg viewBox="0 0 440 520" role="img" aria-label="Blick über einen See im Herbst, Schilf im Vordergrund, dahinter bewaldete Hügel. Ein Bild, das die Familie ausgewählt hat.">
      <rect width="440" height="520" fill="#e6e7e0"/><path d="M0 210c50-30 110-46 170-30 50-22 120-30 180-6 34-10 66-8 90 2v74H0Z" fill="#bcc7bf"/>
      <path d="M0 236c70-18 140-20 210-6 70-16 160-16 230 2v44H0Z" fill="#9fb0a4"/><rect y="276" width="440" height="160" fill="#cfd8d3"/>
      <rect x="40" y="300" width="120" height="3" fill="#e3e9e5"/><rect x="220" y="324" width="160" height="3" fill="#e3e9e5"/><rect x="90" y="356" width="90" height="3" fill="#e3e9e5"/><rect x="260" y="388" width="110" height="3" fill="#e3e9e5"/>
      <path d="M0 430c80-16 170-18 260-4 70-10 130-8 180 0v94H0Z" fill="#a6b49b"/>
      <g stroke="#7d7c62" stroke-width="2.4" fill="none"><path d="M30 520c-2-60 4-110 16-150M48 520c0-50 8-96 22-126M70 520c2-44-2-90-14-122M330 520c2-50 10-90 24-120M352 520c0-46 6-84 18-112M376 520c-4-40-12-74-26-98M400 520c2-36 8-66 20-90"/></g>
      <g fill="#9a8f6a"><rect x="42" y="362" width="7" height="26" rx="3"/><rect x="66" y="390" width="6" height="22" rx="3"/><rect x="351" y="394" width="7" height="24" rx="3"/><rect x="368" y="404" width="6" height="20" rx="3"/></g></svg>`;
  }
  function anzeigeSvg(p) {
    const g = geboren(p.geboren), d = tag(p.tod), a = p.anzeige;
    const umbruch = (t, n) => { const w = t.split(' '), z = ['']; w.forEach(x => { const l = z.length - 1; if (z[l] && (z[l] + ' ' + x).length > n) z.push(x); else z[l] = z[l] ? z[l] + ' ' + x : x; }); return z; };
    const zeilen = [].concat(umbruch(a.oben, 36).map(t => [t, 15, 0]), [['', 10, 0], [voll(p), voll(p).length > 18 ? 26 : 30, 1], [p.geb ? 'geb. ' + p.geb : '', 15, 0], [lang(g) + ' – ' + lang(d), 14, 0], ['', 14, 0]],
      a.satz ? umbruch(a.satz, 40).map(t => [t, 14, 0]) : [], [['', 12, 0]], a.familie.map(f => [f, 14, 0]));
    let y = 70; const tx = zeilen.map(([t, s, gross]) => { y += s * 1.75; return t ? `<text x="200" y="${y}" font-size="${s}" text-anchor="middle"${gross ? ' font-weight="400"' : ''}>${esc(t)}</text>` : ''; }).join('');
    const alt = [a.oben, voll(p), p.geb ? 'geboren ' + p.geb : '', lang(g) + ' bis ' + lang(d), a.satz].concat(a.familie).filter(Boolean).join('. ');
    return `<svg viewBox="0 0 400 520" role="img" aria-label="Traueranzeige als Bild: ${esc(alt)}"><rect width="400" height="520" fill="#fbfaf7"/><rect x="18" y="18" width="364" height="484" fill="none" stroke="#5a554d" stroke-width="1"/>
      <g font-family="'Source Serif 4', Georgia, serif" fill="#24221f">${tx}</g></svg>`;
  }
  function blattHtml(p) {
    const g = geboren(p.geboren), d = tag(p.tod), a = p.anzeige;
    return `<div class="blatt"><p class="ser">${esc(a.oben)}</p><p class="gross">${esc(voll(p))}</p>${p.geb ? `<p class="ser">geb. ${esc(p.geb)}</p>` : ''}
      <p><time datetime="${iso(g)}">${lang(g)}</time> – <time datetime="${iso(d)}">${lang(d)}</time></p>${a.satz ? `<span class="trenn" aria-hidden="true"></span><p>${esc(a.satz)}</p>` : ''}
      <span class="trenn" aria-hidden="true"></span>${a.familie.map(f => `<p>${esc(f)}</p>`).join('')}</div>`;
  }
  function feierHtml(p, ueberschrift) {
    const f = feierInfo(p), x = p.feier;
    let html = ueberschrift ? `<h2>Trauerfeier</h2>` : '';
    if (f.status !== 'kommt') html += `<p>${f.satz}</p>`;
    else html += `<dl class="angaben"><dt>Tag</dt><dd><b><time datetime="${iso(f.d)}T${x.zeit.replace('.', ':')}">${wtLang(f.d)}</time></b></dd><dt>Uhrzeit</dt><dd>${x.zeit} Uhr</dd>
      <dt>Ort</dt><dd>${esc(x.ort)}<br>${esc(x.adresse)}</dd>${x.zugang ? `<dt>Zugang</dt><dd>${esc(x.zugang)}</dd>` : ''}${x.danach ? `<dt>Danach</dt><dd>${esc(x.danach)}</dd>` : ''}</dl>
      <div class="reihe"><button class="knopf knopf-rand" type="button" data-ics><svg width="18" height="18" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><rect x="3.5" y="5" width="17" height="15" rx="2" fill="none" stroke="currentColor" stroke-width="1.6"/><path d="M3.5 10h17M8 3v4M16 3v4" stroke="currentColor" stroke-width="1.6"/></svg>In den Kalender (.ics)</button>
      <a class="linie" href="#">Anfahrt ${pfeil}</a></div>`;
    return html;
  }
  function spendeHtml(p) {
    if (!p.spende) return '';
    const s = p.spende;
    return `<aside class="spende" aria-labelledby="spende-titel"><h3 id="spende-titel">Spenden statt Blumen</h3>
      <p>Im Sinne der Verstorbenen bittet die Familie statt Blumen um eine Spende.</p>
      <dl><dt>Empfänger</dt><dd>${esc(s.empfaenger)}</dd>
      <dt>IBAN <span class="beispiel">Beispiel</span></dt><dd><span class="tel-nr">${s.iban}</span> <button class="kopier" type="button" data-kopie="${s.iban.replace(/ /g, '')}" data-name="IBAN">${kopierZeichen}<span>IBAN kopieren</span></button></dd>
      <dt>Verwendungszweck</dt><dd>${esc(s.zweck)} <button class="kopier" type="button" data-kopie="${esc(s.zweck)}" data-name="Verwendungszweck">${kopierZeichen}<span>Kopieren</span></button></dd></dl>
      <p class="klein">Über diese Seite wird kein Geld gesammelt. Bitte überweisen Sie direkt an den Verein.</p></aside>`;
  }
  function eintragHtml(e, wartet) {
    const d = tag(e.tag);
    return `<li class="eintrag${wartet ? ' wartet' : ''}"><header><b>${esc(e.name)}</b>${wartet ? '<span class="marke-wartet">Wartet auf Freigabe · nur für Sie sichtbar</span>' : `<time datetime="${iso(d)}">${kurz(d)}</time>`}</header><p>${esc(e.text)}</p></li>`;
  }
  function kondoHtml(p) {
    const x = ex(p.id), alle = x.frei.concat(p.kondolenzen.slice().reverse());
    return `<div class="zwei"><div><h2 class="nur-sr">Kondolenzen</h2>
      <ul class="eintraege" id="eintraege">${x.wartet ? eintragHtml(x.wartet, true) : ''}${alle.map(e => eintragHtml(e)).join('')}</ul>
      ${!alle.length && !x.wartet ? '<p>Noch keine Einträge. Sie können der Familie hier als Erste oder Erster schreiben.</p>' : ''}</div>
      <form class="formular" id="kondo" novalidate aria-labelledby="kondo-titel"><h3 id="kondo-titel">Der Familie schreiben</h3>
        <p class="klein">Jeder Eintrag wird vor der Veröffentlichung von uns gelesen. Das dauert meist bis zum nächsten Werktag.</p>
        <div class="bestaetigung" id="kondo-ok" role="status" tabindex="-1"></div>
        <div class="fehlerliste" id="kondo-fehler" tabindex="-1" hidden><p>Bitte prüfen Sie:</p><ul></ul></div>
        <div class="felder">
          <div><label for="k-name">Ihr Name</label><input id="k-name" name="name" autocomplete="name" aria-describedby="k-name-h k-name-f"><p class="hilfe" id="k-name-h">Wird mit Ihrem Eintrag gezeigt.</p><p class="feldfehler" id="k-name-f" hidden></p></div>
          <div><label for="k-text">Ihre Worte</label><textarea id="k-text" name="text" maxlength="1500" aria-describedby="k-text-h k-text-f"></textarea><p class="hilfe" id="k-text-h">Noch 1.500 Zeichen</p><p class="feldfehler" id="k-text-f" hidden></p></div>
          <div><label for="k-mail">Ihre E-Mail</label><input id="k-mail" name="mail" type="email" autocomplete="email" aria-describedby="k-mail-h k-mail-f"><p class="hilfe" id="k-mail-h">Nur für Rückfragen. Wird nicht veröffentlicht.</p><p class="feldfehler" id="k-mail-f" hidden></p></div>
          <button class="knopf knopf-moos" type="submit"><span class="knopf-text">Eintrag senden</span></button>
          <p class="klein">Die Familie sieht alle Einträge. <span class="platzhalter">Platzhalter: Datenschutzhinweis und Löschfrist vom Kunden prüfen lassen</span></p>
        </div></form></div>`;
  }
  function kerzenHtml(p) {
    const x = ex(p.id), n = kerzenZahl(p), namen = p.kerzen.namen.concat(x.kerze && x.kerzenName ? [x.kerzenName] : []);
    const ohne = n - namen.length;
    return `<div class="kerzen"><div><h2 class="nur-sr">Kerzen</h2>
      <p class="zahl-zeile"><span class="zahl-gross" id="kerzen-zahl">${n}</span><span>${n === 1 ? 'stille Kerze' : 'stille Kerzen'}</span></p>
      <p class="namen">${namen.length ? 'Von ' + namen.map(esc).join(', ') + (ohne > 0 ? `, und ${ohne} ohne Namen.` : '.') : 'Alle ohne Namen.'}</p></div>
      <form class="formular" id="kerze" novalidate aria-labelledby="kerze-titel"><h3 id="kerze-titel">Eine Kerze anzünden</h3>
        ${x.kerze ? '' : '<p class="klein">Ein stilles Zeichen für die Familie. Ihre Kerze bleibt vier Wochen sichtbar.</p>'}
        <div class="bestaetigung" id="kerze-ok" role="status" tabindex="-1">${x.kerze ? '<p>Ihre Kerze steht hier.</p><p class="klein">Danke. Pro Gerät ist eine Kerze möglich.</p>' : ''}</div>
        <div class="felder"${x.kerze ? ' hidden' : ''}><div><label for="kz-name">Ihr Name (freiwillig)</label><input id="kz-name" name="name" autocomplete="name" maxlength="60" aria-describedby="kz-h"><p class="hilfe" id="kz-h">Ohne Namen zählt die Kerze still mit.</p></div>
        <button class="knopf knopf-moos" type="submit"><span class="knopf-text">Kerze anzünden</span></button></div></form></div>`;
  }
  function infoHtml(p) {
    const ende = tag(p.tod + LAUFZEIT_TAGE), noindex = istNoindex(p);
    return `<section class="seite-info" aria-labelledby="info-titel"><div><h2 id="info-titel">Über diese Seite</h2>
        <p>Diese Seite bleibt bis zum <time datetime="${iso(ende)}">${lang(ende)}</time> online. Danach löschen wir sie. Die Familie kann sie jederzeit früher löschen lassen.</p>
        ${noindex ? '<p>Auf Wunsch der Familie erscheint diese Seite nicht in Suchmaschinen.</p>' : ''}
        <div class="reihe" style="margin-top:4px"><button class="knopf knopf-rand" type="button" data-kopie="https://www.steinlinde-bestattungen.example/gedenken/${p.id}" data-name="Link">${kopierZeichen}<span>Link zu dieser Seite kopieren</span></button></div></div>
      <div><p class="ausgefuehrt">Die Bestattung begleitet das <b>Bestattungshaus Steinlinde</b>, Steinlindenweg 4, 81241 München. Fragen zur Trauerfeier: <a class="tel-nr" href="tel:+498999998121">089 99998 121</a></p>
        <span class="platzhalter">Platzhalter: Veröffentlichung nur mit Zustimmung der beauftragenden Angehörigen; Nutzungsrecht an Bildern, Löschfrist und Datenschutz vom Kunden prüfen lassen</span></div></section>`;
  }
  const istNoindex = p => zustand.noindexHand === null ? !!p.noindex : zustand.noindexHand;

  function renderGedenk() {
    const p = P(zustand.p), still = zustand.v === 'still' || !p.kondolenzbuch;
    const meta = $('meta[name="robots"]');
    if (istNoindex(p) && !meta) { const m = document.createElement('meta'); m.name = 'robots'; m.content = 'noindex'; document.head.appendChild(m); }
    if (!istNoindex(p) && meta) meta.remove();
    if (zustand.abgelaufen) {
      $('#buehne').innerHTML = `<nav class="pfad" aria-label="Sie sind hier"><a href="#">Start</a> / <a href="?v=liste" data-zurliste>Traueranzeigen</a></nav>
        <div class="abgelaufen"><h1 id="titel" tabindex="-1">Diese Gedenkseite ist nicht mehr online.</h1>
        <p>Die Familie hatte mit uns vereinbart, wie lange die Seite zu sehen ist. Diese Zeit ist vorbei, die Seite und alle Einträge sind gelöscht.</p>
        <p>Wenn Sie Fragen haben oder der Familie etwas schicken möchten, rufen Sie uns an: <a class="tel-nr" href="tel:+498999998121">089 99998 121</a>. Wir geben es weiter.</p>
        <p><a class="linie" href="?v=liste" data-zurliste>Zu den aktuellen Traueranzeigen ${pfeil}</a></p></div>`;
      $$('[data-zurliste]').forEach(a => a.addEventListener('click', e => { e.preventDefault(); setzeVariante('liste', true); $('#titel').focus(); }));
      return;
    }
    const g = geboren(p.geboren), d = tag(p.tod), f = feierInfo(p), mitFoto = p.foto && !zustand.ohneFoto;
    const kopf = `<nav class="pfad" aria-label="Sie sind hier"><a href="#">Start</a> / <a href="?v=liste" data-zurliste>Traueranzeigen</a> / <span aria-current="page">${esc(voll(p))}</span></nav>
      <div class="gedenk-kopf${mitFoto ? '' : ' ohne-bild'}">${mitFoto ? `<figure>${fotoSvg(p)}<figcaption>Ihr Lieblingsplatz am See. Bild: Familie ${esc(p.name)}</figcaption></figure>` : ''}
        <div class="gedenk-text"><p class="rubrik">Wir nehmen Abschied von</p><h1 id="titel" tabindex="-1">${esc(voll(p))}</h1>${p.geb ? `<p class="geb">geb. ${esc(p.geb)}</p>` : ''}
        <p class="lebensdaten"><time datetime="${iso(g)}">${lang(g)}</time> – <time datetime="${iso(d)}">${lang(d)}</time></p>
        <p class="kurzinfo">${f.status === 'kommt' ? `<b>Trauerfeier am ${wtLang(f.d)}, ${p.feier.zeit} Uhr</b><br>${esc(p.feier.ort)}` : f.satz}</p></div></div>`;
    if (still) {
      $('#buehne').innerHTML = kopf + `<section class="still-abschnitt" aria-labelledby="anzeige-titel"><h2 id="anzeige-titel">Traueranzeige</h2>${blattHtml(p)}</section>
        <section class="still-abschnitt" id="feier-abschnitt"><div class="zwei"><div>${feierHtml(p, true)}</div>
          <aside class="ohne-buch" aria-labelledby="ob-titel"><h3 id="ob-titel">Kein öffentliches Kondolenzbuch</h3><p>Die Familie hat sich gegen öffentliche Einträge entschieden. Wenn Sie ihr schreiben möchten, schicken Sie eine Karte an das Bestattungshaus Steinlinde, Steinlindenweg 4, 81241 München. Wir geben sie weiter.</p></aside></div></section>` + infoHtml(p);
    } else {
      const tabs = [['anzeige', 'Anzeige'], ['feier', 'Trauerfeier'], ['kondo', `Kondolenzen (${kondoZahl(p)})`], ['kerzen', `Kerzen (${kerzenZahl(p)})`]];
      const inhalt = {
        anzeige: `<div class="zwei"><div><h2 class="nur-sr">Anzeige</h2>${blattHtml(p)}</div>${p.anzeigeBild ? `<figure class="als-bild">${anzeigeSvg(p)}<figcaption class="klein">Die Anzeige, wie sie in der Zeitung erschien. Derselbe Text steht daneben.</figcaption></figure>` : ''}</div>`,
        feier: `<div class="zwei"><div><h2 class="nur-sr">Trauerfeier</h2>${feierHtml(p, false)}</div>${spendeHtml(p)}</div>`,
        kondo: kondoHtml(p), kerzen: kerzenHtml(p)
      };
      $('#buehne').innerHTML = kopf + `<div class="reiter" role="tablist" aria-label="Gedenkseite für ${esc(voll(p))}">${tabs.map((t, i) => `<button type="button" role="tab" id="tab-${t[0]}" aria-controls="panel-${t[0]}" aria-selected="${i === zustand.reiter}" tabindex="${i === zustand.reiter ? 0 : -1}">${t[1]}</button>`).join('')}</div>
        ${tabs.map((t, i) => `<div role="tabpanel" id="panel-${t[0]}" aria-labelledby="tab-${t[0]}" tabindex="0"${i === zustand.reiter ? '' : ' hidden'}>${inhalt[t[0]]}</div>`).join('')}` + infoHtml(p);
      bindeReiter(); bindeKondo(p); bindeKerze(p);
    }
    $$('[data-zurliste]').forEach(a => a.addEventListener('click', e => { e.preventDefault(); setzeVariante('liste', true); $('#titel').focus(); }));
    $$('[data-ics]').forEach(b => b.addEventListener('click', () => ics(p)));
    $$('[data-kopie]').forEach(b => b.addEventListener('click', () => kopieren(b)));
  }

  function reiterWaehlen(i, fokus) {
    const tabs = $$('[role="tab"]'); if (!tabs.length) return;
    i = (i + tabs.length) % tabs.length; zustand.reiter = i;
    tabs.forEach((t, k) => { const an = k === i; t.setAttribute('aria-selected', an); t.tabIndex = an ? 0 : -1; document.getElementById(t.getAttribute('aria-controls')).hidden = !an; });
    if (fokus) tabs[i].focus();
  }
  function bindeReiter() {
    const liste = $('[role="tablist"]');
    $$('[role="tab"]', liste).forEach((t, i) => t.addEventListener('click', () => reiterWaehlen(i, true)));
    liste.addEventListener('keydown', e => {
      const tabs = $$('[role="tab"]', liste), i = tabs.indexOf(document.activeElement); if (i < 0) return;
      const m = { ArrowRight: 1, ArrowDown: 1, ArrowLeft: -1, ArrowUp: -1 }[e.key];
      if (m) { e.preventDefault(); reiterWaehlen(i + m, true); }
      else if (e.key === 'Home') { e.preventDefault(); reiterWaehlen(0, true); }
      else if (e.key === 'End') { e.preventDefault(); reiterWaehlen(tabs.length - 1, true); }
    });
  }
  function tabText() {
    const p = P(zustand.p), k = $('#tab-kondo'), z = $('#tab-kerzen');
    if (k) k.textContent = `Kondolenzen (${kondoZahl(p)})`;
    if (z) z.textContent = `Kerzen (${kerzenZahl(p)})`;
  }

  /* Kondolenz: Fehlerliste, Senden, Freigabe */
  function feldFehler(feld, text) {
    const f = document.getElementById(feld.id + '-f');
    if (text) { feld.setAttribute('aria-invalid', 'true'); f.textContent = text; f.hidden = false; } else { feld.removeAttribute('aria-invalid'); f.textContent = ''; f.hidden = true; }
  }
  function bindeKondo(p) {
    const form = $('#kondo'); if (!form) return;
    const text = form.elements.text, hilfe = $('#k-text-h');
    const zaehlen = () => { hilfe.textContent = 'Noch ' + (1500 - text.value.length).toLocaleString('de-DE') + ' Zeichen'; };
    text.addEventListener('input', zaehlen);
    form.addEventListener('input', e => { if (e.target.getAttribute('aria-invalid')) feldFehler(e.target, ''); });
    form.addEventListener('submit', e => { e.preventDefault(); kondoSenden(p, false); });
  }
  function kondoSenden(p, auto) {
    const form = $('#kondo'), el = form.elements, fehler = [];
    const pruef = [[el.name, el.name.value.trim() ? '' : 'Bitte geben Sie Ihren Namen an.'],
      [el.text, el.text.value.trim().length >= 3 ? '' : 'Bitte schreiben Sie ein paar Worte.'],
      [el.mail, !el.mail.value.trim() ? 'Bitte geben Sie Ihre E-Mail an. Wir veröffentlichen sie nicht.' : /^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(el.mail.value.trim()) ? '' : 'Die E-Mail sieht unvollständig aus, zum Beispiel name@beispiel.de.']];
    pruef.forEach(([f, t]) => { feldFehler(f, t); if (t) fehler.push([f, t]); });
    const liste = $('#kondo-fehler');
    if (fehler.length) { liste.querySelector('ul').innerHTML = fehler.map(f => `<li><a href="#${f[0].id}">${f[1]}</a></li>`).join(''); liste.hidden = false; if (!auto) liste.focus(); return; }
    liste.hidden = true;
    const k = form.querySelector('[type="submit"]'); k.setAttribute('aria-busy', 'true'); k.querySelector('.knopf-text').textContent = 'Wird gesendet …';
    setTimeout(() => {
      const x = ex(p.id);
      x.wartet = { name: el.name.value.trim(), text: el.text.value.trim(), tag: 0 };
      renderGedenk(); reiterWaehlen(2);
      $('.felder', $('#kondo')).hidden = true;
      $('#kondo-ok').innerHTML = '<p>Danke für Ihre Worte.</p><p>Wir lesen Ihren Eintrag und schalten ihn dann frei, meist bis zum nächsten Werktag. Bis dahin sehen nur Sie ihn hier.</p><button class="knopf knopf-rand" type="button" id="noch-einer">Weiteren Eintrag schreiben</button>';
      $('#noch-einer').addEventListener('click', () => { $('#kondo-ok').innerHTML = ''; $('.felder', $('#kondo')).hidden = false; $('#k-name').focus(); });
      if (!auto) $('#kondo-ok').focus();
      pultZeigen();
    }, 900);
  }
  function freigeben() {
    const p = P(zustand.p), x = ex(p.id); if (!x.wartet) return;
    x.frei.unshift(x.wartet); x.wartet = null;
    const li = $('.eintrag.wartet');
    if (li) { li.outerHTML = eintragHtml(x.frei[0]); }
    tabText(); pultZeigen();
  }
  function bindeKerze(p) {
    const form = $('#kerze'); if (!form) return;
    form.addEventListener('submit', e => { e.preventDefault(); kerzeAnzuenden(p, false); });
  }
  function kerzeAnzuenden(p, auto) {
    const x = ex(p.id); if (x.kerze) return;
    x.kerze = true; x.kerzenName = $('#kz-name').value.trim();
    try { localStorage.setItem('steinlinde-kerze-' + p.id, '1'); } catch (e) {}
    $('#panel-kerzen').innerHTML = kerzenHtml(p); tabText();
    $('#kerze-ok').innerHTML = '<p>Ihre Kerze steht jetzt hier.</p><p class="klein">Danke. Sie bleibt vier Wochen sichtbar. Pro Gerät ist eine Kerze möglich.</p>';
    if (!auto) $('#kerze-ok').focus();
  }

  /* .ics (RFC 5545), Zeit in UTC umgerechnet */
  function berlinNachUtc(y, mo, d, hh, mi) {
    const schaetz = Date.UTC(y, mo - 1, d, hh, mi);
    const t = Object.fromEntries(new Intl.DateTimeFormat('en-CA', { timeZone: 'Europe/Berlin', hourCycle: 'h23', year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' }).formatToParts(new Date(schaetz)).map(x => [x.type, x.value]));
    const alsBerlin = Date.UTC(+t.year, +t.month - 1, +t.day, +t.hour, +t.minute);
    return new Date(schaetz - (alsBerlin - schaetz));
  }
  const icsZeit = d => d.toISOString().replace(/[-:]/g, '').replace(/\.\d{3}/, '');
  const icsText = s => s.replace(/\\/g, '\\\\').replace(/;/g, '\\;').replace(/,/g, '\\,').replace(/\n/g, '\\n');
  function ics(p) {
    const f = feierInfo(p); if (f.status !== 'kommt') return;
    const [hh, mi] = p.feier.zeit.split('.').map(Number), d = f.d;
    const start = berlinNachUtc(d.getUTCFullYear(), d.getUTCMonth() + 1, d.getUTCDate(), hh, mi), ende = new Date(start.getTime() + 60 * 6e4);
    const text = ['BEGIN:VCALENDAR', 'VERSION:2.0', 'PRODID:-//Bestattungshaus Steinlinde//Gedenkseite//DE', 'CALSCALE:GREGORIAN', 'METHOD:PUBLISH', 'BEGIN:VEVENT',
      'UID:trauerfeier-' + p.id + '-' + iso(d) + '@steinlinde-bestattungen.example', 'DTSTAMP:' + icsZeit(new Date()), 'DTSTART:' + icsZeit(start), 'DTEND:' + icsZeit(ende),
      'SUMMARY:' + icsText('Trauerfeier für ' + voll(p)), 'LOCATION:' + icsText(p.feier.ort + ', ' + p.feier.adresse),
      'DESCRIPTION:' + icsText('Gedenkseite: https://www.steinlinde-bestattungen.example/gedenken/' + p.id), 'END:VEVENT', 'END:VCALENDAR'].join('\r\n') + '\r\n';
    const url = URL.createObjectURL(new Blob([text], { type: 'text/calendar;charset=utf-8' }));
    const a = document.createElement('a'); a.href = url; a.download = 'trauerfeier-' + p.vorname.toLowerCase() + '-' + p.id + '.ics';
    document.body.appendChild(a); a.click(); a.remove(); setTimeout(() => URL.revokeObjectURL(url), 1000);
  }

  /* Kopieren mit Rückmeldung (kopieren-feedback, 2000 ms) */
  const meldung = document.createElement('p'); meldung.className = 'nur-sr'; meldung.setAttribute('role', 'status'); document.body.appendChild(meldung);
  function kopieren(b, nurAnzeigen) {
    const text = b.dataset.kopie, span = b.querySelector('span'), alt = b._alt || (b._alt = span.textContent);
    const ok = () => { b.setAttribute('data-kopiert', ''); span.textContent = 'Kopiert'; b.querySelector('svg').outerHTML = haken; meldung.textContent = b.dataset.name + ' kopiert';
      clearTimeout(b._t); b._t = setTimeout(() => { b.removeAttribute('data-kopiert'); span.textContent = alt; b.querySelector('svg').outerHTML = kopierZeichen; meldung.textContent = ''; }, 2000); };
    if (nurAnzeigen) return ok();
    const notweg = () => { const t = document.createElement('textarea'); t.value = text; t.setAttribute('readonly', ''); t.style.position = 'fixed'; t.style.opacity = '0'; document.body.appendChild(t); t.select(); let r = false; try { r = document.execCommand('copy'); } catch (e) {} t.remove(); r ? ok() : (meldung.textContent = 'Kopieren nicht möglich. Bitte markieren Sie den Text von Hand.'); };
    if (navigator.clipboard && window.isSecureContext) navigator.clipboard.writeText(text).then(ok, notweg); else notweg();
  }

  /* ---------- Pult und Varianten ---------- */
  function render() { document.body.dataset.v = zustand.v; zustand.v === 'liste' ? renderListe() : renderGedenk(); pultZeigen(); }
  function setzeVariante(v, schreiben) {
    zustand.v = v; zustand.reiter = 0; zustand.abgelaufen = false; zustand.noindexHand = null; zustand.ohneFoto = false;
    if (v === 'still' && P(zustand.p).kondolenzbuch) zustand.p = 'leutgeb';
    if (v === 'gedenkseite' && !P(zustand.p).kondolenzbuch) zustand.p = 'amannsberger';
    render();
    if (schreiben) { const s = new URLSearchParams(location.search); s.set('v', v); if (v === 'liste') s.delete('p'); else s.set('p', zustand.p); history.replaceState(null, '', '?' + s.toString().replace(/=(?=&|$)/g, '')); }
  }
  function pultZeigen() {
    const v = zustand.v, p = P(zustand.p);
    $$('[data-v]').forEach(b => b.setAttribute('aria-pressed', b.dataset.v === v));
    $('#p-leer').hidden = v !== 'liste'; $('#p-leer').setAttribute('aria-pressed', zustand.leer);
    $('#p-foto').hidden = v !== 'gedenkseite' || !p.foto; $('#p-foto').setAttribute('aria-pressed', zustand.ohneFoto);
    $('#p-noindex').hidden = v === 'liste'; $('#p-noindex').setAttribute('aria-pressed', v !== 'liste' && istNoindex(p));
    $('#p-ablauf').hidden = v === 'liste'; $('#p-ablauf').setAttribute('aria-pressed', zustand.abgelaufen);
    $('#p-frei').hidden = v !== 'gedenkseite'; $('#p-frei').disabled = !(ex(p.id).wartet) || zustand.abgelaufen;
  }
  $$('[data-v]').forEach(b => b.addEventListener('click', () => { stopp(); setzeVariante(b.dataset.v, true); }));
  $('#p-leer').addEventListener('click', () => { zustand.leer = !zustand.leer; filtern(); pultZeigen(); });
  $('#p-foto').addEventListener('click', () => { zustand.ohneFoto = !zustand.ohneFoto; renderGedenk(); pultZeigen(); });
  $('#p-noindex').addEventListener('click', () => { zustand.noindexHand = !istNoindex(P(zustand.p)); renderGedenk(); pultZeigen(); });
  $('#p-ablauf').addEventListener('click', () => { zustand.abgelaufen = !zustand.abgelaufen; renderGedenk(); pultZeigen(); });
  $('#p-frei').addEventListener('click', freigeben);

  /* ---------- Vorführung ---------- */
  let schritte = [], pos = 0, uhrwerk = null, laeuft = false;
  const zuReitern = () => { const r = $('[role="tablist"]'); if (r) scrollTo({ top: r.getBoundingClientRect().top + scrollY - 24 }); };
  function tippe(text) { const s = $('#suche'); s.value = text; s.dispatchEvent(new Event('input', { bubbles: true })); }
  function setzeFeld(id, wert) { const f = $(id); f.value = wert; f.dispatchEvent(new Event('input', { bubbles: true })); }
  function ablauf() {
    if (zustand.v === 'liste') return [
      [1400, () => tippe('P')], [380, () => tippe('Pi')], [380, () => tippe('Pic')], [380, () => tippe('Pich')],
      [2200, () => tippe('')], [900, () => tippe('Me')], [380, () => tippe('Mei')], [380, () => tippe('Meie')], [380, () => tippe('Meier')],
      [2400, () => tippe('')], [900, () => { $('#sort').value = 'feier'; $('#sort').dispatchEvent(new Event('change')); }],
      [2600, () => { $('#sort').value = 'name'; $('#sort').dispatchEvent(new Event('change')); }],
      [2600, () => { $('#sort').value = 'tod'; $('#sort').dispatchEvent(new Event('change')); }], [1400, () => {}]
    ];
    if (zustand.v === 'still') return [
      [1500, () => { const a = $('#feier-abschnitt'); scrollTo({ top: a.getBoundingClientRect().top + scrollY - 24 }); }],
      [2600, () => { const i = $('.seite-info'); scrollTo({ top: i.getBoundingClientRect().top + scrollY - 200 }); }],
      [1200, () => kopieren($('.seite-info [data-kopie]'), true)], [3000, () => scrollTo({ top: 0 })], [1600, () => {}]
    ];
    const p = P(zustand.p);
    return [
      [1300, () => zuReitern()], [900, () => reiterWaehlen(1)], [2800, () => kopieren($('#panel-feier [data-kopie][data-name="Verwendungszweck"]') || $('[data-kopie]'), true)],
      [2200, () => reiterWaehlen(2)], [1400, () => setzeFeld('#k-name', 'Ilse Brenninger')],
      [700, () => setzeFeld('#k-text', 'Liebe Familie Amannsberger, Hedwig fehlt uns in der Siedlung. Wir denken an Sie.')],
      [700, () => setzeFeld('#k-mail', 'ilse.b@beispiel.de')], [900, () => kondoSenden(p, true)],
      [2800, () => freigeben()], [2400, () => reiterWaehlen(3)], [1600, () => kerzeAnzuenden(p, true)],
      [2800, () => reiterWaehlen(0)], [1800, () => neustart()]
    ];
  }
  function takt() {
    if (!laeuft) return;
    if (pos >= schritte.length) { neustart(); schritte = ablauf(); pos = 0; }
    const [warte, tu] = schritte[pos];
    uhrwerk = setTimeout(() => { if (!laeuft) return; tu(); pos++; takt(); }, warte);
  }
  function stopp() { laeuft = false; clearTimeout(uhrwerk); h.classList.remove('vorfuehrung'); }
  function neustart() {
    zustand.extra = {}; zustand.reiter = 0; zustand.suche = ''; zustand.sort = 'tod'; zustand.abgelaufen = false; zustand.noindexHand = null; zustand.ohneFoto = false; zustand.leer = false;
    try { PERSONEN.forEach(p => localStorage.removeItem('steinlinde-kerze-' + p.id)); } catch (e) {}
    render(); scrollTo({ top: 0 });
  }
  function vorfuehren() { stopp(); neustart(); laeuft = true; h.classList.add('vorfuehrung'); schritte = ablauf(); pos = 0; takt(); }
  function pause() { laeuft = false; clearTimeout(uhrwerk); }
  function weiter() { if (laeuft) return; if (!schritte.length) return vorfuehren(); laeuft = true; takt(); }

  PERSONEN.forEach(p => { if (kerzeGeladen(p.id)) ex(p.id).kerze = true; });
  render();

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