diff --git a/PRODUCT.md b/PRODUCT.md
index 689de6c5b..36f74456d 100644
--- a/PRODUCT.md
+++ b/PRODUCT.md
@@ -78,11 +78,14 @@ no hosted runtime to sell.
- Voice: quiet, dense, factual. Terminal vocabulary, no marketing superlatives,
no fabricated transcripts or reasoning traces.
- "It doesn't need to look special — it needs to look like Codewhale."
-- Assets: the founder's whale mark traced to `brand/mark.svg` (with
- `brand/mark-navy.svg`, `brand/mark-gradient.svg`) and the founder's rounded
- monoline wordmark traced from `brand/wordmark0901.png` (`brand/wordmark.svg`
- navy #142352, `brand/wordmark-inverted.svg` white; regenerated with the site
- icons by `scripts/brand/trace-brand.py`). Web copies live in
+- Assets: the canonical vector family lives in the CWC repo at
+ `codewhale-apps/packages/brand/svg/` (mark, mark-gradient, mark-mono,
+ mark-reversed, wordmark, wordmark-inverted); `brand/` and
+ `web/public/brand/` carry byte-identical copies — sync from there, never
+ re-trace. The founder's brand sheet `brand/codewhalemarkfinal.png` remains
+ the source `scripts/brand/braille-mark.py` derives TUI launch art from. The
+ earlier local trace and `scripts/brand/trace-brand.py` were retired
+ 2026-09-15 in favor of the canonical family. Web copies live in
`web/public/brand/`.
- Palette, type, shell direction, and the anti-slop rules are recorded in
`DESIGN.md`; the colour tokens are owned by `crates/tui/src/palette/tokens.rs`
diff --git a/brand/mark-gradient.svg b/brand/mark-gradient.svg
index 412eb6c12..412e4c14d 100644
--- a/brand/mark-gradient.svg
+++ b/brand/mark-gradient.svg
@@ -1 +1,9 @@
-
+
diff --git a/brand/mark-mono.svg b/brand/mark-mono.svg
new file mode 100644
index 000000000..f67e8d277
--- /dev/null
+++ b/brand/mark-mono.svg
@@ -0,0 +1,3 @@
+
diff --git a/brand/mark-navy.svg b/brand/mark-navy.svg
deleted file mode 100644
index 4272f677a..000000000
--- a/brand/mark-navy.svg
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/brand/mark-reversed.svg b/brand/mark-reversed.svg
new file mode 100644
index 000000000..90ceae43c
--- /dev/null
+++ b/brand/mark-reversed.svg
@@ -0,0 +1,3 @@
+
diff --git a/brand/mark.svg b/brand/mark.svg
index 75b6a4d86..71d3b534c 100644
--- a/brand/mark.svg
+++ b/brand/mark.svg
@@ -1 +1,3 @@
-
+
diff --git a/brand/wordmark-inverted.svg b/brand/wordmark-inverted.svg
index e53701417..b0bb87e19 100644
--- a/brand/wordmark-inverted.svg
+++ b/brand/wordmark-inverted.svg
@@ -1 +1,3 @@
-
+
diff --git a/brand/wordmark.svg b/brand/wordmark.svg
index 81133ab7e..03d61e895 100644
--- a/brand/wordmark.svg
+++ b/brand/wordmark.svg
@@ -1 +1,3 @@
-
+
diff --git a/brand/wordmark0901.png b/brand/wordmark0901.png
deleted file mode 100644
index 23f403178..000000000
Binary files a/brand/wordmark0901.png and /dev/null differ
diff --git a/brand/wordmarkinverted.png b/brand/wordmarkinverted.png
deleted file mode 100644
index e66c3fb32..000000000
Binary files a/brand/wordmarkinverted.png and /dev/null differ
diff --git a/crates/tui/src/runtime_web/codewhale-192.png b/crates/tui/src/runtime_web/codewhale-192.png
index bfb67a6ff..7513605ae 100644
Binary files a/crates/tui/src/runtime_web/codewhale-192.png and b/crates/tui/src/runtime_web/codewhale-192.png differ
diff --git a/crates/tui/src/tui/views/mod.rs b/crates/tui/src/tui/views/mod.rs
index 08fbf9f43..1a5626d83 100644
--- a/crates/tui/src/tui/views/mod.rs
+++ b/crates/tui/src/tui/views/mod.rs
@@ -10383,7 +10383,7 @@ context_window = 262144
"Enter must open the theme editor"
);
- // ↓ highlights underwater: preview (persist:false), editor stays open.
+ // ↓ highlights shoreline: preview (persist:false), editor stays open.
match key(&mut view, KeyCode::Down) {
ViewAction::Emit(ViewEvent::ConfigUpdated {
key,
@@ -10391,7 +10391,7 @@ context_window = 262144
persist,
}) => {
assert_eq!(key, "theme");
- assert_eq!(value, "underwater");
+ assert_eq!(value, "shoreline");
assert!(!persist, "highlighting must not persist");
}
other => panic!("highlight must preview, got {other:?}"),
@@ -10433,7 +10433,7 @@ context_window = 262144
persist,
}) => {
assert_eq!(key, "theme");
- assert_eq!(value, "underwater");
+ assert_eq!(value, "shoreline");
assert!(persist, "Apply must persist");
}
other => panic!("enter must persist the highlight, got {other:?}"),
@@ -10508,14 +10508,14 @@ context_window = 262144
modifiers: KeyModifiers::NONE,
})
};
- // Choice index 2 is underwater (system, terminal, underwater, …).
+ // Choice index 2 is shoreline (system, terminal, shoreline, …).
let (rect, _) = view
.last_choice_hitboxes
.borrow()
.iter()
.copied()
.find(|(_, idx)| *idx == 2)
- .expect("rendered underwater hitbox");
+ .expect("rendered shoreline hitbox");
match hover(&mut view, rect.x, rect.y) {
ViewAction::Emit(ViewEvent::ConfigUpdated {
key,
@@ -10523,7 +10523,7 @@ context_window = 262144
persist,
}) => {
assert_eq!(key, "theme");
- assert_eq!(value, "underwater");
+ assert_eq!(value, "shoreline");
assert!(!persist, "hover preview must not persist");
}
other => panic!("hover must preview, got {other:?}"),
diff --git a/extensions/vscode/media/codewhale.svg b/extensions/vscode/media/codewhale.svg
index a082e2bc5..f67e8d277 100644
--- a/extensions/vscode/media/codewhale.svg
+++ b/extensions/vscode/media/codewhale.svg
@@ -1,11 +1,3 @@
-
diff --git a/scripts/brand/trace-brand.py b/scripts/brand/trace-brand.py
deleted file mode 100644
index 054f3d5c9..000000000
--- a/scripts/brand/trace-brand.py
+++ /dev/null
@@ -1,233 +0,0 @@
-#!/usr/bin/env python3
-"""Re-derive every brand surface from the founder's artwork in brand/.
-
- python3 scripts/brand/trace-brand.py # rewrite every output
- python3 scripts/brand/trace-brand.py --check # exit 1 if any output drifted
-
-Founder sources (committed, never regenerated):
-
- brand/codewhalemarkfinal.png 1254x1254 sheet; hero mark top centre
- brand/wordmark0901.png "codewhale" wordmark, navy on white, 2172x724
- brand/wordmarkinverted.png the same wordmark, white on navy, 2508x627
-
-`brand/mark.svg` is the kept trace of the sheet's hero mark (viewBox 0 0 512
-512, one evenodd path, fill currentColor). It is the source of truth for the
-mark: this script does not re-trace it, it derives every other mark surface
-from it. The wordmark is traced from `wordmark0901.png` every run:
-
- magick brand/wordmark0901.png -colorspace Gray -threshold 60% -trim +repage wm.pbm
- potrace wm.pbm -s --flat -t 20 -O 0.4 -a 1.2 -o wm-raw.svg
-
-potrace's `translate(0,H) scale(0.1,-0.1)` group is folded into the
-coordinates so the result is one compact path in a tight pixel viewBox
-(1874x264 for the 0901 render; the wordmark is ~7.1:1).
-
-`wordmarkinverted.png` traces just as cleanly (negate first, threshold 40%),
-but its render is set slightly tighter (2172x310 after trim, ~7.0:1), so the
-0901 render is the one path used for both colourways: geometry that differs
-between the light and dark wordmark is a second wordmark.
-
-Outputs:
-
- brand/wordmark.svg #142352 on transparent
- brand/wordmark-inverted.svg #FFFFFF on transparent, same path
- brand/mark-navy.svg mark.svg path, #142352
- brand/mark-gradient.svg mark.svg path, cobalt->action gradient
- web/public/brand/{mark,mark-gradient,wordmark,wordmark-inverted}.svg
- web/app/icon.svg white mark on the #142352 rounded tile
- web/app/favicon.ico 16/32/48 of the tile
- web/app/apple-icon.png 180, opaque tile
- web/public/icon-192.png, icon-512.png
-
-`web/components/whale.tsx` inlines the mark path; the script checks it still
-matches `brand/mark.svg` rather than rewriting a React component.
-
-Needs ImageMagick 7 (`magick`) and potrace on PATH. No network.
-"""
-
-from __future__ import annotations
-
-import re
-import shutil
-import subprocess
-import sys
-import tempfile
-from pathlib import Path
-
-ROOT = Path(__file__).resolve().parents[2]
-BRAND = ROOT / "brand"
-WEB = ROOT / "web"
-
-NAVY = "#142352"
-WHITE = "#FFFFFF"
-# Gradient stops are the TUI's WHALE_COBALT -> WHALE_ACTION.
-GRADIENT = ("#1535B2", "#6AA6DC")
-
-# The mark in the 512 tile: the sheet's app icon sets the whale at ~2/3 of
-# the tile width, so scale the 512 mark by 0.72 and centre its content box
-# (x 16.9..494.3, y 46.7..471.1) on 256,256.
-TILE_SCALE = 0.72
-TILE_TRANSLATE = (72, 70)
-TILE_RADIUS = 102.4 # 20% of 512, the sheet's corner
-
-THRESHOLD = "60%"
-POTRACE = ["potrace", "-s", "--flat", "-t", "20", "-O", "0.4", "-a", "1.2"]
-
-
-def run(*args: str) -> None:
- subprocess.run(args, check=True)
-
-
-def number(value: float) -> str:
- text = f"{value:.1f}"
- return text[:-2] if text.endswith(".0") else text
-
-
-def fold_potrace(svg: str) -> tuple[str, float, float]:
- """Return (path d, width, height) with potrace's group transform applied."""
- width = float(re.search(r'width="([\d.]+)pt"', svg).group(1))
- height = float(re.search(r'height="([\d.]+)pt"', svg).group(1))
- tx, ty, sx, sy = (
- float(v)
- for v in re.search(
- r"translate\(([-\d.]+),([-\d.]+)\) scale\(([-\d.]+),([-\d.]+)\)", svg
- ).groups()
- )
- d = " ".join(re.findall(r' tuple[str, float, float]:
- pbm = tmp / "wm.pbm"
- raw = tmp / "wm-raw.svg"
- run(
- "magick", str(png), "-colorspace", "Gray", "-threshold", THRESHOLD,
- "-trim", "+repage", str(pbm),
- )
- run(*POTRACE, str(pbm), "-o", str(raw))
- return fold_potrace(raw.read_text())
-
-
-def wordmark_svg(d: str, width: float, height: float, fill: str) -> str:
- return (
- f'\n'
- )
-
-
-def mark_path() -> str:
- return re.search(r' d="([^"]+)"', (BRAND / "mark.svg").read_text()).group(1)
-
-
-def mark_svg(fill: str, defs: str = "") -> str:
- return (
- '\n'
- )
-
-
-def icon_svg() -> str:
- tx, ty = TILE_TRANSLATE
- return (
- '\n'
- )
-
-
-def rasters(icon: Path) -> None:
- run(
- "magick", "-background", "none", str(icon),
- "-define", "icon:auto-resize=48,32,16", str(WEB / "app/favicon.ico"),
- )
- run(
- "magick", "-background", NAVY, str(icon), "-resize", "180x180",
- "-alpha", "off", "-depth", "8", str(WEB / "app/apple-icon.png"),
- )
- for size in (192, 512):
- run(
- "magick", "-background", "none", str(icon), "-resize", f"{size}x{size}",
- "-depth", "8", str(WEB / f"public/icon-{size}.png"),
- )
-
-
-def main() -> int:
- check = "--check" in sys.argv[1:]
- gradient = (
- ''
- f''
- f''
- )
- with tempfile.TemporaryDirectory() as tmpdir:
- tmp = Path(tmpdir)
- d, width, height = trace_wordmark(BRAND / "wordmark0901.png", tmp)
- texts = {
- BRAND / "wordmark.svg": wordmark_svg(d, width, height, NAVY),
- BRAND / "wordmark-inverted.svg": wordmark_svg(d, width, height, WHITE),
- BRAND / "mark-navy.svg": mark_svg(NAVY),
- BRAND / "mark-gradient.svg": mark_svg("url(#mark-gradient)", gradient),
- WEB / "app/icon.svg": icon_svg(),
- }
- texts[WEB / "public/brand/mark.svg"] = (BRAND / "mark.svg").read_text()
- for name in ("mark-gradient", "wordmark", "wordmark-inverted"):
- texts[WEB / "public/brand" / f"{name}.svg"] = texts[BRAND / f"{name}.svg"]
-
- whale = (WEB / "components/whale.tsx").read_text()
- inline = re.search(r'WHALE_MARK =\s*"([^"]+)"', whale).group(1)
- drift = [] if inline == mark_path() else ["web/components/whale.tsx"]
-
- if check:
- drift += [
- str(path.relative_to(ROOT))
- for path, text in texts.items()
- if not path.exists() or path.read_text() != text
- ]
- for name in drift:
- print(f"drift: {name}", file=sys.stderr)
- return 1 if drift else 0
-
- for path, text in texts.items():
- path.write_text(text)
- print(f"wrote {path.relative_to(ROOT)}")
- rasters(WEB / "app/icon.svg")
- print("wrote web/app/favicon.ico apple-icon.png public/icon-192.png icon-512.png")
- for name in drift:
- print(f"drift: {name} no longer matches brand/mark.svg", file=sys.stderr)
- return 1 if drift else 0
-
-
-if __name__ == "__main__":
- if not shutil.which("magick") or not shutil.which("potrace"):
- sys.exit("needs `magick` (ImageMagick 7) and `potrace` on PATH")
- sys.exit(main())
diff --git a/web/app/opengraph-image.tsx b/web/app/opengraph-image.tsx
index 57f058cd9..94715aa88 100644
--- a/web/app/opengraph-image.tsx
+++ b/web/app/opengraph-image.tsx
@@ -7,8 +7,8 @@ export const alt = OG_ALT;
export const size = { width: 1200, height: 630 };
export const contentType = "image/png";
-const mark = readFile(join(process.cwd(), "public/brand/mark.svg")).then((svg) =>
- svg.toString().replace("currentColor", "#ffffff"),
+const mark = readFile(join(process.cwd(), "public/brand/mark-reversed.svg")).then((svg) =>
+ svg.toString(),
);
const wordmark = readFile(join(process.cwd(), "public/brand/wordmark-inverted.svg")).then(
(svg) => svg.toString(),
@@ -37,8 +37,8 @@ export default async function OpengraphImage() {
}}
>
- {/* The traced wordmark is 1874x264 (~7.1:1). */}
-
+ {/* The family wordmark is 1024x160 (6.4:1). */}
+
{IDENTITY_PHRASE}
diff --git a/web/lib/public-auth-routes.ts b/web/lib/public-auth-routes.ts
index 97d45e754..dbe4f9b2d 100644
--- a/web/lib/public-auth-routes.ts
+++ b/web/lib/public-auth-routes.ts
@@ -14,8 +14,9 @@ import { pathLocale } from "./i18n/path";
/**
* App-icon raster (1254×1254) used on public account-entry pages: the canonical
* mark in the current brand treatment — blue gradient glyph on a white rounded
- * tile, generated from repo-root-relative `web/public/brand/mark.svg`. The vector is
- * the master — never hand-edit this PNG, regenerate it. Pinned by hash so a
+ * tile. The master vectors are the canonical family in
+ * `codewhale-apps/packages/brand/svg/` (byte copies in `web/public/brand/`) —
+ * never hand-edit this PNG, regenerate it from the family. Pinned by hash so a
* redrawn or stale substitute fails the test rather than shipping.
*/
export const CANONICAL_MARK_SRC = "/brand/codewhale-mark.png";
diff --git a/web/public/brand/mark-gradient.svg b/web/public/brand/mark-gradient.svg
index e7458e956..412e4c14d 100644
--- a/web/public/brand/mark-gradient.svg
+++ b/web/public/brand/mark-gradient.svg
@@ -1 +1,9 @@
-
+
diff --git a/web/public/brand/mark-mono.svg b/web/public/brand/mark-mono.svg
new file mode 100644
index 000000000..f67e8d277
--- /dev/null
+++ b/web/public/brand/mark-mono.svg
@@ -0,0 +1,3 @@
+
diff --git a/web/public/brand/mark-reversed.svg b/web/public/brand/mark-reversed.svg
new file mode 100644
index 000000000..90ceae43c
--- /dev/null
+++ b/web/public/brand/mark-reversed.svg
@@ -0,0 +1,3 @@
+
diff --git a/web/public/brand/mark.svg b/web/public/brand/mark.svg
index 75b6a4d86..71d3b534c 100644
--- a/web/public/brand/mark.svg
+++ b/web/public/brand/mark.svg
@@ -1 +1,3 @@
-
+
diff --git a/web/public/brand/wordmark-inverted.svg b/web/public/brand/wordmark-inverted.svg
index e53701417..b0bb87e19 100644
--- a/web/public/brand/wordmark-inverted.svg
+++ b/web/public/brand/wordmark-inverted.svg
@@ -1 +1,3 @@
-
+
diff --git a/web/public/brand/wordmark.svg b/web/public/brand/wordmark.svg
index 81133ab7e..03d61e895 100644
--- a/web/public/brand/wordmark.svg
+++ b/web/public/brand/wordmark.svg
@@ -1 +1,3 @@
-
+