# DocumentTopBar — Final Implementation Spec

Authoritative implementation reference for the responsive DocumentTopBar component. Incorporates all resolutions from the issue #161 team review (Felix Brandt, Markus Keller, Sara Holt, Nora Steiner, Tobias Wendt, Leonie Voss). Supersedes `document-topbar-b1-responsive.html` — refer to that file for additional visual mockup detail.

Familienarchiv · 2026-03-31 · Leonie Voss, UX Lead — Final spec after review
📐 Mockup scale notice — all font-size, height, and padding values in the mockup CSS below are scaled to ~55% of actual implementation values. Do not copy sizes from mockup CSS. Use the ⚙ Implementation Reference tables after each section. Mockup CSS is for visual preview only.
⚠ This spec overrides the B1 spec — font sizes, heights, status chip, overflow pill, and touch targets have all changed. Key corrections: title min `text-[11px]`, chip names `text-[9px]`, topbar heights `h-12/h-14`, status chip dot-only, edit button icon-only on mobile.

## 0 · Component architecture

Decompose into these components. Never merge into a single monolith — each has a clear single visual responsibility and must be independently testable.

Component file | Props | Responsibility | Notes
--- | --- | --- | ---
DocumentTopBar.svelte | `doc, canWrite, canAnnotate, fileUrl, annotateMode (bindable)` | Orchestrator. Owns `overflowOpen: $state(false)`. Passes props down. Contains back link, title, action buttons. | Parent layout must wrap in `<header>`. No direct DOM measurement.
PersonChipRow.svelte | `sender, receivers, abbreviated: boolean` | Chip row with arrow. Visible at ≥375px. Hidden at XS via `hidden xs:flex`. | Renders plain-text fallback slot at XS via parent.
PersonChip.svelte | `person, abbreviated: boolean` | Single chip: avatar initials + name. Abbreviated = first initial + last name. | Avatar colour from `personAvatarColor(person.id)`.
OverflowPill.svelte | `extraCount, persons (for tooltip), open (bindable)` | At ≥768px: interactive `<button>` with tooltip. At <768px: `<span aria-hidden="true">` — non-interactive. | `aria-haspopup="listbox"`, `aria-expanded`, `aria-label`. See tooltip rules.
DocumentStatusChip.svelte | `status: DocumentStatus` | Dot-only indicator. Hidden below 768px. `title` + `aria-label` carry the label text. | No text label — removes i18n requirement.
AnnotateHintStrip.svelte | `annotateMode: boolean` | 18px strip below main row. Only rendered when `annotateMode === true` AND viewport ≥768px. | Use `{#if annotateMode}` — no CSS height animation. Hidden via parent responsive class.

Implementation Reference — Svelte state & derived values Svelte 5 runes

Value | Type | Implementation | Notes
--- | --- | --- | ---
overflowOpen | `$state(false)` | `let overflowOpen = $state(false)` | In DocumentTopBar. Passed as bindable to OverflowPill.
visibleReceivers | `$derived` | `$derived(doc.receivers.slice(0, viewportGe768 ? 2 : 1))` | CSS-only: at <768px always 1 shown. At ≥768px show 2 if count==2, else 1. Use CSS to hide — no JS.
extraCount | `$derived` | `$derived(doc.receivers.length - visibleReceivers.length)` | 0 = no pill needed.
formattedDate | `$derived` | See utility module — `formatDate(doc.documentDate, format)` | Format switches via CSS responsive classes, not JS viewport check.
xsMetaLine | `$derived` | `$derived(formatXsMeta(doc))` | Used only at XS. Import from `$lib/utils/personFormat`.
annotateMode | bindable prop | `let { annotateMode = $bindable(false) } = $props()` | Parent page owns state. TopBar toggles it.

## 1 · Design tokens

All CSS custom properties used by the topbar. No hardcoded colours in any component — all must reference these tokens.

Light theme

bg-surface | #FFFFFF — topbar bg
border-line | #E4E2D8 — bottom border, dividers, chip borders
bg-primary | #012851 — accent bar (light), primary btn, avatars
text-primary-fg | #A1DCD8 — text on navy bg
text-ink | #1A1A1A — title, chip names
text-ink-2 | #AAAAAA — date, meta (spec ink-3 mapped here)
bg-muted | #F0EFE9 — chip bg, back btn bg at XS
Accent bar | `border-l-[3px] border-primary` — always present, all breakpoints

Dark theme

bg-surface | #0F1923
border-line | #1E2D3D
Accent bar (dark) | #A1DCD8 — teal replaces navy for accent bar
text-ink | #EAE8E2 — title
text-ink-2 (meta) | #3E5065 — date, meta
Chip bg | #0A1218 · border #1E2D3D
Primary btn (dark) | bg #A1DCD8 · text #012851 — inverted

Implementation Reference — Design Tokens Real values · CSS custom properties

Token / concern | Tailwind class | CSS var | Notes
--- | --- | --- | ---
Accent bar | `border-l-[3px] border-primary` | `var(--color-primary)` | Light: #012851. Dark: resolves to #A1DCD8 via theme. Never hardcode.
Topbar bg | `bg-surface` | `var(--color-surface)` | Auto light/dark via CSS custom property.
Bottom border | `border-b border-line` | `var(--color-line)` | 1px, both themes.
Chip bg | `bg-muted` | `var(--color-muted)` | Light #F0EFE9, dark #0A1218.
Chip border | `border-line` | — | Same token as bottom border.
Hint strip bg (light) | `bg-[rgba(1,40,81,0.05)]` | — | ⚠ --color-primary must be RGB format (1 40 81) for bg-primary/5 to work. If hex, use explicit rgba fallback.
Hint strip bg (dark) | `dark:bg-[rgba(161,220,216,0.04)]` | — | Use explicit rgba. Verify --color-primary-fg is also RGB.
Avatar palette | inline style only | — | 5 values: `['#012851','#5A3080','#007596','#2A6040','#803020']`. Index = `hash(id) % 5`.

## 2 · Receiver overflow patterns

Rule: always show sender + 1st receiver, collapse remaining. At <768px: max 1 receiver shown, overflow pill is a non-interactive span. At ≥768px: show 2 receivers if count==2 (no pill); show 1 + pill if count≥3.

Light theme — all receiver counts
0 receivers
KR
Karl Raddatz
Sender only. No arrow. Diary entries, certificates.
1 receiver
KR
Karl Raddatz
→
ER
Elfriede Raddatz
Both shown. No pill.
2 receivers
KR
Karl Raddatz
→
ER
Elfriede Raddatz
·
HR
Hans Raddatz
≥768px only: both shown, no pill. At <768px: collapse to 1st + "+1" span.
3 receivers
KR
Karl Raddatz
→
ER
Elfriede Raddatz
+2 weitere
≥768px: "+2 weitere" interactive button. <768px: "+2" non-interactive span, aria-hidden.
No sender
ER
Elfriede Raddatz
First available person shown. No arrow. Photos, undated documents.
Implementation Reference — Chip & Overflow Logic Real values · mockup above is ~55% scale

Element | Tailwind classes | Real size | Notes
--- | --- | --- | ---
PersonChip container | `inline-flex items-center gap-1 rounded-full border border-line bg-muted px-2 py-1 whitespace-nowrap shrink-0` | h ~28px, px 8px, py 4px | Not interactive — no hover/focus styles needed.
Avatar circle | `flex w-[18px] h-[18px] shrink-0 items-center justify-center rounded-full text-[7px] font-bold` | 18×18px | bg from `personAvatarColor(id)` — inline style. text-primary-fg for navy bg, white for others.
Chip name (full) | `text-[9px] font-semibold text-ink` | 9px / 600 | ⚠ Most commonly undersized — minimum 9px. Original spec said 6.5px — corrected.
Chip name (abbreviated) | `text-[9px] font-semibold text-ink` | 9px / 600 | "K. Raddatz" format at <768px. Same styling as full name.
Arrow between chips | `text-ink-2 shrink-0 text-[11px]` aria-hidden="true" | 11px | Unicode → (U+2192). aria-hidden always present.
Overflow pill (≥768px) | `inline-flex items-center rounded-full border border-line bg-muted px-2 py-1 text-[9px] font-bold text-ink-2 whitespace-nowrap shrink-0 min-h-[44px] md:min-h-0` | 9px / 700 | Interactive button at ≥768px. Active state: `bg-primary border-primary text-primary-fg`.
Overflow pill (<768px) | `inline-flex items-center rounded-full border border-line bg-muted px-2 py-1 text-[9px] font-bold text-ink-2 whitespace-nowrap shrink-0` | 9px / 700 | `<span aria-hidden="true">` — not a button. No tap behaviour.
PersonChipRow wrapper | `hidden xs:flex items-center gap-1.5 min-w-0 overflow-hidden` | — | Hidden at XS (<375px). flex at ≥375px.
XS plain-text meta | `block xs:hidden text-[9px] text-ink-2 truncate mt-0.5` | 9px | Format: "K.Raddatz → E.Raddatz +4 · 24.12.1943". From `formatXsMeta(doc)`.

## 3 · Viewport-by-viewport

Visual reference for each breakpoint. See B1 spec for full set. Key states shown below.

320 px · XS Mobile
320pxLight
Brief a. Großmutter
K.Raddatz → E.Raddatz · 24.12.1943
XS: square back btn, plain-text meta below title, icon-only edit button. No chips, no annotate, no download.
320pxDark
Brief a. Großmutter
K.Raddatz → E.Raddatz · 24.12.1943
Dark XS: teal accent bar, icon-only edit, dark chip-less meta.
375 px · Standard Mobile
375pxLight · 1 receiver
Brief a. Großmutter, 1943
KR
K. Raddatz
→
ER
E. Raddatz
375px: circle back btn, chip row with abbreviated names, icon-only edit. Annotate hidden.
375pxLight · 3+ receivers
Rundbrief, 1951
KR
K. Raddatz
→
ER
E. Raddatz
+2
375px overflow: "+2" is a non-interactive <span aria-hidden>. No "weitere" — no tooltip on mobile.
768 px · Tablet
768pxLight · 1 receiver
Brief an Großmutter, Weihnachten 1943
24.12.1943
·
KR
Karl Raddatz
→
ER
Elfriede Raddatz
Annotieren
Bearbeiten
768px: full names, dot-only status indicator, Annotate + Bearbeiten + download icon. Status dot replaces text chip.
768pxDark · annotate active
Brief an Großmutter
KR
Karl Raddatz
→
ER
Elfriede Raddatz
Beenden
Annotierungsmodus aktiv
Klicken Sie auf eine Textstelle.
Annotate active: Edit + Download removed. "Beenden" fills teal. Hint strip visible. PDF outline teal at 15%.
1024 px · Laptop / 1440 px · Desktop
1024pxLight · 5 receivers (overflow open)
Rundbrief an die Familie, Sommer 1951
18.07.1951 · Berlin
·
KR
Karl Raddatz
→
ER
Elfriede Raddatz
+4 weitere
Weitere Empfänger
HR
Hans Raddatz
MR
Maria Raddatz
GR
Gerhard Raddatz
Annotieren
Bearbeiten
Active overflow pill fills navy. Tooltip drops below: "Weitere Empfänger" + avatar + name link per person.
Implementation Reference — Topbar Layout per Breakpoint Real values · mockup above is ~55% scale

Element | XS <375px | Mobile 375–767px | Tablet/Desktop ≥768px | Notes
--- | --- | --- | --- | ---
Topbar height | `h-12` 48px | `h-14` 56px | `h-14` 56px | ⚠ Increased from B1 spec (was 44/50/52px) to fit text-[11px]+ chip row.
Back button wrapper | `a href="/documents" w-11 h-11 -ml-2 flex items-center justify-center shrink-0 focus-visible:ring-2` | `w-11 h-11` = 44×44px touch area. -ml-2 = visual alignment. aria-label="Zurück zur Dokumentenliste" always.
Back button visual | `w-6 h-6 rounded-sm bg-muted flex items-center justify-center` | `w-6 h-6 rounded-full border border-line flex items-center justify-center` | `w-7 h-7 rounded-full border border-line flex items-center justify-center` | Shape changes at xs breakpoint. Inner chevron SVG 10×10px.
Title | `font-serif font-extrabold text-[11px] text-ink truncate` | `font-serif font-extrabold text-[11px] text-ink truncate` | `font-serif font-extrabold text-[12px] lg:text-[13px] text-ink truncate` | ⚠ Minimum 11px — original spec said 10px, corrected.
Status indicator | Hidden `hidden` | Hidden `hidden` | `hidden md:block w-2.5 h-2.5 rounded-full shrink-0` | Dot only. title + aria-label carry label. See statusDotClass() for colours.
Date text | In xsMetaLine string | `text-xs text-ink-2 shrink-0` format: "24.12.1943" | `text-xs text-ink-2 shrink-0` format: ≥1024px long ("24. Dezember 1943") | Date format switches via Tailwind: `<span class="lg:hidden">24.12.1943</span><span class="hidden lg:inline">{longDate}</span>`
Location in meta | Hidden | Hidden | Shown if doc.location present at ≥768px: `hidden md:inline` | // TODO: show location when doc.location field available on DTO
Edit button (XS/mobile) | `inline-flex items-center justify-center w-11 h-11 -mr-2 shrink-0` with pencil SVG 18×18px. aria-label="Bearbeiten". | — | ⚠ Icon-only on mobile. "Bearbeiten" text hidden below 768px.
Edit button (tablet+) | — | — | `hidden md:inline-flex h-10 items-center gap-2 px-4 bg-primary text-primary-fg text-[11px] font-bold uppercase tracking-wide rounded-sm` | Shows pencil icon + "Bearbeiten" label at ≥768px.
Annotate button | Hidden `hidden` | Hidden `hidden` | `hidden md:inline-flex h-10 items-center gap-2 px-3 border border-line text-ink-2 text-[11px] font-bold uppercase tracking-wide rounded-sm` | Active: `bg-primary border-primary text-primary-fg`. Label "Annotieren" → "Beenden". aria-pressed={annotateMode}.
Download button | Hidden | Hidden | `hidden md:inline-flex w-10 h-10 items-center justify-center border border-line rounded-sm text-ink-2` | Icon only — download SVG 18×18px.
Divider | Hidden | Hidden | `hidden md:block w-px h-4 bg-line shrink-0` | Between download icon and Bearbeiten button.

## 4 · Responsive rules (authoritative)

All heights and font sizes updated from resolved review. These values override the B1 spec.

Element | ≤ 374px (XS) | 375–767px (mobile) | 768–1023px (tablet) | ≥ 1024px (desktop)
--- | --- | --- | --- | ---
Topbar height | h-12 (48px) | h-14 (56px) | h-14 (56px) | h-14 (56px)
Back button | Square 24×24 `rounded-sm bg-muted` | Circle 24×24 `rounded-full border-line` | Circle 28×28 `rounded-full border-line` | Circle 28×28
Touch target | `w-11 h-11 -ml-2` wrapper around all back button variants. Always 44×44px.
Title size | text-[11px] / 800 | text-[11px] / 800 | text-[12px] / 800 | text-[13px] / 800
Chip row | Hidden → plain-text xsMetaLine | Shown — abbreviated names | Shown — full names | Shown — full names + location
Chip name text | N/A | text-[9px] / 600 | text-[9px] / 600 | text-[9px] / 600
Date format | `dd.mm.yyyy` in xsMetaLine | `24.12.1943` | `24.12.1943` | `24. Dezember 1943`
Status indicator | Hidden | Hidden | Dot only — `w-2.5 h-2.5 rounded-full` | Dot only
Annotate button | Hidden | Hidden | Shown — "Annotieren" | Shown — "Annotieren"
Edit button | Icon only (pencil SVG) | Icon only | Icon + "Bearbeiten" | Icon + "Bearbeiten"
Download button | Hidden | Hidden | Icon only | Icon only
Overflow pill | N/A (chips hidden) | `<span aria-hidden>` "+N" | `<button>` "+N weitere" → tooltip | `<button>` "+N weitere" → tooltip
Hint strip | Hidden | Hidden | 18px strip when annotateMode | 18px strip when annotateMode

Implementation Reference — Tailwind breakpoint setup tailwind.config.ts

Item | Value | Notes
--- | --- | ---
Custom xs breakpoint | `xs: '375px'` in `theme.extend.screens` | ⚠ Tailwind 4 may use different config syntax — verify before using `xs:` prefix classes. Without this, `xs:flex` silently does nothing.
Usage for chip row | `hidden xs:flex` | Hidden at <375px, flex at ≥375px.
Usage for XS meta | `block xs:hidden` | Only visible below 375px.
Usage for overflow pill type | CSS only: `hidden xs:inline-flex` for button, always-rendered span for mobile with `md:hidden` | Prefer CSS over JS viewport check — no SSR issues.

## 5 · Overflow tooltip

Clicking "+N weitere" opens a floating panel. Only at ≥768px. Mobile overflow pill is a non-interactive span.

Implementation Reference — OverflowPill component Real values · Svelte 5

Element | Tailwind classes | Real size | Notes
--- | --- | --- | ---
Pill (interactive ≥768px) | `inline-flex items-center rounded-full border border-line bg-muted px-2 py-1 text-[9px] font-bold text-ink-2 whitespace-nowrap shrink-0 cursor-pointer` | h ~28px, 9px text | Active state: `bg-primary border-primary text-primary-fg`. aria-haspopup="listbox" aria-expanded={open} aria-label="{count} weitere Empfänger".
Pill (non-interactive <768px) | same visual classes as above on `<span aria-hidden="true">` | — | No button, no onclick, no tooltip. Users access full receiver list via document edit page.
Tooltip panel | `absolute top-full left-0 mt-1 bg-surface border border-line rounded-md shadow-lg p-3 min-w-[160px] z-50` | min 160px wide, p 12px | position:relative on chip row container. role="listbox".
Tooltip header | `text-[9px] font-bold uppercase tracking-wide text-ink-2 border-b border-line pb-2 mb-2` | 9px / 700 | Text: "Weitere Empfänger".
Person row | `flex items-center gap-2 py-1 rounded hover:bg-muted` | min 32px tall | role="option". Each row is an <a href="/persons/{id}">. Avatar 16×16px.
Person name in tooltip | `text-[11px] font-medium text-ink` | 11px / 500 | Full name always shown in tooltip.
Keyboard: open | — | — | Enter/Space on pill → open tooltip → focus first role="option".
Keyboard: navigate | — | — | Tab/Shift+Tab between options inside tooltip.
Keyboard: close | — | — | Escape → close + return focus to pill. Click-outside → close. Second click on pill → close.
Click-outside | — | — | Use `use:clickOutside` Svelte action — do not use document.addEventListener directly in $effect.

Implementation Reference — Annotate hint strip AnnotateHintStrip.svelte

Element | Tailwind classes | Real size | Notes
--- | --- | --- | ---
Strip wrapper | `hidden md:flex h-[18px] items-center gap-2 border-t border-dashed px-3.5` | h 18px | Only rendered when annotateMode===true via Svelte {#if}. hidden below md.
Strip bg (light) | `bg-[rgba(1,40,81,0.05)]` | — | ⚠ Use explicit rgba — bg-primary/5 requires RGB --color-primary. Fallback if not converted.
Strip border (light) | `border-[rgba(1,40,81,0.20)]` | — | Same caveat — explicit rgba.
Strip bg (dark) | `dark:bg-[rgba(161,220,216,0.04)]` | — | Explicit rgba.
Label text | `text-[9px] font-bold uppercase tracking-wide text-primary` | 9px / 700 | ⚠ Corrected from spec's 5.5px — minimum 9px. Dark: text-primary-fg.
Body text | `text-[9px] text-ink-2` | 9px | "Klicken Sie auf eine Textstelle im Dokument, um eine Anmerkung hinzuzufügen."

## 6 · Utility functions — `src/lib/utils/personFormat.ts`

Pure functions. Write Vitest unit tests for each before implementing. No DOM, no side effects.

Function | Signature | Behaviour & edge cases
--- | --- | ---
abbreviateName | `abbreviateName(person: Person): string` | "Karl Raddatz" → "K. Raddatz" "Elfriede" (single name) → "Elfriede" (no initial, return as-is) "Karl Müller-Schmidt" → "K. Müller-Schmidt" (preserve hyphenated last name) Split on first space only. First character of first word + ". " + rest.
formatXsMeta | `formatXsMeta(doc: Document): string` | 0 receivers: "K.Raddatz · 24.12.1943" 1 receiver: "K.Raddatz → E.Raddatz · 24.12.1943" 3 receivers: "K.Raddatz → E.Raddatz +2 · 24.12.1943" No sender: "E.Raddatz · 24.12.1943" Abbreviated format: first initial + dot + last name, no space (e.g. "K.Raddatz"). Date: `dd.mm.yyyy` format, no spaces.

personAvatarColor

`personAvatarColor(personId: string): string`

Returns one of: `['#012851','#5A3080','#007596','#2A6040','#803020']`
Must be deterministic: same ID always returns same colour.
Implementation: `PALETTE[simpleHash(id) % PALETTE.length]`
simpleHash: sum of char codes, or djb2. Never interpolate the ID string into CSS directly.
Test: 1000 random UUIDs → all map to valid palette entry.

formatDate | `formatDate(isoDate: string, format: 'short' | 'long'): string` | short: "24.12.1943" — use `Intl.DateTimeFormat('de-DE', {day:'2-digit', month:'2-digit', year:'numeric'})` long: "24. Dezember 1943" — use `month: 'long'` Always parse with `new Date(isoDate + 'T12:00:00')` to avoid UTC off-by-one.
statusDotClass | `statusDotClass(status: DocumentStatus): string` | PLACEHOLDER → `'bg-gray-400'` UPLOADED → `'bg-emerald-500'` TRANSCRIBED → `'bg-blue-400'` REVIEWED → `'bg-amber-400'` ARCHIVED → `'bg-emerald-600'`
statusLabel (for title/aria) | `statusLabel(status: DocumentStatus): string` | German labels (not shown as text, used in title + aria-label only): PLACEHOLDER → "Platzhalter" · UPLOADED → "Hochgeladen" · TRANSCRIBED → "Transkribiert" REVIEWED → "Geprüft" · ARCHIVED → "Archiviert"

## 7 · Accessibility requirements (WCAG 2.2 AA)

Element | Requirement
--- | ---
Landmark | Parent page must wrap topbar in `<header role="banner">` or the topbar must itself be in `<header>`. Verify parent layout.
Back link | `aria-label="Zurück zur Dokumentenliste"` always present — icon is the only visible element at XS.
Edit button (icon-only mobile) | `aria-label="Bearbeiten"` always present, even at mobile where it renders icon-only.
Annotate button | `aria-pressed={annotateMode}`. Label changes: "Annotieren" → "Beenden".
Overflow pill | `aria-haspopup="listbox"` (not "true"), `aria-expanded={overflowOpen}`, `aria-label="{extraCount} weitere Empfänger anzeigen"`.
Overflow tooltip | `role="listbox"` on panel. `role="option"` on each person row. Not a modal — no focus trap needed.
Tooltip focus flow | Opening tooltip moves focus to first `role="option"`. Tab/Shift+Tab navigates within. Escape closes + returns focus to pill.
Arrow between chips | `aria-hidden="true"` on the → character. Directionality conveyed by order, not just the arrow.
Status dot | `title={statusLabel(doc.status)}` for hover tooltip. `aria-label={statusLabel(doc.status)}`. No text label rendered.
Touch targets | Back button: 44×44px via wrapper. Edit (mobile): 44×44px via wrapper. Overflow pill: naturally ≥44px wide at ≥375px. All verified.
Focus rings | Never `outline:none` without a replacement. All interactive elements: `focus-visible:ring-2 focus-visible:ring-primary`.
Colour alone | Status uses colour + aria-label. Never colour as only signal. Annotate mode uses label change "Beenden" + visual fill + hint strip.

## 8 · Acceptance criteria

ID | Criterion
--- | ---
AC-01 | Topbar renders correctly at 320px, 375px, 768px, 1024px, 1440px — matches spec screenshots. Verified by /proofshot at all 5 widths.
AC-02 | Light and dark themes match token table — no hardcoded hex values in any component file.
AC-03 | 0-receiver, 1-receiver, 2-receiver, 3-receiver, and 5-receiver cases all render correctly per Section 2.
AC-04 | No-sender case: chip row shows receivers only (or first person if no sender), no arrow rendered.
AC-05 | Overflow tooltip opens/closes on click, Enter/Space, Escape. Escape returns focus to pill. Click-outside closes.
AC-06 | Opening tooltip moves focus to first person link inside tooltip.
AC-07 | Overflow tooltip links navigate to correct /persons/{id} URL.
AC-08 | Overflow pill at <768px is a non-interactive span — no tooltip, no tap action.
AC-09 | Annotate mode: Edit + Download hidden; hint strip visible; button label "Beenden"; aria-pressed=true.
AC-10 | Annotate hint strip NOT rendered below 768px even when annotateMode===true.
AC-11 | Status dot visible at ≥768px with correct colour per status. Hidden below.
AC-12 | All touch targets ≥44×44px at mobile (back button, edit button, overflow pill where interactive).
AC-13 | aria-pressed, aria-haspopup="listbox", aria-expanded, aria-label on all interactive elements.
AC-14 | svelte-check passes with no new type errors.
AC-15 | Unit tests pass for: abbreviateName (full, single, hyphenated), formatXsMeta (0/1/3+ receivers, no sender), personAvatarColor (deterministic, palette-only), statusDotClass (all 5 values), statusLabel (all 5 values), formatDate (short, long, UTC boundary).
AC-16 | Visual proof: /proofshot against document detail page at all 5 viewport widths, both light and dark themes (10 screenshots).

DocumentTopBar Final Spec · Familienarchiv · 2026-03-31 · Leonie Voss
Supersedes `document-topbar-b1-responsive.html` · All resolutions from issue #161 review incorporated