From 4e115086e6363accb3342d7ddd472ca7ac2d9411 Mon Sep 17 00:00:00 2001 From: Justin Paul Date: Sat, 6 Jun 2026 14:11:59 +0000 Subject: [PATCH] Add brand identity: Origin logo + monogram icon, palette, generator Signed-off-by: Justin Paul --- CLAUDE.md | 11 ++ docs/brand/README.md | 50 +++++++ docs/brand/favicon.svg | 5 + docs/brand/generate.py | 189 +++++++++++++++++++++++++++ docs/brand/provenance-icon.svg | 5 + docs/brand/provenance-logo-plain.svg | 20 +++ docs/brand/provenance-logo.svg | 21 +++ docs/brand/provenance-mark.svg | 19 +++ 8 files changed, 320 insertions(+) create mode 100644 docs/brand/README.md create mode 100644 docs/brand/favicon.svg create mode 100644 docs/brand/generate.py create mode 100644 docs/brand/provenance-icon.svg create mode 100644 docs/brand/provenance-logo-plain.svg create mode 100644 docs/brand/provenance-logo.svg create mode 100644 docs/brand/provenance-mark.svg 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 @@ + +Provenance icon + + + 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''' +Provenance + +{origin_mark(mark_cx, mark_cy, R)} + + + +''' +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''' +Provenance + +{origin_mark(mark_cx, mcy2, R)} + + +''' +open(f"{OUT}/provenance-logo-plain.svg", "w").write(logo2) + +# ---- 2. Mark only (square) ---- +S = 96 +c = S / 2 +markonly = f''' +Provenance mark + +{origin_mark(c, c, 34, conn_sw=1.6, tick_sw=2.2)} + +''' +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''' +Provenance icon + + + +''' + +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 @@ + +Provenance icon + + + 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 @@ + +Provenance + + + + + + + + + + + + + + + + + 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 @@ + +Provenance + + + + + + + + + + + + + + + + + + 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 @@ + +Provenance mark + + + + + + + + + + + + + + + +