diff --git a/frontend/src/lib/timeline/BucketHeaderChip.svelte b/frontend/src/lib/timeline/BucketHeaderChip.svelte
new file mode 100644
index 00000000..9d12cab4
--- /dev/null
+++ b/frontend/src/lib/timeline/BucketHeaderChip.svelte
@@ -0,0 +1,59 @@
+
+
+
+ {m.timeline_tag_chip_label()}:
+
+ {name}
+
diff --git a/frontend/src/lib/timeline/BucketHeaderChip.svelte.spec.ts b/frontend/src/lib/timeline/BucketHeaderChip.svelte.spec.ts
new file mode 100644
index 00000000..61c926ad
--- /dev/null
+++ b/frontend/src/lib/timeline/BucketHeaderChip.svelte.spec.ts
@@ -0,0 +1,44 @@
+import { describe, it, expect, afterEach } from 'vitest';
+import { cleanup, render } from 'vitest-browser-svelte';
+import * as m from '$lib/paraglide/messages.js';
+import BucketHeaderChip from './BucketHeaderChip.svelte';
+
+afterEach(() => cleanup());
+
+describe('BucketHeaderChip (REQ-015/009)', () => {
+ it('renders the root-tag name', () => {
+ render(BucketHeaderChip, { name: 'Krieg', color: 'sienna' });
+ expect(document.body.textContent).toContain('Krieg');
+ });
+
+ it('tints the chip with var(--c-tag-{token}) for a known colour token (REQ-015)', () => {
+ render(BucketHeaderChip, { name: 'Krieg', color: 'sienna' });
+ const chip = document.querySelector('[data-testid="bucket-header-chip"]') as HTMLElement;
+ expect(chip.getAttribute('style')).toContain('var(--c-tag-sienna)');
+ });
+
+ it('renders a neutral chip with no --c-tag- binding when colour is null (REQ-015)', () => {
+ render(BucketHeaderChip, { name: 'Ohne Thema', color: null });
+ expect(document.body.textContent).toContain('Ohne Thema');
+ expect(document.body.innerHTML).not.toContain('var(--c-tag-');
+ });
+
+ it('falls back to neutral for an unknown colour token, never a broken var (REQ-015)', () => {
+ // "krieg" is a §2 demo class name, not a real --c-tag-* token.
+ render(BucketHeaderChip, { name: 'Krieg', color: 'krieg' });
+ expect(document.body.innerHTML).not.toContain('var(--c-tag-');
+ });
+
+ it('prefixes the name with an sr-only theme label so colour is never the only cue', () => {
+ render(BucketHeaderChip, { name: 'Krieg', color: 'sienna' });
+ const srOnly = document.querySelector('.sr-only');
+ expect(srOnly?.textContent).toContain(m.timeline_tag_chip_label());
+ });
+
+ it('renders an HTML-bearing name as inert text, never markup (REQ-009)', () => {
+ const evil = '
';
+ render(BucketHeaderChip, { name: evil, color: null });
+ expect(document.body.textContent).toContain(evil);
+ expect(document.querySelector('img')).toBeNull();
+ });
+});