diff --git a/CLAUDE.md b/CLAUDE.md
index 3e07527..5dbc2ef 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -68,6 +68,17 @@ Don't get ahead of the phases. GEDCOM lands before the assistant (so AI writes t
Provenance is **source-available** under **BUSL-1.1** (see [LICENSE](LICENSE)): free for personal/family/non-commercial use, no third-party commercial hosting, and each release converts to **AGPL-3.0** four years after it ships. The DCO sign-off keeps the licensing chain clean so the maintainer can manage that conversion and a possible future hosted offering. Don't add code under an incompatible license, and don't vendor dependencies whose licenses conflict with eventual AGPL distribution.
+## Brand
+
+Visual identity lives in [docs/brand/](docs/brand/) (see its README for full guidance). Use these as the frontend's design tokens:
+
+- **Ink** (primary text/marks): `#1A1A17` light / `#F2EEE6` dark
+- **Bronze** (accent, constant): `#A06A42`
+- **Paper** (knockout on bronze, constant): `#F7F3EC`
+- **Muted** (secondary text): `#6B6862` light / `#9A968E` dark
+
+Wordmark is a serif (heritage register); UI body/secondary text is a humanist sans. Logo lockup: `docs/brand/provenance-logo.svg`; app icon/favicon: `docs/brand/provenance-icon.svg` and `favicon.svg`. Don't recolor outside the palette or add gradients/shadows — the look is flat and warm.
+
## Owner & contact
Maintainer: **Justin Paul** (`justin@jpaul.io`). This deployment targets a home lab: Authentik at `auth.jpaul.io` for auth, `mail.jpaul.io` for SMTP, behind Caddy + Cloudflare Tunnel.
diff --git a/docs/brand/README.md b/docs/brand/README.md
new file mode 100644
index 0000000..92116f5
--- /dev/null
+++ b/docs/brand/README.md
@@ -0,0 +1,50 @@
+# Provenance — Brand
+
+Draft v0.1. The visual identity for Provenance: an **Origin mark** (primary logo) and a **monogram tile** (app icon / favicon), in a warm ink-and-bronze palette.
+
+## Concept
+
+The mark is a survey-datum / origin point: a ring with cardinal ticks (a surveyor's monument — the **land / property** side of the product) enclosing four nodes connected to a center point (a **family graph** — the **people** side). One mark for both halves of what Provenance does. The bronze echoes survey-marker disks and aged document seals — heritage without sepia cliché.
+
+## Palette
+
+| Role | Light | Dark | Hex notes |
+|------|-------|------|-----------|
+| Ink (primary text/marks) | `#1A1A17` | `#F2EEE6` | warm near-black / warm off-white |
+| Bronze (accent) | `#A06A42` | `#A06A42` | constant in both modes |
+| Paper (knockout on bronze) | `#F7F3EC` | `#F7F3EC` | constant |
+| Muted (tagline/secondary) | `#6B6862` | `#9A968E` | |
+
+The SVG assets carry an embedded `prefers-color-scheme` rule, so ink and muted tones auto-adapt to light/dark; bronze and paper are intentionally fixed (bronze is mid-tone and reads on both).
+
+## Typography
+
+- **Wordmark:** a refined transitional **serif** (the heritage/archival register). The wordmark in these assets is **outlined to vector paths**, so the files render identically everywhere with no font dependency. For production UI headings, pair with a comparable licensed serif (e.g. a Times/Garamond-class face).
+- **Tagline & secondary:** a clean humanist/grotesque **sans**.
+- **Tagline:** *where it came from matters* — sentence case, never title case.
+
+## Assets
+
+| File | Use |
+|------|-----|
+| `provenance-logo.svg` | Primary horizontal lockup — mark + wordmark + tagline |
+| `provenance-logo-plain.svg` | Lockup without tagline (tight spaces, headers) |
+| `provenance-mark.svg` | Mark only — square, for avatars, small placements, loading states |
+| `provenance-icon.svg` | App icon — 512px bronze monogram tile |
+| `favicon.svg` | Favicon — 48px monogram tile |
+| `generate.py` | Reproducible generator for all of the above |
+
+## Usage notes
+
+- **Clear space:** keep padding around the lockup at least the height of the mark's center-to-tick distance (≈ the ring radius). Don't crowd it.
+- **Minimum size:** the full lockup stays legible down to ~140px wide; below that, use the mark or the icon.
+- **Don't:** recolor outside the palette, add gradients/shadows, stretch, or rotate. Don't put the ink lockup on a busy or mid-tone background where it loses contrast — use the icon tile instead.
+- **Backgrounds:** the lockup and mark are transparent and adapt to light/dark. The icon tile supplies its own bronze background.
+
+## Regenerating
+
+```sh
+python3 docs/brand/generate.py
+```
+
+Requires `matplotlib` and the Liberation Serif/Sans fonts (or edit the font paths at the top of the script). Outputs all SVGs into this directory.
diff --git a/docs/brand/favicon.svg b/docs/brand/favicon.svg
new file mode 100644
index 0000000..30e656c
--- /dev/null
+++ b/docs/brand/favicon.svg
@@ -0,0 +1,5 @@
+
diff --git a/docs/brand/generate.py b/docs/brand/generate.py
new file mode 100644
index 0000000..7cab4f3
--- /dev/null
+++ b/docs/brand/generate.py
@@ -0,0 +1,189 @@
+#!/usr/bin/env python3
+"""Generate Provenance brand assets as portable, font-independent SVGs."""
+import os
+from matplotlib.textpath import TextPath
+from matplotlib.font_manager import FontProperties
+from matplotlib.path import Path
+
+SERIF = "/usr/share/fonts/truetype/liberation/LiberationSerif-Regular.ttf"
+SANS = "/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf"
+
+INK = "#1A1A17"
+INK_DARK = "#F2EEE6"
+BRONZE = "#A06A42"
+PAPER = "#F7F3EC"
+MUTED = "#6B6862"
+MUTED_DARK = "#9A968E"
+
+OUT = os.path.join(os.path.dirname(os.path.abspath(__file__)))
+os.makedirs(OUT, exist_ok=True)
+
+
+def text_to_path(s, font_path, size):
+ """Return (d_string, width, height). Coordinates: top-left origin, y down."""
+ fp = FontProperties(fname=font_path)
+ tp = TextPath((0, 0), s, size=size, prop=fp)
+ verts, codes = tp.vertices, tp.codes
+ xs = verts[:, 0]
+ ys = verts[:, 1]
+ xmin, xmax = xs.min(), xs.max()
+ ymin, ymax = ys.min(), ys.max()
+
+ def tx(x):
+ return round(float(x - xmin), 2)
+
+ def ty(y):
+ return round(float(ymax - y), 2)
+
+ d = []
+ i = 0
+ n = len(codes)
+ while i < n:
+ c = codes[i]
+ if c == Path.MOVETO:
+ x, y = verts[i]
+ d.append(f"M{tx(x)} {ty(y)}")
+ i += 1
+ elif c == Path.LINETO:
+ x, y = verts[i]
+ d.append(f"L{tx(x)} {ty(y)}")
+ i += 1
+ elif c == Path.CURVE3:
+ x1, y1 = verts[i]
+ x2, y2 = verts[i + 1]
+ d.append(f"Q{tx(x1)} {ty(y1)} {tx(x2)} {ty(y2)}")
+ i += 2
+ elif c == Path.CURVE4:
+ x1, y1 = verts[i]
+ x2, y2 = verts[i + 1]
+ x3, y3 = verts[i + 2]
+ d.append(f"C{tx(x1)} {ty(y1)} {tx(x2)} {ty(y2)} {tx(x3)} {ty(y3)}")
+ i += 3
+ elif c == Path.CLOSEPOLY:
+ d.append("Z")
+ i += 1
+ else:
+ i += 1
+ return "".join(d), round(float(xmax - xmin), 2), round(float(ymax - ymin), 2)
+
+
+def origin_mark(cx, cy, R, conn_sw=1.5, tick_sw=2):
+ """Origin/survey-datum mark primitives as an SVG fragment string.
+ Ink elements get class='ink'; bronze elements class='br'."""
+ import math
+ diag = R * 0.7071
+ nodes = [(cx + diag, cy - diag), (cx - diag, cy - diag),
+ (cx + diag, cy + diag), (cx - diag, cy + diag)]
+ parts = []
+ # ring (bronze)
+ parts.append(f'')
+ # cardinal ticks (bronze) crossing the ring
+ t_in, t_out = R - 4, R + 4
+ for (dx, dy) in [(0, -1), (0, 1), (1, 0), (-1, 0)]:
+ x1, y1 = cx + dx * t_in, cy + dy * t_in
+ x2, y2 = cx + dx * t_out, cy + dy * t_out
+ parts.append(f'')
+ # connectors (ink) center -> diagonal nodes
+ for (nx, ny) in nodes:
+ parts.append(f'')
+ # diagonal nodes (ink)
+ nr = max(2.0, R * 0.105)
+ for (nx, ny) in nodes:
+ parts.append(f'')
+ # center dot (ink)
+ parts.append(f'')
+ return "\n".join(parts)
+
+
+STYLE = f""".ink{{fill:{INK}}}.ink-s{{stroke:{INK}}}.br{{fill:{BRONZE}}}.br-s{{stroke:{BRONZE}}}.muted{{fill:{MUTED}}}
+@media (prefers-color-scheme:dark){{.ink{{fill:{INK_DARK}}}.ink-s{{stroke:{INK_DARK}}}.muted{{fill:{MUTED_DARK}}}}}"""
+
+# ---- 1. Primary horizontal lockup ----
+WM_SIZE = 64
+TAG_SIZE = 15
+wm_d, wm_w, wm_h = text_to_path("Provenance", SERIF, WM_SIZE)
+tag_d, tag_w, tag_h = text_to_path("where it came from matters", SANS, TAG_SIZE)
+
+pad = 28
+R = 30
+mark_box = 2 * (R + 6)
+gap = 26
+block_gap = 12
+text_block_h = wm_h + block_gap + tag_h
+content_h = max(mark_box, text_block_h)
+H = round(pad * 2 + content_h, 2)
+mark_cx = pad + (R + 6)
+mark_cy = round(H / 2, 2)
+text_x = pad + mark_box + gap
+W = round(text_x + max(wm_w, tag_w) + pad, 2)
+# vertically center the text block
+block_top = (H - text_block_h) / 2
+wm_y = round(block_top, 2)
+tag_y = round(block_top + wm_h + block_gap, 2)
+
+logo = f'''
+'''
+open(f"{OUT}/provenance-logo.svg", "w").write(logo)
+
+# ---- 1b. Lockup without tagline ----
+H2 = round(pad * 2 + max(mark_box, wm_h), 2)
+mcy2 = round(H2 / 2, 2)
+wm_y2 = round((H2 - wm_h) / 2, 2)
+W2 = round(text_x + wm_w + pad, 2)
+logo2 = f'''
+'''
+open(f"{OUT}/provenance-logo-plain.svg", "w").write(logo2)
+
+# ---- 2. Mark only (square) ----
+S = 96
+c = S / 2
+markonly = f'''
+'''
+open(f"{OUT}/provenance-mark.svg", "w").write(markonly)
+
+# ---- 3. App icon / monogram tile (square) ----
+def monogram(side, radius_ratio=0.22):
+ p_size = side * 0.62
+ pd, pw, ph = text_to_path("P", SERIF, p_size)
+ px = round((side - pw) / 2, 2)
+ py = round((side - ph) / 2, 2)
+ rx = round(side * radius_ratio, 2)
+ return f'''
+'''
+
+open(f"{OUT}/provenance-icon.svg", "w").write(monogram(512))
+open(f"{OUT}/favicon.svg", "w").write(monogram(48, radius_ratio=0.18))
+
+print("WROTE:")
+for f in sorted(os.listdir(OUT)):
+ p = os.path.join(OUT, f)
+ print(f" {f} ({os.path.getsize(p)} bytes)")
+print(f"\nlogo lockup: {W} x {H}")
+print(f"wordmark path size: w={wm_w} h={wm_h}")
+print(f"tagline path size: w={tag_w} h={tag_h}")
diff --git a/docs/brand/provenance-icon.svg b/docs/brand/provenance-icon.svg
new file mode 100644
index 0000000..7cc781b
--- /dev/null
+++ b/docs/brand/provenance-icon.svg
@@ -0,0 +1,5 @@
+
diff --git a/docs/brand/provenance-logo-plain.svg b/docs/brand/provenance-logo-plain.svg
new file mode 100644
index 0000000..820aefd
--- /dev/null
+++ b/docs/brand/provenance-logo-plain.svg
@@ -0,0 +1,20 @@
+
diff --git a/docs/brand/provenance-logo.svg b/docs/brand/provenance-logo.svg
new file mode 100644
index 0000000..b683aad
--- /dev/null
+++ b/docs/brand/provenance-logo.svg
@@ -0,0 +1,21 @@
+
diff --git a/docs/brand/provenance-mark.svg b/docs/brand/provenance-mark.svg
new file mode 100644
index 0000000..114a504
--- /dev/null
+++ b/docs/brand/provenance-mark.svg
@@ -0,0 +1,19 @@
+