@conciso/design-system-mcp 2.7.0 → 2.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (19) hide show
  1. package/package.json +2 -2
  2. package/snapshot/manifests/components.json +15 -15
  3. package/snapshot/services/addon-docs/mdx/grundlagen-barrierefreiheit--/303/274bersicht.json +1 -1
  4. package/snapshot/services/addon-docs/mdx/komponenten-call-to-action-cta-band.json +1 -1
  5. package/snapshot/services/core/docgen/komponenten-call-to-action-cta-band.json +1 -1
  6. package/snapshot/services/core/docgen/komponenten-cards-teaser-featured-karte.json +1 -1
  7. package/snapshot/services/core/docgen/komponenten-cards-teaser-icon-karte.json +1 -1
  8. package/snapshot/services/core/docgen/komponenten-hero-hero-bild.json +1 -1
  9. package/snapshot/services/core/docgen/komponenten-hero-st/303/266rer.json +1 -1
  10. package/snapshot/services/core/docgen/komponenten-tabelle-tabelle.json +1 -1
  11. package/snapshot/services/core/docgen/komponenten-tabelle-vergleichstabelle.json +1 -1
  12. package/snapshot/services/core/docgen/seitenmuster-seminar-/302/267-training-fakten-liste.json +1 -1
  13. package/snapshot/services/core/docgen/seitenmuster-wissensbeitrag-article-header.json +1 -1
  14. package/snapshot/services/core/docgen/seitenmuster-wissensbeitrag-author-card.json +1 -1
  15. package/snapshot/services/core/docgen/seitenmuster-wissensbeitrag-avatar.json +1 -1
  16. package/snapshot/services/core/docgen/seitenmuster-wissensbeitrag-callout.json +1 -1
  17. package/snapshot/services/core/docgen/seitenmuster-wissensbeitrag-figure.json +1 -1
  18. package/snapshot/services/core/docgen/seitenmuster-wissensbeitrag-inhaltsverzeichnis.json +1 -1
  19. package/snapshot/services/core/docgen/seitenmuster-wissensbeitrag-pull-quote.json +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@conciso/design-system-mcp",
3
- "version": "2.7.0",
3
+ "version": "2.7.1",
4
4
  "description": "MCP-Server für Consumer des Conciso Design System, beantwortet docs-list, docs-show und docs-show-story über stdio aus einem mitgelieferten Storybook-Snapshot, ohne Netzzugriff.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -29,7 +29,7 @@
29
29
  "prepack": "node scripts/prepack.mjs",
30
30
  "test": "node --test test/*.test.mjs",
31
31
  "test:smoke": "node scripts/smoke-test.mjs",
32
- "test:eval-checker": "node --test eval/checker.test.mjs",
32
+ "test:eval-checker": "node --test eval/checker.test.mjs eval/run-eval.test.mjs",
33
33
  "eval": "node eval/run-eval.mjs",
34
34
  "lint": "eslint ."
35
35
  },
@@ -317,7 +317,7 @@
317
317
  "komponenten-cards-teaser-featured-karte": {
318
318
  "id": "komponenten-cards-teaser-featured-karte",
319
319
  "name": "FeaturedCardComponent",
320
- "description": "FeaturedCard (cds-featured-card) — Wrapper um `.card-featured` aus\ncss/components.css (css/components.css:230–249): die horizontale Großkarte für\n**genau einen** hervorgehobenen Beitrag oder Termin (Bild links 60 %, Textspalte\nrechts 40 % als absolut positioniertes Overlay), siehe `docs/index.html`\nAbschnitt „Featured · horizontale Großkarte“ (`#gt-events-featured`).\n\n**Entscheidung 1 — `href` entscheidet zwischen `<a>` und `<article>`.** Die\nDoku-Beispiele zeigen die Featured-Card ausschließlich als Link\n(`a.card.card-elevated.card-featured`), das Ticket verlangt aber ausdrücklich eine\nVariante ohne `<a>`. `.card-elevated` ist nach `CONTRIBUTING.md` §4 („Elevation =\nInteraktivität“) im CSS auf `a.card-elevated` gescoped — eine `<article>` bekäme\nden Schatten mit gesetzter Klasse ohnehin nicht. Die Komponente führt die Klasse\ndeshalb nur mit, wenn sie auch wirkt: mit `href` ein `<a class=\"card card-elevated\ncard-featured\">`, ohne `href` ein `<article class=\"card card-featured\">` (ruht\nflach mit dem `--bd-strong`-Rahmen aus `.card`, wie jede statische Fläche). Media\nund Body stehen dafür einmal in einem gemeinsamen `<ng-template>` und werden in\nbeiden Zweigen per `<ng-container [ngTemplateOutlet]>` eingesetzt — dasselbe\nMuster wie `SectionComponent` (`section.component.ts`) für „zwei Wurzelelemente,\nein Körper“. `<ng-container>` rendert selbst kein Element, die Kindselektoren aus\nEntscheidung 2 bleiben also unberührt: im laufenden Storybook erneut per\n`querySelector`/`getBoundingClientRect` gemessen (siehe Play-Funktionen), Ergebnis\nunverändert zur vorherigen, noch duplizierten Fassung.\n\n**Entscheidung 2 — die Pill ist direkt komponiertes Markup, kein `<cds-pill>`.**\nDas CSS arbeitet mit direkten Kindselektoren: `.card-featured>.card-media`,\n`.card-featured-body>.pill`, `.card-featured-body>.card-text`,\n`.card-featured-body>.card-title-hero` (jeweils mit `margin`/`min-height`/\n`line-clamp`-Übernahmen, siehe CSS-Kommentare dort). `<cds-pill>` rendert sein\n`.pill`-`<span>` nicht auf dem eigenen Host-Element, sondern eine Ebene tiefer\n(`pill.component.ts`: Host ist `<cds-pill>`, `.pill` sitzt im Template darunter) —\nprojiziert als `<cds-pill class=\"pill\">` stünde die Pille damit als\n`.card-featured-body>cds-pill>.pill`, zwei Ebenen statt einer, und die\nKindselektoren griffen nicht mehr (leise: kein Fehler, nur falsches Layout).\nDieselbe Abwägung wie in `download-cta.component.ts` (Kommentar über den\nButtons): dort verhindert `.cta-dl-actions`, das seine Kinder per Flex streckt,\ndass ein `<cds-button>`-Host mitstreckt; hier verhindert ein Kindselektor, dass\nirgendein zusätzliches Host-Element dazwischentritt. In beiden Fällen werden die\nKlassen direkt komponiert statt eine Wrapper-Komponente zu verwenden. Geprüft im\nlaufenden Storybook (`Komponenten/Cards & Teaser/Featured-Karte`): die gerenderte\nKette ist `.card-featured>.card-media` und `.card-featured-body` mit `.pill`,\n`.card-text`, `.card-title-hero` als direkten Kindern, kein zusätzliches Element\ndazwischen (siehe Play-Funktionen der Stories).\n\nWeil die Pille direkt komponiert wird, übernimmt diese Komponente auch die\n`aria-label`-Regel von `PillComponent` (`pill.component.ts`) von Hand nach:\nDefault „Bereich `<pill>`“, überschreibbar über `pillAriaLabel` — nötig, weil die\nPille nicht immer einen Bereich benennt (siehe `pillAriaLabel`-Doku unten).\n\n**Entscheidung 3 — kein separates `data-area` für die Pille.** Die Doku setzt\nKarte und Pille immer auf denselben Bereich (`docs/index.html:8979`: Karte\n`data-area=\"es\"`, Pille `data-area=\"es\"`). Die Komponente bindet deshalb `area`\nauf beide, statt einen zweiten Bereichs-Input zu erfinden.\n\n**Entscheidung 4 — kein CTA- und kein Meta-Input.** Die Doku-Vorlage zeigt\nzusätzlich einen Meta-Strip (Datum/Ort/Format) und einen `.card-cta-link`-Fuß;\ndas Ticket benennt als API aber nur `title`/`text`/`imageSrc`/`imageAlt`/`href`/\n`pill`/`area`. Diese Komponente bildet exakt diese Fläche ab, ohne Inputs zu\nerfinden, die das Ticket nicht vorsieht — ein Meta-Strip oder ein CTA-Fuß wären\neine spätere, eigene Erweiterung.\n\nVerwendungsguidance dieser Gruppe: siehe Card (`komponenten-cards-teaser-card--verwendung`).",
320
+ "description": "FeaturedCard (cds-featured-card) — Wrapper um `.card-featured` aus\ncss/components.css (css/components.css:230–249): die horizontale Großkarte für\n**genau einen** hervorgehobenen Beitrag oder Termin (Bild links 60 %, Textspalte\nrechts 40 % als absolut positioniertes Overlay), siehe `docs/index.html`\nAbschnitt „Featured · horizontale Großkarte“ (`#gt-events-featured`).\n\n**Entscheidung 1 — `href` entscheidet zwischen `<a>` und `<article>`.** Die\nDoku-Beispiele zeigen die Featured-Card ausschließlich als Link\n(`a.card.card-elevated.card-featured`), diese Komponente unterstützt aber ausdrücklich eine\nVariante ohne `<a>`. `.card-elevated` ist nach `CONTRIBUTING.md` §4 („Elevation =\nInteraktivität“) im CSS auf `a.card-elevated` gescoped — eine `<article>` bekäme\nden Schatten mit gesetzter Klasse ohnehin nicht. Die Komponente führt die Klasse\ndeshalb nur mit, wenn sie auch wirkt: mit `href` ein `<a class=\"card card-elevated\ncard-featured\">`, ohne `href` ein `<article class=\"card card-featured\">` (ruht\nflach mit dem `--bd-strong`-Rahmen aus `.card`, wie jede statische Fläche). Media\nund Body stehen dafür einmal in einem gemeinsamen `<ng-template>` und werden in\nbeiden Zweigen per `<ng-container [ngTemplateOutlet]>` eingesetzt — dasselbe\nMuster wie `SectionComponent` (`section.component.ts`) für „zwei Wurzelelemente,\nein Körper“. `<ng-container>` rendert selbst kein Element, die Kindselektoren aus\nEntscheidung 2 bleiben also unberührt: im laufenden Storybook erneut per\n`querySelector`/`getBoundingClientRect` gemessen (siehe Play-Funktionen), Ergebnis\nunverändert zur vorherigen, noch duplizierten Fassung.\n\n**Entscheidung 2 — die Pill ist direkt komponiertes Markup, kein `<cds-pill>`.**\nDas CSS arbeitet mit direkten Kindselektoren: `.card-featured>.card-media`,\n`.card-featured-body>.pill`, `.card-featured-body>.card-text`,\n`.card-featured-body>.card-title-hero` (jeweils mit `margin`/`min-height`/\n`line-clamp`-Übernahmen, siehe CSS-Kommentare dort). `<cds-pill>` rendert sein\n`.pill`-`<span>` nicht auf dem eigenen Host-Element, sondern eine Ebene tiefer\n(`pill.component.ts`: Host ist `<cds-pill>`, `.pill` sitzt im Template darunter) —\nprojiziert als `<cds-pill class=\"pill\">` stünde die Pille damit als\n`.card-featured-body>cds-pill>.pill`, zwei Ebenen statt einer, und die\nKindselektoren griffen nicht mehr (leise: kein Fehler, nur falsches Layout).\nDieselbe Abwägung wie in `download-cta.component.ts` (Kommentar über den\nButtons): dort verhindert `.cta-dl-actions`, das seine Kinder per Flex streckt,\ndass ein `<cds-button>`-Host mitstreckt; hier verhindert ein Kindselektor, dass\nirgendein zusätzliches Host-Element dazwischentritt. In beiden Fällen werden die\nKlassen direkt komponiert statt eine Wrapper-Komponente zu verwenden. Geprüft im\nlaufenden Storybook (`Komponenten/Cards & Teaser/Featured-Karte`): die gerenderte\nKette ist `.card-featured>.card-media` und `.card-featured-body` mit `.pill`,\n`.card-text`, `.card-title-hero` als direkten Kindern, kein zusätzliches Element\ndazwischen (siehe Play-Funktionen der Stories).\n\nWeil die Pille direkt komponiert wird, übernimmt diese Komponente auch die\n`aria-label`-Regel von `PillComponent` (`pill.component.ts`) von Hand nach:\nDefault „Bereich `<pill>`“, überschreibbar über `pillAriaLabel` — nötig, weil die\nPille nicht immer einen Bereich benennt (siehe `pillAriaLabel`-Doku unten).\n\n**Entscheidung 3 — kein separates `data-area` für die Pille.** Die Doku setzt\nKarte und Pille immer auf denselben Bereich (`docs/index.html:8979`: Karte\n`data-area=\"es\"`, Pille `data-area=\"es\"`). Die Komponente bindet deshalb `area`\nauf beide, statt einen zweiten Bereichs-Input zu erfinden.\n\n**Entscheidung 4 — kein CTA- und kein Meta-Input.** Die Doku-Vorlage zeigt\nzusätzlich einen Meta-Strip (Datum/Ort/Format) und einen `.card-cta-link`-Fuß;\ndiese Komponente benennt als API aber nur `title`/`text`/`imageSrc`/`imageAlt`/`href`/\n`pill`/`area`. Diese Komponente bildet exakt diese Fläche ab, ohne zusätzliche Inputs zu\nerfinden — ein Meta-Strip oder ein CTA-Fuß wären\neine spätere, eigene Erweiterung.\n\nVerwendungsguidance dieser Gruppe: siehe Card (`komponenten-cards-teaser-card--verwendung`).",
321
321
  "docgen": {
322
322
  "$ref": "../services/core/docgen/komponenten-cards-teaser-featured-karte.json#/components/komponenten-cards-teaser-featured-karte"
323
323
  },
@@ -328,7 +328,7 @@
328
328
  "komponenten-cards-teaser-icon-karte": {
329
329
  "id": "komponenten-cards-teaser-icon-karte",
330
330
  "name": "IconCardComponent",
331
- "description": "IconCard (`[cdsIconCard]`) — Wrapper um `.ep-card` aus css/components.css\n(css/components.css:1298–1340): die Icon-Kachel mit farbiger Icon-Fläche, Eyebrow,\nTitel, Text und optionaler Pfeil-CTA-Zeile, die auf den Beispielseiten Bereichs-\nund Angebots-Einstiege trägt (`docs/index.html:9401–9420` u. a., 81 Vorkommen).\n\n**Entscheidung 1 — Attributselektor, kein Element-Selektor.** Erste Fassung dieser\nKomponente war `cds-icon-card` als eigenes Element mit `href`-Input, das intern\nzwischen `<a class=\"ep-card ep-card-link\">` und `<div class=\"ep-card\">` wählte\n(`@if`/`@else` + `<ng-template>`). Gemessen im laufenden Storybook: zwei Karten mit\nunterschiedlich langem Text ergaben im `.ep-cards`-Grid ungleiche `.ep-card`-Höhen\n(174px/270px), weil `align-items:stretch` nur den unsichtbaren `<cds-icon-card>`-Host\nstreckt, nicht das `.ep-card`-Element eine Ebene darunter. Dieselbe Messung mit\n`.ep-card` OHNE Host-Wrapper (rohes Markup direkt in `.ep-cards`) ergab 306px/306px,\nCTA-Unterkanten auf identischer Y-Koordinate — das CSS gleicht die Höhen korrekt aus,\nsobald `.ep-card` selbst das Grid-Kind ist. Der Fehler saß also im Wrapper-Element,\nnicht in der CSS-Schicht (siehe „Im Raster“-Story, dort dieselbe Messung als\nPlay-Funktion gepinnt).\n\nDie Lösung, analog zu Angular Materials `a[mat-button], button[mat-button]`: die\nKomponente hängt sich als Attribut an ein vom Konsumenten geschriebenes `<a>` oder\n`<div>`, statt ein eigenes Element zu sein. Dadurch trägt **derselbe** native Knoten,\nden der Konsument in sein Grid schreibt, sowohl die Komponentenlogik als auch die\n`.ep-card`-Klasse — kein Host-Element dazwischen, `align-items:stretch` trifft direkt\nauf `.ep-card`.\n\n**`href` ist deshalb kein Input mehr.** Ob die Karte ein Link ist, entscheidet der\nKonsument über das gewählte Tag (`<a cdsIconCard href=\"…\">` vs. `<div cdsIconCard>`)\nund setzt `href` als natives Attribut direkt am `<a>` — Angular muss dafür nichts\nverwalten. `.ep-card-link` (nur auf `a.ep-card-link` wirksam, `CONTRIBUTING.md` §4)\nwird deshalb vom Host-Tag abgeleitet, nicht mehr per Verzweigung im Template\ngerendert.\n\n**`.ep-card-link` hängt an `<a>` UND `href`, nicht am Tag allein.** Ein `<a\ncdsIconCard>` ohne `href` ist weder fokussierbar noch hat es eine Link-Rolle —\n`.ep-card-link` (Schatten, Hover-Anhebung, Cursor) nur am Tag festzumachen hätte\neine Karte erzeugt, die aussieht wie ein Link, aber keiner ist, und genau die\nGarantie aufgegeben, die die frühere `href`-Input-Fassung strukturell hatte (leerer\nString → zwingend der `<div>`-Zweig). `isLink()` prüft deshalb beides.\n\n**`isLink()` ist eine Methode im Host-Binding, kein einmalig berechnetes Feld.**\n`href` kann nach dem Erstellen zur Laufzeit gesetzt/entfernt werden (z. B.\n`<a cdsIconCard [attr.href]=\"urlSignal()\">`). Ein Host-Binding-Ausdruck\n(`'[class.ep-card-link]': 'isLink()'`) wird bei JEDEM Refresh der DECLARING VIEW\nneu ausgewertet — also der Elternvorlage, die `<a cdsIconCard>` schreibt —, nicht\nnur bei einem Refresh der eigenen (OnPush-gesteuerten) Kindvorlage dieser\nKomponente. Das ist kein Sonderfall wie bei einem in ADR-0007 §3 verworfenen\nGetter (dort ging es um einen Getter in einer INTERPOLATION der eigenen\nKindvorlage, die unter OnPush nur bei eigenen Signal-Änderungen neu läuft):\nHost-Bindings werden unabhängig vom OnPush-Status der Komponente selbst\nmitgeführt, wenn die Elternansicht aktualisiert wird — reagiert also auf\n`[attr.href]`/`[href]`-Bindungen des Konsumenten, ob Signal- oder\nZone.js-getrieben, ohne `effect()` (ADR-0007 §4) und ohne `MutationObserver`.\nEmpirisch geprüft (Spike-Story mit `[attr.href]=\"signal()\"`-Toggle zur Laufzeit,\nseither wieder gelöscht): Setzen des Signals nach Erstrender schaltet\n`.ep-card-link` zuverlässig um. Einzige Grenze: eine Änderung, die AUSSERHALB von\nAngular direkt am nativen Element vorgenommen wird (z. B. `el.setAttribute\n('href', …)` von Fremdcode), löst keinen Refresh aus — das gilt aber für jede\nAngular-Bindung gleichermaßen und ist kein Sonderfall dieser Komponente.\n\n**Entscheidung 2 — Icon als projizierter Inhalt, OHNE dass das SVG eine eigene\nGrößenklasse tragen muss.** Wie bei `cds-stoerer` (siehe dessen Klassendoku) sind\ndie Icons auf den Beispielseiten wechselnde Heroicons, keine DS-Bereichsglyphen aus\nder Registry `icons/cds-icons.ts` — Inhalt, nicht Chrom. Deshalb Projektion über\n`<ng-content select=\"[cdsIcon]\">` statt eines `icon`-Strings. Anders als beim Störer\nist `.ep-card-icon` hier aber ein echter CONTAINER: css/components.css:1310+1318\nsetzt Maße und Stroke über den Nachfahren-Selektor `.ep-card-icon svg{width:32px;\nheight:32px;stroke-width:var(--icon-stroke-sm)}`, nicht über eine Klasse auf dem\nSVG selbst. Der Konsument liefert deshalb ein unverändertes `<svg cdsIcon\nviewBox=\"…\" aria-hidden=\"true\" focusable=\"false\">…</svg>` OHNE zusätzliche Klasse —\ndas Verhältnis Größe/Stroke kommt allein aus dem Container. Geprüft an\ncss/components.css:1298–1318 (Kommentar dort: „Farbe kommt aus der Komponente,\nnicht aus … inline gesetzten style-Attributen“) und in der Story „Interaktiv“\ngepinnt (Play-Funktion misst die gerenderte SVG-Breite).\n\n**`data-area` sitzt auf drei Elementen** (`.ep-card` (Host), `.ep-card-icon`,\n`.ep-card-eyebrow`, wie im Mockup, `docs/index.html:4681–4686`): die CTA-Farbe\nbraucht dagegen KEIN eigenes `data-area`, sie kommt über den Nachfahren-Selektor\n`.ep-card[data-area=\"…\"] .ep-card-cta` (css/components.css:1336–1339) vom\n`data-area` der Karte selbst.\n\n**Kein erfundener `aria-label`.** Der Pfeil-Suffix der CTA-Zeile ist reine visuelle\nAffordanz (`docs/index.html:4686`: `<span aria-hidden=\"true\">→</span>`) und steht\ndeshalb `aria-hidden`, unverändert zum Mockup — kein zusätzlicher, selbst\nausgedachter zugänglicher Name.\n\n**Entscheidung 3 — kein `cdsIconCards`-Raster.** `.ep-cards` ist ein reines\n`display:grid` ohne Struktur oder Verhalten (dieselbe Begründung wie beim Verzicht\nauf einen `layout-grid`-Wrapper, siehe `spec.md`). Konsumenten schreiben\n`<div class=\"ep-cards\">` von Hand (siehe Story „Im Raster“).\n\nVerwendungsguidance dieser Gruppe: siehe Card (`komponenten-cards-teaser-card--verwendung`).",
331
+ "description": "IconCard (`[cdsIconCard]`) — Wrapper um `.ep-card` aus css/components.css\n(css/components.css:1298–1340): die Icon-Kachel mit farbiger Icon-Fläche, Eyebrow,\nTitel, Text und optionaler Pfeil-CTA-Zeile, die auf den Beispielseiten Bereichs-\nund Angebots-Einstiege trägt (`docs/index.html:9401–9420` u. a., 81 Vorkommen).\n\n**Entscheidung 1 — Attributselektor, kein Element-Selektor.** Erste Fassung dieser\nKomponente war `cds-icon-card` als eigenes Element mit `href`-Input, das intern\nzwischen `<a class=\"ep-card ep-card-link\">` und `<div class=\"ep-card\">` wählte\n(`@if`/`@else` + `<ng-template>`). Gemessen im laufenden Storybook: zwei Karten mit\nunterschiedlich langem Text ergaben im `.ep-cards`-Grid ungleiche `.ep-card`-Höhen\n(174px/270px), weil `align-items:stretch` nur den unsichtbaren `<cds-icon-card>`-Host\nstreckt, nicht das `.ep-card`-Element eine Ebene darunter. Dieselbe Messung mit\n`.ep-card` OHNE Host-Wrapper (rohes Markup direkt in `.ep-cards`) ergab 306px/306px,\nCTA-Unterkanten auf identischer Y-Koordinate — das CSS gleicht die Höhen korrekt aus,\nsobald `.ep-card` selbst das Grid-Kind ist. Der Fehler saß also im Wrapper-Element,\nnicht in der CSS-Schicht (siehe „Im Raster“-Story, dort dieselbe Messung als\nPlay-Funktion gepinnt).\n\nDie Lösung, analog zu Angular Materials `a[mat-button], button[mat-button]`: die\nKomponente hängt sich als Attribut an ein vom Konsumenten geschriebenes `<a>` oder\n`<div>`, statt ein eigenes Element zu sein. Dadurch trägt **derselbe** native Knoten,\nden der Konsument in sein Grid schreibt, sowohl die Komponentenlogik als auch die\n`.ep-card`-Klasse — kein Host-Element dazwischen, `align-items:stretch` trifft direkt\nauf `.ep-card`.\n\n**`href` ist deshalb kein Input mehr.** Ob die Karte ein Link ist, entscheidet der\nKonsument über das gewählte Tag (`<a cdsIconCard href=\"…\">` vs. `<div cdsIconCard>`)\nund setzt `href` als natives Attribut direkt am `<a>` — Angular muss dafür nichts\nverwalten. `.ep-card-link` (nur auf `a.ep-card-link` wirksam, `CONTRIBUTING.md` §4)\nwird deshalb vom Host-Tag abgeleitet, nicht mehr per Verzweigung im Template\ngerendert.\n\n**`.ep-card-link` hängt an `<a>` UND `href`, nicht am Tag allein.** Ein `<a\ncdsIconCard>` ohne `href` ist weder fokussierbar noch hat es eine Link-Rolle —\n`.ep-card-link` (Schatten, Hover-Anhebung, Cursor) nur am Tag festzumachen hätte\neine Karte erzeugt, die aussieht wie ein Link, aber keiner ist, und genau die\nGarantie aufgegeben, die die frühere `href`-Input-Fassung strukturell hatte (leerer\nString → zwingend der `<div>`-Zweig). `isLink()` prüft deshalb beides.\n\n**`isLink()` ist eine Methode im Host-Binding, kein einmalig berechnetes Feld.**\n`href` kann nach dem Erstellen zur Laufzeit gesetzt/entfernt werden (z. B.\n`<a cdsIconCard [attr.href]=\"urlSignal()\">`). Ein Host-Binding-Ausdruck\n(`'[class.ep-card-link]': 'isLink()'`) wird bei JEDEM Refresh der DECLARING VIEW\nneu ausgewertet — also der Elternvorlage, die `<a cdsIconCard>` schreibt —, nicht\nnur bei einem Refresh der eigenen (OnPush-gesteuerten) Kindvorlage dieser\nKomponente. Das ist kein Sonderfall wie bei einem in ADR-0007 §3 verworfenen\nGetter (dort ging es um einen Getter in einer INTERPOLATION der eigenen\nKindvorlage, die unter OnPush nur bei eigenen Signal-Änderungen neu läuft):\nHost-Bindings werden unabhängig vom OnPush-Status der Komponente selbst\nmitgeführt, wenn die Elternansicht aktualisiert wird — reagiert also auf\n`[attr.href]`/`[href]`-Bindungen des Konsumenten, ob Signal- oder\nZone.js-getrieben, ohne `effect()` (ADR-0007 §4) und ohne `MutationObserver`.\nEmpirisch geprüft (Spike-Story mit `[attr.href]=\"signal()\"`-Toggle zur Laufzeit,\nseither wieder gelöscht): Setzen des Signals nach Erstrender schaltet\n`.ep-card-link` zuverlässig um. Einzige Grenze: eine Änderung, die AUSSERHALB von\nAngular direkt am nativen Element vorgenommen wird (z. B. `el.setAttribute\n('href', …)` von Fremdcode), löst keinen Refresh aus — das gilt aber für jede\nAngular-Bindung gleichermaßen und ist kein Sonderfall dieser Komponente.\n\n**Entscheidung 2 — Icon als projizierter Inhalt, OHNE dass das SVG eine eigene\nGrößenklasse tragen muss.** Wie bei `cds-stoerer` (siehe dessen Klassendoku) sind\ndie Icons auf den Beispielseiten wechselnde Heroicons, keine DS-Bereichsglyphen aus\nder Registry `icons/cds-icons.ts` — Inhalt, nicht Chrom. Deshalb Projektion über\n`<ng-content select=\"[cdsIcon]\">` statt eines `icon`-Strings. Anders als beim Störer\nist `.ep-card-icon` hier aber ein echter CONTAINER: css/components.css:1310+1318\nsetzt Maße und Stroke über den Nachfahren-Selektor `.ep-card-icon svg{width:32px;\nheight:32px;stroke-width:var(--icon-stroke-sm)}`, nicht über eine Klasse auf dem\nSVG selbst. Der Konsument liefert deshalb ein unverändertes `<svg cdsIcon\nviewBox=\"…\" aria-hidden=\"true\" focusable=\"false\">…</svg>` OHNE zusätzliche Klasse —\ndas Verhältnis Größe/Stroke kommt allein aus dem Container. Geprüft an\ncss/components.css:1298–1318 (Kommentar dort: „Farbe kommt aus der Komponente,\nnicht aus … inline gesetzten style-Attributen“) und in der Story „Interaktiv“\ngepinnt (Play-Funktion misst die gerenderte SVG-Breite).\n\n**`data-area` sitzt auf drei Elementen** (`.ep-card` (Host), `.ep-card-icon`,\n`.ep-card-eyebrow`, wie im Mockup, `docs/index.html:4681–4686`): die CTA-Farbe\nbraucht dagegen KEIN eigenes `data-area`, sie kommt über den Nachfahren-Selektor\n`.ep-card[data-area=\"…\"] .ep-card-cta` (css/components.css:1336–1339) vom\n`data-area` der Karte selbst.\n\n**Kein erfundener `aria-label`.** Der Pfeil-Suffix der CTA-Zeile ist reine visuelle\nAffordanz (`docs/index.html:4686`: `<span aria-hidden=\"true\">→</span>`) und steht\ndeshalb `aria-hidden`, unverändert zum Mockup — kein zusätzlicher, selbst\nausgedachter zugänglicher Name.\n\n**Entscheidung 3 — kein `cdsIconCards`-Raster.** `.ep-cards` ist ein reines\n`display:grid` ohne Struktur oder Verhalten (dieselbe Begründung wie beim Verzicht\nauf einen `layout-grid`-Wrapper). Konsumenten schreiben\n`<div class=\"ep-cards\">` von Hand (siehe Story „Im Raster“).\n\nVerwendungsguidance dieser Gruppe: siehe Card (`komponenten-cards-teaser-card--verwendung`).",
332
332
  "docgen": {
333
333
  "$ref": "../services/core/docgen/komponenten-cards-teaser-icon-karte.json#/components/komponenten-cards-teaser-icon-karte"
334
334
  },
@@ -383,7 +383,7 @@
383
383
  "komponenten-call-to-action-cta-band": {
384
384
  "id": "komponenten-call-to-action-cta-band",
385
385
  "name": "CtaBandComponent",
386
- "description": "CtaBand (`[cdsCtaBand]`) — Wrapper um `.ep-cta-band` aus css/components.css\n(css/components.css:1456, Kopf-Duo `.ep-cta-h2`/`.ep-cta-sub` ebenda): das\nbereichsgefärbte Page-End-CTA-Band, das laut Doku-Site (`docs/index.html:6237`,\nAnker `gt-cta-band`) am Ende jeder Customer-Page direkt vor dem Footer steht — die\n„letzte Einladung“, wenn die Nutzerin die Seite durchgelesen hat. Ausgezählt: 22\nVorkommen auf den Beispielseiten, ausnahmslos als `<div class=\"ep-cta-band\"\nstyle=\"background:…\">` mit genau einer Aktion.\n\n**Entscheidung 1 — Attributselektor, kein eigenes Element (ADR-0008, derselbe\n„Fläche am Host“-Fall wie `cds-section`).** `css/components.css` trägt für\n`.ep-cta-band` KEIN `[data-area]` (per Grep geprüft, keine einzige Regel dieser\nForm existiert). Die Bandfläche kommt in allen 22 Vorkommen als Inline-Style\nDIREKT am `.ep-cta-band`-Element (`style=\"background:var(--co-700);color:#fff\"`,\ndocs/index.html:6304 u. a.), dem Bereich der Page folgend, nie aus einem\n`data-area`-Attribut. Ein Element-Selektor würde exakt den in ADR-0008 „Fall 2“\ngemessenen Fehler wiederholen: der `<cds-cta-band>`-Host ist ein unbekanntes\nCustom Element (`display:inline`), sein einziges Kind würde als Block\nherausgebrochen, ein am Host gesetzter Inline-`background` hätte keine eigene Box\nzum Malen. Die Komponente hängt sich deshalb als Attribut an ein vom Konsumenten\ngeschriebenes `<div>` (`<div cdsCtaBand style=\"background:…\">`) — das Element, das\nden Style trägt, und `.ep-cta-band` sind derselbe Knoten. Nur `<div>`, kein\n`<section>`-Zwilling wie bei `cds-section`: alle 22 Vorkommen benutzen\nausnahmslos ein `<div>`, keines variiert das Tag.\n\n**Entscheidung 2 — `area` färbt nur die Aktion, nicht das Band.** Aus demselben\nGrund wie bei `cds-section` (siehe dessen Klassendoku, Entscheidung 2) bleibt die\nBandfläche Sache des Konsumenten: welche Bereichsfarbe ein Band trägt, ist eine\nEntscheidung der Seite (Bereichszugehörigkeit der Customer-Page), keine\nBauteil-Eigenschaft — und das CSS bietet an dieser Klasse ohnehin keinen\n`[data-area]`-Haken an (siehe Entscheidung 1). `area` steuert deshalb\nausschließlich die Button-Farbklasse (`.btn-{area}`), nicht die Füllung des Bands.\n\n**Entscheidung 3 — `.btn-on-band` + `.btn-filled` direkt komponiert, nicht über\n`cds-button` projiziert.** `cds-button` KENNT den Modifier\n(`variant=\"filled-on-band\"`, geprüft über `npx storybook tools docs show --id\nkomponenten-buttons-button` — die Story „Auf Bereichs-Band“ existiert dort\nbereits). Trotzdem baut diese Komponente die `.btn`-Klassen direkt zusammen\n(Präzedenzfall `download-cta.component.ts`), aus einem härteren Grund als dort:\n`cds-button` rendert IMMER ein `<button>` — sein Input-Vertrag\n(`ButtonComponentInputs`) kennt kein `href` — und kann die vom Ticket geforderte\nVerzweigung „`primaryHref` gesetzt → `<a>`“ strukturell gar nicht erfüllen. Ein\nprojiziertes `<cds-button>` brächte hier also kein Custom-Element-Layoutproblem\n(kein Kindselektor an `.ep-cta-band` hängt von der DOM-Tiefe ab), sondern schlicht\ndie falsche Fähigkeit.\n\n**Entscheidung 4 — bewusst nur EINE Aktion, kein `secondaryLabel`.** Eine frühere\nFassung bot `secondaryLabel`/`secondaryHref` an, ungeprüft gegen das Mockup. Beide\nBelege dagegen: erstens zeigt KEINES der 22 `.ep-cta-band`-Vorkommen in\n`docs/index.html` eine zweite Aktion — der Zwei-Aktionen-Fall hat keine Vorlage.\nZweitens (der schwerere Grund) lässt sich eine zweite, optisch zurückhaltendere\nAktion mit den vorhandenen Klassen gar nicht bauen: `.btn-{area}.btn-on-band{color:\n…}` (css/components.css:47–51) überschreibt die Textfarbe JEDER Variante auf den\nBereichston — genau den Ton, den die Doku durchgängig als Bandhintergrund nennt.\nNur `.btn-filled` tauscht zusätzlich die Füllung gegen Weiß; `.btn-outlined`,\n`.btn-tonal` und `.btn-text` blieben transparent und zeigten Text in derselben\nFarbe wie das Band darunter — unsichtbar. Eine zweite Aktion hätte deshalb\nzwangsläufig dieselbe Optik wie die erste getragen, keine Hierarchie, nur zwei\ngleich gewichtete Buttons. Das ist eine CSS-Lücke (kein kontrastsicherer\nsekundärer On-Band-Stil), festgehalten in\n`.scratch/angular-seitenbausteine/issues/16-css-luecke-zweite-aktion-auf-band.md`\n— gemeldet, nicht in diesem Wrapper improvisiert. Solange sie besteht, trägt\n`cds-cta-band` nur eine Aktion; ein Konsument mit echtem Bedarf für eine zweite\nschreibt sie außerhalb der Komponente ins Band hinein (`<ng-content>` gibt es\nhier bewusst nicht, um genau das nicht als unterstützten Pfad zu suggerieren).\nOhne zweite Aktion braucht es auch keinen eigenen Container: die einzelne Aktion\nzentriert sich über das ererbte `.ep-cta-band{text-align:center}` von selbst,\ngenau wie in allen 22 Mockup-Vorkommen — ein zusätzliches Flex-Layout im Wrapper\nhätte eine Darstellung erfunden, die die CSS-Schicht so nicht kennt (ADR-0001;\ndie einzige existierende Deklaration dieser Form ist `.ep-hero-ctas`,\ncss/components.css:1222, eine andere Klasse für einen anderen Bauteiltyp).\n\n**Kein `aria-label` auf der Aktion.** Anders als bei `cds-feature`\n(`ctaAriaLabel`, 11 von 12 Vorkommen mit `aria-label`) zeigt keines der 22\nBand-Vorkommen einen `aria-label` auf seiner Aktion — ausgezählt, nicht\ngeschätzt. Die sichtbaren Texte sind durchgängig konkrete Verben mit Ziel\n(„Termin buchen“, „Gespräch anfragen“, „Offene Stellen ansehen“ …) und stehen je\nSeite nur einmal; anders als bei einer Karten-Liste braucht es hier keine\nDisambiguierung. Kein entsprechender Input.",
386
+ "description": "CtaBand (`[cdsCtaBand]`) — Wrapper um `.ep-cta-band` aus css/components.css\n(css/components.css:1456, Kopf-Duo `.ep-cta-h2`/`.ep-cta-sub` ebenda): das\nbereichsgefärbte Page-End-CTA-Band, das laut Doku-Site (`docs/index.html:6237`,\nAnker `gt-cta-band`) am Ende jeder Customer-Page direkt vor dem Footer steht — die\n„letzte Einladung“, wenn die Nutzerin die Seite durchgelesen hat. Ausgezählt: 22\nVorkommen auf den Beispielseiten, ausnahmslos als `<div class=\"ep-cta-band\"\nstyle=\"background:…\">` mit genau einer Aktion.\n\n**Entscheidung 1 — Attributselektor, kein eigenes Element (ADR-0008, derselbe\n„Fläche am Host“-Fall wie `cds-section`).** `css/components.css` trägt für\n`.ep-cta-band` KEIN `[data-area]` (per Grep geprüft, keine einzige Regel dieser\nForm existiert). Die Bandfläche kommt in allen 22 Vorkommen als Inline-Style\nDIREKT am `.ep-cta-band`-Element (`style=\"background:var(--co-700);color:#fff\"`,\ndocs/index.html:6304 u. a.), dem Bereich der Page folgend, nie aus einem\n`data-area`-Attribut. Ein Element-Selektor würde exakt den in ADR-0008 „Fall 2“\ngemessenen Fehler wiederholen: der `<cds-cta-band>`-Host ist ein unbekanntes\nCustom Element (`display:inline`), sein einziges Kind würde als Block\nherausgebrochen, ein am Host gesetzter Inline-`background` hätte keine eigene Box\nzum Malen. Die Komponente hängt sich deshalb als Attribut an ein vom Konsumenten\ngeschriebenes `<div>` (`<div cdsCtaBand style=\"background:…\">`) — das Element, das\nden Style trägt, und `.ep-cta-band` sind derselbe Knoten. Nur `<div>`, kein\n`<section>`-Zwilling wie bei `cds-section`: alle 22 Vorkommen benutzen\nausnahmslos ein `<div>`, keines variiert das Tag.\n\n**Entscheidung 2 — `area` färbt nur die Aktion, nicht das Band.** Aus demselben\nGrund wie bei `cds-section` (siehe dessen Klassendoku, Entscheidung 2) bleibt die\nBandfläche Sache des Konsumenten: welche Bereichsfarbe ein Band trägt, ist eine\nEntscheidung der Seite (Bereichszugehörigkeit der Customer-Page), keine\nBauteil-Eigenschaft — und das CSS bietet an dieser Klasse ohnehin keinen\n`[data-area]`-Haken an (siehe Entscheidung 1). `area` steuert deshalb\nausschließlich die Button-Farbklasse (`.btn-{area}`), nicht die Füllung des Bands.\n\n**Entscheidung 3 — `.btn-on-band` + `.btn-filled` direkt komponiert, nicht über\n`cds-button` projiziert.** `cds-button` KENNT den Modifier\n(`variant=\"filled-on-band\"`, geprüft über `npx storybook tools docs show --id\nkomponenten-buttons-button` — die Story „Auf Bereichs-Band“ existiert dort\nbereits). Trotzdem baut diese Komponente die `.btn`-Klassen direkt zusammen\n(Präzedenzfall `download-cta.component.ts`), aus einem härteren Grund als dort:\n`cds-button` rendert IMMER ein `<button>` — sein Input-Vertrag\n(`ButtonComponentInputs`) kennt kein `href` — und kann die hier nötige\nVerzweigung „`primaryHref` gesetzt → `<a>`“ strukturell gar nicht erfüllen. Ein\nprojiziertes `<cds-button>` brächte hier also kein Custom-Element-Layoutproblem\n(kein Kindselektor an `.ep-cta-band` hängt von der DOM-Tiefe ab), sondern schlicht\ndie falsche Fähigkeit.\n\n**Entscheidung 4 — bewusst nur EINE Aktion, kein `secondaryLabel`.** Eine frühere\nFassung bot `secondaryLabel`/`secondaryHref` an, ungeprüft gegen das Mockup. Beide\nBelege dagegen: erstens zeigt KEINES der 22 `.ep-cta-band`-Vorkommen in\n`docs/index.html` eine zweite Aktion — der Zwei-Aktionen-Fall hat keine Vorlage.\nZweitens (der schwerere Grund) lässt sich eine zweite, optisch zurückhaltendere\nAktion mit den vorhandenen Klassen gar nicht bauen: `.btn-{area}.btn-on-band{color:\n…}` (css/components.css:47–51) überschreibt die Textfarbe JEDER Variante auf den\nBereichston — genau den Ton, den die Doku durchgängig als Bandhintergrund nennt.\nNur `.btn-filled` tauscht zusätzlich die Füllung gegen Weiß; `.btn-outlined`,\n`.btn-tonal` und `.btn-text` blieben transparent und zeigten Text in derselben\nFarbe wie das Band darunter — unsichtbar. Eine zweite Aktion hätte deshalb\nzwangsläufig dieselbe Optik wie die erste getragen, keine Hierarchie, nur zwei\ngleich gewichtete Buttons. Das ist eine CSS-Lücke (kein kontrastsicherer\nsekundärer On-Band-Stil) — gemeldet, nicht in diesem Wrapper improvisiert. Solange sie besteht, trägt\n`cds-cta-band` nur eine Aktion; ein Konsument mit echtem Bedarf für eine zweite\nschreibt sie außerhalb der Komponente ins Band hinein (`<ng-content>` gibt es\nhier bewusst nicht, um genau das nicht als unterstützten Pfad zu suggerieren).\nOhne zweite Aktion braucht es auch keinen eigenen Container: die einzelne Aktion\nzentriert sich über das ererbte `.ep-cta-band{text-align:center}` von selbst,\ngenau wie in allen 22 Mockup-Vorkommen — ein zusätzliches Flex-Layout im Wrapper\nhätte eine Darstellung erfunden, die die CSS-Schicht so nicht kennt (ADR-0001;\ndie einzige existierende Deklaration dieser Form ist `.ep-hero-ctas`,\ncss/components.css:1222, eine andere Klasse für einen anderen Bauteiltyp).\n\n**Kein `aria-label` auf der Aktion.** Anders als bei `cds-feature`\n(`ctaAriaLabel`, 11 von 12 Vorkommen mit `aria-label`) zeigt keines der 22\nBand-Vorkommen einen `aria-label` auf seiner Aktion — ausgezählt, nicht\ngeschätzt. Die sichtbaren Texte sind durchgängig konkrete Verben mit Ziel\n(„Termin buchen“, „Gespräch anfragen“, „Offene Stellen ansehen“ …) und stehen je\nSeite nur einmal; anders als bei einer Karten-Liste braucht es hier keine\nDisambiguierung. Kein entsprechender Input.",
387
387
  "docgen": {
388
388
  "$ref": "../services/core/docgen/komponenten-call-to-action-cta-band.json#/components/komponenten-call-to-action-cta-band"
389
389
  },
@@ -415,7 +415,7 @@
415
415
  "komponenten-tabelle-tabelle": {
416
416
  "id": "komponenten-tabelle-tabelle",
417
417
  "name": "TableComponent",
418
- "description": "Table (`cds-table`) — Wrapper um `.tbl`/`.tbl-wrap` aus css/components.css\n(css/components.css:1462–1480): die Datentabelle der Doku-Site (`sec-table`,\nNav-Einträge „Standard“ und „Gestreift“). Ausgezählt: außerhalb von `sec-table`\nselbst kommt `.tbl-wrap` nur zweimal vor, beide auf Beitragsseiten\n(`docs/index.html:8622`, `15080`) — keine der 24 Beispielseiten außerhalb von\nWissensbeiträgen setzt aktuell eine Tabelle ein. Neuntes Ticket der\nSeitenbausteine-Serie.\n\n**Kein Daten-Input.** Eine `columns`/`rows`-API würde Zellinhalte auf Strings\nfestlegen; die Beispielseiten setzen darin Badges, Links und `data-num`\n(`docs/index.html:5076–5177`). Der Konsument projiziert `<thead>`/`<tbody>`/\n`<tfoot>` unverändert per `<ng-content>`, die Komponente liefert nur die\nHülle: `.tbl-wrap`, `.tbl`, `<caption>` und die Striped-Klasse.\n\n**`caption` ist Pflicht (`input.required<string>()`), abweichend von der\nTicket-Skizze (dort `caption? = ''`).** Die eigene Doku dieser Komponente\n(`storybook-angular/src/docs/komponenten/tabelle.mdx:137`) nennt `<caption>`\nausdrücklich „Pflicht, Screenreader lesen den Titel vor, bevor die Zellen\nvorgelesen werden“ und wiederholt das in den Dos (`tabelle.mdx:147`:\n„`<caption>` und `scope`-Attribute immer setzen, auch bei einfachen\nTabellen“). Ausgezählt: alle 5 realen `.tbl`-Vorkommen in `docs/index.html`\nhaben eine `<caption>`, ausnahmslos. Ein optionaler Input mit Default `''`\nhätte genau den Fall erlaubt, den die eigene Doku verbietet — nach\nADR-0007 §2 ist `caption` damit Inhalt, nicht Beiwerk: ohne sie ist eine\n`cds-table` eine Tabelle, die die eigenen Barrierefreiheitsregeln bricht,\nalso sinnlos. Ein vergessenes Binding wirft jetzt `NG0950`, statt eine\nunbeschriftete Tabelle still auszuliefern.\n\n**Element-Selektor, `.tbl-wrap`/`.tbl` sitzen im eigenen Template, nicht am\nHost (ADR-0008-Standardfall, analog zu `cds-facts`/`cds-faq`).** Ausgezählt:\nalle 5 `.tbl-wrap`-Vorkommen in `docs/index.html` (Zeilen 5061, 5116, 5236,\n8622, 15080). Keines ist selbst ein direktes Grid-/Flex-Kind — das einzige\nVorkommen in einem `.layout-grid` (Zeile 5236) sitzt eine Ebene tiefer in\neinem `.col-8`, das seinerseits der Grid-Child ist; `.tbl-wrap` ist dort ein\ngewöhnlicher Block-Nachfahre, dessen Breite vom umschließenden `.col-8`\nkommt, nicht von `align-items:stretch` auf einer bestimmten DOM-Tiefe (kein\n`.ep-card`-Fall). Keines trägt eine `col-*`-Klasse selbst, keines variiert\ndas Tag (immer `<div class=\"tbl-wrap\">`). Ein `<cds-table>`-Host ohne eigene\nKlasse und ohne eigenes `background` gerät deshalb nicht in den „Fläche am\nHost“-Fall aus ADR-0008 Fall 2: die Komponente setzt nichts auf dem Host,\ndas eine eigene Box bräuchte, `.tbl-wrap` bleibt ein normaler Block-Nachfahre\nunabhängig vom `display` des `<cds-table>`-Hosts (derselbe Grund, warum\n`cds-facts`/`cds-faq` als Element-Selektor funktionieren).\n\nEine Attributselektor-Variante direkt am `<table>` (`table[cdsTable]`, analog\nzu `cdsSection`) wurde erwogen und verworfen: `<table>` ist als Tag ohnehin\nfix (kein Tag-Wechsel wie bei `cdsIconCard`), aber `.tbl-wrap` bräuchte dann\nein ZWEITES Element, das der Konsument von Hand um die Tabelle schreiben\nmüsste (`<div class=\"tbl-wrap\" tabindex=\"0\" role=\"region\" aria-label=\"…\">\n<table cdsTable>…`) — genau die beiden Aufgaben, die laut Ticket am ehesten\nvergessen werden (fokussierbarer, benannter Scroll-Container), blieben dann\nbeim Konsumenten statt bei der Komponente. Der Element-Selektor hält Wrap und\nTabelle zusammen in einem Template.\n\n**Geprüft im laufenden Storybook (Story „Interaktiv“, Play-Funktion):**\n`<table class=\"tbl\">` liegt direkt unter `<div class=\"tbl-wrap\">`, `<caption>`\nist ihr erstes Kind, danach folgen ohne zusätzlichen Knoten dazwischen genau\ndie projizierten `<thead>`/`<tbody>`-Elemente — `<ng-content>` fügt selbst\nkein DOM-Element ein, es verschiebt nur die vom Konsumenten geschriebenen\nnativen Elemente an ihre Stelle. Damit bleibt die Tabellenstruktur (`<table>`\n→ `<caption>` + `<thead>` + `<tbody>` + optional `<tfoot>`, keine fremde\nEbene dazwischen) exakt so gültig wie im rohen Markup.\n\n**`.tbl-wrap` ist immer fokussierbar UND immer benannt.** Das CSS gibt dem\nScroll-Container einen eigenen `:focus-visible`-Ring\n(css/components.css:1463) — ein scrollbarer Bereich muss per Tastatur\nerreichbar sein (WCAG 2.1.1), `tabindex=\"0\"` steht deshalb fest im Template.\n`role=\"region\"` braucht laut HTML-AAM einen zugänglichen Namen, sonst bekäme\nder Bereich keine Landmark-Rolle — weil `caption` jetzt Pflicht ist, kann\n`accessibleName()` (`scrollLabel() || caption()`) nicht mehr leer werden,\n`role`/`aria-label` sind deshalb unbedingt gesetzt, kein Nullfall mehr zu\nbehandeln. `scrollLabel` bleibt eine optionale Überschreibung (Vorrang vor\n`caption`) für Fälle, in denen der sichtbare Tabellentitel für den\nScroll-Container zu lang oder zu unspezifisch wäre.\n\n**`.tbl-sort` bleibt außen vor.** Die Klasse existiert\n(css/components.css:1477–1482), die Sortierlogik nicht — kein zugehöriges\nJS in `docs/main.js` für die Beispielseiten. Ein Wrapper, der nur den\nButton-Look lieferte, täuschte Funktion vor, die es nicht gibt. Siehe\n`.scratch/angular-seitenbausteine/issues/19-fehlende-sortierlogik-tbl-sort.md`.",
418
+ "description": "Table (`cds-table`) — Wrapper um `.tbl`/`.tbl-wrap` aus css/components.css\n(css/components.css:1462–1480): die Datentabelle der Doku-Site (`sec-table`,\nNav-Einträge „Standard“ und „Gestreift“). Ausgezählt: außerhalb von `sec-table`\nselbst kommt `.tbl-wrap` nur zweimal vor, beide auf Beitragsseiten\n(`docs/index.html:8622`, `15080`) — keine der 24 Beispielseiten außerhalb von\nWissensbeiträgen setzt aktuell eine Tabelle ein.\n\n**Kein Daten-Input.** Eine `columns`/`rows`-API würde Zellinhalte auf Strings\nfestlegen; die Beispielseiten setzen darin Badges, Links und `data-num`\n(`docs/index.html:5076–5177`). Der Konsument projiziert `<thead>`/`<tbody>`/\n`<tfoot>` unverändert per `<ng-content>`, die Komponente liefert nur die\nHülle: `.tbl-wrap`, `.tbl`, `<caption>` und die Striped-Klasse.\n\n**`caption` ist Pflicht (`input.required<string>()`), abweichend von der\nursprünglichen Skizze (dort `caption? = ''`).** Die eigene Doku dieser Komponente\n(`storybook-angular/src/docs/komponenten/tabelle.mdx:137`) nennt `<caption>`\nausdrücklich „Pflicht, Screenreader lesen den Titel vor, bevor die Zellen\nvorgelesen werden“ und wiederholt das in den Dos (`tabelle.mdx:147`:\n„`<caption>` und `scope`-Attribute immer setzen, auch bei einfachen\nTabellen“). Ausgezählt: alle 5 realen `.tbl`-Vorkommen in `docs/index.html`\nhaben eine `<caption>`, ausnahmslos. Ein optionaler Input mit Default `''`\nhätte genau den Fall erlaubt, den die eigene Doku verbietet — nach\nADR-0007 §2 ist `caption` damit Inhalt, nicht Beiwerk: ohne sie ist eine\n`cds-table` eine Tabelle, die die eigenen Barrierefreiheitsregeln bricht,\nalso sinnlos. Ein vergessenes Binding wirft jetzt `NG0950`, statt eine\nunbeschriftete Tabelle still auszuliefern.\n\n**Element-Selektor, `.tbl-wrap`/`.tbl` sitzen im eigenen Template, nicht am\nHost (ADR-0008-Standardfall, analog zu `cds-facts`/`cds-faq`).** Ausgezählt:\nalle 5 `.tbl-wrap`-Vorkommen in `docs/index.html` (Zeilen 5061, 5116, 5236,\n8622, 15080). Keines ist selbst ein direktes Grid-/Flex-Kind — das einzige\nVorkommen in einem `.layout-grid` (Zeile 5236) sitzt eine Ebene tiefer in\neinem `.col-8`, das seinerseits der Grid-Child ist; `.tbl-wrap` ist dort ein\ngewöhnlicher Block-Nachfahre, dessen Breite vom umschließenden `.col-8`\nkommt, nicht von `align-items:stretch` auf einer bestimmten DOM-Tiefe (kein\n`.ep-card`-Fall). Keines trägt eine `col-*`-Klasse selbst, keines variiert\ndas Tag (immer `<div class=\"tbl-wrap\">`). Ein `<cds-table>`-Host ohne eigene\nKlasse und ohne eigenes `background` gerät deshalb nicht in den „Fläche am\nHost“-Fall aus ADR-0008 Fall 2: die Komponente setzt nichts auf dem Host,\ndas eine eigene Box bräuchte, `.tbl-wrap` bleibt ein normaler Block-Nachfahre\nunabhängig vom `display` des `<cds-table>`-Hosts (derselbe Grund, warum\n`cds-facts`/`cds-faq` als Element-Selektor funktionieren).\n\nEine Attributselektor-Variante direkt am `<table>` (`table[cdsTable]`, analog\nzu `cdsSection`) wurde erwogen und verworfen: `<table>` ist als Tag ohnehin\nfix (kein Tag-Wechsel wie bei `cdsIconCard`), aber `.tbl-wrap` bräuchte dann\nein ZWEITES Element, das der Konsument von Hand um die Tabelle schreiben\nmüsste (`<div class=\"tbl-wrap\" tabindex=\"0\" role=\"region\" aria-label=\"…\">\n<table cdsTable>…`) — genau die beiden Aufgaben, die am ehesten\nvergessen werden (fokussierbarer, benannter Scroll-Container), blieben dann\nbeim Konsumenten statt bei der Komponente. Der Element-Selektor hält Wrap und\nTabelle zusammen in einem Template.\n\n**Geprüft im laufenden Storybook (Story „Interaktiv“, Play-Funktion):**\n`<table class=\"tbl\">` liegt direkt unter `<div class=\"tbl-wrap\">`, `<caption>`\nist ihr erstes Kind, danach folgen ohne zusätzlichen Knoten dazwischen genau\ndie projizierten `<thead>`/`<tbody>`-Elemente — `<ng-content>` fügt selbst\nkein DOM-Element ein, es verschiebt nur die vom Konsumenten geschriebenen\nnativen Elemente an ihre Stelle. Damit bleibt die Tabellenstruktur (`<table>`\n→ `<caption>` + `<thead>` + `<tbody>` + optional `<tfoot>`, keine fremde\nEbene dazwischen) exakt so gültig wie im rohen Markup.\n\n**`.tbl-wrap` ist immer fokussierbar UND immer benannt.** Das CSS gibt dem\nScroll-Container einen eigenen `:focus-visible`-Ring\n(css/components.css:1463) — ein scrollbarer Bereich muss per Tastatur\nerreichbar sein (WCAG 2.1.1), `tabindex=\"0\"` steht deshalb fest im Template.\n`role=\"region\"` braucht laut HTML-AAM einen zugänglichen Namen, sonst bekäme\nder Bereich keine Landmark-Rolle — weil `caption` jetzt Pflicht ist, kann\n`accessibleName()` (`scrollLabel() || caption()`) nicht mehr leer werden,\n`role`/`aria-label` sind deshalb unbedingt gesetzt, kein Nullfall mehr zu\nbehandeln. `scrollLabel` bleibt eine optionale Überschreibung (Vorrang vor\n`caption`) für Fälle, in denen der sichtbare Tabellentitel für den\nScroll-Container zu lang oder zu unspezifisch wäre.\n\n**`.tbl-sort` bleibt außen vor.** Die Klasse existiert\n(css/components.css:1477–1482), die Sortierlogik nicht — kein zugehöriges\nJS in `docs/main.js` für die Beispielseiten. Ein Wrapper, der nur den\nButton-Look lieferte, täuschte Funktion vor, die es nicht gibt.",
419
419
  "docgen": {
420
420
  "$ref": "../services/core/docgen/komponenten-tabelle-tabelle.json#/components/komponenten-tabelle-tabelle"
421
421
  },
@@ -436,7 +436,7 @@
436
436
  "komponenten-tabelle-vergleichstabelle": {
437
437
  "id": "komponenten-tabelle-vergleichstabelle",
438
438
  "name": "CompareComponent",
439
- "description": "Compare (`cds-compare`) — Wrapper um `.ep-compare*` aus css/components.css\n(css/components.css:1275–1294): die aufklappbare Vergleichstabelle für den zeilenweisen\nDirektvergleich mehrerer Pakete/Tarife (Doku-Site `sec-table`, Nav-Eintrag\n„Vergleichstabelle (aufklappbar)“, `docs/index.html:5182–5204`). Zehntes Ticket der\nSeitenbausteine-Serie, nach `cds-table` (Ticket 09).\n\n**Mit Daten-Input, anders als `cds-table`.** `cds-table` projiziert `<thead>`/`<tbody>`\nunverändert per `<ng-content>`, weil seine Zellen Badges, Links und `data-num` tragen können\n(siehe dessen Klassendoku). Hier ist der Zellinhalt strukturell auf Ja/Nein oder eine kurze\nAngabe begrenzt — ausgezählt: alle 22 Datenzellen des einzigen realen Vorkommens\n(`docs/index.html:11970–12038`, 11 Zeilen × 2 Spalten, ohne die Zeilenüberschriften) sind\nentweder ein `.ep-compare-yes`/`-no`-Marker (18×, davon 14× „Ja“, 4× „Nein“) oder ein kurzer\nText wie „bis 70 Mrd. Param.“ bzw. „30 Min.“ (4×) — nie ein Badge, Link oder anderes Markup.\nWeil Spaltenzahl, `pro`-Spalte und Zellinhalt damit vollständig aus zwei Arrays (`columns`,\n`rows`) hervorgehen, würde `<ng-content>` beim Konsumenten nur denselben `@for`-Code\nduplizieren, ohne eine Freiheit zu gewinnen, die die Zell-Typisierung (`boolean | string`)\nohnehin ausschließt.\n\n**Ja/Nein-Zellen tragen einen Screenreader-Text, übernommen statt erfunden.** Im einzigen\nrealen Vorkommen steht hinter jeder Glyphe ein `.sr-only`-Text, ausnahmslos:\n`<span class=\"ep-compare-yes\" aria-hidden=\"true\">✓</span><span class=\"sr-only\">Enthalten</span>`\n(14×) bzw. `<span class=\"ep-compare-no\" aria-hidden=\"true\">−</span><span class=\"sr-only\">Nicht\nenthalten</span>` (4×, „−“ ist U+2212 Minus, kein Bindestrich). Genau diese Zeichen und dieser\nWortlaut, keine eigene Formulierung — Farbe und Glyphe allein tragen keine Bedeutung\n(WCAG 1.4.1), der `.sr-only`-Text ist deshalb kein optionaler Zusatz, sondern fest verdrahtet,\nnicht per Input überschreibbar.\n\n**`caption` ist Pflicht (`input.required<string>()`), abweichend von der Ticket-Skizze (dort\nkein Input dafür vorgesehen) — dieselbe Begründung wie bei `cds-table`.** Die eigene Doku\n(`storybook-angular/src/docs/komponenten/tabelle.mdx:91`) nennt `<caption class=\"sr-only\">`\nausdrücklich als Teil des Bauteils, und das einzige reale Vorkommen hat eine. Anders als bei\n`cds-table` ist sie hier `.sr-only`, nicht sichtbar: der Klartext steht schon im `<summary>`\n(`summary`-Input), die `<caption>` wiederholt ihn nur für Screenreader, die nach dem Aufklappen\ndirekt in die Tabelle springen, ohne den Summary-Text erneut vorgelesen zu bekommen.\n\n**`rowsLabel` (erste Kopfzelle, `<th scope=\"col\">Funktion</th>` im Mockup) ist dagegen Beiwerk,\noptional mit Default `''`.** Auch dafür sieht die Ticket-Skizze keinen Input vor. Den Text fest\nim Template zu verdrahten hieße, AI.Box-Copy in einen wiederverwendbaren Wrapper zu backen\n(ADR-0007 §2); ihn ganz wegzulassen wich ohne CSS-Befund vom einzigen Beleg ab, ohne dass die\nDoku das verlangt. Anders als `caption` ist die Kopfzelle nicht barrierefreiheitsrelevant\nPflicht: ein leeres `<th scope=\"col\">` bleibt eine gültige, in Vergleichsmatrizen verbreitete\nEcke ohne eigenen Namen. Deshalb Beiwerk, kein Inhalt — das Beispielwort „Funktion“ steht in\nder Story, nicht in der Klasse.\n\n**Natives `<details>`/`<summary>` bleibt erhalten, kein nachgebautes Disclosure** (Ticket-\nVorgabe, analog `cds-faq`): Tastaturbedienung und Toggle-Verhalten kommen vom Browser, das CSS\nhängt direkt an `[open]` (`.ep-compare[open] .ep-compare-caret`). `<details class=\"ep-compare\">`\nsitzt deshalb im TEMPLATE, nicht am Host — derselbe Grund wie bei `cds-table`s\n`.tbl-wrap`/`.tbl`: ein `<cds-compare>`-Host ist ein unbekanntes Custom Element, kein echtes\n`<details>`, und kann dessen native Disclosure-Semantik nicht annehmen.\n\n**Element-Selektor (ADR-0008-Standardfall).** `.ep-compare` sitzt im einzigen realen Vorkommen\nals gewöhnlicher Block-Nachfahre in `.ep-section` (`docs/index.html:11969`) — kein Grid-/\nFlex-Kind, keine `col-*`-Klasse vom Konsumenten, kein Tag-Wechsel (immer `<details>`). Keines\nder drei ADR-0008-Kriterien greift, ein Attributselektor brächte hier nichts.\n\n**`open` ist ein reiner Anfangszustand, keine Zwei-Wege-Bindung.** Gebunden über\n`[attr.open]=\"open() ? '' : null\"`: Angular schreibt das Attribut nur, wenn sich der GEPRÜFTE\nAusdruckswert seit dem letzten Change-Detection-Lauf ändert. `open()` bleibt nach dem ersten\nRendern konstant (die Komponente schreibt nie in dieses Signal zurück), ein nachfolgender\nnativer Toggle wird deshalb nicht rückgängig gemacht — geprüft in der Story „Interaktiv“\n(Play-Funktion: Klick öffnet, `toggled` feuert `true`, ein zweiter, vom Toggle unabhängiger\nInteraktionsschritt lässt die Tabelle weiterhin offen).\n\nVerwendungsguidance dieser Gruppe: siehe Tabelle (`komponenten-tabelle-tabelle--verwendung`).",
439
+ "description": "Compare (`cds-compare`) — Wrapper um `.ep-compare*` aus css/components.css\n(css/components.css:1275–1294): die aufklappbare Vergleichstabelle für den zeilenweisen\nDirektvergleich mehrerer Pakete/Tarife (Doku-Site `sec-table`, Nav-Eintrag\n„Vergleichstabelle (aufklappbar)“, `docs/index.html:5182–5204`), nach `cds-table`.\n\n**Mit Daten-Input, anders als `cds-table`.** `cds-table` projiziert `<thead>`/`<tbody>`\nunverändert per `<ng-content>`, weil seine Zellen Badges, Links und `data-num` tragen können\n(siehe dessen Klassendoku). Hier ist der Zellinhalt strukturell auf Ja/Nein oder eine kurze\nAngabe begrenzt — ausgezählt: alle 22 Datenzellen des einzigen realen Vorkommens\n(`docs/index.html:11970–12038`, 11 Zeilen × 2 Spalten, ohne die Zeilenüberschriften) sind\nentweder ein `.ep-compare-yes`/`-no`-Marker (18×, davon 14× „Ja“, 4× „Nein“) oder ein kurzer\nText wie „bis 70 Mrd. Param.“ bzw. „30 Min.“ (4×) — nie ein Badge, Link oder anderes Markup.\nWeil Spaltenzahl, `pro`-Spalte und Zellinhalt damit vollständig aus zwei Arrays (`columns`,\n`rows`) hervorgehen, würde `<ng-content>` beim Konsumenten nur denselben `@for`-Code\nduplizieren, ohne eine Freiheit zu gewinnen, die die Zell-Typisierung (`boolean | string`)\nohnehin ausschließt.\n\n**Ja/Nein-Zellen tragen einen Screenreader-Text, übernommen statt erfunden.** Im einzigen\nrealen Vorkommen steht hinter jeder Glyphe ein `.sr-only`-Text, ausnahmslos:\n`<span class=\"ep-compare-yes\" aria-hidden=\"true\">✓</span><span class=\"sr-only\">Enthalten</span>`\n(14×) bzw. `<span class=\"ep-compare-no\" aria-hidden=\"true\">−</span><span class=\"sr-only\">Nicht\nenthalten</span>` (4×, „−“ ist U+2212 Minus, kein Bindestrich). Genau diese Zeichen und dieser\nWortlaut, keine eigene Formulierung — Farbe und Glyphe allein tragen keine Bedeutung\n(WCAG 1.4.1), der `.sr-only`-Text ist deshalb kein optionaler Zusatz, sondern fest verdrahtet,\nnicht per Input überschreibbar.\n\n**`caption` ist Pflicht (`input.required<string>()`), abweichend von der ursprünglichen Skizze (dort\nkein Input dafür vorgesehen) — dieselbe Begründung wie bei `cds-table`.** Die eigene Doku\n(`storybook-angular/src/docs/komponenten/tabelle.mdx:91`) nennt `<caption class=\"sr-only\">`\nausdrücklich als Teil des Bauteils, und das einzige reale Vorkommen hat eine. Anders als bei\n`cds-table` ist sie hier `.sr-only`, nicht sichtbar: der Klartext steht schon im `<summary>`\n(`summary`-Input), die `<caption>` wiederholt ihn nur für Screenreader, die nach dem Aufklappen\ndirekt in die Tabelle springen, ohne den Summary-Text erneut vorgelesen zu bekommen.\n\n**`rowsLabel` (erste Kopfzelle, `<th scope=\"col\">Funktion</th>` im Mockup) ist dagegen Beiwerk,\noptional mit Default `''`.** Auch dafür sieht die ursprüngliche Skizze keinen Input vor. Den Text fest\nim Template zu verdrahten hieße, AI.Box-Copy in einen wiederverwendbaren Wrapper zu backen\n(ADR-0007 §2); ihn ganz wegzulassen wich ohne CSS-Befund vom einzigen Beleg ab, ohne dass die\nDoku das verlangt. Anders als `caption` ist die Kopfzelle nicht barrierefreiheitsrelevant\nPflicht: ein leeres `<th scope=\"col\">` bleibt eine gültige, in Vergleichsmatrizen verbreitete\nEcke ohne eigenen Namen. Deshalb Beiwerk, kein Inhalt — das Beispielwort „Funktion“ steht in\nder Story, nicht in der Klasse.\n\n**Natives `<details>`/`<summary>` bleibt erhalten, kein nachgebautes Disclosure**\n(analog `cds-faq`): Tastaturbedienung und Toggle-Verhalten kommen vom Browser, das CSS\nhängt direkt an `[open]` (`.ep-compare[open] .ep-compare-caret`). `<details class=\"ep-compare\">`\nsitzt deshalb im TEMPLATE, nicht am Host — derselbe Grund wie bei `cds-table`s\n`.tbl-wrap`/`.tbl`: ein `<cds-compare>`-Host ist ein unbekanntes Custom Element, kein echtes\n`<details>`, und kann dessen native Disclosure-Semantik nicht annehmen.\n\n**Element-Selektor (ADR-0008-Standardfall).** `.ep-compare` sitzt im einzigen realen Vorkommen\nals gewöhnlicher Block-Nachfahre in `.ep-section` (`docs/index.html:11969`) — kein Grid-/\nFlex-Kind, keine `col-*`-Klasse vom Konsumenten, kein Tag-Wechsel (immer `<details>`). Keines\nder drei ADR-0008-Kriterien greift, ein Attributselektor brächte hier nichts.\n\n**`open` ist ein reiner Anfangszustand, keine Zwei-Wege-Bindung.** Gebunden über\n`[attr.open]=\"open() ? '' : null\"`: Angular schreibt das Attribut nur, wenn sich der GEPRÜFTE\nAusdruckswert seit dem letzten Change-Detection-Lauf ändert. `open()` bleibt nach dem ersten\nRendern konstant (die Komponente schreibt nie in dieses Signal zurück), ein nachfolgender\nnativer Toggle wird deshalb nicht rückgängig gemacht — geprüft in der Story „Interaktiv“\n(Play-Funktion: Klick öffnet, `toggled` feuert `true`, ein zweiter, vom Toggle unabhängiger\nInteraktionsschritt lässt die Tabelle weiterhin offen).\n\nVerwendungsguidance dieser Gruppe: siehe Tabelle (`komponenten-tabelle-tabelle--verwendung`).",
440
440
  "docgen": {
441
441
  "$ref": "../services/core/docgen/komponenten-tabelle-vergleichstabelle.json#/components/komponenten-tabelle-vergleichstabelle"
442
442
  },
@@ -585,7 +585,7 @@
585
585
  "komponenten-hero-hero-bild": {
586
586
  "id": "komponenten-hero-hero-bild",
587
587
  "name": "HeroImageComponent",
588
- "description": "HeroImage (cds-hero-image) — Wrapper um `.hero-image` aus css/components.css\n(css/components.css:851–878) mit optionaler Caption als Gradient-Overlay\n(`-caption`, `-caption-eyebrow`, `-caption-title`, `-caption-text`). Vollbreites,\nrandloses `<figure>` im 21:9-Format, der Standard-Hero auf allen Customer-Pages\n(17 Vorkommen in den Beispielseiten). Semantisch ein figure/figcaption-Pattern:\n`alt` trägt die faktische Bildbeschreibung, die Caption die redaktionelle\nEinordnung (Eyebrow, Titel, Text) — beide sollen sich nach `docs/index.html`\n(Abschnitt „In-Article Figure“, dasselbe Prinzip) nicht inhaltlich decken.\n\n**Entscheidung 1 — kein `<figcaption>` ohne Textinhalt.** Bleiben `eyebrow`,\n`heading` und `text` alle leer, entfällt das `<figcaption>`-Element vollständig\nstatt leer zu rendern (Variante „Hero ohne Caption“, siehe\n`storybook-angular/src/docs/seitenmuster/wissensbeitrag.mdx` Abschnitt „Aufbau“,\nZeile 3: Beitrags-Heros zeigen das visuelle Versprechen ohne Marketing-Overlay).\nEin leeres `<figcaption>` wäre totes Markup ohne Zugänglichkeitsnutzen.\n\n**Entscheidung 2 — `headingLevel` statt fest verdrahtetem `<h1>`.** Das Mockup\n(`docs/index.html:9412` u. a.) rendert `.hero-image-caption-title` durchgehend als\n`<h1>`, weil der Hero dort das erste Überschriften-Element der Seite ist. Sitzt\nder Hero dagegen unter einem eigenen `<h1>` — der Wissensbeitrag hat seinen\nTitel bereits im Article-Header (`.article-title`, ebenfalls `<h1>`) —, entstünde\nmit einem zweiten `<h1>` eine doppelte Top-Überschrift. `headingLevel` (Default\n`1`) macht die Ebene explizit, `.hero-image-caption-title` ist reine CSS-Klasse\nund trägt keine Heading-Semantik, das Styling bleibt in beiden Fällen identisch.\n\n**Entscheidung 3 — `objectPosition` als Style-Binding, die eine Ausnahme von\nSpec-Randbedingung 6.** Der Bildausschnitt ist eine Eigenschaft des konkreten\nBildes (Motiv, Kopfposition), nicht der Seite — anders als z. B. eine\nSektionsfläche, die vom Seitenkontext abhängt. `docs/index.html` setzt ihn\ndeshalb an jedem Hero individuell direkt am `<img>` (z. B. `object-position:center\n70%`, Zeile 2640). Ohne diesen Input wäre die Komponente für Porträt-lastige\nMotive unbrauchbar: `object-fit:cover` beschneidet das Bild sobald der\n`max-height`-Cap greift (siehe `.hero-image-media`), und ohne Steuerung landet\nder Ausschnitt zufällig in der Bildmitte. Als Style-Binding am `<img>`, nicht als\nKlasse — es gibt in der CSS-Schicht keine Modifier-Klassen dafür, nur den\nInline-Weg, den das Mockup selbst nutzt.\n\n**Entscheidung 4 — `tabindex`/`id` sind nicht Teil der Komponente.** Das Mockup\nsetzt beides am `<figure>` (`id=\"ep-landing-main\" tabindex=\"-1\"`, z. B. Zeile\n9406) als Sprungziel des Skip-Links. Das Sprungziel ist eine Entscheidung der\nSeite (welches Element „Hauptinhalt“ ist, hängt vom Seitenaufbau ab, nicht vom\nHero selbst), deshalb setzt der Konsument beides am `<cds-hero-image>`-Host.\nDamit dieses Sprungziel eine eigene Box hat, ist der Host per\n`:host{display:block}` selbst ein Block (Muster wie `cds-footer-main`): Als\nunbekanntes Element wäre er `display:inline` mit herausgebrochenem Block-Kind\nund hätte keine brauchbare Box für Fokus und Scroll-Position (ADR-0008, Fall 2).",
588
+ "description": "HeroImage (cds-hero-image) — Wrapper um `.hero-image` aus css/components.css\n(css/components.css:851–878) mit optionaler Caption als Gradient-Overlay\n(`-caption`, `-caption-eyebrow`, `-caption-title`, `-caption-text`). Vollbreites,\nrandloses `<figure>` im 21:9-Format, der Standard-Hero auf allen Customer-Pages\n(17 Vorkommen in den Beispielseiten). Semantisch ein figure/figcaption-Pattern:\n`alt` trägt die faktische Bildbeschreibung, die Caption die redaktionelle\nEinordnung (Eyebrow, Titel, Text) — beide sollen sich nach `docs/index.html`\n(Abschnitt „In-Article Figure“, dasselbe Prinzip) nicht inhaltlich decken.\n\n**Entscheidung 1 — kein `<figcaption>` ohne Textinhalt.** Bleiben `eyebrow`,\n`heading` und `text` alle leer, entfällt das `<figcaption>`-Element vollständig\nstatt leer zu rendern (Variante „Hero ohne Caption“, siehe\n`storybook-angular/src/docs/seitenmuster/wissensbeitrag.mdx` Abschnitt „Aufbau“,\nZeile 3: Beitrags-Heros zeigen das visuelle Versprechen ohne Marketing-Overlay).\nEin leeres `<figcaption>` wäre totes Markup ohne Zugänglichkeitsnutzen.\n\n**Entscheidung 2 — `headingLevel` statt fest verdrahtetem `<h1>`.** Das Mockup\n(`docs/index.html:9412` u. a.) rendert `.hero-image-caption-title` durchgehend als\n`<h1>`, weil der Hero dort das erste Überschriften-Element der Seite ist. Sitzt\nder Hero dagegen unter einem eigenen `<h1>` — der Wissensbeitrag hat seinen\nTitel bereits im Article-Header (`.article-title`, ebenfalls `<h1>`) —, entstünde\nmit einem zweiten `<h1>` eine doppelte Top-Überschrift. `headingLevel` (Default\n`1`) macht die Ebene explizit, `.hero-image-caption-title` ist reine CSS-Klasse\nund trägt keine Heading-Semantik, das Styling bleibt in beiden Fällen identisch.\n\n**Entscheidung 3 — `objectPosition` als Style-Binding, bewusste Ausnahme von\nder Regel, dass Mockup-Inline-Styles nicht in den Wrapper wandern.** Der\nBildausschnitt ist eine Eigenschaft des konkreten\nBildes (Motiv, Kopfposition), nicht der Seite — anders als z. B. eine\nSektionsfläche, die vom Seitenkontext abhängt. `docs/index.html` setzt ihn\ndeshalb an jedem Hero individuell direkt am `<img>` (z. B. `object-position:center\n70%`, Zeile 2640). Ohne diesen Input wäre die Komponente für Porträt-lastige\nMotive unbrauchbar: `object-fit:cover` beschneidet das Bild sobald der\n`max-height`-Cap greift (siehe `.hero-image-media`), und ohne Steuerung landet\nder Ausschnitt zufällig in der Bildmitte. Als Style-Binding am `<img>`, nicht als\nKlasse — es gibt in der CSS-Schicht keine Modifier-Klassen dafür, nur den\nInline-Weg, den das Mockup selbst nutzt.\n\n**Entscheidung 4 — `tabindex`/`id` sind nicht Teil der Komponente.** Das Mockup\nsetzt beides am `<figure>` (`id=\"ep-landing-main\" tabindex=\"-1\"`, z. B. Zeile\n9406) als Sprungziel des Skip-Links. Das Sprungziel ist eine Entscheidung der\nSeite (welches Element „Hauptinhalt“ ist, hängt vom Seitenaufbau ab, nicht vom\nHero selbst), deshalb setzt der Konsument beides am `<cds-hero-image>`-Host.\nDamit dieses Sprungziel eine eigene Box hat, ist der Host per\n`:host{display:block}` selbst ein Block (Muster wie `cds-footer-main`): Als\nunbekanntes Element wäre er `display:inline` mit herausgebrochenem Block-Kind\nund hätte keine brauchbare Box für Fokus und Scroll-Position (ADR-0008, Fall 2).",
589
589
  "docgen": {
590
590
  "$ref": "../services/core/docgen/komponenten-hero-hero-bild.json#/components/komponenten-hero-hero-bild"
591
591
  },
@@ -606,7 +606,7 @@
606
606
  "komponenten-hero-störer": {
607
607
  "id": "komponenten-hero-störer",
608
608
  "name": "StoererSetComponent",
609
- "description": "StoererSet (cds-stoerer-set) — Wrapper um `.stoerer-set` / `.stoerer-list` aus\ncss/components.css (css/components.css:926–974): das Set aus ein bis drei\n`<cds-stoerer>`-Kacheln, das auf der Startseite oben rechts über dem Hero-Bild\nliegt. Der Positionsrahmen `.stoerer-hero` (umschließt Hero-Bild UND Set,\n`container-type:inline-size` für die Container-Query-Schwelle) ist bewusst NICHT\nTeil dieser Komponente, sondern bleibt beim Konsumenten (siehe\n`.scratch/angular-seitenbausteine/issues/03-stoerer.md`): er umschließt Hero UND\nSet gemeinsam, das könnte diese Komponente als internes Detail nicht leisten, ohne\ndas Hero-Bild selbst zu kennen.\n\n**Entscheidung — `<li>` ohne strukturelles Wrapper-Element.** `.stoerer-list` ist\nein `<ul>`; nach HTML-AAM bekommt ein `<li>` die implizite Rolle `listitem` nur als\ndirektes Kind eines `<ul>`/`<ol>`/`<menu>` — Screenreader kündigen die Liste sonst\nnicht mit Anzahl an. Ein einfaches `<ng-content>` hätte `<cds-stoerer>` selbst\nzwischen `<ul>` und `<li>` geschoben (kein direktes Kind mehr), und ein\n`display:contents`-Wrapper verbietet sich als neues CSS (ADR-0001). Gelöst über\ndasselbe Muster, das `cds-area-tabs`/`cds-area-tab` bereits für „projizierte\nKind-Komponente, deren Markup an einer selbst bestimmten Stelle im eigenen Template\nlandet“ etabliert (`area-tabs/area-tabs.component.ts`): `cds-stoerer` rendert nicht\nin sich selbst, sondern in ein `<ng-template>` und liefert es als `TemplateRef`\n(`stoerer.component.ts`). Diese Komponente liest die projizierten `<cds-stoerer>`\nüber `contentChildren()` und setzt ihr Template per `ngTemplateOutlet` direkt in\nein selbst gerendertes `<li>` ein. Im laufenden Storybook geprüft (`Komponenten/Hero/\nStörer` → Story „Interaktiv“, DOM live im Browser per `querySelector` abgefragt,\nzusätzlich in der Play-Funktion der Story gepinnt): `<ul class=\"stoerer-list\">`\nhat ausschließlich `<li>` als direkte Kinder (`ul.stoerer-list > *` → `[\"LI\",\"LI\"]`),\n`<cds-stoerer>` selbst taucht im gerenderten DOM nirgends auf — Angular hängt\nunprojizierten Content ohne passendes `<ng-content>`-Ziel gar nicht erst ein, exakt\nwie bei `<cds-area-tab>` innerhalb von `<cds-area-tabs>`. Ein `items`-Array (die im\nTicket vermerkte Alternative) hätte denselben DOM-Baum ergeben, aber den\nKonsumenten gezwungen, das projizierte Icon jeder Kachel in ein Datenobjekt\numzuziehen; mit dieser Lösung bleibt die deklarative, Content-projizierende API\nerhalten.\n\nVerwendungsguidance dieser Gruppe: siehe Hero-Bild (`komponenten-hero-hero-bild--verwendung`).",
609
+ "description": "StoererSet (cds-stoerer-set) — Wrapper um `.stoerer-set` / `.stoerer-list` aus\ncss/components.css (css/components.css:926–974): das Set aus ein bis drei\n`<cds-stoerer>`-Kacheln, das auf der Startseite oben rechts über dem Hero-Bild\nliegt. Der Positionsrahmen `.stoerer-hero` (umschließt Hero-Bild UND Set,\n`container-type:inline-size` für die Container-Query-Schwelle) ist bewusst NICHT\nTeil dieser Komponente, sondern bleibt beim Konsumenten: er umschließt Hero UND\nSet gemeinsam, das könnte diese Komponente als internes Detail nicht leisten, ohne\ndas Hero-Bild selbst zu kennen.\n\n**Entscheidung — `<li>` ohne strukturelles Wrapper-Element.** `.stoerer-list` ist\nein `<ul>`; nach HTML-AAM bekommt ein `<li>` die implizite Rolle `listitem` nur als\ndirektes Kind eines `<ul>`/`<ol>`/`<menu>` — Screenreader kündigen die Liste sonst\nnicht mit Anzahl an. Ein einfaches `<ng-content>` hätte `<cds-stoerer>` selbst\nzwischen `<ul>` und `<li>` geschoben (kein direktes Kind mehr), und ein\n`display:contents`-Wrapper verbietet sich als neues CSS (ADR-0001). Gelöst über\ndasselbe Muster, das `cds-area-tabs`/`cds-area-tab` bereits für „projizierte\nKind-Komponente, deren Markup an einer selbst bestimmten Stelle im eigenen Template\nlandet“ etabliert (`area-tabs/area-tabs.component.ts`): `cds-stoerer` rendert nicht\nin sich selbst, sondern in ein `<ng-template>` und liefert es als `TemplateRef`\n(`stoerer.component.ts`). Diese Komponente liest die projizierten `<cds-stoerer>`\nüber `contentChildren()` und setzt ihr Template per `ngTemplateOutlet` direkt in\nein selbst gerendertes `<li>` ein. Im laufenden Storybook geprüft (`Komponenten/Hero/\nStörer` → Story „Interaktiv“, DOM live im Browser per `querySelector` abgefragt,\nzusätzlich in der Play-Funktion der Story gepinnt): `<ul class=\"stoerer-list\">`\nhat ausschließlich `<li>` als direkte Kinder (`ul.stoerer-list > *` → `[\"LI\",\"LI\"]`),\n`<cds-stoerer>` selbst taucht im gerenderten DOM nirgends auf — Angular hängt\nunprojizierten Content ohne passendes `<ng-content>`-Ziel gar nicht erst ein, exakt\nwie bei `<cds-area-tab>` innerhalb von `<cds-area-tabs>`. Ein `items`-Array (die\nerwogene Alternative) hätte denselben DOM-Baum ergeben, aber den\nKonsumenten gezwungen, das projizierte Icon jeder Kachel in ein Datenobjekt\numzuziehen; mit dieser Lösung bleibt die deklarative, Content-projizierende API\nerhalten.\n\nVerwendungsguidance dieser Gruppe: siehe Hero-Bild (`komponenten-hero-hero-bild--verwendung`).",
610
610
  "docgen": {
611
611
  "$ref": "../services/core/docgen/komponenten-hero-störer.json#/components/komponenten-hero-störer"
612
612
  },
@@ -703,7 +703,7 @@
703
703
  "seitenmuster-wissensbeitrag-article-header": {
704
704
  "id": "seitenmuster-wissensbeitrag-article-header",
705
705
  "name": "ArticleHeaderComponent",
706
- "description": "ArticleHeader (`cds-article-header`) — Wrapper um `.article-header` aus\ncss/components.css (css/components.css:1486–1490, 1508–1519): der zentrierte Kopf\neines Wissensbeitrags (Breadcrumb, optionale Pill, H1, Lead, Meta-Strip). Elftes\nTicket der Seitenbausteine-Serie. Hauptvorlage ist\n`storybook-angular/src/docs/seitenmuster/wissensbeitrag.mdx`, Abschnitt „Article\nHeader“ — dort steht das für dieses Ticket verbindliche Markup samt Begründung,\nnicht nur das Mockup in `docs/index.html`.\n\n**Breadcrumb sitzt im Header — abweichend von der allgemeinen Navigationsregel,\nbewusst der Ticket-Vorlage folgend.** `docs/index.html:2423` dokumentiert für\nUnterseiten allgemein, wörtlich: „Den Breadcrumb nicht in einen zentrierten\n`.article-header` einbetten, sonst wird er mittig ausgerichtet und bekommt einen\nabweichenden Abstand zur Nav“ (Tabellenzeile „Nicht“ der Breadcrumb-Doku) — und\nausgezählt\nsind es genau die beiden EINZIGEN vollständigen Beispielseiten mit\n`.article-header` (Stellenanzeige `docs/index.html:11035`, Wissensbeitrag · KI\n`docs/index.html:15020`), die dieser Regel folgen: beide setzen den Breadcrumb in\neine EIGENE, linksbündige `.ep-section` VOR dem Header (`docs/index.html:\n11023–11031` bzw. `15010–15015`). Dem stehen 6 ISOLIERTE Doku-Demos gegenüber, die\nden Breadcrumb INNERHALB von `.article-header` zeigen: 5 kleine Referenz-Boxen in\nder Doku-Sektion selbst (`docs/index.html:7850`, `8304`, `8331`, `8361`, `8392` —\nohne Topnav/Hero/Footer, reine Bauteil-Illustrationen) plus das Code-Beispiel in\n`wissensbeitrag.mdx`. Das Ticket benennt genau diese Doku (nicht die\nBeispielseiten) als Hauptvorlage. Diese Komponente folgt der Ticket-Vorgabe\n(Breadcrumb als Input, im Header) und dokumentiert den Widerspruch als eigenen\nBefund\n(`.scratch/angular-seitenbausteine/issues/20-breadcrumb-in-oder-vor-article-header.md`).\nKonsumenten, die der allgemeinen Regel folgen wollen (wie die beiden echten\nBeispielseiten), lassen `breadcrumb` leer und setzen die Leiste selbst davor\n(siehe Story „Ohne Breadcrumb“).\n\n**Letzter Breadcrumb-Eintrag immer ohne Link, mit `aria-current=\"page\"`** — so vom\nTicket als Akzeptanzkriterium gefordert. Die beiden Wissensbeitrag-Inline-Beispiele\n(`docs/index.html:7850–7856`, `wissensbeitrag.mdx`) zeigen dafür nur 2 Einträge,\nBEIDE als `<a>`, ohne `aria-current` — eine zur Kürze vereinfachte Doku-Skizze,\nkein vollständiges Barrierefreiheits-Beispiel. Das volle, korrekte Muster (Trenner\ndekorativ, letzter Eintrag `<span aria-current=\"page\">` ohne Link) steht an\nanderer Stelle exakt so vor (`docs/index.html:2406–2412` allgemeine\nBreadcrumb-Doku, `docs/index.html:11023–11030` Stellenanzeige) und ist zusätzlich\neine explizite Akzeptanzbedingung des Tickets — die Komponente folgt deshalb\ndiesem Muster für den letzten Array-Eintrag, unabhängig von einem dort eventuell\ngesetzten `href`.\n\n**Keine bereichsgefärbte Breadcrumb-Verlinkung (`.t-{area}`).** Die\nInline-Beispiele färben ihren zweiten (dort letzten) Eintrag mit `.t-ki`/`.t-es`/\n`.t-wo`; das Ticket sieht dafür aber kein API-Feld vor (`{label, href?}`, kein\n`area` pro Eintrag), und eine Zuordnung „Bereichsfarbe auf den vorletzten\nEintrag“ wäre geraten, nicht aus der Doku ableitbar — zumal der gefärbte Eintrag\ndort ein Link ist, kein `aria-current`-Element, also nicht deckungsgleich mit dem\noben umgesetzten Muster. Bleibt bewusst außen vor.\n\n**Pill über `cds-pill`, kein direkt komponiertes `<span class=\"pill\">`.** Geprüft\nper `npx storybook tools docs show --id komponenten-chips-badges-pills-pill`:\n`PillComponent`s Host ist `<cds-pill>`, `.pill` sitzt eine Ebene darunter auf\neinem inneren `<span>` (anders als bei `FeaturedCardComponent`, wo genau das wegen\n`.card-featured-body>.pill`-Kindselektoren nicht ginge). `.article-header`/`.pill`\nhaben keinen solchen Kindselektor (css/components.css:1486–1507 geprüft) — die\nzusätzliche Ebene ist hier folgenlos, deshalb bleibt `cds-pill` als echte\nKomponente eingesetzt, keine Klassen-Duplikation nötig.\n\n**Pille rendert nur, wenn `area` UND `pill` gesetzt sind — kein erfundener\nDefault.** `PillComponent.area` ist als `CdsArea` mit einem eigenen Default\n(`'ki'`) typisiert; diese Komponente führt `area` dagegen als `CdsArea | undefined`\n(keine Konfigurations-Vorgabe laut Ticket-API). Ein erster Entwurf reichte\n`area()` per `as CdsArea`-Cast durch, damit ein `undefined`-Binding\n`cds-pill`s eigenen Default überschreiben kann — eine Typ-Lüge, um einen Fall\nabzudecken, der beim Nachzählen gar nicht vorkommt: **alle 9** `.pill`-Vorkommen\nim Article-Header-Kontext (`docs/index.html:7856, 7912–7914, 8310, 8337, 8367,\n8398, 15021`) tragen ein `data-area`, keines ohne Bereich. Die Pille rendert\ndeshalb nur innerhalb von `@if (area(); as pillArea)` (narrowt `CdsArea |\nundefined` sauber auf `CdsArea`, ohne Cast) — ist `pill()` gesetzt, `area()`\naber nicht, bleibt die Pille schlicht weg, statt eine geratene Bereichsfarbe zu\nzeigen. Gepinnt in der Story „Pille ohne Bereich“.\n\n**`pillAriaLabel`, wie bei `FeaturedCardComponent`.** `PillComponent`s eigener\nDefault („Bereich `<label>`“) passt nur, wenn die Pille einen Bereichsnamen trägt\n(Stellenanzeige, `docs/index.html:11036`: „Bereich Effektive Software“). Im\nWissensbeitrag trägt die Pille dagegen die Lesezeit (`wissensbeitrag.mdx`,\nAbschnitt „Pill · Lesezeit“: `aria-label=\"Lesezeit 8 Minuten\"`) — ein Text, den\ndie Komponente nicht zuverlässig aus einem freien `pill`-String wie\n„8 min Lesezeit“ herleiten kann (keine Zahl zu parsen, keine Garantie, dass der\nText überhaupt eine Lesezeit ist). `pillAriaLabel` bleibt deshalb optional, ohne\neigenen Default: ungesetzt geht `undefined` direkt an `cds-pill`s `ariaLabel`\ndurch, das dann auf seinen eigenen Default („Bereich `<pill>`“) zurückfällt —\nkeine Logik-Duplikation nötig, weil hier die echte `PillComponent` verwendet\nwird, keine Klassen-Nachbildung.\n\n**`date`/`dateLabel` getrennt, ANDERS als `cds-stoerer`.** `cds-stoerer`\nformatiert ein reines ISO-Datum selbst nach `de-DE`\n(`stoerer.component.ts:formattedDate`, inkl. manuellem Aufbau der\n`Date`-Komponenten gegen den UTC-Mitternacht-Fallstrick von `new Date(iso)`).\nDiese Komponente tut das NICHT: `date` liefert nur den maschinenlesbaren\n`datetime`-Wert, `dateLabel` die sichtbare Schreibweise — vom Ticket ausdrücklich\nals zwei getrennte Inputs vorgegeben. Begründung der Abweichung: Erstens deckt\nsich das mit jeder realen Instanz (6 von 6 `.article-meta-date`-Vorkommen tragen\neinen redaktionell gesetzten Text wie „13. Mai 2026“, keinen von\n`Intl.DateTimeFormat` erzeugten) — anders als der Störer, dessen Kacheln NUR ein\nISO-Datum ohne separaten Text führen. Zweitens umgeht die Trennung den\nZeitzonen-Fallstrick vollständig, statt ihn zu beheben: ohne eigene Formatierung\nkonstruiert diese Komponente nie ein `Date`-Objekt aus `date()`, die\nUTC-Mitternacht-Falle kann hier gar nicht erst auftreten. Ist `dateLabel` leer,\nzeigt die Komponente `date()` unverändert als sichtbaren Text (bewusst als\nISO-Form, nicht geraten formatiert) — in der Story „Interaktiv“ als Warnfall extra\nbenannt, damit niemand das versehentlich auf eine Produktionsseite bringt.\n\n**Meta-Strip nur, wenn es etwas zu zeigen gibt.** `authorName`/`date` sind laut\nTicket-API Beiwerk (Default `''`); ausgezählt haben alle 6 realen\n`.article-meta`-Vorkommen sowohl Autor als auch Datum, eine Stellenanzeige\n(`docs/index.html:11035`) nutzt `.article-header` dagegen KOMPLETT OHNE\n`.article-meta` (dort ein `<dl>` aus Job-Fakten statt Autor/Datum) — dieser\nabweichenden Verwendung folgt diese Komponente nicht (sie hat in der Ticket-API\nkeinen Platz), sie belegt aber, dass ein leerer Meta-Strip ein realer Fall ist.\n`.article-meta` (und darin `.article-meta-author`/`.article-meta-sep`) rendern\ndeshalb nur, wenn `authorName()` bzw. `date()` tatsächlich etwas liefern; der\nTrenner nur, wenn beide Seiten vorhanden sind.\n\n**Avatar projiziert über `[cdsAvatar]`, PLUS `cds-avatar-stack`.** Die\nTicket-Skizze nennt nur `<ng-content select=\"[cdsAvatar]\">`; ergänzt um den\nTag-Selektor `cds-avatar-stack`, weil `AvatarStackComponent` (siehe dort) ein\nElement-Selektor ist, kein `[cdsAvatar]`-Attribut trägt und sonst nicht in den\nMeta-Strip projiziert werden könnte — nötig für die vom Ticket geforderte Story\n„Mehrere Autor:innen“.\n\n**Host als Block.** `.article-header` sitzt am Host und setzt nur\n`max-width:880px;margin:0 auto`. An einem unbekannten, also `display:inline`\ngerenderten Element greift beides nicht, die Block-Kinder liefen über die\n880 px hinaus. `:host{display:block}` wie bei `cds-hero-image` und\n`cds-footer-main`; Story „Breite begrenzt“ pinnt das.",
706
+ "description": "ArticleHeader (`cds-article-header`) — Wrapper um `.article-header` aus\ncss/components.css (css/components.css:1486–1490, 1508–1519): der zentrierte Kopf\neines Wissensbeitrags (Breadcrumb, optionale Pill, H1, Lead, Meta-Strip).\nHauptvorlage ist\n`storybook-angular/src/docs/seitenmuster/wissensbeitrag.mdx`, Abschnitt „Article\nHeader“ — dort steht das verbindliche Markup samt Begründung,\nnicht nur das Mockup in `docs/index.html`.\n\n**Breadcrumb sitzt im Header — abweichend von der allgemeinen Navigationsregel,\nbewusst der Hauptvorlage folgend.** `docs/index.html:2423` dokumentiert für\nUnterseiten allgemein, wörtlich: „Den Breadcrumb nicht in einen zentrierten\n`.article-header` einbetten, sonst wird er mittig ausgerichtet und bekommt einen\nabweichenden Abstand zur Nav“ (Tabellenzeile „Nicht“ der Breadcrumb-Doku) — und\nausgezählt\nsind es genau die beiden EINZIGEN vollständigen Beispielseiten mit\n`.article-header` (Stellenanzeige `docs/index.html:11035`, Wissensbeitrag · KI\n`docs/index.html:15020`), die dieser Regel folgen: beide setzen den Breadcrumb in\neine EIGENE, linksbündige `.ep-section` VOR dem Header (`docs/index.html:\n11023–11031` bzw. `15010–15015`). Dem stehen 6 ISOLIERTE Doku-Demos gegenüber, die\nden Breadcrumb INNERHALB von `.article-header` zeigen: 5 kleine Referenz-Boxen in\nder Doku-Sektion selbst (`docs/index.html:7850`, `8304`, `8331`, `8361`, `8392` —\nohne Topnav/Hero/Footer, reine Bauteil-Illustrationen) plus das Code-Beispiel in\n`wissensbeitrag.mdx`. Diese Komponente folgt bewusst dieser Doku, nicht den\nBeispielseiten (Breadcrumb als Input, im Header), und dokumentiert den\nWiderspruch als eigenen Befund.\nKonsumenten, die der allgemeinen Regel folgen wollen (wie die beiden echten\nBeispielseiten), lassen `breadcrumb` leer und setzen die Leiste selbst davor\n(siehe Story „Ohne Breadcrumb“).\n\n**Letzter Breadcrumb-Eintrag immer ohne Link, mit `aria-current=\"page\"`.** Die\nbeiden Wissensbeitrag-Inline-Beispiele\n(`docs/index.html:7850–7856`, `wissensbeitrag.mdx`) zeigen dafür nur 2 Einträge,\nBEIDE als `<a>`, ohne `aria-current` — eine zur Kürze vereinfachte Doku-Skizze,\nkein vollständiges Barrierefreiheits-Beispiel. Das volle, korrekte Muster (Trenner\ndekorativ, letzter Eintrag `<span aria-current=\"page\">` ohne Link) steht an\nanderer Stelle exakt so vor (`docs/index.html:2406–2412` allgemeine\nBreadcrumb-Doku, `docs/index.html:11023–11030` Stellenanzeige) — die Komponente\nfolgt deshalb diesem Muster für den letzten Array-Eintrag, unabhängig von einem\ndort eventuell gesetzten `href`.\n\n**Keine bereichsgefärbte Breadcrumb-Verlinkung (`.t-{area}`).** Die\nInline-Beispiele färben ihren zweiten (dort letzten) Eintrag mit `.t-ki`/`.t-es`/\n`.t-wo`; die API sieht dafür aber kein Feld vor (`{label, href?}`, kein\n`area` pro Eintrag), und eine Zuordnung „Bereichsfarbe auf den vorletzten\nEintrag“ wäre geraten, nicht aus der Doku ableitbar — zumal der gefärbte Eintrag\ndort ein Link ist, kein `aria-current`-Element, also nicht deckungsgleich mit dem\noben umgesetzten Muster. Bleibt bewusst außen vor.\n\n**Pill über `cds-pill`, kein direkt komponiertes `<span class=\"pill\">`.** Geprüft\nper `npx storybook tools docs show --id komponenten-chips-badges-pills-pill`:\n`PillComponent`s Host ist `<cds-pill>`, `.pill` sitzt eine Ebene darunter auf\neinem inneren `<span>` (anders als bei `FeaturedCardComponent`, wo genau das wegen\n`.card-featured-body>.pill`-Kindselektoren nicht ginge). `.article-header`/`.pill`\nhaben keinen solchen Kindselektor (css/components.css:1486–1507 geprüft) — die\nzusätzliche Ebene ist hier folgenlos, deshalb bleibt `cds-pill` als echte\nKomponente eingesetzt, keine Klassen-Duplikation nötig.\n\n**Pille rendert nur, wenn `area` UND `pill` gesetzt sind — kein erfundener\nDefault.** `PillComponent.area` ist als `CdsArea` mit einem eigenen Default\n(`'ki'`) typisiert; diese Komponente führt `area` dagegen als `CdsArea | undefined`\n(kein eigener Default vorgegeben). Ein erster Entwurf reichte\n`area()` per `as CdsArea`-Cast durch, damit ein `undefined`-Binding\n`cds-pill`s eigenen Default überschreiben kann — eine Typ-Lüge, um einen Fall\nabzudecken, der beim Nachzählen gar nicht vorkommt: **alle 9** `.pill`-Vorkommen\nim Article-Header-Kontext (`docs/index.html:7856, 7912–7914, 8310, 8337, 8367,\n8398, 15021`) tragen ein `data-area`, keines ohne Bereich. Die Pille rendert\ndeshalb nur innerhalb von `@if (area(); as pillArea)` (narrowt `CdsArea |\nundefined` sauber auf `CdsArea`, ohne Cast) — ist `pill()` gesetzt, `area()`\naber nicht, bleibt die Pille schlicht weg, statt eine geratene Bereichsfarbe zu\nzeigen. Gepinnt in der Story „Pille ohne Bereich“.\n\n**`pillAriaLabel`, wie bei `FeaturedCardComponent`.** `PillComponent`s eigener\nDefault („Bereich `<label>`“) passt nur, wenn die Pille einen Bereichsnamen trägt\n(Stellenanzeige, `docs/index.html:11036`: „Bereich Effektive Software“). Im\nWissensbeitrag trägt die Pille dagegen die Lesezeit (`wissensbeitrag.mdx`,\nAbschnitt „Pill · Lesezeit“: `aria-label=\"Lesezeit 8 Minuten\"`) — ein Text, den\ndie Komponente nicht zuverlässig aus einem freien `pill`-String wie\n„8 min Lesezeit“ herleiten kann (keine Zahl zu parsen, keine Garantie, dass der\nText überhaupt eine Lesezeit ist). `pillAriaLabel` bleibt deshalb optional, ohne\neigenen Default: ungesetzt geht `undefined` direkt an `cds-pill`s `ariaLabel`\ndurch, das dann auf seinen eigenen Default („Bereich `<pill>`“) zurückfällt —\nkeine Logik-Duplikation nötig, weil hier die echte `PillComponent` verwendet\nwird, keine Klassen-Nachbildung.\n\n**`date`/`dateLabel` getrennt, ANDERS als `cds-stoerer`.** `cds-stoerer`\nformatiert ein reines ISO-Datum selbst nach `de-DE`\n(`stoerer.component.ts:formattedDate`, inkl. manuellem Aufbau der\n`Date`-Komponenten gegen den UTC-Mitternacht-Fallstrick von `new Date(iso)`).\nDiese Komponente tut das NICHT: `date` liefert nur den maschinenlesbaren\n`datetime`-Wert, `dateLabel` die sichtbare Schreibweise — bewusst als zwei\ngetrennte Inputs. Begründung der Abweichung: Erstens deckt\nsich das mit jeder realen Instanz (6 von 6 `.article-meta-date`-Vorkommen tragen\neinen redaktionell gesetzten Text wie „13. Mai 2026“, keinen von\n`Intl.DateTimeFormat` erzeugten) — anders als der Störer, dessen Kacheln NUR ein\nISO-Datum ohne separaten Text führen. Zweitens umgeht die Trennung den\nZeitzonen-Fallstrick vollständig, statt ihn zu beheben: ohne eigene Formatierung\nkonstruiert diese Komponente nie ein `Date`-Objekt aus `date()`, die\nUTC-Mitternacht-Falle kann hier gar nicht erst auftreten. Ist `dateLabel` leer,\nzeigt die Komponente `date()` unverändert als sichtbaren Text (bewusst als\nISO-Form, nicht geraten formatiert) — in der Story „Interaktiv“ als Warnfall extra\nbenannt, damit niemand das versehentlich auf eine Produktionsseite bringt.\n\n**Meta-Strip nur, wenn es etwas zu zeigen gibt.** `authorName`/`date` sind bewusst\nBeiwerk (Default `''`); ausgezählt haben alle 6 realen\n`.article-meta`-Vorkommen sowohl Autor als auch Datum, eine Stellenanzeige\n(`docs/index.html:11035`) nutzt `.article-header` dagegen KOMPLETT OHNE\n`.article-meta` (dort ein `<dl>` aus Job-Fakten statt Autor/Datum) — dieser\nabweichenden Verwendung folgt diese Komponente nicht (dafür ist in der API kein\nFeld vorgesehen), sie belegt aber, dass ein leerer Meta-Strip ein realer Fall ist.\n`.article-meta` (und darin `.article-meta-author`/`.article-meta-sep`) rendern\ndeshalb nur, wenn `authorName()` bzw. `date()` tatsächlich etwas liefern; der\nTrenner nur, wenn beide Seiten vorhanden sind.\n\n**Avatar projiziert über `[cdsAvatar]`, PLUS `cds-avatar-stack`.** Der\nursprüngliche Entwurf nennt nur `<ng-content select=\"[cdsAvatar]\">`; ergänzt um den\nTag-Selektor `cds-avatar-stack`, weil `AvatarStackComponent` (siehe dort) ein\nElement-Selektor ist, kein `[cdsAvatar]`-Attribut trägt und sonst nicht in den\nMeta-Strip projiziert werden könnte — nötig für die Story\n„Mehrere Autor:innen“.\n\n**Host als Block.** `.article-header` sitzt am Host und setzt nur\n`max-width:880px;margin:0 auto`. An einem unbekannten, also `display:inline`\ngerenderten Element greift beides nicht, die Block-Kinder liefen über die\n880 px hinaus. `:host{display:block}` wie bei `cds-hero-image` und\n`cds-footer-main`; Story „Breite begrenzt“ pinnt das.",
707
707
  "docgen": {
708
708
  "$ref": "../services/core/docgen/seitenmuster-wissensbeitrag-article-header.json#/components/seitenmuster-wissensbeitrag-article-header"
709
709
  },
@@ -714,7 +714,7 @@
714
714
  "seitenmuster-wissensbeitrag-avatar": {
715
715
  "id": "seitenmuster-wissensbeitrag-avatar",
716
716
  "name": "AvatarComponent",
717
- "description": "Avatar (`div[cdsAvatar]`) — Wrapper um `.article-avatar` aus css/components.css\n(css/components.css:1520–1536): der Initialen-/Foto-Kreis im Meta-Strip eines\nWissensbeitrags (`.article-meta-author`), in der Author-Card (`-lg`) und im\nTeam-Tile-Grid (`-xl`). Elftes Ticket der Seitenbausteine-Serie.\n\n**Attributselektor (ADR-0008), gemessen an der Geschwister-Kette.** Ausgezählt in\n`docs/index.html`: 8 der 11 Instanzen der Grundgröße sitzen als direkte, adjazente\nGeschwister in einer von 3 `.article-avatar-stack`-Kacheln (2 Avatare in Zeilen\n8343–8344, je 3 in Zeilen 8373–8375 und 8404–8406) — genau dort greift\n`.article-avatar-stack .article-avatar + .article-avatar`\n(css/components.css:1540–1541), eine Geschwister-Kette (Kriterium `spec.md` 2b).\nEin Element-Selektor `cds-avatar`, der `.article-avatar` auf einem INNEREN Element\nrendert (wie `cds-facts` es mit `<dl>` tut, weil `<dl>` ein Custom Element nicht\nannehmen kann), würde diese Kette brechen: die beiden `.article-avatar`-Knoten\nlägen dann je eine Ebene UNTER ihrem eigenen `<cds-avatar>`-Host und wären keine\nGeschwister mehr — `+` verlangt denselben Elternknoten, egal wie dicht die Hosts\nselbst nebeneinanderstehen. Eine zweite, ebenfalls tragfähige Variante\n(Element-Selektor MIT `host: { class: 'article-avatar' }`, ohne innere\nWrapper-Ebene) hätte dieselbe Geschwister-Kette ebenso erfüllt — verworfen zugunsten\ndes Attributselektors nur wegen des zweiten, unabhängigen Grundes unten, nicht weil\nsie die Kette nicht gelöst hätte.\n\n**Gemessen, nicht nur hergeleitet.** Story „Avatar-Stapel“ (Play-Funktion) rendert\ndrei `div[cdsAvatar]` in einem `cds-avatar-stack` und misst\n`getBoundingClientRect()` der drei `.article-avatar`-Knoten: bei 40 px Kachelbreite\nund `margin-left:-12px` (css/components.css:1541) überlappen der zweite und dritte\nKreis den jeweils vorherigen um 12 px (Differenz der `left`-Koordinaten = 28 px,\nnicht 40 px) — plus Screenshot-Baseline (`visual-snapshots/…avatar-stapel.png`) als\nvisuelle Bestätigung, nicht nur die Zahl.\n\n**Zweiter, unabhängiger Grund: Wiederverwendung als Projektions-Selektor.**\n`cds-article-header` projiziert den Avatar über\n`<ng-content select=\"[cdsAvatar]\">` (siehe dessen Klassendoku) — dasselbe Attribut,\ndas hier den Host selektiert, dient dem Elternteil zugleich als\nContent-Projection-Selektor. Ein Element-Selektor bräuchte dafür eine zusätzliche,\neigens erfundene Marker-Klasse; der Attributselektor liefert die Zielscheibe für\nbeide Zwecke aus einer Hand, ohne dass der Konsument zwei Attribute schreiben muss.\n\n**Initialen werden aus `name` abgeleitet, kein eigener Input** (Ticket-Vorgabe):\nerstes Zeichen des ersten und des letzten durch Whitespace getrennten Worts, groß\n— ein einzelnes Wort liefert dessen erste zwei Zeichen. Zwei Inputs für dieselbe\nInformation (Name UND Initialen) könnten auseinanderlaufen, sobald nur eines\ngepflegt wird.\n\n**`aria-hidden=\"true\"` fest verdrahtet, kein Input.** Ausgezählt: von 53\n`.article-avatar`-Instanzen (11 Grundgröße + 17 `-lg` + 25 `-xl`) tragen 45 das\nAttribut direkt am Element; die restlichen 8 sind genau die Grundgrößen-Avatare\nINNERHALB von `.article-avatar-stack`, wo bereits der Stack-Container selbst\n`aria-hidden=\"true\"` trägt (siehe `AvatarStackComponent`) — dort wäre ein zweites\n`aria-hidden` am Kind redundant, aber unschädlich: verschachteltes `aria-hidden`\nändert nichts am Ergebnis, der Teilbaum ist so oder so für Assistenztechnologie\nunsichtbar. Der Avatar trägt es deshalb immer: 100 % Deckung mit dem Mockup im\nAlleinstand, harmlose Redundanz im Stapel.",
717
+ "description": "Avatar (`div[cdsAvatar]`) — Wrapper um `.article-avatar` aus css/components.css\n(css/components.css:1520–1536): der Initialen-/Foto-Kreis im Meta-Strip eines\nWissensbeitrags (`.article-meta-author`), in der Author-Card (`-lg`) und im\nTeam-Tile-Grid (`-xl`).\n\n**Attributselektor (ADR-0008), gemessen an der Geschwister-Kette.** Ausgezählt in\n`docs/index.html`: 8 der 11 Instanzen der Grundgröße sitzen als direkte, adjazente\nGeschwister in einer von 3 `.article-avatar-stack`-Kacheln (2 Avatare in Zeilen\n8343–8344, je 3 in Zeilen 8373–8375 und 8404–8406) — genau dort greift\n`.article-avatar-stack .article-avatar + .article-avatar`\n(css/components.css:1540–1541), eine Geschwister-Kette.\nEin Element-Selektor `cds-avatar`, der `.article-avatar` auf einem INNEREN Element\nrendert (wie `cds-facts` es mit `<dl>` tut, weil `<dl>` ein Custom Element nicht\nannehmen kann), würde diese Kette brechen: die beiden `.article-avatar`-Knoten\nlägen dann je eine Ebene UNTER ihrem eigenen `<cds-avatar>`-Host und wären keine\nGeschwister mehr — `+` verlangt denselben Elternknoten, egal wie dicht die Hosts\nselbst nebeneinanderstehen. Eine zweite, ebenfalls tragfähige Variante\n(Element-Selektor MIT `host: { class: 'article-avatar' }`, ohne innere\nWrapper-Ebene) hätte dieselbe Geschwister-Kette ebenso erfüllt — verworfen zugunsten\ndes Attributselektors nur wegen des zweiten, unabhängigen Grundes unten, nicht weil\nsie die Kette nicht gelöst hätte.\n\n**Gemessen, nicht nur hergeleitet.** Story „Avatar-Stapel“ (Play-Funktion) rendert\ndrei `div[cdsAvatar]` in einem `cds-avatar-stack` und misst\n`getBoundingClientRect()` der drei `.article-avatar`-Knoten: bei 40 px Kachelbreite\nund `margin-left:-12px` (css/components.css:1541) überlappen der zweite und dritte\nKreis den jeweils vorherigen um 12 px (Differenz der `left`-Koordinaten = 28 px,\nnicht 40 px) — plus Screenshot-Baseline (`visual-snapshots/…avatar-stapel.png`) als\nvisuelle Bestätigung, nicht nur die Zahl.\n\n**Zweiter, unabhängiger Grund: Wiederverwendung als Projektions-Selektor.**\n`cds-article-header` projiziert den Avatar über\n`<ng-content select=\"[cdsAvatar]\">` (siehe dessen Klassendoku) — dasselbe Attribut,\ndas hier den Host selektiert, dient dem Elternteil zugleich als\nContent-Projection-Selektor. Ein Element-Selektor bräuchte dafür eine zusätzliche,\neigens erfundene Marker-Klasse; der Attributselektor liefert die Zielscheibe für\nbeide Zwecke aus einer Hand, ohne dass der Konsument zwei Attribute schreiben muss.\n\n**Initialen werden aus `name` abgeleitet, kein eigener Input:**\nerstes Zeichen des ersten und des letzten durch Whitespace getrennten Worts, groß\n— ein einzelnes Wort liefert dessen erste zwei Zeichen. Zwei Inputs für dieselbe\nInformation (Name UND Initialen) könnten auseinanderlaufen, sobald nur eines\ngepflegt wird.\n\n**`aria-hidden=\"true\"` fest verdrahtet, kein Input.** Ausgezählt: von 53\n`.article-avatar`-Instanzen (11 Grundgröße + 17 `-lg` + 25 `-xl`) tragen 45 das\nAttribut direkt am Element; die restlichen 8 sind genau die Grundgrößen-Avatare\nINNERHALB von `.article-avatar-stack`, wo bereits der Stack-Container selbst\n`aria-hidden=\"true\"` trägt (siehe `AvatarStackComponent`) — dort wäre ein zweites\n`aria-hidden` am Kind redundant, aber unschädlich: verschachteltes `aria-hidden`\nändert nichts am Ergebnis, der Teilbaum ist so oder so für Assistenztechnologie\nunsichtbar. Der Avatar trägt es deshalb immer: 100 % Deckung mit dem Mockup im\nAlleinstand, harmlose Redundanz im Stapel.",
718
718
  "docgen": {
719
719
  "$ref": "../services/core/docgen/seitenmuster-wissensbeitrag-avatar.json#/components/seitenmuster-wissensbeitrag-avatar"
720
720
  },
@@ -725,7 +725,7 @@
725
725
  "seitenmuster-wissensbeitrag-inhaltsverzeichnis": {
726
726
  "id": "seitenmuster-wissensbeitrag-inhaltsverzeichnis",
727
727
  "name": "ArticleTocComponent",
728
- "description": "ArticleToc (`cds-article-toc`) — Wrapper um `.article-toc*` aus css/components.css\n(css/components.css:1577–1589): das aufklappbare Inhaltsverzeichnis am Anfang eines\nWissensbeitrags. Zwölftes Ticket der Seitenbausteine-Serie („Artikel-Körper“).\nAusgezählt: genau 2 reale Vorkommen, beide mit 5 Einträgen und identischem\nsichtbarem Text „Inhalt“ plus identischem `aria-label`\n(`docs/index.html:7948–7960`, Doku-Sektion mit Platzhalter-Hrefs `#gt-article-toc`;\n`docs/index.html:15049–15061`, reale Beispielseite Wissensbeitrag · KI mit echten\nAbschnittsankern wie `#wb-ki-demo`).\n\n**Element-Selektor, ADR-0008-Standardfall.** `.article-toc` sitzt in beiden\nVorkommen als gewöhnlicher Block-Nachfahre direkt in `.article-body`\n(`docs/index.html:7950`, `15051` je eine Ebene über dem `<details>`) — kein\nGrid-/Flex-Kind, keine Layout-Klasse (`col-*`) vom Konsumenten, kein\nGeschwister-Kombinator (`.article-toc` kommt in keiner CSS-Regel mit `+`/`~`\nvor), kein Tag-Wechsel (immer `<details>`). Keines der drei ADR-0008-Kriterien\ngreift; ein Attributselektor brächte hier nichts. Die einzige direkte\nKind-Regel von `.article-body` (`.article-body > p`, css/components.css:1566)\nzielt auf `p`, nicht auf `details` — ein `<cds-article-toc>`-Host zwischen\n`.article-body` und dem `<details>` bricht deshalb keine Selektorkette.\n\n**Natives `<details>`/`<summary>`, kein nachgebautes Disclosure** (Ticket-Vorgabe,\nAkzeptanzkriterium): Tastaturbedienung (Enter/Space auf `<summary>`) und\nToggle-Zustand kommen vollständig vom Browser, das CSS hängt direkt an `[open]`\n(`.article-toc[open] .article-toc-caret`, css/components.css:1583). `<details\nclass=\"article-toc\">` sitzt deshalb im TEMPLATE, nicht am Host — derselbe Grund\nwie bei `cds-compare`s `<details class=\"ep-compare\">` (`compare.component.ts`):\nein `<cds-article-toc>`-Host ist ein unbekanntes Custom Element und kann native\nDisclosure-Semantik nicht annehmen.\n\n**Zweites Vorkommen des `<details>`-mit-drehendem-Caret-Musters, nicht\nzusammengezogen mit `cds-compare` (ADR-0007 §5).** Beide Bauteile teilen die\nForm (natives `<details>`, SVG-Caret, `[open]`-Rotation über\n`transition:transform`), aber nicht die Aufgabe: `cds-compare` trägt eine\nVergleichstabelle mit `columns`/`rows`/`toggled`-Output, dieses Bauteil trägt\neine reine Link-Liste ohne Output (siehe API-Vorgabe des Tickets, die für dieses\nBauteil bewusst keinen `toggled` vorsieht). Zwei gleiche Stellen sind ein\nZufall, kein Muster — eine gemeinsame Basisklasse für zwei Vorkommen hätte nur\ndas `<details>`+Caret-Skelett gebündelt und dabei entweder Verhalten\nvereinheitlicht (ein erzwungener `toggled`-Output ohne Zweck hier) oder so viel\nparametrisiert, dass nichts gewonnen wäre.\n\n**`aria-label` am `<summary>` ist fest verdrahtet, kein Input.** Ausgezählt:\nbeide realen Vorkommen tragen wortgleich\n`aria-label=\"Inhaltsverzeichnis ein- und ausklappen\"` — der sichtbare Text\n„Inhalt“ (`summary`-Input) allein wäre außerhalb des Beitragskontexts nicht\neindeutig, das `aria-label` schreibt deshalb aus, was das Element tut. Genau\ndieser Wortlaut aus dem Mockup übernommen, kein erfundener zugänglicher Name.\n\n**Keine Nummerierung im Markup.** Die sichtbaren Ziffern vor jedem Eintrag\nkommen aus `counter(toc)` auf `.article-toc-list li::before`\n(css/components.css:1586), nicht aus dem `<ol>`-Default (`list-style:none` setzt\nihn zurück) — die Komponente muss dafür nichts weiter tun als `<ol\nclass=\"article-toc-list\">` zu rendern.",
728
+ "description": "ArticleToc (`cds-article-toc`) — Wrapper um `.article-toc*` aus css/components.css\n(css/components.css:1577–1589): das aufklappbare Inhaltsverzeichnis am Anfang eines\nWissensbeitrags. Ausgezählt: genau 2 reale Vorkommen, beide mit 5 Einträgen und identischem\nsichtbarem Text „Inhalt“ plus identischem `aria-label`\n(`docs/index.html:7948–7960`, Doku-Sektion mit Platzhalter-Hrefs `#gt-article-toc`;\n`docs/index.html:15049–15061`, reale Beispielseite Wissensbeitrag · KI mit echten\nAbschnittsankern wie `#wb-ki-demo`).\n\n**Element-Selektor, ADR-0008-Standardfall.** `.article-toc` sitzt in beiden\nVorkommen als gewöhnlicher Block-Nachfahre direkt in `.article-body`\n(`docs/index.html:7950`, `15051` je eine Ebene über dem `<details>`) — kein\nGrid-/Flex-Kind, keine Layout-Klasse (`col-*`) vom Konsumenten, kein\nGeschwister-Kombinator (`.article-toc` kommt in keiner CSS-Regel mit `+`/`~`\nvor), kein Tag-Wechsel (immer `<details>`). Keines der drei ADR-0008-Kriterien\ngreift; ein Attributselektor brächte hier nichts. Die einzige direkte\nKind-Regel von `.article-body` (`.article-body > p`, css/components.css:1566)\nzielt auf `p`, nicht auf `details` — ein `<cds-article-toc>`-Host zwischen\n`.article-body` und dem `<details>` bricht deshalb keine Selektorkette.\n\n**Natives `<details>`/`<summary>`, kein nachgebautes Disclosure:** Tastaturbedienung\n(Enter/Space auf `<summary>`) und\nToggle-Zustand kommen vollständig vom Browser, das CSS hängt direkt an `[open]`\n(`.article-toc[open] .article-toc-caret`, css/components.css:1583). `<details\nclass=\"article-toc\">` sitzt deshalb im TEMPLATE, nicht am Host — derselbe Grund\nwie bei `cds-compare`s `<details class=\"ep-compare\">` (`compare.component.ts`):\nein `<cds-article-toc>`-Host ist ein unbekanntes Custom Element und kann native\nDisclosure-Semantik nicht annehmen.\n\n**Zweites Vorkommen des `<details>`-mit-drehendem-Caret-Musters, nicht\nzusammengezogen mit `cds-compare` (ADR-0007 §5).** Beide Bauteile teilen die\nForm (natives `<details>`, SVG-Caret, `[open]`-Rotation über\n`transition:transform`), aber nicht die Aufgabe: `cds-compare` trägt eine\nVergleichstabelle mit `columns`/`rows`/`toggled`-Output, dieses Bauteil trägt\neine reine Link-Liste ohne Output (die API sieht für dieses\nBauteil bewusst keinen `toggled` vor). Zwei gleiche Stellen sind ein\nZufall, kein Muster — eine gemeinsame Basisklasse für zwei Vorkommen hätte nur\ndas `<details>`+Caret-Skelett gebündelt und dabei entweder Verhalten\nvereinheitlicht (ein erzwungener `toggled`-Output ohne Zweck hier) oder so viel\nparametrisiert, dass nichts gewonnen wäre.\n\n**`aria-label` am `<summary>` ist fest verdrahtet, kein Input.** Ausgezählt:\nbeide realen Vorkommen tragen wortgleich\n`aria-label=\"Inhaltsverzeichnis ein- und ausklappen\"` — der sichtbare Text\n„Inhalt“ (`summary`-Input) allein wäre außerhalb des Beitragskontexts nicht\neindeutig, das `aria-label` schreibt deshalb aus, was das Element tut. Genau\ndieser Wortlaut aus dem Mockup übernommen, kein erfundener zugänglicher Name.\n\n**Keine Nummerierung im Markup.** Die sichtbaren Ziffern vor jedem Eintrag\nkommen aus `counter(toc)` auf `.article-toc-list li::before`\n(css/components.css:1586), nicht aus dem `<ol>`-Default (`list-style:none` setzt\nihn zurück) — die Komponente muss dafür nichts weiter tun als `<ol\nclass=\"article-toc-list\">` zu rendern.",
729
729
  "docgen": {
730
730
  "$ref": "../services/core/docgen/seitenmuster-wissensbeitrag-inhaltsverzeichnis.json#/components/seitenmuster-wissensbeitrag-inhaltsverzeichnis"
731
731
  },
@@ -736,7 +736,7 @@
736
736
  "seitenmuster-wissensbeitrag-callout": {
737
737
  "id": "seitenmuster-wissensbeitrag-callout",
738
738
  "name": "ArticleCalloutComponent",
739
- "description": "ArticleCallout (`cds-article-callout`) — Wrapper um `.article-callout*` aus\ncss/components.css (css/components.css:1591–1600): der bereichsgetönte\nAside-Block für Praxis-Beispiele und „In der Realität“-Einschübe im Lauftext\neines Wissensbeitrags. Zwölftes Ticket der Seitenbausteine-Serie\n(„Artikel-Körper“). Ausgezählt: 5 reale Vorkommen, je eines pro Bereich in der\nDoku-Sektion (`docs/index.html:8086–8115`, co/ki/es/wo) plus eines in der realen\nBeispielseite Wissensbeitrag · KI (`docs/index.html:15110–15114`).\n\n**Element-Selektor, ADR-0008-Standardfall.** `.article-callout` sitzt in allen 5\nVorkommen als gewöhnlicher Block-Nachfahre — die 4 Doku-Boxen je in einem\nschlichten `<div>` innerhalb eines `display:flex;flex-direction:column`-Stapels\n(kein Stretch-Bedarf in Spaltenrichtung), die reale Instanz direkt in\n`.article-body`. Keines ist ein Grid-/Flex-Kind mit Streckungsbedarf, keines\nträgt eine `col-*`-Klasse, `.article-callout` kommt in keiner CSS-Regel mit\neinem Geschwister-Kombinator (`+`/`~`) vor, das Tag variiert nicht (immer\n`<aside>`). Keines der drei ADR-0008-Kriterien greift. Die einzige direkte\nKind-Regel von `.article-body` (`.article-body > p`, css/components.css:1566)\nzielt auf `p`, nicht auf `aside` — ein `<cds-article-callout>`-Host zwischen\n`.article-body` und dem `<aside>` bricht deshalb keine Selektorkette.\n\n**Projizierte Absätze bleiben direkte Kinder von `.article-callout`, kein\n`<div>` um `<ng-content>`** (Ticket-Vorgabe: `.article-callout > p` ist ein\nKindselektor, css/components.css:1600). `<ng-content>` selbst fügt kein\nDOM-Element ein — es verschiebt nur die vom Konsumenten geschriebenen\n`<p>`-Elemente an ihre Stelle direkt im `<aside>`, eine Ebene unter dem\n`<cds-article-callout>`-Host, aber ohne zusätzlichen Knoten dazwischen. Geprüft\nim laufenden Storybook (Story „Interaktiv“, Play-Funktion):\n`aside.article-callout > p` liefert sowohl die Eyebrow als auch jeden\nprojizierten Absatz, `:scope > *` zählt keine fremde Zwischenebene.\n\n**`eyebrow` ist Beiwerk (Default `''`), `area` ist Konfiguration mit\nverteidigbarer Vorgabe (`'co'`).** Die Basisregel `.article-callout` selbst\n(ohne `[data-area]`) rendert bereits exakt wie `[data-area=\"co\"]`\n(`background:var(--co-50)`, `border-left-color:var(--co-500)`, beide Werte\nidentisch an beiden Stellen, css/components.css:1591–1592) — `'co'` ist damit\nkeine erfundene Markenfarbe, sondern der reale CSS-Fallback ohne Attribut.\nAnders als bei `cds-pill` (dessen `.pill` ohne `data-area` gar keine Füllfarbe\nhat) gibt es hier keinen sinnvollen „kein Bereich“-Zustand.\n\n**Bekannter CSS-Befund, nicht im Wrapper geflickt (ADR-0001):**\n`.article-callout-eyebrow` (Spezifität 0,1,0) verliert gegen\n`.article-callout > p` (Spezifität 0,1,1, css/components.css:1600), weil die\nEyebrow selbst ein `<p>` UND ein direktes Kind von `.article-callout` ist.\nGemessen in rohem Markup ohne Angular (Playwright/Chromium,\n`getComputedStyle`): die Eyebrow rendert in JEDEM Bereich mit `font-size:16px`\nstatt `12px` und `margin-bottom:0` statt `4px`; die Akzentfarbe geht zusätzlich\nverloren, wenn kein bereichsspezifischer Override existiert (`co` und „kein\n`data-area`“ zeigen `--tx-primary` statt `--co-700`). Diese Komponente\nreproduziert exakt die Klassen und die Struktur des Mockups und zeigt deshalb\ndenselben, vorbestehenden Fehler wie rohes HTML — siehe\n`.scratch/angular-seitenbausteine/issues/22-css-luecke-callout-eyebrow-spezifitaet.md`.\n\n**`aria-labelledby` auf `<aside>`, sobald `eyebrow` gesetzt ist — Zusatz zum\nMockup, keine CSS-Änderung.** `<aside>` hat implizit die Landmark-Rolle\n`complementary`; ein Wissensbeitrag mit mehr als einem Callout hat damit\nmehrere gleichnamige Landmarks ohne zugänglichen Namen. Gemessen mit\naxe-core an rohem Markup ohne Angular (zwei `<aside class=\"article-callout\">`\nohne Auszeichnung): `landmark-unique` schlägt fehl, unabhängig von dieser\nKomponente — das Mockup selbst kennt diese Auszeichnung nicht (siehe\n`.scratch/angular-seitenbausteine/issues/23-doku-luecke-callout-landmark-label.md`).\nDie Eyebrow ist bereits ein prägnanter, vom Redakteur gepflegter Kurztext\ngenau für diesen Zweck (z. B. „In der Praxis“) — sie bekommt deshalb eine\ngenerierte `id`, auf die `aria-labelledby` zeigt, sobald sie existiert. Ohne\nEyebrow bleibt das `<aside>` unbenannt wie im Mockup: ein zugänglicher Name\nließe sich sonst nur aus dem projizierten Absatztext raten, und das wäre eine\nerfundene Bezeichnung, keine reale.",
739
+ "description": "ArticleCallout (`cds-article-callout`) — Wrapper um `.article-callout*` aus\ncss/components.css (css/components.css:1591–1600): der bereichsgetönte\nAside-Block für Praxis-Beispiele und „In der Realität“-Einschübe im Lauftext\neines Wissensbeitrags. Ausgezählt: 5 reale Vorkommen, je eines pro Bereich in der\nDoku-Sektion (`docs/index.html:8086–8115`, co/ki/es/wo) plus eines in der realen\nBeispielseite Wissensbeitrag · KI (`docs/index.html:15110–15114`).\n\n**Element-Selektor, ADR-0008-Standardfall.** `.article-callout` sitzt in allen 5\nVorkommen als gewöhnlicher Block-Nachfahre — die 4 Doku-Boxen je in einem\nschlichten `<div>` innerhalb eines `display:flex;flex-direction:column`-Stapels\n(kein Stretch-Bedarf in Spaltenrichtung), die reale Instanz direkt in\n`.article-body`. Keines ist ein Grid-/Flex-Kind mit Streckungsbedarf, keines\nträgt eine `col-*`-Klasse, `.article-callout` kommt in keiner CSS-Regel mit\neinem Geschwister-Kombinator (`+`/`~`) vor, das Tag variiert nicht (immer\n`<aside>`). Keines der drei ADR-0008-Kriterien greift. Die einzige direkte\nKind-Regel von `.article-body` (`.article-body > p`, css/components.css:1566)\nzielt auf `p`, nicht auf `aside` — ein `<cds-article-callout>`-Host zwischen\n`.article-body` und dem `<aside>` bricht deshalb keine Selektorkette.\n\n**Projizierte Absätze bleiben direkte Kinder von `.article-callout`, kein\n`<div>` um `<ng-content>`:** `.article-callout > p` ist ein\nKindselektor, css/components.css:1600. `<ng-content>` selbst fügt kein\nDOM-Element ein — es verschiebt nur die vom Konsumenten geschriebenen\n`<p>`-Elemente an ihre Stelle direkt im `<aside>`, eine Ebene unter dem\n`<cds-article-callout>`-Host, aber ohne zusätzlichen Knoten dazwischen. Geprüft\nim laufenden Storybook (Story „Interaktiv“, Play-Funktion):\n`aside.article-callout > p` liefert sowohl die Eyebrow als auch jeden\nprojizierten Absatz, `:scope > *` zählt keine fremde Zwischenebene.\n\n**`eyebrow` ist Beiwerk (Default `''`), `area` ist Konfiguration mit\nverteidigbarer Vorgabe (`'co'`).** Die Basisregel `.article-callout` selbst\n(ohne `[data-area]`) rendert bereits exakt wie `[data-area=\"co\"]`\n(`background:var(--co-50)`, `border-left-color:var(--co-500)`, beide Werte\nidentisch an beiden Stellen, css/components.css:1591–1592) — `'co'` ist damit\nkeine erfundene Markenfarbe, sondern der reale CSS-Fallback ohne Attribut.\nAnders als bei `cds-pill` (dessen `.pill` ohne `data-area` gar keine Füllfarbe\nhat) gibt es hier keinen sinnvollen „kein Bereich“-Zustand.\n\n**Bekannter CSS-Befund, nicht im Wrapper geflickt (ADR-0001):**\n`.article-callout-eyebrow` (Spezifität 0,1,0) verliert gegen\n`.article-callout > p` (Spezifität 0,1,1, css/components.css:1600), weil die\nEyebrow selbst ein `<p>` UND ein direktes Kind von `.article-callout` ist.\nGemessen in rohem Markup ohne Angular (Playwright/Chromium,\n`getComputedStyle`): die Eyebrow rendert in JEDEM Bereich mit `font-size:16px`\nstatt `12px` und `margin-bottom:0` statt `4px`; die Akzentfarbe geht zusätzlich\nverloren, wenn kein bereichsspezifischer Override existiert (`co` und „kein\n`data-area`“ zeigen `--tx-primary` statt `--co-700`). Diese Komponente\nreproduziert exakt die Klassen und die Struktur des Mockups und zeigt deshalb\ndenselben, vorbestehenden Fehler wie rohes HTML.\n\n**`aria-labelledby` auf `<aside>`, sobald `eyebrow` gesetzt ist — Zusatz zum\nMockup, keine CSS-Änderung.** `<aside>` hat implizit die Landmark-Rolle\n`complementary`; ein Wissensbeitrag mit mehr als einem Callout hat damit\nmehrere gleichnamige Landmarks ohne zugänglichen Namen. Gemessen mit\naxe-core an rohem Markup ohne Angular (zwei `<aside class=\"article-callout\">`\nohne Auszeichnung): `landmark-unique` schlägt fehl, unabhängig von dieser\nKomponente — das Mockup selbst kennt diese Auszeichnung nicht.\nDie Eyebrow ist bereits ein prägnanter, vom Redakteur gepflegter Kurztext\ngenau für diesen Zweck (z. B. „In der Praxis“) — sie bekommt deshalb eine\ngenerierte `id`, auf die `aria-labelledby` zeigt, sobald sie existiert. Ohne\nEyebrow bleibt das `<aside>` unbenannt wie im Mockup: ein zugänglicher Name\nließe sich sonst nur aus dem projizierten Absatztext raten, und das wäre eine\nerfundene Bezeichnung, keine reale.",
740
740
  "docgen": {
741
741
  "$ref": "../services/core/docgen/seitenmuster-wissensbeitrag-callout.json#/components/seitenmuster-wissensbeitrag-callout"
742
742
  },
@@ -747,7 +747,7 @@
747
747
  "seitenmuster-wissensbeitrag-figure": {
748
748
  "id": "seitenmuster-wissensbeitrag-figure",
749
749
  "name": "ArticleFigureComponent",
750
- "description": "ArticleFigure (`cds-article-figure`) — Wrapper um `.article-figure`/\n`.article-figcaption` aus css/components.css (css/components.css:1573–1575):\ndas redaktionelle Inline-Bild mit optionaler Bildunterschrift im Lauftext eines\nWissensbeitrags. Zwölftes Ticket der Seitenbausteine-Serie („Artikel-Körper“).\nAusgezählt: 3 reale Vorkommen — Doku-Demo mit Caption\n(`docs/index.html:8028–8031`), reales Body-Bild mit Caption in der Beispielseite\nWissensbeitrag · KI (`docs/index.html:15115–15118`, identischer Bild- und\nCaption-Text wie die Doku-Demo) und ein dritter, captionsloser Sonderfall direkt\nunter dem Article Header derselben Beispielseite (`docs/index.html:15040–15042`,\ndazu unten mehr).\n\n**Element-Selektor, ADR-0008-Standardfall.** In allen 3 Vorkommen sitzt\n`.article-figure` als gewöhnlicher Block-Nachfahre (zwei in `.article-body`,\neines direkt in einer eigenen `.ep-section`) — kein Grid-/Flex-Kind, keine\n`col-*`-Klasse vom Konsumenten, kein Tag-Wechsel (immer `<figure>`), kein\nVorkommen von `.article-figure` mit einem Geschwister-Kombinator in\ncss/components.css. Die einzige direkte Kind-Regel von `.article-body`\n(`.article-body > p`, css/components.css:1566) zielt auf `p`, nicht auf\n`figure` — ein `<cds-article-figure>`-Host zwischen `.article-body` und dem\n`<figure>` bricht deshalb keine Selektorkette.\n\n**`loading=\"lazy\"` ist fest verdrahtet, kein Input — bewusst NUR der\nIn-Article-Fall, nicht der dritte Mockup-Beleg.** Ausgezählt tragen 2 der 3\nVorkommen `loading=\"lazy\"` (die beiden Bilder MIT Caption, mittig im\nLauftext, „below the fold“ — genau der in `wissensbeitrag.mdx`, Abschnitt\n„In-Article Figure“, dokumentierte Dos-Punkt). Das dritte Vorkommen\n(`docs/index.html:15040–15042`) trägt stattdessen `loading=\"eager\"\nfetchpriority=\"high\"` UND hat keine Caption — es ist das erste Bild direkt\nunter dem Article Header (Kommentar im Mockup: „Hero-Bild auf Body-Breite …\nidentisches Bild wie Vorschaubild für Wiedererkennung“), sitzt außerhalb von\n`.article-body` und bekommt seine 720-px-Breite über ein\nInline-`style=\"max-width:720px;margin:0 auto\"` am Konsumenten, nicht über die\nKomponente (Randbedingung 6 der Spec: Mockup-Inline-Styles wandern nicht in\nden Wrapper). Die Ticket-API sieht dafür kein `loading`/`fetchpriority`-Input\nvor; dieses Bauteil deckt deshalb den (häufigeren, dokumentierten) In-Body-Fall\nab. Für den Lead-Bild-Sonderfall bleibt `<figure class=\"article-figure\">` roh\nschreibbar oder — sofern die Semantik passt — `cds-hero-image` (Ticket 02) zu\nprüfen.\n\n**`alt` und `caption` sind bewusst getrennte Pflicht-/Beiwerk-Inputs, keine\nPrüfung auf inhaltliche Überschneidung.** `wissensbeitrag.mdx` dokumentiert die\nbeiden Rollen ausdrücklich als unterschiedliche Jobs\n(Dont: „Alt-Text und Caption inhaltlich identisch, Screenreader liest\ndoppelt“) — Vorgabe an den Konsumenten, nicht etwas, das eine Wrapper-Komponente\nzur Laufzeit sinnvoll erzwingen könnte.",
750
+ "description": "ArticleFigure (`cds-article-figure`) — Wrapper um `.article-figure`/\n`.article-figcaption` aus css/components.css (css/components.css:1573–1575):\ndas redaktionelle Inline-Bild mit optionaler Bildunterschrift im Lauftext eines\nWissensbeitrags. Ausgezählt: 3 reale Vorkommen — Doku-Demo mit Caption\n(`docs/index.html:8028–8031`), reales Body-Bild mit Caption in der Beispielseite\nWissensbeitrag · KI (`docs/index.html:15115–15118`, identischer Bild- und\nCaption-Text wie die Doku-Demo) und ein dritter, captionsloser Sonderfall direkt\nunter dem Article Header derselben Beispielseite (`docs/index.html:15040–15042`,\ndazu unten mehr).\n\n**Element-Selektor, ADR-0008-Standardfall.** In allen 3 Vorkommen sitzt\n`.article-figure` als gewöhnlicher Block-Nachfahre (zwei in `.article-body`,\neines direkt in einer eigenen `.ep-section`) — kein Grid-/Flex-Kind, keine\n`col-*`-Klasse vom Konsumenten, kein Tag-Wechsel (immer `<figure>`), kein\nVorkommen von `.article-figure` mit einem Geschwister-Kombinator in\ncss/components.css. Die einzige direkte Kind-Regel von `.article-body`\n(`.article-body > p`, css/components.css:1566) zielt auf `p`, nicht auf\n`figure` — ein `<cds-article-figure>`-Host zwischen `.article-body` und dem\n`<figure>` bricht deshalb keine Selektorkette.\n\n**`loading=\"lazy\"` ist fest verdrahtet, kein Input — bewusst NUR der\nIn-Article-Fall, nicht der dritte Mockup-Beleg.** Ausgezählt tragen 2 der 3\nVorkommen `loading=\"lazy\"` (die beiden Bilder MIT Caption, mittig im\nLauftext, „below the fold“ — genau der in `wissensbeitrag.mdx`, Abschnitt\n„In-Article Figure“, dokumentierte Dos-Punkt). Das dritte Vorkommen\n(`docs/index.html:15040–15042`) trägt stattdessen `loading=\"eager\"\nfetchpriority=\"high\"` UND hat keine Caption — es ist das erste Bild direkt\nunter dem Article Header (Kommentar im Mockup: „Hero-Bild auf Body-Breite …\nidentisches Bild wie Vorschaubild für Wiedererkennung“), sitzt außerhalb von\n`.article-body` und bekommt seine 720-px-Breite über ein\nInline-`style=\"max-width:720px;margin:0 auto\"` am Konsumenten, nicht über die\nKomponente (Mockup-Inline-Styles wandern bewusst nicht in den Wrapper). Die\nAPI sieht dafür kein `loading`/`fetchpriority`-Input\nvor; dieses Bauteil deckt deshalb den (häufigeren, dokumentierten) In-Body-Fall\nab. Für den Lead-Bild-Sonderfall bleibt `<figure class=\"article-figure\">` roh\nschreibbar oder — sofern die Semantik passt — `cds-hero-image` zu\nprüfen.\n\n**`alt` und `caption` sind bewusst getrennte Pflicht-/Beiwerk-Inputs, keine\nPrüfung auf inhaltliche Überschneidung.** `wissensbeitrag.mdx` dokumentiert die\nbeiden Rollen ausdrücklich als unterschiedliche Jobs\n(Dont: „Alt-Text und Caption inhaltlich identisch, Screenreader liest\ndoppelt“) — Vorgabe an den Konsumenten, nicht etwas, das eine Wrapper-Komponente\nzur Laufzeit sinnvoll erzwingen könnte.",
751
751
  "docgen": {
752
752
  "$ref": "../services/core/docgen/seitenmuster-wissensbeitrag-figure.json#/components/seitenmuster-wissensbeitrag-figure"
753
753
  },
@@ -758,7 +758,7 @@
758
758
  "seitenmuster-wissensbeitrag-pull-quote": {
759
759
  "id": "seitenmuster-wissensbeitrag-pull-quote",
760
760
  "name": "ArticlePullquoteComponent",
761
- "description": "ArticlePullquote (`cds-article-pullquote`) — Wrapper um `.article-pullquote` aus\ncss/components.css (css/components.css:1608–1612): die typografische\nHervorhebung eines Satzes aus dem eigenen Lauftext eines Wissensbeitrags.\nZwölftes Ticket der Seitenbausteine-Serie („Artikel-Körper“). Ausgezählt: 2\nreale Vorkommen, wortgleich, beide `data-area=\"ki\"` — Doku-Demo\n(`docs/index.html:8136`) und reale Beispielseite Wissensbeitrag · KI\n(`docs/index.html:15075`).\n\n**Nicht `cds-blockquote` (`BlockquoteComponent`, `.bq`) — Ticket-Vorgabe, im\nJSDoc beider Bauteile wechselseitig abgegrenzt.** Pull-Quote und Blockquote\nteilen die typografische Familie (Serif, bereichsgefärbter Linksakzent in\n`{area}-500`), aber nicht den Zweck, dokumentiert in `wissensbeitrag.mdx`,\nAbschnitt „Pull-Quote gegen Blockquote“:\n\n| | Pull-Quote (dieses Bauteil) | Blockquote (`cds-blockquote`) |\n|---|---|---|\n| Wessen Stimme? | Der Beitrag zitiert sich selbst — ein Schlüsselsatz aus dem eigenen Lauftext | Eine dritte, benannte Person (Kund:in, Forschungsstimme, Branchen-Statement) |\n| Attribution | Keine (`quote` ist der einzige Input) | Pflicht (`name`, `roleLabel`) |\n| Hintergrund/Icon | Keiner — `quotes:none`, kein Quote-Icon, bewusst „offen“ statt umrahmt | Getönte Akzent-Box (`--bq-bg`) mit Quote-Icon (`.bq-icon`) |\n| Tag | `<blockquote class=\"article-pullquote\">` direkt, kein umschließendes `<figure>` | `<figure class=\"bq\">` außen, `<blockquote>` (ohne eigene Klasse) innen für den Zitattext |\n\nEin Self-Quote im eigenen Beitrag (Blockquote-Optik auf einem Satz des\nAutors) wirkt sonst absurd — der Autor zitiert die Person, die den Beitrag\nschreibt. Wer eine externe Stimme mit Namen und Rolle zeigen will, verwendet\n`cds-blockquote`.\n\n**`quote` enthält die deutschen Anführungszeichen bereits als Teil des Texts,\ndie Komponente ergänzt keine.** `.article-pullquote{quotes:none}`\n(css/components.css:1608) — es gibt keine `content:open-quote`/`close-quote`-Regel\nin der CSS-Schicht, weder hier noch bei `.bq blockquote` oder\n`.testimonial blockquote`. Ausgezählt stehen die Anführungszeichen in BEIDEN\nrealen Vorkommen als literale Zeichen im Text\n(„KI scheitert selten an der Technologie. …“), nicht CSS-generiert. Wie bei\n`cds-compare`/`cds-table` ist Copy-Text Sache des Konsumenten (ADR-0007 §2) —\ndie Story liefert deshalb `quote` bereits mit „…“ (CONTRIBUTING.md §6), die\nKomponente rendert den String unverändert, ohne eigene Anführungszeichen zu\nerfinden.\n\n**Element-Selektor, ADR-0008-Standardfall.** `.article-pullquote` sitzt in\nbeiden Vorkommen als gewöhnlicher Block-Nachfahre in `.article-body` — kein\nGrid-/Flex-Kind, keine `col-*`-Klasse vom Konsumenten, kein\nGeschwister-Kombinator in css/components.css, kein Tag-Wechsel (immer\n`<blockquote>`). Die einzige direkte Kind-Regel von `.article-body`\n(`.article-body > p`, css/components.css:1566) zielt auf `p`, nicht auf\n`blockquote` — ein `<cds-article-pullquote>`-Host zwischen `.article-body` und\ndem `<blockquote>` bricht deshalb keine Selektorkette.\n\n**`area` ist Konfiguration mit verteidigbarer Vorgabe (`'co'`), keine\nerfundene Bereichsfarbe.** Die Basisregel `.article-pullquote` selbst (ohne\n`[data-area]`) setzt bereits exakt denselben Wert wie `[data-area=\"co\"]`\n(`border-left-color:var(--co-500)`, css/components.css:1608–1609) — `'co'` ist\ndamit der reale CSS-Fallback ohne Attribut, nicht geraten.",
761
+ "description": "ArticlePullquote (`cds-article-pullquote`) — Wrapper um `.article-pullquote` aus\ncss/components.css (css/components.css:1608–1612): die typografische\nHervorhebung eines Satzes aus dem eigenen Lauftext eines Wissensbeitrags.\nAusgezählt: 2 reale Vorkommen, wortgleich, beide `data-area=\"ki\"` — Doku-Demo\n(`docs/index.html:8136`) und reale Beispielseite Wissensbeitrag · KI\n(`docs/index.html:15075`).\n\n**Nicht `cds-blockquote` (`BlockquoteComponent`, `.bq`) — im\nJSDoc beider Bauteile wechselseitig abgegrenzt.** Pull-Quote und Blockquote\nteilen die typografische Familie (Serif, bereichsgefärbter Linksakzent in\n`{area}-500`), aber nicht den Zweck, dokumentiert in `wissensbeitrag.mdx`,\nAbschnitt „Pull-Quote gegen Blockquote“:\n\n| | Pull-Quote (dieses Bauteil) | Blockquote (`cds-blockquote`) |\n|---|---|---|\n| Wessen Stimme? | Der Beitrag zitiert sich selbst — ein Schlüsselsatz aus dem eigenen Lauftext | Eine dritte, benannte Person (Kund:in, Forschungsstimme, Branchen-Statement) |\n| Attribution | Keine (`quote` ist der einzige Input) | Pflicht (`name`, `roleLabel`) |\n| Hintergrund/Icon | Keiner — `quotes:none`, kein Quote-Icon, bewusst „offen“ statt umrahmt | Getönte Akzent-Box (`--bq-bg`) mit Quote-Icon (`.bq-icon`) |\n| Tag | `<blockquote class=\"article-pullquote\">` direkt, kein umschließendes `<figure>` | `<figure class=\"bq\">` außen, `<blockquote>` (ohne eigene Klasse) innen für den Zitattext |\n\nEin Self-Quote im eigenen Beitrag (Blockquote-Optik auf einem Satz des\nAutors) wirkt sonst absurd — der Autor zitiert die Person, die den Beitrag\nschreibt. Wer eine externe Stimme mit Namen und Rolle zeigen will, verwendet\n`cds-blockquote`.\n\n**`quote` enthält die deutschen Anführungszeichen bereits als Teil des Texts,\ndie Komponente ergänzt keine.** `.article-pullquote{quotes:none}`\n(css/components.css:1608) — es gibt keine `content:open-quote`/`close-quote`-Regel\nin der CSS-Schicht, weder hier noch bei `.bq blockquote` oder\n`.testimonial blockquote`. Ausgezählt stehen die Anführungszeichen in BEIDEN\nrealen Vorkommen als literale Zeichen im Text\n(„KI scheitert selten an der Technologie. …“), nicht CSS-generiert. Wie bei\n`cds-compare`/`cds-table` ist Copy-Text Sache des Konsumenten (ADR-0007 §2) —\ndie Story liefert deshalb `quote` bereits mit „…“ (CONTRIBUTING.md §6), die\nKomponente rendert den String unverändert, ohne eigene Anführungszeichen zu\nerfinden.\n\n**Element-Selektor, ADR-0008-Standardfall.** `.article-pullquote` sitzt in\nbeiden Vorkommen als gewöhnlicher Block-Nachfahre in `.article-body` — kein\nGrid-/Flex-Kind, keine `col-*`-Klasse vom Konsumenten, kein\nGeschwister-Kombinator in css/components.css, kein Tag-Wechsel (immer\n`<blockquote>`). Die einzige direkte Kind-Regel von `.article-body`\n(`.article-body > p`, css/components.css:1566) zielt auf `p`, nicht auf\n`blockquote` — ein `<cds-article-pullquote>`-Host zwischen `.article-body` und\ndem `<blockquote>` bricht deshalb keine Selektorkette.\n\n**`area` ist Konfiguration mit verteidigbarer Vorgabe (`'co'`), keine\nerfundene Bereichsfarbe.** Die Basisregel `.article-pullquote` selbst (ohne\n`[data-area]`) setzt bereits exakt denselben Wert wie `[data-area=\"co\"]`\n(`border-left-color:var(--co-500)`, css/components.css:1608–1609) — `'co'` ist\ndamit der reale CSS-Fallback ohne Attribut, nicht geraten.",
762
762
  "docgen": {
763
763
  "$ref": "../services/core/docgen/seitenmuster-wissensbeitrag-pull-quote.json#/components/seitenmuster-wissensbeitrag-pull-quote"
764
764
  },
@@ -780,7 +780,7 @@
780
780
  "seitenmuster-wissensbeitrag-author-card": {
781
781
  "id": "seitenmuster-wissensbeitrag-author-card",
782
782
  "name": "AuthorCardComponent",
783
- "description": "AuthorCard (`div[cdsAuthorCard]`) — Wrapper um `.author-card` aus css/components.css\n(css/components.css:1602–1606): der Avatar-plus-Bio-Strip am Ende eines\nWissensbeitrags, einzeln oder mehrfach in `cds-author-card-group`. Dreizehntes und\nletztes Ticket der Seitenbausteine-Serie. Ausgezählt: 17 reale Vorkommen in\n`docs/index.html` — 4 einzelstehend (2 Doku-Demos `docs/index.html:8193,8207`, 1\nArtikel-Demo `8424`, 1 reale Beispielseite `15179`) und 13 in fünf Gruppen (siehe\n`AuthorCardGroupComponent`).\n\n**Attributselektor (ADR-0008), gemessen im `is-grid`-Fall statt nur hergeleitet.**\n`.author-card-group.is-grid` (css/components.css:1555) ist `display:grid` ohne\neigenes `align-items` — der CSS-Default `stretch` gleicht die Kartenhöhen einer\nReihe deshalb NUR aus, wenn `.author-card` selbst das Grid-Kind ist. Spike-Story\n(zwei Karten, 600-px-Raster, kurze gegen deutlich längere Bio, seither wieder\ngelöscht) hat beide Varianten nebeneinander gebaut und mit `getBoundingClientRect()`\ngemessen:\n\n| Variante | Grid-Kind (Tag) | `.author-card`-Höhen | Bio-Rect (kurz / lang) |\n|---|---|---|---|\n| Element-Selektor `<cds-author-card-naive>` | `CDS-AUTHOR-CARD-NAIVE`, 204/204 px (gestreckt) | **84 / 204 px** (ungleich) | 24×73 / 144×200 px |\n| Attributselektor `div[cdsAuthorCardSpike]` | `DIV.author-card`, 204/204 px | **204 / 204 px** (gleich) | — |\n\nBeim Element-Selektor wird der UNSICHTBARE `<cds-author-card-naive>`-Host auf 204 px\ngestreckt, die sichtbare `.author-card`-Box eine Ebene darunter bleibt bei ihrer\nInhaltshöhe (84 px) — exakt der Fehler aus ADR-0008 Fall 1. Mit dem Attributselektor\nIST `.author-card` das Grid-Kind, `align-items:stretch` trifft direkt zu.\n\n**Ehrlicher Zusatz, anders als bei `cds-icon-card`: die Baseline-Screenshots beider\nSpike-Varianten waren byte-identisch** (`sha1` gleich). `.author-card` trägt (anders\nals `.ep-card`) weder Hintergrund noch Rahmen noch Schatten — der zusätzliche,\nunsichtbare Leerraum unter der kurzen Bio malt in keiner der beiden Varianten ein\nPixel. Der Fehler ist damit heute rein strukturell (falsches Boxmodell, `getBoundingClientRect()`\nauf `.author-card` lügt), nicht optisch sichtbar. Er wird real, sobald `.author-card`\nkünftig einen Rahmen/Hintergrund/Hover-Zustand bekommt, und er verfälscht schon\nheute jede Messung/Positionierung, die sich auf die tatsächliche Kartenhöhe verlässt\n(u. a. die Story „Als Raster“ unten, die genau das pinnt). Das ADR-0008-Kriterium\nist strukturell formuliert („die Stelle im DOM einnehmen, die es ohne Angular\neinnähme“), nicht optisch — der Attributselektor bleibt deshalb die richtige Wahl,\nobwohl der Effekt heute unsichtbar ist.\n\n**`area` ist bewusst NICHT Teil der API — CSS-Befund, nicht erfundene Konfiguration.**\nAlle 17 realen `.author-card`-Vorkommen tragen `data-area`, und sowohl\n`docs/index.html:8185` als auch `wissensbeitrag.mdx:199` behaupten wörtlich, das\nfärbe „nur den Avatar“. Ausgezählt gibt es aber in `css/components.css` (Light UND\nDark) KEINE einzige `.author-card[data-area=\"…\"]`-Regel — weder direkt noch über\neinen Nachfahren-Selektor auf `.article-avatar`, dessen eigene\n`[data-area]`-Färbung (css/components.css:1523–1526) ein Element-Selektor auf dem\nAvatar selbst ist, unabhängig vom Vorfahren. `data-area` auf `.author-card` ist\ndamit, gegen die Doku-Aussage, komplett wirkungslos — die Bereichsfarbe kommt\nausschließlich vom `area`-Input, den der Konsument direkt an das projizierte\n`[cdsAvatar]` bindet (siehe unten). Ein `area`-Input, der hier nur `data-area` ohne\njede CSS-Wirkung setzt, wäre eine erfundene Konfiguration ohne Gegenwert — Befund\ngemeldet, nicht im Wrapper geflickt (ADR-0001):\n`.scratch/angular-seitenbausteine/issues/24-css-luecke-author-card-data-area.md`.\n\n**Avatar projiziert über `[cdsAvatar]`, wie bei `cds-article-header`.** Der\nKonsument setzt Größe (`size=\"lg\"`, css/components.css:1527) und Bereich\n(`area=\"…\"`) direkt am projizierten `<div cdsAvatar>` — diese Komponente reicht\nnichts durch, exakt die Trennung, die `cds-article-header` bereits etabliert\n(siehe dessen Klassendoku, Abschnitt „Avatar projiziert über `[cdsAvatar]`“).\n\n**Eyebrow als `<h3>`, nur wenn gesetzt** (Beiwerk, Default `''`) — deckungsgleich\nmit allen 4 einzelstehenden realen Vorkommen: 2 tragen `<h3 class=\"author-card-eyebrow\">`\n(`docs/index.html:8196,8427`, „Über den Autor“/„Über die Autorin“, Ticket-Vorgabe:\neine Ebene unter dem `<h2>` des Article-Body), die anderen 2 (Doku-Demos ohne\numgebende Artikelstruktur) haben gar keinen. Innerhalb einer Gruppe trägt laut\n`wissensbeitrag.mdx` („Eyebrow-Konvention“) keine einzelne Karte mehr einen eigenen\nEyebrow — die Gruppen-Überschrift ersetzt sie (siehe `AuthorCardGroupComponent`);\nKonsumenten lassen `eyebrow` dafür einfach leer.\n\n**`roleLabel`, NICHT `role` wie im Ticket-Text — gemessene Abweichung, kein\nFreihand-Entscheid.** Ein erster Entwurf folgte der Ticket-API wörtlich\n(`role`-Input, mit `role=\"…\"` als schlichtem Attribut in den Stories gesetzt, wie\n`name`/`bio`/`eyebrow`). Storybooks eingebauter a11y-Check (`aria-roles`) schlug\ndarauf für JEDE Story mit echtem Rollentext fehl: `role` ist ein globales\nHTML-/ARIA-Attribut, und ein Angular-Input MIT DIESEM NAMEN wird bei einem\nungebundenen `role=\"Senior AI Engineer …\"` zwar als Startwert an den Input\ndurchgereicht, bleibt aber ZUSÄTZLICH als echtes `role`-Attribut im DOM stehen —\naxe meldete wörtlich „Roles must be one of the valid ARIA roles: Senior, AI,\nEngineer, …“, weil der Freitext als ARIA-Rolle interpretiert wird. Exakt dieselbe\nKollision umgehen `BlockquoteComponent`, `TestimonialComponent` und\n`TeamVoiceComponent` bereits mit `roleLabel` statt `role`\n(`grep -rn \"readonly role\" angular-lib/projects/design-system-angular/src/lib/`\nzeigt nur diese drei). Diese Komponente folgt demselben, bereits etablierten\nMuster statt die Kollision ein viertes Mal zu wiederholen — CSS-Klasse bleibt\nunverändert `.author-card-role`, nur der Input-Name weicht vom Ticket-Text ab.",
783
+ "description": "AuthorCard (`div[cdsAuthorCard]`) — Wrapper um `.author-card` aus css/components.css\n(css/components.css:1602–1606): der Avatar-plus-Bio-Strip am Ende eines\nWissensbeitrags, einzeln oder mehrfach in `cds-author-card-group`. Ausgezählt: 17 reale Vorkommen in\n`docs/index.html` — 4 einzelstehend (2 Doku-Demos `docs/index.html:8193,8207`, 1\nArtikel-Demo `8424`, 1 reale Beispielseite `15179`) und 13 in fünf Gruppen (siehe\n`AuthorCardGroupComponent`).\n\n**Attributselektor (ADR-0008), gemessen im `is-grid`-Fall statt nur hergeleitet.**\n`.author-card-group.is-grid` (css/components.css:1555) ist `display:grid` ohne\neigenes `align-items` — der CSS-Default `stretch` gleicht die Kartenhöhen einer\nReihe deshalb NUR aus, wenn `.author-card` selbst das Grid-Kind ist. Spike-Story\n(zwei Karten, 600-px-Raster, kurze gegen deutlich längere Bio, seither wieder\ngelöscht) hat beide Varianten nebeneinander gebaut und mit `getBoundingClientRect()`\ngemessen:\n\n| Variante | Grid-Kind (Tag) | `.author-card`-Höhen | Bio-Rect (kurz / lang) |\n|---|---|---|---|\n| Element-Selektor `<cds-author-card-naive>` | `CDS-AUTHOR-CARD-NAIVE`, 204/204 px (gestreckt) | **84 / 204 px** (ungleich) | 24×73 / 144×200 px |\n| Attributselektor `div[cdsAuthorCardSpike]` | `DIV.author-card`, 204/204 px | **204 / 204 px** (gleich) | — |\n\nBeim Element-Selektor wird der UNSICHTBARE `<cds-author-card-naive>`-Host auf 204 px\ngestreckt, die sichtbare `.author-card`-Box eine Ebene darunter bleibt bei ihrer\nInhaltshöhe (84 px) — exakt der Fehler aus ADR-0008 Fall 1. Mit dem Attributselektor\nIST `.author-card` das Grid-Kind, `align-items:stretch` trifft direkt zu.\n\n**Ehrlicher Zusatz, anders als bei `cds-icon-card`: die Baseline-Screenshots beider\nSpike-Varianten waren byte-identisch** (`sha1` gleich). `.author-card` trägt (anders\nals `.ep-card`) weder Hintergrund noch Rahmen noch Schatten — der zusätzliche,\nunsichtbare Leerraum unter der kurzen Bio malt in keiner der beiden Varianten ein\nPixel. Der Fehler ist damit heute rein strukturell (falsches Boxmodell, `getBoundingClientRect()`\nauf `.author-card` lügt), nicht optisch sichtbar. Er wird real, sobald `.author-card`\nkünftig einen Rahmen/Hintergrund/Hover-Zustand bekommt, und er verfälscht schon\nheute jede Messung/Positionierung, die sich auf die tatsächliche Kartenhöhe verlässt\n(u. a. die Story „Als Raster“ unten, die genau das pinnt). Das ADR-0008-Kriterium\nist strukturell formuliert („die Stelle im DOM einnehmen, die es ohne Angular\neinnähme“), nicht optisch — der Attributselektor bleibt deshalb die richtige Wahl,\nobwohl der Effekt heute unsichtbar ist.\n\n**`area` ist bewusst NICHT Teil der API — CSS-Befund, nicht erfundene Konfiguration.**\nAlle 17 realen `.author-card`-Vorkommen tragen `data-area`, und sowohl\n`docs/index.html:8185` als auch `wissensbeitrag.mdx:199` behaupten wörtlich, das\nfärbe „nur den Avatar“. Ausgezählt gibt es aber in `css/components.css` (Light UND\nDark) KEINE einzige `.author-card[data-area=\"…\"]`-Regel — weder direkt noch über\neinen Nachfahren-Selektor auf `.article-avatar`, dessen eigene\n`[data-area]`-Färbung (css/components.css:1523–1526) ein Element-Selektor auf dem\nAvatar selbst ist, unabhängig vom Vorfahren. `data-area` auf `.author-card` ist\ndamit, gegen die Doku-Aussage, komplett wirkungslos — die Bereichsfarbe kommt\nausschließlich vom `area`-Input, den der Konsument direkt an das projizierte\n`[cdsAvatar]` bindet (siehe unten). Ein `area`-Input, der hier nur `data-area` ohne\njede CSS-Wirkung setzt, wäre eine erfundene Konfiguration ohne Gegenwert — Befund\ngemeldet, nicht im Wrapper geflickt (ADR-0001).\n\n**Avatar projiziert über `[cdsAvatar]`, wie bei `cds-article-header`.** Der\nKonsument setzt Größe (`size=\"lg\"`, css/components.css:1527) und Bereich\n(`area=\"…\"`) direkt am projizierten `<div cdsAvatar>` — diese Komponente reicht\nnichts durch, exakt die Trennung, die `cds-article-header` bereits etabliert\n(siehe dessen Klassendoku, Abschnitt „Avatar projiziert über `[cdsAvatar]`“).\n\n**Eyebrow als `<h3>`, nur wenn gesetzt** (Beiwerk, Default `''`) — deckungsgleich\nmit allen 4 einzelstehenden realen Vorkommen: 2 tragen `<h3 class=\"author-card-eyebrow\">`\n(`docs/index.html:8196,8427`, „Über den Autor“/„Über die Autorin“:\neine Ebene unter dem `<h2>` des Article-Body), die anderen 2 (Doku-Demos ohne\numgebende Artikelstruktur) haben gar keinen. Innerhalb einer Gruppe trägt laut\n`wissensbeitrag.mdx` („Eyebrow-Konvention“) keine einzelne Karte mehr einen eigenen\nEyebrow — die Gruppen-Überschrift ersetzt sie (siehe `AuthorCardGroupComponent`);\nKonsumenten lassen `eyebrow` dafür einfach leer.\n\n**`roleLabel`, NICHT `role` wie in einem ersten Entwurf — gemessene Abweichung, kein\nFreihand-Entscheid.** Der erste Entwurf setzte `role` wörtlich um\n(`role`-Input, mit `role=\"…\"` als schlichtem Attribut in den Stories gesetzt, wie\n`name`/`bio`/`eyebrow`). Storybooks eingebauter a11y-Check (`aria-roles`) schlug\ndarauf für JEDE Story mit echtem Rollentext fehl: `role` ist ein globales\nHTML-/ARIA-Attribut, und ein Angular-Input MIT DIESEM NAMEN wird bei einem\nungebundenen `role=\"Senior AI Engineer …\"` zwar als Startwert an den Input\ndurchgereicht, bleibt aber ZUSÄTZLICH als echtes `role`-Attribut im DOM stehen —\naxe meldete wörtlich „Roles must be one of the valid ARIA roles: Senior, AI,\nEngineer, …“, weil der Freitext als ARIA-Rolle interpretiert wird. Exakt dieselbe\nKollision umgehen `BlockquoteComponent`, `TestimonialComponent` und\n`TeamVoiceComponent` bereits mit `roleLabel` statt `role`\n(`grep -rn \"readonly role\" angular-lib/projects/design-system-angular/src/lib/`\nzeigt nur diese drei). Diese Komponente folgt demselben, bereits etablierten\nMuster statt die Kollision ein viertes Mal zu wiederholen — CSS-Klasse bleibt\nunverändert `.author-card-role`, nur der Input-Name weicht vom ursprünglichen Entwurf ab.",
784
784
  "docgen": {
785
785
  "$ref": "../services/core/docgen/seitenmuster-wissensbeitrag-author-card.json#/components/seitenmuster-wissensbeitrag-author-card"
786
786
  },
@@ -791,7 +791,7 @@
791
791
  "seitenmuster-seminar-·-training-fakten-liste": {
792
792
  "id": "seitenmuster-seminar-·-training-fakten-liste",
793
793
  "name": "FactsComponent",
794
- "description": "Facts (`cds-facts`) — Wrapper um `.ep-facts` aus css/components.css\n(css/components.css:1261–1272): die Definitionsliste für die harten Eckdaten\neines Angebots (Termin, Dauer, Ort, Preis), einspaltig mit Haarlinie zwischen\nden Paaren oder als `.is-grid` zweispaltig für Kästen, die neben Inhalt stehen.\nBewusst ohne eigenen Rahmen und ohne Fläche, sie zieht in einen vorhandenen\nContainer ein (Angebots-Box, Sticky-Sidebar) — Doku-Sektion\n`docs/index.html:9069` (`gt-seminar-fakten`), Beispielseiten `docs/index.html:\n9076–9082`, `13718–13726`, `16293–16300`.\n\n**Element-Selektor, kein Attribut (ADR-0008-Standardfall).** Ausgezählt: keines\nder 3 `.ep-facts`-Vorkommen in `docs/index.html` sitzt in einem `.layout-grid`\nmit `col-*`, keines wird von einem Grid-/Flex-Elternteil in der Höhe gedehnt\n(der Flex-Fall in Zeile 16290 ist `flex-direction:column`, dort füllen Block-\nElemente die Breite ihres Containers unabhängig von einer zusätzlichen\nWrapper-Ebene — anders als bei `.ep-card`/ADR-0008 Fall 1 geht es dort um Höhe,\ndie durch Block-Layout NICHT automatisch nach unten durchgereicht wird). Das\nTag variiert nicht (immer `<dl>`). Analog zu `cds-faq` (`faq.component.ts`, das\nintern `<details>` in einem `<div class=\"ep-faq\">` rendert) trägt hier die\nINNERE `<dl class=\"ep-facts\">` die CSS-Klasse, nicht der `<cds-facts>`-Host: die\nSemantik einer Definitionsliste hängt am `<dl>`-Tag selbst, ein Custom-Element\nkann dieses Tag nicht annehmen. Da `.ep-facts > div + div` ausschließlich\nNachfahren INNERHALB der eigenen Vorlage anspricht, bricht eine zusätzliche\nHost-Ebene darüber nichts — anders als bei `cds-section` (Fall 2) hängt hier\nkeine von außen gesetzte Fläche oder ein von außen adressierter Nachfahre an\ndieser Tiefe.\n\n**`<dl>`/`<div>`/`<dt>`/`<dd>` exakt wie vom CSS erwartet.** `.ep-facts > div +\ndiv` (css/components.css:1262) setzt Haarlinie und Abstand ab dem ZWEITEN Paar;\nohne das umschließende `<div>` pro Paar griffe der Selektor nicht. Jedes\n`items()`-Element wird deshalb zu genau einem `<div><dt>…</dt><dd>…</dd></div>`.\n\n**`area` färbt `<dt>` über `.t-{area}`, nicht über `data-area`.** Anders als bei\n`cds-feature`/`cds-icon-card` kennt `.ep-facts` selbst kein `[data-area]`; der\nCSS-Kommentar zu dieser Klasse (`css/components.css:1259`) benennt\nstattdessen ausdrücklich „Bereichston der Labels über `.t-XX` im Markup\n(dark-safe), nie als Inline-Farbe“ als sanktionierten Weg. Ausgezählt: alle 3\n`.ep-facts`-Vorkommen in `docs/index.html` setzen `.t-wo` auf JEDEM `<dt>`\nderselben Liste (nie gemischt, nie eine andere Bereichsfarbe) — ein optionaler\n`area`-Input, der `.t-{area}` einheitlich auf alle `<dt>` der Instanz anwendet,\nbildet dieses Muster nach, ohne einen Farbwert zu erfinden (`.t-XX` ist eine\nbestehende, bereits kontrast- und dark-mode-geprüfte Utility, `css/components.css\n:638–642`). Ungesetzt (Default) bleibt `<dt>` ohne zusätzliche Klasse, erbt also\ndie umgebende Textfarbe — das deckt Kontexte ohne Bereichsbindung ab.\n\n**Über die Ticket-API hinaus:** `area` stand nicht in der ursprünglichen\nAPI-Skizze von Ticket 08. Ergänzt, weil ohne ihn KEINES der 3 realen\n`.ep-facts`-Vorkommen originalgetreu nachgebaut werden könnte — anders als bei\n`cds-tier`s `area` (dort deckt die Einschränkung eine tatsächliche CSS-Lücke\nab) fehlt hier keine CSS-Regel, nur ein API-Hook auf eine bereits vollständige\nKlasse. Siehe Ticket-Bericht für die Abwägung.",
794
+ "description": "Facts (`cds-facts`) — Wrapper um `.ep-facts` aus css/components.css\n(css/components.css:1261–1272): die Definitionsliste für die harten Eckdaten\neines Angebots (Termin, Dauer, Ort, Preis), einspaltig mit Haarlinie zwischen\nden Paaren oder als `.is-grid` zweispaltig für Kästen, die neben Inhalt stehen.\nBewusst ohne eigenen Rahmen und ohne Fläche, sie zieht in einen vorhandenen\nContainer ein (Angebots-Box, Sticky-Sidebar) — Doku-Sektion\n`docs/index.html:9069` (`gt-seminar-fakten`), Beispielseiten `docs/index.html:\n9076–9082`, `13718–13726`, `16293–16300`.\n\n**Element-Selektor, kein Attribut (ADR-0008-Standardfall).** Ausgezählt: keines\nder 3 `.ep-facts`-Vorkommen in `docs/index.html` sitzt in einem `.layout-grid`\nmit `col-*`, keines wird von einem Grid-/Flex-Elternteil in der Höhe gedehnt\n(der Flex-Fall in Zeile 16290 ist `flex-direction:column`, dort füllen Block-\nElemente die Breite ihres Containers unabhängig von einer zusätzlichen\nWrapper-Ebene — anders als bei `.ep-card`/ADR-0008 Fall 1 geht es dort um Höhe,\ndie durch Block-Layout NICHT automatisch nach unten durchgereicht wird). Das\nTag variiert nicht (immer `<dl>`). Analog zu `cds-faq` (`faq.component.ts`, das\nintern `<details>` in einem `<div class=\"ep-faq\">` rendert) trägt hier die\nINNERE `<dl class=\"ep-facts\">` die CSS-Klasse, nicht der `<cds-facts>`-Host: die\nSemantik einer Definitionsliste hängt am `<dl>`-Tag selbst, ein Custom-Element\nkann dieses Tag nicht annehmen. Da `.ep-facts > div + div` ausschließlich\nNachfahren INNERHALB der eigenen Vorlage anspricht, bricht eine zusätzliche\nHost-Ebene darüber nichts — anders als bei `cds-section` (Fall 2) hängt hier\nkeine von außen gesetzte Fläche oder ein von außen adressierter Nachfahre an\ndieser Tiefe.\n\n**`<dl>`/`<div>`/`<dt>`/`<dd>` exakt wie vom CSS erwartet.** `.ep-facts > div +\ndiv` (css/components.css:1262) setzt Haarlinie und Abstand ab dem ZWEITEN Paar;\nohne das umschließende `<div>` pro Paar griffe der Selektor nicht. Jedes\n`items()`-Element wird deshalb zu genau einem `<div><dt>…</dt><dd>…</dd></div>`.\n\n**`area` färbt `<dt>` über `.t-{area}`, nicht über `data-area`.** Anders als bei\n`cds-feature`/`cds-icon-card` kennt `.ep-facts` selbst kein `[data-area]`; der\nCSS-Kommentar zu dieser Klasse (`css/components.css:1259`) benennt\nstattdessen ausdrücklich „Bereichston der Labels über `.t-XX` im Markup\n(dark-safe), nie als Inline-Farbe“ als sanktionierten Weg. Ausgezählt: alle 3\n`.ep-facts`-Vorkommen in `docs/index.html` setzen `.t-wo` auf JEDEM `<dt>`\nderselben Liste (nie gemischt, nie eine andere Bereichsfarbe) — ein optionaler\n`area`-Input, der `.t-{area}` einheitlich auf alle `<dt>` der Instanz anwendet,\nbildet dieses Muster nach, ohne einen Farbwert zu erfinden (`.t-XX` ist eine\nbestehende, bereits kontrast- und dark-mode-geprüfte Utility, `css/components.css\n:638–642`). Ungesetzt (Default) bleibt `<dt>` ohne zusätzliche Klasse, erbt also\ndie umgebende Textfarbe — das deckt Kontexte ohne Bereichsbindung ab.\n\n**Über die ursprüngliche API hinaus:** `area` stand nicht in der ursprünglichen\nAPI-Skizze. Ergänzt, weil ohne ihn KEINES der 3 realen\n`.ep-facts`-Vorkommen originalgetreu nachgebaut werden könnte — anders als bei\n`cds-tier`s `area` (dort deckt die Einschränkung eine tatsächliche CSS-Lücke\nab) fehlt hier keine CSS-Regel, nur ein API-Hook auf eine bereits vollständige\nKlasse.",
795
795
  "docgen": {
796
796
  "$ref": "../services/core/docgen/seitenmuster-seminar-·-training-fakten-liste.json#/components/seitenmuster-seminar-·-training-fakten-liste"
797
797
  },
@@ -9,7 +9,7 @@
9
9
  "name": "Übersicht",
10
10
  "path": "./src/docs/grundlagen/barrierefreiheit.mdx",
11
11
  "title": "Grundlagen/Barrierefreiheit",
12
- "content": "import { Meta, Unstyled } from '@storybook/addon-docs/blocks';\n\n<Meta title=\"Grundlagen/Barrierefreiheit\" name=\"Übersicht\" />\n\n# Barrierefreiheit\n\nKontrastverhältnisse aller Brand-Farben, Tastaturnavigation, ARIA Patterns, Touch\nTargets, Reduced Motion. Pflicht sind die Stufen A und AA, AAA nehmen wir mit, wo es ohne\nNachteil geht. Vollständige Kriterien-Übersicht:\n[WCAG 2.2 Quickref (W3C)](https://www.w3.org/WAI/WCAG22/quickref/). Quelle:\n`docs/index.html#sec-a11y`.\n\n## Anspruch\n\nDas System ist auf Konformitätsstufe AA gebaut. Nach WCAG-Definition heißt das: alle\nErfolgskriterien der Stufe A und alle der Stufe AA sind erfüllt. AA liegt also nicht über\nA, sondern schließt es ein. Der Anspruch gilt für jedes Kriterium, nicht nur für Kontrast,\nebenso für Tastaturbedienung, Fokus, Struktur, Beschriftung, Bewegung und Zielgrößen.\n\nStufe AAA ist Zugabe, kein Abnahmekriterium. Wo sie ohne Nachteil für Gestaltung oder\nVerständlichkeit erreichbar ist, nehmen wir sie mit. Wo sie etwas kostet, bleibt AA der\nMaßstab. An einem AAA-Kriterium scheitert keine Änderung.\n\n**Pflicht, A und AA.** Gilt für alles, was das System ausliefert. Zwei Prüfungen laufen\nautomatisch: `npm run check:contrast` rendert die Doku in beiden Modi und muss 0 melden,\n`npm run check:dark-states` findet Zustände, die im Dark Mode unlesbar würden. Alles, was\nsich nicht messen lässt (Tastaturpfad, Screenreader-Ausgabe), gehört in die Prüfung von\nHand.\n\n**Freiwillig darüber.** An vielen Stellen liegt das System über der Pflicht. Die meisten\nTextfarben erreichen 7:1 und mehr (AAA). Touch-Targets liegen bei 44 px, das ist 2.5.5 auf\nAAA, verlangt wären 24 px. Diese Marken zeigen den Überschuss, nicht die Messlatte, sie\ndürfen an einzelnen Stellen fehlen, ohne dass etwas kaputt ist.\n\n**Nicht erfüllt, bewusst.** 2.3.3 Animation from Interactions (AAA) verlangt, dass eine\ndurch Bedienung ausgelöste Bewegung abschaltbar ist. Der Hover-Lift von 2 px auf\nklickbaren Karten und auf dem Störer läuft auch bei `prefers-reduced-motion: reduce`, weil\ner die Klickbarkeit signalisiert. Auto-Bewegung ist davon getrennt, die Carousels\nrespektieren die Einstellung.\n\n## Kontrastverhältnisse · Textfarben auf Weiß\n\nDie Modus-Markierung zeigt, auf welchen Theme-Modus sich ein Wert bezieht: Light für die\nhellen Token-Werte (Page-Background `#FFFFFF`), Dark für die dunklen Pendants\n(Page-Background `#151A1F`).\n\n| Farbe | Wert (Light) | Kontrast auf Weiß | Einsatz | Stufe |\n|---|---|---|---|---|\n| `--tx-primary` | #333E48 | 10,4:1 | Fließtext, Headlines, UI-Labels | AAA |\n| `--tx-secondary` | #4A6565 | 5,8:1 | Lead, Sub-Titel, Captions | AA |\n| `--tx-muted` | #5A7171 | 5,25:1 | Leise Texte: Eyebrow, Counter, Meta-Angaben, Captions. Abgedunkelt von #6E8585 (3,9:1), das AA für Normaltext verfehlte | AA |\n| Corporate 700 | #007575 | 5,5:1 | Links, farbige Überschriften | AA |\n| Angewandte KI 800 | #475705 | 8,1:1 | Texteinsatz (700 mit 4,4:1 nicht ausreichend) | AAA |\n| Angewandte KI 700 | #6B8208 | 4,4:1 | Knapp unter 4,5:1 AA-Schwelle | Fail |\n| Effektive Software 500 | #2F5FD4 | 5,7:1 | Links, farbige Überschriften, Akzentflächen. Button-Background nutzt `--es-700` (`.btn-es`) | AA |\n| Effektive Software 700 | #1B3D9A | 9,7:1 | Fließtext, dunkle Überschriften | AAA |\n| Wirksame Organisationen 600 | #347A22 | 5,3:1 | Buttons mit weißer Schrift (mindestens 600) | AA |\n| Wirksame Organisationen 700 | #285E1A | 7,8:1 | Texteinsatz, Links | AAA |\n| Rosé 500 | #C23060 | 5,3:1 | Links, Buttons, farbige Labels | AA |\n| Rosé 700 | #7E1639 | 10,2:1 | Texteinsatz, dunkle Überschriften | AAA |\n\nAngewandte-KI-700 ist die einzige Zeile, die AA für Normaltext verfehlt (4,4:1 gegen die\n4,5:1-Schwelle), deshalb trägt farbiger KI-Text `--ki-800`, nicht `--ki-700`.\n\nDer folgende Block zeigt zusätzlich zur Tabelle die tatsächliche Farbe jedes Tokens als\nFläche und macht die Lesbarkeit der weißen „Aa“-Beschriftung direkt sichtbar, statt sie nur\nals Zahl zu benennen. Werte 1:1 aus `docs/index.html#gt-a11y-kontrast` übernommen.\n\nexport const withCode = (text) =>\n text.split('`').map((part, i) =>\n i % 2 === 1 ? <code key={i} className=\"token\">{part}</code> : part\n );\n\nexport const ContrastRow = ({ bg, fg, mode, name, ratio, badgeText, badgeClass }) => (\n <div className=\"contrast-row\">\n <div className=\"cswatch\" style={{ background: bg, color: fg }}>Aa</div>\n <div className=\"cinfo\">\n <div className=\"cname\">\n <span className=\"a11y-mode\">{mode}</span>\n {withCode(name)}\n </div>\n <div className=\"cratio\">{withCode(ratio)}</div>\n </div>\n <span className={`cbadge ${badgeClass}`}>{badgeText}</span>\n </div>\n);\n\nexport const contrastTextLight = [\n { bg: '#333E48', fg: '#fff', mode: 'Light', name: \"`--tx-primary` #333E48 auf Weiß\", ratio: \"10,4:1, Fließtext, Headlines, UI-Labels\", badgeText: 'AAA ✓', badgeClass: 'c-aaa' },\n { bg: '#4A6565', fg: '#fff', mode: 'Light', name: \"`--tx-secondary` #4A6565 auf Weiß\", ratio: \"5,8:1, Lead, Sub-Titel, Captions\", badgeText: 'AA ✓', badgeClass: 'c-aa' },\n { bg: '#5A7171', fg: '#fff', mode: 'Light', name: \"`--tx-muted` #5A7171 auf Weiß\", ratio: \"5,25:1, leise Texte: Eyebrow, Counter, Meta-Angaben, Captions · abgedunkelt von #6E8585 (3,9:1), das AA für Normaltext verfehlte\", badgeText: 'AA ✓', badgeClass: 'c-aa' },\n { bg: '#007575', fg: '#fff', mode: 'Light', name: \"Corporate 700 #007575 auf Weiß\", ratio: \"5,5:1, Links, farbige Überschriften\", badgeText: 'AA ✓', badgeClass: 'c-aa' },\n { bg: '#475705', fg: '#fff', mode: 'Light', name: \"Angewandte KI 800 #475705 auf Weiß\", ratio: \"8,1:1, Texteinsatz (700 mit 4,4:1 nicht ausreichend)\", badgeText: 'AAA ✓', badgeClass: 'c-aaa' },\n { bg: '#6B8208', fg: '#fff', mode: 'Light', name: \"Angewandte KI 700 #6B8208 auf Weiß\", ratio: \"4,4:1, knapp unter 4,5:1 AA-Schwelle\", badgeText: 'Fail ✗', badgeClass: 'c-fail-badge' },\n { bg: '#2F5FD4', fg: '#fff', mode: 'Light', name: \"Effektive Software 500 #2F5FD4 auf Weiß\", ratio: \"5,7:1, Links, farbige Überschriften, Akzentflächen · Button-Background nutzt `--es-700` (`.btn-es`)\", badgeText: 'AA ✓', badgeClass: 'c-aa' },\n { bg: '#1B3D9A', fg: '#fff', mode: 'Light', name: \"Effektive Software 700 #1B3D9A auf Weiß\", ratio: \"9,7:1, Fließtext, dunkle Überschriften\", badgeText: 'AAA ✓', badgeClass: 'c-aaa' },\n { bg: '#347A22', fg: '#fff', mode: 'Light', name: \"Wirksame Organisationen 600 #347A22 auf Weiß\", ratio: \"5,3:1, Buttons mit weißer Schrift (mind. 600)\", badgeText: 'AA ✓', badgeClass: 'c-aa' },\n { bg: '#285E1A', fg: '#fff', mode: 'Light', name: \"Wirksame Organisationen 700 #285E1A auf Weiß\", ratio: \"7,8:1, Texteinsatz, Links\", badgeText: 'AAA ✓', badgeClass: 'c-aaa' },\n { bg: '#C23060', fg: '#fff', mode: 'Light', name: \"Rosé 500 #C23060 auf Weiß\", ratio: \"5,3:1, Links, Buttons, farbige Labels\", badgeText: 'AA ✓', badgeClass: 'c-aa' },\n { bg: '#7E1639', fg: '#fff', mode: 'Light', name: \"Rosé 700 #7E1639 auf Weiß\", ratio: \"10,2:1, Texteinsatz, dunkle Überschriften\", badgeText: 'AAA ✓', badgeClass: 'c-aaa' },\n];\n\n<Unstyled>\n {contrastTextLight.map((row, i) => (\n <ContrastRow key={i} {...row} />\n ))}\n</Unstyled>\n\n## Kontrastverhältnisse · Akzentfarben (nicht für Text)\n\n| Farbe | Wert | Kontrast auf Weiß | Hinweis |\n|---|---|---|---|\n| Corporate 500 | #00BEBE | 2,3:1 | Nur als Hintergrundfläche, dunkler Text #002B2B (6,0:1 auf der Fläche) |\n| Angewandte KI 500 | #B5E61C | 1,4:1 | Ausschließlich als Akzent, Text #475705 auf der Fläche (5,6:1) |\n| Wirksame Organisationen 500 | #44A030 | 3,3:1 | Nur Large Text / UI-Komponenten, weiße Schrift auf 500 verfehlt (3,3:1), mindestens 600 verwenden |\n\nDer Block zeigt, wie kräftig die reine Akzentfläche wirkt und mit welcher (nicht weißen)\nTextfarbe die „Aa“-Beschriftung überhaupt lesbar bleibt, was die Tabelle nur als Fließtext\nnennt. Werte 1:1 aus `docs/index.html#gt-a11y-akzent` übernommen.\n\nexport const contrastAccent = [\n { bg: '#00BEBE', fg: '#002B2B', mode: 'Light', name: \"Corporate 500 #00BEBE, Akzentfläche\", ratio: \"2,3:1 auf Weiß, nur als Hintergrundfläche · dunkler Text: #002B2B (6,0:1 ✓)\", badgeText: 'Fläche only', badgeClass: 'c-fail-badge' },\n { bg: '#B5E61C', fg: '#475705', mode: 'Light', name: \"Angewandte KI 500 #B5E61C, Akzentfläche\", ratio: \"1,4:1 auf Weiß, ausschließlich als Akzent · Text: #475705 auf Fläche (5,6:1 ✓)\", badgeText: 'Fläche only', badgeClass: 'c-fail-badge' },\n { bg: '#44A030', fg: '#0A1F05', mode: 'Light', name: \"Wirksame Organisationen 500 #44A030\", ratio: \"3,3:1 auf Weiß, nur Large Text / UI-Komponenten · weiße Schrift: 3,3:1 ✗ → mind. 600 verwenden\", badgeText: 'AA Large ✓', badgeClass: 'c-aa' },\n];\n\n<Unstyled>\n {contrastAccent.map((row, i) => (\n <ContrastRow key={i} {...row} />\n ))}\n</Unstyled>\n\n## Kontrastverhältnisse · Dark Mode\n\nIm Dark Mode ist die Page-Background `#151A1F` (Token `--bg-page`), gehobene Flächen wie\nKarten sitzen auf `#28323D` (`--bg-surface`). Die Basisfläche trägt 17,5:1 gegen Weiß und\nliegt damit über der Material-Schwelle von 15,8:1, die sicherstellt, dass Fließtext auch\nauf der höchsten Elevationsstufe noch 4,5:1 erreicht.\n\n| Farbe | Wert (Dark) | Kontrast auf `#151A1F` | Einsatz | Stufe |\n|---|---|---|---|---|\n| `--tx-primary` | #DDE9E9 | 14,1:1 | Fließtext, Headlines, UI-Labels. Auf bg-surface 10,5:1 | AAA |\n| `--tx-secondary` | #A6C6C6 | 9,6:1 | Lead, Sub-Titel, Captions, Body in Karten. Auf bg-surface 7,1:1 | AAA |\n| `--tx-muted` | #93B6B6 | 8,0:1 | Gedämpfte Meta-Angaben. Auf bg-surface 6,0:1 (vorher 4,1:1, verfehlte dort AA) | AAA |\n| Corporate 200 | #80DEDE | 11,2:1 | Eyebrows, Card-Eyebrows, Bereichs-Text. Light-Pendant `--co-700` | AAA |\n| Corporate 300 | #4DD0D0 | 9,4:1 | Pillar-Titles, Code-Tokens, Footer-Links. Light-Pendant `--co-700` | AAA |\n| Angewandte KI 200 | #D6F06D | 13,8:1 | KI-Bereichstexte, Tab-Active-State. Light-Pendant `--ki-800` | AAA |\n| Effektive Software 200 | #98B7EE | 8,6:1 | ES-Bereichstexte. Light-Pendant `--es-700` | AAA |\n| Wirksame Organisationen 200 | #A3D48E | 10,3:1 | WO-Bereichstexte. Light-Pendant `--wo-700` | AAA |\n| `--c-success` | #5CE8A0 | 11,3:1 | Erfolgs-Marker in Do/Don't-Listen. Light-Pendant #0E6644 (6,99:1) | AAA |\n| `--c-warning` | #F0C060 | 10,4:1 | Hinweise, Beta-Zustände. Light-Pendant #8A5E0A (5,70:1) | AAA |\n| `--c-error` | #FFA5A5 | 9,3:1 | Fehler-Marker, Inline-Fehlertext, Required-Asterisks. Auf bg-surface 6,9:1. Light-Pendant #B22020 (6,73:1) | AAA |\n\nDer Block zeigt dieselben Farben, wie sie im Dark Mode tatsächlich erscheinen, inklusive\nder bei den Status-Farben vom Muster abweichenden Textfarbe für „Aa“, was die Tabelle nicht\nabbildet. Werte 1:1 aus `docs/index.html#gt-a11y-kontrast-dark` übernommen. Die Zeilen\ntragen ihre Dark-Werte als feste Hex-Angaben (kein CSS-Token, kein `data-theme`-Wrapper):\ngenau wie in `docs/index.html` selbst sind Fläche und „Aa“-Textfarbe pro Zeile hart codiert,\ndeshalb bleibt der Block unabhängig vom aktuell aktiven Storybook-Theme korrekt.\n\nexport const contrastDark = [\n { bg: '#DDE9E9', fg: '#151A1F', mode: 'Dark', name: \"`--tx-primary` #DDE9E9 auf #151A1F\", ratio: \"14,1:1, Fließtext, Headlines, UI-Labels · auf bg-surface 10,5:1\", badgeText: 'AAA ✓', badgeClass: 'c-aaa' },\n { bg: '#A6C6C6', fg: '#151A1F', mode: 'Dark', name: \"`--tx-secondary` #A6C6C6 auf #151A1F\", ratio: \"9,6:1, Lead, Sub-Titel, Captions, Body in Karten · auf bg-surface 7,1:1\", badgeText: 'AAA ✓', badgeClass: 'c-aaa' },\n { bg: '#93B6B6', fg: '#151A1F', mode: 'Dark', name: \"`--tx-muted` #93B6B6 auf #151A1F\", ratio: \"8,0:1, gedämpfte Meta-Angaben · auf bg-surface 6,0:1 (vorher 4,1:1, verfehlte dort AA)\", badgeText: 'AAA ✓', badgeClass: 'c-aaa' },\n { bg: '#80DEDE', fg: '#151A1F', mode: 'Dark', name: \"Corporate 200 #80DEDE auf #151A1F\", ratio: \"11,2:1, Eyebrows, Card-Eyebrows, Bereichs-Text · Light-Pendant: `--co-700`\", badgeText: 'AAA ✓', badgeClass: 'c-aaa' },\n { bg: '#4DD0D0', fg: '#151A1F', mode: 'Dark', name: \"Corporate 300 #4DD0D0 auf #151A1F\", ratio: \"9,4:1, Pillar-Titles, Code-Tokens, Footer-Links · Light-Pendant: `--co-700`\", badgeText: 'AAA ✓', badgeClass: 'c-aaa' },\n { bg: '#D6F06D', fg: '#151A1F', mode: 'Dark', name: \"Angewandte KI 200 #D6F06D auf #151A1F\", ratio: \"13,8:1, KI-Bereichstexte, Tab-Active-State · Light-Pendant: `--ki-800`\", badgeText: 'AAA ✓', badgeClass: 'c-aaa' },\n { bg: '#98B7EE', fg: '#151A1F', mode: 'Dark', name: \"Effektive Software 200 #98B7EE auf #151A1F\", ratio: \"8,6:1, ES-Bereichstexte · Light-Pendant: `--es-700`\", badgeText: 'AAA ✓', badgeClass: 'c-aaa' },\n { bg: '#A3D48E', fg: '#151A1F', mode: 'Dark', name: \"Wirksame Organisationen 200 #A3D48E auf #151A1F\", ratio: \"10,3:1, WO-Bereichstexte · Light-Pendant: `--wo-700`\", badgeText: 'AAA ✓', badgeClass: 'c-aaa' },\n { bg: '#5CE8A0', fg: '#092417', mode: 'Dark', name: \"`--c-success` #5CE8A0 auf #151A1F\", ratio: \"11,3:1, ✓-Marker in Do/Don't-Listen · Light-Pendant: #0E6644 (6,99:1 ✓)\", badgeText: 'AAA ✓', badgeClass: 'c-aaa' },\n { bg: '#F0C060', fg: '#271E0A', mode: 'Dark', name: \"`--c-warning` #F0C060 auf #151A1F\", ratio: \"10,4:1, Hinweise, Beta-Zustände · Light-Pendant: #8A5E0A (5,70:1 ✓)\", badgeText: 'AAA ✓', badgeClass: 'c-aaa' },\n { bg: '#FFA5A5', fg: '#3D0F0F', mode: 'Dark', name: \"`--c-error` #FFA5A5 auf #151A1F\", ratio: \"9,3:1, ✕-Marker, Inline-Fehlertext, Required-Asterisks · auf bg-surface 6,9:1 · Light-Pendant: #B22020 (6,73:1 ✓)\", badgeText: 'AAA ✓', badgeClass: 'c-aaa' },\n];\n\n<Unstyled>\n {contrastDark.map((row, i) => (\n <ContrastRow key={i} {...row} />\n ))}\n</Unstyled>\n\n## Tastaturnavigation\n\nJede Funktion ist ohne Maus erreichbar. Vier Kriterien greifen zusammen: 2.1.1 Keyboard\n(alles bedienbar), 2.1.2 No Keyboard Trap (überall wieder heraus), 2.4.3 Focus Order\n(sinnvolle Reihenfolge) und 2.4.7 Focus Visible (man sieht, wo man steht).\n\n- **Reihenfolge kommt aus dem Markup.** Die Tab-Folge ergibt sich aus der\n DOM-Reihenfolge, nicht aus der CSS-Position (WCAG 2.4.3). Regel: keine positiven\n `tabindex`-Werte, sie stellen die Kette global um.\n- **Sprungziele mit `tabindex=\"-1\"`.** Ein Anker allein bewegt in manchen Browsern nur den\n Scroll, nicht den Fokus. Landepunkte wie `#main-content` tragen deshalb\n `tabindex=\"-1\"`, programmatisch fokussierbar, aber nicht in der Tab-Kette.\n- **Skip-Link zuerst.** Erstes fokussierbares Element der Seite, außerhalb des Viewports\n geparkt und erst im Fokus sichtbar (WCAG 2.4.1). Überspringt die Navigation und setzt\n den Fokus auf den Hauptinhalt.\n- **Keine Tastaturfalle.** Jedes Element, das den Fokus einfängt, gibt ihn wieder her\n (WCAG 2.1.2). Escape schließt Submenüs, Dropdowns und die Suche und setzt den Fokus auf\n das auslösende Element zurück.\n- **Fokus darf nicht verdeckt sein.** Das fokussierte Element muss sichtbar bleiben, nicht\n hinter einer klebenden Leiste liegen (2.4.11, neu in WCAG 2.2).\n- **Ein Bauteil, ein Muster.** Gleichartige Bauteile nehmen dieselben Tasten. Alles, was\n eine Liste von Optionen öffnet (Submenü, Custom Select, Combobox), reagiert auf\n Pfeil-runter/-hoch, Home/End und Escape.\n\n| Bauteil | Tasten | Verhalten |\n|---|---|---|\n| Skip-Link | Enter | setzt den Fokus auf den Hauptinhalt |\n| Topnav-Submenü | Enter, Space, Pfeil ↓/↑, Home, End, Esc | Caret öffnet, Pfeile wandern durch die Einträge, Esc schließt und gibt den Fokus zurück |\n| Custom Select | Enter, Space, Pfeil ↓/↑, Home, End, Esc, Tab, Buchstabe | öffnet und wählt, ein Buchstabe springt zur passenden Option (Puffer 600 ms), Tab schließt und geht weiter |\n| Combobox & Multi-Select | Pfeil ↓/↑, Home, End, Enter, Esc, Backspace | wie Select, Backspace im leeren Feld entfernt die letzte Auswahl |\n| Bild-Carousel | Pfeil ←/→ | eine Slide zurück oder vor |\n| Logo-Karussell | Pfeil ←/↑/→/↓, Home, End | auf den Pagination-Dots, jede Taste pausiert zusätzlich den Auto-Wechsel |\n| Such-Feld der Beitragsübersicht | `/`, Esc | `/` fokussiert die Suche, Esc leert das Feld |\n\n## Touch Targets\n\nZwei Kriterien regeln die Zielgröße, und sie verlangen nicht dasselbe: 2.5.8 Target Size\n(Minimum) fordert auf AA mindestens 24 × 24 px, 2.5.5 Target Size (Enhanced) auf AAA\nmindestens 44 × 44 px. 2.5.8 ist neu in WCAG 2.2, in 2.1 gibt es auf AA gar keine\nZielgrößen-Anforderung. Das System zielt auf 44 px, liegt damit über der AA-Pflicht und\ntrifft zugleich die Empfehlung der Plattform-Guidelines. Wo 44 px nicht erreichbar sind,\ngilt 24 px als harte Untergrenze.\n\nAbstand zählt mit: 2.5.8 lässt kleinere Ziele zu, wenn genug Freiraum um sie liegt.\nZwischen benachbarten Zielen deshalb mindestens `--s2` (8 px), damit ein Daumen nicht zwei\nAktionen gleichzeitig trifft.\n\n| Bauteil | Maß | Wo gesetzt |\n|---|---|---|\n| Buttons, alle Varianten | `min-height: 44px` | `.btn` |\n| Formularfelder (Input, Textarea, Select) | `min-height: 44px` | `.field input`, `textarea`, `select` |\n| Back-to-Top-Button | 44 × 44 px | `.back-to-top` |\n| Carousel-Pfeile | 44 × 44 px | `.img-slider-btn` |\n| Beispielseiten-Tabs und Aufklapp-Caret | 44 px | `.ep-tab`, `.ep-tab-toggle` |\n| Topnav-Suchfeld | `height: 44px` | `.ep-nav-search-input` |\n| Topnav-Caret, mobil | 44 × 44 px | `.ep-nav-item-toggle` im Mobil-Breakpoint |\n| Snackbar | `min-height: 48px` | `.snack` |\n\nZwei bewusste Abweichungen: Segmented Control (`.seg-option label`) und Area Tabs\n(`.atab`) liegen bei `min-height: 40px`. Beide sind Zeilen in einer dicht gesetzten\nUmschaltgruppe, in der 44 px die Leiste optisch auseinanderziehen würden. Sie erfüllen\n2.5.8 (AA) deutlich, verfehlen 2.5.5 (AAA) um 4 px. Neue Bauteile orientieren sich an\n44 px, nicht an diesen beiden.\n\n**Die 44-px-Ausnahme von der Spacing-Skala:** Die 44 stehen hartcodiert im CSS, weil die\nSpacing-Skala keinen 44-px-Schritt hat. Das ist eine der erlaubten Ausnahmen der\nStrict-Scale-Konvention (siehe `Grundlagen/Spacing & Grid`): Werte, die eine physische\nMindestgröße beschreiben, folgen ihrer eigenen Logik statt dem 8pt-Rhythmus.\n\n## ARIA Patterns\n\n**Focus-Ring.** Sichtbarer Fokusindikator für alle interaktiven Elemente (WCAG 2.4.7).\n\n```css\n--focus-ring: 0 0 0 3px var(--co-50), 0 0 0 5px var(--co-700);\n:focus-visible {\n outline: none;\n box-shadow: var(--focus-ring);\n}\n```\n\n**Formulare & Fehler.** Fehlermeldungen programmatisch mit dem Feld verknüpfen (WCAG\n1.3.1, 3.3.1).\n\n```html\n<input aria-invalid=\"true\" aria-describedby=\"err-id\">\n<span id=\"err-id\" role=\"alert\">Pflichtfeld</span>\n```\n\n**Dekorative Icons.** SVGs ohne inhaltliche Bedeutung vor Screenreadern verbergen (WCAG\n1.1.1).\n\n```html\n<svg aria-hidden=\"true\" focusable=\"false\" viewBox=\"0 0 24 24\">\n <path d=\"…\"/>\n</svg>\n```\n\n**Icon-only Buttons.** Buttons ohne sichtbares Label brauchen ein zugängliches\nTextalternativ (WCAG 1.1.1).\n\n```html\n<button aria-label=\"Menü öffnen\">\n <svg aria-hidden=\"true\" focusable=\"false\">…</svg>\n</button>\n```\n\n**Tab Panels.** Tabs als Tablist mit vollständigen ARIA-Relationen (WCAG 4.1.2).\n\n```html\n<div role=\"tablist\">\n <button role=\"tab\" aria-selected=\"true\" aria-controls=\"panel-1\" id=\"tab-1\">Tab 1</button>\n</div>\n<div role=\"tabpanel\" id=\"panel-1\" aria-labelledby=\"tab-1\" tabindex=\"0\">…</div>\n```\n\n**Live Regions.** Dynamische Inhalte für Screenreader ankündigen (WCAG 4.1.3).\n\n```html\n<!-- Status, höflich, unterbricht nicht -->\n<div role=\"status\" aria-live=\"polite\">Gespeichert.</div>\n\n<!-- Alert, sofort, unterbricht -->\n<div role=\"alert\" aria-live=\"assertive\">Verbindungsfehler.</div>\n```\n\n**Toggle & Filter-Buttons.** Buttons mit zwei Zuständen (gedrückt, nicht gedrückt)\nbrauchen `aria-pressed`, damit Screenreader den Status ansagen (WCAG 4.1.2). Verwendung\nunter anderem bei Filter-Chips.\n\n```html\n<button class=\"chip\" aria-pressed=\"true\" data-area=\"ki\">Angewandte KI</button>\n\n/* Style stützt sich auf das ARIA-Attribut, nicht auf eine separate\n .sel- oder .active-Klasse */\n.chip[aria-pressed=\"true\"] { … }\n```\n\n**Such-Feld als Landmark.** Such-Bereiche bekommen `role=\"search\"` plus eigenes\n`aria-label`, damit wird das Suchfeld zur Landmark, die Screenreader-Nutzerinnen direkt\nanspringen (WCAG 1.3.1, 2.4.6).\n\n```html\n<div role=\"search\" aria-label=\"Beiträge durchsuchen\">\n <svg aria-hidden=\"true\" focusable=\"false\">…</svg>\n <input type=\"search\" aria-label=\"Beiträge nach Stichwort durchsuchen\">\n</div>\n```\n\n**Aktive Navigation.** Der aktive Eintrag in einer Navigation wird via\n`aria-current=\"page\"` markiert, ein sichtbarer Active-State allein reicht für\nScreenreader nicht (WCAG 1.3.1).\n\n```html\n<a class=\"ep-nav-btn\" href=\"/ueber-uns\" aria-current=\"page\">Unternehmen</a>\n\n.ep-nav-btn[aria-current=\"page\"] { color: var(--co-700); }\n```\n\n**Visuell versteckt (`.sr-only`).** Text nur für Screenreader, visuell entfernt. Trägt die\nBedeutung hinter rein grafischen Markern (Häkchen, Minus) in Vergleichstabellen, Glyph\nallein reicht nicht (WCAG 1.1.1, 1.3.1).\n\n```html\n<td>\n <span class=\"ep-compare-yes\" aria-hidden=\"true\">✓</span>\n <span class=\"sr-only\">Enthalten</span>\n</td>\n\n.sr-only {\n position: absolute; width: 1px; height: 1px;\n margin: -1px; padding: 0; overflow: hidden;\n clip: rect(0,0,0,0); white-space: nowrap; border: 0;\n}\n```\n\n## Reduzierte Bewegung & High Contrast\n\n**`prefers-reduced-motion`.** Nutzer mit Gleichgewichts- oder Aufmerksamkeitsstörungen\nkönnen Animationen deaktivieren (WCAG 2.3.3 AAA).\n\n```css\n@media (prefers-reduced-motion: reduce) {\n *, *::before, *::after {\n animation-duration: 0.01ms !important;\n transition-duration: 0.01ms !important;\n scroll-behavior: auto !important;\n }\n}\n```\n\n**`forced-colors` (High Contrast).** Windows High Contrast Mode ersetzt Farben durch\nSystemfarben, Grenzen durch Borders statt nur Farbe ausdrücken.\n\n```css\n@media (forced-colors: active) {\n .btn-filled {\n border: 2px solid ButtonText;\n forced-color-adjust: none;\n }\n .btn-outlined { border-color: ButtonText; }\n}\n```\n\n**Theme-Umschaltung.** Das Design System kennt genau zwei Modi, Light und Dark.\nUmgeschaltet wird ausschließlich über das Attribut `[data-theme=\"dark\"]` am `<html>`.\nEinen dritten Modus, der der Betriebssystem-Präferenz folgt, gibt es bewusst nicht,\n`prefers-color-scheme` wird im ausgelieferten CSS nicht ausgewertet, Standard ist Light.\n`color-scheme: dark` ist im Dark-Theme fest gesetzt, damit native Controls (Checkbox,\nRadio, Scrollbar, Select-Popup) dark-thematisiert rendern.\n\n**Smooth-Scroll für In-Page-Anker.** Klicks auf In-Page-Anker innerhalb derselben Seite\nanimieren das Scrollen statt hart zu springen. Bei `prefers-reduced-motion: reduce` wird\nder Hard-Jump beibehalten, weil sanftes Scrollen für vestibulär empfindliche Nutzerinnen\nSchwindel auslösen kann (WCAG 2.3.3).\n\n## Video-Embed\n\nVideos (zum Beispiel YouTube) werden als responsiver `<iframe>` eingebunden. Ein\nGroßteil der Barrierefreiheit hängt am Videoinhalt selbst (Untertitel, Transkript) und\nlässt sich nicht allein über das Embed erzwingen.\n\n```html\n<iframe\n src=\"https://www.youtube-nocookie.com/embed/ID\"\n title=\"Was ist Keycloak? Erklärt von …\"\n loading=\"lazy\"\n referrerpolicy=\"strict-origin-when-cross-origin\"\n allowfullscreen\n style=\"width:100%;aspect-ratio:16/9;border:0;border-radius:var(--r-lg)\"></iframe>\n```\n\n- **iframe braucht `title`.** Jeder Video-iframe bekommt einen aussagekräftigen `title`\n (WCAG 4.1.2), Screenreader kündigen den Frame damit benannt an.\n- **Untertitel (WCAG 1.2.2, AA).** Aufgezeichnete Videos brauchen Untertitel. Automatische\n YouTube-Untertitel allein erfüllen die Anforderung nicht (Qualität, Zeichensetzung).\n Fremdsprachige Videos bekommen Untertitel in der Seitensprache.\n- **Transkript & Audiodeskription.** Ein Transkript in der Nähe des Videos hilft allen,\n die es nicht abspielen können, und dient Suchmaschinen. Enthält das Bild Informationen,\n die im Ton fehlen, ist zusätzlich eine Audiodeskription nötig (WCAG 1.2.3/1.2.5).\n- **Kein Autoplay.** Nie `autoplay=1` setzen, Ton und Bewegung starten erst auf Klick\n (WCAG 1.4.2, 2.2.2), das respektiert auch `prefers-reduced-motion`.\n- **Responsiv und ohne Rahmen.** `width:100%` plus `aspect-ratio:16/9` hält das Video\n seitenbreit ohne horizontales Scrollen.\n- **Datenschutz.** Einbindung über `youtube-nocookie.com` und `loading=\"lazy\"` reduziert\n Tracking und Ladelast. Für die produktive Seite gilt zusätzlich: erst nach Einwilligung\n laden, da beim Abspielen Cookies gesetzt werden.\n- **Sprache kennzeichnen.** Weicht die Videosprache von der Seitensprache ab, im\n umgebenden Text ausweisen (zum Beispiel Videosprache Englisch).\n- **Fokus & Tastatur.** Der YouTube-Player ist tastaturbedienbar und beschriftet, der\n iframe ist fokussierbar, keine positiven `tabindex`-Werte vergeben.\n\nWas das Design-System liefert versus was der Videoinhalt liefern muss: Embed-seitig sind\n`title`, kein Autoplay, responsives 16:9, Datenschutz-Host und Sprach-Hinweis abgedeckt.\nUntertitel, Transkript und Audiodeskription hängen an der Videoquelle und müssen beim\nUpload hinterlegt werden, das kann das Design System nicht erzwingen.\n\n## Gates\n\n- `npm run check:contrast` rendert `docs/index.html` in beiden Modi in Chromium und muss\n 0 Befunde melden (Text gegen 4,5:1 beziehungsweise 3:1 bei Großtext, getönte\n Bauteil-Füllungen gegen den 1,3:1-Faustwert, Bedienelement-Rahmen gegen 3:1).\n- `npm run check:dark-states` findet Zustände (Hover, Focus, Active), die im Dark Mode\n dunkel auf dunkel und damit unlesbar würden.\n- Storybook fährt `@storybook/addon-a11y` global scharf: `a11y: { test: 'error' }` in\n `.storybook/preview.ts`, axe-Verstöße lassen den Test-Runner fehlschlagen.\n- Einzelne Stories mit einem bekannten, im CSS-Kern liegenden Befund dürfen lokal\n `a11y: { test: 'todo' }` setzen (im Panel weiter sichtbar, aber nicht blockierend). Zum\n Zeitpunkt dieser Seite sind in `storybook-angular/src/lib` **keine** aktiven\n `test: 'todo'`-Overrides gesetzt. `storybook-angular/README.md` dokumentiert drei\n frühere, inzwischen im CSS-Kern behobene Befunde (DownloadCta-Eyebrow, CodeBlock-Copy,\n Slider-Wertanzeige) als Referenz dafür, wie eine solche Ausnahme aussieht und wann sie\n wieder scharf geschaltet wird.\n\n## Siehe auch\n\n- `docs/index.html#gt-a11y-touch` (44-px-Ausnahme)\n- CONTRIBUTING.md § 1 Grundprinzipien, § 3 Farbe & Kontrast\n- `storybook-angular/README.md`, Abschnitt „Frühere a11y-Befunde im CSS-Kern (behoben)“\n",
12
+ "content": "import { Meta, Unstyled } from '@storybook/addon-docs/blocks';\n\n<Meta title=\"Grundlagen/Barrierefreiheit\" name=\"Übersicht\" />\n\n# Barrierefreiheit\n\nKontrastverhältnisse aller Brand-Farben, Tastaturnavigation, ARIA Patterns, Touch\nTargets, Reduced Motion. Pflicht sind die Stufen A und AA, AAA nehmen wir mit, wo es ohne\nNachteil geht. Vollständige Kriterien-Übersicht:\n[WCAG 2.2 Quickref (W3C)](https://www.w3.org/WAI/WCAG22/quickref/). Quelle:\n`docs/index.html#sec-a11y`.\n\n## Anspruch\n\nDas System ist auf Konformitätsstufe AA gebaut. Nach WCAG-Definition heißt das: alle\nErfolgskriterien der Stufe A und alle der Stufe AA sind erfüllt. AA liegt also nicht über\nA, sondern schließt es ein. Der Anspruch gilt für jedes Kriterium, nicht nur für Kontrast,\nebenso für Tastaturbedienung, Fokus, Struktur, Beschriftung, Bewegung und Zielgrößen.\n\nStufe AAA ist Zugabe, kein Abnahmekriterium. Wo sie ohne Nachteil für Gestaltung oder\nVerständlichkeit erreichbar ist, nehmen wir sie mit. Wo sie etwas kostet, bleibt AA der\nMaßstab. An einem AAA-Kriterium scheitert keine Änderung.\n\n**Pflicht, A und AA.** Gilt für alles, was das System ausliefert. Zwei Prüfungen laufen\nautomatisch: `npm run check:contrast` rendert die Doku in beiden Modi und muss 0 melden,\n`npm run check:dark-states` findet Zustände, die im Dark Mode unlesbar würden. Alles, was\nsich nicht messen lässt (Tastaturpfad, Screenreader-Ausgabe), gehört in die Prüfung von\nHand.\n\n**Freiwillig darüber.** An vielen Stellen liegt das System über der Pflicht. Die meisten\nTextfarben erreichen 7:1 und mehr (AAA). Touch-Targets liegen bei 44 px, das ist 2.5.5 auf\nAAA, verlangt wären 24 px. Diese Marken zeigen den Überschuss, nicht die Messlatte, sie\ndürfen an einzelnen Stellen fehlen, ohne dass etwas kaputt ist.\n\n**Nicht erfüllt, bewusst.** 2.3.3 Animation from Interactions (AAA) verlangt, dass eine\ndurch Bedienung ausgelöste Bewegung abschaltbar ist. Der Hover-Lift von 2 px auf\nklickbaren Karten und auf dem Störer läuft auch bei `prefers-reduced-motion: reduce`, weil\ner die Klickbarkeit signalisiert. Auto-Bewegung ist davon getrennt, die Carousels\nrespektieren die Einstellung.\n\n## Kontrastverhältnisse · Textfarben auf Weiß\n\nDie Modus-Markierung zeigt, auf welchen Theme-Modus sich ein Wert bezieht: Light für die\nhellen Token-Werte (Page-Background `#FFFFFF`), Dark für die dunklen Pendants\n(Page-Background `#151A1F`).\n\n| Farbe | Wert (Light) | Kontrast auf Weiß | Einsatz | Stufe |\n|---|---|---|---|---|\n| `--tx-primary` | #333E48 | 10,4:1 | Fließtext, Headlines, UI-Labels | AAA |\n| `--tx-secondary` | #4A6565 | 5,8:1 | Lead, Sub-Titel, Captions | AA |\n| `--tx-muted` | #5A7171 | 5,25:1 | Leise Texte: Eyebrow, Counter, Meta-Angaben, Captions. Abgedunkelt von #6E8585 (3,9:1), das AA für Normaltext verfehlte | AA |\n| Corporate 700 | #007575 | 5,5:1 | Links, farbige Überschriften | AA |\n| Angewandte KI 800 | #475705 | 8,1:1 | Texteinsatz (700 mit 4,4:1 nicht ausreichend) | AAA |\n| Angewandte KI 700 | #6B8208 | 4,4:1 | Knapp unter 4,5:1 AA-Schwelle | Fail |\n| Effektive Software 500 | #2F5FD4 | 5,7:1 | Links, farbige Überschriften, Akzentflächen. Button-Background nutzt `--es-700` (`.btn-es`) | AA |\n| Effektive Software 700 | #1B3D9A | 9,7:1 | Fließtext, dunkle Überschriften | AAA |\n| Wirksame Organisationen 600 | #347A22 | 5,3:1 | Buttons mit weißer Schrift (mindestens 600) | AA |\n| Wirksame Organisationen 700 | #285E1A | 7,8:1 | Texteinsatz, Links | AAA |\n| Rosé 500 | #C23060 | 5,3:1 | Links, Buttons, farbige Labels | AA |\n| Rosé 700 | #7E1639 | 10,2:1 | Texteinsatz, dunkle Überschriften | AAA |\n\nAngewandte-KI-700 ist die einzige Zeile, die AA für Normaltext verfehlt (4,4:1 gegen die\n4,5:1-Schwelle), deshalb trägt farbiger KI-Text `--ki-800`, nicht `--ki-700`.\n\nDiese Tabelle zeigt nur die reinen Kontrastwerte. Welches Token (Ink, Fill, Band oder\nTönung) für welche Rolle passt und wann eine Bereichsfarbe als Textfarbe überhaupt in\nFrage kommt, beschreibt `Grundlagen/Farben` ausführlich, inklusive Dos & Don'ts.\n\nDer folgende Block zeigt zusätzlich zur Tabelle die tatsächliche Farbe jedes Tokens als\nFläche und macht die Lesbarkeit der weißen „Aa“-Beschriftung direkt sichtbar, statt sie nur\nals Zahl zu benennen. Werte 1:1 aus `docs/index.html#gt-a11y-kontrast` übernommen.\n\nexport const withCode = (text) =>\n text.split('`').map((part, i) =>\n i % 2 === 1 ? <code key={i} className=\"token\">{part}</code> : part\n );\n\nexport const ContrastRow = ({ bg, fg, mode, name, ratio, badgeText, badgeClass }) => (\n <div className=\"contrast-row\">\n <div className=\"cswatch\" style={{ background: bg, color: fg }}>Aa</div>\n <div className=\"cinfo\">\n <div className=\"cname\">\n <span className=\"a11y-mode\">{mode}</span>\n {withCode(name)}\n </div>\n <div className=\"cratio\">{withCode(ratio)}</div>\n </div>\n <span className={`cbadge ${badgeClass}`}>{badgeText}</span>\n </div>\n);\n\nexport const contrastTextLight = [\n { bg: '#333E48', fg: '#fff', mode: 'Light', name: \"`--tx-primary` #333E48 auf Weiß\", ratio: \"10,4:1, Fließtext, Headlines, UI-Labels\", badgeText: 'AAA ✓', badgeClass: 'c-aaa' },\n { bg: '#4A6565', fg: '#fff', mode: 'Light', name: \"`--tx-secondary` #4A6565 auf Weiß\", ratio: \"5,8:1, Lead, Sub-Titel, Captions\", badgeText: 'AA ✓', badgeClass: 'c-aa' },\n { bg: '#5A7171', fg: '#fff', mode: 'Light', name: \"`--tx-muted` #5A7171 auf Weiß\", ratio: \"5,25:1, leise Texte: Eyebrow, Counter, Meta-Angaben, Captions · abgedunkelt von #6E8585 (3,9:1), das AA für Normaltext verfehlte\", badgeText: 'AA ✓', badgeClass: 'c-aa' },\n { bg: '#007575', fg: '#fff', mode: 'Light', name: \"Corporate 700 #007575 auf Weiß\", ratio: \"5,5:1, Links, farbige Überschriften\", badgeText: 'AA ✓', badgeClass: 'c-aa' },\n { bg: '#475705', fg: '#fff', mode: 'Light', name: \"Angewandte KI 800 #475705 auf Weiß\", ratio: \"8,1:1, Texteinsatz (700 mit 4,4:1 nicht ausreichend)\", badgeText: 'AAA ✓', badgeClass: 'c-aaa' },\n { bg: '#6B8208', fg: '#fff', mode: 'Light', name: \"Angewandte KI 700 #6B8208 auf Weiß\", ratio: \"4,4:1, knapp unter 4,5:1 AA-Schwelle\", badgeText: 'Fail ✗', badgeClass: 'c-fail-badge' },\n { bg: '#2F5FD4', fg: '#fff', mode: 'Light', name: \"Effektive Software 500 #2F5FD4 auf Weiß\", ratio: \"5,7:1, Links, farbige Überschriften, Akzentflächen · Button-Background nutzt `--es-700` (`.btn-es`)\", badgeText: 'AA ✓', badgeClass: 'c-aa' },\n { bg: '#1B3D9A', fg: '#fff', mode: 'Light', name: \"Effektive Software 700 #1B3D9A auf Weiß\", ratio: \"9,7:1, Fließtext, dunkle Überschriften\", badgeText: 'AAA ✓', badgeClass: 'c-aaa' },\n { bg: '#347A22', fg: '#fff', mode: 'Light', name: \"Wirksame Organisationen 600 #347A22 auf Weiß\", ratio: \"5,3:1, Buttons mit weißer Schrift (mind. 600)\", badgeText: 'AA ✓', badgeClass: 'c-aa' },\n { bg: '#285E1A', fg: '#fff', mode: 'Light', name: \"Wirksame Organisationen 700 #285E1A auf Weiß\", ratio: \"7,8:1, Texteinsatz, Links\", badgeText: 'AAA ✓', badgeClass: 'c-aaa' },\n { bg: '#C23060', fg: '#fff', mode: 'Light', name: \"Rosé 500 #C23060 auf Weiß\", ratio: \"5,3:1, Links, Buttons, farbige Labels\", badgeText: 'AA ✓', badgeClass: 'c-aa' },\n { bg: '#7E1639', fg: '#fff', mode: 'Light', name: \"Rosé 700 #7E1639 auf Weiß\", ratio: \"10,2:1, Texteinsatz, dunkle Überschriften\", badgeText: 'AAA ✓', badgeClass: 'c-aaa' },\n];\n\n<Unstyled>\n {contrastTextLight.map((row, i) => (\n <ContrastRow key={i} {...row} />\n ))}\n</Unstyled>\n\n## Kontrastverhältnisse · Akzentfarben (nicht für Text)\n\n| Farbe | Wert | Kontrast auf Weiß | Hinweis |\n|---|---|---|---|\n| Corporate 500 | #00BEBE | 2,3:1 | Nur als Hintergrundfläche, dunkler Text #002B2B (6,0:1 auf der Fläche) |\n| Angewandte KI 500 | #B5E61C | 1,4:1 | Ausschließlich als Akzent, Text #475705 auf der Fläche (5,6:1) |\n| Wirksame Organisationen 500 | #44A030 | 3,3:1 | Nur Large Text / UI-Komponenten, weiße Schrift auf 500 verfehlt (3,3:1), mindestens 600 verwenden |\n\nDer Block zeigt, wie kräftig die reine Akzentfläche wirkt und mit welcher (nicht weißen)\nTextfarbe die „Aa“-Beschriftung überhaupt lesbar bleibt, was die Tabelle nur als Fließtext\nnennt. Werte 1:1 aus `docs/index.html#gt-a11y-akzent` übernommen.\n\nexport const contrastAccent = [\n { bg: '#00BEBE', fg: '#002B2B', mode: 'Light', name: \"Corporate 500 #00BEBE, Akzentfläche\", ratio: \"2,3:1 auf Weiß, nur als Hintergrundfläche · dunkler Text: #002B2B (6,0:1 ✓)\", badgeText: 'Fläche only', badgeClass: 'c-fail-badge' },\n { bg: '#B5E61C', fg: '#475705', mode: 'Light', name: \"Angewandte KI 500 #B5E61C, Akzentfläche\", ratio: \"1,4:1 auf Weiß, ausschließlich als Akzent · Text: #475705 auf Fläche (5,6:1 ✓)\", badgeText: 'Fläche only', badgeClass: 'c-fail-badge' },\n { bg: '#44A030', fg: '#0A1F05', mode: 'Light', name: \"Wirksame Organisationen 500 #44A030\", ratio: \"3,3:1 auf Weiß, nur Large Text / UI-Komponenten · weiße Schrift: 3,3:1 ✗ → mind. 600 verwenden\", badgeText: 'AA Large ✓', badgeClass: 'c-aa' },\n];\n\n<Unstyled>\n {contrastAccent.map((row, i) => (\n <ContrastRow key={i} {...row} />\n ))}\n</Unstyled>\n\n## Kontrastverhältnisse · Dark Mode\n\nIm Dark Mode ist die Page-Background `#151A1F` (Token `--bg-page`), gehobene Flächen wie\nKarten sitzen auf `#28323D` (`--bg-surface`). Die Basisfläche trägt 17,5:1 gegen Weiß und\nliegt damit über der Material-Schwelle von 15,8:1, die sicherstellt, dass Fließtext auch\nauf der höchsten Elevationsstufe noch 4,5:1 erreicht.\n\n| Farbe | Wert (Dark) | Kontrast auf `#151A1F` | Einsatz | Stufe |\n|---|---|---|---|---|\n| `--tx-primary` | #DDE9E9 | 14,1:1 | Fließtext, Headlines, UI-Labels. Auf bg-surface 10,5:1 | AAA |\n| `--tx-secondary` | #A6C6C6 | 9,6:1 | Lead, Sub-Titel, Captions, Body in Karten. Auf bg-surface 7,1:1 | AAA |\n| `--tx-muted` | #93B6B6 | 8,0:1 | Gedämpfte Meta-Angaben. Auf bg-surface 6,0:1 (vorher 4,1:1, verfehlte dort AA) | AAA |\n| Corporate 200 | #80DEDE | 11,2:1 | Eyebrows, Card-Eyebrows, Bereichs-Text. Light-Pendant `--co-700` | AAA |\n| Corporate 300 | #4DD0D0 | 9,4:1 | Pillar-Titles, Code-Tokens, Footer-Links. Light-Pendant `--co-700` | AAA |\n| Angewandte KI 200 | #D6F06D | 13,8:1 | KI-Bereichstexte, Tab-Active-State. Light-Pendant `--ki-800` | AAA |\n| Effektive Software 200 | #98B7EE | 8,6:1 | ES-Bereichstexte. Light-Pendant `--es-700` | AAA |\n| Wirksame Organisationen 200 | #A3D48E | 10,3:1 | WO-Bereichstexte. Light-Pendant `--wo-700` | AAA |\n| `--c-success` | #5CE8A0 | 11,3:1 | Erfolgs-Marker in Do/Don't-Listen. Light-Pendant #0E6644 (6,99:1) | AAA |\n| `--c-warning` | #F0C060 | 10,4:1 | Hinweise, Beta-Zustände. Light-Pendant #8A5E0A (5,70:1) | AAA |\n| `--c-error` | #FFA5A5 | 9,3:1 | Fehler-Marker, Inline-Fehlertext, Required-Asterisks. Auf bg-surface 6,9:1. Light-Pendant #B22020 (6,73:1) | AAA |\n\nDer Block zeigt dieselben Farben, wie sie im Dark Mode tatsächlich erscheinen, inklusive\nder bei den Status-Farben vom Muster abweichenden Textfarbe für „Aa“, was die Tabelle nicht\nabbildet. Werte 1:1 aus `docs/index.html#gt-a11y-kontrast-dark` übernommen. Die Zeilen\ntragen ihre Dark-Werte als feste Hex-Angaben (kein CSS-Token, kein `data-theme`-Wrapper):\ngenau wie in `docs/index.html` selbst sind Fläche und „Aa“-Textfarbe pro Zeile hart codiert,\ndeshalb bleibt der Block unabhängig vom aktuell aktiven Storybook-Theme korrekt.\n\nexport const contrastDark = [\n { bg: '#DDE9E9', fg: '#151A1F', mode: 'Dark', name: \"`--tx-primary` #DDE9E9 auf #151A1F\", ratio: \"14,1:1, Fließtext, Headlines, UI-Labels · auf bg-surface 10,5:1\", badgeText: 'AAA ✓', badgeClass: 'c-aaa' },\n { bg: '#A6C6C6', fg: '#151A1F', mode: 'Dark', name: \"`--tx-secondary` #A6C6C6 auf #151A1F\", ratio: \"9,6:1, Lead, Sub-Titel, Captions, Body in Karten · auf bg-surface 7,1:1\", badgeText: 'AAA ✓', badgeClass: 'c-aaa' },\n { bg: '#93B6B6', fg: '#151A1F', mode: 'Dark', name: \"`--tx-muted` #93B6B6 auf #151A1F\", ratio: \"8,0:1, gedämpfte Meta-Angaben · auf bg-surface 6,0:1 (vorher 4,1:1, verfehlte dort AA)\", badgeText: 'AAA ✓', badgeClass: 'c-aaa' },\n { bg: '#80DEDE', fg: '#151A1F', mode: 'Dark', name: \"Corporate 200 #80DEDE auf #151A1F\", ratio: \"11,2:1, Eyebrows, Card-Eyebrows, Bereichs-Text · Light-Pendant: `--co-700`\", badgeText: 'AAA ✓', badgeClass: 'c-aaa' },\n { bg: '#4DD0D0', fg: '#151A1F', mode: 'Dark', name: \"Corporate 300 #4DD0D0 auf #151A1F\", ratio: \"9,4:1, Pillar-Titles, Code-Tokens, Footer-Links · Light-Pendant: `--co-700`\", badgeText: 'AAA ✓', badgeClass: 'c-aaa' },\n { bg: '#D6F06D', fg: '#151A1F', mode: 'Dark', name: \"Angewandte KI 200 #D6F06D auf #151A1F\", ratio: \"13,8:1, KI-Bereichstexte, Tab-Active-State · Light-Pendant: `--ki-800`\", badgeText: 'AAA ✓', badgeClass: 'c-aaa' },\n { bg: '#98B7EE', fg: '#151A1F', mode: 'Dark', name: \"Effektive Software 200 #98B7EE auf #151A1F\", ratio: \"8,6:1, ES-Bereichstexte · Light-Pendant: `--es-700`\", badgeText: 'AAA ✓', badgeClass: 'c-aaa' },\n { bg: '#A3D48E', fg: '#151A1F', mode: 'Dark', name: \"Wirksame Organisationen 200 #A3D48E auf #151A1F\", ratio: \"10,3:1, WO-Bereichstexte · Light-Pendant: `--wo-700`\", badgeText: 'AAA ✓', badgeClass: 'c-aaa' },\n { bg: '#5CE8A0', fg: '#092417', mode: 'Dark', name: \"`--c-success` #5CE8A0 auf #151A1F\", ratio: \"11,3:1, ✓-Marker in Do/Don't-Listen · Light-Pendant: #0E6644 (6,99:1 ✓)\", badgeText: 'AAA ✓', badgeClass: 'c-aaa' },\n { bg: '#F0C060', fg: '#271E0A', mode: 'Dark', name: \"`--c-warning` #F0C060 auf #151A1F\", ratio: \"10,4:1, Hinweise, Beta-Zustände · Light-Pendant: #8A5E0A (5,70:1 ✓)\", badgeText: 'AAA ✓', badgeClass: 'c-aaa' },\n { bg: '#FFA5A5', fg: '#3D0F0F', mode: 'Dark', name: \"`--c-error` #FFA5A5 auf #151A1F\", ratio: \"9,3:1, ✕-Marker, Inline-Fehlertext, Required-Asterisks · auf bg-surface 6,9:1 · Light-Pendant: #B22020 (6,73:1 ✓)\", badgeText: 'AAA ✓', badgeClass: 'c-aaa' },\n];\n\n<Unstyled>\n {contrastDark.map((row, i) => (\n <ContrastRow key={i} {...row} />\n ))}\n</Unstyled>\n\n## Tastaturnavigation\n\nJede Funktion ist ohne Maus erreichbar. Vier Kriterien greifen zusammen: 2.1.1 Keyboard\n(alles bedienbar), 2.1.2 No Keyboard Trap (überall wieder heraus), 2.4.3 Focus Order\n(sinnvolle Reihenfolge) und 2.4.7 Focus Visible (man sieht, wo man steht).\n\n- **Reihenfolge kommt aus dem Markup.** Die Tab-Folge ergibt sich aus der\n DOM-Reihenfolge, nicht aus der CSS-Position (WCAG 2.4.3). Regel: keine positiven\n `tabindex`-Werte, sie stellen die Kette global um.\n- **Sprungziele mit `tabindex=\"-1\"`.** Ein Anker allein bewegt in manchen Browsern nur den\n Scroll, nicht den Fokus. Landepunkte wie `#main-content` tragen deshalb\n `tabindex=\"-1\"`, programmatisch fokussierbar, aber nicht in der Tab-Kette.\n- **Skip-Link zuerst.** Erstes fokussierbares Element der Seite, außerhalb des Viewports\n geparkt und erst im Fokus sichtbar (WCAG 2.4.1). Überspringt die Navigation und setzt\n den Fokus auf den Hauptinhalt.\n- **Keine Tastaturfalle.** Jedes Element, das den Fokus einfängt, gibt ihn wieder her\n (WCAG 2.1.2). Escape schließt Submenüs, Dropdowns und die Suche und setzt den Fokus auf\n das auslösende Element zurück.\n- **Fokus darf nicht verdeckt sein.** Das fokussierte Element muss sichtbar bleiben, nicht\n hinter einer klebenden Leiste liegen (2.4.11, neu in WCAG 2.2).\n- **Ein Bauteil, ein Muster.** Gleichartige Bauteile nehmen dieselben Tasten. Alles, was\n eine Liste von Optionen öffnet (Submenü, Custom Select, Combobox), reagiert auf\n Pfeil-runter/-hoch, Home/End und Escape.\n\n| Bauteil | Tasten | Verhalten |\n|---|---|---|\n| Skip-Link | Enter | setzt den Fokus auf den Hauptinhalt |\n| Topnav-Submenü | Enter, Space, Pfeil ↓/↑, Home, End, Esc | Caret öffnet, Pfeile wandern durch die Einträge, Esc schließt und gibt den Fokus zurück |\n| Custom Select | Enter, Space, Pfeil ↓/↑, Home, End, Esc, Tab, Buchstabe | öffnet und wählt, ein Buchstabe springt zur passenden Option (Puffer 600 ms), Tab schließt und geht weiter |\n| Combobox & Multi-Select | Pfeil ↓/↑, Home, End, Enter, Esc, Backspace | wie Select, Backspace im leeren Feld entfernt die letzte Auswahl |\n| Bild-Carousel | Pfeil ←/→ | eine Slide zurück oder vor |\n| Logo-Karussell | Pfeil ←/↑/→/↓, Home, End | auf den Pagination-Dots, jede Taste pausiert zusätzlich den Auto-Wechsel |\n| Such-Feld der Beitragsübersicht | `/`, Esc | `/` fokussiert die Suche, Esc leert das Feld |\n\n## Touch Targets\n\nZwei Kriterien regeln die Zielgröße, und sie verlangen nicht dasselbe: 2.5.8 Target Size\n(Minimum) fordert auf AA mindestens 24 × 24 px, 2.5.5 Target Size (Enhanced) auf AAA\nmindestens 44 × 44 px. 2.5.8 ist neu in WCAG 2.2, in 2.1 gibt es auf AA gar keine\nZielgrößen-Anforderung. Das System zielt auf 44 px, liegt damit über der AA-Pflicht und\ntrifft zugleich die Empfehlung der Plattform-Guidelines. Wo 44 px nicht erreichbar sind,\ngilt 24 px als harte Untergrenze.\n\nAbstand zählt mit: 2.5.8 lässt kleinere Ziele zu, wenn genug Freiraum um sie liegt.\nZwischen benachbarten Zielen deshalb mindestens `--s2` (8 px), damit ein Daumen nicht zwei\nAktionen gleichzeitig trifft.\n\n| Bauteil | Maß | Wo gesetzt |\n|---|---|---|\n| Buttons, alle Varianten | `min-height: 44px` | `.btn` |\n| Formularfelder (Input, Textarea, Select) | `min-height: 44px` | `.field input`, `textarea`, `select` |\n| Back-to-Top-Button | 44 × 44 px | `.back-to-top` |\n| Carousel-Pfeile | 44 × 44 px | `.img-slider-btn` |\n| Beispielseiten-Tabs und Aufklapp-Caret | 44 px | `.ep-tab`, `.ep-tab-toggle` |\n| Topnav-Suchfeld | `height: 44px` | `.ep-nav-search-input` |\n| Topnav-Caret, mobil | 44 × 44 px | `.ep-nav-item-toggle` im Mobil-Breakpoint |\n| Snackbar | `min-height: 48px` | `.snack` |\n\nZwei bewusste Abweichungen: Segmented Control (`.seg-option label`) und Area Tabs\n(`.atab`) liegen bei `min-height: 40px`. Beide sind Zeilen in einer dicht gesetzten\nUmschaltgruppe, in der 44 px die Leiste optisch auseinanderziehen würden. Sie erfüllen\n2.5.8 (AA) deutlich, verfehlen 2.5.5 (AAA) um 4 px. Neue Bauteile orientieren sich an\n44 px, nicht an diesen beiden.\n\n**Die 44-px-Ausnahme von der Spacing-Skala:** Die 44 stehen hartcodiert im CSS, weil die\nSpacing-Skala keinen 44-px-Schritt hat. Das ist eine der erlaubten Ausnahmen der\nStrict-Scale-Konvention (siehe `Grundlagen/Spacing & Grid`): Werte, die eine physische\nMindestgröße beschreiben, folgen ihrer eigenen Logik statt dem 8pt-Rhythmus.\n\n## ARIA Patterns\n\n**Focus-Ring.** Sichtbarer Fokusindikator für alle interaktiven Elemente (WCAG 2.4.7).\n\n```css\n--focus-ring: 0 0 0 3px var(--co-50), 0 0 0 5px var(--co-700);\n:focus-visible {\n outline: none;\n box-shadow: var(--focus-ring);\n}\n```\n\n**Formulare & Fehler.** Fehlermeldungen programmatisch mit dem Feld verknüpfen (WCAG\n1.3.1, 3.3.1).\n\n```html\n<input aria-invalid=\"true\" aria-describedby=\"err-id\">\n<span id=\"err-id\" role=\"alert\">Pflichtfeld</span>\n```\n\n**Dekorative Icons.** SVGs ohne inhaltliche Bedeutung vor Screenreadern verbergen (WCAG\n1.1.1).\n\n```html\n<svg aria-hidden=\"true\" focusable=\"false\" viewBox=\"0 0 24 24\">\n <path d=\"…\"/>\n</svg>\n```\n\n**Icon-only Buttons.** Buttons ohne sichtbares Label brauchen ein zugängliches\nTextalternativ (WCAG 1.1.1).\n\n```html\n<button aria-label=\"Menü öffnen\">\n <svg aria-hidden=\"true\" focusable=\"false\">…</svg>\n</button>\n```\n\n**Tab Panels.** Tabs als Tablist mit vollständigen ARIA-Relationen (WCAG 4.1.2).\n\n```html\n<div role=\"tablist\">\n <button role=\"tab\" aria-selected=\"true\" aria-controls=\"panel-1\" id=\"tab-1\">Tab 1</button>\n</div>\n<div role=\"tabpanel\" id=\"panel-1\" aria-labelledby=\"tab-1\" tabindex=\"0\">…</div>\n```\n\n**Live Regions.** Dynamische Inhalte für Screenreader ankündigen (WCAG 4.1.3).\n\n```html\n<!-- Status, höflich, unterbricht nicht -->\n<div role=\"status\" aria-live=\"polite\">Gespeichert.</div>\n\n<!-- Alert, sofort, unterbricht -->\n<div role=\"alert\" aria-live=\"assertive\">Verbindungsfehler.</div>\n```\n\n**Toggle & Filter-Buttons.** Buttons mit zwei Zuständen (gedrückt, nicht gedrückt)\nbrauchen `aria-pressed`, damit Screenreader den Status ansagen (WCAG 4.1.2). Verwendung\nunter anderem bei Filter-Chips.\n\n```html\n<button class=\"chip\" aria-pressed=\"true\" data-area=\"ki\">Angewandte KI</button>\n\n/* Style stützt sich auf das ARIA-Attribut, nicht auf eine separate\n .sel- oder .active-Klasse */\n.chip[aria-pressed=\"true\"] { … }\n```\n\n**Such-Feld als Landmark.** Such-Bereiche bekommen `role=\"search\"` plus eigenes\n`aria-label`, damit wird das Suchfeld zur Landmark, die Screenreader-Nutzerinnen direkt\nanspringen (WCAG 1.3.1, 2.4.6).\n\n```html\n<div role=\"search\" aria-label=\"Beiträge durchsuchen\">\n <svg aria-hidden=\"true\" focusable=\"false\">…</svg>\n <input type=\"search\" aria-label=\"Beiträge nach Stichwort durchsuchen\">\n</div>\n```\n\n**Aktive Navigation.** Der aktive Eintrag in einer Navigation wird via\n`aria-current=\"page\"` markiert, ein sichtbarer Active-State allein reicht für\nScreenreader nicht (WCAG 1.3.1).\n\n```html\n<a class=\"ep-nav-btn\" href=\"/ueber-uns\" aria-current=\"page\">Unternehmen</a>\n\n.ep-nav-btn[aria-current=\"page\"] { color: var(--co-700); }\n```\n\n**Visuell versteckt (`.sr-only`).** Text nur für Screenreader, visuell entfernt. Trägt die\nBedeutung hinter rein grafischen Markern (Häkchen, Minus) in Vergleichstabellen, Glyph\nallein reicht nicht (WCAG 1.1.1, 1.3.1).\n\n```html\n<td>\n <span class=\"ep-compare-yes\" aria-hidden=\"true\">✓</span>\n <span class=\"sr-only\">Enthalten</span>\n</td>\n\n.sr-only {\n position: absolute; width: 1px; height: 1px;\n margin: -1px; padding: 0; overflow: hidden;\n clip: rect(0,0,0,0); white-space: nowrap; border: 0;\n}\n```\n\n## Reduzierte Bewegung & High Contrast\n\n**`prefers-reduced-motion`.** Nutzer mit Gleichgewichts- oder Aufmerksamkeitsstörungen\nkönnen Animationen deaktivieren (WCAG 2.3.3 AAA).\n\n```css\n@media (prefers-reduced-motion: reduce) {\n *, *::before, *::after {\n animation-duration: 0.01ms !important;\n transition-duration: 0.01ms !important;\n scroll-behavior: auto !important;\n }\n}\n```\n\n**`forced-colors` (High Contrast).** Windows High Contrast Mode ersetzt Farben durch\nSystemfarben, Grenzen durch Borders statt nur Farbe ausdrücken.\n\n```css\n@media (forced-colors: active) {\n .btn-filled {\n border: 2px solid ButtonText;\n forced-color-adjust: none;\n }\n .btn-outlined { border-color: ButtonText; }\n}\n```\n\n**Theme-Umschaltung.** Das Design System kennt genau zwei Modi, Light und Dark.\nUmgeschaltet wird ausschließlich über das Attribut `[data-theme=\"dark\"]` am `<html>`.\nEinen dritten Modus, der der Betriebssystem-Präferenz folgt, gibt es bewusst nicht,\n`prefers-color-scheme` wird im ausgelieferten CSS nicht ausgewertet, Standard ist Light.\n`color-scheme: dark` ist im Dark-Theme fest gesetzt, damit native Controls (Checkbox,\nRadio, Scrollbar, Select-Popup) dark-thematisiert rendern.\n\n**Smooth-Scroll für In-Page-Anker.** Klicks auf In-Page-Anker innerhalb derselben Seite\nanimieren das Scrollen statt hart zu springen. Bei `prefers-reduced-motion: reduce` wird\nder Hard-Jump beibehalten, weil sanftes Scrollen für vestibulär empfindliche Nutzerinnen\nSchwindel auslösen kann (WCAG 2.3.3).\n\n## Video-Embed\n\nVideos (zum Beispiel YouTube) werden als responsiver `<iframe>` eingebunden. Ein\nGroßteil der Barrierefreiheit hängt am Videoinhalt selbst (Untertitel, Transkript) und\nlässt sich nicht allein über das Embed erzwingen.\n\n```html\n<iframe\n src=\"https://www.youtube-nocookie.com/embed/ID\"\n title=\"Was ist Keycloak? Erklärt von …\"\n loading=\"lazy\"\n referrerpolicy=\"strict-origin-when-cross-origin\"\n allowfullscreen\n style=\"width:100%;aspect-ratio:16/9;border:0;border-radius:var(--r-lg)\"></iframe>\n```\n\n- **iframe braucht `title`.** Jeder Video-iframe bekommt einen aussagekräftigen `title`\n (WCAG 4.1.2), Screenreader kündigen den Frame damit benannt an.\n- **Untertitel (WCAG 1.2.2, AA).** Aufgezeichnete Videos brauchen Untertitel. Automatische\n YouTube-Untertitel allein erfüllen die Anforderung nicht (Qualität, Zeichensetzung).\n Fremdsprachige Videos bekommen Untertitel in der Seitensprache.\n- **Transkript & Audiodeskription.** Ein Transkript in der Nähe des Videos hilft allen,\n die es nicht abspielen können, und dient Suchmaschinen. Enthält das Bild Informationen,\n die im Ton fehlen, ist zusätzlich eine Audiodeskription nötig (WCAG 1.2.3/1.2.5).\n- **Kein Autoplay.** Nie `autoplay=1` setzen, Ton und Bewegung starten erst auf Klick\n (WCAG 1.4.2, 2.2.2), das respektiert auch `prefers-reduced-motion`.\n- **Responsiv und ohne Rahmen.** `width:100%` plus `aspect-ratio:16/9` hält das Video\n seitenbreit ohne horizontales Scrollen.\n- **Datenschutz.** Einbindung über `youtube-nocookie.com` und `loading=\"lazy\"` reduziert\n Tracking und Ladelast. Für die produktive Seite gilt zusätzlich: erst nach Einwilligung\n laden, da beim Abspielen Cookies gesetzt werden.\n- **Sprache kennzeichnen.** Weicht die Videosprache von der Seitensprache ab, im\n umgebenden Text ausweisen (zum Beispiel Videosprache Englisch).\n- **Fokus & Tastatur.** Der YouTube-Player ist tastaturbedienbar und beschriftet, der\n iframe ist fokussierbar, keine positiven `tabindex`-Werte vergeben.\n\nWas das Design-System liefert versus was der Videoinhalt liefern muss: Embed-seitig sind\n`title`, kein Autoplay, responsives 16:9, Datenschutz-Host und Sprach-Hinweis abgedeckt.\nUntertitel, Transkript und Audiodeskription hängen an der Videoquelle und müssen beim\nUpload hinterlegt werden, das kann das Design System nicht erzwingen.\n\n## Gates\n\n- `npm run check:contrast` rendert `docs/index.html` in beiden Modi in Chromium und muss\n 0 Befunde melden (Text gegen 4,5:1 beziehungsweise 3:1 bei Großtext, getönte\n Bauteil-Füllungen gegen den 1,3:1-Faustwert, Bedienelement-Rahmen gegen 3:1).\n- `npm run check:dark-states` findet Zustände (Hover, Focus, Active), die im Dark Mode\n dunkel auf dunkel und damit unlesbar würden.\n- Storybook fährt `@storybook/addon-a11y` global scharf: `a11y: { test: 'error' }` in\n `.storybook/preview.ts`, axe-Verstöße lassen den Test-Runner fehlschlagen.\n- Einzelne Stories mit einem bekannten, im CSS-Kern liegenden Befund dürfen lokal\n `a11y: { test: 'todo' }` setzen (im Panel weiter sichtbar, aber nicht blockierend). Zum\n Zeitpunkt dieser Seite sind in `storybook-angular/src/lib` **keine** aktiven\n `test: 'todo'`-Overrides gesetzt. `storybook-angular/README.md` dokumentiert drei\n frühere, inzwischen im CSS-Kern behobene Befunde (DownloadCta-Eyebrow, CodeBlock-Copy,\n Slider-Wertanzeige) als Referenz dafür, wie eine solche Ausnahme aussieht und wann sie\n wieder scharf geschaltet wird.\n\n## Siehe auch\n\n- `Grundlagen/Farben` für Token-Rollen je Farbstufe (Ink, Fill, Band, Tönung) und Dos & Don'ts\n- `docs/index.html#gt-a11y-touch` (44-px-Ausnahme)\n- CONTRIBUTING.md § 1 Grundprinzipien, § 3 Farbe & Kontrast\n- `storybook-angular/README.md`, Abschnitt „Frühere a11y-Befunde im CSS-Kern (behoben)“\n",
13
13
  "summary": "# Barrierefreiheit Kontrastverhältnisse aller Brand-Farben, Tastaturnavigation, ARIA Patte..."
14
14
  }
15
15
  }
@@ -9,7 +9,7 @@
9
9
  "name": "Verwendung",
10
10
  "path": "./src/docs/komponenten/cta-verwendung.mdx",
11
11
  "title": "Komponenten/Call to Action/CTA-Band",
12
- "content": "import { Meta } from '@storybook/addon-docs/blocks';\nimport * as CtaBandStories from '../../lib/cta-band/cta-band.stories';\n\n<Meta of={CtaBandStories} name=\"Verwendung\" />\n\n# Verwendung\n\nCall to Action deckt zwei Pattern-Familien ab: das Page-End-CTA-Band als letzte, konkrete\nEinladung am Ende jeder Customer-Page, und vier Download-CTA-Varianten für\nRessourcen-Downloads. Ergänzend dokumentiert diese Seite den Editorial Split als\nsekundäre Konversionsfläche neben dem primären Page-End-CTA. Das Page-End-CTA-Band hat\nseit Ticket 07 der Seitenbausteine-Serie ein eigenes Bauteil (`CtaBand`, Attributselektor\n`[cdsCtaBand]`), ebenso die vier Download-Varianten (`DownloadCta`); der Editorial Split\nbleibt ein CSS-Rezept ohne Angular-Komponente.\n\n## Wann welche Variante\n\n| Variante | Bauteil | Wann einsetzen |\n|---|---|---|\n| Page-End-CTA-Band | `CtaBand` (`[cdsCtaBand]`) | Letzte Einladung am Ende jeder Customer-Page, direkt vor dem Footer, eine Aktion auf der Bereichsfarbe als Hintergrund |\n| Editorial Split | bauteillos (CSS-Rezept `.ep-section` + `.layout-grid`) | Sekundäre oder tertiäre Konversionsfläche neben dem primären Page-End-CTA (zum Beispiel ein Karriere-Block auf der Landing), ohne Card-Chrome |\n| Download · Hero | `DownloadCta` | Hauptressource einer Seite, Landing Pages, Ressourcenseiten, Kampagnen, maximal einmal pro Seite |\n| Download · Mit Vorschaubild | `DownloadCta` | Whitepapers und Reports, deren Deckblatt als Vorschau den Inhalt sofort greifbar macht |\n| Download · Kompakt | `DownloadCta` | Ressourcenlisten mit mehreren Downloads (zum Beispiel eine Mediathek) |\n| Download · Minimal | `DownloadCta` | Quellenangaben, Fließtext-Links oder Nebenbereiche (Footer, Blog, Dokumentation) |\n\n### Page-End-CTA-Band (`CtaBand`)\n\nAttributselektor `[cdsCtaBand]` statt eigenem Element: die Bandfläche ist im Mockup\nausnahmslos ein Inline-Style direkt am Element (`.ep-cta-band`, Padding\n`var(--s12) var(--s8)`, zentriert) — ein eigenes Element würde denselben „Fläche am\nHost“-Fehler reproduzieren, den `docs/adr/0008` für `cds-section` gemessen hat. Der\nKonsument setzt Hintergrund und Schrift deshalb\nweiterhin selbst, auf demselben `<div cdsCtaBand>`, das die Komponente trägt:\nBereichsfarbe (`--co-700` / `--ki-800` / `--es-700` / `--wo-700`), Schrift `color:#fff`.\nAuf der dunklen Fläche wird der Filled-Button über die zusammengesetzte Modifier-Klasse\n`.btn-on-band` invertiert (weißer Hintergrund, Text in der Bereichsfarbe) — die\nKomponente setzt diese Klassen direkt (`cds-button` kennt zwar den Modifier, kann aber\nkeinen `<a>` rendern, siehe `cta-band.component.ts`). Die Aktion führt in der\nRegel auf die adaptive Kontaktseite (`data-ep=\"kontakt\"`), nie auf ein Overlay oder\nModal. Bewusst nur EINE Aktion, kein `secondaryLabel`: keines der 22\nMockup-Vorkommen zeigt eine zweite, und `.btn-on-band` lässt sich mit den\nvorhandenen CSS-Klassen ohnehin nicht in einer zurückhaltenderen zweiten Variante\nbauen (jede Nicht-`filled`-Variante würde Text in Bandfarbe auf Bandfarbe zeigen —\nfestgehalten in\n`.scratch/angular-seitenbausteine/issues/16-css-luecke-zweite-aktion-auf-band.md`).\n\n```html\n<div cdsCtaBand\n heading=\"Erstgespräch, 30 Minuten, kostenfrei.\"\n sub=\"Du schilderst Deine Situation, wir geben eine erste Einschätzung.\"\n primaryLabel=\"Termin buchen\"\n primaryHref=\"/kontakt\"\n style=\"background:var(--co-700);color:#fff\">\n</div>\n```\n\n### Editorial Split (bauteillos)\n\nRuhige 2-Spalten-Aufteilung in einer normalen `.ep-section`: links ein Foto im\n4:3-Format mit `--r-md`, rechts Eyebrow, H2, Lead und Button. Kein Card-Container, kein\nSchatten, keine Border. Bühne entsteht über die Hintergrundfarbe der Sektion\n(typischerweise `--co-50` oder `--n-50`) und über das Foto links, nicht über\nCard-Chrome, das dem Markenwert „Ruhig“ widerspräche.\n\n```html\n<div class=\"ep-section\" style=\"background:var(--co-50)\">\n <div class=\"layout-grid\" style=\"align-items:center\">\n <div class=\"col-6\">\n <img src=\"bild.jpg\" alt=\"Beschreibend\"\n style=\"width:100%;aspect-ratio:4/3;object-fit:cover;border-radius:var(--r-md)\">\n </div>\n <div class=\"col-6\">\n <div class=\"ep-section-label t-co\">Karriere</div>\n <h2 class=\"ep-section-h2\">Dein Fußabdruck bei Conciso.</h2>\n <p class=\"ep-section-sub\">Projekte mit Wirkung, ein Team, das füreinander einsteht.</p>\n <button class=\"btn btn-filled btn-co\">Stellen entdecken</button>\n </div>\n </div>\n</div>\n```\n\n## Barrierefreiheit\n\n| Aspekt | Regel |\n|---|---|\n| Button-Label | `aria-label` mit Dateiname und Format auf dem Download-Button setzen (zum Beispiel „Figma-Bibliothek herunterladen (Figma, 48 MB)“), der Text „Herunterladen“ allein nennt kein Ziel |\n| Format- und Größenangabe | Dateiformat und Dateigröße immer sichtbar im Markup, nicht nur im Button-Label |\n| Brand Area | Bereichsfarbe des zugehörigen Dokuments nutzen, keine bereichsfremde Farbe |\n| Kontrast auf Bereichsfläche | Der Filled-Button wird auf der farbigen Fläche des Page-End-CTA-Band über `.btn-on-band` invertiert (weißer Hintergrund, Text in Bereichsfarbe); ohne Inversion würde der Button auf der eigenen Bereichsfarbe stehen und Kontrast verlieren |\n\n## Dos & Don'ts\n\n**Tun**\n\n- Dateiformat und -größe immer angeben, Nutzer sollen vor dem Download wissen, was sie erhalten\n- Brand-Area-Farbe des zugehörigen Dokuments verwenden, stärkt die visuelle Zugehörigkeit\n- `aria-label` auf dem Button setzen, zum Beispiel „Figma-Bibliothek herunterladen (Figma, 48 MB)“\n- Pro Seite maximal einen Hero-CTA, mehrere Hero-Blöcke erzeugen visuelle Konkurrenz\n\n**Nicht tun**\n\n- Download-Button ohne Format und Größe, erhöht die kognitive Last und bricht Vertrauen\n- Hero-Variante für unwichtige Sekundär-Downloads, die Größe signalisiert Wichtigkeit\n- Generisches „Herunterladen“ ohne Dateinamen, schlecht für Screenreader und Kontext\n- Bereichsfremde Farbe für ein Dokument verwenden, bricht den thematischen Zusammenhang\n\n## Verwandte Seiten\n\n- Komponenten/Call to Action/CTA-Band (Story, deckt `CtaBand` ab)\n- Komponenten/Call to Action/DownloadCta (Story, deckt alle vier Download-Varianten ab)\n- Doku-Sektion: `docs/index.html#gt-cta-band` (Page-End-CTA-Band)\n- Doku-Sektion: `docs/index.html#gt-cta-editorial` (Editorial Split)\n",
12
+ "content": "import { Meta } from '@storybook/addon-docs/blocks';\nimport * as CtaBandStories from '../../lib/cta-band/cta-band.stories';\n\n<Meta of={CtaBandStories} name=\"Verwendung\" />\n\n# Verwendung\n\nCall to Action deckt zwei Pattern-Familien ab: das Page-End-CTA-Band als letzte, konkrete\nEinladung am Ende jeder Customer-Page, und vier Download-CTA-Varianten für\nRessourcen-Downloads. Ergänzend dokumentiert diese Seite den Editorial Split als\nsekundäre Konversionsfläche neben dem primären Page-End-CTA. Das Page-End-CTA-Band hat\nein eigenes Bauteil (`CtaBand`, Attributselektor\n`[cdsCtaBand]`), ebenso die vier Download-Varianten (`DownloadCta`); der Editorial Split\nbleibt ein CSS-Rezept ohne Angular-Komponente.\n\n## Wann welche Variante\n\n| Variante | Bauteil | Wann einsetzen |\n|---|---|---|\n| Page-End-CTA-Band | `CtaBand` (`[cdsCtaBand]`) | Letzte Einladung am Ende jeder Customer-Page, direkt vor dem Footer, eine Aktion auf der Bereichsfarbe als Hintergrund |\n| Editorial Split | bauteillos (CSS-Rezept `.ep-section` + `.layout-grid`) | Sekundäre oder tertiäre Konversionsfläche neben dem primären Page-End-CTA (zum Beispiel ein Karriere-Block auf der Landing), ohne Card-Chrome |\n| Download · Hero | `DownloadCta` | Hauptressource einer Seite, Landing Pages, Ressourcenseiten, Kampagnen, maximal einmal pro Seite |\n| Download · Mit Vorschaubild | `DownloadCta` | Whitepapers und Reports, deren Deckblatt als Vorschau den Inhalt sofort greifbar macht |\n| Download · Kompakt | `DownloadCta` | Ressourcenlisten mit mehreren Downloads (zum Beispiel eine Mediathek) |\n| Download · Minimal | `DownloadCta` | Quellenangaben, Fließtext-Links oder Nebenbereiche (Footer, Blog, Dokumentation) |\n\n### Page-End-CTA-Band (`CtaBand`)\n\nAttributselektor `[cdsCtaBand]` statt eigenem Element: die Bandfläche ist im Mockup\nausnahmslos ein Inline-Style direkt am Element (`.ep-cta-band`, Padding\n`var(--s12) var(--s8)`, zentriert) — ein eigenes Element würde denselben „Fläche am\nHost“-Fehler reproduzieren, den `docs/adr/0008` für `cds-section` gemessen hat. Der\nKonsument setzt Hintergrund und Schrift deshalb\nweiterhin selbst, auf demselben `<div cdsCtaBand>`, das die Komponente trägt:\nBereichsfarbe (`--co-700` / `--ki-800` / `--es-700` / `--wo-700`), Schrift `color:#fff`.\nAuf der dunklen Fläche wird der Filled-Button über die zusammengesetzte Modifier-Klasse\n`.btn-on-band` invertiert (weißer Hintergrund, Text in der Bereichsfarbe) — die\nKomponente setzt diese Klassen direkt (`cds-button` kennt zwar den Modifier, kann aber\nkeinen `<a>` rendern, siehe `cta-band.component.ts`). Die Aktion führt in der\nRegel auf die adaptive Kontaktseite (`data-ep=\"kontakt\"`), nie auf ein Overlay oder\nModal. Bewusst nur EINE Aktion, kein `secondaryLabel`: keines der 22\nMockup-Vorkommen zeigt eine zweite, und `.btn-on-band` lässt sich mit den\nvorhandenen CSS-Klassen ohnehin nicht in einer zurückhaltenderen zweiten Variante\nbauen (jede Nicht-`filled`-Variante würde Text in Bandfarbe auf Bandfarbe zeigen).\n\n```html\n<div cdsCtaBand\n heading=\"Erstgespräch, 30 Minuten, kostenfrei.\"\n sub=\"Du schilderst Deine Situation, wir geben eine erste Einschätzung.\"\n primaryLabel=\"Termin buchen\"\n primaryHref=\"/kontakt\"\n style=\"background:var(--co-700);color:#fff\">\n</div>\n```\n\n### Editorial Split (bauteillos)\n\nRuhige 2-Spalten-Aufteilung in einer normalen `.ep-section`: links ein Foto im\n4:3-Format mit `--r-md`, rechts Eyebrow, H2, Lead und Button. Kein Card-Container, kein\nSchatten, keine Border. Bühne entsteht über die Hintergrundfarbe der Sektion\n(typischerweise `--co-50` oder `--n-50`) und über das Foto links, nicht über\nCard-Chrome, das dem Markenwert „Ruhig“ widerspräche.\n\n```html\n<div class=\"ep-section\" style=\"background:var(--co-50)\">\n <div class=\"layout-grid\" style=\"align-items:center\">\n <div class=\"col-6\">\n <img src=\"bild.jpg\" alt=\"Beschreibend\"\n style=\"width:100%;aspect-ratio:4/3;object-fit:cover;border-radius:var(--r-md)\">\n </div>\n <div class=\"col-6\">\n <div class=\"ep-section-label t-co\">Karriere</div>\n <h2 class=\"ep-section-h2\">Dein Fußabdruck bei Conciso.</h2>\n <p class=\"ep-section-sub\">Projekte mit Wirkung, ein Team, das füreinander einsteht.</p>\n <button class=\"btn btn-filled btn-co\">Stellen entdecken</button>\n </div>\n </div>\n</div>\n```\n\n## Barrierefreiheit\n\n| Aspekt | Regel |\n|---|---|\n| Button-Label | `aria-label` mit Dateiname und Format auf dem Download-Button setzen (zum Beispiel „Figma-Bibliothek herunterladen (Figma, 48 MB)“), der Text „Herunterladen“ allein nennt kein Ziel |\n| Format- und Größenangabe | Dateiformat und Dateigröße immer sichtbar im Markup, nicht nur im Button-Label |\n| Brand Area | Bereichsfarbe des zugehörigen Dokuments nutzen, keine bereichsfremde Farbe |\n| Kontrast auf Bereichsfläche | Der Filled-Button wird auf der farbigen Fläche des Page-End-CTA-Band über `.btn-on-band` invertiert (weißer Hintergrund, Text in Bereichsfarbe); ohne Inversion würde der Button auf der eigenen Bereichsfarbe stehen und Kontrast verlieren |\n\n## Dos & Don'ts\n\n**Tun**\n\n- Dateiformat und -größe immer angeben, Nutzer sollen vor dem Download wissen, was sie erhalten\n- Brand-Area-Farbe des zugehörigen Dokuments verwenden, stärkt die visuelle Zugehörigkeit\n- `aria-label` auf dem Button setzen, zum Beispiel „Figma-Bibliothek herunterladen (Figma, 48 MB)“\n- Pro Seite maximal einen Hero-CTA, mehrere Hero-Blöcke erzeugen visuelle Konkurrenz\n\n**Nicht tun**\n\n- Download-Button ohne Format und Größe, erhöht die kognitive Last und bricht Vertrauen\n- Hero-Variante für unwichtige Sekundär-Downloads, die Größe signalisiert Wichtigkeit\n- Generisches „Herunterladen“ ohne Dateinamen, schlecht für Screenreader und Kontext\n- Bereichsfremde Farbe für ein Dokument verwenden, bricht den thematischen Zusammenhang\n\n## Verwandte Seiten\n\n- Komponenten/Call to Action/CTA-Band (Story, deckt `CtaBand` ab)\n- Komponenten/Call to Action/DownloadCta (Story, deckt alle vier Download-Varianten ab)\n- Doku-Sektion: `docs/index.html#gt-cta-band` (Page-End-CTA-Band)\n- Doku-Sektion: `docs/index.html#gt-cta-editorial` (Editorial Split)\n",
13
13
  "summary": "# Verwendung Call to Action deckt zwei Pattern-Familien ab: das Page-End-CTA-Band als letz..."
14
14
  }
15
15
  }
@@ -4,7 +4,7 @@
4
4
  "id": "komponenten-call-to-action-cta-band",
5
5
  "name": "CtaBandComponent",
6
6
  "path": "./src/lib/cta-band/cta-band.stories.ts",
7
- "description": "CtaBand (`[cdsCtaBand]`) — Wrapper um `.ep-cta-band` aus css/components.css\n(css/components.css:1456, Kopf-Duo `.ep-cta-h2`/`.ep-cta-sub` ebenda): das\nbereichsgefärbte Page-End-CTA-Band, das laut Doku-Site (`docs/index.html:6237`,\nAnker `gt-cta-band`) am Ende jeder Customer-Page direkt vor dem Footer steht — die\n„letzte Einladung“, wenn die Nutzerin die Seite durchgelesen hat. Ausgezählt: 22\nVorkommen auf den Beispielseiten, ausnahmslos als `<div class=\"ep-cta-band\"\nstyle=\"background:…\">` mit genau einer Aktion.\n\n**Entscheidung 1 — Attributselektor, kein eigenes Element (ADR-0008, derselbe\n„Fläche am Host“-Fall wie `cds-section`).** `css/components.css` trägt für\n`.ep-cta-band` KEIN `[data-area]` (per Grep geprüft, keine einzige Regel dieser\nForm existiert). Die Bandfläche kommt in allen 22 Vorkommen als Inline-Style\nDIREKT am `.ep-cta-band`-Element (`style=\"background:var(--co-700);color:#fff\"`,\ndocs/index.html:6304 u. a.), dem Bereich der Page folgend, nie aus einem\n`data-area`-Attribut. Ein Element-Selektor würde exakt den in ADR-0008 „Fall 2“\ngemessenen Fehler wiederholen: der `<cds-cta-band>`-Host ist ein unbekanntes\nCustom Element (`display:inline`), sein einziges Kind würde als Block\nherausgebrochen, ein am Host gesetzter Inline-`background` hätte keine eigene Box\nzum Malen. Die Komponente hängt sich deshalb als Attribut an ein vom Konsumenten\ngeschriebenes `<div>` (`<div cdsCtaBand style=\"background:…\">`) — das Element, das\nden Style trägt, und `.ep-cta-band` sind derselbe Knoten. Nur `<div>`, kein\n`<section>`-Zwilling wie bei `cds-section`: alle 22 Vorkommen benutzen\nausnahmslos ein `<div>`, keines variiert das Tag.\n\n**Entscheidung 2 — `area` färbt nur die Aktion, nicht das Band.** Aus demselben\nGrund wie bei `cds-section` (siehe dessen Klassendoku, Entscheidung 2) bleibt die\nBandfläche Sache des Konsumenten: welche Bereichsfarbe ein Band trägt, ist eine\nEntscheidung der Seite (Bereichszugehörigkeit der Customer-Page), keine\nBauteil-Eigenschaft — und das CSS bietet an dieser Klasse ohnehin keinen\n`[data-area]`-Haken an (siehe Entscheidung 1). `area` steuert deshalb\nausschließlich die Button-Farbklasse (`.btn-{area}`), nicht die Füllung des Bands.\n\n**Entscheidung 3 — `.btn-on-band` + `.btn-filled` direkt komponiert, nicht über\n`cds-button` projiziert.** `cds-button` KENNT den Modifier\n(`variant=\"filled-on-band\"`, geprüft über `npx storybook tools docs show --id\nkomponenten-buttons-button` — die Story „Auf Bereichs-Band“ existiert dort\nbereits). Trotzdem baut diese Komponente die `.btn`-Klassen direkt zusammen\n(Präzedenzfall `download-cta.component.ts`), aus einem härteren Grund als dort:\n`cds-button` rendert IMMER ein `<button>` — sein Input-Vertrag\n(`ButtonComponentInputs`) kennt kein `href` — und kann die vom Ticket geforderte\nVerzweigung „`primaryHref` gesetzt → `<a>`“ strukturell gar nicht erfüllen. Ein\nprojiziertes `<cds-button>` brächte hier also kein Custom-Element-Layoutproblem\n(kein Kindselektor an `.ep-cta-band` hängt von der DOM-Tiefe ab), sondern schlicht\ndie falsche Fähigkeit.\n\n**Entscheidung 4 — bewusst nur EINE Aktion, kein `secondaryLabel`.** Eine frühere\nFassung bot `secondaryLabel`/`secondaryHref` an, ungeprüft gegen das Mockup. Beide\nBelege dagegen: erstens zeigt KEINES der 22 `.ep-cta-band`-Vorkommen in\n`docs/index.html` eine zweite Aktion — der Zwei-Aktionen-Fall hat keine Vorlage.\nZweitens (der schwerere Grund) lässt sich eine zweite, optisch zurückhaltendere\nAktion mit den vorhandenen Klassen gar nicht bauen: `.btn-{area}.btn-on-band{color:\n…}` (css/components.css:47–51) überschreibt die Textfarbe JEDER Variante auf den\nBereichston — genau den Ton, den die Doku durchgängig als Bandhintergrund nennt.\nNur `.btn-filled` tauscht zusätzlich die Füllung gegen Weiß; `.btn-outlined`,\n`.btn-tonal` und `.btn-text` blieben transparent und zeigten Text in derselben\nFarbe wie das Band darunter — unsichtbar. Eine zweite Aktion hätte deshalb\nzwangsläufig dieselbe Optik wie die erste getragen, keine Hierarchie, nur zwei\ngleich gewichtete Buttons. Das ist eine CSS-Lücke (kein kontrastsicherer\nsekundärer On-Band-Stil), festgehalten in\n`.scratch/angular-seitenbausteine/issues/16-css-luecke-zweite-aktion-auf-band.md`\n— gemeldet, nicht in diesem Wrapper improvisiert. Solange sie besteht, trägt\n`cds-cta-band` nur eine Aktion; ein Konsument mit echtem Bedarf für eine zweite\nschreibt sie außerhalb der Komponente ins Band hinein (`<ng-content>` gibt es\nhier bewusst nicht, um genau das nicht als unterstützten Pfad zu suggerieren).\nOhne zweite Aktion braucht es auch keinen eigenen Container: die einzelne Aktion\nzentriert sich über das ererbte `.ep-cta-band{text-align:center}` von selbst,\ngenau wie in allen 22 Mockup-Vorkommen — ein zusätzliches Flex-Layout im Wrapper\nhätte eine Darstellung erfunden, die die CSS-Schicht so nicht kennt (ADR-0001;\ndie einzige existierende Deklaration dieser Form ist `.ep-hero-ctas`,\ncss/components.css:1222, eine andere Klasse für einen anderen Bauteiltyp).\n\n**Kein `aria-label` auf der Aktion.** Anders als bei `cds-feature`\n(`ctaAriaLabel`, 11 von 12 Vorkommen mit `aria-label`) zeigt keines der 22\nBand-Vorkommen einen `aria-label` auf seiner Aktion — ausgezählt, nicht\ngeschätzt. Die sichtbaren Texte sind durchgängig konkrete Verben mit Ziel\n(„Termin buchen“, „Gespräch anfragen“, „Offene Stellen ansehen“ …) und stehen je\nSeite nur einmal; anders als bei einer Karten-Liste braucht es hier keine\nDisambiguierung. Kein entsprechender Input.",
7
+ "description": "CtaBand (`[cdsCtaBand]`) — Wrapper um `.ep-cta-band` aus css/components.css\n(css/components.css:1456, Kopf-Duo `.ep-cta-h2`/`.ep-cta-sub` ebenda): das\nbereichsgefärbte Page-End-CTA-Band, das laut Doku-Site (`docs/index.html:6237`,\nAnker `gt-cta-band`) am Ende jeder Customer-Page direkt vor dem Footer steht — die\n„letzte Einladung“, wenn die Nutzerin die Seite durchgelesen hat. Ausgezählt: 22\nVorkommen auf den Beispielseiten, ausnahmslos als `<div class=\"ep-cta-band\"\nstyle=\"background:…\">` mit genau einer Aktion.\n\n**Entscheidung 1 — Attributselektor, kein eigenes Element (ADR-0008, derselbe\n„Fläche am Host“-Fall wie `cds-section`).** `css/components.css` trägt für\n`.ep-cta-band` KEIN `[data-area]` (per Grep geprüft, keine einzige Regel dieser\nForm existiert). Die Bandfläche kommt in allen 22 Vorkommen als Inline-Style\nDIREKT am `.ep-cta-band`-Element (`style=\"background:var(--co-700);color:#fff\"`,\ndocs/index.html:6304 u. a.), dem Bereich der Page folgend, nie aus einem\n`data-area`-Attribut. Ein Element-Selektor würde exakt den in ADR-0008 „Fall 2“\ngemessenen Fehler wiederholen: der `<cds-cta-band>`-Host ist ein unbekanntes\nCustom Element (`display:inline`), sein einziges Kind würde als Block\nherausgebrochen, ein am Host gesetzter Inline-`background` hätte keine eigene Box\nzum Malen. Die Komponente hängt sich deshalb als Attribut an ein vom Konsumenten\ngeschriebenes `<div>` (`<div cdsCtaBand style=\"background:…\">`) — das Element, das\nden Style trägt, und `.ep-cta-band` sind derselbe Knoten. Nur `<div>`, kein\n`<section>`-Zwilling wie bei `cds-section`: alle 22 Vorkommen benutzen\nausnahmslos ein `<div>`, keines variiert das Tag.\n\n**Entscheidung 2 — `area` färbt nur die Aktion, nicht das Band.** Aus demselben\nGrund wie bei `cds-section` (siehe dessen Klassendoku, Entscheidung 2) bleibt die\nBandfläche Sache des Konsumenten: welche Bereichsfarbe ein Band trägt, ist eine\nEntscheidung der Seite (Bereichszugehörigkeit der Customer-Page), keine\nBauteil-Eigenschaft — und das CSS bietet an dieser Klasse ohnehin keinen\n`[data-area]`-Haken an (siehe Entscheidung 1). `area` steuert deshalb\nausschließlich die Button-Farbklasse (`.btn-{area}`), nicht die Füllung des Bands.\n\n**Entscheidung 3 — `.btn-on-band` + `.btn-filled` direkt komponiert, nicht über\n`cds-button` projiziert.** `cds-button` KENNT den Modifier\n(`variant=\"filled-on-band\"`, geprüft über `npx storybook tools docs show --id\nkomponenten-buttons-button` — die Story „Auf Bereichs-Band“ existiert dort\nbereits). Trotzdem baut diese Komponente die `.btn`-Klassen direkt zusammen\n(Präzedenzfall `download-cta.component.ts`), aus einem härteren Grund als dort:\n`cds-button` rendert IMMER ein `<button>` — sein Input-Vertrag\n(`ButtonComponentInputs`) kennt kein `href` — und kann die hier nötige\nVerzweigung „`primaryHref` gesetzt → `<a>`“ strukturell gar nicht erfüllen. Ein\nprojiziertes `<cds-button>` brächte hier also kein Custom-Element-Layoutproblem\n(kein Kindselektor an `.ep-cta-band` hängt von der DOM-Tiefe ab), sondern schlicht\ndie falsche Fähigkeit.\n\n**Entscheidung 4 — bewusst nur EINE Aktion, kein `secondaryLabel`.** Eine frühere\nFassung bot `secondaryLabel`/`secondaryHref` an, ungeprüft gegen das Mockup. Beide\nBelege dagegen: erstens zeigt KEINES der 22 `.ep-cta-band`-Vorkommen in\n`docs/index.html` eine zweite Aktion — der Zwei-Aktionen-Fall hat keine Vorlage.\nZweitens (der schwerere Grund) lässt sich eine zweite, optisch zurückhaltendere\nAktion mit den vorhandenen Klassen gar nicht bauen: `.btn-{area}.btn-on-band{color:\n…}` (css/components.css:47–51) überschreibt die Textfarbe JEDER Variante auf den\nBereichston — genau den Ton, den die Doku durchgängig als Bandhintergrund nennt.\nNur `.btn-filled` tauscht zusätzlich die Füllung gegen Weiß; `.btn-outlined`,\n`.btn-tonal` und `.btn-text` blieben transparent und zeigten Text in derselben\nFarbe wie das Band darunter — unsichtbar. Eine zweite Aktion hätte deshalb\nzwangsläufig dieselbe Optik wie die erste getragen, keine Hierarchie, nur zwei\ngleich gewichtete Buttons. Das ist eine CSS-Lücke (kein kontrastsicherer\nsekundärer On-Band-Stil) — gemeldet, nicht in diesem Wrapper improvisiert. Solange sie besteht, trägt\n`cds-cta-band` nur eine Aktion; ein Konsument mit echtem Bedarf für eine zweite\nschreibt sie außerhalb der Komponente ins Band hinein (`<ng-content>` gibt es\nhier bewusst nicht, um genau das nicht als unterstützten Pfad zu suggerieren).\nOhne zweite Aktion braucht es auch keinen eigenen Container: die einzelne Aktion\nzentriert sich über das ererbte `.ep-cta-band{text-align:center}` von selbst,\ngenau wie in allen 22 Mockup-Vorkommen — ein zusätzliches Flex-Layout im Wrapper\nhätte eine Darstellung erfunden, die die CSS-Schicht so nicht kennt (ADR-0001;\ndie einzige existierende Deklaration dieser Form ist `.ep-hero-ctas`,\ncss/components.css:1222, eine andere Klasse für einen anderen Bauteiltyp).\n\n**Kein `aria-label` auf der Aktion.** Anders als bei `cds-feature`\n(`ctaAriaLabel`, 11 von 12 Vorkommen mit `aria-label`) zeigt keines der 22\nBand-Vorkommen einen `aria-label` auf seiner Aktion — ausgezählt, nicht\ngeschätzt. Die sichtbaren Texte sind durchgängig konkrete Verben mit Ziel\n(„Termin buchen“, „Gespräch anfragen“, „Offene Stellen ansehen“ …) und stehen je\nSeite nur einmal; anders als bei einer Karten-Liste braucht es hier keine\nDisambiguierung. Kein entsprechender Input.",
8
8
  "jsDocTags": {},
9
9
  "argTypes": {
10
10
  "area": {
@@ -4,7 +4,7 @@
4
4
  "id": "komponenten-cards-teaser-featured-karte",
5
5
  "name": "FeaturedCardComponent",
6
6
  "path": "./src/lib/featured-card/featured-card.stories.ts",
7
- "description": "FeaturedCard (cds-featured-card) — Wrapper um `.card-featured` aus\ncss/components.css (css/components.css:230–249): die horizontale Großkarte für\n**genau einen** hervorgehobenen Beitrag oder Termin (Bild links 60 %, Textspalte\nrechts 40 % als absolut positioniertes Overlay), siehe `docs/index.html`\nAbschnitt „Featured · horizontale Großkarte“ (`#gt-events-featured`).\n\n**Entscheidung 1 — `href` entscheidet zwischen `<a>` und `<article>`.** Die\nDoku-Beispiele zeigen die Featured-Card ausschließlich als Link\n(`a.card.card-elevated.card-featured`), das Ticket verlangt aber ausdrücklich eine\nVariante ohne `<a>`. `.card-elevated` ist nach `CONTRIBUTING.md` §4 („Elevation =\nInteraktivität“) im CSS auf `a.card-elevated` gescoped — eine `<article>` bekäme\nden Schatten mit gesetzter Klasse ohnehin nicht. Die Komponente führt die Klasse\ndeshalb nur mit, wenn sie auch wirkt: mit `href` ein `<a class=\"card card-elevated\ncard-featured\">`, ohne `href` ein `<article class=\"card card-featured\">` (ruht\nflach mit dem `--bd-strong`-Rahmen aus `.card`, wie jede statische Fläche). Media\nund Body stehen dafür einmal in einem gemeinsamen `<ng-template>` und werden in\nbeiden Zweigen per `<ng-container [ngTemplateOutlet]>` eingesetzt — dasselbe\nMuster wie `SectionComponent` (`section.component.ts`) für „zwei Wurzelelemente,\nein Körper“. `<ng-container>` rendert selbst kein Element, die Kindselektoren aus\nEntscheidung 2 bleiben also unberührt: im laufenden Storybook erneut per\n`querySelector`/`getBoundingClientRect` gemessen (siehe Play-Funktionen), Ergebnis\nunverändert zur vorherigen, noch duplizierten Fassung.\n\n**Entscheidung 2 — die Pill ist direkt komponiertes Markup, kein `<cds-pill>`.**\nDas CSS arbeitet mit direkten Kindselektoren: `.card-featured>.card-media`,\n`.card-featured-body>.pill`, `.card-featured-body>.card-text`,\n`.card-featured-body>.card-title-hero` (jeweils mit `margin`/`min-height`/\n`line-clamp`-Übernahmen, siehe CSS-Kommentare dort). `<cds-pill>` rendert sein\n`.pill`-`<span>` nicht auf dem eigenen Host-Element, sondern eine Ebene tiefer\n(`pill.component.ts`: Host ist `<cds-pill>`, `.pill` sitzt im Template darunter) —\nprojiziert als `<cds-pill class=\"pill\">` stünde die Pille damit als\n`.card-featured-body>cds-pill>.pill`, zwei Ebenen statt einer, und die\nKindselektoren griffen nicht mehr (leise: kein Fehler, nur falsches Layout).\nDieselbe Abwägung wie in `download-cta.component.ts` (Kommentar über den\nButtons): dort verhindert `.cta-dl-actions`, das seine Kinder per Flex streckt,\ndass ein `<cds-button>`-Host mitstreckt; hier verhindert ein Kindselektor, dass\nirgendein zusätzliches Host-Element dazwischentritt. In beiden Fällen werden die\nKlassen direkt komponiert statt eine Wrapper-Komponente zu verwenden. Geprüft im\nlaufenden Storybook (`Komponenten/Cards & Teaser/Featured-Karte`): die gerenderte\nKette ist `.card-featured>.card-media` und `.card-featured-body` mit `.pill`,\n`.card-text`, `.card-title-hero` als direkten Kindern, kein zusätzliches Element\ndazwischen (siehe Play-Funktionen der Stories).\n\nWeil die Pille direkt komponiert wird, übernimmt diese Komponente auch die\n`aria-label`-Regel von `PillComponent` (`pill.component.ts`) von Hand nach:\nDefault „Bereich `<pill>`“, überschreibbar über `pillAriaLabel` — nötig, weil die\nPille nicht immer einen Bereich benennt (siehe `pillAriaLabel`-Doku unten).\n\n**Entscheidung 3 — kein separates `data-area` für die Pille.** Die Doku setzt\nKarte und Pille immer auf denselben Bereich (`docs/index.html:8979`: Karte\n`data-area=\"es\"`, Pille `data-area=\"es\"`). Die Komponente bindet deshalb `area`\nauf beide, statt einen zweiten Bereichs-Input zu erfinden.\n\n**Entscheidung 4 — kein CTA- und kein Meta-Input.** Die Doku-Vorlage zeigt\nzusätzlich einen Meta-Strip (Datum/Ort/Format) und einen `.card-cta-link`-Fuß;\ndas Ticket benennt als API aber nur `title`/`text`/`imageSrc`/`imageAlt`/`href`/\n`pill`/`area`. Diese Komponente bildet exakt diese Fläche ab, ohne Inputs zu\nerfinden, die das Ticket nicht vorsieht — ein Meta-Strip oder ein CTA-Fuß wären\neine spätere, eigene Erweiterung.\n\nVerwendungsguidance dieser Gruppe: siehe Card (`komponenten-cards-teaser-card--verwendung`).",
7
+ "description": "FeaturedCard (cds-featured-card) — Wrapper um `.card-featured` aus\ncss/components.css (css/components.css:230–249): die horizontale Großkarte für\n**genau einen** hervorgehobenen Beitrag oder Termin (Bild links 60 %, Textspalte\nrechts 40 % als absolut positioniertes Overlay), siehe `docs/index.html`\nAbschnitt „Featured · horizontale Großkarte“ (`#gt-events-featured`).\n\n**Entscheidung 1 — `href` entscheidet zwischen `<a>` und `<article>`.** Die\nDoku-Beispiele zeigen die Featured-Card ausschließlich als Link\n(`a.card.card-elevated.card-featured`), diese Komponente unterstützt aber ausdrücklich eine\nVariante ohne `<a>`. `.card-elevated` ist nach `CONTRIBUTING.md` §4 („Elevation =\nInteraktivität“) im CSS auf `a.card-elevated` gescoped — eine `<article>` bekäme\nden Schatten mit gesetzter Klasse ohnehin nicht. Die Komponente führt die Klasse\ndeshalb nur mit, wenn sie auch wirkt: mit `href` ein `<a class=\"card card-elevated\ncard-featured\">`, ohne `href` ein `<article class=\"card card-featured\">` (ruht\nflach mit dem `--bd-strong`-Rahmen aus `.card`, wie jede statische Fläche). Media\nund Body stehen dafür einmal in einem gemeinsamen `<ng-template>` und werden in\nbeiden Zweigen per `<ng-container [ngTemplateOutlet]>` eingesetzt — dasselbe\nMuster wie `SectionComponent` (`section.component.ts`) für „zwei Wurzelelemente,\nein Körper“. `<ng-container>` rendert selbst kein Element, die Kindselektoren aus\nEntscheidung 2 bleiben also unberührt: im laufenden Storybook erneut per\n`querySelector`/`getBoundingClientRect` gemessen (siehe Play-Funktionen), Ergebnis\nunverändert zur vorherigen, noch duplizierten Fassung.\n\n**Entscheidung 2 — die Pill ist direkt komponiertes Markup, kein `<cds-pill>`.**\nDas CSS arbeitet mit direkten Kindselektoren: `.card-featured>.card-media`,\n`.card-featured-body>.pill`, `.card-featured-body>.card-text`,\n`.card-featured-body>.card-title-hero` (jeweils mit `margin`/`min-height`/\n`line-clamp`-Übernahmen, siehe CSS-Kommentare dort). `<cds-pill>` rendert sein\n`.pill`-`<span>` nicht auf dem eigenen Host-Element, sondern eine Ebene tiefer\n(`pill.component.ts`: Host ist `<cds-pill>`, `.pill` sitzt im Template darunter) —\nprojiziert als `<cds-pill class=\"pill\">` stünde die Pille damit als\n`.card-featured-body>cds-pill>.pill`, zwei Ebenen statt einer, und die\nKindselektoren griffen nicht mehr (leise: kein Fehler, nur falsches Layout).\nDieselbe Abwägung wie in `download-cta.component.ts` (Kommentar über den\nButtons): dort verhindert `.cta-dl-actions`, das seine Kinder per Flex streckt,\ndass ein `<cds-button>`-Host mitstreckt; hier verhindert ein Kindselektor, dass\nirgendein zusätzliches Host-Element dazwischentritt. In beiden Fällen werden die\nKlassen direkt komponiert statt eine Wrapper-Komponente zu verwenden. Geprüft im\nlaufenden Storybook (`Komponenten/Cards & Teaser/Featured-Karte`): die gerenderte\nKette ist `.card-featured>.card-media` und `.card-featured-body` mit `.pill`,\n`.card-text`, `.card-title-hero` als direkten Kindern, kein zusätzliches Element\ndazwischen (siehe Play-Funktionen der Stories).\n\nWeil die Pille direkt komponiert wird, übernimmt diese Komponente auch die\n`aria-label`-Regel von `PillComponent` (`pill.component.ts`) von Hand nach:\nDefault „Bereich `<pill>`“, überschreibbar über `pillAriaLabel` — nötig, weil die\nPille nicht immer einen Bereich benennt (siehe `pillAriaLabel`-Doku unten).\n\n**Entscheidung 3 — kein separates `data-area` für die Pille.** Die Doku setzt\nKarte und Pille immer auf denselben Bereich (`docs/index.html:8979`: Karte\n`data-area=\"es\"`, Pille `data-area=\"es\"`). Die Komponente bindet deshalb `area`\nauf beide, statt einen zweiten Bereichs-Input zu erfinden.\n\n**Entscheidung 4 — kein CTA- und kein Meta-Input.** Die Doku-Vorlage zeigt\nzusätzlich einen Meta-Strip (Datum/Ort/Format) und einen `.card-cta-link`-Fuß;\ndiese Komponente benennt als API aber nur `title`/`text`/`imageSrc`/`imageAlt`/`href`/\n`pill`/`area`. Diese Komponente bildet exakt diese Fläche ab, ohne zusätzliche Inputs zu\nerfinden — ein Meta-Strip oder ein CTA-Fuß wären\neine spätere, eigene Erweiterung.\n\nVerwendungsguidance dieser Gruppe: siehe Card (`komponenten-cards-teaser-card--verwendung`).",
8
8
  "jsDocTags": {},
9
9
  "argTypes": {
10
10
  "area": {
@@ -4,7 +4,7 @@
4
4
  "id": "komponenten-cards-teaser-icon-karte",
5
5
  "name": "IconCardComponent",
6
6
  "path": "./src/lib/icon-card/icon-card.stories.ts",
7
- "description": "IconCard (`[cdsIconCard]`) — Wrapper um `.ep-card` aus css/components.css\n(css/components.css:1298–1340): die Icon-Kachel mit farbiger Icon-Fläche, Eyebrow,\nTitel, Text und optionaler Pfeil-CTA-Zeile, die auf den Beispielseiten Bereichs-\nund Angebots-Einstiege trägt (`docs/index.html:9401–9420` u. a., 81 Vorkommen).\n\n**Entscheidung 1 — Attributselektor, kein Element-Selektor.** Erste Fassung dieser\nKomponente war `cds-icon-card` als eigenes Element mit `href`-Input, das intern\nzwischen `<a class=\"ep-card ep-card-link\">` und `<div class=\"ep-card\">` wählte\n(`@if`/`@else` + `<ng-template>`). Gemessen im laufenden Storybook: zwei Karten mit\nunterschiedlich langem Text ergaben im `.ep-cards`-Grid ungleiche `.ep-card`-Höhen\n(174px/270px), weil `align-items:stretch` nur den unsichtbaren `<cds-icon-card>`-Host\nstreckt, nicht das `.ep-card`-Element eine Ebene darunter. Dieselbe Messung mit\n`.ep-card` OHNE Host-Wrapper (rohes Markup direkt in `.ep-cards`) ergab 306px/306px,\nCTA-Unterkanten auf identischer Y-Koordinate — das CSS gleicht die Höhen korrekt aus,\nsobald `.ep-card` selbst das Grid-Kind ist. Der Fehler saß also im Wrapper-Element,\nnicht in der CSS-Schicht (siehe „Im Raster“-Story, dort dieselbe Messung als\nPlay-Funktion gepinnt).\n\nDie Lösung, analog zu Angular Materials `a[mat-button], button[mat-button]`: die\nKomponente hängt sich als Attribut an ein vom Konsumenten geschriebenes `<a>` oder\n`<div>`, statt ein eigenes Element zu sein. Dadurch trägt **derselbe** native Knoten,\nden der Konsument in sein Grid schreibt, sowohl die Komponentenlogik als auch die\n`.ep-card`-Klasse — kein Host-Element dazwischen, `align-items:stretch` trifft direkt\nauf `.ep-card`.\n\n**`href` ist deshalb kein Input mehr.** Ob die Karte ein Link ist, entscheidet der\nKonsument über das gewählte Tag (`<a cdsIconCard href=\"…\">` vs. `<div cdsIconCard>`)\nund setzt `href` als natives Attribut direkt am `<a>` — Angular muss dafür nichts\nverwalten. `.ep-card-link` (nur auf `a.ep-card-link` wirksam, `CONTRIBUTING.md` §4)\nwird deshalb vom Host-Tag abgeleitet, nicht mehr per Verzweigung im Template\ngerendert.\n\n**`.ep-card-link` hängt an `<a>` UND `href`, nicht am Tag allein.** Ein `<a\ncdsIconCard>` ohne `href` ist weder fokussierbar noch hat es eine Link-Rolle —\n`.ep-card-link` (Schatten, Hover-Anhebung, Cursor) nur am Tag festzumachen hätte\neine Karte erzeugt, die aussieht wie ein Link, aber keiner ist, und genau die\nGarantie aufgegeben, die die frühere `href`-Input-Fassung strukturell hatte (leerer\nString → zwingend der `<div>`-Zweig). `isLink()` prüft deshalb beides.\n\n**`isLink()` ist eine Methode im Host-Binding, kein einmalig berechnetes Feld.**\n`href` kann nach dem Erstellen zur Laufzeit gesetzt/entfernt werden (z. B.\n`<a cdsIconCard [attr.href]=\"urlSignal()\">`). Ein Host-Binding-Ausdruck\n(`'[class.ep-card-link]': 'isLink()'`) wird bei JEDEM Refresh der DECLARING VIEW\nneu ausgewertet — also der Elternvorlage, die `<a cdsIconCard>` schreibt —, nicht\nnur bei einem Refresh der eigenen (OnPush-gesteuerten) Kindvorlage dieser\nKomponente. Das ist kein Sonderfall wie bei einem in ADR-0007 §3 verworfenen\nGetter (dort ging es um einen Getter in einer INTERPOLATION der eigenen\nKindvorlage, die unter OnPush nur bei eigenen Signal-Änderungen neu läuft):\nHost-Bindings werden unabhängig vom OnPush-Status der Komponente selbst\nmitgeführt, wenn die Elternansicht aktualisiert wird — reagiert also auf\n`[attr.href]`/`[href]`-Bindungen des Konsumenten, ob Signal- oder\nZone.js-getrieben, ohne `effect()` (ADR-0007 §4) und ohne `MutationObserver`.\nEmpirisch geprüft (Spike-Story mit `[attr.href]=\"signal()\"`-Toggle zur Laufzeit,\nseither wieder gelöscht): Setzen des Signals nach Erstrender schaltet\n`.ep-card-link` zuverlässig um. Einzige Grenze: eine Änderung, die AUSSERHALB von\nAngular direkt am nativen Element vorgenommen wird (z. B. `el.setAttribute\n('href', …)` von Fremdcode), löst keinen Refresh aus — das gilt aber für jede\nAngular-Bindung gleichermaßen und ist kein Sonderfall dieser Komponente.\n\n**Entscheidung 2 — Icon als projizierter Inhalt, OHNE dass das SVG eine eigene\nGrößenklasse tragen muss.** Wie bei `cds-stoerer` (siehe dessen Klassendoku) sind\ndie Icons auf den Beispielseiten wechselnde Heroicons, keine DS-Bereichsglyphen aus\nder Registry `icons/cds-icons.ts` — Inhalt, nicht Chrom. Deshalb Projektion über\n`<ng-content select=\"[cdsIcon]\">` statt eines `icon`-Strings. Anders als beim Störer\nist `.ep-card-icon` hier aber ein echter CONTAINER: css/components.css:1310+1318\nsetzt Maße und Stroke über den Nachfahren-Selektor `.ep-card-icon svg{width:32px;\nheight:32px;stroke-width:var(--icon-stroke-sm)}`, nicht über eine Klasse auf dem\nSVG selbst. Der Konsument liefert deshalb ein unverändertes `<svg cdsIcon\nviewBox=\"…\" aria-hidden=\"true\" focusable=\"false\">…</svg>` OHNE zusätzliche Klasse —\ndas Verhältnis Größe/Stroke kommt allein aus dem Container. Geprüft an\ncss/components.css:1298–1318 (Kommentar dort: „Farbe kommt aus der Komponente,\nnicht aus … inline gesetzten style-Attributen“) und in der Story „Interaktiv“\ngepinnt (Play-Funktion misst die gerenderte SVG-Breite).\n\n**`data-area` sitzt auf drei Elementen** (`.ep-card` (Host), `.ep-card-icon`,\n`.ep-card-eyebrow`, wie im Mockup, `docs/index.html:4681–4686`): die CTA-Farbe\nbraucht dagegen KEIN eigenes `data-area`, sie kommt über den Nachfahren-Selektor\n`.ep-card[data-area=\"…\"] .ep-card-cta` (css/components.css:1336–1339) vom\n`data-area` der Karte selbst.\n\n**Kein erfundener `aria-label`.** Der Pfeil-Suffix der CTA-Zeile ist reine visuelle\nAffordanz (`docs/index.html:4686`: `<span aria-hidden=\"true\">→</span>`) und steht\ndeshalb `aria-hidden`, unverändert zum Mockup — kein zusätzlicher, selbst\nausgedachter zugänglicher Name.\n\n**Entscheidung 3 — kein `cdsIconCards`-Raster.** `.ep-cards` ist ein reines\n`display:grid` ohne Struktur oder Verhalten (dieselbe Begründung wie beim Verzicht\nauf einen `layout-grid`-Wrapper, siehe `spec.md`). Konsumenten schreiben\n`<div class=\"ep-cards\">` von Hand (siehe Story „Im Raster“).\n\nVerwendungsguidance dieser Gruppe: siehe Card (`komponenten-cards-teaser-card--verwendung`).",
7
+ "description": "IconCard (`[cdsIconCard]`) — Wrapper um `.ep-card` aus css/components.css\n(css/components.css:1298–1340): die Icon-Kachel mit farbiger Icon-Fläche, Eyebrow,\nTitel, Text und optionaler Pfeil-CTA-Zeile, die auf den Beispielseiten Bereichs-\nund Angebots-Einstiege trägt (`docs/index.html:9401–9420` u. a., 81 Vorkommen).\n\n**Entscheidung 1 — Attributselektor, kein Element-Selektor.** Erste Fassung dieser\nKomponente war `cds-icon-card` als eigenes Element mit `href`-Input, das intern\nzwischen `<a class=\"ep-card ep-card-link\">` und `<div class=\"ep-card\">` wählte\n(`@if`/`@else` + `<ng-template>`). Gemessen im laufenden Storybook: zwei Karten mit\nunterschiedlich langem Text ergaben im `.ep-cards`-Grid ungleiche `.ep-card`-Höhen\n(174px/270px), weil `align-items:stretch` nur den unsichtbaren `<cds-icon-card>`-Host\nstreckt, nicht das `.ep-card`-Element eine Ebene darunter. Dieselbe Messung mit\n`.ep-card` OHNE Host-Wrapper (rohes Markup direkt in `.ep-cards`) ergab 306px/306px,\nCTA-Unterkanten auf identischer Y-Koordinate — das CSS gleicht die Höhen korrekt aus,\nsobald `.ep-card` selbst das Grid-Kind ist. Der Fehler saß also im Wrapper-Element,\nnicht in der CSS-Schicht (siehe „Im Raster“-Story, dort dieselbe Messung als\nPlay-Funktion gepinnt).\n\nDie Lösung, analog zu Angular Materials `a[mat-button], button[mat-button]`: die\nKomponente hängt sich als Attribut an ein vom Konsumenten geschriebenes `<a>` oder\n`<div>`, statt ein eigenes Element zu sein. Dadurch trägt **derselbe** native Knoten,\nden der Konsument in sein Grid schreibt, sowohl die Komponentenlogik als auch die\n`.ep-card`-Klasse — kein Host-Element dazwischen, `align-items:stretch` trifft direkt\nauf `.ep-card`.\n\n**`href` ist deshalb kein Input mehr.** Ob die Karte ein Link ist, entscheidet der\nKonsument über das gewählte Tag (`<a cdsIconCard href=\"…\">` vs. `<div cdsIconCard>`)\nund setzt `href` als natives Attribut direkt am `<a>` — Angular muss dafür nichts\nverwalten. `.ep-card-link` (nur auf `a.ep-card-link` wirksam, `CONTRIBUTING.md` §4)\nwird deshalb vom Host-Tag abgeleitet, nicht mehr per Verzweigung im Template\ngerendert.\n\n**`.ep-card-link` hängt an `<a>` UND `href`, nicht am Tag allein.** Ein `<a\ncdsIconCard>` ohne `href` ist weder fokussierbar noch hat es eine Link-Rolle —\n`.ep-card-link` (Schatten, Hover-Anhebung, Cursor) nur am Tag festzumachen hätte\neine Karte erzeugt, die aussieht wie ein Link, aber keiner ist, und genau die\nGarantie aufgegeben, die die frühere `href`-Input-Fassung strukturell hatte (leerer\nString → zwingend der `<div>`-Zweig). `isLink()` prüft deshalb beides.\n\n**`isLink()` ist eine Methode im Host-Binding, kein einmalig berechnetes Feld.**\n`href` kann nach dem Erstellen zur Laufzeit gesetzt/entfernt werden (z. B.\n`<a cdsIconCard [attr.href]=\"urlSignal()\">`). Ein Host-Binding-Ausdruck\n(`'[class.ep-card-link]': 'isLink()'`) wird bei JEDEM Refresh der DECLARING VIEW\nneu ausgewertet — also der Elternvorlage, die `<a cdsIconCard>` schreibt —, nicht\nnur bei einem Refresh der eigenen (OnPush-gesteuerten) Kindvorlage dieser\nKomponente. Das ist kein Sonderfall wie bei einem in ADR-0007 §3 verworfenen\nGetter (dort ging es um einen Getter in einer INTERPOLATION der eigenen\nKindvorlage, die unter OnPush nur bei eigenen Signal-Änderungen neu läuft):\nHost-Bindings werden unabhängig vom OnPush-Status der Komponente selbst\nmitgeführt, wenn die Elternansicht aktualisiert wird — reagiert also auf\n`[attr.href]`/`[href]`-Bindungen des Konsumenten, ob Signal- oder\nZone.js-getrieben, ohne `effect()` (ADR-0007 §4) und ohne `MutationObserver`.\nEmpirisch geprüft (Spike-Story mit `[attr.href]=\"signal()\"`-Toggle zur Laufzeit,\nseither wieder gelöscht): Setzen des Signals nach Erstrender schaltet\n`.ep-card-link` zuverlässig um. Einzige Grenze: eine Änderung, die AUSSERHALB von\nAngular direkt am nativen Element vorgenommen wird (z. B. `el.setAttribute\n('href', …)` von Fremdcode), löst keinen Refresh aus — das gilt aber für jede\nAngular-Bindung gleichermaßen und ist kein Sonderfall dieser Komponente.\n\n**Entscheidung 2 — Icon als projizierter Inhalt, OHNE dass das SVG eine eigene\nGrößenklasse tragen muss.** Wie bei `cds-stoerer` (siehe dessen Klassendoku) sind\ndie Icons auf den Beispielseiten wechselnde Heroicons, keine DS-Bereichsglyphen aus\nder Registry `icons/cds-icons.ts` — Inhalt, nicht Chrom. Deshalb Projektion über\n`<ng-content select=\"[cdsIcon]\">` statt eines `icon`-Strings. Anders als beim Störer\nist `.ep-card-icon` hier aber ein echter CONTAINER: css/components.css:1310+1318\nsetzt Maße und Stroke über den Nachfahren-Selektor `.ep-card-icon svg{width:32px;\nheight:32px;stroke-width:var(--icon-stroke-sm)}`, nicht über eine Klasse auf dem\nSVG selbst. Der Konsument liefert deshalb ein unverändertes `<svg cdsIcon\nviewBox=\"…\" aria-hidden=\"true\" focusable=\"false\">…</svg>` OHNE zusätzliche Klasse —\ndas Verhältnis Größe/Stroke kommt allein aus dem Container. Geprüft an\ncss/components.css:1298–1318 (Kommentar dort: „Farbe kommt aus der Komponente,\nnicht aus … inline gesetzten style-Attributen“) und in der Story „Interaktiv“\ngepinnt (Play-Funktion misst die gerenderte SVG-Breite).\n\n**`data-area` sitzt auf drei Elementen** (`.ep-card` (Host), `.ep-card-icon`,\n`.ep-card-eyebrow`, wie im Mockup, `docs/index.html:4681–4686`): die CTA-Farbe\nbraucht dagegen KEIN eigenes `data-area`, sie kommt über den Nachfahren-Selektor\n`.ep-card[data-area=\"…\"] .ep-card-cta` (css/components.css:1336–1339) vom\n`data-area` der Karte selbst.\n\n**Kein erfundener `aria-label`.** Der Pfeil-Suffix der CTA-Zeile ist reine visuelle\nAffordanz (`docs/index.html:4686`: `<span aria-hidden=\"true\">→</span>`) und steht\ndeshalb `aria-hidden`, unverändert zum Mockup — kein zusätzlicher, selbst\nausgedachter zugänglicher Name.\n\n**Entscheidung 3 — kein `cdsIconCards`-Raster.** `.ep-cards` ist ein reines\n`display:grid` ohne Struktur oder Verhalten (dieselbe Begründung wie beim Verzicht\nauf einen `layout-grid`-Wrapper). Konsumenten schreiben\n`<div class=\"ep-cards\">` von Hand (siehe Story „Im Raster“).\n\nVerwendungsguidance dieser Gruppe: siehe Card (`komponenten-cards-teaser-card--verwendung`).",
8
8
  "jsDocTags": {},
9
9
  "argTypes": {
10
10
  "area": {
@@ -4,7 +4,7 @@
4
4
  "id": "komponenten-hero-hero-bild",
5
5
  "name": "HeroImageComponent",
6
6
  "path": "./src/lib/hero-image/hero-image.stories.ts",
7
- "description": "HeroImage (cds-hero-image) — Wrapper um `.hero-image` aus css/components.css\n(css/components.css:851–878) mit optionaler Caption als Gradient-Overlay\n(`-caption`, `-caption-eyebrow`, `-caption-title`, `-caption-text`). Vollbreites,\nrandloses `<figure>` im 21:9-Format, der Standard-Hero auf allen Customer-Pages\n(17 Vorkommen in den Beispielseiten). Semantisch ein figure/figcaption-Pattern:\n`alt` trägt die faktische Bildbeschreibung, die Caption die redaktionelle\nEinordnung (Eyebrow, Titel, Text) — beide sollen sich nach `docs/index.html`\n(Abschnitt „In-Article Figure“, dasselbe Prinzip) nicht inhaltlich decken.\n\n**Entscheidung 1 — kein `<figcaption>` ohne Textinhalt.** Bleiben `eyebrow`,\n`heading` und `text` alle leer, entfällt das `<figcaption>`-Element vollständig\nstatt leer zu rendern (Variante „Hero ohne Caption“, siehe\n`storybook-angular/src/docs/seitenmuster/wissensbeitrag.mdx` Abschnitt „Aufbau“,\nZeile 3: Beitrags-Heros zeigen das visuelle Versprechen ohne Marketing-Overlay).\nEin leeres `<figcaption>` wäre totes Markup ohne Zugänglichkeitsnutzen.\n\n**Entscheidung 2 — `headingLevel` statt fest verdrahtetem `<h1>`.** Das Mockup\n(`docs/index.html:9412` u. a.) rendert `.hero-image-caption-title` durchgehend als\n`<h1>`, weil der Hero dort das erste Überschriften-Element der Seite ist. Sitzt\nder Hero dagegen unter einem eigenen `<h1>` — der Wissensbeitrag hat seinen\nTitel bereits im Article-Header (`.article-title`, ebenfalls `<h1>`) —, entstünde\nmit einem zweiten `<h1>` eine doppelte Top-Überschrift. `headingLevel` (Default\n`1`) macht die Ebene explizit, `.hero-image-caption-title` ist reine CSS-Klasse\nund trägt keine Heading-Semantik, das Styling bleibt in beiden Fällen identisch.\n\n**Entscheidung 3 — `objectPosition` als Style-Binding, die eine Ausnahme von\nSpec-Randbedingung 6.** Der Bildausschnitt ist eine Eigenschaft des konkreten\nBildes (Motiv, Kopfposition), nicht der Seite — anders als z. B. eine\nSektionsfläche, die vom Seitenkontext abhängt. `docs/index.html` setzt ihn\ndeshalb an jedem Hero individuell direkt am `<img>` (z. B. `object-position:center\n70%`, Zeile 2640). Ohne diesen Input wäre die Komponente für Porträt-lastige\nMotive unbrauchbar: `object-fit:cover` beschneidet das Bild sobald der\n`max-height`-Cap greift (siehe `.hero-image-media`), und ohne Steuerung landet\nder Ausschnitt zufällig in der Bildmitte. Als Style-Binding am `<img>`, nicht als\nKlasse — es gibt in der CSS-Schicht keine Modifier-Klassen dafür, nur den\nInline-Weg, den das Mockup selbst nutzt.\n\n**Entscheidung 4 — `tabindex`/`id` sind nicht Teil der Komponente.** Das Mockup\nsetzt beides am `<figure>` (`id=\"ep-landing-main\" tabindex=\"-1\"`, z. B. Zeile\n9406) als Sprungziel des Skip-Links. Das Sprungziel ist eine Entscheidung der\nSeite (welches Element „Hauptinhalt“ ist, hängt vom Seitenaufbau ab, nicht vom\nHero selbst), deshalb setzt der Konsument beides am `<cds-hero-image>`-Host.\nDamit dieses Sprungziel eine eigene Box hat, ist der Host per\n`:host{display:block}` selbst ein Block (Muster wie `cds-footer-main`): Als\nunbekanntes Element wäre er `display:inline` mit herausgebrochenem Block-Kind\nund hätte keine brauchbare Box für Fokus und Scroll-Position (ADR-0008, Fall 2).",
7
+ "description": "HeroImage (cds-hero-image) — Wrapper um `.hero-image` aus css/components.css\n(css/components.css:851–878) mit optionaler Caption als Gradient-Overlay\n(`-caption`, `-caption-eyebrow`, `-caption-title`, `-caption-text`). Vollbreites,\nrandloses `<figure>` im 21:9-Format, der Standard-Hero auf allen Customer-Pages\n(17 Vorkommen in den Beispielseiten). Semantisch ein figure/figcaption-Pattern:\n`alt` trägt die faktische Bildbeschreibung, die Caption die redaktionelle\nEinordnung (Eyebrow, Titel, Text) — beide sollen sich nach `docs/index.html`\n(Abschnitt „In-Article Figure“, dasselbe Prinzip) nicht inhaltlich decken.\n\n**Entscheidung 1 — kein `<figcaption>` ohne Textinhalt.** Bleiben `eyebrow`,\n`heading` und `text` alle leer, entfällt das `<figcaption>`-Element vollständig\nstatt leer zu rendern (Variante „Hero ohne Caption“, siehe\n`storybook-angular/src/docs/seitenmuster/wissensbeitrag.mdx` Abschnitt „Aufbau“,\nZeile 3: Beitrags-Heros zeigen das visuelle Versprechen ohne Marketing-Overlay).\nEin leeres `<figcaption>` wäre totes Markup ohne Zugänglichkeitsnutzen.\n\n**Entscheidung 2 — `headingLevel` statt fest verdrahtetem `<h1>`.** Das Mockup\n(`docs/index.html:9412` u. a.) rendert `.hero-image-caption-title` durchgehend als\n`<h1>`, weil der Hero dort das erste Überschriften-Element der Seite ist. Sitzt\nder Hero dagegen unter einem eigenen `<h1>` — der Wissensbeitrag hat seinen\nTitel bereits im Article-Header (`.article-title`, ebenfalls `<h1>`) —, entstünde\nmit einem zweiten `<h1>` eine doppelte Top-Überschrift. `headingLevel` (Default\n`1`) macht die Ebene explizit, `.hero-image-caption-title` ist reine CSS-Klasse\nund trägt keine Heading-Semantik, das Styling bleibt in beiden Fällen identisch.\n\n**Entscheidung 3 — `objectPosition` als Style-Binding, bewusste Ausnahme von\nder Regel, dass Mockup-Inline-Styles nicht in den Wrapper wandern.** Der\nBildausschnitt ist eine Eigenschaft des konkreten\nBildes (Motiv, Kopfposition), nicht der Seite — anders als z. B. eine\nSektionsfläche, die vom Seitenkontext abhängt. `docs/index.html` setzt ihn\ndeshalb an jedem Hero individuell direkt am `<img>` (z. B. `object-position:center\n70%`, Zeile 2640). Ohne diesen Input wäre die Komponente für Porträt-lastige\nMotive unbrauchbar: `object-fit:cover` beschneidet das Bild sobald der\n`max-height`-Cap greift (siehe `.hero-image-media`), und ohne Steuerung landet\nder Ausschnitt zufällig in der Bildmitte. Als Style-Binding am `<img>`, nicht als\nKlasse — es gibt in der CSS-Schicht keine Modifier-Klassen dafür, nur den\nInline-Weg, den das Mockup selbst nutzt.\n\n**Entscheidung 4 — `tabindex`/`id` sind nicht Teil der Komponente.** Das Mockup\nsetzt beides am `<figure>` (`id=\"ep-landing-main\" tabindex=\"-1\"`, z. B. Zeile\n9406) als Sprungziel des Skip-Links. Das Sprungziel ist eine Entscheidung der\nSeite (welches Element „Hauptinhalt“ ist, hängt vom Seitenaufbau ab, nicht vom\nHero selbst), deshalb setzt der Konsument beides am `<cds-hero-image>`-Host.\nDamit dieses Sprungziel eine eigene Box hat, ist der Host per\n`:host{display:block}` selbst ein Block (Muster wie `cds-footer-main`): Als\nunbekanntes Element wäre er `display:inline` mit herausgebrochenem Block-Kind\nund hätte keine brauchbare Box für Fokus und Scroll-Position (ADR-0008, Fall 2).",
8
8
  "jsDocTags": {},
9
9
  "argTypes": {
10
10
  "alt": {
@@ -4,7 +4,7 @@
4
4
  "id": "komponenten-hero-störer",
5
5
  "name": "StoererSetComponent",
6
6
  "path": "./src/lib/stoerer/stoerer.stories.ts",
7
- "description": "StoererSet (cds-stoerer-set) — Wrapper um `.stoerer-set` / `.stoerer-list` aus\ncss/components.css (css/components.css:926–974): das Set aus ein bis drei\n`<cds-stoerer>`-Kacheln, das auf der Startseite oben rechts über dem Hero-Bild\nliegt. Der Positionsrahmen `.stoerer-hero` (umschließt Hero-Bild UND Set,\n`container-type:inline-size` für die Container-Query-Schwelle) ist bewusst NICHT\nTeil dieser Komponente, sondern bleibt beim Konsumenten (siehe\n`.scratch/angular-seitenbausteine/issues/03-stoerer.md`): er umschließt Hero UND\nSet gemeinsam, das könnte diese Komponente als internes Detail nicht leisten, ohne\ndas Hero-Bild selbst zu kennen.\n\n**Entscheidung — `<li>` ohne strukturelles Wrapper-Element.** `.stoerer-list` ist\nein `<ul>`; nach HTML-AAM bekommt ein `<li>` die implizite Rolle `listitem` nur als\ndirektes Kind eines `<ul>`/`<ol>`/`<menu>` — Screenreader kündigen die Liste sonst\nnicht mit Anzahl an. Ein einfaches `<ng-content>` hätte `<cds-stoerer>` selbst\nzwischen `<ul>` und `<li>` geschoben (kein direktes Kind mehr), und ein\n`display:contents`-Wrapper verbietet sich als neues CSS (ADR-0001). Gelöst über\ndasselbe Muster, das `cds-area-tabs`/`cds-area-tab` bereits für „projizierte\nKind-Komponente, deren Markup an einer selbst bestimmten Stelle im eigenen Template\nlandet“ etabliert (`area-tabs/area-tabs.component.ts`): `cds-stoerer` rendert nicht\nin sich selbst, sondern in ein `<ng-template>` und liefert es als `TemplateRef`\n(`stoerer.component.ts`). Diese Komponente liest die projizierten `<cds-stoerer>`\nüber `contentChildren()` und setzt ihr Template per `ngTemplateOutlet` direkt in\nein selbst gerendertes `<li>` ein. Im laufenden Storybook geprüft (`Komponenten/Hero/\nStörer` → Story „Interaktiv“, DOM live im Browser per `querySelector` abgefragt,\nzusätzlich in der Play-Funktion der Story gepinnt): `<ul class=\"stoerer-list\">`\nhat ausschließlich `<li>` als direkte Kinder (`ul.stoerer-list > *` → `[\"LI\",\"LI\"]`),\n`<cds-stoerer>` selbst taucht im gerenderten DOM nirgends auf — Angular hängt\nunprojizierten Content ohne passendes `<ng-content>`-Ziel gar nicht erst ein, exakt\nwie bei `<cds-area-tab>` innerhalb von `<cds-area-tabs>`. Ein `items`-Array (die im\nTicket vermerkte Alternative) hätte denselben DOM-Baum ergeben, aber den\nKonsumenten gezwungen, das projizierte Icon jeder Kachel in ein Datenobjekt\numzuziehen; mit dieser Lösung bleibt die deklarative, Content-projizierende API\nerhalten.\n\nVerwendungsguidance dieser Gruppe: siehe Hero-Bild (`komponenten-hero-hero-bild--verwendung`).",
7
+ "description": "StoererSet (cds-stoerer-set) — Wrapper um `.stoerer-set` / `.stoerer-list` aus\ncss/components.css (css/components.css:926–974): das Set aus ein bis drei\n`<cds-stoerer>`-Kacheln, das auf der Startseite oben rechts über dem Hero-Bild\nliegt. Der Positionsrahmen `.stoerer-hero` (umschließt Hero-Bild UND Set,\n`container-type:inline-size` für die Container-Query-Schwelle) ist bewusst NICHT\nTeil dieser Komponente, sondern bleibt beim Konsumenten: er umschließt Hero UND\nSet gemeinsam, das könnte diese Komponente als internes Detail nicht leisten, ohne\ndas Hero-Bild selbst zu kennen.\n\n**Entscheidung — `<li>` ohne strukturelles Wrapper-Element.** `.stoerer-list` ist\nein `<ul>`; nach HTML-AAM bekommt ein `<li>` die implizite Rolle `listitem` nur als\ndirektes Kind eines `<ul>`/`<ol>`/`<menu>` — Screenreader kündigen die Liste sonst\nnicht mit Anzahl an. Ein einfaches `<ng-content>` hätte `<cds-stoerer>` selbst\nzwischen `<ul>` und `<li>` geschoben (kein direktes Kind mehr), und ein\n`display:contents`-Wrapper verbietet sich als neues CSS (ADR-0001). Gelöst über\ndasselbe Muster, das `cds-area-tabs`/`cds-area-tab` bereits für „projizierte\nKind-Komponente, deren Markup an einer selbst bestimmten Stelle im eigenen Template\nlandet“ etabliert (`area-tabs/area-tabs.component.ts`): `cds-stoerer` rendert nicht\nin sich selbst, sondern in ein `<ng-template>` und liefert es als `TemplateRef`\n(`stoerer.component.ts`). Diese Komponente liest die projizierten `<cds-stoerer>`\nüber `contentChildren()` und setzt ihr Template per `ngTemplateOutlet` direkt in\nein selbst gerendertes `<li>` ein. Im laufenden Storybook geprüft (`Komponenten/Hero/\nStörer` → Story „Interaktiv“, DOM live im Browser per `querySelector` abgefragt,\nzusätzlich in der Play-Funktion der Story gepinnt): `<ul class=\"stoerer-list\">`\nhat ausschließlich `<li>` als direkte Kinder (`ul.stoerer-list > *` → `[\"LI\",\"LI\"]`),\n`<cds-stoerer>` selbst taucht im gerenderten DOM nirgends auf — Angular hängt\nunprojizierten Content ohne passendes `<ng-content>`-Ziel gar nicht erst ein, exakt\nwie bei `<cds-area-tab>` innerhalb von `<cds-area-tabs>`. Ein `items`-Array (die\nerwogene Alternative) hätte denselben DOM-Baum ergeben, aber den\nKonsumenten gezwungen, das projizierte Icon jeder Kachel in ein Datenobjekt\numzuziehen; mit dieser Lösung bleibt die deklarative, Content-projizierende API\nerhalten.\n\nVerwendungsguidance dieser Gruppe: siehe Hero-Bild (`komponenten-hero-hero-bild--verwendung`).",
8
8
  "jsDocTags": {},
9
9
  "argTypes": {
10
10
  "label": {
@@ -4,7 +4,7 @@
4
4
  "id": "komponenten-tabelle-tabelle",
5
5
  "name": "TableComponent",
6
6
  "path": "./src/lib/table/table.stories.ts",
7
- "description": "Table (`cds-table`) — Wrapper um `.tbl`/`.tbl-wrap` aus css/components.css\n(css/components.css:1462–1480): die Datentabelle der Doku-Site (`sec-table`,\nNav-Einträge „Standard“ und „Gestreift“). Ausgezählt: außerhalb von `sec-table`\nselbst kommt `.tbl-wrap` nur zweimal vor, beide auf Beitragsseiten\n(`docs/index.html:8622`, `15080`) — keine der 24 Beispielseiten außerhalb von\nWissensbeiträgen setzt aktuell eine Tabelle ein. Neuntes Ticket der\nSeitenbausteine-Serie.\n\n**Kein Daten-Input.** Eine `columns`/`rows`-API würde Zellinhalte auf Strings\nfestlegen; die Beispielseiten setzen darin Badges, Links und `data-num`\n(`docs/index.html:5076–5177`). Der Konsument projiziert `<thead>`/`<tbody>`/\n`<tfoot>` unverändert per `<ng-content>`, die Komponente liefert nur die\nHülle: `.tbl-wrap`, `.tbl`, `<caption>` und die Striped-Klasse.\n\n**`caption` ist Pflicht (`input.required<string>()`), abweichend von der\nTicket-Skizze (dort `caption? = ''`).** Die eigene Doku dieser Komponente\n(`storybook-angular/src/docs/komponenten/tabelle.mdx:137`) nennt `<caption>`\nausdrücklich „Pflicht, Screenreader lesen den Titel vor, bevor die Zellen\nvorgelesen werden“ und wiederholt das in den Dos (`tabelle.mdx:147`:\n„`<caption>` und `scope`-Attribute immer setzen, auch bei einfachen\nTabellen“). Ausgezählt: alle 5 realen `.tbl`-Vorkommen in `docs/index.html`\nhaben eine `<caption>`, ausnahmslos. Ein optionaler Input mit Default `''`\nhätte genau den Fall erlaubt, den die eigene Doku verbietet — nach\nADR-0007 §2 ist `caption` damit Inhalt, nicht Beiwerk: ohne sie ist eine\n`cds-table` eine Tabelle, die die eigenen Barrierefreiheitsregeln bricht,\nalso sinnlos. Ein vergessenes Binding wirft jetzt `NG0950`, statt eine\nunbeschriftete Tabelle still auszuliefern.\n\n**Element-Selektor, `.tbl-wrap`/`.tbl` sitzen im eigenen Template, nicht am\nHost (ADR-0008-Standardfall, analog zu `cds-facts`/`cds-faq`).** Ausgezählt:\nalle 5 `.tbl-wrap`-Vorkommen in `docs/index.html` (Zeilen 5061, 5116, 5236,\n8622, 15080). Keines ist selbst ein direktes Grid-/Flex-Kind — das einzige\nVorkommen in einem `.layout-grid` (Zeile 5236) sitzt eine Ebene tiefer in\neinem `.col-8`, das seinerseits der Grid-Child ist; `.tbl-wrap` ist dort ein\ngewöhnlicher Block-Nachfahre, dessen Breite vom umschließenden `.col-8`\nkommt, nicht von `align-items:stretch` auf einer bestimmten DOM-Tiefe (kein\n`.ep-card`-Fall). Keines trägt eine `col-*`-Klasse selbst, keines variiert\ndas Tag (immer `<div class=\"tbl-wrap\">`). Ein `<cds-table>`-Host ohne eigene\nKlasse und ohne eigenes `background` gerät deshalb nicht in den „Fläche am\nHost“-Fall aus ADR-0008 Fall 2: die Komponente setzt nichts auf dem Host,\ndas eine eigene Box bräuchte, `.tbl-wrap` bleibt ein normaler Block-Nachfahre\nunabhängig vom `display` des `<cds-table>`-Hosts (derselbe Grund, warum\n`cds-facts`/`cds-faq` als Element-Selektor funktionieren).\n\nEine Attributselektor-Variante direkt am `<table>` (`table[cdsTable]`, analog\nzu `cdsSection`) wurde erwogen und verworfen: `<table>` ist als Tag ohnehin\nfix (kein Tag-Wechsel wie bei `cdsIconCard`), aber `.tbl-wrap` bräuchte dann\nein ZWEITES Element, das der Konsument von Hand um die Tabelle schreiben\nmüsste (`<div class=\"tbl-wrap\" tabindex=\"0\" role=\"region\" aria-label=\"…\">\n<table cdsTable>…`) — genau die beiden Aufgaben, die laut Ticket am ehesten\nvergessen werden (fokussierbarer, benannter Scroll-Container), blieben dann\nbeim Konsumenten statt bei der Komponente. Der Element-Selektor hält Wrap und\nTabelle zusammen in einem Template.\n\n**Geprüft im laufenden Storybook (Story „Interaktiv“, Play-Funktion):**\n`<table class=\"tbl\">` liegt direkt unter `<div class=\"tbl-wrap\">`, `<caption>`\nist ihr erstes Kind, danach folgen ohne zusätzlichen Knoten dazwischen genau\ndie projizierten `<thead>`/`<tbody>`-Elemente — `<ng-content>` fügt selbst\nkein DOM-Element ein, es verschiebt nur die vom Konsumenten geschriebenen\nnativen Elemente an ihre Stelle. Damit bleibt die Tabellenstruktur (`<table>`\n→ `<caption>` + `<thead>` + `<tbody>` + optional `<tfoot>`, keine fremde\nEbene dazwischen) exakt so gültig wie im rohen Markup.\n\n**`.tbl-wrap` ist immer fokussierbar UND immer benannt.** Das CSS gibt dem\nScroll-Container einen eigenen `:focus-visible`-Ring\n(css/components.css:1463) — ein scrollbarer Bereich muss per Tastatur\nerreichbar sein (WCAG 2.1.1), `tabindex=\"0\"` steht deshalb fest im Template.\n`role=\"region\"` braucht laut HTML-AAM einen zugänglichen Namen, sonst bekäme\nder Bereich keine Landmark-Rolle — weil `caption` jetzt Pflicht ist, kann\n`accessibleName()` (`scrollLabel() || caption()`) nicht mehr leer werden,\n`role`/`aria-label` sind deshalb unbedingt gesetzt, kein Nullfall mehr zu\nbehandeln. `scrollLabel` bleibt eine optionale Überschreibung (Vorrang vor\n`caption`) für Fälle, in denen der sichtbare Tabellentitel für den\nScroll-Container zu lang oder zu unspezifisch wäre.\n\n**`.tbl-sort` bleibt außen vor.** Die Klasse existiert\n(css/components.css:1477–1482), die Sortierlogik nicht — kein zugehöriges\nJS in `docs/main.js` für die Beispielseiten. Ein Wrapper, der nur den\nButton-Look lieferte, täuschte Funktion vor, die es nicht gibt. Siehe\n`.scratch/angular-seitenbausteine/issues/19-fehlende-sortierlogik-tbl-sort.md`.",
7
+ "description": "Table (`cds-table`) — Wrapper um `.tbl`/`.tbl-wrap` aus css/components.css\n(css/components.css:1462–1480): die Datentabelle der Doku-Site (`sec-table`,\nNav-Einträge „Standard“ und „Gestreift“). Ausgezählt: außerhalb von `sec-table`\nselbst kommt `.tbl-wrap` nur zweimal vor, beide auf Beitragsseiten\n(`docs/index.html:8622`, `15080`) — keine der 24 Beispielseiten außerhalb von\nWissensbeiträgen setzt aktuell eine Tabelle ein.\n\n**Kein Daten-Input.** Eine `columns`/`rows`-API würde Zellinhalte auf Strings\nfestlegen; die Beispielseiten setzen darin Badges, Links und `data-num`\n(`docs/index.html:5076–5177`). Der Konsument projiziert `<thead>`/`<tbody>`/\n`<tfoot>` unverändert per `<ng-content>`, die Komponente liefert nur die\nHülle: `.tbl-wrap`, `.tbl`, `<caption>` und die Striped-Klasse.\n\n**`caption` ist Pflicht (`input.required<string>()`), abweichend von der\nursprünglichen Skizze (dort `caption? = ''`).** Die eigene Doku dieser Komponente\n(`storybook-angular/src/docs/komponenten/tabelle.mdx:137`) nennt `<caption>`\nausdrücklich „Pflicht, Screenreader lesen den Titel vor, bevor die Zellen\nvorgelesen werden“ und wiederholt das in den Dos (`tabelle.mdx:147`:\n„`<caption>` und `scope`-Attribute immer setzen, auch bei einfachen\nTabellen“). Ausgezählt: alle 5 realen `.tbl`-Vorkommen in `docs/index.html`\nhaben eine `<caption>`, ausnahmslos. Ein optionaler Input mit Default `''`\nhätte genau den Fall erlaubt, den die eigene Doku verbietet — nach\nADR-0007 §2 ist `caption` damit Inhalt, nicht Beiwerk: ohne sie ist eine\n`cds-table` eine Tabelle, die die eigenen Barrierefreiheitsregeln bricht,\nalso sinnlos. Ein vergessenes Binding wirft jetzt `NG0950`, statt eine\nunbeschriftete Tabelle still auszuliefern.\n\n**Element-Selektor, `.tbl-wrap`/`.tbl` sitzen im eigenen Template, nicht am\nHost (ADR-0008-Standardfall, analog zu `cds-facts`/`cds-faq`).** Ausgezählt:\nalle 5 `.tbl-wrap`-Vorkommen in `docs/index.html` (Zeilen 5061, 5116, 5236,\n8622, 15080). Keines ist selbst ein direktes Grid-/Flex-Kind — das einzige\nVorkommen in einem `.layout-grid` (Zeile 5236) sitzt eine Ebene tiefer in\neinem `.col-8`, das seinerseits der Grid-Child ist; `.tbl-wrap` ist dort ein\ngewöhnlicher Block-Nachfahre, dessen Breite vom umschließenden `.col-8`\nkommt, nicht von `align-items:stretch` auf einer bestimmten DOM-Tiefe (kein\n`.ep-card`-Fall). Keines trägt eine `col-*`-Klasse selbst, keines variiert\ndas Tag (immer `<div class=\"tbl-wrap\">`). Ein `<cds-table>`-Host ohne eigene\nKlasse und ohne eigenes `background` gerät deshalb nicht in den „Fläche am\nHost“-Fall aus ADR-0008 Fall 2: die Komponente setzt nichts auf dem Host,\ndas eine eigene Box bräuchte, `.tbl-wrap` bleibt ein normaler Block-Nachfahre\nunabhängig vom `display` des `<cds-table>`-Hosts (derselbe Grund, warum\n`cds-facts`/`cds-faq` als Element-Selektor funktionieren).\n\nEine Attributselektor-Variante direkt am `<table>` (`table[cdsTable]`, analog\nzu `cdsSection`) wurde erwogen und verworfen: `<table>` ist als Tag ohnehin\nfix (kein Tag-Wechsel wie bei `cdsIconCard`), aber `.tbl-wrap` bräuchte dann\nein ZWEITES Element, das der Konsument von Hand um die Tabelle schreiben\nmüsste (`<div class=\"tbl-wrap\" tabindex=\"0\" role=\"region\" aria-label=\"…\">\n<table cdsTable>…`) — genau die beiden Aufgaben, die am ehesten\nvergessen werden (fokussierbarer, benannter Scroll-Container), blieben dann\nbeim Konsumenten statt bei der Komponente. Der Element-Selektor hält Wrap und\nTabelle zusammen in einem Template.\n\n**Geprüft im laufenden Storybook (Story „Interaktiv“, Play-Funktion):**\n`<table class=\"tbl\">` liegt direkt unter `<div class=\"tbl-wrap\">`, `<caption>`\nist ihr erstes Kind, danach folgen ohne zusätzlichen Knoten dazwischen genau\ndie projizierten `<thead>`/`<tbody>`-Elemente — `<ng-content>` fügt selbst\nkein DOM-Element ein, es verschiebt nur die vom Konsumenten geschriebenen\nnativen Elemente an ihre Stelle. Damit bleibt die Tabellenstruktur (`<table>`\n→ `<caption>` + `<thead>` + `<tbody>` + optional `<tfoot>`, keine fremde\nEbene dazwischen) exakt so gültig wie im rohen Markup.\n\n**`.tbl-wrap` ist immer fokussierbar UND immer benannt.** Das CSS gibt dem\nScroll-Container einen eigenen `:focus-visible`-Ring\n(css/components.css:1463) — ein scrollbarer Bereich muss per Tastatur\nerreichbar sein (WCAG 2.1.1), `tabindex=\"0\"` steht deshalb fest im Template.\n`role=\"region\"` braucht laut HTML-AAM einen zugänglichen Namen, sonst bekäme\nder Bereich keine Landmark-Rolle — weil `caption` jetzt Pflicht ist, kann\n`accessibleName()` (`scrollLabel() || caption()`) nicht mehr leer werden,\n`role`/`aria-label` sind deshalb unbedingt gesetzt, kein Nullfall mehr zu\nbehandeln. `scrollLabel` bleibt eine optionale Überschreibung (Vorrang vor\n`caption`) für Fälle, in denen der sichtbare Tabellentitel für den\nScroll-Container zu lang oder zu unspezifisch wäre.\n\n**`.tbl-sort` bleibt außen vor.** Die Klasse existiert\n(css/components.css:1477–1482), die Sortierlogik nicht — kein zugehöriges\nJS in `docs/main.js` für die Beispielseiten. Ein Wrapper, der nur den\nButton-Look lieferte, täuschte Funktion vor, die es nicht gibt.",
8
8
  "jsDocTags": {},
9
9
  "argTypes": {
10
10
  "caption": {
@@ -4,7 +4,7 @@
4
4
  "id": "komponenten-tabelle-vergleichstabelle",
5
5
  "name": "CompareComponent",
6
6
  "path": "./src/lib/compare/compare.stories.ts",
7
- "description": "Compare (`cds-compare`) — Wrapper um `.ep-compare*` aus css/components.css\n(css/components.css:1275–1294): die aufklappbare Vergleichstabelle für den zeilenweisen\nDirektvergleich mehrerer Pakete/Tarife (Doku-Site `sec-table`, Nav-Eintrag\n„Vergleichstabelle (aufklappbar)“, `docs/index.html:5182–5204`). Zehntes Ticket der\nSeitenbausteine-Serie, nach `cds-table` (Ticket 09).\n\n**Mit Daten-Input, anders als `cds-table`.** `cds-table` projiziert `<thead>`/`<tbody>`\nunverändert per `<ng-content>`, weil seine Zellen Badges, Links und `data-num` tragen können\n(siehe dessen Klassendoku). Hier ist der Zellinhalt strukturell auf Ja/Nein oder eine kurze\nAngabe begrenzt — ausgezählt: alle 22 Datenzellen des einzigen realen Vorkommens\n(`docs/index.html:11970–12038`, 11 Zeilen × 2 Spalten, ohne die Zeilenüberschriften) sind\nentweder ein `.ep-compare-yes`/`-no`-Marker (18×, davon 14× „Ja“, 4× „Nein“) oder ein kurzer\nText wie „bis 70 Mrd. Param.“ bzw. „30 Min.“ (4×) — nie ein Badge, Link oder anderes Markup.\nWeil Spaltenzahl, `pro`-Spalte und Zellinhalt damit vollständig aus zwei Arrays (`columns`,\n`rows`) hervorgehen, würde `<ng-content>` beim Konsumenten nur denselben `@for`-Code\nduplizieren, ohne eine Freiheit zu gewinnen, die die Zell-Typisierung (`boolean | string`)\nohnehin ausschließt.\n\n**Ja/Nein-Zellen tragen einen Screenreader-Text, übernommen statt erfunden.** Im einzigen\nrealen Vorkommen steht hinter jeder Glyphe ein `.sr-only`-Text, ausnahmslos:\n`<span class=\"ep-compare-yes\" aria-hidden=\"true\">✓</span><span class=\"sr-only\">Enthalten</span>`\n(14×) bzw. `<span class=\"ep-compare-no\" aria-hidden=\"true\">−</span><span class=\"sr-only\">Nicht\nenthalten</span>` (4×, „−“ ist U+2212 Minus, kein Bindestrich). Genau diese Zeichen und dieser\nWortlaut, keine eigene Formulierung — Farbe und Glyphe allein tragen keine Bedeutung\n(WCAG 1.4.1), der `.sr-only`-Text ist deshalb kein optionaler Zusatz, sondern fest verdrahtet,\nnicht per Input überschreibbar.\n\n**`caption` ist Pflicht (`input.required<string>()`), abweichend von der Ticket-Skizze (dort\nkein Input dafür vorgesehen) — dieselbe Begründung wie bei `cds-table`.** Die eigene Doku\n(`storybook-angular/src/docs/komponenten/tabelle.mdx:91`) nennt `<caption class=\"sr-only\">`\nausdrücklich als Teil des Bauteils, und das einzige reale Vorkommen hat eine. Anders als bei\n`cds-table` ist sie hier `.sr-only`, nicht sichtbar: der Klartext steht schon im `<summary>`\n(`summary`-Input), die `<caption>` wiederholt ihn nur für Screenreader, die nach dem Aufklappen\ndirekt in die Tabelle springen, ohne den Summary-Text erneut vorgelesen zu bekommen.\n\n**`rowsLabel` (erste Kopfzelle, `<th scope=\"col\">Funktion</th>` im Mockup) ist dagegen Beiwerk,\noptional mit Default `''`.** Auch dafür sieht die Ticket-Skizze keinen Input vor. Den Text fest\nim Template zu verdrahten hieße, AI.Box-Copy in einen wiederverwendbaren Wrapper zu backen\n(ADR-0007 §2); ihn ganz wegzulassen wich ohne CSS-Befund vom einzigen Beleg ab, ohne dass die\nDoku das verlangt. Anders als `caption` ist die Kopfzelle nicht barrierefreiheitsrelevant\nPflicht: ein leeres `<th scope=\"col\">` bleibt eine gültige, in Vergleichsmatrizen verbreitete\nEcke ohne eigenen Namen. Deshalb Beiwerk, kein Inhalt — das Beispielwort „Funktion“ steht in\nder Story, nicht in der Klasse.\n\n**Natives `<details>`/`<summary>` bleibt erhalten, kein nachgebautes Disclosure** (Ticket-\nVorgabe, analog `cds-faq`): Tastaturbedienung und Toggle-Verhalten kommen vom Browser, das CSS\nhängt direkt an `[open]` (`.ep-compare[open] .ep-compare-caret`). `<details class=\"ep-compare\">`\nsitzt deshalb im TEMPLATE, nicht am Host — derselbe Grund wie bei `cds-table`s\n`.tbl-wrap`/`.tbl`: ein `<cds-compare>`-Host ist ein unbekanntes Custom Element, kein echtes\n`<details>`, und kann dessen native Disclosure-Semantik nicht annehmen.\n\n**Element-Selektor (ADR-0008-Standardfall).** `.ep-compare` sitzt im einzigen realen Vorkommen\nals gewöhnlicher Block-Nachfahre in `.ep-section` (`docs/index.html:11969`) — kein Grid-/\nFlex-Kind, keine `col-*`-Klasse vom Konsumenten, kein Tag-Wechsel (immer `<details>`). Keines\nder drei ADR-0008-Kriterien greift, ein Attributselektor brächte hier nichts.\n\n**`open` ist ein reiner Anfangszustand, keine Zwei-Wege-Bindung.** Gebunden über\n`[attr.open]=\"open() ? '' : null\"`: Angular schreibt das Attribut nur, wenn sich der GEPRÜFTE\nAusdruckswert seit dem letzten Change-Detection-Lauf ändert. `open()` bleibt nach dem ersten\nRendern konstant (die Komponente schreibt nie in dieses Signal zurück), ein nachfolgender\nnativer Toggle wird deshalb nicht rückgängig gemacht — geprüft in der Story „Interaktiv“\n(Play-Funktion: Klick öffnet, `toggled` feuert `true`, ein zweiter, vom Toggle unabhängiger\nInteraktionsschritt lässt die Tabelle weiterhin offen).\n\nVerwendungsguidance dieser Gruppe: siehe Tabelle (`komponenten-tabelle-tabelle--verwendung`).",
7
+ "description": "Compare (`cds-compare`) — Wrapper um `.ep-compare*` aus css/components.css\n(css/components.css:1275–1294): die aufklappbare Vergleichstabelle für den zeilenweisen\nDirektvergleich mehrerer Pakete/Tarife (Doku-Site `sec-table`, Nav-Eintrag\n„Vergleichstabelle (aufklappbar)“, `docs/index.html:5182–5204`), nach `cds-table`.\n\n**Mit Daten-Input, anders als `cds-table`.** `cds-table` projiziert `<thead>`/`<tbody>`\nunverändert per `<ng-content>`, weil seine Zellen Badges, Links und `data-num` tragen können\n(siehe dessen Klassendoku). Hier ist der Zellinhalt strukturell auf Ja/Nein oder eine kurze\nAngabe begrenzt — ausgezählt: alle 22 Datenzellen des einzigen realen Vorkommens\n(`docs/index.html:11970–12038`, 11 Zeilen × 2 Spalten, ohne die Zeilenüberschriften) sind\nentweder ein `.ep-compare-yes`/`-no`-Marker (18×, davon 14× „Ja“, 4× „Nein“) oder ein kurzer\nText wie „bis 70 Mrd. Param.“ bzw. „30 Min.“ (4×) — nie ein Badge, Link oder anderes Markup.\nWeil Spaltenzahl, `pro`-Spalte und Zellinhalt damit vollständig aus zwei Arrays (`columns`,\n`rows`) hervorgehen, würde `<ng-content>` beim Konsumenten nur denselben `@for`-Code\nduplizieren, ohne eine Freiheit zu gewinnen, die die Zell-Typisierung (`boolean | string`)\nohnehin ausschließt.\n\n**Ja/Nein-Zellen tragen einen Screenreader-Text, übernommen statt erfunden.** Im einzigen\nrealen Vorkommen steht hinter jeder Glyphe ein `.sr-only`-Text, ausnahmslos:\n`<span class=\"ep-compare-yes\" aria-hidden=\"true\">✓</span><span class=\"sr-only\">Enthalten</span>`\n(14×) bzw. `<span class=\"ep-compare-no\" aria-hidden=\"true\">−</span><span class=\"sr-only\">Nicht\nenthalten</span>` (4×, „−“ ist U+2212 Minus, kein Bindestrich). Genau diese Zeichen und dieser\nWortlaut, keine eigene Formulierung — Farbe und Glyphe allein tragen keine Bedeutung\n(WCAG 1.4.1), der `.sr-only`-Text ist deshalb kein optionaler Zusatz, sondern fest verdrahtet,\nnicht per Input überschreibbar.\n\n**`caption` ist Pflicht (`input.required<string>()`), abweichend von der ursprünglichen Skizze (dort\nkein Input dafür vorgesehen) — dieselbe Begründung wie bei `cds-table`.** Die eigene Doku\n(`storybook-angular/src/docs/komponenten/tabelle.mdx:91`) nennt `<caption class=\"sr-only\">`\nausdrücklich als Teil des Bauteils, und das einzige reale Vorkommen hat eine. Anders als bei\n`cds-table` ist sie hier `.sr-only`, nicht sichtbar: der Klartext steht schon im `<summary>`\n(`summary`-Input), die `<caption>` wiederholt ihn nur für Screenreader, die nach dem Aufklappen\ndirekt in die Tabelle springen, ohne den Summary-Text erneut vorgelesen zu bekommen.\n\n**`rowsLabel` (erste Kopfzelle, `<th scope=\"col\">Funktion</th>` im Mockup) ist dagegen Beiwerk,\noptional mit Default `''`.** Auch dafür sieht die ursprüngliche Skizze keinen Input vor. Den Text fest\nim Template zu verdrahten hieße, AI.Box-Copy in einen wiederverwendbaren Wrapper zu backen\n(ADR-0007 §2); ihn ganz wegzulassen wich ohne CSS-Befund vom einzigen Beleg ab, ohne dass die\nDoku das verlangt. Anders als `caption` ist die Kopfzelle nicht barrierefreiheitsrelevant\nPflicht: ein leeres `<th scope=\"col\">` bleibt eine gültige, in Vergleichsmatrizen verbreitete\nEcke ohne eigenen Namen. Deshalb Beiwerk, kein Inhalt — das Beispielwort „Funktion“ steht in\nder Story, nicht in der Klasse.\n\n**Natives `<details>`/`<summary>` bleibt erhalten, kein nachgebautes Disclosure**\n(analog `cds-faq`): Tastaturbedienung und Toggle-Verhalten kommen vom Browser, das CSS\nhängt direkt an `[open]` (`.ep-compare[open] .ep-compare-caret`). `<details class=\"ep-compare\">`\nsitzt deshalb im TEMPLATE, nicht am Host — derselbe Grund wie bei `cds-table`s\n`.tbl-wrap`/`.tbl`: ein `<cds-compare>`-Host ist ein unbekanntes Custom Element, kein echtes\n`<details>`, und kann dessen native Disclosure-Semantik nicht annehmen.\n\n**Element-Selektor (ADR-0008-Standardfall).** `.ep-compare` sitzt im einzigen realen Vorkommen\nals gewöhnlicher Block-Nachfahre in `.ep-section` (`docs/index.html:11969`) — kein Grid-/\nFlex-Kind, keine `col-*`-Klasse vom Konsumenten, kein Tag-Wechsel (immer `<details>`). Keines\nder drei ADR-0008-Kriterien greift, ein Attributselektor brächte hier nichts.\n\n**`open` ist ein reiner Anfangszustand, keine Zwei-Wege-Bindung.** Gebunden über\n`[attr.open]=\"open() ? '' : null\"`: Angular schreibt das Attribut nur, wenn sich der GEPRÜFTE\nAusdruckswert seit dem letzten Change-Detection-Lauf ändert. `open()` bleibt nach dem ersten\nRendern konstant (die Komponente schreibt nie in dieses Signal zurück), ein nachfolgender\nnativer Toggle wird deshalb nicht rückgängig gemacht — geprüft in der Story „Interaktiv“\n(Play-Funktion: Klick öffnet, `toggled` feuert `true`, ein zweiter, vom Toggle unabhängiger\nInteraktionsschritt lässt die Tabelle weiterhin offen).\n\nVerwendungsguidance dieser Gruppe: siehe Tabelle (`komponenten-tabelle-tabelle--verwendung`).",
8
8
  "jsDocTags": {},
9
9
  "argTypes": {
10
10
  "caption": {
@@ -4,7 +4,7 @@
4
4
  "id": "seitenmuster-seminar-·-training-fakten-liste",
5
5
  "name": "FactsComponent",
6
6
  "path": "./src/lib/facts/facts.stories.ts",
7
- "description": "Facts (`cds-facts`) — Wrapper um `.ep-facts` aus css/components.css\n(css/components.css:1261–1272): die Definitionsliste für die harten Eckdaten\neines Angebots (Termin, Dauer, Ort, Preis), einspaltig mit Haarlinie zwischen\nden Paaren oder als `.is-grid` zweispaltig für Kästen, die neben Inhalt stehen.\nBewusst ohne eigenen Rahmen und ohne Fläche, sie zieht in einen vorhandenen\nContainer ein (Angebots-Box, Sticky-Sidebar) — Doku-Sektion\n`docs/index.html:9069` (`gt-seminar-fakten`), Beispielseiten `docs/index.html:\n9076–9082`, `13718–13726`, `16293–16300`.\n\n**Element-Selektor, kein Attribut (ADR-0008-Standardfall).** Ausgezählt: keines\nder 3 `.ep-facts`-Vorkommen in `docs/index.html` sitzt in einem `.layout-grid`\nmit `col-*`, keines wird von einem Grid-/Flex-Elternteil in der Höhe gedehnt\n(der Flex-Fall in Zeile 16290 ist `flex-direction:column`, dort füllen Block-\nElemente die Breite ihres Containers unabhängig von einer zusätzlichen\nWrapper-Ebene — anders als bei `.ep-card`/ADR-0008 Fall 1 geht es dort um Höhe,\ndie durch Block-Layout NICHT automatisch nach unten durchgereicht wird). Das\nTag variiert nicht (immer `<dl>`). Analog zu `cds-faq` (`faq.component.ts`, das\nintern `<details>` in einem `<div class=\"ep-faq\">` rendert) trägt hier die\nINNERE `<dl class=\"ep-facts\">` die CSS-Klasse, nicht der `<cds-facts>`-Host: die\nSemantik einer Definitionsliste hängt am `<dl>`-Tag selbst, ein Custom-Element\nkann dieses Tag nicht annehmen. Da `.ep-facts > div + div` ausschließlich\nNachfahren INNERHALB der eigenen Vorlage anspricht, bricht eine zusätzliche\nHost-Ebene darüber nichts — anders als bei `cds-section` (Fall 2) hängt hier\nkeine von außen gesetzte Fläche oder ein von außen adressierter Nachfahre an\ndieser Tiefe.\n\n**`<dl>`/`<div>`/`<dt>`/`<dd>` exakt wie vom CSS erwartet.** `.ep-facts > div +\ndiv` (css/components.css:1262) setzt Haarlinie und Abstand ab dem ZWEITEN Paar;\nohne das umschließende `<div>` pro Paar griffe der Selektor nicht. Jedes\n`items()`-Element wird deshalb zu genau einem `<div><dt>…</dt><dd>…</dd></div>`.\n\n**`area` färbt `<dt>` über `.t-{area}`, nicht über `data-area`.** Anders als bei\n`cds-feature`/`cds-icon-card` kennt `.ep-facts` selbst kein `[data-area]`; der\nCSS-Kommentar zu dieser Klasse (`css/components.css:1259`) benennt\nstattdessen ausdrücklich „Bereichston der Labels über `.t-XX` im Markup\n(dark-safe), nie als Inline-Farbe“ als sanktionierten Weg. Ausgezählt: alle 3\n`.ep-facts`-Vorkommen in `docs/index.html` setzen `.t-wo` auf JEDEM `<dt>`\nderselben Liste (nie gemischt, nie eine andere Bereichsfarbe) — ein optionaler\n`area`-Input, der `.t-{area}` einheitlich auf alle `<dt>` der Instanz anwendet,\nbildet dieses Muster nach, ohne einen Farbwert zu erfinden (`.t-XX` ist eine\nbestehende, bereits kontrast- und dark-mode-geprüfte Utility, `css/components.css\n:638–642`). Ungesetzt (Default) bleibt `<dt>` ohne zusätzliche Klasse, erbt also\ndie umgebende Textfarbe — das deckt Kontexte ohne Bereichsbindung ab.\n\n**Über die Ticket-API hinaus:** `area` stand nicht in der ursprünglichen\nAPI-Skizze von Ticket 08. Ergänzt, weil ohne ihn KEINES der 3 realen\n`.ep-facts`-Vorkommen originalgetreu nachgebaut werden könnte — anders als bei\n`cds-tier`s `area` (dort deckt die Einschränkung eine tatsächliche CSS-Lücke\nab) fehlt hier keine CSS-Regel, nur ein API-Hook auf eine bereits vollständige\nKlasse. Siehe Ticket-Bericht für die Abwägung.",
7
+ "description": "Facts (`cds-facts`) — Wrapper um `.ep-facts` aus css/components.css\n(css/components.css:1261–1272): die Definitionsliste für die harten Eckdaten\neines Angebots (Termin, Dauer, Ort, Preis), einspaltig mit Haarlinie zwischen\nden Paaren oder als `.is-grid` zweispaltig für Kästen, die neben Inhalt stehen.\nBewusst ohne eigenen Rahmen und ohne Fläche, sie zieht in einen vorhandenen\nContainer ein (Angebots-Box, Sticky-Sidebar) — Doku-Sektion\n`docs/index.html:9069` (`gt-seminar-fakten`), Beispielseiten `docs/index.html:\n9076–9082`, `13718–13726`, `16293–16300`.\n\n**Element-Selektor, kein Attribut (ADR-0008-Standardfall).** Ausgezählt: keines\nder 3 `.ep-facts`-Vorkommen in `docs/index.html` sitzt in einem `.layout-grid`\nmit `col-*`, keines wird von einem Grid-/Flex-Elternteil in der Höhe gedehnt\n(der Flex-Fall in Zeile 16290 ist `flex-direction:column`, dort füllen Block-\nElemente die Breite ihres Containers unabhängig von einer zusätzlichen\nWrapper-Ebene — anders als bei `.ep-card`/ADR-0008 Fall 1 geht es dort um Höhe,\ndie durch Block-Layout NICHT automatisch nach unten durchgereicht wird). Das\nTag variiert nicht (immer `<dl>`). Analog zu `cds-faq` (`faq.component.ts`, das\nintern `<details>` in einem `<div class=\"ep-faq\">` rendert) trägt hier die\nINNERE `<dl class=\"ep-facts\">` die CSS-Klasse, nicht der `<cds-facts>`-Host: die\nSemantik einer Definitionsliste hängt am `<dl>`-Tag selbst, ein Custom-Element\nkann dieses Tag nicht annehmen. Da `.ep-facts > div + div` ausschließlich\nNachfahren INNERHALB der eigenen Vorlage anspricht, bricht eine zusätzliche\nHost-Ebene darüber nichts — anders als bei `cds-section` (Fall 2) hängt hier\nkeine von außen gesetzte Fläche oder ein von außen adressierter Nachfahre an\ndieser Tiefe.\n\n**`<dl>`/`<div>`/`<dt>`/`<dd>` exakt wie vom CSS erwartet.** `.ep-facts > div +\ndiv` (css/components.css:1262) setzt Haarlinie und Abstand ab dem ZWEITEN Paar;\nohne das umschließende `<div>` pro Paar griffe der Selektor nicht. Jedes\n`items()`-Element wird deshalb zu genau einem `<div><dt>…</dt><dd>…</dd></div>`.\n\n**`area` färbt `<dt>` über `.t-{area}`, nicht über `data-area`.** Anders als bei\n`cds-feature`/`cds-icon-card` kennt `.ep-facts` selbst kein `[data-area]`; der\nCSS-Kommentar zu dieser Klasse (`css/components.css:1259`) benennt\nstattdessen ausdrücklich „Bereichston der Labels über `.t-XX` im Markup\n(dark-safe), nie als Inline-Farbe“ als sanktionierten Weg. Ausgezählt: alle 3\n`.ep-facts`-Vorkommen in `docs/index.html` setzen `.t-wo` auf JEDEM `<dt>`\nderselben Liste (nie gemischt, nie eine andere Bereichsfarbe) — ein optionaler\n`area`-Input, der `.t-{area}` einheitlich auf alle `<dt>` der Instanz anwendet,\nbildet dieses Muster nach, ohne einen Farbwert zu erfinden (`.t-XX` ist eine\nbestehende, bereits kontrast- und dark-mode-geprüfte Utility, `css/components.css\n:638–642`). Ungesetzt (Default) bleibt `<dt>` ohne zusätzliche Klasse, erbt also\ndie umgebende Textfarbe — das deckt Kontexte ohne Bereichsbindung ab.\n\n**Über die ursprüngliche API hinaus:** `area` stand nicht in der ursprünglichen\nAPI-Skizze. Ergänzt, weil ohne ihn KEINES der 3 realen\n`.ep-facts`-Vorkommen originalgetreu nachgebaut werden könnte — anders als bei\n`cds-tier`s `area` (dort deckt die Einschränkung eine tatsächliche CSS-Lücke\nab) fehlt hier keine CSS-Regel, nur ein API-Hook auf eine bereits vollständige\nKlasse.",
8
8
  "jsDocTags": {},
9
9
  "argTypes": {
10
10
  "area": {
@@ -4,7 +4,7 @@
4
4
  "id": "seitenmuster-wissensbeitrag-article-header",
5
5
  "name": "ArticleHeaderComponent",
6
6
  "path": "./src/lib/article-header/article-header.stories.ts",
7
- "description": "ArticleHeader (`cds-article-header`) — Wrapper um `.article-header` aus\ncss/components.css (css/components.css:1486–1490, 1508–1519): der zentrierte Kopf\neines Wissensbeitrags (Breadcrumb, optionale Pill, H1, Lead, Meta-Strip). Elftes\nTicket der Seitenbausteine-Serie. Hauptvorlage ist\n`storybook-angular/src/docs/seitenmuster/wissensbeitrag.mdx`, Abschnitt „Article\nHeader“ — dort steht das für dieses Ticket verbindliche Markup samt Begründung,\nnicht nur das Mockup in `docs/index.html`.\n\n**Breadcrumb sitzt im Header — abweichend von der allgemeinen Navigationsregel,\nbewusst der Ticket-Vorlage folgend.** `docs/index.html:2423` dokumentiert für\nUnterseiten allgemein, wörtlich: „Den Breadcrumb nicht in einen zentrierten\n`.article-header` einbetten, sonst wird er mittig ausgerichtet und bekommt einen\nabweichenden Abstand zur Nav“ (Tabellenzeile „Nicht“ der Breadcrumb-Doku) — und\nausgezählt\nsind es genau die beiden EINZIGEN vollständigen Beispielseiten mit\n`.article-header` (Stellenanzeige `docs/index.html:11035`, Wissensbeitrag · KI\n`docs/index.html:15020`), die dieser Regel folgen: beide setzen den Breadcrumb in\neine EIGENE, linksbündige `.ep-section` VOR dem Header (`docs/index.html:\n11023–11031` bzw. `15010–15015`). Dem stehen 6 ISOLIERTE Doku-Demos gegenüber, die\nden Breadcrumb INNERHALB von `.article-header` zeigen: 5 kleine Referenz-Boxen in\nder Doku-Sektion selbst (`docs/index.html:7850`, `8304`, `8331`, `8361`, `8392` —\nohne Topnav/Hero/Footer, reine Bauteil-Illustrationen) plus das Code-Beispiel in\n`wissensbeitrag.mdx`. Das Ticket benennt genau diese Doku (nicht die\nBeispielseiten) als Hauptvorlage. Diese Komponente folgt der Ticket-Vorgabe\n(Breadcrumb als Input, im Header) und dokumentiert den Widerspruch als eigenen\nBefund\n(`.scratch/angular-seitenbausteine/issues/20-breadcrumb-in-oder-vor-article-header.md`).\nKonsumenten, die der allgemeinen Regel folgen wollen (wie die beiden echten\nBeispielseiten), lassen `breadcrumb` leer und setzen die Leiste selbst davor\n(siehe Story „Ohne Breadcrumb“).\n\n**Letzter Breadcrumb-Eintrag immer ohne Link, mit `aria-current=\"page\"`** — so vom\nTicket als Akzeptanzkriterium gefordert. Die beiden Wissensbeitrag-Inline-Beispiele\n(`docs/index.html:7850–7856`, `wissensbeitrag.mdx`) zeigen dafür nur 2 Einträge,\nBEIDE als `<a>`, ohne `aria-current` — eine zur Kürze vereinfachte Doku-Skizze,\nkein vollständiges Barrierefreiheits-Beispiel. Das volle, korrekte Muster (Trenner\ndekorativ, letzter Eintrag `<span aria-current=\"page\">` ohne Link) steht an\nanderer Stelle exakt so vor (`docs/index.html:2406–2412` allgemeine\nBreadcrumb-Doku, `docs/index.html:11023–11030` Stellenanzeige) und ist zusätzlich\neine explizite Akzeptanzbedingung des Tickets — die Komponente folgt deshalb\ndiesem Muster für den letzten Array-Eintrag, unabhängig von einem dort eventuell\ngesetzten `href`.\n\n**Keine bereichsgefärbte Breadcrumb-Verlinkung (`.t-{area}`).** Die\nInline-Beispiele färben ihren zweiten (dort letzten) Eintrag mit `.t-ki`/`.t-es`/\n`.t-wo`; das Ticket sieht dafür aber kein API-Feld vor (`{label, href?}`, kein\n`area` pro Eintrag), und eine Zuordnung „Bereichsfarbe auf den vorletzten\nEintrag“ wäre geraten, nicht aus der Doku ableitbar — zumal der gefärbte Eintrag\ndort ein Link ist, kein `aria-current`-Element, also nicht deckungsgleich mit dem\noben umgesetzten Muster. Bleibt bewusst außen vor.\n\n**Pill über `cds-pill`, kein direkt komponiertes `<span class=\"pill\">`.** Geprüft\nper `npx storybook tools docs show --id komponenten-chips-badges-pills-pill`:\n`PillComponent`s Host ist `<cds-pill>`, `.pill` sitzt eine Ebene darunter auf\neinem inneren `<span>` (anders als bei `FeaturedCardComponent`, wo genau das wegen\n`.card-featured-body>.pill`-Kindselektoren nicht ginge). `.article-header`/`.pill`\nhaben keinen solchen Kindselektor (css/components.css:1486–1507 geprüft) — die\nzusätzliche Ebene ist hier folgenlos, deshalb bleibt `cds-pill` als echte\nKomponente eingesetzt, keine Klassen-Duplikation nötig.\n\n**Pille rendert nur, wenn `area` UND `pill` gesetzt sind — kein erfundener\nDefault.** `PillComponent.area` ist als `CdsArea` mit einem eigenen Default\n(`'ki'`) typisiert; diese Komponente führt `area` dagegen als `CdsArea | undefined`\n(keine Konfigurations-Vorgabe laut Ticket-API). Ein erster Entwurf reichte\n`area()` per `as CdsArea`-Cast durch, damit ein `undefined`-Binding\n`cds-pill`s eigenen Default überschreiben kann — eine Typ-Lüge, um einen Fall\nabzudecken, der beim Nachzählen gar nicht vorkommt: **alle 9** `.pill`-Vorkommen\nim Article-Header-Kontext (`docs/index.html:7856, 7912–7914, 8310, 8337, 8367,\n8398, 15021`) tragen ein `data-area`, keines ohne Bereich. Die Pille rendert\ndeshalb nur innerhalb von `@if (area(); as pillArea)` (narrowt `CdsArea |\nundefined` sauber auf `CdsArea`, ohne Cast) — ist `pill()` gesetzt, `area()`\naber nicht, bleibt die Pille schlicht weg, statt eine geratene Bereichsfarbe zu\nzeigen. Gepinnt in der Story „Pille ohne Bereich“.\n\n**`pillAriaLabel`, wie bei `FeaturedCardComponent`.** `PillComponent`s eigener\nDefault („Bereich `<label>`“) passt nur, wenn die Pille einen Bereichsnamen trägt\n(Stellenanzeige, `docs/index.html:11036`: „Bereich Effektive Software“). Im\nWissensbeitrag trägt die Pille dagegen die Lesezeit (`wissensbeitrag.mdx`,\nAbschnitt „Pill · Lesezeit“: `aria-label=\"Lesezeit 8 Minuten\"`) — ein Text, den\ndie Komponente nicht zuverlässig aus einem freien `pill`-String wie\n„8 min Lesezeit“ herleiten kann (keine Zahl zu parsen, keine Garantie, dass der\nText überhaupt eine Lesezeit ist). `pillAriaLabel` bleibt deshalb optional, ohne\neigenen Default: ungesetzt geht `undefined` direkt an `cds-pill`s `ariaLabel`\ndurch, das dann auf seinen eigenen Default („Bereich `<pill>`“) zurückfällt —\nkeine Logik-Duplikation nötig, weil hier die echte `PillComponent` verwendet\nwird, keine Klassen-Nachbildung.\n\n**`date`/`dateLabel` getrennt, ANDERS als `cds-stoerer`.** `cds-stoerer`\nformatiert ein reines ISO-Datum selbst nach `de-DE`\n(`stoerer.component.ts:formattedDate`, inkl. manuellem Aufbau der\n`Date`-Komponenten gegen den UTC-Mitternacht-Fallstrick von `new Date(iso)`).\nDiese Komponente tut das NICHT: `date` liefert nur den maschinenlesbaren\n`datetime`-Wert, `dateLabel` die sichtbare Schreibweise — vom Ticket ausdrücklich\nals zwei getrennte Inputs vorgegeben. Begründung der Abweichung: Erstens deckt\nsich das mit jeder realen Instanz (6 von 6 `.article-meta-date`-Vorkommen tragen\neinen redaktionell gesetzten Text wie „13. Mai 2026“, keinen von\n`Intl.DateTimeFormat` erzeugten) — anders als der Störer, dessen Kacheln NUR ein\nISO-Datum ohne separaten Text führen. Zweitens umgeht die Trennung den\nZeitzonen-Fallstrick vollständig, statt ihn zu beheben: ohne eigene Formatierung\nkonstruiert diese Komponente nie ein `Date`-Objekt aus `date()`, die\nUTC-Mitternacht-Falle kann hier gar nicht erst auftreten. Ist `dateLabel` leer,\nzeigt die Komponente `date()` unverändert als sichtbaren Text (bewusst als\nISO-Form, nicht geraten formatiert) — in der Story „Interaktiv“ als Warnfall extra\nbenannt, damit niemand das versehentlich auf eine Produktionsseite bringt.\n\n**Meta-Strip nur, wenn es etwas zu zeigen gibt.** `authorName`/`date` sind laut\nTicket-API Beiwerk (Default `''`); ausgezählt haben alle 6 realen\n`.article-meta`-Vorkommen sowohl Autor als auch Datum, eine Stellenanzeige\n(`docs/index.html:11035`) nutzt `.article-header` dagegen KOMPLETT OHNE\n`.article-meta` (dort ein `<dl>` aus Job-Fakten statt Autor/Datum) — dieser\nabweichenden Verwendung folgt diese Komponente nicht (sie hat in der Ticket-API\nkeinen Platz), sie belegt aber, dass ein leerer Meta-Strip ein realer Fall ist.\n`.article-meta` (und darin `.article-meta-author`/`.article-meta-sep`) rendern\ndeshalb nur, wenn `authorName()` bzw. `date()` tatsächlich etwas liefern; der\nTrenner nur, wenn beide Seiten vorhanden sind.\n\n**Avatar projiziert über `[cdsAvatar]`, PLUS `cds-avatar-stack`.** Die\nTicket-Skizze nennt nur `<ng-content select=\"[cdsAvatar]\">`; ergänzt um den\nTag-Selektor `cds-avatar-stack`, weil `AvatarStackComponent` (siehe dort) ein\nElement-Selektor ist, kein `[cdsAvatar]`-Attribut trägt und sonst nicht in den\nMeta-Strip projiziert werden könnte — nötig für die vom Ticket geforderte Story\n„Mehrere Autor:innen“.\n\n**Host als Block.** `.article-header` sitzt am Host und setzt nur\n`max-width:880px;margin:0 auto`. An einem unbekannten, also `display:inline`\ngerenderten Element greift beides nicht, die Block-Kinder liefen über die\n880 px hinaus. `:host{display:block}` wie bei `cds-hero-image` und\n`cds-footer-main`; Story „Breite begrenzt“ pinnt das.",
7
+ "description": "ArticleHeader (`cds-article-header`) — Wrapper um `.article-header` aus\ncss/components.css (css/components.css:1486–1490, 1508–1519): der zentrierte Kopf\neines Wissensbeitrags (Breadcrumb, optionale Pill, H1, Lead, Meta-Strip).\nHauptvorlage ist\n`storybook-angular/src/docs/seitenmuster/wissensbeitrag.mdx`, Abschnitt „Article\nHeader“ — dort steht das verbindliche Markup samt Begründung,\nnicht nur das Mockup in `docs/index.html`.\n\n**Breadcrumb sitzt im Header — abweichend von der allgemeinen Navigationsregel,\nbewusst der Hauptvorlage folgend.** `docs/index.html:2423` dokumentiert für\nUnterseiten allgemein, wörtlich: „Den Breadcrumb nicht in einen zentrierten\n`.article-header` einbetten, sonst wird er mittig ausgerichtet und bekommt einen\nabweichenden Abstand zur Nav“ (Tabellenzeile „Nicht“ der Breadcrumb-Doku) — und\nausgezählt\nsind es genau die beiden EINZIGEN vollständigen Beispielseiten mit\n`.article-header` (Stellenanzeige `docs/index.html:11035`, Wissensbeitrag · KI\n`docs/index.html:15020`), die dieser Regel folgen: beide setzen den Breadcrumb in\neine EIGENE, linksbündige `.ep-section` VOR dem Header (`docs/index.html:\n11023–11031` bzw. `15010–15015`). Dem stehen 6 ISOLIERTE Doku-Demos gegenüber, die\nden Breadcrumb INNERHALB von `.article-header` zeigen: 5 kleine Referenz-Boxen in\nder Doku-Sektion selbst (`docs/index.html:7850`, `8304`, `8331`, `8361`, `8392` —\nohne Topnav/Hero/Footer, reine Bauteil-Illustrationen) plus das Code-Beispiel in\n`wissensbeitrag.mdx`. Diese Komponente folgt bewusst dieser Doku, nicht den\nBeispielseiten (Breadcrumb als Input, im Header), und dokumentiert den\nWiderspruch als eigenen Befund.\nKonsumenten, die der allgemeinen Regel folgen wollen (wie die beiden echten\nBeispielseiten), lassen `breadcrumb` leer und setzen die Leiste selbst davor\n(siehe Story „Ohne Breadcrumb“).\n\n**Letzter Breadcrumb-Eintrag immer ohne Link, mit `aria-current=\"page\"`.** Die\nbeiden Wissensbeitrag-Inline-Beispiele\n(`docs/index.html:7850–7856`, `wissensbeitrag.mdx`) zeigen dafür nur 2 Einträge,\nBEIDE als `<a>`, ohne `aria-current` — eine zur Kürze vereinfachte Doku-Skizze,\nkein vollständiges Barrierefreiheits-Beispiel. Das volle, korrekte Muster (Trenner\ndekorativ, letzter Eintrag `<span aria-current=\"page\">` ohne Link) steht an\nanderer Stelle exakt so vor (`docs/index.html:2406–2412` allgemeine\nBreadcrumb-Doku, `docs/index.html:11023–11030` Stellenanzeige) — die Komponente\nfolgt deshalb diesem Muster für den letzten Array-Eintrag, unabhängig von einem\ndort eventuell gesetzten `href`.\n\n**Keine bereichsgefärbte Breadcrumb-Verlinkung (`.t-{area}`).** Die\nInline-Beispiele färben ihren zweiten (dort letzten) Eintrag mit `.t-ki`/`.t-es`/\n`.t-wo`; die API sieht dafür aber kein Feld vor (`{label, href?}`, kein\n`area` pro Eintrag), und eine Zuordnung „Bereichsfarbe auf den vorletzten\nEintrag“ wäre geraten, nicht aus der Doku ableitbar — zumal der gefärbte Eintrag\ndort ein Link ist, kein `aria-current`-Element, also nicht deckungsgleich mit dem\noben umgesetzten Muster. Bleibt bewusst außen vor.\n\n**Pill über `cds-pill`, kein direkt komponiertes `<span class=\"pill\">`.** Geprüft\nper `npx storybook tools docs show --id komponenten-chips-badges-pills-pill`:\n`PillComponent`s Host ist `<cds-pill>`, `.pill` sitzt eine Ebene darunter auf\neinem inneren `<span>` (anders als bei `FeaturedCardComponent`, wo genau das wegen\n`.card-featured-body>.pill`-Kindselektoren nicht ginge). `.article-header`/`.pill`\nhaben keinen solchen Kindselektor (css/components.css:1486–1507 geprüft) — die\nzusätzliche Ebene ist hier folgenlos, deshalb bleibt `cds-pill` als echte\nKomponente eingesetzt, keine Klassen-Duplikation nötig.\n\n**Pille rendert nur, wenn `area` UND `pill` gesetzt sind — kein erfundener\nDefault.** `PillComponent.area` ist als `CdsArea` mit einem eigenen Default\n(`'ki'`) typisiert; diese Komponente führt `area` dagegen als `CdsArea | undefined`\n(kein eigener Default vorgegeben). Ein erster Entwurf reichte\n`area()` per `as CdsArea`-Cast durch, damit ein `undefined`-Binding\n`cds-pill`s eigenen Default überschreiben kann — eine Typ-Lüge, um einen Fall\nabzudecken, der beim Nachzählen gar nicht vorkommt: **alle 9** `.pill`-Vorkommen\nim Article-Header-Kontext (`docs/index.html:7856, 7912–7914, 8310, 8337, 8367,\n8398, 15021`) tragen ein `data-area`, keines ohne Bereich. Die Pille rendert\ndeshalb nur innerhalb von `@if (area(); as pillArea)` (narrowt `CdsArea |\nundefined` sauber auf `CdsArea`, ohne Cast) — ist `pill()` gesetzt, `area()`\naber nicht, bleibt die Pille schlicht weg, statt eine geratene Bereichsfarbe zu\nzeigen. Gepinnt in der Story „Pille ohne Bereich“.\n\n**`pillAriaLabel`, wie bei `FeaturedCardComponent`.** `PillComponent`s eigener\nDefault („Bereich `<label>`“) passt nur, wenn die Pille einen Bereichsnamen trägt\n(Stellenanzeige, `docs/index.html:11036`: „Bereich Effektive Software“). Im\nWissensbeitrag trägt die Pille dagegen die Lesezeit (`wissensbeitrag.mdx`,\nAbschnitt „Pill · Lesezeit“: `aria-label=\"Lesezeit 8 Minuten\"`) — ein Text, den\ndie Komponente nicht zuverlässig aus einem freien `pill`-String wie\n„8 min Lesezeit“ herleiten kann (keine Zahl zu parsen, keine Garantie, dass der\nText überhaupt eine Lesezeit ist). `pillAriaLabel` bleibt deshalb optional, ohne\neigenen Default: ungesetzt geht `undefined` direkt an `cds-pill`s `ariaLabel`\ndurch, das dann auf seinen eigenen Default („Bereich `<pill>`“) zurückfällt —\nkeine Logik-Duplikation nötig, weil hier die echte `PillComponent` verwendet\nwird, keine Klassen-Nachbildung.\n\n**`date`/`dateLabel` getrennt, ANDERS als `cds-stoerer`.** `cds-stoerer`\nformatiert ein reines ISO-Datum selbst nach `de-DE`\n(`stoerer.component.ts:formattedDate`, inkl. manuellem Aufbau der\n`Date`-Komponenten gegen den UTC-Mitternacht-Fallstrick von `new Date(iso)`).\nDiese Komponente tut das NICHT: `date` liefert nur den maschinenlesbaren\n`datetime`-Wert, `dateLabel` die sichtbare Schreibweise — bewusst als zwei\ngetrennte Inputs. Begründung der Abweichung: Erstens deckt\nsich das mit jeder realen Instanz (6 von 6 `.article-meta-date`-Vorkommen tragen\neinen redaktionell gesetzten Text wie „13. Mai 2026“, keinen von\n`Intl.DateTimeFormat` erzeugten) — anders als der Störer, dessen Kacheln NUR ein\nISO-Datum ohne separaten Text führen. Zweitens umgeht die Trennung den\nZeitzonen-Fallstrick vollständig, statt ihn zu beheben: ohne eigene Formatierung\nkonstruiert diese Komponente nie ein `Date`-Objekt aus `date()`, die\nUTC-Mitternacht-Falle kann hier gar nicht erst auftreten. Ist `dateLabel` leer,\nzeigt die Komponente `date()` unverändert als sichtbaren Text (bewusst als\nISO-Form, nicht geraten formatiert) — in der Story „Interaktiv“ als Warnfall extra\nbenannt, damit niemand das versehentlich auf eine Produktionsseite bringt.\n\n**Meta-Strip nur, wenn es etwas zu zeigen gibt.** `authorName`/`date` sind bewusst\nBeiwerk (Default `''`); ausgezählt haben alle 6 realen\n`.article-meta`-Vorkommen sowohl Autor als auch Datum, eine Stellenanzeige\n(`docs/index.html:11035`) nutzt `.article-header` dagegen KOMPLETT OHNE\n`.article-meta` (dort ein `<dl>` aus Job-Fakten statt Autor/Datum) — dieser\nabweichenden Verwendung folgt diese Komponente nicht (dafür ist in der API kein\nFeld vorgesehen), sie belegt aber, dass ein leerer Meta-Strip ein realer Fall ist.\n`.article-meta` (und darin `.article-meta-author`/`.article-meta-sep`) rendern\ndeshalb nur, wenn `authorName()` bzw. `date()` tatsächlich etwas liefern; der\nTrenner nur, wenn beide Seiten vorhanden sind.\n\n**Avatar projiziert über `[cdsAvatar]`, PLUS `cds-avatar-stack`.** Der\nursprüngliche Entwurf nennt nur `<ng-content select=\"[cdsAvatar]\">`; ergänzt um den\nTag-Selektor `cds-avatar-stack`, weil `AvatarStackComponent` (siehe dort) ein\nElement-Selektor ist, kein `[cdsAvatar]`-Attribut trägt und sonst nicht in den\nMeta-Strip projiziert werden könnte — nötig für die Story\n„Mehrere Autor:innen“.\n\n**Host als Block.** `.article-header` sitzt am Host und setzt nur\n`max-width:880px;margin:0 auto`. An einem unbekannten, also `display:inline`\ngerenderten Element greift beides nicht, die Block-Kinder liefen über die\n880 px hinaus. `:host{display:block}` wie bei `cds-hero-image` und\n`cds-footer-main`; Story „Breite begrenzt“ pinnt das.",
8
8
  "jsDocTags": {},
9
9
  "argTypes": {
10
10
  "area": {
@@ -4,7 +4,7 @@
4
4
  "id": "seitenmuster-wissensbeitrag-author-card",
5
5
  "name": "AuthorCardComponent",
6
6
  "path": "./src/lib/author-card/author-card.stories.ts",
7
- "description": "AuthorCard (`div[cdsAuthorCard]`) — Wrapper um `.author-card` aus css/components.css\n(css/components.css:1602–1606): der Avatar-plus-Bio-Strip am Ende eines\nWissensbeitrags, einzeln oder mehrfach in `cds-author-card-group`. Dreizehntes und\nletztes Ticket der Seitenbausteine-Serie. Ausgezählt: 17 reale Vorkommen in\n`docs/index.html` — 4 einzelstehend (2 Doku-Demos `docs/index.html:8193,8207`, 1\nArtikel-Demo `8424`, 1 reale Beispielseite `15179`) und 13 in fünf Gruppen (siehe\n`AuthorCardGroupComponent`).\n\n**Attributselektor (ADR-0008), gemessen im `is-grid`-Fall statt nur hergeleitet.**\n`.author-card-group.is-grid` (css/components.css:1555) ist `display:grid` ohne\neigenes `align-items` — der CSS-Default `stretch` gleicht die Kartenhöhen einer\nReihe deshalb NUR aus, wenn `.author-card` selbst das Grid-Kind ist. Spike-Story\n(zwei Karten, 600-px-Raster, kurze gegen deutlich längere Bio, seither wieder\ngelöscht) hat beide Varianten nebeneinander gebaut und mit `getBoundingClientRect()`\ngemessen:\n\n| Variante | Grid-Kind (Tag) | `.author-card`-Höhen | Bio-Rect (kurz / lang) |\n|---|---|---|---|\n| Element-Selektor `<cds-author-card-naive>` | `CDS-AUTHOR-CARD-NAIVE`, 204/204 px (gestreckt) | **84 / 204 px** (ungleich) | 24×73 / 144×200 px |\n| Attributselektor `div[cdsAuthorCardSpike]` | `DIV.author-card`, 204/204 px | **204 / 204 px** (gleich) | — |\n\nBeim Element-Selektor wird der UNSICHTBARE `<cds-author-card-naive>`-Host auf 204 px\ngestreckt, die sichtbare `.author-card`-Box eine Ebene darunter bleibt bei ihrer\nInhaltshöhe (84 px) — exakt der Fehler aus ADR-0008 Fall 1. Mit dem Attributselektor\nIST `.author-card` das Grid-Kind, `align-items:stretch` trifft direkt zu.\n\n**Ehrlicher Zusatz, anders als bei `cds-icon-card`: die Baseline-Screenshots beider\nSpike-Varianten waren byte-identisch** (`sha1` gleich). `.author-card` trägt (anders\nals `.ep-card`) weder Hintergrund noch Rahmen noch Schatten — der zusätzliche,\nunsichtbare Leerraum unter der kurzen Bio malt in keiner der beiden Varianten ein\nPixel. Der Fehler ist damit heute rein strukturell (falsches Boxmodell, `getBoundingClientRect()`\nauf `.author-card` lügt), nicht optisch sichtbar. Er wird real, sobald `.author-card`\nkünftig einen Rahmen/Hintergrund/Hover-Zustand bekommt, und er verfälscht schon\nheute jede Messung/Positionierung, die sich auf die tatsächliche Kartenhöhe verlässt\n(u. a. die Story „Als Raster“ unten, die genau das pinnt). Das ADR-0008-Kriterium\nist strukturell formuliert („die Stelle im DOM einnehmen, die es ohne Angular\neinnähme“), nicht optisch — der Attributselektor bleibt deshalb die richtige Wahl,\nobwohl der Effekt heute unsichtbar ist.\n\n**`area` ist bewusst NICHT Teil der API — CSS-Befund, nicht erfundene Konfiguration.**\nAlle 17 realen `.author-card`-Vorkommen tragen `data-area`, und sowohl\n`docs/index.html:8185` als auch `wissensbeitrag.mdx:199` behaupten wörtlich, das\nfärbe „nur den Avatar“. Ausgezählt gibt es aber in `css/components.css` (Light UND\nDark) KEINE einzige `.author-card[data-area=\"…\"]`-Regel — weder direkt noch über\neinen Nachfahren-Selektor auf `.article-avatar`, dessen eigene\n`[data-area]`-Färbung (css/components.css:1523–1526) ein Element-Selektor auf dem\nAvatar selbst ist, unabhängig vom Vorfahren. `data-area` auf `.author-card` ist\ndamit, gegen die Doku-Aussage, komplett wirkungslos — die Bereichsfarbe kommt\nausschließlich vom `area`-Input, den der Konsument direkt an das projizierte\n`[cdsAvatar]` bindet (siehe unten). Ein `area`-Input, der hier nur `data-area` ohne\njede CSS-Wirkung setzt, wäre eine erfundene Konfiguration ohne Gegenwert — Befund\ngemeldet, nicht im Wrapper geflickt (ADR-0001):\n`.scratch/angular-seitenbausteine/issues/24-css-luecke-author-card-data-area.md`.\n\n**Avatar projiziert über `[cdsAvatar]`, wie bei `cds-article-header`.** Der\nKonsument setzt Größe (`size=\"lg\"`, css/components.css:1527) und Bereich\n(`area=\"…\"`) direkt am projizierten `<div cdsAvatar>` — diese Komponente reicht\nnichts durch, exakt die Trennung, die `cds-article-header` bereits etabliert\n(siehe dessen Klassendoku, Abschnitt „Avatar projiziert über `[cdsAvatar]`“).\n\n**Eyebrow als `<h3>`, nur wenn gesetzt** (Beiwerk, Default `''`) — deckungsgleich\nmit allen 4 einzelstehenden realen Vorkommen: 2 tragen `<h3 class=\"author-card-eyebrow\">`\n(`docs/index.html:8196,8427`, „Über den Autor“/„Über die Autorin“, Ticket-Vorgabe:\neine Ebene unter dem `<h2>` des Article-Body), die anderen 2 (Doku-Demos ohne\numgebende Artikelstruktur) haben gar keinen. Innerhalb einer Gruppe trägt laut\n`wissensbeitrag.mdx` („Eyebrow-Konvention“) keine einzelne Karte mehr einen eigenen\nEyebrow — die Gruppen-Überschrift ersetzt sie (siehe `AuthorCardGroupComponent`);\nKonsumenten lassen `eyebrow` dafür einfach leer.\n\n**`roleLabel`, NICHT `role` wie im Ticket-Text — gemessene Abweichung, kein\nFreihand-Entscheid.** Ein erster Entwurf folgte der Ticket-API wörtlich\n(`role`-Input, mit `role=\"…\"` als schlichtem Attribut in den Stories gesetzt, wie\n`name`/`bio`/`eyebrow`). Storybooks eingebauter a11y-Check (`aria-roles`) schlug\ndarauf für JEDE Story mit echtem Rollentext fehl: `role` ist ein globales\nHTML-/ARIA-Attribut, und ein Angular-Input MIT DIESEM NAMEN wird bei einem\nungebundenen `role=\"Senior AI Engineer …\"` zwar als Startwert an den Input\ndurchgereicht, bleibt aber ZUSÄTZLICH als echtes `role`-Attribut im DOM stehen —\naxe meldete wörtlich „Roles must be one of the valid ARIA roles: Senior, AI,\nEngineer, …“, weil der Freitext als ARIA-Rolle interpretiert wird. Exakt dieselbe\nKollision umgehen `BlockquoteComponent`, `TestimonialComponent` und\n`TeamVoiceComponent` bereits mit `roleLabel` statt `role`\n(`grep -rn \"readonly role\" angular-lib/projects/design-system-angular/src/lib/`\nzeigt nur diese drei). Diese Komponente folgt demselben, bereits etablierten\nMuster statt die Kollision ein viertes Mal zu wiederholen — CSS-Klasse bleibt\nunverändert `.author-card-role`, nur der Input-Name weicht vom Ticket-Text ab.",
7
+ "description": "AuthorCard (`div[cdsAuthorCard]`) — Wrapper um `.author-card` aus css/components.css\n(css/components.css:1602–1606): der Avatar-plus-Bio-Strip am Ende eines\nWissensbeitrags, einzeln oder mehrfach in `cds-author-card-group`. Ausgezählt: 17 reale Vorkommen in\n`docs/index.html` — 4 einzelstehend (2 Doku-Demos `docs/index.html:8193,8207`, 1\nArtikel-Demo `8424`, 1 reale Beispielseite `15179`) und 13 in fünf Gruppen (siehe\n`AuthorCardGroupComponent`).\n\n**Attributselektor (ADR-0008), gemessen im `is-grid`-Fall statt nur hergeleitet.**\n`.author-card-group.is-grid` (css/components.css:1555) ist `display:grid` ohne\neigenes `align-items` — der CSS-Default `stretch` gleicht die Kartenhöhen einer\nReihe deshalb NUR aus, wenn `.author-card` selbst das Grid-Kind ist. Spike-Story\n(zwei Karten, 600-px-Raster, kurze gegen deutlich längere Bio, seither wieder\ngelöscht) hat beide Varianten nebeneinander gebaut und mit `getBoundingClientRect()`\ngemessen:\n\n| Variante | Grid-Kind (Tag) | `.author-card`-Höhen | Bio-Rect (kurz / lang) |\n|---|---|---|---|\n| Element-Selektor `<cds-author-card-naive>` | `CDS-AUTHOR-CARD-NAIVE`, 204/204 px (gestreckt) | **84 / 204 px** (ungleich) | 24×73 / 144×200 px |\n| Attributselektor `div[cdsAuthorCardSpike]` | `DIV.author-card`, 204/204 px | **204 / 204 px** (gleich) | — |\n\nBeim Element-Selektor wird der UNSICHTBARE `<cds-author-card-naive>`-Host auf 204 px\ngestreckt, die sichtbare `.author-card`-Box eine Ebene darunter bleibt bei ihrer\nInhaltshöhe (84 px) — exakt der Fehler aus ADR-0008 Fall 1. Mit dem Attributselektor\nIST `.author-card` das Grid-Kind, `align-items:stretch` trifft direkt zu.\n\n**Ehrlicher Zusatz, anders als bei `cds-icon-card`: die Baseline-Screenshots beider\nSpike-Varianten waren byte-identisch** (`sha1` gleich). `.author-card` trägt (anders\nals `.ep-card`) weder Hintergrund noch Rahmen noch Schatten — der zusätzliche,\nunsichtbare Leerraum unter der kurzen Bio malt in keiner der beiden Varianten ein\nPixel. Der Fehler ist damit heute rein strukturell (falsches Boxmodell, `getBoundingClientRect()`\nauf `.author-card` lügt), nicht optisch sichtbar. Er wird real, sobald `.author-card`\nkünftig einen Rahmen/Hintergrund/Hover-Zustand bekommt, und er verfälscht schon\nheute jede Messung/Positionierung, die sich auf die tatsächliche Kartenhöhe verlässt\n(u. a. die Story „Als Raster“ unten, die genau das pinnt). Das ADR-0008-Kriterium\nist strukturell formuliert („die Stelle im DOM einnehmen, die es ohne Angular\neinnähme“), nicht optisch — der Attributselektor bleibt deshalb die richtige Wahl,\nobwohl der Effekt heute unsichtbar ist.\n\n**`area` ist bewusst NICHT Teil der API — CSS-Befund, nicht erfundene Konfiguration.**\nAlle 17 realen `.author-card`-Vorkommen tragen `data-area`, und sowohl\n`docs/index.html:8185` als auch `wissensbeitrag.mdx:199` behaupten wörtlich, das\nfärbe „nur den Avatar“. Ausgezählt gibt es aber in `css/components.css` (Light UND\nDark) KEINE einzige `.author-card[data-area=\"…\"]`-Regel — weder direkt noch über\neinen Nachfahren-Selektor auf `.article-avatar`, dessen eigene\n`[data-area]`-Färbung (css/components.css:1523–1526) ein Element-Selektor auf dem\nAvatar selbst ist, unabhängig vom Vorfahren. `data-area` auf `.author-card` ist\ndamit, gegen die Doku-Aussage, komplett wirkungslos — die Bereichsfarbe kommt\nausschließlich vom `area`-Input, den der Konsument direkt an das projizierte\n`[cdsAvatar]` bindet (siehe unten). Ein `area`-Input, der hier nur `data-area` ohne\njede CSS-Wirkung setzt, wäre eine erfundene Konfiguration ohne Gegenwert — Befund\ngemeldet, nicht im Wrapper geflickt (ADR-0001).\n\n**Avatar projiziert über `[cdsAvatar]`, wie bei `cds-article-header`.** Der\nKonsument setzt Größe (`size=\"lg\"`, css/components.css:1527) und Bereich\n(`area=\"…\"`) direkt am projizierten `<div cdsAvatar>` — diese Komponente reicht\nnichts durch, exakt die Trennung, die `cds-article-header` bereits etabliert\n(siehe dessen Klassendoku, Abschnitt „Avatar projiziert über `[cdsAvatar]`“).\n\n**Eyebrow als `<h3>`, nur wenn gesetzt** (Beiwerk, Default `''`) — deckungsgleich\nmit allen 4 einzelstehenden realen Vorkommen: 2 tragen `<h3 class=\"author-card-eyebrow\">`\n(`docs/index.html:8196,8427`, „Über den Autor“/„Über die Autorin“:\neine Ebene unter dem `<h2>` des Article-Body), die anderen 2 (Doku-Demos ohne\numgebende Artikelstruktur) haben gar keinen. Innerhalb einer Gruppe trägt laut\n`wissensbeitrag.mdx` („Eyebrow-Konvention“) keine einzelne Karte mehr einen eigenen\nEyebrow — die Gruppen-Überschrift ersetzt sie (siehe `AuthorCardGroupComponent`);\nKonsumenten lassen `eyebrow` dafür einfach leer.\n\n**`roleLabel`, NICHT `role` wie in einem ersten Entwurf — gemessene Abweichung, kein\nFreihand-Entscheid.** Der erste Entwurf setzte `role` wörtlich um\n(`role`-Input, mit `role=\"…\"` als schlichtem Attribut in den Stories gesetzt, wie\n`name`/`bio`/`eyebrow`). Storybooks eingebauter a11y-Check (`aria-roles`) schlug\ndarauf für JEDE Story mit echtem Rollentext fehl: `role` ist ein globales\nHTML-/ARIA-Attribut, und ein Angular-Input MIT DIESEM NAMEN wird bei einem\nungebundenen `role=\"Senior AI Engineer …\"` zwar als Startwert an den Input\ndurchgereicht, bleibt aber ZUSÄTZLICH als echtes `role`-Attribut im DOM stehen —\naxe meldete wörtlich „Roles must be one of the valid ARIA roles: Senior, AI,\nEngineer, …“, weil der Freitext als ARIA-Rolle interpretiert wird. Exakt dieselbe\nKollision umgehen `BlockquoteComponent`, `TestimonialComponent` und\n`TeamVoiceComponent` bereits mit `roleLabel` statt `role`\n(`grep -rn \"readonly role\" angular-lib/projects/design-system-angular/src/lib/`\nzeigt nur diese drei). Diese Komponente folgt demselben, bereits etablierten\nMuster statt die Kollision ein viertes Mal zu wiederholen — CSS-Klasse bleibt\nunverändert `.author-card-role`, nur der Input-Name weicht vom ursprünglichen Entwurf ab.",
8
8
  "jsDocTags": {},
9
9
  "argTypes": {
10
10
  "bio": {
@@ -4,7 +4,7 @@
4
4
  "id": "seitenmuster-wissensbeitrag-avatar",
5
5
  "name": "AvatarComponent",
6
6
  "path": "./src/lib/avatar/avatar.stories.ts",
7
- "description": "Avatar (`div[cdsAvatar]`) — Wrapper um `.article-avatar` aus css/components.css\n(css/components.css:1520–1536): der Initialen-/Foto-Kreis im Meta-Strip eines\nWissensbeitrags (`.article-meta-author`), in der Author-Card (`-lg`) und im\nTeam-Tile-Grid (`-xl`). Elftes Ticket der Seitenbausteine-Serie.\n\n**Attributselektor (ADR-0008), gemessen an der Geschwister-Kette.** Ausgezählt in\n`docs/index.html`: 8 der 11 Instanzen der Grundgröße sitzen als direkte, adjazente\nGeschwister in einer von 3 `.article-avatar-stack`-Kacheln (2 Avatare in Zeilen\n8343–8344, je 3 in Zeilen 8373–8375 und 8404–8406) — genau dort greift\n`.article-avatar-stack .article-avatar + .article-avatar`\n(css/components.css:1540–1541), eine Geschwister-Kette (Kriterium `spec.md` 2b).\nEin Element-Selektor `cds-avatar`, der `.article-avatar` auf einem INNEREN Element\nrendert (wie `cds-facts` es mit `<dl>` tut, weil `<dl>` ein Custom Element nicht\nannehmen kann), würde diese Kette brechen: die beiden `.article-avatar`-Knoten\nlägen dann je eine Ebene UNTER ihrem eigenen `<cds-avatar>`-Host und wären keine\nGeschwister mehr — `+` verlangt denselben Elternknoten, egal wie dicht die Hosts\nselbst nebeneinanderstehen. Eine zweite, ebenfalls tragfähige Variante\n(Element-Selektor MIT `host: { class: 'article-avatar' }`, ohne innere\nWrapper-Ebene) hätte dieselbe Geschwister-Kette ebenso erfüllt — verworfen zugunsten\ndes Attributselektors nur wegen des zweiten, unabhängigen Grundes unten, nicht weil\nsie die Kette nicht gelöst hätte.\n\n**Gemessen, nicht nur hergeleitet.** Story „Avatar-Stapel“ (Play-Funktion) rendert\ndrei `div[cdsAvatar]` in einem `cds-avatar-stack` und misst\n`getBoundingClientRect()` der drei `.article-avatar`-Knoten: bei 40 px Kachelbreite\nund `margin-left:-12px` (css/components.css:1541) überlappen der zweite und dritte\nKreis den jeweils vorherigen um 12 px (Differenz der `left`-Koordinaten = 28 px,\nnicht 40 px) — plus Screenshot-Baseline (`visual-snapshots/…avatar-stapel.png`) als\nvisuelle Bestätigung, nicht nur die Zahl.\n\n**Zweiter, unabhängiger Grund: Wiederverwendung als Projektions-Selektor.**\n`cds-article-header` projiziert den Avatar über\n`<ng-content select=\"[cdsAvatar]\">` (siehe dessen Klassendoku) — dasselbe Attribut,\ndas hier den Host selektiert, dient dem Elternteil zugleich als\nContent-Projection-Selektor. Ein Element-Selektor bräuchte dafür eine zusätzliche,\neigens erfundene Marker-Klasse; der Attributselektor liefert die Zielscheibe für\nbeide Zwecke aus einer Hand, ohne dass der Konsument zwei Attribute schreiben muss.\n\n**Initialen werden aus `name` abgeleitet, kein eigener Input** (Ticket-Vorgabe):\nerstes Zeichen des ersten und des letzten durch Whitespace getrennten Worts, groß\n— ein einzelnes Wort liefert dessen erste zwei Zeichen. Zwei Inputs für dieselbe\nInformation (Name UND Initialen) könnten auseinanderlaufen, sobald nur eines\ngepflegt wird.\n\n**`aria-hidden=\"true\"` fest verdrahtet, kein Input.** Ausgezählt: von 53\n`.article-avatar`-Instanzen (11 Grundgröße + 17 `-lg` + 25 `-xl`) tragen 45 das\nAttribut direkt am Element; die restlichen 8 sind genau die Grundgrößen-Avatare\nINNERHALB von `.article-avatar-stack`, wo bereits der Stack-Container selbst\n`aria-hidden=\"true\"` trägt (siehe `AvatarStackComponent`) — dort wäre ein zweites\n`aria-hidden` am Kind redundant, aber unschädlich: verschachteltes `aria-hidden`\nändert nichts am Ergebnis, der Teilbaum ist so oder so für Assistenztechnologie\nunsichtbar. Der Avatar trägt es deshalb immer: 100 % Deckung mit dem Mockup im\nAlleinstand, harmlose Redundanz im Stapel.",
7
+ "description": "Avatar (`div[cdsAvatar]`) — Wrapper um `.article-avatar` aus css/components.css\n(css/components.css:1520–1536): der Initialen-/Foto-Kreis im Meta-Strip eines\nWissensbeitrags (`.article-meta-author`), in der Author-Card (`-lg`) und im\nTeam-Tile-Grid (`-xl`).\n\n**Attributselektor (ADR-0008), gemessen an der Geschwister-Kette.** Ausgezählt in\n`docs/index.html`: 8 der 11 Instanzen der Grundgröße sitzen als direkte, adjazente\nGeschwister in einer von 3 `.article-avatar-stack`-Kacheln (2 Avatare in Zeilen\n8343–8344, je 3 in Zeilen 8373–8375 und 8404–8406) — genau dort greift\n`.article-avatar-stack .article-avatar + .article-avatar`\n(css/components.css:1540–1541), eine Geschwister-Kette.\nEin Element-Selektor `cds-avatar`, der `.article-avatar` auf einem INNEREN Element\nrendert (wie `cds-facts` es mit `<dl>` tut, weil `<dl>` ein Custom Element nicht\nannehmen kann), würde diese Kette brechen: die beiden `.article-avatar`-Knoten\nlägen dann je eine Ebene UNTER ihrem eigenen `<cds-avatar>`-Host und wären keine\nGeschwister mehr — `+` verlangt denselben Elternknoten, egal wie dicht die Hosts\nselbst nebeneinanderstehen. Eine zweite, ebenfalls tragfähige Variante\n(Element-Selektor MIT `host: { class: 'article-avatar' }`, ohne innere\nWrapper-Ebene) hätte dieselbe Geschwister-Kette ebenso erfüllt — verworfen zugunsten\ndes Attributselektors nur wegen des zweiten, unabhängigen Grundes unten, nicht weil\nsie die Kette nicht gelöst hätte.\n\n**Gemessen, nicht nur hergeleitet.** Story „Avatar-Stapel“ (Play-Funktion) rendert\ndrei `div[cdsAvatar]` in einem `cds-avatar-stack` und misst\n`getBoundingClientRect()` der drei `.article-avatar`-Knoten: bei 40 px Kachelbreite\nund `margin-left:-12px` (css/components.css:1541) überlappen der zweite und dritte\nKreis den jeweils vorherigen um 12 px (Differenz der `left`-Koordinaten = 28 px,\nnicht 40 px) — plus Screenshot-Baseline (`visual-snapshots/…avatar-stapel.png`) als\nvisuelle Bestätigung, nicht nur die Zahl.\n\n**Zweiter, unabhängiger Grund: Wiederverwendung als Projektions-Selektor.**\n`cds-article-header` projiziert den Avatar über\n`<ng-content select=\"[cdsAvatar]\">` (siehe dessen Klassendoku) — dasselbe Attribut,\ndas hier den Host selektiert, dient dem Elternteil zugleich als\nContent-Projection-Selektor. Ein Element-Selektor bräuchte dafür eine zusätzliche,\neigens erfundene Marker-Klasse; der Attributselektor liefert die Zielscheibe für\nbeide Zwecke aus einer Hand, ohne dass der Konsument zwei Attribute schreiben muss.\n\n**Initialen werden aus `name` abgeleitet, kein eigener Input:**\nerstes Zeichen des ersten und des letzten durch Whitespace getrennten Worts, groß\n— ein einzelnes Wort liefert dessen erste zwei Zeichen. Zwei Inputs für dieselbe\nInformation (Name UND Initialen) könnten auseinanderlaufen, sobald nur eines\ngepflegt wird.\n\n**`aria-hidden=\"true\"` fest verdrahtet, kein Input.** Ausgezählt: von 53\n`.article-avatar`-Instanzen (11 Grundgröße + 17 `-lg` + 25 `-xl`) tragen 45 das\nAttribut direkt am Element; die restlichen 8 sind genau die Grundgrößen-Avatare\nINNERHALB von `.article-avatar-stack`, wo bereits der Stack-Container selbst\n`aria-hidden=\"true\"` trägt (siehe `AvatarStackComponent`) — dort wäre ein zweites\n`aria-hidden` am Kind redundant, aber unschädlich: verschachteltes `aria-hidden`\nändert nichts am Ergebnis, der Teilbaum ist so oder so für Assistenztechnologie\nunsichtbar. Der Avatar trägt es deshalb immer: 100 % Deckung mit dem Mockup im\nAlleinstand, harmlose Redundanz im Stapel.",
8
8
  "jsDocTags": {},
9
9
  "argTypes": {
10
10
  "area": {
@@ -4,7 +4,7 @@
4
4
  "id": "seitenmuster-wissensbeitrag-callout",
5
5
  "name": "ArticleCalloutComponent",
6
6
  "path": "./src/lib/article-callout/article-callout.stories.ts",
7
- "description": "ArticleCallout (`cds-article-callout`) — Wrapper um `.article-callout*` aus\ncss/components.css (css/components.css:1591–1600): der bereichsgetönte\nAside-Block für Praxis-Beispiele und „In der Realität“-Einschübe im Lauftext\neines Wissensbeitrags. Zwölftes Ticket der Seitenbausteine-Serie\n(„Artikel-Körper“). Ausgezählt: 5 reale Vorkommen, je eines pro Bereich in der\nDoku-Sektion (`docs/index.html:8086–8115`, co/ki/es/wo) plus eines in der realen\nBeispielseite Wissensbeitrag · KI (`docs/index.html:15110–15114`).\n\n**Element-Selektor, ADR-0008-Standardfall.** `.article-callout` sitzt in allen 5\nVorkommen als gewöhnlicher Block-Nachfahre — die 4 Doku-Boxen je in einem\nschlichten `<div>` innerhalb eines `display:flex;flex-direction:column`-Stapels\n(kein Stretch-Bedarf in Spaltenrichtung), die reale Instanz direkt in\n`.article-body`. Keines ist ein Grid-/Flex-Kind mit Streckungsbedarf, keines\nträgt eine `col-*`-Klasse, `.article-callout` kommt in keiner CSS-Regel mit\neinem Geschwister-Kombinator (`+`/`~`) vor, das Tag variiert nicht (immer\n`<aside>`). Keines der drei ADR-0008-Kriterien greift. Die einzige direkte\nKind-Regel von `.article-body` (`.article-body > p`, css/components.css:1566)\nzielt auf `p`, nicht auf `aside` — ein `<cds-article-callout>`-Host zwischen\n`.article-body` und dem `<aside>` bricht deshalb keine Selektorkette.\n\n**Projizierte Absätze bleiben direkte Kinder von `.article-callout`, kein\n`<div>` um `<ng-content>`** (Ticket-Vorgabe: `.article-callout > p` ist ein\nKindselektor, css/components.css:1600). `<ng-content>` selbst fügt kein\nDOM-Element ein — es verschiebt nur die vom Konsumenten geschriebenen\n`<p>`-Elemente an ihre Stelle direkt im `<aside>`, eine Ebene unter dem\n`<cds-article-callout>`-Host, aber ohne zusätzlichen Knoten dazwischen. Geprüft\nim laufenden Storybook (Story „Interaktiv“, Play-Funktion):\n`aside.article-callout > p` liefert sowohl die Eyebrow als auch jeden\nprojizierten Absatz, `:scope > *` zählt keine fremde Zwischenebene.\n\n**`eyebrow` ist Beiwerk (Default `''`), `area` ist Konfiguration mit\nverteidigbarer Vorgabe (`'co'`).** Die Basisregel `.article-callout` selbst\n(ohne `[data-area]`) rendert bereits exakt wie `[data-area=\"co\"]`\n(`background:var(--co-50)`, `border-left-color:var(--co-500)`, beide Werte\nidentisch an beiden Stellen, css/components.css:1591–1592) — `'co'` ist damit\nkeine erfundene Markenfarbe, sondern der reale CSS-Fallback ohne Attribut.\nAnders als bei `cds-pill` (dessen `.pill` ohne `data-area` gar keine Füllfarbe\nhat) gibt es hier keinen sinnvollen „kein Bereich“-Zustand.\n\n**Bekannter CSS-Befund, nicht im Wrapper geflickt (ADR-0001):**\n`.article-callout-eyebrow` (Spezifität 0,1,0) verliert gegen\n`.article-callout > p` (Spezifität 0,1,1, css/components.css:1600), weil die\nEyebrow selbst ein `<p>` UND ein direktes Kind von `.article-callout` ist.\nGemessen in rohem Markup ohne Angular (Playwright/Chromium,\n`getComputedStyle`): die Eyebrow rendert in JEDEM Bereich mit `font-size:16px`\nstatt `12px` und `margin-bottom:0` statt `4px`; die Akzentfarbe geht zusätzlich\nverloren, wenn kein bereichsspezifischer Override existiert (`co` und „kein\n`data-area`“ zeigen `--tx-primary` statt `--co-700`). Diese Komponente\nreproduziert exakt die Klassen und die Struktur des Mockups und zeigt deshalb\ndenselben, vorbestehenden Fehler wie rohes HTML — siehe\n`.scratch/angular-seitenbausteine/issues/22-css-luecke-callout-eyebrow-spezifitaet.md`.\n\n**`aria-labelledby` auf `<aside>`, sobald `eyebrow` gesetzt ist — Zusatz zum\nMockup, keine CSS-Änderung.** `<aside>` hat implizit die Landmark-Rolle\n`complementary`; ein Wissensbeitrag mit mehr als einem Callout hat damit\nmehrere gleichnamige Landmarks ohne zugänglichen Namen. Gemessen mit\naxe-core an rohem Markup ohne Angular (zwei `<aside class=\"article-callout\">`\nohne Auszeichnung): `landmark-unique` schlägt fehl, unabhängig von dieser\nKomponente — das Mockup selbst kennt diese Auszeichnung nicht (siehe\n`.scratch/angular-seitenbausteine/issues/23-doku-luecke-callout-landmark-label.md`).\nDie Eyebrow ist bereits ein prägnanter, vom Redakteur gepflegter Kurztext\ngenau für diesen Zweck (z. B. „In der Praxis“) — sie bekommt deshalb eine\ngenerierte `id`, auf die `aria-labelledby` zeigt, sobald sie existiert. Ohne\nEyebrow bleibt das `<aside>` unbenannt wie im Mockup: ein zugänglicher Name\nließe sich sonst nur aus dem projizierten Absatztext raten, und das wäre eine\nerfundene Bezeichnung, keine reale.",
7
+ "description": "ArticleCallout (`cds-article-callout`) — Wrapper um `.article-callout*` aus\ncss/components.css (css/components.css:1591–1600): der bereichsgetönte\nAside-Block für Praxis-Beispiele und „In der Realität“-Einschübe im Lauftext\neines Wissensbeitrags. Ausgezählt: 5 reale Vorkommen, je eines pro Bereich in der\nDoku-Sektion (`docs/index.html:8086–8115`, co/ki/es/wo) plus eines in der realen\nBeispielseite Wissensbeitrag · KI (`docs/index.html:15110–15114`).\n\n**Element-Selektor, ADR-0008-Standardfall.** `.article-callout` sitzt in allen 5\nVorkommen als gewöhnlicher Block-Nachfahre — die 4 Doku-Boxen je in einem\nschlichten `<div>` innerhalb eines `display:flex;flex-direction:column`-Stapels\n(kein Stretch-Bedarf in Spaltenrichtung), die reale Instanz direkt in\n`.article-body`. Keines ist ein Grid-/Flex-Kind mit Streckungsbedarf, keines\nträgt eine `col-*`-Klasse, `.article-callout` kommt in keiner CSS-Regel mit\neinem Geschwister-Kombinator (`+`/`~`) vor, das Tag variiert nicht (immer\n`<aside>`). Keines der drei ADR-0008-Kriterien greift. Die einzige direkte\nKind-Regel von `.article-body` (`.article-body > p`, css/components.css:1566)\nzielt auf `p`, nicht auf `aside` — ein `<cds-article-callout>`-Host zwischen\n`.article-body` und dem `<aside>` bricht deshalb keine Selektorkette.\n\n**Projizierte Absätze bleiben direkte Kinder von `.article-callout`, kein\n`<div>` um `<ng-content>`:** `.article-callout > p` ist ein\nKindselektor, css/components.css:1600. `<ng-content>` selbst fügt kein\nDOM-Element ein — es verschiebt nur die vom Konsumenten geschriebenen\n`<p>`-Elemente an ihre Stelle direkt im `<aside>`, eine Ebene unter dem\n`<cds-article-callout>`-Host, aber ohne zusätzlichen Knoten dazwischen. Geprüft\nim laufenden Storybook (Story „Interaktiv“, Play-Funktion):\n`aside.article-callout > p` liefert sowohl die Eyebrow als auch jeden\nprojizierten Absatz, `:scope > *` zählt keine fremde Zwischenebene.\n\n**`eyebrow` ist Beiwerk (Default `''`), `area` ist Konfiguration mit\nverteidigbarer Vorgabe (`'co'`).** Die Basisregel `.article-callout` selbst\n(ohne `[data-area]`) rendert bereits exakt wie `[data-area=\"co\"]`\n(`background:var(--co-50)`, `border-left-color:var(--co-500)`, beide Werte\nidentisch an beiden Stellen, css/components.css:1591–1592) — `'co'` ist damit\nkeine erfundene Markenfarbe, sondern der reale CSS-Fallback ohne Attribut.\nAnders als bei `cds-pill` (dessen `.pill` ohne `data-area` gar keine Füllfarbe\nhat) gibt es hier keinen sinnvollen „kein Bereich“-Zustand.\n\n**Bekannter CSS-Befund, nicht im Wrapper geflickt (ADR-0001):**\n`.article-callout-eyebrow` (Spezifität 0,1,0) verliert gegen\n`.article-callout > p` (Spezifität 0,1,1, css/components.css:1600), weil die\nEyebrow selbst ein `<p>` UND ein direktes Kind von `.article-callout` ist.\nGemessen in rohem Markup ohne Angular (Playwright/Chromium,\n`getComputedStyle`): die Eyebrow rendert in JEDEM Bereich mit `font-size:16px`\nstatt `12px` und `margin-bottom:0` statt `4px`; die Akzentfarbe geht zusätzlich\nverloren, wenn kein bereichsspezifischer Override existiert (`co` und „kein\n`data-area`“ zeigen `--tx-primary` statt `--co-700`). Diese Komponente\nreproduziert exakt die Klassen und die Struktur des Mockups und zeigt deshalb\ndenselben, vorbestehenden Fehler wie rohes HTML.\n\n**`aria-labelledby` auf `<aside>`, sobald `eyebrow` gesetzt ist — Zusatz zum\nMockup, keine CSS-Änderung.** `<aside>` hat implizit die Landmark-Rolle\n`complementary`; ein Wissensbeitrag mit mehr als einem Callout hat damit\nmehrere gleichnamige Landmarks ohne zugänglichen Namen. Gemessen mit\naxe-core an rohem Markup ohne Angular (zwei `<aside class=\"article-callout\">`\nohne Auszeichnung): `landmark-unique` schlägt fehl, unabhängig von dieser\nKomponente — das Mockup selbst kennt diese Auszeichnung nicht.\nDie Eyebrow ist bereits ein prägnanter, vom Redakteur gepflegter Kurztext\ngenau für diesen Zweck (z. B. „In der Praxis“) — sie bekommt deshalb eine\ngenerierte `id`, auf die `aria-labelledby` zeigt, sobald sie existiert. Ohne\nEyebrow bleibt das `<aside>` unbenannt wie im Mockup: ein zugänglicher Name\nließe sich sonst nur aus dem projizierten Absatztext raten, und das wäre eine\nerfundene Bezeichnung, keine reale.",
8
8
  "jsDocTags": {},
9
9
  "argTypes": {
10
10
  "area": {
@@ -4,7 +4,7 @@
4
4
  "id": "seitenmuster-wissensbeitrag-figure",
5
5
  "name": "ArticleFigureComponent",
6
6
  "path": "./src/lib/article-figure/article-figure.stories.ts",
7
- "description": "ArticleFigure (`cds-article-figure`) — Wrapper um `.article-figure`/\n`.article-figcaption` aus css/components.css (css/components.css:1573–1575):\ndas redaktionelle Inline-Bild mit optionaler Bildunterschrift im Lauftext eines\nWissensbeitrags. Zwölftes Ticket der Seitenbausteine-Serie („Artikel-Körper“).\nAusgezählt: 3 reale Vorkommen — Doku-Demo mit Caption\n(`docs/index.html:8028–8031`), reales Body-Bild mit Caption in der Beispielseite\nWissensbeitrag · KI (`docs/index.html:15115–15118`, identischer Bild- und\nCaption-Text wie die Doku-Demo) und ein dritter, captionsloser Sonderfall direkt\nunter dem Article Header derselben Beispielseite (`docs/index.html:15040–15042`,\ndazu unten mehr).\n\n**Element-Selektor, ADR-0008-Standardfall.** In allen 3 Vorkommen sitzt\n`.article-figure` als gewöhnlicher Block-Nachfahre (zwei in `.article-body`,\neines direkt in einer eigenen `.ep-section`) — kein Grid-/Flex-Kind, keine\n`col-*`-Klasse vom Konsumenten, kein Tag-Wechsel (immer `<figure>`), kein\nVorkommen von `.article-figure` mit einem Geschwister-Kombinator in\ncss/components.css. Die einzige direkte Kind-Regel von `.article-body`\n(`.article-body > p`, css/components.css:1566) zielt auf `p`, nicht auf\n`figure` — ein `<cds-article-figure>`-Host zwischen `.article-body` und dem\n`<figure>` bricht deshalb keine Selektorkette.\n\n**`loading=\"lazy\"` ist fest verdrahtet, kein Input — bewusst NUR der\nIn-Article-Fall, nicht der dritte Mockup-Beleg.** Ausgezählt tragen 2 der 3\nVorkommen `loading=\"lazy\"` (die beiden Bilder MIT Caption, mittig im\nLauftext, „below the fold“ — genau der in `wissensbeitrag.mdx`, Abschnitt\n„In-Article Figure“, dokumentierte Dos-Punkt). Das dritte Vorkommen\n(`docs/index.html:15040–15042`) trägt stattdessen `loading=\"eager\"\nfetchpriority=\"high\"` UND hat keine Caption — es ist das erste Bild direkt\nunter dem Article Header (Kommentar im Mockup: „Hero-Bild auf Body-Breite …\nidentisches Bild wie Vorschaubild für Wiedererkennung“), sitzt außerhalb von\n`.article-body` und bekommt seine 720-px-Breite über ein\nInline-`style=\"max-width:720px;margin:0 auto\"` am Konsumenten, nicht über die\nKomponente (Randbedingung 6 der Spec: Mockup-Inline-Styles wandern nicht in\nden Wrapper). Die Ticket-API sieht dafür kein `loading`/`fetchpriority`-Input\nvor; dieses Bauteil deckt deshalb den (häufigeren, dokumentierten) In-Body-Fall\nab. Für den Lead-Bild-Sonderfall bleibt `<figure class=\"article-figure\">` roh\nschreibbar oder — sofern die Semantik passt — `cds-hero-image` (Ticket 02) zu\nprüfen.\n\n**`alt` und `caption` sind bewusst getrennte Pflicht-/Beiwerk-Inputs, keine\nPrüfung auf inhaltliche Überschneidung.** `wissensbeitrag.mdx` dokumentiert die\nbeiden Rollen ausdrücklich als unterschiedliche Jobs\n(Dont: „Alt-Text und Caption inhaltlich identisch, Screenreader liest\ndoppelt“) — Vorgabe an den Konsumenten, nicht etwas, das eine Wrapper-Komponente\nzur Laufzeit sinnvoll erzwingen könnte.",
7
+ "description": "ArticleFigure (`cds-article-figure`) — Wrapper um `.article-figure`/\n`.article-figcaption` aus css/components.css (css/components.css:1573–1575):\ndas redaktionelle Inline-Bild mit optionaler Bildunterschrift im Lauftext eines\nWissensbeitrags. Ausgezählt: 3 reale Vorkommen — Doku-Demo mit Caption\n(`docs/index.html:8028–8031`), reales Body-Bild mit Caption in der Beispielseite\nWissensbeitrag · KI (`docs/index.html:15115–15118`, identischer Bild- und\nCaption-Text wie die Doku-Demo) und ein dritter, captionsloser Sonderfall direkt\nunter dem Article Header derselben Beispielseite (`docs/index.html:15040–15042`,\ndazu unten mehr).\n\n**Element-Selektor, ADR-0008-Standardfall.** In allen 3 Vorkommen sitzt\n`.article-figure` als gewöhnlicher Block-Nachfahre (zwei in `.article-body`,\neines direkt in einer eigenen `.ep-section`) — kein Grid-/Flex-Kind, keine\n`col-*`-Klasse vom Konsumenten, kein Tag-Wechsel (immer `<figure>`), kein\nVorkommen von `.article-figure` mit einem Geschwister-Kombinator in\ncss/components.css. Die einzige direkte Kind-Regel von `.article-body`\n(`.article-body > p`, css/components.css:1566) zielt auf `p`, nicht auf\n`figure` — ein `<cds-article-figure>`-Host zwischen `.article-body` und dem\n`<figure>` bricht deshalb keine Selektorkette.\n\n**`loading=\"lazy\"` ist fest verdrahtet, kein Input — bewusst NUR der\nIn-Article-Fall, nicht der dritte Mockup-Beleg.** Ausgezählt tragen 2 der 3\nVorkommen `loading=\"lazy\"` (die beiden Bilder MIT Caption, mittig im\nLauftext, „below the fold“ — genau der in `wissensbeitrag.mdx`, Abschnitt\n„In-Article Figure“, dokumentierte Dos-Punkt). Das dritte Vorkommen\n(`docs/index.html:15040–15042`) trägt stattdessen `loading=\"eager\"\nfetchpriority=\"high\"` UND hat keine Caption — es ist das erste Bild direkt\nunter dem Article Header (Kommentar im Mockup: „Hero-Bild auf Body-Breite …\nidentisches Bild wie Vorschaubild für Wiedererkennung“), sitzt außerhalb von\n`.article-body` und bekommt seine 720-px-Breite über ein\nInline-`style=\"max-width:720px;margin:0 auto\"` am Konsumenten, nicht über die\nKomponente (Mockup-Inline-Styles wandern bewusst nicht in den Wrapper). Die\nAPI sieht dafür kein `loading`/`fetchpriority`-Input\nvor; dieses Bauteil deckt deshalb den (häufigeren, dokumentierten) In-Body-Fall\nab. Für den Lead-Bild-Sonderfall bleibt `<figure class=\"article-figure\">` roh\nschreibbar oder — sofern die Semantik passt — `cds-hero-image` zu\nprüfen.\n\n**`alt` und `caption` sind bewusst getrennte Pflicht-/Beiwerk-Inputs, keine\nPrüfung auf inhaltliche Überschneidung.** `wissensbeitrag.mdx` dokumentiert die\nbeiden Rollen ausdrücklich als unterschiedliche Jobs\n(Dont: „Alt-Text und Caption inhaltlich identisch, Screenreader liest\ndoppelt“) — Vorgabe an den Konsumenten, nicht etwas, das eine Wrapper-Komponente\nzur Laufzeit sinnvoll erzwingen könnte.",
8
8
  "jsDocTags": {},
9
9
  "argTypes": {
10
10
  "alt": {
@@ -4,7 +4,7 @@
4
4
  "id": "seitenmuster-wissensbeitrag-inhaltsverzeichnis",
5
5
  "name": "ArticleTocComponent",
6
6
  "path": "./src/lib/article-toc/article-toc.stories.ts",
7
- "description": "ArticleToc (`cds-article-toc`) — Wrapper um `.article-toc*` aus css/components.css\n(css/components.css:1577–1589): das aufklappbare Inhaltsverzeichnis am Anfang eines\nWissensbeitrags. Zwölftes Ticket der Seitenbausteine-Serie („Artikel-Körper“).\nAusgezählt: genau 2 reale Vorkommen, beide mit 5 Einträgen und identischem\nsichtbarem Text „Inhalt“ plus identischem `aria-label`\n(`docs/index.html:7948–7960`, Doku-Sektion mit Platzhalter-Hrefs `#gt-article-toc`;\n`docs/index.html:15049–15061`, reale Beispielseite Wissensbeitrag · KI mit echten\nAbschnittsankern wie `#wb-ki-demo`).\n\n**Element-Selektor, ADR-0008-Standardfall.** `.article-toc` sitzt in beiden\nVorkommen als gewöhnlicher Block-Nachfahre direkt in `.article-body`\n(`docs/index.html:7950`, `15051` je eine Ebene über dem `<details>`) — kein\nGrid-/Flex-Kind, keine Layout-Klasse (`col-*`) vom Konsumenten, kein\nGeschwister-Kombinator (`.article-toc` kommt in keiner CSS-Regel mit `+`/`~`\nvor), kein Tag-Wechsel (immer `<details>`). Keines der drei ADR-0008-Kriterien\ngreift; ein Attributselektor brächte hier nichts. Die einzige direkte\nKind-Regel von `.article-body` (`.article-body > p`, css/components.css:1566)\nzielt auf `p`, nicht auf `details` — ein `<cds-article-toc>`-Host zwischen\n`.article-body` und dem `<details>` bricht deshalb keine Selektorkette.\n\n**Natives `<details>`/`<summary>`, kein nachgebautes Disclosure** (Ticket-Vorgabe,\nAkzeptanzkriterium): Tastaturbedienung (Enter/Space auf `<summary>`) und\nToggle-Zustand kommen vollständig vom Browser, das CSS hängt direkt an `[open]`\n(`.article-toc[open] .article-toc-caret`, css/components.css:1583). `<details\nclass=\"article-toc\">` sitzt deshalb im TEMPLATE, nicht am Host — derselbe Grund\nwie bei `cds-compare`s `<details class=\"ep-compare\">` (`compare.component.ts`):\nein `<cds-article-toc>`-Host ist ein unbekanntes Custom Element und kann native\nDisclosure-Semantik nicht annehmen.\n\n**Zweites Vorkommen des `<details>`-mit-drehendem-Caret-Musters, nicht\nzusammengezogen mit `cds-compare` (ADR-0007 §5).** Beide Bauteile teilen die\nForm (natives `<details>`, SVG-Caret, `[open]`-Rotation über\n`transition:transform`), aber nicht die Aufgabe: `cds-compare` trägt eine\nVergleichstabelle mit `columns`/`rows`/`toggled`-Output, dieses Bauteil trägt\neine reine Link-Liste ohne Output (siehe API-Vorgabe des Tickets, die für dieses\nBauteil bewusst keinen `toggled` vorsieht). Zwei gleiche Stellen sind ein\nZufall, kein Muster — eine gemeinsame Basisklasse für zwei Vorkommen hätte nur\ndas `<details>`+Caret-Skelett gebündelt und dabei entweder Verhalten\nvereinheitlicht (ein erzwungener `toggled`-Output ohne Zweck hier) oder so viel\nparametrisiert, dass nichts gewonnen wäre.\n\n**`aria-label` am `<summary>` ist fest verdrahtet, kein Input.** Ausgezählt:\nbeide realen Vorkommen tragen wortgleich\n`aria-label=\"Inhaltsverzeichnis ein- und ausklappen\"` — der sichtbare Text\n„Inhalt“ (`summary`-Input) allein wäre außerhalb des Beitragskontexts nicht\neindeutig, das `aria-label` schreibt deshalb aus, was das Element tut. Genau\ndieser Wortlaut aus dem Mockup übernommen, kein erfundener zugänglicher Name.\n\n**Keine Nummerierung im Markup.** Die sichtbaren Ziffern vor jedem Eintrag\nkommen aus `counter(toc)` auf `.article-toc-list li::before`\n(css/components.css:1586), nicht aus dem `<ol>`-Default (`list-style:none` setzt\nihn zurück) — die Komponente muss dafür nichts weiter tun als `<ol\nclass=\"article-toc-list\">` zu rendern.",
7
+ "description": "ArticleToc (`cds-article-toc`) — Wrapper um `.article-toc*` aus css/components.css\n(css/components.css:1577–1589): das aufklappbare Inhaltsverzeichnis am Anfang eines\nWissensbeitrags. Ausgezählt: genau 2 reale Vorkommen, beide mit 5 Einträgen und identischem\nsichtbarem Text „Inhalt“ plus identischem `aria-label`\n(`docs/index.html:7948–7960`, Doku-Sektion mit Platzhalter-Hrefs `#gt-article-toc`;\n`docs/index.html:15049–15061`, reale Beispielseite Wissensbeitrag · KI mit echten\nAbschnittsankern wie `#wb-ki-demo`).\n\n**Element-Selektor, ADR-0008-Standardfall.** `.article-toc` sitzt in beiden\nVorkommen als gewöhnlicher Block-Nachfahre direkt in `.article-body`\n(`docs/index.html:7950`, `15051` je eine Ebene über dem `<details>`) — kein\nGrid-/Flex-Kind, keine Layout-Klasse (`col-*`) vom Konsumenten, kein\nGeschwister-Kombinator (`.article-toc` kommt in keiner CSS-Regel mit `+`/`~`\nvor), kein Tag-Wechsel (immer `<details>`). Keines der drei ADR-0008-Kriterien\ngreift; ein Attributselektor brächte hier nichts. Die einzige direkte\nKind-Regel von `.article-body` (`.article-body > p`, css/components.css:1566)\nzielt auf `p`, nicht auf `details` — ein `<cds-article-toc>`-Host zwischen\n`.article-body` und dem `<details>` bricht deshalb keine Selektorkette.\n\n**Natives `<details>`/`<summary>`, kein nachgebautes Disclosure:** Tastaturbedienung\n(Enter/Space auf `<summary>`) und\nToggle-Zustand kommen vollständig vom Browser, das CSS hängt direkt an `[open]`\n(`.article-toc[open] .article-toc-caret`, css/components.css:1583). `<details\nclass=\"article-toc\">` sitzt deshalb im TEMPLATE, nicht am Host — derselbe Grund\nwie bei `cds-compare`s `<details class=\"ep-compare\">` (`compare.component.ts`):\nein `<cds-article-toc>`-Host ist ein unbekanntes Custom Element und kann native\nDisclosure-Semantik nicht annehmen.\n\n**Zweites Vorkommen des `<details>`-mit-drehendem-Caret-Musters, nicht\nzusammengezogen mit `cds-compare` (ADR-0007 §5).** Beide Bauteile teilen die\nForm (natives `<details>`, SVG-Caret, `[open]`-Rotation über\n`transition:transform`), aber nicht die Aufgabe: `cds-compare` trägt eine\nVergleichstabelle mit `columns`/`rows`/`toggled`-Output, dieses Bauteil trägt\neine reine Link-Liste ohne Output (die API sieht für dieses\nBauteil bewusst keinen `toggled` vor). Zwei gleiche Stellen sind ein\nZufall, kein Muster — eine gemeinsame Basisklasse für zwei Vorkommen hätte nur\ndas `<details>`+Caret-Skelett gebündelt und dabei entweder Verhalten\nvereinheitlicht (ein erzwungener `toggled`-Output ohne Zweck hier) oder so viel\nparametrisiert, dass nichts gewonnen wäre.\n\n**`aria-label` am `<summary>` ist fest verdrahtet, kein Input.** Ausgezählt:\nbeide realen Vorkommen tragen wortgleich\n`aria-label=\"Inhaltsverzeichnis ein- und ausklappen\"` — der sichtbare Text\n„Inhalt“ (`summary`-Input) allein wäre außerhalb des Beitragskontexts nicht\neindeutig, das `aria-label` schreibt deshalb aus, was das Element tut. Genau\ndieser Wortlaut aus dem Mockup übernommen, kein erfundener zugänglicher Name.\n\n**Keine Nummerierung im Markup.** Die sichtbaren Ziffern vor jedem Eintrag\nkommen aus `counter(toc)` auf `.article-toc-list li::before`\n(css/components.css:1586), nicht aus dem `<ol>`-Default (`list-style:none` setzt\nihn zurück) — die Komponente muss dafür nichts weiter tun als `<ol\nclass=\"article-toc-list\">` zu rendern.",
8
8
  "jsDocTags": {},
9
9
  "argTypes": {
10
10
  "items": {
@@ -4,7 +4,7 @@
4
4
  "id": "seitenmuster-wissensbeitrag-pull-quote",
5
5
  "name": "ArticlePullquoteComponent",
6
6
  "path": "./src/lib/article-pullquote/article-pullquote.stories.ts",
7
- "description": "ArticlePullquote (`cds-article-pullquote`) — Wrapper um `.article-pullquote` aus\ncss/components.css (css/components.css:1608–1612): die typografische\nHervorhebung eines Satzes aus dem eigenen Lauftext eines Wissensbeitrags.\nZwölftes Ticket der Seitenbausteine-Serie („Artikel-Körper“). Ausgezählt: 2\nreale Vorkommen, wortgleich, beide `data-area=\"ki\"` — Doku-Demo\n(`docs/index.html:8136`) und reale Beispielseite Wissensbeitrag · KI\n(`docs/index.html:15075`).\n\n**Nicht `cds-blockquote` (`BlockquoteComponent`, `.bq`) — Ticket-Vorgabe, im\nJSDoc beider Bauteile wechselseitig abgegrenzt.** Pull-Quote und Blockquote\nteilen die typografische Familie (Serif, bereichsgefärbter Linksakzent in\n`{area}-500`), aber nicht den Zweck, dokumentiert in `wissensbeitrag.mdx`,\nAbschnitt „Pull-Quote gegen Blockquote“:\n\n| | Pull-Quote (dieses Bauteil) | Blockquote (`cds-blockquote`) |\n|---|---|---|\n| Wessen Stimme? | Der Beitrag zitiert sich selbst — ein Schlüsselsatz aus dem eigenen Lauftext | Eine dritte, benannte Person (Kund:in, Forschungsstimme, Branchen-Statement) |\n| Attribution | Keine (`quote` ist der einzige Input) | Pflicht (`name`, `roleLabel`) |\n| Hintergrund/Icon | Keiner — `quotes:none`, kein Quote-Icon, bewusst „offen“ statt umrahmt | Getönte Akzent-Box (`--bq-bg`) mit Quote-Icon (`.bq-icon`) |\n| Tag | `<blockquote class=\"article-pullquote\">` direkt, kein umschließendes `<figure>` | `<figure class=\"bq\">` außen, `<blockquote>` (ohne eigene Klasse) innen für den Zitattext |\n\nEin Self-Quote im eigenen Beitrag (Blockquote-Optik auf einem Satz des\nAutors) wirkt sonst absurd — der Autor zitiert die Person, die den Beitrag\nschreibt. Wer eine externe Stimme mit Namen und Rolle zeigen will, verwendet\n`cds-blockquote`.\n\n**`quote` enthält die deutschen Anführungszeichen bereits als Teil des Texts,\ndie Komponente ergänzt keine.** `.article-pullquote{quotes:none}`\n(css/components.css:1608) — es gibt keine `content:open-quote`/`close-quote`-Regel\nin der CSS-Schicht, weder hier noch bei `.bq blockquote` oder\n`.testimonial blockquote`. Ausgezählt stehen die Anführungszeichen in BEIDEN\nrealen Vorkommen als literale Zeichen im Text\n(„KI scheitert selten an der Technologie. …“), nicht CSS-generiert. Wie bei\n`cds-compare`/`cds-table` ist Copy-Text Sache des Konsumenten (ADR-0007 §2) —\ndie Story liefert deshalb `quote` bereits mit „…“ (CONTRIBUTING.md §6), die\nKomponente rendert den String unverändert, ohne eigene Anführungszeichen zu\nerfinden.\n\n**Element-Selektor, ADR-0008-Standardfall.** `.article-pullquote` sitzt in\nbeiden Vorkommen als gewöhnlicher Block-Nachfahre in `.article-body` — kein\nGrid-/Flex-Kind, keine `col-*`-Klasse vom Konsumenten, kein\nGeschwister-Kombinator in css/components.css, kein Tag-Wechsel (immer\n`<blockquote>`). Die einzige direkte Kind-Regel von `.article-body`\n(`.article-body > p`, css/components.css:1566) zielt auf `p`, nicht auf\n`blockquote` — ein `<cds-article-pullquote>`-Host zwischen `.article-body` und\ndem `<blockquote>` bricht deshalb keine Selektorkette.\n\n**`area` ist Konfiguration mit verteidigbarer Vorgabe (`'co'`), keine\nerfundene Bereichsfarbe.** Die Basisregel `.article-pullquote` selbst (ohne\n`[data-area]`) setzt bereits exakt denselben Wert wie `[data-area=\"co\"]`\n(`border-left-color:var(--co-500)`, css/components.css:1608–1609) — `'co'` ist\ndamit der reale CSS-Fallback ohne Attribut, nicht geraten.",
7
+ "description": "ArticlePullquote (`cds-article-pullquote`) — Wrapper um `.article-pullquote` aus\ncss/components.css (css/components.css:1608–1612): die typografische\nHervorhebung eines Satzes aus dem eigenen Lauftext eines Wissensbeitrags.\nAusgezählt: 2 reale Vorkommen, wortgleich, beide `data-area=\"ki\"` — Doku-Demo\n(`docs/index.html:8136`) und reale Beispielseite Wissensbeitrag · KI\n(`docs/index.html:15075`).\n\n**Nicht `cds-blockquote` (`BlockquoteComponent`, `.bq`) — im\nJSDoc beider Bauteile wechselseitig abgegrenzt.** Pull-Quote und Blockquote\nteilen die typografische Familie (Serif, bereichsgefärbter Linksakzent in\n`{area}-500`), aber nicht den Zweck, dokumentiert in `wissensbeitrag.mdx`,\nAbschnitt „Pull-Quote gegen Blockquote“:\n\n| | Pull-Quote (dieses Bauteil) | Blockquote (`cds-blockquote`) |\n|---|---|---|\n| Wessen Stimme? | Der Beitrag zitiert sich selbst — ein Schlüsselsatz aus dem eigenen Lauftext | Eine dritte, benannte Person (Kund:in, Forschungsstimme, Branchen-Statement) |\n| Attribution | Keine (`quote` ist der einzige Input) | Pflicht (`name`, `roleLabel`) |\n| Hintergrund/Icon | Keiner — `quotes:none`, kein Quote-Icon, bewusst „offen“ statt umrahmt | Getönte Akzent-Box (`--bq-bg`) mit Quote-Icon (`.bq-icon`) |\n| Tag | `<blockquote class=\"article-pullquote\">` direkt, kein umschließendes `<figure>` | `<figure class=\"bq\">` außen, `<blockquote>` (ohne eigene Klasse) innen für den Zitattext |\n\nEin Self-Quote im eigenen Beitrag (Blockquote-Optik auf einem Satz des\nAutors) wirkt sonst absurd — der Autor zitiert die Person, die den Beitrag\nschreibt. Wer eine externe Stimme mit Namen und Rolle zeigen will, verwendet\n`cds-blockquote`.\n\n**`quote` enthält die deutschen Anführungszeichen bereits als Teil des Texts,\ndie Komponente ergänzt keine.** `.article-pullquote{quotes:none}`\n(css/components.css:1608) — es gibt keine `content:open-quote`/`close-quote`-Regel\nin der CSS-Schicht, weder hier noch bei `.bq blockquote` oder\n`.testimonial blockquote`. Ausgezählt stehen die Anführungszeichen in BEIDEN\nrealen Vorkommen als literale Zeichen im Text\n(„KI scheitert selten an der Technologie. …“), nicht CSS-generiert. Wie bei\n`cds-compare`/`cds-table` ist Copy-Text Sache des Konsumenten (ADR-0007 §2) —\ndie Story liefert deshalb `quote` bereits mit „…“ (CONTRIBUTING.md §6), die\nKomponente rendert den String unverändert, ohne eigene Anführungszeichen zu\nerfinden.\n\n**Element-Selektor, ADR-0008-Standardfall.** `.article-pullquote` sitzt in\nbeiden Vorkommen als gewöhnlicher Block-Nachfahre in `.article-body` — kein\nGrid-/Flex-Kind, keine `col-*`-Klasse vom Konsumenten, kein\nGeschwister-Kombinator in css/components.css, kein Tag-Wechsel (immer\n`<blockquote>`). Die einzige direkte Kind-Regel von `.article-body`\n(`.article-body > p`, css/components.css:1566) zielt auf `p`, nicht auf\n`blockquote` — ein `<cds-article-pullquote>`-Host zwischen `.article-body` und\ndem `<blockquote>` bricht deshalb keine Selektorkette.\n\n**`area` ist Konfiguration mit verteidigbarer Vorgabe (`'co'`), keine\nerfundene Bereichsfarbe.** Die Basisregel `.article-pullquote` selbst (ohne\n`[data-area]`) setzt bereits exakt denselben Wert wie `[data-area=\"co\"]`\n(`border-left-color:var(--co-500)`, css/components.css:1608–1609) — `'co'` ist\ndamit der reale CSS-Fallback ohne Attribut, nicht geraten.",
8
8
  "jsDocTags": {},
9
9
  "argTypes": {
10
10
  "area": {