diff --git a/frontend/src/lib/timeline/EventNote.svelte b/frontend/src/lib/timeline/EventNote.svelte
new file mode 100644
index 00000000..96672d78
--- /dev/null
+++ b/frontend/src/lib/timeline/EventNote.svelte
@@ -0,0 +1,47 @@
+
+
+{#if hasContent}
+
+
+ {description}
+
+ {#if clamped || expanded}
+
+ {/if}
+
+{/if}
diff --git a/frontend/src/lib/timeline/event-note.svelte.spec.ts b/frontend/src/lib/timeline/event-note.svelte.spec.ts
new file mode 100644
index 00000000..80124e47
--- /dev/null
+++ b/frontend/src/lib/timeline/event-note.svelte.spec.ts
@@ -0,0 +1,74 @@
+import { describe, it, expect, afterEach } from 'vitest';
+import { cleanup, render } from 'vitest-browser-svelte';
+import { tick } from 'svelte';
+import * as m from '$lib/paraglide/messages.js';
+import EventNote from './EventNote.svelte';
+
+afterEach(() => cleanup());
+
+const LONG_NOTE = Array.from({ length: 15 }, (_, i) => `Zeile ${i + 1}`).join('\n');
+
+describe('EventNote (REQ-002–008)', () => {
+ it('escapesHtml — renders XSS payload as inert text, no injected element (REQ-002)', () => {
+ render(EventNote, { description: '' });
+ // The literal string should appear as text content
+ expect(document.body.textContent).toContain('');
+ // No injected