diff --git a/docs/specs/geschichten-reader-journey-spec.html b/docs/specs/geschichten-reader-journey-spec.html
index ac537ed7..f5970dbd 100644
--- a/docs/specs/geschichten-reader-journey-spec.html
+++ b/docs/specs/geschichten-reader-journey-spec.html
@@ -421,7 +421,7 @@
Seitenstruktur
Page title font-family:var(--font-display);font-size:24px;color:var(--navy) Fraunces, nicht fett
- Editorial list card bg-white shadow-sm border border-brand-sand rounded-sm wraps alle Zeilen
+ Editorial list card bg-white shadow-sm border border-brand-sand rounded-sm (implementiert: bg-surface border-line — semantische Tokens für Dark Mode) wraps alle Zeilen
Listenzeile
List row flex gap-0 border-b border-brand-sand last:border-0 hover:bg-surface min-h-[44px] auf Mobile
Meta column w-40 shrink-0 flex flex-col gap-1 p-3 border-r border-line-2 feste Breite — breit genug für text-sm Namen ohne Umbruch
@@ -658,7 +658,7 @@
Doc reference card flex gap-3 items-start bg-white border border-brand-sand rounded-sm p-3 hover:shadow-sm links zu /documents/[id]
Doc icon w-9 h-9 bg-surface rounded flex items-center justify-center shrink-0 Dateisymbol SVG
Mobile
- … Menü (Mobile) ml-auto text-ink-3; öffnet BottomSheet mit Bearbeiten + Löschen BLOG_WRITE-Aktionen auf Mobile
+ … Menü (Mobile) ml-auto text-ink-3; öffnet BottomSheet mit Bearbeiten + Löschen (implementiert: Bearbeiten/Löschen bleiben inline in der Metazeile auf allen Breiten — kein BottomSheet) BLOG_WRITE-Aktionen auf Mobile
Person chips (Mobile) flex-wrap, volle Breite kein horizontales Scrollen
Doc cards (Mobile) flex-col gap-2 stapeln vertikal
@@ -713,7 +713,7 @@
Die Schaltflächen „+ Neue Geschichte", „Bearbeiten" und „Löschen" werden nur gerendert, wenn currentUser.permissions.includes('BLOG_WRITE') wahr ist.
Nicht nur ausblenden — Backend-Endpunkte für Schreib-/Löschoperationen sind ebenfalls durch @RequirePermission(Permission.BLOG_WRITE) geschützt.
- Auf Mobile werden Bearbeiten/Löschen aus dem Layout entfernt und erscheinen in einem BottomSheet, das über das ··· Menü in der Metazeile geöffnet wird.
+ Auf Mobile werden Bearbeiten/Löschen aus dem Layout entfernt und erscheinen in einem BottomSheet, das über das ··· Menü in der Metazeile geöffnet wird. (implementiert: Aktionen bleiben inline in der Metazeile — h-11 Touch-Targets, kein BottomSheet)
Barrierefreiheit
diff --git a/docs/specs/lesereisen-reader-spec.html b/docs/specs/lesereisen-reader-spec.html
index 7c0756d0..060ec634 100644
--- a/docs/specs/lesereisen-reader-spec.html
+++ b/docs/specs/lesereisen-reader-spec.html
@@ -634,7 +634,7 @@
Journey-Badge inline-flex px-2 py-px rounded-sm text-[10px] font-bold uppercase tracking-widest bg-journey-tint text-journey border border-journey-border mb-2 über dem Titel; nicht für STORY
Titel font-serif text-3xl text-ink leading-tight mb-4 gleich wie Story
Metabar flex items-center gap-3 pb-4 border-b border-subtle mb-4 gleich wie Story
- Bearbeiten/Löschen nur BLOG_WRITE; auf Mobile im ··· BottomSheet gleich wie Story
+ Bearbeiten/Löschen nur BLOG_WRITE; auf Mobile im ··· BottomSheet (implementiert: inline in der Metazeile auf allen Breiten) gleich wie Story
Intro-Absatz
Intro (body) font-serif text-lg text-ink-2 italic leading-relaxed mb-6 pb-4 border-b border-dashed border-subtle nur rendern wenn body nicht leer; kein HTML-Rendering — plaintext
Dokument-Item
@@ -650,7 +650,7 @@
Interlude-Block pl-3 border-l-2 border-journey-border bg-journey-tint rounded-r-sm py-2 pr-3 my-4 item.document === null
Interlude-Text text-base italic text-ink leading-relaxed item.note; plaintext
Mobile
- ··· Menü ml-auto text-ink-3; öffnet BottomSheet mit Bearbeiten + Löschen BLOG_WRITE; gleich wie Story
+ ··· Menü ml-auto text-ink-3; öffnet BottomSheet mit Bearbeiten + Löschen (implementiert: kein BottomSheet — Aktionen inline) BLOG_WRITE; gleich wie Story
Touch Target (Brief öffnen) min-h-[44px] durch padding auf der Karte WCAG 2.2 AA
@@ -711,7 +711,7 @@
Berechtigungen
„Bearbeiten" und „Löschen" nur für currentUser.permissions.includes('BLOG_WRITE') — gleich wie Story.
- Auf Mobile: Bearbeiten/Löschen im BottomSheet hinter ··· — gleich wie Story.
+ Auf Mobile: Bearbeiten/Löschen im BottomSheet hinter ··· — gleich wie Story. (implementiert: Aktionen bleiben inline in der Metazeile — h-11 Touch-Targets)
Barrierefreiheit
diff --git a/frontend/src/lib/geschichte/README.md b/frontend/src/lib/geschichte/README.md
index f1e6e959..b569a567 100644
--- a/frontend/src/lib/geschichte/README.md
+++ b/frontend/src/lib/geschichte/README.md
@@ -23,16 +23,17 @@ Utilities: `utils.ts`.
| `JourneyItemRow.svelte` | `JourneyEditor.svelte` | Item row: drag handle, move-up/down, note textarea (PATCH on blur), inline remove confirm |
| `JourneyAddBar.svelte` | `JourneyEditor.svelte` | Two add buttons: document picker (`DocumentPickerDropdown`) and interlude draft form |
| `GeschichtenCard.svelte` | Person-detail sidebar | Sidebar card showing the 3 most recent stories linked to a person — NOT the list page |
-| `GeschichteListRow.svelte` | `/geschichten` (list) | Row component for the list; shows JOURNEY type badge (`text-xs` orange pill) |
+| `GeschichteListRow.svelte` | `/geschichten` (list) | Editorial list row: meta column (avatar, author, date, REISE badge), title + excerpt content column |
| `StoryReader.svelte` | `/geschichten/[id]` (STORY branch) | Renders sanitised rich-text body, persons section, documents section, and author actions |
| `JourneyReader.svelte` | `/geschichten/[id]` (JOURNEY branch) | Renders intro paragraph, ordered items list, empty-state; delegates to ItemCard/Interlude |
-| `JourneyItemCard.svelte` | `JourneyReader.svelte` | Whole-card `` for a document item; dated/undated aria-label, ✎ annotation glyph |
-| `JourneyInterlude.svelte` | `JourneyReader.svelte` | Typographic aside between letters; ❦ glyph, `aria-label="Kuratorennotiz"` |
+| `JourneyItemCard.svelte` | `JourneyReader.svelte` | Card per document item: title, meta line (date · von X an Y), "Brief öffnen →" link, mint-border note |
+| `JourneyInterlude.svelte` | `JourneyReader.svelte` | Left-accent interlude box between letters (mode-aware tokens); `aria-label="Kuratorennotiz"` |
## utils.ts
-`formatAuthorName(author)` — joins `firstName + lastName`, falls back to `email` (for list/summary shapes).
-`formatAuthorDisplayName(author)` — returns `displayName` (for detail `AuthorView` shape).
+`formatAuthorName(author)` — joins `firstName + lastName`, falls back to the localized `person_unknown` key (for list/summary shapes; email is not exposed).
+`formatAuthorDisplayName(author)` — returns `displayName`, localizing the server's `[Unbekannt]` fallback (for detail `AuthorView` shape).
+`formatDocumentMetaLine(doc)` — `"12.07.1938 · von Franz an Emma"`; shared by `JourneyItemCard`, `JourneyItemRow`, and the story doc-reference cards.
`formatPublishedAt(publishedAt, style)` — wraps `formatDate` with null check; `style` is `'short'` (list) or `'long'` (detail).
## Public list is PUBLISHED-only
diff --git a/frontend/src/lib/shared/utils/extractText.ts b/frontend/src/lib/shared/utils/extractText.ts
index 331d9dea..a5ccb976 100644
--- a/frontend/src/lib/shared/utils/extractText.ts
+++ b/frontend/src/lib/shared/utils/extractText.ts
@@ -1,13 +1,18 @@
/**
- * **Not a sanitizer.** This module extracts visible text from a (presumed
- * already-sanitised) HTML string for excerpt rendering. It is safe ONLY
- * because the Geschichte body is sanitised against the OWASP allow-list
- * on the server before persistence, and via DOMPurify on render.
+ * **Not a sanitizer.** This module extracts visible text from an HTML (or
+ * plain-text) string for excerpt rendering. The safety invariant is: the
+ * OUTPUT must only ever be rendered via Svelte text interpolation — never
+ * `{@html}`. The DOMParser document is inert (scripts don't execute), but
+ * the returned string is whatever text the input carried.
+ *
+ * Note on inputs: STORY bodies are additionally sanitised against the OWASP
+ * allow-list on the server; JOURNEY intros are stored VERBATIM (unsanitised
+ * by design — see GeschichteService.bodyForType) and arrive here untrusted.
*
* Do not use these helpers to defend against XSS — `safeHtml()` in
* `./sanitize.ts` is the only sanitiser. Calling `extractText()` on
- * untrusted input that has not been sanitised does not protect against
- * `javascript:` URLs, event-handler attributes, or `` payloads.
+ * untrusted input does not protect against `javascript:` URLs,
+ * event-handler attributes, or `` payloads.
*/
/**