{"name":"ZVV Atlas Showcase","description":"Living Documentation der ZVV Atlas Foundation — jedes Pattern als lauffähige Referenz.","atlasVersion":"0.2","repo":"https://github.com/zvvch/zvv-atlas","docs":"https://zvvch.github.io/zvv-atlas","live":"https://atlas.zvv.dev","contact":"ict@zvv.zh.ch","version":"0.0.0","commit":"8a0d6695c7f5aa13d4f2df4e79c3ba5f26c75959","buildTime":null,"environment":"production","atlas":{"latestVersion":"0.3.0","releases":{"count":6,"items":[{"version":"0.3.0","date":"2026-08-11","body":"### Changed — die offenen Spec-Entscheidungen sind gefallen und umgesetzt\n\n- **Alle 19 Fragen von `/entscheidungen` sind beantwortet** (MRA, 6. August 2026; Entscheidungsblock am Ende von `docs/proposals/ungedeckte-luecken.md`): zwölf Empfehlungen der Papiere bestätigt, vier eigene Entscheide (CSP-Nonces aus der Middleware, neutrale Rate-Limit-Antwort, Durchsetzung in der Server-Action, Kundenradar-Deck bleibt unter `/info`), drei ehrlich offen gehalten — die zwei Rate-Limit-Zahlen liegen beim Betrieb, die CTA-Zieladresse bei der Betriebsstelle.\n- **Geschlossen und aus `LUECKEN` entfernt:** L02 (E2E ist App-Entscheid; Rückweg über `patterns/e2e.md`), L03 (Datumszusage gestrichen, Guide entsteht aus dem ersten realen Migrations-PR), L08 (Referenz-Zeile + Legende in der Drift-Tabelle), L09 (Toolchain von `packages/atlas` deklariert, Atlas-CI installiert per `npm ci` reproduzierbar).\n- **Spec-Erweiterungen mit Conformance-Wirkung (Minor):** `02` §3.6 Rate-Limit-Mechanik (neutral, Server-Action) · `06` §5 Magic-Link-TTL 60 Minuten samt Supabase-Einstellort · `10` §5.1 `zvv_`-Namenskonvention für Rollen · `10` §7.1 Baseline-CSP mit Nonces (Stufe 1: Report-Only, Referenz `example/proxy.ts`) · `13` §9 Meta-Routen (`/info` Pflicht, `/changelog` ab Versionierung, `/docs` optional) und Versionierungs-Doktrin `version + commit + buildTime + environment`.\n- **Reduziert statt geschlossen, bewusst:** L04 (Report-Only bis zum Release-Zyklus ohne Verstösse), L05 (Sichtprüfung, einzige Lücke ohne Wächter). L06a und L07 sind noch am selben Tag per Delegation entschieden — 5 Anfragen/h pro Adresse und 20/h pro Quell-IP samt kopierbarem Fixed-Window-Muster (`02` §3.6), der CTA «Konto beantragen» zeigt auf `mailto:ict@zvv.zh.ch`. Alle Wächter sind umgedreht und halten den entschiedenen Zustand fest.\n\n### Changed — `/info` lässt sich durchscrollen\n\n- **Das Deck war eine Umschalt-Präsentation; das Mausrad tat nichts.** Wer ein Deck geschickt bekommt, probiert aber zuerst zu scrollen — eine Seite, die darauf nicht reagiert, wirkt kaputt. Jetzt eine echte Scroll-Bahn mit `scroll-snap-type: y mandatory`, jede Slide ein Rastpunkt.\n- **Bewusst kein Scroll-Hijacking.** Das Mausrad wird nicht abgefangen: Rad, Trackpad, Wischen, Scrollbalken und Tastatur nehmen denselben nativen Weg. Die Slides liegen dafür in Leserichtung im DOM statt absolut übereinander — der `aria-hidden`-Riegel auf den inaktiven Slides fällt weg, was scrollbar erreichbar ist, gehört auch in den Accessibility-Baum.\n- **Zähler und Punkte werden gerechnet, nicht beobachtet.** Ein `IntersectionObserver` liefert in einem nicht gezeichneten Dokument gar keine Einträge — in genau der Lage gemessen und verworfen; dieselbe Falle stellt `requestAnimationFrame`. Der Einblend-Effekt ist Zutat statt Voraussetzung: Bis die Messung übernimmt, ist jede Slide sichtbar, sonst wäre die Seite ohne JavaScript leer.\n- **Fussangel behoben:** Die Pfeiltaste rechts navigierte auf der letzten Slide still aus dem Deck. Beim Scrollen erreicht man das Ende viel häufiger — der Absprung gehört jetzt allein dem beschrifteten Knopf, wie die Deck-Spec ihn auch beschreibt.\n\n### Fixed — Geist Mono war dokumentiert, aber nie geladen\n\n- **`/typography` zeigte den Import aus `geist/font/mono`, der Showcase lud ihn nie:** `--font-geist-mono` war ein reiner System-Fallback, Code rendert bisher in Consolas beziehungsweise Menlo. Jetzt ist das Paket installiert und `GeistMono.variable` am `<html>`; die Fallback-Kette steht als `var()`-Default in der kanonischen `assets/color-tokens.css`.\n\n### Fixed — die Mindestbreite galt überall und war nirgends erfüllbar\n\n- **`docs/01` §1 forderte 80 px (mobil) / 120 px (Desktop) Logobreite — und keine einzige Logo-Platzierung in Atlas hielt sie ein.** Topbar 40 px, Sidebar-Kopf 44 px, eingeklappte Rail 36 px, OG-Karte 74 px. Erfüllt war die Regel nur auf der Login-Seite und in der `/brand`-Seite, die sie erklärt. Die Spec war in der eigenen Referenz-Shell geometrisch nicht erfüllbar: 120 px Breite ergeben 55 px Höhe, mit Schutzraum 83 px — ein Band dieser Höhe ist keine Topbar mehr.\n- **Die Ursache war eine Zahl, die zwei Dinge meinte.** «Mindestbreite» stand für ein **Präsenz-Minimum** (wie gross muss die Marke auftreten, wo sie allein für den Absender steht), begründet wurde sie aber mit **Lesbarkeit** («darunter wird das Logo unleserlich»). Deshalb las jeder sie als universell, und deshalb brach sie überall dort, wo das Logo als Herkunftsmarker in der Chrome sitzt statt als Absender auf einer Markenfläche.\n- **Die Regel hat jetzt einen Geltungsbereich** — drei Flächenarten statt einer Zahl: **Markenfläche** (Login, Deckblatt, Mail-Kopf, Print, Hero) behält 80/120 px Breite · **Anwendungs-Chrome** (Topbar, Sidebar-Kopf, Footer) misst in der **Höhe**, ≥ 20 px, Standardmass `43 × 20` · **ohne CSS gerendert** (OG-Karte, PDF) misst als Anteil der Fläche, ≥ 5 % der Canvas-Höhe. Die Chrome misst in der Höhe, weil die Höhe das ist, was ein Band tatsächlich beschränkt. Und «px» ohne Bezugsrahmen ist bedeutungslos: Die OG-Karte rendert 74 px auf 1200 px Canvas — in einer Teams-Vorschau sind das effektiv rund 31 px.\n- **Neue Konstanten `LOGO_MIN_HOEHE_CHROME` und `LOGO_CHROME`** in `@zvvch/atlas/brand`. Die bestehende `LOGO_MIN_BREITE` bleibt, trägt aber jetzt im Doc-Kommentar, wofür sie gilt.\n- **`LOGO_MIN_BREITE` war seit v0.1 exportiert und wurde von keinem Layout je gelesen** — ausgerechnet mit der Begründung, «damit ein Layout sie rechnen kann statt sie zu schätzen». Genau deshalb fiel der Widerspruch drei Versionen lang niemandem auf. Eine Zahl, die niemand prüft, ist eine Notiz. Neuer Wächter-Block in `example/tests/brand.test.ts`: Standardmass hält die Untergrenze · Breite ist aus der Höhe gerechnet · Topbar und Sidebar setzen kein eigenes Mass · die eingeklappte Rail trägt Logo plus Schutzraum. **Gegengeprüft, dass alle vier rot werden:** Rail auf `w-14` zurückgesetzt und `width={40}` am Logo hartkodiert — beide Male rot, nach dem Zurücknehmen wieder grün.\n- **Der Wächter fand sofort eine Drift, die niemand gesucht hatte:** Das Standardmass `44 × 20` aus der App-Shell war selbst off-ratio — `round(20 × 2,173895) = 43`, nicht 44. Zwei von Hand getippte Zahlen, 2,20 statt 2,174. Korrigiert auf **`43 × 20`**; gesetzt wird ab jetzt die Höhe, die Breite ist gerechnet.\n- **Die eingeklappte Rail wächst von `w-14` auf `w-16`.** 43 px Logo + 2 × 5 px Schutzraum + 2 × 4 px Padding = 61 px; in 56 px blieben dem Schutzraum 2 px statt 5, und die Rail-Kante stünde im Schutzraum. Die Rail-Breite folgt jetzt aus dem Logo, nicht umgekehrt. Das eingeklappte Sonder-Mass `36 × 17` entfällt — die Identität schrumpft nicht, nur weil die Rail schmal wird.\n- **`docs/03` §2.1 forderte `text-zvv-primary` für den App-Namen, die Referenz-Topbar rendert ihn ohne Farbe.** Hier war die Doku die falsche Seite, aus drei unabhängigen Gründen: `patterns/logo-anwendung.md` §4 (Spec) verbietet eine Zweitfarbe neben dem Logo, `--color-zvv-primary` ist ein festes Hex **ohne Dark-Variante** (es existiert für Satori und Mail, wo HSL-Tokens nicht greifen), und auf dem Dark-Card landet es gemessen bei 4,09:1 — unter WCAG AA für 14-px-Text. Die Forderung ist gestrichen, der Grund steht daneben, damit sie nicht zurückkommt.\n- **Nebenbei:** `example/app/(showcase)/brand/page.tsx` rechnete mit hartkodiertem `/ 2.17` statt `ZVV_LOGO_VERHAELTNIS` — eine zweite Wahrheit für das Seitenverhältnis, ausgerechnet in der Demo, die das Rechnen aus einer Grösse vorführt.\n\n**Conformance-Wirkung:** keine App wird rückwirkend nicht-konform — die Spec wird präzisiert und in genau einer Richtung gelockert. Beim nächsten Release ist das ein **Minor**.\n\n### Added — die Topbar-only-Variante hat endlich eine Implementierung\n\n- **`docs/03` §2.1 spezifizierte die Topbar-only-Ausprägung vollständig — acht Pflicht-Elemente, `h-16`, eine Textfarbe für den App-Namen — und nichts im Showcase baute sie.** Genau deshalb konnten dort eine Bandhöhe und ein `text-zvv-primary` stehen, denen die reale Referenz-Topbar widersprach, ohne dass es je auffiel. **Eine Spec, der kein Code widersprechen kann, driftet unbemerkt** — dieselbe Fehlerklasse wie die nie gelesene `LOGO_MIN_BREITE`.\n- **Neu `example/components/layout/topbar-brand.tsx`** — die echte Komponente, nicht ein für den Showcase nachgezeichnetes Band: Logo im Chrome-Standardmass, App-Name als Text ohne Zweitfarbe, Breadcrumb, Pills-Nav mit Pre-Login-Zustand, Divider, User-Menü, Theme-Toggle. Vorgeführt in `/navigation` im Rahmen, nach dem Muster, das die Login-Vorschau gesetzt hat: **kein kopiertes Markup**, sondern dieselben Bausteine, die eine echte App verwendet.\n- **Drei Wächter darauf** — Bandhöhe aus §2.1, Band trägt Logo plus Schutzraum, App-Name ohne Zweitfarbe. Gegengeprüft: `h-16`→`h-12` und ein eingefärbter App-Name machen sie rot, zurückgenommen wieder grün.\n\n### Added — die App-Shell lernt vom besten Schüler\n\n- **Der Showcase hatte die schwächere Navigation als FZDB** — eine entkoppelte Topbar mit zentriertem Logo, eine Sidebar ohne Identitäts-Anker, kein Breadcrumb. Nach dem Drift-Grundsatz (CLAUDE.md §6) ist das ein Atlas-Bug: das FZDB-Muster ist jetzt als Pattern geschärft, nicht der Showcase als Einzelfall geflickt.\n- **Neu [`patterns/app-shell.md`](./patterns/app-shell.md)** (Guideline): Drei-Zonen-Shell — full-height Sidebar als **Identitäts-Anker** (ZVV-Logo + App-Name + Mandanten-Chip oben, ⌘K-Suche direkt darunter, Gruppen-Nav mit Uppercase-Labels in der Mitte, Einklappen-Toggle + Konto-Zeile mit User-Karte und Notification-Bell unten), Topbar als **Arbeitszeile** in drei Zonen (Breadcrumb nur mit Eltern-Ebenen links, Seitentitel zentriert, seitenbezogene Aktion + Theme rechts). Grundregel „keine Doppel-Identität\": jede Ecke beantwortet genau eine Frage — Chip „wessen Daten\", User-Karte „wer bin ich\", Titel „wo bin ich\".\n- **Zwei Linien über die ganze App:** Die Bänder sind in beiden Spalten gleich hoch — oben `h-12` (Identitäts-Header · Arbeitszeile), unten `h-14` (Konto-Zeile · Footer). Ihre Kanten ergeben zwei durchgehende horizontale Linien. Die erste Fassung hatte einen ~90 px hohen Kopf gegen eine 48-px-Zeile und einen 112-px-Fuss gegen einen 66-px-Footer; an der Sidebar-Kante entstanden zwei Stufen, die die Spalten wie getrennte Seiten wirken liessen.\n- **In einer flachen App ist die Nav-Gruppe die Eltern-Ebene des Breadcrumbs.** Der zentrierte Titel verlangt, dass der Breadcrumb die aktuelle Seite weglässt — auf einstufigen Routen wie `/tokens` bleibt danach aber nichts übrig, weil der Weg dort nur aus der Seite selbst besteht. Die Gruppe aus `NAV_GROUPS` füllt die Lücke (`⌂ › Foundation` + Titel „Color-Tokens\") und ist bewusst kein Link: eine Gruppe ist eine Ordnung, keine Route. Vorbild ist Vercels „All Projects\" links neben dem Seitentitel.\n- **Aus der Regel folgt die Platzierung, nicht umgekehrt.** Mandanten-Chip und Suche stehen **unter** der oberen Linie statt im Kopf. Der **Einklappen-Toggle wanderte in die Arbeitszeile** — sonst bräuchte das untere Band zwei Zeilen. Das ist zugleich shadcns kanonische `SidebarTrigger`-Position und hat den Nebeneffekt, dass der Toggle in beiden Rail-Zuständen an derselben Stelle liegt. Im Icon-Modus zieht die Notification-Bell mit in die Arbeitszeile, weil im 56-Pixel-Rail neben dem Avatar kein Platz ist.\n- **Muster verifiziert statt nur kopiert:** Die Verteilung deckt sich mit shadcn/ui's Sidebar-Anatomie (`SidebarHeader` = „branding/workspace switchers\", `SidebarFooter` = „user menus\"), den gängigen SaaS-Navigations-Analysen (Konto als Utility ans Sidebar-Ende, Topbar nur für global Gültiges) und dem Vercel-Dashboard als Reinform (Team-Switcher oben links, Suche in der Sidebar, User + Bell unten links, Topbar fast leer). Die erste Fassung hatte Name/Rolle dreifach (Chip, Begrüssung, User-Karte) — die Begrüssung flog raus, die Rolle steht nur noch unten.\n- **`docs/03-navigation.md` nachgezogen:** §1 Drei-Zonen-Layout, §2 in zwei Ausprägungen geteilt (Topbar-only als Brand-Träger vs. Arbeitszeile), §3.0 die Sidebar-Etagen inkl. Suche und Konto-Zeile, §4 Home-Icon statt App-Name als erstes Breadcrumb-Element und Eltern-only-Breadcrumb bei zentriertem Titel.\n- **Eine Korrektur gegen die Referenz selbst:** FZDB zeigt im Sidebar-Header kein Original-ZVV-Logo, nur einen Text-Platzhalter. Atlas übernimmt die FZDB-Struktur, macht das Original-Logo aber zur Pflicht (01 · Identity) — die Lücke steht als Drift-Notiz im Kapitel und gehört ins FZDB-Backlog.\n- **Showcase umgebaut** (`example/components/layout/`): Sidebar full-height mit Identitäts-Header, Suche und Konto-Zeile (Persona-Karte + Mock-Bell), Topbar als Drei-Zonen-Arbeitszeile mit zentriertem Seitentitel, `AppShell` als echte Drei-Zonen-Komponente statt totem Code. Sidebar, Topbar und Footer teilen eine Fläche (`bg-card` / `dark:bg-muted/30`) und bilden einen Chrome-Rahmen um den Content-Canvas.\n\n### Added — ein Grössen-Budget, das das Erreichte hält\n\n- **Ohne Wächter ist eine Aufräumaktion eine Aufräumaktion mit Verfallsdatum.** Auf drei Routen fielen rund 1 MB weg, nur weil zwei statische Importe auf `next/dynamic` umgestellt wurden. Der nächste statische Import einer schweren Bibliothek stellt den alten Zustand wieder her — und niemand merkt es, weil die Seite ja funktioniert.\n- **`npm run groessen`** startet den gebauten Server, misst je Route das **eager** geladene JavaScript und vergleicht es mit `example/bundle-baseline.json`. Neuer CI-Schritt **nach** dem Build.\n- **Gemessen statt geschätzt.** Der Wert kommt aus den `<script src>`-Einträgen des ausgelieferten HTML, nicht aus einem Manifest: Turbopack schreibt kein verlässliches Route→Chunk-Manifest, und eine Schätzung aus Dateinamen hätte genau den Fehler, den dieser Wächter verhindern soll — eine Zahl, die plausibel aussieht und nicht stimmt.\n- **Toleranz: der grössere Wert aus +10 % und +50 kB je Route.** Nicht enger, weil Next die Chunk-Aufteilung zwischen Versionen umbaut und ein Gate, das bei jedem Upgrade flattert, ignoriert wird. Nicht weiter, weil eine einzelne schwere Bibliothek deutlich darüber liegt — Recharts schlug mit 441 kB zu Buche, Lottie mit 304 kB.\n- **Gegengeprüft, dass das Gate wirklich greift:** Grundlinie für eine Route um 300 kB abgesenkt → Exit 1 mit Nennung der Route; danach zurückgesetzt → Exit 0. Ein Gate, dessen Rotwerden man nicht gesehen hat, ist eine Behauptung.\n- **Die Fehlermeldung nennt beide möglichen Ursachen und was sie verlangen** — statisch importierte Bibliothek (dann hinter `next/dynamic`) oder absichtliches Wachstum (dann Grundlinie bewusst neu schreiben und im Commit begründen) — und schliesst ausdrücklich aus, die Toleranz anzuheben.\n\n**Was die Grundlinie sichtbar macht:** `/icons` liegt bei **1 570 kB** und damit 632 kB über der Grundlast. Das ist der Namespace-Import `import * as Icons from 'lucide-react'`, der Tree-Shaking aushebelt (S5 im Standardisierungspapier, offen). Die Zahl steht jetzt eingecheckt da, statt nur in einer Analyse.\n\n### Fixed — der Drift-Wächter prüfte nur ein Drittel der Form\n\n- **`example/tests/brand.test.ts` verglich nur `geometrie`.** `art` und `evenodd` blieben ungeprüft — ein `polygon`, das im Modul als `path` steht, oder ein verlorenes `fill-rule=\"evenodd\"` wären unbemerkt geblieben. Beides kippt das Zeichenergebnis sichtbar: Ohne `evenodd` füllen sich die zwei ausgesparten Bögen des Logos.\n- **Gegengeprüft, dass der Wächter wirklich anschlägt:** einmal mit entferntem `evenodd`, einmal mit `polygon` → `path` verfälscht. Beide Male rot, nach `npm run sync-atlas` wieder grün. Ein Wächter, dessen Rotwerden man nicht gesehen hat, ist eine Behauptung.\n- Nebenbei: Der erste Anlauf des erweiterten Tests war selbst falsch — sein CSS-Parser schleppte `<style>` in den Klassennamen und meldete deshalb überall `evenodd: false`. Aufgefallen nur, weil der Test danach fehlschlug.\n\n### Changed — der Doku-Deploy überspringt reine Showcase-Pushes\n\n- **`pages.yml` baute und deployte bei jedem Push auf `main`**, auch wenn nur `example/` betroffen war. Der Showcase ist eine eigenständige Next.js-App auf Vercel und in `_config.yml` ausdrücklich aus dem Jekyll-Build ausgeschlossen — solche Pushes konnten die Doku-Site gar nicht verändern.\n- **Selbst gemessen an den letzten 40 Commits auf `main`: 13 (33 %) berühren ausschliesslich `example/`.** Die Vorlage in `docs/proposals/stack-standard.md` nannte 93 %; diese Zahl liess sich nicht reproduzieren und ist hier bewusst durch die eigene Messung ersetzt.\n- **`paths-ignore` statt einer `paths`-Positivliste.** Eine Positivliste müsste jede Datei kennen, die Jekyll rendert (`docs/`, `patterns/`, Wurzel-Markdown, `assets/`, `packages/**/*.md`, `_config.yml`) und veraltete still, sobald jemand eine neue Quelle hinzufügt — die Site wäre dann stumm veraltet. Der Ausschluss kennt nur den einen Ordner, der nachweislich nichts beiträgt; alles Neue deployt weiterhin.\n\n### Changed — schwere Bibliotheken laden jetzt überall über `next/dynamic`\n\n- **Zwei von vier Ladestellen waren falsch.** `/charts/echarts` und `/dashboard` luden ihre Bibliothek seit jeher über `next/dynamic`; `app/(showcase)/charts/recharts/page.tsx` importierte Recharts statisch, und `components/motion/zvv-lottie.tsx:4` den Lottie-**Player** — obwohl die Animationsdaten längst lazy nachluden. Der Player kam damit in jeden Routen-Chunk, der die Komponente auch nur erwähnt.\n- **Gemessen, beide Seiten** (eager referenzierte Chunks, unkomprimiert wie von `next start` ausgeliefert):\n\n  | Route | vorher | nachher | Differenz |\n  |---|---:|---:|---:|\n  | `/charts/recharts` | 1 390.6 kB | 949.2 kB | **−441.4 kB** |\n  | `/lotties` | 1 307.1 kB | 1 003.0 kB | **−304.1 kB** |\n  | `/marketing-hero` | 1 285.5 kB | 981.3 kB | **−304.2 kB** |\n  | `/` (Grundlast) | 937.9 kB | 937.9 kB | unverändert |\n\n  Über Brotli liefert Vercel davon grob ein Drittel aus. Die unveränderte Grundlast belegt, dass die Umstellung keine Nebenwirkung auf andere Routen hat.\n- **Der Befund, der die Faustregel umdreht:** Die *kleinere* Bibliothek war die teurere. Recharts kostete auf seiner Route mehr als ECharts auf seiner, weil ECharts korrekt lazy lud. Nicht die Grösse entscheidet, sondern der Ladeweg — deshalb steht die Regel jetzt in `docs/04-ui-stack.md` §7 und nicht nur in einem Commit.\n- **Zwei Dinge bewusst so und nicht anders:** Die `SrOnlyTable`-Fassungen der Diagrammdaten bleiben in `page.tsx` und werden serverseitig gerendert — lägen sie im `ssr: false`-Chunk, hätte eine Screenreader-Nutzerin bis zum Laden des Client-Bundles gar keine Daten, und die Auslagerung tauschte Barrierefreiheit gegen Kilobytes. Und `zvv-lottie.tsx` stösst den Player-Chunk **parallel** zum JSON an (`void import('lottie-react')`) statt danach, sonst entstünde eine Wartekette aus zwei Anfragen.\n- Die Paket-Fassung `packages/atlas/src/motion/zvv-lottie.tsx` ist mitgezogen — sonst bekämen konsumierende Apps weiter den statischen Import.\n\n### Fixed — die Satori-Prüfung liess den häufigsten Fehler durch\n\nEine adversarische Gegenprüfung über die gestern Nacht ausgelieferte Marken-Ebene meldete 49 Befunde; 41 überlebten drei Widerlegungs-Versuche. Die schwersten stecken ausgerechnet in `pruefeSatori()` — dem Werkzeug, das solche Fehler finden soll.\n\n- **`display-fehlt` zählte Kinder, Satori zählt nicht.** Die Regel lautete `kinder.length > 1`. Satoris tatsächliche Bedingung ist `children && typeof children !== 'string'` — ein `<div>` mit **einem** Element-Kind bricht bereits ab. Damit ging der häufigste Fall überhaupt durch: `<div><span>…</span></div>` galt als sauber, der Abruf lieferte 500. Genau die Fehlerklasse, die das Modul als lauten Fehler abfangen soll.\n- **`display: block` genügt nicht** an einem `<div>` mit Kind-Elementen, obwohl Satori `block` an anderer Stelle als gültigen Wert führt. Zwei verschiedene Prüfungen, die die alte Fassung vermengte.\n- **`display: grid` fällt nicht still auf Block zurück, sondern bricht ab.** Diese Falschaussage stand in `satori.ts`, in `docs/13-praesenz.md` §7 und in `patterns/og-card.md` §5 — ein Spec-Kapitel behauptete also, ein 500er sei kosmetisch. Dasselbe gilt für relative Bild-URLs.\n- **«Drei der vier schweigen» war falsch — es ist genau einer** (`className`). Drei brechen ab und fallen beim ersten Abruf auf. Die Kopfzeile behauptete das Gegenteil und liess die Prüfung wichtiger erscheinen, als sie ist.\n- **Neu abgedeckt:** `backgroundImage: url(…)` mit relativer Adresse — Satori bricht dort genauso ab wie bei einem `<img>`, und der Fall wird leichter übersehen, weil er wie CSS aussieht.\n- **`display` wird nur noch an `<div>` erzwungen.** Satori verlangt es nur dort; die alte Fassung meldete `<span>` mit zwei Kindern als 500er, obwohl er klaglos rendert.\n\n**Alle Regeln sind jetzt an `@vercel/og` gemessen, nicht aus einer Doku übernommen:** Jeder Fall der Tabelle in `brauchtDisplay()` wurde einmal durch `new ImageResponse(el).arrayBuffer()` geschickt. `example/tests/brand.test.ts` friert die neun gemessenen Fälle ein.\n\n### Fixed — drei benannte Wächter-Tests, die es nicht gab\n\n- `assets/generate-logo-module.mjs`, `zvv-logo.generated.ts` und `docs/13-praesenz.md` nannten `packages/atlas/tests/zvv-logo.test.ts` als Drift-Wächter. **Die Datei existierte nicht** — der reale Test liegt in `example/tests/brand.test.ts`, weil nur die Example-CI zuverlässig läuft (Lücke L09). Verweise korrigiert.\n- `example/scripts/sync-atlas-assets.mjs` nannte `tests/brand-sync.test.ts`. **Existierte ebenfalls nicht** — jetzt geschrieben: vergleicht jede Kopie unter `lib/atlas/brand/` byteweise mit der Quelle im Package, prüft die Dateiliste auf Vollständigkeit und überspringt sich selbst dort, wo `../packages` nicht erreichbar ist (Vercel).\n- **`npm run logo-module` hängt jetzt einen Prettier-Lauf an.** Ohne ihn erzeugte jede Regenerierung einen reinen Format-Diff, und der naheliegendste Drift-Test — regenerieren, `git diff` prüfen — war unbrauchbar.\n\n### Fixed — Zahlen und Verweise, die nicht stimmten\n\n| Behauptung | stand da | gemessen |\n|---|---|---|\n| Kontrast `#0072ce` gegen Weiss | 4.31:1 | **4.89:1** (die 4.31 aus `docs/01` §2 sind ein anderer Vergleich) |\n| Overlay-Animationsklassen | 59 | **76 je Fassung**, 152 in beiden |\n| Gebautes CSS | 106 138 B | **114 838 B** |\n| Titelgrössen-Stufen | vier | **fünf** (96/76/60/48/40), und sie hängen am Bildzeichen |\n\nDazu: ein Querverweis in `docs/14-wirkung.md` auf eine Atlas-Regel zu Zahlen- und Datumsformat, **die es nicht gibt** — entfernt. Ein Kontrastwert ebendort, der nicht selbst geprüft war — durch einen Verweis auf `docs/01` §2 ersetzt statt neu behauptet. Die Manifest-Querverweise aller drei Apps zeigten auf §4 (Hausschrift) statt §6 (Manifest). Und «zusammen unter 60 Zeilen» traf auf keine der drei Apps zu.\n\n### Changed — `icon.svg` **oder** `icon.tsx`, beides ist konform\n\nKapitel 13 verlangte `app/icon.svg`, während fzdb und gtfs `app/icon.tsx` fahren — die Conformance-Tabelle setzte trotzdem bei beiden einen Haken. **Die Spec war zu eng, nicht die Apps falsch.** §2 führt die Abwägung jetzt aus: Das statische SVG bleibt bei jeder Auflösung scharf, kostet aber eine **zweite** Quelle für dasselbe Zeichen und braucht einen Test, der Datei und Kachel-Funktion zusammenhält. Das generierte PNG hat nur eine Quelle. Atlas fährt den einen Weg, fzdb und gtfs den anderen; beide sind konform.\n\n### Removed — drei Abhängigkeiten ohne eine einzige Referenz\n\n- **`react-hook-form`, `@hookform/resolvers`, `tailwindcss-animate`** aus `example/package.json` entfernt: zusammen **3 555 kB** installiert, **null** Referenzen im gesamten Quellcode. 25 → 22 direkte Abhängigkeiten. Am Bundle ändert das nichts — gebündelt wurde nie etwas davon. Der Gewinn ist, dass die Abhängigkeitsliste wieder beschreibt, was die App tut.\n\n### Changed — Kapitel 04 beschreibt, was gilt, statt was gelten sollte\n\n- **Die Forms-Vorgabe war eine Behauptung.** §4 schrieb «react-hook-form + Zod ist Pflicht für Forms ≥ 3 Felder». Gemessen am 05.08.2026: `zvv-fzdb` nutzt react-hook-form in **0 Dateien** und hat es nicht einmal installiert, dafür `useActionState` in **26**; der Showcase 0 gegen 2. **Keine ZVV-App hat sich je daran gehalten** — nicht aus Unkenntnis, sondern weil React 19 das Problem verlagert hat. Der Standard ist jetzt Server Action + `useActionState` + `zod`, mit react-hook-form als begründbare Abweichung für vielstufige Assistenten und dynamische Feldlisten.\n- **Versionsangaben nachgezogen:** Recharts 2.x → 3.x, ECharts 5.x → 6.x (installiert sind `^3.8.1` und `^6.1.0`).\n- **Die ECharts-Faustregel nennt jetzt eine gemessene Zahl** statt «900KB»: **1 127 854 B roh / 370 098 B gzip**, der grösste Einzel-Chunk des Builds. Wichtiger als die Grösse ist aber der Ladeweg — live gemessen kostet das korrekt lazy geladene ECharts **+3.9 kB** eager, das **statisch** importierte Recharts **+129.6 kB**. Die kleinere Bibliothek war teurer, weil sie falsch geladen wurde.\n- **Neuer §7a: die Overlay-Animationen wirken nicht.** 59 Klassenangaben (`animate-in`, `animate-out`, `fade-in`, `slide-in-from…`) in sechs Komponenten, in **beiden** Fassungen — und **0 Vorkommen** im gebauten CSS, während `animate-spin` und `animate-pulse` aus Tailwind selbst je dreimal vorkommen. Ursache: `tailwindcss-animate` war deklariert, aber nirgends eingebunden. Die Abhängigkeit ist entfernt; die Klassen bleiben stehen, weil ihre Auflösung eine **Entscheidung** ist: entfernen (kein Verhaltensunterschied) oder `tw-animate-css` einbinden — dann aber zwingend mit der `prefers-reduced-motion`-Regel, die dieses Kapitel fordert und die **in keiner CSS-Datei des Repos existiert**.\n\n### Changed — Kapitel 13 führt den wahren Conformance-Stand\n\n- **Die Tabelle in §8 war eine Stunde nach dem Schreiben veraltet.** Sie führte `fzdb` mit drei ❌ und die übrigen Apps als «nicht erhoben». Inzwischen sind **drei Apps konform** — atlas, [fzdb](https://github.com/zvvch/zvv-fzdb/pull/358) und [gtfs](https://github.com/zvvch/zvv-mcp-gtfs/pull/3) —, und die Tabelle hat zwei Spalten mehr (Metadaten, Test).\n- **Neu: was der Durchgang bei fzdb und gtfs zutage gefördert hat.** Sieben Befunde, **einer davon laut.** Sechs waren nur sichtbar, wenn man das gerenderte PNG ansieht oder die Route **ohne Sitzung** abruft — der teuerste war, dass `/opengraph-image` in fzdb mit `307 → /login` antwortete und die Link-Vorschau deshalb nie in einem Chat erschien. Es fiel nie auf, weil dieselbe URL im angemeldeten Browser ein einwandfreies Bild liefert.\n- Der Abschnitt steht im Kapitel und nicht im CHANGELOG, weil er die Begründung für die zwei Pflichtschritte in §7 trägt: das PNG ansehen, und die Route ohne Sitzung abrufen.\n\n### Added — Kapitel 13 steht jetzt auch im Agenten-Vertrag\n\n- **`/techstack` und `/llms.txt` kannten die Präsenz-Spec nicht.** Kapitel 13 ist verbindlich, aber `example/lib/atlas/techstack.ts` ist die Quelle, aus der ein Agent den Stack liest — was dort fehlt, existiert für ihn nicht. Neuer Abschnitt 9 «Präsenz» mit acht Regeln **PR01–PR08**, alle als `spec · belegt` mit Datei und Zeile.\n- **Die Regeln tragen die real gefundenen Fehler als Hinweis**, nicht als abstrakte Warnung: der 307er der fzdb-Metadaten-Routen (PR07), die Ersatzschrift statt Hausschrift (PR03), der Logo-Nachbau (PR04), die zwei Eckenradien (PR05). Ein Agent liest damit nicht «achte auf X», sondern «X ist in dieser Codebasis so passiert».\n- **PR08 benennt ausdrücklich die Grenze der Maschine:** `pruefeSatori()` findet vier Fallen; eine verzerrte Grafik, ein Titel, der gegen das Bildzeichen drängt, und eine gar nicht geladene Schrift gehören nicht dazu. Beides ist beim Ausrollen auf zvv-fzdb aufgetreten und nur am gerenderten PNG aufgefallen.\n- Der Abnahme-Abschnitt wandert von `nr: '9'` auf `nr: '10'`; die Regel-IDs (`A01`–`A03`) bleiben unverändert, damit Abweichungstabellen in den App-Repos gültig bleiben. Präfix `PR`, weil `P01`–`P07` im Abschnitt «Atlas-Package konsumieren» bereits vergeben sind.\n\n### Added — Kapitel 13 «Präsenz» und 14 «Wirkung»; die Aussenwirkung wird ein Bausatz\n\n- **Die Flächen, auf denen eine ZVV-App zuerst erscheint, waren pro App abgeschrieben.** `zvv-fzdb` und der Showcase fuhren zwei fast gleiche Fassungen derselben 200 Zeilen OG-Karte, die sich in Details unterschieden, die niemand entschieden hatte — Titelgrösse 72 gegen 96 px, Eckenradius 0.19 gegen 0.22. Das ist der Fall, den `CLAUDE.md` §6 meint: Drift zwischen Apps ist ein Atlas-Bug.\n- **Neu: `@zvvch/atlas/brand`.** `ogCard()` baut die 1200×630-Karte; eine App gibt Titel, Domain, Bildzeichen und Fusszeile mit und sonst nichts. `appIcon()`/`appleIcon()` setzen die Kachel-Konvention um, mit `ICON_RADIUS` als Faktor statt zwei Pixelwerten, die auseinanderlaufen. Der Ordner wird von `sync-atlas-assets.mjs` nach `example/lib/atlas/brand/` **gesynct**, nicht kopiert — er importiert nur aus sich selbst und aus `react`, es gibt also keinen Importpfad-Unterschied und damit keinen Grund für zwei gepflegte Fassungen.\n- **Das echte Dachlogo statt eines Nachbaus.** Beide OG-Karten zeichneten in der Kopfzeile ein weisses Quadrat mit den Buchstaben «ZVV» — eine erfundene Wortmarke, die `docs/01` §1 ausdrücklich verbietet, ausgerechnet auf der Fläche, die am weitesten herumkommt. `zvvLogo()` zeichnet jetzt die Geometrie aus `assets/zvv-logo.svg`; sie kommt über `assets/generate-logo-module.mjs` ins Package, und der Generator prüft dabei, dass die Weiss-Variante wirklich nur ein Fill-Swap ist. Ein Test vergleicht Modul gegen Quelldatei bei jedem Lauf.\n- **Die Karte rendert erstmals in der Hausschrift.** Satori — der Renderer hinter `next/og` — liest TTF, OTF und WOFF, aber **kein WOFF2**; der ZVV liefert Brown Narrow nur als WOFF2. Bis hierher rendert also jede ZVV-OG-Karte in einer Ersatzschrift, unbemerkt, weil sie nur in fremden Oberflächen erscheint. `example/scripts/generate-og-fonts.mjs` (`npm run og-fonts`) entpackt sie einmalig nach TTF und legt sie als Base64-Modul ab — derselbe Weg, den `zvv-fzdb` für seine Report-Exporte seit Monaten in Produktion fährt. Kosten offen benannt: rund 290 kB committeter Text, server-only, nicht im Client-Bundle.\n- **`pruefeSatori()` findet vier Fallen, bevor die Karte rendert** — mehrere Kinder ohne `display: flex` (500er), `className` statt `style`, `display: grid`, relative Bild-URL. **Drei davon schweigen sonst.** Ein Fehler auf diesen Flächen ist besonders teuer, weil ihn niemand sieht: Sie erscheinen nur in fremden Oberflächen, und Teams und Slack cachen eine Karte für Tage. Die Prüfung ist deshalb exportiert und nicht intern — jede ZVV-App soll ihre eigene Karte damit prüfen.\n- **Neu ausgeliefert:** `/apple-icon` (iOS liest kein SVG als Touch-Icon und erzeugt sonst einen Bildschirmabzug der Startseite als Symbol) und `/manifest.webmanifest` (ohne Manifest schneidet Chrome den Seitentitel als Symbolnamen nach etwa zwölf Zeichen ab). Dazu der vollständige Metadaten-Block mit `openGraph` und `twitter.card: summary_large_image` — die kleine Variante beschneidet die Karte auf ein Quadrat und schneidet dabei den Titel weg.\n- **Neu: [`docs/13-praesenz.md`](docs/13-praesenz.md) (Spec)** — Pflichtteil, verbindliche Werte, Prüfung, Conformance-Liste. Dazu die Recipes [`patterns/og-card.md`](patterns/og-card.md), [`patterns/app-icon.md`](patterns/app-icon.md) und [`patterns/logo-anwendung.md`](patterns/logo-anwendung.md) — letzteres sammelt Schutzraum, Mindestgrössen, Co-Branding und das Nachbau-Verbot an einem Ort.\n- **Neu: [`docs/14-wirkung.md`](docs/14-wirkung.md) (Guideline)** — die marketingpsychologische Begründung, mit einer ausdrücklichen Grenze: Wirkung ist erlaubt, wo sie hilft, eine wahre Aussage ankommen zu lassen; sie ist verboten, wo sie eine Entscheidung erzeugt, die ohne sie anders ausgefallen wäre. Bei einer öffentlichen Stelle fehlt das Korrektiv des Wettbewerbs — Nutzende können nicht abstimmen, indem sie gehen. Sechs ausgeschlossene Techniken sind einzeln benannt.\n- **Neu: [`/brand`](https://atlas.zvv.dev/brand)** — die Regeln als laufende Seite. Sie zeigt kein Bildschirmfoto der Karte, sondern **die Karte selbst**: `/opengraph-image` ist dieselbe Route, die Teams abruft. Dazu der Nachbau neben dem Original und der Schutzraum sichtbar gemacht.\n- **`.prettierrc` an der Repo-Wurzel.** Ohne ihn wendet ein Prettier-Lauf ausserhalb von `example/` die Standardwerte an — also genau den gegenteiligen Stil zur eigenen Doku. Beim Anlegen dieser Änderung selbst passiert. Kein Gate, nur die Konfiguration; die Grenze aus L09 bleibt unverändert.\n\n**Offen und ausdrücklich nicht entschieden:** Kapitel 13 ist als **Spec** angelegt. Damit sind alle bestehenden ZVV-Apps gegen dieses Kapitel zunächst nicht konform — `zvv-fzdb` fehlen Hausschrift, echtes Logo und Manifest, die übrigen sind nicht erhoben. Ob das einen Versionssprung auf **v0.3.0** rechtfertigt, ist eine Maintainer-Entscheidung: `CLAUDE.md` §5 wertet ein neues Kapitel als Minor, aber diese Spec macht bestehende Apps nicht-konform. Die Version wurde deshalb **nicht** angefasst.\n\n### Fixed — Confluence-Querverweise überlebten den Space-Umzug nicht\n\n- **Der Space `ZVVDEV` existiert nicht mehr**; die arc42-Seiten liegen jetzt in `ZVV IT` (`/spaces/zvvit`). Confluence hatte einen Teil der Querverweise beim Speichern in Seiten-Referenzen umgewandelt, die an **Space-Key und Titel** hängen statt an der Seiten-ID — die lösen nach dem Umzug nicht mehr auf und öffnen eine **leere neue Seite** statt des Ziels. Betroffen waren die Verweise in den Kapitel-Panels; Links in Tabellenzellen (etwa die Kapiteltabelle der Produktseiten) hatte der Umzug automatisch mitgezogen. Warum die Umwandlung nur einen Teil erfasst, ist offen und im Recipe als offen benannt.\n- **Korrigiert:** KundenRadar-Kapitel 4, 5, 6 und 10 sowie die beiden Verweise auf Fahrzeugdatenbank und Mailer in Atlas-Kapitel 6 — jetzt in der Form `…/wiki/pages/viewpage.action?pageId=…`. Diese bleibt nachweislich als reine URL erhalten (`link`-Mark statt `ri:page`-Referenz), hängt weder an Space noch an Titel und übersteht den nächsten Umzug. An der abgelegten ADF-Struktur gegengeprüft.\n- **`patterns/mermaid-diagram.md` §7 um den Abschnitt «Querverweise ID-basiert schreiben» ergänzt** — dieselbe Stelle, die schon den Makro-Knoten regelt, weil beide Fallen aus derselben HTML→ADF-Umwandlung stammen. Nicht geprüft und deshalb offen: die arc42-Sets von Mailer, Typeform und Fahrzeugdatenbank sowie die übrigen Atlas-Kapitel.\n\n### Added — `/entscheidungen`: die offenen Spec-Entscheidungen als beantwortbare Seite\n\n- **Das Problem war nicht, dass die Fragen fehlen — sondern dass sie nur als Fliesstext existierten.** `docs/proposals/ungedeckte-luecken.md` und `docs/proposals/meta-views.md` enthalten neunzehn Punkte, die ein Agent nicht selbst entscheiden darf, weil sie Spec-Kapitel berühren und damit für alle acht ZVV-Apps gelten. Wer sie beantworten wollte, musste beide Papiere lesen, die Fragen selbst herausschälen und die Antworten von Hand formatieren. Die neue Seite dreht das um: eine Frage pro Block, Optionen zum Anklicken, ein Freitextfeld — und daraus ein fertiger Markdown-Block.\n- **Keine erfundenen Fragen: jede trägt ihre Fundstelle im Datensatz.** `example/lib/atlas/entscheidungen.ts` hält Papier **und** Abschnitt als Pflichtfelder (`Herkunft`), und der Test prüft nicht nur, dass die Datei existiert, sondern dass die zitierte Abschnittsnummer im Papier als Überschrift wirklich vorkommt (`§ 7` → `## 7. …`). Eine Frage, die sich auf einen Paragrafen beruft, den es nicht gibt, macht die CI rot.\n- **Keine zweite Wahrheit: die Befunde bleiben in `techstack.ts`.** Für `L02`–`L09` existiert dort bereits ein `LUECKEN`-Eintrag mit Befund, Belegen und Agenten-Anweisung. Das neue Modul verweist über eine ID darauf, statt den Text zu kopieren — die Seite zeigt Befund und Belege ausgeklappt aus derselben Quelle wie `/techstack` und `/llms.txt`. Ein Test greift genau den Rückfall ab: Taucht der Anfang eines `LUECKEN`-Befunds im Quelltext von `entscheidungen.ts` auf, schlägt er an.\n- **Die Empfehlung des Papiers steht vorausgewählt da — und ist als Empfehlung markiert.** Wo ein Papier eine Empfehlung ausspricht, soll der Leser bestätigen oder abweichen, nicht bei null anfangen. Wo es ausdrücklich **keine** ausspricht (`L04`-Nonce, die vier `L06a`-Fragen, `L07`-Adresse, `MV-kundenradar`), steht auch keine Vorauswahl: Sieben Fragen tragen die Marke «ohne Empfehlung», und ein Test hält diese Liste fest. Eine Seite, die dem Leser eine Meinung in den Mund legt, die das Papier verweigert hat, wäre schlimmer als gar keine Seite.\n- **Vorauswahl zählt nicht als Antwort.** Sonst meldete der Fortschritt «19 von 19», bevor jemand gelesen hat. Gezählt wird nur, was jemand angefasst hat; wer dem Papier folgt, erledigt die Bestätigung mit einem Knopf — der aber ausdrücklich **nur offene** Fragen anfasst, damit er eine bewusste Abweichung nicht stillschweigend zurücknimmt.\n- **Zwei Fallen, die erst am laufenden Server sichtbar wurden.** Erstens: Ein Klick auf die bereits vorausgewählte Empfehlung löst in React **kein** `change` aus — ohne zusätzliches `onClick` liesse sich ausgerechnet der häufigste Fall (Empfehlung bestätigen) mit der Maus nicht erledigen. Zweitens: `<fieldset disabled>` setzt `element.disabled` der Kinder nicht, nur `:disabled` — die Prüfung musste entsprechend nachgezogen werden.\n- **Vier Fragen ruhen, solange ihre Vorbedingung nicht erfüllt ist.** Die vier Rate-Limit-Fragen hängen an `L06`, die Nonce-Frage an `L04`, die CTA-Adresse an `L07`. Ruhende Fragen verschwinden nicht — sie stehen abgeblendet mit Begründung da, sind per `fieldset disabled` aus der Tastatur-Reihenfolge genommen und fallen aus dem Nenner des Fortschritts. Im Markdown-Block bekommen sie einen eigenen Abschnitt statt stillschweigend zu fehlen.\n- **Der erzeugte Block folgt dem bestehenden Vorbild statt ein zweites Format zu erfinden:** gleiche Überschrift und gleiche Aufzählungsform wie «Entscheidungen (MRA, 3. August 2026)» in `meta-views.md`. Je Entscheidung wer, wann, was und die Begründung — dazu Herkunft und die Empfehlung des Papiers samt Vermerk «gefolgt» oder «abgewichen», damit später niemand rekonstruieren muss, wovon abgewichen wurde. Unbeantwortete Fragen stehen als «Offen geblieben» drin; ein Block, der verschweigt, was fehlt, ist gefährlicher als eine leere Seite.\n- **localStorage ja, aber ohne stilles Überschreiben.** Die Liste ist länger als ein Bildschirm; ein versehentlicher Reload nach zwanzig Minuten Nachdenken wäre der teuerste Fehler, den diese Seite verursachen kann, und eine Datenbank soll der Showcase nicht bekommen. Beim Laden wird deshalb **nichts** wiederhergestellt: Ein gefundener Entwurf meldet sich als schmaler Streifen mit «Wiederherstellen» und «Verwerfen». Gelesen wird er über `useSyncExternalStore` mit `null` als Server-Schnappschuss — kein Hydration-Konflikt, und vor allem kein `setState` im Effekt-Rumpf, das `react-hooks/set-state-in-effect` (aktiv in v7.1.1) zu Recht verbietet. Der Schnappschuss ist nach dem ersten Lesen eingefroren, sonst legte ihn React ab dem ersten Schreiben als Endlosschleife aus.\n- **Barrierefreiheit ist gebaut, nicht behauptet:** jede Frage ist ein `<fieldset>` mit der Frage als `<legend>`, jede Option ein Radio mit verknüpftem `<label>`, jedes Freitextfeld ein `<textarea>` mit `<label htmlFor>` und `aria-describedby`. Der Fortschritt ist ein `role=\"progressbar\"` mit `aria-valuetext` im Klartext; der Kopier-Knopf gibt seine Rückmeldung nicht nur als Icon, sondern als Text in einer `aria-live`-Region — und die bleibt stehen, bis sich eine Antwort ändert, statt nach 1,5 Sekunden wegzublinken.\n- **Bewusst nicht gebaut:** kein Absenden, kein Backend, keine Motion. Die Seite erzeugt Text zum Kopieren, mehr nicht — die Entscheidung fällt im Papier und im PR, nicht in einem Formular. Ebenso bewusst **nicht** enthalten: `L01` (Prettier), im Papier bereits mit «EMPFEHLUNG: A — einführen» entschieden, und `L06b` (Magic-Link-TTL), laut Papier «keine Entscheidung, sondern eine Recherche». Beide stehen samt Begründung im Abschnitt «Was hier bewusst nicht gefragt wird» — eine Auslassung ohne Begründung sähe aus wie eine Lücke in der Lücken-Seite.\n\n### Added — Prettier eingeführt; Lücke L01 entschieden und geschlossen\n\n- **Kapitel 07 führte einen Formatter als Pflicht, den es nicht gab.** `docs/07-tech-stack.md:22` verlangte «Prettier 3+», `:141` schrieb `singleQuote`, `semi: false` und `printWidth: 100` vor — im ganzen Repo existierte weder ein Paket noch eine Config noch ein `format`-Script. Der reale Code führte zwei Stile nebeneinander: das shadcn-Generat und die gerüsteten `next.config.ts` / `eslint.config.mjs` mit doppelten Anführungszeichen und Semikolons, der handgeschriebene Atlas-Code ohne. Erzwungen wurde keiner von beiden.\n- **Entschieden wurde für die Doku-Vorgabe.** `example/.prettierrc` trägt exakt die drei Werte aus `docs/07:141`, keinen vierten — die Config ist die kopierbare Vorlage für App-Repos und darf nicht von ihrer eigenen Doku abweichen. Prettier ist auf `3.9.6` **exakt gepinnt**: Ein Range hiesse, dieselbe Datei wird je nach Installationszeitpunkt anders formatiert und das Gate zufällig rot.\n- **Das shadcn-Generat wird mitformatiert, nicht ausgenommen.** Die Entscheidungsvorlage hatte `components/ui/**` in der `.prettierignore` vorgesehen; das hätte den zweiten Stil dauerhaft festgeschrieben, statt ihn zu beenden. `npx shadcn add` überschreibt eine Datei ohnehin komplett, ein anschliessendes `npm run format` stellt den Stil in einem Befehl wieder her. Die Abweichung von der Vorlage ist dort dokumentiert statt stillschweigend.\n- **Ausgenommen ist, was nicht von Hand geschrieben ist:** Build-Ausgaben, das Lockfile (npm schreibt es bei jedem Install neu), die 34 maschinellen Lottie-Exporte — und vor allem die sechs Dateien, die `scripts/sync-atlas-assets.mjs` bei jedem `predev` und `prebuild` aus `assets/` überschreibt. Wer die Kopie formatiert, aber nicht die Quelle, bekommt sie beim nächsten `npm run dev` zurückgesetzt und das Gate flattert ohne Zutun. Empirisch gegengeprüft: nach einem vollen `npm run build` ist `format:check` weiterhin grün.\n- **`.gitattributes` mit `* text=auto eol=lf` ergänzt.** Ohne sie checkt git unter Windows wegen `core.autocrlf=true` mit CRLF aus, und `format:check` meldet lokal jede unveränderte Datei als nicht formatiert, während die CI grün ist — ein Gate, das lokal immer rot ist, wird ignoriert. Nachgewiesen an `vitest.config.ts`: identischer Inhalt, mit CRLF rot, mit LF grün. Am committeten Inhalt ändert die Datei nichts, der Index führte bereits alle 323 Textdateien mit LF.\n- **Der Reformat-Diff ist ein eigener Commit** (92 Dateien in `example/`), getrennt vom Werkzeug-Commit — vermischt wäre keiner von beiden reviewbar. Inhaltliche Neutralität nachgewiesen: Testzahl unverändert bei 62, `typecheck` und `eslint` sauber, `next build` erfolgreich; alle Code-Dateien normalisiert verglichen, die verbleibenden Unterschiede erklären sich restlos durch Gruppierungsklammern und den JSX-Marker `{\" \"}`. Für `lib/atlas/techstack.ts` wurden zusätzlich die **ausgewerteten Modul-Exporte** beider Fassungen verglichen — identisch, und `/llms.txt` rendert zeichengleich.\n- **Nachgezogen:** `L01` ist aus `LUECKEN` entfernt, an seine Stelle treten die Regeln **T05** (Scripts, jetzt inklusive `format:check`) und **T07** (die drei Config-Werte). Der Wächter in `example/tests/techstack.test.ts` ist umgedreht — er hält die Einrichtung fest, statt ihre Abwesenheit zu behaupten, und wird rot, wenn jemand Paket, Pin, Scripts, Config oder Gate wieder entfernt. Die Beleg-Anker sind mitgezogen: von 182 Zeilenangaben blieben 147 unverändert, 35 wurden auf neue Zeilennummern gesetzt oder entfielen mit `L01`, 34 kamen neu hinzu — der Schwerpunkt liegt bei `docs/07-tech-stack.md` (15), `example/package.json` und `example-ci.yml`. Die IDs `L02`–`L09` bleiben unverändert, damit Abweichungstabellen in den App-Repos gültig bleiben.\n\n**Bewusste Grenze — das Gate deckt nur `example/` ab.** Es hängt am `working-directory: example` des Jobs; ein von dort gestarteter Prettier-Lauf sieht `packages/atlas` strukturell nicht. Das ist keine Nachlässigkeit, sondern die Konsequenz aus der offenen Lücke `L09`: `atlas-ci.yml` installiert die Toolchain dort flüchtig und ungepinnt (`npm install --no-save --no-package-lock --no-workspaces …`), und ein Format-Gate auf einer unbestimmten Toolchain erbt deren Unbestimmtheit. **Folge, die offen benannt sei:** `packages/atlas`, `docs/`, `patterns/` und `assets/` wurden **nicht** formatiert und werden **nicht** bewacht — dort gilt der alte Stil weiter, inklusive der zweiten Kopie des shadcn-Generats unter `packages/atlas/src/ui/`. Sobald `L09` entschieden ist, gehört das Gate dorthin ausgedehnt.\n\n### Fixed — Nav-Eintrag «Login» riss aus dem Showcase; neue Vorschau `/login-pattern`\n\n- **Das Problem war der Rahmenbruch, nicht die Seite.** `example/app/login/page.tsx` liegt ausserhalb der `(showcase)`-Gruppe und erbt nur das Root-Layout. Ein Klick auf den Sidebar-Eintrag «Login» landete deshalb in einer Vollbild-Seite ohne Topbar, Sidebar und Fusszeile — und ohne Rückweg ausser dem Browser-Zurück. Bei jedem anderen Nav-Eintrag bleibt der Rahmen stehen.\n- **`/login` bleibt, wo es ist, und bleibt standalone richtig.** In echten Apps steht die Anmeldeseite selbstverständlich ohne App-Shell; sie ist genau deshalb das Vorbild. Geändert hat sich nur, worauf der Nav-Eintrag zeigt: neu auf `/login-pattern`, das das Pattern **im** Rahmen vorführt und das Vollbild über «Vollbild öffnen» (neuer Tab) erreichbar hält.\n- **Kein zweites Markup — das war die entscheidende Randbedingung.** Der gemeinsame Teil steckt neu in `example/components/showcase/login-pattern/login-view.tsx`; Vollbild-Route **und** Vorschau rendern dieselbe Komponente. Eine Kopie für die Vorschau wäre garantiert vom Original abgedriftet, und ein Showcase, der etwas anderes zeigt als sein Vorbild, ist die Drift, die Atlas laut `CLAUDE.md` §6 abstellen soll. Reihenfolge bewusst so: erst extrahieren und die Vollbild-Seite darauf umbauen, dann die Vorschau — damit die geteilte Quelle bewiesen und nicht nur behauptet ist.\n- **Drei Dinge sind absichtlich NICHT in die geteilte Komponente gewandert:** `<main id=\"main\" tabIndex={-1}>` (existiert im `(showcase)`-Layout bereits und ist Ziel des Skip-Links — eine zweite Instanz gäbe doppelte ID und zwei `main`-Landmarks), die bildschirmhohe Hülle `min-h-screen` (würde die Vorschau aufblasen) und der sticky Stufen-Umschalter (würde im Showcase unter der Topbar kleben). Die Stufe hält weiterhin die jeweilige Route und reicht sie als Prop hinein.\n- **Der Vorführ-Zustand wird abgeleitet, nicht gespiegelt.** `demo` gewinnt während des Renderns über den internen State (`demo === 'laden'` statt `useEffect(() => setPending(true))`) — kein `setState` im Effekt-Rumpf, und vor allem keine zweite Wahrheit, die mit dem internen Zustand auseinanderlaufen könnte. Ohne `demo` verhält sich die Komponente unverändert wie bisher, samt Mock-Timern; die laufen jetzt zusätzlich in ein `clearTimeout` beim Unmount, sonst feuerte ein verspäteter Toast auf einer ganz anderen Seite.\n- **Zwei Props gegen zwei konkrete Regressionen:** `headingLevel=\"h2\"`, weil die Login-Card ein `h1` trägt und die Vorschau-Seite über den `SectionHeader` bereits eines hat; `priorityLogo={false}`, weil das Logo in der Vorschau nicht das LCP-Element ist und Next bei mehreren `priority`-Bildern warnt.\n- **Der Umschalter kennt nur Zustände, die es real gibt** — Interaktiv (Ruhe), Laden, Gesendet. Einen Fehlerzustand hat die Referenz-Umsetzung nicht; er steht samt zwei weiteren Lücken (Server-Action statt `setTimeout`, fehlende `@keyframes` zu den gestaffelten `animationDelay`) als benannte Abweichung von `patterns/login-page.md` auf der Seite, statt in der Vorschau nachgestellt zu werden. Ein Fehler-Markup, das nur die Vorschau kennt, wäre exakt die verbotene Drift.\n- **Der Vorschau-Rahmen ist Pflicht, nicht Dekoration:** die Hintergrundschichten der Login-Seite sind `absolute inset-0` mit 600px-Blobs und `blur-3xl`. Im Vollbild trägt `min-h-screen` die Höhe; in der Vorschau übernehmen `relative isolate`, `overflow-hidden` und eine gesetzte Mindesthöhe — ohne sie liefen die Blobs über die Showcase-Seite.\n- **Randnotiz Breadcrumbs:** `components/layout/breadcrumbs.tsx` kannte nur das Segment `login`; für `login-pattern` gibt es jetzt einen eigenen Eintrag, sonst stünde dort der rohe Slug.\n\n### Added — `/techstack` + `/llms.txt`: der Tech-Stack als Vertrag statt als Steckbrief\n\n- **Das Problem war nicht, dass die Doku schweigt — sondern dass sie an mehreren Stellen etwas anderes sagt als das Referenz-Repo.** Ein Agent, der `docs/07-tech-stack.md` gehorsam liest, legt eine `.eslintrc.json` an, die ESLint 9 ohne `ESLINT_USE_FLAT_CONFIG=false` gar nicht liest (`docs/07-tech-stack.md:128` gegen `example/eslint.config.mjs:1`), setzt `target: ES2022` gegen beide realen tsconfigs (`:69` gegen `example/tsconfig.json:3`), sucht ein Prettier, das im ganzen Repo nicht existiert (`:22`), und legt `tests/e2e/` an, wo nie ein Playwright installiert wird (`:118`). Vier Fehler, alle aus **korrekter** Befolgung der Quelle. Genau dort raten Agenten heute.\n- **`example/app/(showcase)/techstack/page.tsx`** ist deshalb kein Stack-Portrait, sondern ein Regelwerk mit Beweislast: 53 Regeln in zehn Abschnitten, sortiert nach der Reihenfolge, in der die Entscheidungen real anfallen (Geltung → Toolchain → Versionen → TypeScript → App Router → Package → Qualitätstore → next.config → Environment → Abnahme). Jeder Abschnitt trägt einen Zielsatz und ein prüfbares Abschlusskriterium; jede Regel nennt Datei und Zeile, verlinkt auf die GitHub-Blob-Ansicht mit `#L`-Anker.\n- **Zwei Achsen statt einer.** *Verbindlichkeit* zitiert Atlas (`spec` = MUSS, `guideline` = SOLL) — und die Legende sagt ausdrücklich, dass nur diese beiden Atlas-Kategorien sind (`CLAUDE.md:33-34`, `docs/index.md:17-18`), während `vorbild` und alle drei Klassen Etiketten dieser Seite sind. *Klasse* beobachtet das Verhältnis Doku↔Repo: `belegt` (beide einig), `entschieden` (Widerspruch — der Vertrag entscheidet und nennt **beide** Fundstellen samt Soll/Ist nebeneinander), `ungedeckt` (kein Vorbild oder keine Doku-Deckung — Erfindungsverbot statt erfundener Regel). Acht Regeln stehen auf `entschieden`, neun Lücken sind namentlich benannt.\n- **Der Tie-Break ist als Entscheidung dieses Vertrags ausgewiesen, nicht als Fundstelle.** Bei Widerspruch gewinnt das Referenz-Repo, weil es baut — aber Atlas regelt diesen Fall nirgends. `CLAUDE.md:62` handelt von Drift zwischen **Apps** und schreibt die Gegenrichtung vor («Pattern in Atlas schärfen, dann Apps angleichen»); es wird nur als Analogie für den Rückfluss zitiert und die Seite sagt das wörtlich. Eine erfundene Regel mit geliehener Fussnote wäre der gefährlichste Verstoss gegen die eigene Belegpflicht gewesen.\n- **`example/app/llms.txt/route.ts`** liefert denselben Vertrag als `text/plain` — Route-Handler statt einer Datei in `public/`, weil eine statische Zweitfassung eine zweite Wahrheit wäre. Maschinen-Affordanzen im Text: Regelzeilen beginnen bei Spalte 0 mit `<ID> [VERBINDLICHKEIT · KLASSE]`, Fortsetzungen sind vier Leerzeichen eingerückt, Befehlszeilen tragen `$ ` — jede Regel ist mit einem einzeiligen Grep greifbar. Am laufenden Server verifiziert statt angenommen, dass der Ordnername mit Punkt die URL `/llms.txt` ergibt: `200 text/plain`, 33 KB, `ETag`-`304`-Runde, `POST → 405`, `OPTIONS → 204`. Neuer Helfer `cachedText()` in `example/lib/atlas/http.ts` als Klartext-Zwilling zu `cachedJson()` — dieselbe F29-Semantik, damit die Cache-Konvention an einem Ort bleibt.\n- **Zwei Prompts, weil es zwei Fälle gibt.** Modus NEU baut, Modus ANGLEICHEN prüft — und der zweite trägt die Bremse: «Ändere nichts, bevor ich die Liste freigegeben habe», dazu den Hinweis, dass eine Abweichung als Atlas-Issue gehört und nicht in der geprüften App weggebügelt wird. Beide enthalten die harte Zeile: jede Zahl, die in Code landet, hat in `llms.txt` eine Quelle — steht sie dort nicht, wird sie nicht geschrieben.\n- **Zwei Tabellen, die es bisher nirgends gab.** Die Subpath-Tabelle des Pakets mit Server-Safety-Spalte beantwortet die erste Frage jeder neuen Route und die häufigste 500er-Ursache: **sechs** der 19 UI-Dateien sind server-safe (`badge`, `button`, `card`, `empty-state`, `input`, `skeleton`), obwohl `packages/atlas/README.md:119` pauschal behauptet, alle UI-Subpaths seien Client — eine Zeile später sagt dieselbe Tabelle «je Komponente». Dazu vier Auflösungs-Fallen: `/theme` importiert `MotionConfig` hart aus dem *optionalen* Peer `framer-motion`, der `/ui`-Barrel zieht `sonner` und `cmdk` mit, `/motion/heroes` löst nicht auf (Wildcard erwartet `heroes.tsx`, real ist es `heroes/index.tsx`), und `/ui/*` hat kein `.ts`-Fallback, `/motion/*` schon. Die Env-Tabelle führt die Spalte «wo real gelesen» und macht damit sichtbar, dass das Doku-Schema zugleich unvollständig (fünf real gelesene Variablen-Paare fehlen) und teilweise tot ist (`NEXT_PUBLIC_APP_NAME` liest niemand).\n- **`example/lib/atlas/techstack.ts` ist die einzige Quelle für beide Ausgaben** — React-frei, damit der Test unter `environment: 'node'` läuft. Die Ist-Versionen der Kurzantwort werden aus der `package.json` abgeleitet statt handkopiert: handkopierte Versionen werden beim ersten `npm update` still falsch, und zwar mit vollem Beleg-Schmuck daneben.\n- **`example/tests/techstack.test.ts` macht die Belegpflicht zum CI-Gate statt zum Vorsatz.** Geprüft wird nicht nur, ob der zitierte Pfad existiert und genug Zeilen hat, sondern ob der **erwartete Anker-Text an der zitierten Stelle steht** — sonst verrotten die Zeilennummern lautlos, sobald jemand oberhalb etwas einfügt. Der Typ erzwingt das auf Compiler-Ebene: wer eine Zeilennummer zitiert, muss einen Anker mitliefern. Der Mechanismus hat sofort gegriffen und einen falschen Beleg gefangen (`docs/10-security.md:112` statt `:111`). Dazu Paritätsprüfungen: jede Regel-ID kommt im Klartext genau einmal als Regelzeile vor, jede entschiedene Regel hat Gegenbeleg **und** Soll **und** Ist, alle IDs sind eindeutig. 11 neue Tests, Suite von 27 auf 38.\n- **Selbstbelastung bleibt drin.** Die Seite nennt zwei Stellen, an denen das Referenz-Projekt seine eigenen Regeln nicht erfüllt: `example/package.json` steht auf `0.1.0`, während Repo-Root und Package auf `0.2.0` stehen, und das von `docs/08-engineering.md:215` geforderte Feld `zvvAtlas` fehlt dort. Der Text liest die Version zur Laufzeit, damit der Befund verschwindet, sobald er behoben ist. Eine Seite, die Belegpflicht predigt, kann bei sich selbst keine Ausnahme machen.\n- **Bewusst nicht gebaut:** keine Motion, keine eigene Client-Insel, kein `dangerouslySetInnerHTML`. Die Seite ist ein Dokument, das zur Hälfte per `curl` gelesen wird — sie ist vollständig server-gerendert und ohne JavaScript lesbar (gegengeprüft: alle Regeltexte stehen im HTML). Einzige Client-Komponente ist der bestehende `CodeSnippet`, der ausschliesslich Strings entgegennimmt. Ebenso bewusst **nicht** dupliziert: Farbwerte, Zeitfenster, Feldlängen und Event-Namen aus den vier Spec-Kapiteln — eine Guideline-Seite, die ausgerechnet die Kapitel abschreibt, deren Änderung ein Major-Bump ist, wäre genau die Drift, die sie bekämpft.\n\n### Fixed — Deck-Slide «In der Anwendung»: die drei Geräterahmen waren leer\n\n- **Ursache: `X-Frame-Options: DENY` verbietet die Einbettung auch in die eigene Herkunft.** Der Showcase setzte den Header über `source: \"/:path*\"` für jede Route, und die Geräterahmen auf `/info` (Slide 3) zeigen drei Seiten **derselben** App. Am laufenden Server nachgemessen: `contentDocument` bei allen drei `iframe`s leer, kein `frame-ancestors`-Header vorhanden. Die Rückmeldung «kein Inhalt, keine Überlagerung» war damit wörtlich zu lesen — die Slide behauptete unter drei weissen Kästen, in ihnen laufe der Showcase selbst.\n- **`example/next.config.ts`** setzt für genau drei Pfade (`/`, `/organigramm`, `/mermaid`) zusätzlich `Content-Security-Policy: frame-ancestors 'self'`. Browser ziehen die CSP-Direktive dem älteren Header vor, `DENY` wird dort wirkungslos; fremde Origins bleiben ausgesperrt. Im Browser gegengeprüft: die drei Pfade betten ein, `/tokens`, `/info` und `/organigramm/x` weiterhin nicht. Die Pfade stehen als `EINGEBETTETE_PFADE` einzeln aufgezählt statt als Präfix-Muster, damit Unterseiten die Lockerung nicht erben.\n- **`docs/10-security.md` §7 zieht die Ausnahme nach** — mit vier Bedingungen (Pfade einzeln, Liste neben dem einbettenden Code, alle übrigen Routen unverändert, Begründung in der App-Doku). Eine Ausnahme im Code ohne Eintrag in der Spec wäre genau die Drift, die Atlas abstellen soll.\n\n### Changed — Deck-Slide «In der Anwendung»: Argument links, gestaffelte Bühne rechts\n\n- **`components/deck/device-stage.tsx` ersetzt `device-showcase.tsx`.** Die Slide ist jetzt ein Zweispalter: links eine Leitzeile und drei nummerierte Belege mit je einer Quellenangabe (`example/`, `Kapitel 03`, `patterns/organigramm.md` · `Kapitel 01`), rechts die drei Geräte gestaffelt in der Tiefe statt nebeneinander in einer Reihe. Der Text trägt die Slide auch ohne die Rahmen — hängt ein `iframe`, steht das Argument trotzdem.\n- **Jedes Gerät trägt dauerhaft eine Beschriftung** mit Route, logischer Viewport-Grösse (1440 × 900 / 834 × 1112 / 390 × 844) und einem Satz dazu, was dort läuft. Die Texte gab es schon, sie hingen aber an `:hover` und waren im projizierten Vortrag nie zu sehen. Ab `xl` liegt die Beschriftung als Überlagerung auf der Gehäuseoberkante, darunter wird dasselbe Element zur Bildunterschrift — ein Markup, zwei Anordnungen, drei `iframe`s statt sechs.\n- **Neuer Baustein `Beleg`** in `components/deck/blocks.tsx`: nummerierte Behauptung mit optionaler Quellenzeile. Die Form für Slides, die etwas beweisen statt aufzählen sollen — `Bullet` sagt «gleichwertig», ein nummerierter Beleg sagt «erster von genau dreien».\n- **Mount-Sperre:** einmal betreten, bleiben die Rahmen geladen. Vorher hingen sie am Sichtbarkeitszustand und luden bei jedem Zurückblättern alle drei Seiten samt Mermaid-Bibliothek neu.\n- **Ersatzfläche je Rahmen** für den Fall, dass die Einbettung doch scheitert: die Kachel verlinkt auf die Seite, der pulsende Live-Punkt hört auf, und die Leitzeile der Textspalte wechselt mit. Beide Zustände im Browser durchgespielt.\n- **Skalierung ohne JavaScript-Messung** über eine CSS-Variable pro Umbruchpunkt statt `ResizeObserver` — kein leerer erster Frame, kein Sprung nach der Hydration, keine Annahme über die Polsterung des Decks. Geometrie auf 390 × 844 bis 1920 × 1080 vermessen: kein waagrechter Überlauf, keine sich überschneidenden Beschriftungen, Slide-Höhe im Rahmen der übrigen Slides.\n\n### Fixed — Confluence-Mermaid: das Recipe schrieb den doppelten Knoten vor\n\n- **`patterns/mermaid-diagram.md` §7 korrigiert.** Der Abschnitt behauptete, ein blosser Codeblock mit `language=mermaid` rendere in Confluence nicht, und verlangte, den `extension`-Knoten des Forge-Makros **zusätzlich** von Hand zu schreiben. Zutreffend ist das Gegenteil: Die Umwandlung der Confluence-API von HTML nach ADF erzeugt aus einem `<pre><code class=\"language-mermaid\">` selbst beide Knoten — das Makro mit korrekt durchnummeriertem `guestParams.index` und einen eingeklappten `expand` „Diagram\" mit dem Quelltext. Wer den Makro-Knoten mitschickt, verhindert genau diese Paarung: Das Makro rendert das Diagramm, der Codeblock bleibt zusätzlich als sichtbarer Quelltext darunter stehen.\n- **Wirkung im Bestand:** Fünf Atlas-Kapitelseiten im Space ZVV-DEV zeigten jedes Diagramm doppelt — Kapitel 3, 5, 7 und 10 mit je einem, Kapitel 6 mit zwei. Korrigiert am 3. August 2026, indem die von Hand gesetzten `extension`-Knoten entfernt und der Rest unverändert zurückgeschrieben wurde; die Paarung entsteht beim Speichern neu. Gegengeprüft an der abgelegten ADF-Struktur: genau ein `extension`-Knoten, gefolgt vom `expand` mit dem Codeblock. Die Fehlerfall-Tabelle in Kapitel 6.6 führte denselben Irrtum als Abhilfe („nötig ist zusätzlich der Makro-Knoten\") und ist auf den tatsächlichen Fall umgeschrieben.\n- **Warum es lange nicht auffiel:** Die arc42-Seiten des KundenRadar entstanden am selben Tag **ohne** den Zusatz-Knoten und rendern seither korrekt — betroffen waren allein die Seiten, die nach dem Recipe nachbearbeitet wurden. Die neue Fassung nennt ihre Geltungsgrenze ausdrücklich: geprüft über die Atlassian-MCP-Schnittstelle mit `contentFormat: html`; für Storage-Format, ADF und den Editor ist das Verhalten offen und im Recipe als offen markiert.\n\n### Added — Meta-Views Stufe 1: `appMeta()`, `/api/meta`, `/info`\n\n- **`@zvvch/atlas` → `appMeta()`, `resolveEnvironment()`, `ENVIRONMENT_LABEL`** (`packages/atlas/src/lib/app-meta.ts`) — die Selbstauskunft einer ZVV-App als ausführbarer Helfer. Der Satz «jede offizielle Auslieferung muss sich als **version + commit + build time + environment** erklären lassen» steht heute **wortgleich** in den `CLAUDE.md` von zvv-fzdb und zvv-mailer — zwei Repos, dieselbe Doktrin, aber fünfmal einzeln als Header-Markup ausformuliert. Jetzt einmal. Die Identitäts-Felder (Name, Zweck, Repo, Kontakt) gibt der Aufrufer mit, die Release-Werte kommen aus Standard-Variablen (`NEXT_PUBLIC_APP_VERSION`, `NEXT_PUBLIC_GIT_COMMIT`, `NEXT_PUBLIC_BUILD_TIME`, `NEXT_PUBLIC_APP_ENV`), jeder einzeln überschreibbar. DOM-frei und ohne Dateizugriff — bleibt damit im server-sicheren Wurzel-Export.\n- **Zwei bewusste Entscheidungen im Helfer:** `buildTime` fällt **nicht** auf «jetzt» zurück, sondern bleibt `null` — eine erfundene Build-Zeit sieht bei jedem Abruf anders aus und täuscht einen Neu-Deploy vor. Und `integration` wird am Branch-Namen erkannt: Vercel kennt nur `production` und `preview`, der Integrations-Branch fährt als Preview-Deployment und meldete sich sonst als beliebiges Preview, womit die Aggregation ihn falsch zählt.\n- **`/api/meta` als Standard-Pfad** (Showcase) — öffentlich und ohne Auth (Entscheid MRA, 3. August 2026). Das ist der eigentliche Hebel: erst maschinenlesbar können `zvv-statuspage` und `zvv-cockpit` den Stand aller Apps einsammeln statt ihn zu raten. Pflichtteil ist `appMeta()`, alles darunter app-spezifische Zugabe. Cachebar mit Validatoren (`public, max-age=300` + ETag), Methoden-Vertrag wie die übrigen Endpunkte.\n- **`/api/atlas` bleibt bestehen** und liefert denselben Payload — beide Routen speisen sich aus `lib/atlas/meta.ts`, damit sie nicht auseinanderlaufen. Neue Integrationen zeigen auf `/api/meta`. Verifiziert: beide Antworten byte-gleich.\n- **Showcase `/info` als Management-Deck** — Vollbild-Präsentation aus vier Slides statt Datenblatt-Seite: Leitzeile, Topologie-Diagramm, «Was & für wen», Steckbrief. Blättern per Tastatur (→ ␣ PageDown · ← PageUp · Home · End), Wischen und Knöpfen; alle Slides bleiben gemountet und werden per `opacity` überblendet, damit das Diagramm einmal rendert statt bei jedem Blättern neu. Die Fläche ist fest dunkel, unabhängig vom Theme — ein Deck wird projiziert, nicht neben der App gelesen. Bewusst **ausserhalb der Showcase-Shell** (`app/info/`), weil ein Vollbild-Deck keine Sidebar verträgt. Wiederverwendbare Teile liegen in `components/deck/` (Hülle, Bausteine, Diagramm-Slide); der Steckbrief mit Version, Commit, Build-Zeit und Umgebung ist die letzte Slide, nicht der Zweck der Seite.\n- **Genre-Korrektur gegenüber der ersten Fassung:** `/info` war zunächst als technische Auskunftsseite gebaut, und das Proposal hatte empfohlen, das bestehende Deck des KundenRadars nach `/story` zu verschieben. Das war die falsche Richtung (Rückmeldung MRA) — **`kundenradar/info` ist die Referenz-Implementierung des Genres**, nicht die Abweichung. Das Proposal führt die Korrektur mit und hält den Deck-Aufbau als Spec fest (§4.1a). Die Atlas-Fassung nutzt dabei `mermaidTheme()` statt handgeschriebener Theme-Variablen — dieselbe Palette zweimal zu definieren ist genau die Drift, die Atlas abstellen soll.\n- Tests: `packages/atlas/tests/app-meta.test.ts` (Environment-Ableitung inkl. Integrations-Branch und Zurückweisung unbekannter Angaben, Variablen-Fallbacks, kein Build-Zeit-Default).\n\n### Fixed — Changelog-Parser zählte auf Windows-Checkouts null Releases\n\n- **`example/lib/atlas/version.ts` trennt jetzt auf `\\r?\\n`** statt nur auf `\\n`. Blieb ein `\\r` am Zeilenende stehen, scheiterte die Datums-Gruppe der Release-Regex: `\\r` ist in JavaScript ein Zeilenterminator, den `.` nicht matcht. Folge war ein stiller Totalausfall — `getAtlasReleases()` lieferte null statt fünf Releases, `/api/atlas` meldete `latestVersion: \"0.0.0\"`. Auf Linux (CI, Vercel) fiel das nie auf, weil die Datei dort LF hat; sichtbar wurde es erst, als der Steckbrief des Decks «Atlas v0.0.0 · 0 Releases» anzeigte.\n- **`docs/proposals/meta-views.md`** — die Analyse dahinter, aus den bestehenden Implementierungen in fünf Apps. Kernbefund: es sind **drei Genres, nicht eines**, und «Info» existiert faktisch nur einmal — `kundenradar/info` ist eine Produkt-Story im Präsentationsmodus, keine Auskunftsseite, und muss der Pflichtroute weichen. Dazu vier Changelog-Implementierungen mit zwei unvereinbaren Philosophien (release- vs. produktorientiert) und drei Routennamen für dieselbe Sache. Entschieden: `/api/meta` öffentlich, `CHANGELOG.md` als einzige Quelle mit abgeleiteter Feature-Sicht, Fachdoku-Route heisst `/docs`. Stufen 2 und 3 stehen aus.\n\n### Added — Mermaid: Kategorie-Reihen und Akzent-Palette\n\n- **`MERMAID_ACCENTS` + `MERMAID_CLASS_DEFS`** (`packages/atlas/src/lib/mermaid-theme.ts`) — die sieben Akzenttöne des Organigramms als benanntes Objekt und als fertige `classDef`-Zeilen. Letztere halten Hex-Werte aus dem Diagramm-Quelltext heraus: voranstellen, dann `class KNOTEN petrol`. `MERMAID_ACCENT` bleibt als Einzelwert bestehen und zeigt jetzt auf `MERMAID_ACCENTS.blau`.\n- **Die Kategorie-Reihen sind gesetzt** (`pie1…pie12`, `cScale0…cScale11` + `cScaleLabel*`, `git0…git7` + `gitBranchLabel*`) — **dieselbe Falle wie das Sequenzdiagramm-Gelb, eine Diagrammfamilie weiter** (Rückmeldung MRA: «mermaids haben noch nicht mehr farben erhalten»). Zeitstrahl-, Reise-, Torten- und gitGraph-Diagramme färben ihre Abschnitte nicht über `primaryColor`, sondern über diese durchnummerierten Reihen; ungesetzt stünde ein Zeitstrahl im Mermaid-Regenbogen neben blauen Flussdiagrammen. Zwei Feinheiten sind bewusst: Mermaid hält zwölf Plätze bereit, die Palette hat sieben Töne — die Plätze 8–12 sind **abgedunkelte** Wiederholungen, weil eine Aufhellung die weisse Beschriftung unter WCAG AA drücken würde; und hell und dunkel teilen sich dieselbe Reihe, weil die Töne auf mittlerer Helligkeit sitzen und sich in beide Richtungen abheben. Torten brauchen zusätzlich `pieOpacity: '1'` (Mermaids Standard von 0.7 wäscht die Akzente zu Pastell aus) und `pieSectionTextColor: '#ffffff'`.\n- **Grundsatz im Pattern verankert (§2.5):** die Grundfläche bleibt einfarbig. Farbe kommt nur an zwei Stellen dazu — bewusste Hervorhebung per `classDef` und Kategorien, die das Diagramm ohnehin selbst einfärbt. Flächendeckend eingefärbte Knoten lesen sich als Dekoration und kosten die Möglichkeit, überhaupt etwas hervorzuheben.\n- **Showcase `/mermaid`** — neue Sektionen **Zeitstrahl** und **Torte** als Belege der beiden Fälle, das Flussdiagramm markiert seine drei Rollen (Quelle · Auslieferung · Konsum) über `MERMAID_CLASS_DEFS`. Dazu eine Übersicht der 30 Diagrammfamilien dieser Mermaid-Version, geteilt in abgestimmt / abgestimmt-ohne-Demo / nicht abgestimmt. `gantt` ist als nächster lohnender Kandidat benannt, aber bewusst **nicht** halb abgestimmt: sein Variablensatz ist eigenständig und ein halber Satz fällt erst im fertigen Diagramm auf. Verifiziert im Live-Render: Fluss blau/petrol/grün, Zeitstrahl blau/rost/petrol, Torte blau/rost — kein Mermaid-Standardton mehr im DOM.\n\n### Changed — Lightbox: die ganze Fläche ist der Auslöser, Inhalt füllt das Vollbild\n\n- **Flächen-Trigger statt Eck-Symbol** — der Weg mit der Maus in die obere rechte Ecke war unbequem (Rückmeldung MRA). Der naheliegende Gegenvorschlag, das Symbol zu **zentrieren**, ist der falsche Ausweg: dort deckt es genau die Stelle ab, an der bei einem Diagramm die Aussage steht. Jetzt ist die ganze Fläche der Button (`absolute inset-0`, `cursor-zoom-in`), das `Maximize2`-Symbol bleibt als `aria-hidden`-Hinweis und ist auf Touch dauerhaft sichtbar. Trefferfläche gemessen: von ~28×28 px auf 946×196 px beim Mermaid-Flussdiagramm. Als **Überlagerung** gebaut, nicht als Umhüllung, damit der Inhalt nicht in einem `<button>` verschachtelt wird; der Preis — die Fläche schluckt Textauswahl und Klicks nach innen — ist im Pattern als Geltungsgrenze festgehalten (nur nicht-interaktive Artefakte).\n- **Panel grösser, Inhalt zentriert und auf volle Breite** — `min(95vw, 92vh·ratio)` statt `min(90vw, 85vh·ratio)`. Entscheidend war aber nicht das Panel: SVGs bringen aus ihrer Quelle eine eigene `max-width` mit (Mermaid schreibt sie als Inline-Style) und blieben im Vollbild auf Inline-Grösse stehen, mitten in einem viel grösseren Panel. Der Inhalts-Wrapper sticht das jetzt mit `[&_svg]:!max-w-none [&_svg]:w-full [&_svg]:h-auto`. Die Selektoren gehören auf den Wrapper und **nicht** aufs Panel — sonst zöge `w-full` auch das Icon im Close-Button auf Panelbreite (Descendant-Selektor schlägt `h-4 w-4`). Gemessen am Organigramm (Viewport 1298×1283): Panel 1233×835, Diagramm 1183×785, 96% der Panelbreite.\n- **Glas mit Richtung** — das Panel füllt jetzt mit einem Verlauf aus Weiss und dem ZVV-Hellblau `#ebf4fb` (derselbe Ton, den Organigramm und Mermaid-Diagramme als Fläche nutzen) statt mit flachem `bg-card/25`. Deckkraft durchgehend unter 50%, weiterhin **kein** zweiter `backdrop-blur` auf dem Panel — der Glaseffekt bleibt voll erhalten.\n\n### Changed — Organigramm: eine Farbe pro Abteilung\n\n- **Abteilungs-Palette (`DEPT_ACCENTS` in `assets/organigramm/orgchart.mjs`)** — das Organigramm stand in drei Abstufungen desselben Blaus (Header, Team-Panel, Sub-Panel) und las sich dadurch als Farbverlauf statt als sieben gleichrangige Einheiten (Rückmeldung MRA). Jede Abteilung bekommt jetzt einen eigenen Farbton: Rost `#be4f17`, Petrol `#007d86`, Beere `#ca3771`, Grün `#15814b`, Violett `#9d50bb`, Bronze `#986806`. Team- und Sub-Panel sind unverändert Aufhellungen (92% / 62% Weiss) — neu aber des jeweiligen Akzents, wodurch die Unterscheidung durch die ganze Box trägt. Drei Eigenschaften der Palette sind bewusst gesetzt und im Pattern dokumentiert: (1) **gleiche Luminanz** — alle Töne liegen auf ~4.9:1 Kontrast gegen Weiss, dem Wert des ZVV-Blaus, unterscheiden sich also im Farbton statt im Gewicht und erfüllen zugleich WCAG AA für den weissen Header-Text; (2) **alternierend warm/kalt**, damit benachbarte Boxen der beiden Reihen maximal differieren; (3) **chart-lokal** — kein Brand-Token und nicht mit den VBZ-Linienfarben zu verwechseln.\n- **Direktion und erste Abteilung bleiben `--color-zvv-primary`** — das Token bleibt sichtbar gesetzt, und mit ihm stehen die Tints `#ebf4fb`/`#a6ceee` weiter im generierten SVG, aus dem `mermaidTheme()` sie zitiert. Die Provenienz-Kette des Mermaid-Patterns bricht also nicht.\n- **Ausweichen an drei Stellen:** `\"color\": \"#rrggbb\"` an einer Abteilung überschreibt die Palette (ein gesetzter, aber ungültiger Wert wirft mit Abteilungsname, statt still zurückzufallen); `--monochrome` im CLI und der Umschalter **«Farbig ↔ Einfarbig»** im Showcase-Editor rendern alles im Token-Blau — die Variante für Ausgaben, die dem einfarbigen zvv.ch-PDF folgen. Der Umschalter gilt für Vorschau, SVG-Export und beide Embed-Snippets. Tests: `example/tests/orgchart-palette.test.ts` (Zuordnung, Panel-Ableitung, Vorrangregel, Kontrast-/Luminanz-Garantie).\n- **Zuständigkeiten-Diagramm unverändert** — dort kodiert die Blau-Abstufung die Ebenen (Kanton/Gemeinden → ZVV → MVU → weitere VU), sie ist Bedeutung und nicht Dekoration.\n\n### Added — Mermaid-Pattern\n\n- **`patterns/mermaid-diagram.md`** — Guideline: Mermaid-Diagramme in ZVV-Farben. Kanonisiert drei empirisch belegte Befunde, die bisher jede App einzeln gekostet haben: (1) Mermaid **misst** Beschriftungsrahmen mit der `body`-Schrift, **rendert** aber mit `themeVariables.fontFamily` — weichen sie ab, wird Text abgeschnitten (gemessen: 200 px Text in 163 px Rahmen, Faktor 0.815 = Breitenverhältnis Geist zu Segoe UI). Die beiden naheliegenden Korrekturen sind als Irrwege dokumentiert: eine CSS-Regel auf `svg text` ändert nur die Darstellung und verschlimmert es, eine feste Systemschrift bricht beim nächsten Schriftwechsel. (2) `wrappingWidth` deckelt bei 200 px und **schneidet ab statt umzubrechen** — erkennbar an exakt `200.0` px breiten Labels neben kurzen mit natürlicher Breite; Empfehlung 340, Kantenlabels haben eine eigene Deckelung. (3) Farbabstimmung auf das Organigramm, Dunkelmodus als eigene Abstimmung statt Umkehrung. Dazu: `mermaid.render()` statt `run()` (misst keinen verdeckten Container), `await document.fonts.ready`, `securityLevel: 'strict'`, lazy Nachladen, Vollbild mit Ratio-Sizing aus der `viewBox`. Referenz: `zvv-mcp-gtfs/web/components/mermaid-diagram.tsx`, weiterentwickelt aus zvv-fzdb.\n- **Showcase `/mermaid`** — eigene Sektion mit Nav-Eintrag (Gruppe Komponenten): Flussdiagramm mit `classDef`-Akzent, **Sequenzdiagramm als eigentlicher Farbtest** (ohne die Akteur-Variablen stünde es gelb neben blauen Flussdiagrammen) und lange Beschriftungen als Beleg für `wrappingWidth`. Das Vollbild kommt aus dem bestehenden `Lightbox`-Primitive statt aus einer zweiten Implementierung — `DiagramLightbox` nimmt den SVG-String von `mermaid.render()` direkt entgegen und leitet die Dialogbreite aus der viewBox ab. Verifiziert im Live-Render: kein Label bei exakt 200 px, lange Beschriftungen brechen auf zwei Zeilen mit vollständigem Text um, Akteur-Fläche auf dem Palettenwert. Neue Abhängigkeit `mermaid` (lazy geladen, nur auf dieser Seite).\n- **Umbruchbreite nach Sichtprüfung von 340 auf 260 korrigiert**, dazu `MERMAID_NODE_PADDING` (10 statt Mermaids 15). Die erste Fassung folgte der Empfehlung 340 — im gerenderten Ergebnis wurden Knoten mit langem Text damit 356 px breit und wirkten neben kurzen Knoten wuchtig (Rückmeldung MRA). 260 liegt weiterhin komfortabel über der abschneidenden Grenze von 200; gemessen schrumpfte dasselbe Flussdiagramm von 775 auf 568 px Gesamtbreite und die Box mit langem Label von 356 auf 300 px, ohne Textverlust. Ergänzt im Pattern: **Kantenlabels ziehen die Ränge auseinander** und sind der häufigere Grund für ein zu breites Diagramm als die Knoten selbst — mit Messanleitung zur Unterscheidung.\n- **`@zvvch/atlas` → `mermaidTheme()`, `MERMAID_ACCENT`, `MERMAID_WRAPPING_WIDTH`** (`packages/atlas/src/lib/mermaid-theme.ts`) — die Palette als ausführbarer Export statt als abzutippende Tabelle. Werte `#ebf4fb`/`#a6ceee`/`#0072ce` stammen wörtlich aus `assets/organigramm/zvv-organigramm.svg` (kanonische Quelle). Setzt die **Sequenzdiagramm-Variablen** mit (`actorBkg`, `signalColor`, `noteBkgColor` …) — ohne sie stehen Sequenzdiagramme in Mermaids Standardgelb, während die Flussdiagramme blau sind; der häufigste Fehler beim Umfärben, weil er erst Wochen später auffällt. Bewusst **DOM-frei** (Schrift als Parameter), damit der Helfer im server-sicheren Wurzel-Export bleibt und das Import-Prüfskript nicht bricht. Tests: `packages/atlas/tests/mermaid-theme.test.ts`.\n\n### Fixed — AEM-Embed: Lightbox-Overlay nicht mehr auf Design-Grösse gedeckelt\n\n- **Render-Option `scale` + AEM-Snippets mit 2× natural size** — die zvv.ch-Lightbox (lightGallery) zeigt ein Overlay-`<img>` nie grösser als seine natural size: mit `width`/`height` = viewBox (1392px) blieb das Widget-Organigramm im Overlay sichtbar kleiner als die alten Print-Renditions der Site (7016px-PNG, füllt den Viewport) — Feedback MRA vom INT-Test. Die AEM-Embeds beider Diagramme rendern jetzt mit `scale: 2` (natural size 2× viewBox, beim Vektorformat verlustfrei und ohne Dateikosten); die viewBox bleibt Design-Grösse und deckelt inline weiter via Container-`max-width`. `buildAemImageSnippet()` übernimmt die echte natural size ins `<img>` und wirft bei responsive gerendertem SVG, statt still ein Mini-Overlay zu bauen. CLI-Output unverändert (Default 1×, eingecheckte SVGs byte-identisch). Pattern: [lightbox.md §7](./patterns/lightbox.md) (jetzt drei Pflicht-Details) · Tests: `example/tests/orgchart-embed.test.ts`.\n\n### Changed — Einheitliches Export-Panel\n\n- **Showcase `/organigramm`** — beide Diagramme haben jetzt dasselbe Export-Layout (geteiltes `ExportPanel`, gleiches `1fr|320px`-Grid): Font-Checkbox, AEM-Image-Markup-Checkbox, Download- und Embed-Button, Snippet-Beispiel und Hinweise sind identisch. Vorher hatte die Zuständigkeiten-Sektion nur eine lose Button-Zeile (Feedback MRA); der Font war dort zudem nicht abwählbar.\n\n### Changed — Diagramm-Benennung folgt zvv.ch\n\n- **`organigramm.data.json` → `title: \"Organisation und Mitarbeitende im ZVV\"`** — die AEM-Seite benennt das Diagramm neu so (vorher «Organigramm ZVV»); Atlas zieht die kanonische Benennung nach (Drift = Atlas-Bug). Die generierte SVG-`<desc>` prefixt jetzt `data.title` statt eines hartkodierten Satzes, die Embed-Kommentare beider Diagramme nutzen `data.title` — künftige Umbenennungen sind damit reine Daten-Edits. «Zuständigkeiten im ZVV» war bereits deckungsgleich.\n\n### Added — Lightbox-Pattern\n\n- **AEM-Andocken (zvv.ch), Zero-JS** — Pattern §7: Widgets auf zvv.ch nutzen die bestehende Site-Lightbox (lightGallery) statt eigener Overlay-Logik. Die Clientlib bootstrapt jedes `.cmp-image`; das Embed liefert dafür `cmp-image--lightbox`-Markup mit dem SVG als data-URI-`<img>` (feste `width`/`height` für die natural size, Font zwingend eingebettet — SVG-as-image lädt keine Seiten-Ressourcen). SEO in dieser Variante via JSON-LD + `alt` (aus `<desc>`). Generator: «AEM-Image-Markup»-Option im `/organigramm`-Export (`buildAemImageSnippet()`).\n\n- **`patterns/lightbox.md`** — Guideline: Glassmorphism-Lightbox für nicht-interaktive Artefakte (Diagramme, Grafiken). Hover-Trigger (`group-hover`, tastatur-erreichbar), ein einziger Blur-Layer (`bg-background/30 backdrop-blur-md`), Glas-Panel (`bg-card/25` + `border-white/15` + `ring-white/10`, bewusst ohne zweiten `backdrop-blur`), Dialog-Breite aus dem SVG-Seitenverhältnis (`min(90vw, 85vh·ratio)`), Schliessen via ESC/Backdrop/Button, Scroll-Lock ohne Layout-Sprung, Focus-Trap mit Fokus-Restore (WCAG 2.1.2/2.4.3). Kanonisierung der zvv-fzdb-`/spec`-Implementierung (Mermaid-Diagramme); Abgrenzung: interaktive Modals bleiben bei shadcn/Radix `Dialog`.\n- **`@zvvch/atlas/ui` → `Lightbox`** — das Pattern als eigenständiges, wiederverwendbares UI-Primitive (`packages/atlas/src/ui/lightbox.tsx`): children-basiert (Diagramme, Grafiken, Bilder), `aspectRatio`-Prop fürs Ratio-Sizing, optionaler `fullscreen`-Inhalt, Focus-Trap intern. Showcase-Kopie in `example/components/ui/` (Dogfooding ausstehend), Demo auf `/components`.\n- **Showcase `/organigramm`** — beide Org-Diagramme öffnen in der Lightbox; `diagram-lightbox.tsx` ist ein dünner Wrapper um das Primitive für vorgerenderte SVG-Strings (extrahiert `aspectRatio` aus der viewBox).\n- **Showcase `/lightbox`** — eigene Komponenten-Seite mit Nav-Eintrag (konsistent zu Toasts/Empty States/Skeletons): Demos für Bild, Generator-Diagramm und Hochformat (zeigt das Ratio-Sizing), Usage-Snippet und A11y-Garantien; die Lightbox-Sektion auf `/components` entfällt dafür.\n\n### Added — Organigramm-Pattern\n\n- **`patterns/organigramm.md`** — Guideline: die beiden offiziellen ZVV-Org-Diagramme als generierte Artefakte (Daten → SVG statt Grafik-Tool). Alle Daten sind öffentlich (entsprechen der Publikation auf zvv.ch → Organisation, Einstufung MRA Juli 2026).\n- **`assets/organigramm/`** — `organigramm.data.json` + `zustaendigkeiten.data.json` (Daten), `generate.mjs` (Node-Generator, null Dependencies, liest `--color-zvv-primary` aus `assets/color-tokens.css`, Font ZVV Brown Narrow, optional `--embed-font`, `--chart` wählt ein einzelnes Chart), `zvv-organigramm.svg` + `zvv-zustaendigkeiten.svg` (eingecheckte Outputs). Root-Script: `npm run orgchart` generiert beide.\n- Organigramm portiert aus dem Standalone-Python-Generator (Layout geometrie-identisch verifiziert); Brand-Fixes gegenüber dem Original: Token-Farbe statt hartcodiertem `#0083CA`, Corporate-Font statt Liberation Sans.\n- **Zuständigkeiten-Diagramm** — Nachbau der zweiten offiziellen Grafik der Organisations-Seite (Kanton/Gemeinden → ZVV → 8 MVU → 26 weitere Verkehrsunternehmen) als `renderZustaendigkeiten` in derselben Daten→SVG-Pipeline.\n- **`assets/organigramm/orgchart.mjs`** — Render-Logik als pures Modul (`renderOrgchart`, `renderZustaendigkeiten`, `toPublicData`), geteilt von CLI und Showcase. `--public`-Flag rendert das Organigramm namenlos (= offizielles zvv.ch-PDF).\n- **Showcase `/organigramm`** — Live-Editor (JSON → SVG on-the-fly, client-side) startet mit den echten, offiziell publizierten Org-Daten; Scope-Toggle Vollversion/Public, SVG-Download mit optionalem Font-Embedding, HTML-Embed-Snippet-Generator für CMS-Widgets (responsives Inline-SVG); Zuständigkeiten-Sektion mit denselben Exporten. `sync-atlas-assets.mjs` spiegelt Render-Modul + beide Daten-JSONs.\n- **Embeds ohne Titel** — Render-Option `omitTitle` (CLI `--no-title`) lässt den eingerenderten Titel weg; die Embed-Snippets beider Diagramme nutzen sie, weil die Überschrift auf der Zielseite (zvv.ch/AEM) als eigenes Element gepflegt wird. `aria-label` bleibt; SVG-Download und Inline-Ansicht behalten den Titel (Standalone-Artefakte).\n- **SEO-Semantik für Embeds** — `toJsonLd()` in `orgchart.mjs` generiert schema.org-`Organization` (Abteilungen als `department`, Personen als `employee` mit `jobTitle`, Rollen-Marker `*`/`(Stv.)`/`(a.i.)` gestrippt; nach `toPublicData()` ohne Personen); das Organigramm-Embed-Snippet legt es als `<script type=\"application/ld+json\">` bei. Beide Renderer schreiben `<title>`/`<desc>` ins SVG (Bildersuche, Standalone-Dateien) — unabhängig von `omitTitle`. Unit-Tests: `example/tests/orgchart-seo.test.ts`.\n\n### Added — Atlas-Projekt-Mark\n\n- **`assets/atlas-icon.svg`** — eigenständiges Atlas-App-Icon (Favicon, Vercel-Avatar, OG). Folgt der ZVV-App-Icon-Konvention (weisses Glyph auf ZVV-blauem `#0072CE` rounded-square Tile, wie fzdb/mailer/tama); Atlas-Glyph: Meridian-Globus (Atlas = Kartenwerk aller ZVV-Apps). Grenzt sich bewusst vom unveränderlichen ZVV-Dachlogo ab (`docs/01-identity.md` §5).\n- **`example/app/icon.svg`** — App-Router-Favicon, ersetzt die Next.js-Default-`favicon.ico`. Wird via `sync-atlas-assets.mjs` aus dem kanonischen Asset gespiegelt.\n- **`example/app/opengraph-image.tsx`** — 1200×630 OG/Twitter-Card im ZVV-OG-Stil (vgl. fzdb): Blau-Gradient, „Atlas\"-Hero, Meridian-Globus-Anker, Spec-Kapitel-Strip.\n\n### Fixed — drei Zusagen, die dieser Bump fällig gemacht hätte\n\n- **Der Sprung auf v0.3 hätte zwei Versprechen gebrochen, die auf genau diese Nummer lauteten:** `docs/08` sagte «CLI-Tooling in v0.3 wird das auswerten und Drift reporten», `docs/09` «OpenTelemetry/Otel-Collector ist Backlog für v0.3». Beides existiert nicht. Ein drittes (`docs/01`, Submodule «geplant für v0.3+») stand knapp daneben.\n- **Das ist derselbe Fehler, den `docs/proposals/ungedeckte-luecken.md` für den v0.2-Bump bereits protokolliert hat** — dort war es die Zusage «v0.2 wird Migration-Guides liefern», die mit dem Bump fällig wurde und nicht eingelöst war. Eine Zusage auf eine Versionsnummer wird beim Release stillschweigend zur Falschaussage; niemand prüft beim Bumpen die Prosa.\n- **Alle drei nennen jetzt keine Version mehr**, sondern sagen ausdrücklich «Backlog, ohne Zusage auf eine Version». Damit kann der nächste Bump sie nicht erneut brechen.\n- Die überfällige v0.2-Zusage zu den Migration-Guides (`docs/07`, `docs/04`) bleibt offen — sie hat einen eigenen Eintrag in `ungedeckte-luecken.md` und einen Entscheidungsträger."},{"version":"0.2.0","date":"2026-06-04","body":"### Added — Installierbares Package `@zvvch/atlas`\n\nAtlas wird zum npm-Workspace-Monorepo. Die wiederverwendbaren Bausteine sind als **Source-Package** extrahiert und über **GitHub Packages** (privat) installierbar — ZVV-Apps konsumieren sie via `npm i @zvvch/atlas` + Next.js `transpilePackages`.\n\n- **`packages/atlas/`** — kanonische Quelle (Foundation + Komponenten), 48 Dateien:\n  - Foundation-Root (`@zvvch/atlas`): `cn`, VBZ-Linienfarben, Lottie-Meta, Zürich-Zeit, Easings — server-safe.\n  - `@zvvch/atlas/ui` — 18 shadcn/ui-Primitives (Radix).\n  - `@zvvch/atlas/motion` — Motion-System (Framer + Canvas2D), Lottie-Tools, LinkedIn-Banner, 5 Projekt-Heroes — reduced-motion-safe.\n  - `@zvvch/atlas/theme` — ThemeProvider + Auto-Tag/Nacht-Toggle.\n  - `@zvvch/atlas/styles.css` — Brand-Tokens + VBZ-Farben.\n  - `@zvvch/atlas/assets/*` — Logos.\n- **Publish-Pipeline:** `.github/workflows/publish-atlas.yml` (Tag `atlas-v*` → GitHub Packages, Version-Gate gegen `package.json`).\n- **Scope `@zvvch`** statt `@zvv`: GitHub Packages verlangt Scope == Org-Name (`github.com/zvvch`).\n- **Integrationsguide:** `docs/12-atlas-package.md` (Auth, transpilePackages, Tailwind-`@source`, Peer-Deps).\n\n### Notes\n\n- Source-Shipping (kein gebautes `dist`): Consumer transpilen via `transpilePackages` — `\"use client\"` bleibt erhalten, optimal fürs reine Next.js-Ökosystem.\n- **Dogfooding deferred:** `example/` konsumiert das Package noch nicht — der Vercel-Build läuft mit Root-Directory `example/` (kein Parent-Zugriff). Die Migration auf Repo-Root-Workspace-Build ist ein dokumentierter Folgeschritt; bis dahin behält `example/` eigene Kopien."},{"version":"0.1.2","date":"2026-05-29","body":"### Added — Halo-Basisprojekt\n\nLiving-Documentation-App unter `example/` wird zum Halo-Basisprojekt für künftige ZVV-Apps. 20 Showcase-Routen, 4 API-Endpoints, Mock-Backend mit 4 Personas, 250 Mock-Fahrzeugen, deterministischen Seeded-PRNG.\n\n**Phase A → C Sektionen (20 total):**\n- Identity: `/tokens`, `/typography`\n- Auth: `/login` (Stage 1+2 Toggle), `/auth-flow` (5-Step + Pre-Check-Matrix)\n- Navigation: `/navigation`, `/components`, `/toasts`\n- Data: `/tables` (TanStack 250 Vehicles), `/forms/simple`, `/forms/multi-tab` (4-Tab Edit)\n- Visualisierung: `/dashboard`, `/charts/recharts` (Bar/Pie/Line/Area), `/charts/echarts` (Bar/Donut/Smooth/Heatmap)\n- Mailing: `/mail-templates` (Iframe-Sandbox-Preview, fiktive `atlas-demo/*` Slugs)\n- Observability: `/health-status`, `/icons`, `/skeletons`, `/empty-states`\n- Marketing: `/marketing-hero` (Cockpit-Style Glassmorphism)\n- Home: `/` Pattern-Index Cards\n\n**APIs:**\n- `/api/health` — Atlas-Pflicht-Endpoint\n- `/api/atlas` — Conformance + Stack + Patterns JSON\n- `/api/changelog` — Atlas-CHANGELOG.md geparsed\n\n**Cutting-Edge Stack (über v0.1.1 hinaus):**\n- @vercel/analytics + @vercel/speed-insights (Production-Tracking)\n- @tanstack/react-table v8 (mit React-19-Lint-disable für Hook-Compat)\n- recharts 3 + echarts/echarts-for-react (Chart-Library-Split)\n- Husky + lint-staged + @commitlint/cli + @commitlint/config-conventional\n- cmdk via shadcn Command (für Command-Palette)\n\n**Halo-Layer:**\n- Cookie-persistente collapsible Sidebar (FZDB-Style, eigenständige Implementation)\n- Command Palette (Cmd+K) für Quick-Nav durch alle 20 Sektionen + APIs\n- Route-Group `app/(showcase)/` für AppShell-wrapped Pages\n- `/login` standalone (kein AppShell, kein Scroll)\n- Mock-Persona-Switcher in Topbar mit RBAC-Demo\n\n### Added — Doku\n- `example/README.md` umgeschrieben — Quick-Start + Stack + Architektur-Highlights\n- `CONTRIBUTING.md` (Atlas-Root) — Workflow für neue Patterns, SemVer, Spec-Schärfegrad\n\n### Changed\n- `_config.yml` Jekyll-Exclude erweitert: `example`, `node_modules`\n- Identity-Doc: Live-Example-Box auf `/tokens` und `/typography`\n- Auth-Doc: Live-Example-Box auf `/login`\n- Navigation-Doc: Live-Example-Box auf `/navigation`\n- UI-Stack-Doc: Live-Example-Boxen auf `/components`, `/toasts`\n- Observability-Doc: Live-Example-Box auf `/health-status`\n- patterns/login-page: Live-Example-Box mit Stage-Toggle-Hinweis"},{"version":"0.1.1","date":"2026-05-28","body":"### Added — Atlas Showcase MVP (Phase A)\n\nEigenständiges Beispiel-Projekt unter `example/`. Erste 9 Sektionen + Login:\n- `/` Pattern-Index, `/tokens`, `/typography`, `/navigation`, `/components`, `/toasts`, `/empty-states`, `/health-status`, `/login`\n\nMock-Backend (in-memory TypeScript), Mock-Persona-Switcher, Atlas-Asset-Bridge via `scripts/sync-atlas-assets.mjs` (predev/prebuild Copy aus `../assets/`).\n\nVercel-Deploy auf `zvv-atlas.vercel.app`. Custom-Domain `atlas.zvv.dev` registriert, wartet auf Cloudflare-DNS."},{"version":"0.1.0","date":"2026-05-28","body":"### Added — Spec-Kapitel (verbindlich)\n- `docs/01-identity.md` — Logo, Farben (WCAG-AA-korrigiert), Schrift (ZVV Brown Narrow + Geist Mono), Tone-of-Voice. Inkl. Sektion 0: Verhältnis zu storybook.zvv.ch\n- `docs/02-auth.md` — Supabase Auth, Magic-Link via `generateActionLink`-Pattern (custom, NICHT `signInWithOtp`), Microsoft SSO mit zwei Maturity-Stufen, Pre-Check gegen Issue #31\n- `docs/06-mailing.md` — zvv-mailer-Integration, Slug-Setup, Wire-Format (`rawHtml` als Body-only), bekannte Falle `RAW_HTML_WRAPPED_BY_NON_RAW_TEMPLATE`\n- `docs/10-security.md` — RLS-first, Zwei-Client-Modell (Auth/Service-Role), `user_profiles` zentrale Tenant-Tabelle, `getTenantContext()` mit `react.cache`, Cookie/Header-Spec, E2E-Mock-Pattern\n- `patterns/login-page.md` — 3-Column-Layout, Microsoft-Button (Stufe 1 disabled / Stufe 2 primary), Magic-Link-Form, Feature-Cards 4+4 + Mobile-2×4-Grid\n\n### Added — Guidelines-Kapitel (empfohlen)\n- `docs/03-navigation.md` — Topbar/Sidebar/Footer/Breadcrumb/Env-Ribbon-Pattern\n- `docs/04-ui-stack.md` — Tailwind v4, shadcn/ui, Radix, lucide, sonner, react-hook-form, Recharts (default) vs ECharts (datenintensiv)\n- `docs/05-data-layer.md` — Supabase ssr, Migrations-Convention, Typed Schema, Edge Functions optional\n- `docs/07-tech-stack.md` — Next.js 15.5+/16 App Router, React 19.2 LTS, TS strict, pnpm, Vitest+Playwright\n- `docs/08-engineering.md` — Branching (topic → integration → main), Conventional Commits, SemVer, Atlas-Conformance-Declaration (`zvvAtlas` Custom-Field in `package.json`)\n- `docs/09-observability.md` — `/api/health` Pflicht, strukturierte JSON-Logs, Vercel Runtime Logs, zvv-cockpit + zvv-statuspage als zentrale Dashboards\n- `docs/11-hosting.md` — Vercel, Domain `<app>.zvv.ch` (PROD) / `<app>-int.zvv.dev` (INT), Env-Variables, Cron-Jobs, Caching-Strategie\n\n### Added — Assets\n- `assets/zvv-logo.svg` — Original monochrome (fill `#1d1d1b`)\n- `assets/zvv-logo-white.svg` — White-Variante für Dark-Mode (Fill-Swap)\n- `assets/fonts/ZVVBrownNarrowWeb-Regular.woff2` + `ZVVBrownNarrowWeb-Bold.woff2` — Corporate-Font, kopiert aus FZDB v0.64.6 (Lineto-lizenziert)\n- `assets/color-tokens.css` — kanonische Token-Definition (HSL-Variablen für Light/Dark, WCAG-AA-korrigierte Primary auf 35% lightness, AAA-Muted-Foreground auf 38%)\n\n### Added — Repo-Infrastruktur\n- `LICENSE` — Internal-Use für ZVV / Mandatsträger; Brand-Assets explizit ausgeschlossen von Public-Distribution\n- `.github/workflows/pages.yml` — GitHub-Pages-Deploy via Jekyll (auto auf push to main)\n- `_config.yml` — Minimal Jekyll-Config (jekyll-theme-minimal, deutsch)\n\n### Decisions\n- **Stack-Defaults:** Next.js 15.5+ App Router · React 19.2 (jeweils neueste LTS) · Tailwind 4.x (jeweils neueste LTS) · TypeScript strict\n- **Corporate-Font:** ZVV Brown Narrow (Lineto). Geist NUR für Mono.\n- **Chart-Library-Split:** Recharts default, ECharts für datenintensive Apps (FZDB/Kontoradar-Klasse)\n- **fzdb-spec ignoriert:** Vorgänger-Spec-Repo wird für Atlas v0.1 nicht ausgewertet (bewusste Entscheidung)\n- **Verhältnis zu storybook.zvv.ch:** Brand-Identität wird kanonisch von Storybook geerbt. Komponenten-Stack ist Atlas-eigen (shadcn/ui + Radix + Tailwind v4, nicht die Storybook-React-Komponenten direkt)."},{"version":"0.0.1","date":"2026-05-28","body":"### Added\n- README.md mit Vision, Mandate-Mix, Repo-Struktur\n- CLAUDE.md als Aufbau-Briefing für nächste Claude-Session\n- ZVV-Logo-Assets (`assets/zvv-logo.svg`, `assets/zvv-logo-white.svg`)\n- `.gitignore` minimal"}]},"latestReleaseDate":"2026-08-11"},"conformance":{"declaresAtlasVersion":"0.3.0","stack":{"framework":"Next.js 16.2","react":"19.2","typescript":"6 strict","tailwind":"4.x","ui":"shadcn/ui + Radix","icons":"lucide-react","toasts":"sonner","analytics":"@vercel/analytics + @vercel/speed-insights"},"patterns":["identity","auth","mailing","security","navigation","data-layer","tech-stack","engineering","observability","hosting"]}}