diff --git a/frontend/src/lib/timeline/EventPill.svelte b/frontend/src/lib/timeline/EventPill.svelte
new file mode 100644
index 00000000..9b125cb7
--- /dev/null
+++ b/frontend/src/lib/timeline/EventPill.svelte
@@ -0,0 +1,59 @@
+
+
+
+
+
+ {config.glyph}
+ {config.label}
+
+
+ {#if entry.title}
+ {entry.title}
+ {/if}
+ {#if dateLabel}
+ {dateLabel}
+ {/if}
+
+ {#if canEdit}
+
+ ✎
+ {m.btn_edit()}
+
+ {/if}
+
+
diff --git a/frontend/src/lib/timeline/EventPill.svelte.spec.ts b/frontend/src/lib/timeline/EventPill.svelte.spec.ts
new file mode 100644
index 00000000..945ea65f
--- /dev/null
+++ b/frontend/src/lib/timeline/EventPill.svelte.spec.ts
@@ -0,0 +1,90 @@
+import { describe, it, expect, afterEach } from 'vitest';
+import { cleanup, render } from 'vitest-browser-svelte';
+import EventPill from './EventPill.svelte';
+import { makeEntry } from './test-factories';
+
+afterEach(() => cleanup());
+
+const EVENT_ID = '33333333-3333-3333-3333-333333333333';
+
+function derived(derivedType: 'BIRTH' | 'DEATH' | 'MARRIAGE', title: string) {
+ return makeEntry({
+ kind: 'EVENT',
+ derived: true,
+ derivedType,
+ title,
+ senderName: '',
+ receiverName: '',
+ precision: 'YEAR',
+ eventDate: '1914-01-01',
+ documentId: undefined
+ });
+}
+
+describe('EventPill', () => {
+ it('renders a derived marriage as ⚭ + "Heirat" + title (REQ-007)', () => {
+ render(EventPill, { entry: derived('MARRIAGE', 'Heirat: Karl & Elfriede') });
+ expect(document.body.textContent).toContain('⚭');
+ expect(document.body.textContent).toContain('Heirat');
+ expect(document.body.textContent).toContain('Heirat: Karl & Elfriede');
+ });
+
+ it('renders a derived birth as * + "Geburt" (REQ-007)', () => {
+ render(EventPill, { entry: derived('BIRTH', 'Geburt: Hans') });
+ expect(document.body.textContent).toContain('*');
+ expect(document.body.textContent).toContain('Geburt');
+ });
+
+ it('renders a derived death as † + "Tod" (REQ-007)', () => {
+ render(EventPill, { entry: derived('DEATH', 'Tod: Karl') });
+ expect(document.body.textContent).toContain('†');
+ expect(document.body.textContent).toContain('Tod');
+ });
+
+ it('wraps the glyph aria-hidden with an sr-only label sibling (REQ-018)', () => {
+ render(EventPill, { entry: derived('BIRTH', 'Geburt: Hans') });
+ const hidden = document.querySelector('[aria-hidden="true"]');
+ expect(hidden?.textContent).toBe('*');
+ const srOnly = document.querySelector('.sr-only');
+ expect(srOnly?.textContent).toBe('Geburt');
+ });
+
+ it('shows an edit affordance for a curated PERSONAL event with an eventId (REQ-008)', () => {
+ render(EventPill, {
+ entry: makeEntry({
+ kind: 'EVENT',
+ derived: false,
+ type: 'PERSONAL',
+ eventId: EVENT_ID,
+ title: 'Auswanderung',
+ senderName: '',
+ receiverName: '',
+ documentId: undefined
+ })
+ });
+ const edit = document.querySelector('[data-testid="event-edit"]') as HTMLAnchorElement | null;
+ expect(edit).not.toBeNull();
+ expect(edit?.getAttribute('href')).toContain(EVENT_ID);
+ });
+
+ it('shows no edit affordance when eventId is null (REQ-008)', () => {
+ render(EventPill, {
+ entry: makeEntry({
+ kind: 'EVENT',
+ derived: false,
+ type: 'PERSONAL',
+ eventId: undefined,
+ title: 'Auswanderung',
+ senderName: '',
+ receiverName: '',
+ documentId: undefined
+ })
+ });
+ expect(document.querySelector('[data-testid="event-edit"]')).toBeNull();
+ });
+
+ it('shows no edit affordance for a derived event (REQ-008)', () => {
+ render(EventPill, { entry: derived('MARRIAGE', 'Heirat') });
+ expect(document.querySelector('[data-testid="event-edit"]')).toBeNull();
+ });
+});