diff --git a/frontend/src/lib/shared/primitives/Card.svelte b/frontend/src/lib/shared/primitives/Card.svelte
new file mode 100644
index 00000000..7ff0fe39
--- /dev/null
+++ b/frontend/src/lib/shared/primitives/Card.svelte
@@ -0,0 +1,84 @@
+
+
+
+ {#if caption}
+
+
+ {caption}
+
+ {/if}
+
+ {#if children}
+ {@render children()}
+ {/if}
+
diff --git a/frontend/src/lib/shared/primitives/Card.svelte.spec.ts b/frontend/src/lib/shared/primitives/Card.svelte.spec.ts
new file mode 100644
index 00000000..d1ca81c1
--- /dev/null
+++ b/frontend/src/lib/shared/primitives/Card.svelte.spec.ts
@@ -0,0 +1,199 @@
+/**
+ * Card.svelte.spec.ts
+ *
+ * RED-first: written before Card.svelte exists.
+ * Tests all three accent variants, padding values, radius, section-caption helper,
+ * fallback for invalid accent props, and dark-mode token correctness.
+ */
+import { afterEach, describe, expect, it } from 'vitest';
+import { cleanup, render } from 'vitest-browser-svelte';
+import { createRawSnippet } from 'svelte';
+import { page } from 'vitest/browser';
+import Card from './Card.svelte';
+
+afterEach(() => cleanup());
+
+describe('Card', () => {
+ // ── Rendering ──────────────────────────────────────────────────────────────
+
+ it('renders children via snippet slot', async () => {
+ const children = createRawSnippet(() => ({
+ render: () => `Archival content`,
+ setup: () => {}
+ }));
+ render(Card, { props: { children } });
+ await expect.element(page.getByText('Archival content')).toBeInTheDocument();
+ });
+
+ it('has data-testid="card" on the root element', async () => {
+ render(Card);
+ await expect.element(page.getByTestId('card')).toBeInTheDocument();
+ });
+
+ // ── Base classes ───────────────────────────────────────────────────────────
+
+ it('has bg-surface token class', async () => {
+ render(Card);
+ const el = document.querySelector('[data-testid="card"]');
+ expect(el?.className).toContain('bg-surface');
+ });
+
+ it('has border-line token class', async () => {
+ render(Card);
+ const el = document.querySelector('[data-testid="card"]');
+ expect(el?.className).toContain('border-line');
+ });
+
+ it('has shadow-sm class', async () => {
+ render(Card);
+ const el = document.querySelector('[data-testid="card"]');
+ expect(el?.className).toContain('shadow-sm');
+ });
+
+ it('has rounded-sm class (2px radius)', async () => {
+ render(Card);
+ const el = document.querySelector('[data-testid="card"]');
+ expect(el?.className).toContain('rounded-sm');
+ });
+
+ // ── Accent: top (default) ─────────────────────────────────────────────────
+
+ it('renders "top" accent variant by default', async () => {
+ render(Card);
+ const el = document.querySelector('[data-testid="card"]') as HTMLElement;
+ expect(el?.dataset.accent).toBe('top');
+ });
+
+ it('applies top accent border-top style via var(--c-accent)', async () => {
+ render(Card, { props: { accent: 'top' } });
+ const el = document.querySelector('[data-testid="card"]') as HTMLElement;
+ const style = el?.getAttribute('style') ?? '';
+ // The top accent is delivered as an inline style using var(--c-accent)
+ expect(style).toContain('var(--c-accent)');
+ expect(style).toContain('border-top');
+ });
+
+ // ── Accent: left ──────────────────────────────────────────────────────────
+
+ it('renders "left" accent variant correctly', async () => {
+ render(Card, { props: { accent: 'left' } });
+ const el = document.querySelector('[data-testid="card"]') as HTMLElement;
+ expect(el?.dataset.accent).toBe('left');
+ });
+
+ it('applies left accent border-left style via var(--c-accent)', async () => {
+ render(Card, { props: { accent: 'left' } });
+ const el = document.querySelector('[data-testid="card"]') as HTMLElement;
+ const style = el?.getAttribute('style') ?? '';
+ expect(style).toContain('var(--c-accent)');
+ expect(style).toContain('border-left');
+ });
+
+ // ── Accent: none ──────────────────────────────────────────────────────────
+
+ it('renders "none" accent variant correctly', async () => {
+ render(Card, { props: { accent: 'none' } });
+ const el = document.querySelector('[data-testid="card"]') as HTMLElement;
+ expect(el?.dataset.accent).toBe('none');
+ });
+
+ it('does NOT apply accent inline style when accent="none"', async () => {
+ render(Card, { props: { accent: 'none' } });
+ const el = document.querySelector('[data-testid="card"]') as HTMLElement;
+ const style = el?.getAttribute('style') ?? '';
+ // No border-top or border-left with var(--c-accent) when accent is none
+ expect(style).not.toContain('var(--c-accent)');
+ });
+
+ // ── Fallback for invalid accent ────────────────────────────────────────────
+
+ it('falls back to "top" for an unknown accent value', async () => {
+ // @ts-expect-error — intentionally passing invalid prop to test runtime fallback
+ render(Card, { props: { accent: 'invalid-value' } });
+ const el = document.querySelector('[data-testid="card"]') as HTMLElement;
+ expect(el?.dataset.accent).toBe('top');
+ });
+
+ // ── Padding ───────────────────────────────────────────────────────────────
+
+ it('defaults to padding="md" (24px)', async () => {
+ render(Card);
+ const el = document.querySelector('[data-testid="card"]') as HTMLElement;
+ expect(el?.dataset.padding).toBe('md');
+ });
+
+ it('applies p-6 (24px) class for padding="md"', async () => {
+ render(Card, { props: { padding: 'md' } });
+ const el = document.querySelector('[data-testid="card"]') as HTMLElement;
+ expect(el?.className).toContain('p-6');
+ });
+
+ it('applies p-5 (20px) class for padding="sm"', async () => {
+ render(Card, { props: { padding: 'sm' } });
+ const el = document.querySelector('[data-testid="card"]') as HTMLElement;
+ expect(el?.className).toContain('p-5');
+ });
+
+ // ── Section-caption helper ─────────────────────────────────────────────────
+
+ it('does NOT render a caption element when caption prop is absent', async () => {
+ render(Card);
+ const caption = document.querySelector('[data-testid="card-caption"]');
+ expect(caption).toBeNull();
+ });
+
+ it('renders the section-caption helper when caption text is provided', async () => {
+ render(Card, { props: { caption: 'Briefkorrespondenz' } });
+ await expect.element(page.getByTestId('card-caption')).toBeInTheDocument();
+ });
+
+ it('caption has font-sans Montserrat token class', async () => {
+ render(Card, { props: { caption: 'Dokumente' } });
+ const el = document.querySelector('[data-testid="card-caption"]') as HTMLElement;
+ expect(el?.className).toContain('font-sans');
+ });
+
+ it('caption has text-ink-3 token class', async () => {
+ render(Card, { props: { caption: 'Personen' } });
+ const el = document.querySelector('[data-testid="card-caption"]') as HTMLElement;
+ expect(el?.className).toContain('text-ink-3');
+ });
+
+ it('caption has uppercase class', async () => {
+ render(Card, { props: { caption: 'Übersicht' } });
+ const el = document.querySelector('[data-testid="card-caption"]') as HTMLElement;
+ expect(el?.className).toContain('uppercase');
+ });
+
+ it('caption has font-bold class (700 weight)', async () => {
+ render(Card, { props: { caption: 'Briefwechsel' } });
+ const el = document.querySelector('[data-testid="card-caption"]') as HTMLElement;
+ expect(el?.className).toContain('font-bold');
+ });
+
+ it('renders caption text content', async () => {
+ render(Card, { props: { caption: 'Zeitstrahl' } });
+ await expect.element(page.getByText('Zeitstrahl')).toBeInTheDocument();
+ });
+
+ // ── Dark-mode token contract ───────────────────────────────────────────────
+
+ it('accent uses var(--c-accent) token — never raw hex — for dark-mode compatibility', async () => {
+ render(Card, { props: { accent: 'top' } });
+ const el = document.querySelector('[data-testid="card"]') as HTMLElement;
+ const style = el?.getAttribute('style') ?? '';
+ // Must use the CSS variable, not any hardcoded hex color
+ expect(style).toContain('var(--c-accent)');
+ expect(style).not.toMatch(/#[0-9a-fA-F]{3,6}/);
+ });
+
+ it('no raw Tailwind color class (e.g. green-*, blue-*) on card element', async () => {
+ render(Card, { props: { accent: 'top' } });
+ const el = document.querySelector('[data-testid="card"]') as HTMLElement;
+ const cls = el?.className ?? '';
+ // Check for raw Tailwind palette colors (bg-green-*, border-blue-*, etc.)
+ expect(cls).not.toMatch(
+ /\b(bg|border|text)-(red|green|blue|yellow|purple|pink|indigo|gray|slate|zinc|stone|orange|amber|lime|emerald|teal|cyan|sky|violet|fuchsia|rose)-\d+/
+ );
+ });
+});