diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index dcbb116..534a575 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -101,3 +101,5 @@ jobs:
$B tinymodel "turn a model into a dict" -C crates/lockdocs-core/tests/fixture | grep -q to_dict
- name: Docs build
run: bun run docs:build
+ - name: Brand home (hashes and surface copies agree)
+ run: python3 brand/build.py --check
diff --git a/PROJECT.md b/PROJECT.md
index bd29682..8c10456 100644
--- a/PROJECT.md
+++ b/PROJECT.md
@@ -20,6 +20,9 @@ MIT licensed.
- `packages/lockdocs`: the npm launcher; `packages/npm/*`: native binaries
- `bench/`: version-sensitive questions, project pins, and the runner
- `docs/`: the VitePress site; `scripts/`: version sync
+- `brand/`: the brand home — vector masters, generated favicons and app icons,
+ colour and type tokens. The site's logo, favicon and theme colours are copies
+ of it. See `brand/README.md`
## Release
diff --git a/brand/README.md b/brand/README.md
new file mode 100644
index 0000000..5ef1a6f
--- /dev/null
+++ b/brand/README.md
@@ -0,0 +1,161 @@
+# lockdocs brand
+
+This folder is the source of truth for the lockdocs mark, its small sizes, its
+app icon, its colours and its type. Every surface copies from it — `brand.json`
+lists which file each surface is a copy of — and nothing draws its own.
+`build.py` rebuilds every derived file from the masters in `svg/`:
+
+```bash
+pip install pillow numpy resvg-py
+python3 brand/build.py # regenerate (favicon, app icons, tokens.css, provenance)
+python3 brand/build.py --resnap # also redraw the 16/32 px grids from the master
+python3 brand/build.py --check # verify hashes and surface copies (CI runs this; stdlib only)
+```
+
+## Name
+
+Running text is `lockdocs`: lower case, one word, everywhere — the README, the
+docs site, the npm package `@sylphx/lockdocs`, the registry entry
+`io.github.SylphxAI/lockdocs`, and the `lockdocs` binary. No document in this
+repository writes `Lockdocs` or `LockDocs`. The only upper-case form is the
+`LOCKDOCS_*` environment-variable prefix, which is not the name.
+
+lockdocs is operated by Sylphx Limited. The operator line is never part of the
+brand: it belongs to the licence, legal pages and the site footer, and the mark
+never carries it.
+
+There are no local-script names: the product ships in English.
+
+The mark is a padlock with no lettering and there is no wordmark, so the name
+never appears inside the logo and has no capitalised form anywhere.
+
+## Files
+
+| Need | File |
+|---|---|
+| The mark, anywhere it is placed (site header, README, slides) | `svg/lockdocs-symbol.svg` |
+| One-colour printing, engraving, embroidery, stamps | `svg/lockdocs-symbol-black.svg`, `svg/lockdocs-symbol-white.svg` |
+| App icon master (store listing, launcher) | `svg/lockdocs-app-icon.svg` |
+| Full-bleed square for masks (Android maskable, Apple touch, vendors that crop) | `svg/lockdocs-maskable.svg` |
+| Browser tab | `favicon/favicon.svg` (32 px pixel grid), `favicon/favicon.ico` (16, 32, 48), `favicon/favicon-16.png`, `favicon/favicon-32.png`, `favicon/favicon-48.png` |
+| Home screen, store listing, future web-app manifest | `app-icon/icon-192.png`, `app-icon/icon-512.png`, `app-icon/icon-1024.png`, `app-icon/icon-maskable-192.png`, `app-icon/icon-maskable-512.png`, `app-icon/apple-touch-icon-180.png` |
+| The website's own logo file (served at `/lockdocs/logo.svg`) | `docs/public/logo.svg` |
+| Colour and type tokens | `tokens.json` (source), `tokens.css` (generated) |
+| Where every file came from, with its SHA-256 | `provenance.json` |
+
+## Colours
+
+| Token | Hex | Used for |
+|---|---|---|
+| `--brand-color-bg` | `#06080C` | Page ground; the app-icon ground; the document lines cut out of the lock body |
+| `--brand-color-bg-raised` | `#0B0E14` | Alternating background: code blocks, sidebar, footer |
+| `--brand-color-bg-elevated` | `#0E1118` | Elevated surfaces: menus, dialogs, hover cards |
+| `--brand-color-bg-soft` | `#11151D` | Soft fills and panels |
+| `--brand-color-accent` | `#7C9CFF` | The lock body; links, badges and primary buttons |
+| `--brand-color-accent-hover` | `#9DB4FF` | Links, badges and primary buttons on hover |
+| `--brand-color-accent-active` | `#6480E8` | Links and buttons when pressed |
+| `--brand-color-mint` | `#42D6A4` | The shackle; second stop of the hero gradient |
+| `--brand-color-gold` | `#FFD166` | Third stop of the hero gradient |
+
+The docs site gets its theme colours from these tokens: `custom.css` imports
+`brand/tokens.css` and assigns `--vp-c-*` from `var(--brand-color-*)`, and
+`docs/.vitepress/config.ts` reads the `theme-color` meta tag from
+`tokens.json`. The values are unchanged from the site that shipped before the
+brand home existed.
+
+## Type
+
+Body and interface are set in Inter (`--brand-font-sans`), served by VitePress
+with the docs theme from the built site's own `/assets` — self-hosted, no font
+CDN — under the SIL Open Font License 1.1. The theme already defaults
+`--vp-font-family-base` to it, so no surface declares its own face. No type
+face is part of the mark, because the mark has no lettering.
+
+## Small sizes
+
+16 px and 32 px are not scaled renders. `build.py` renders the app icon at 8x,
+quantises every sample to the three colours in `brand.json` (`palette`), and
+writes the result as a character grid: `favicon/grid-16.txt` and
+`favicon/grid-32.txt`. A pixel takes the colour most of its samples agree on,
+and stays empty when fewer than `snap_threshold` (0.5) of its samples are
+filled.
+
+The grid files are meant to be hand-edited — a later `build.py` run draws from
+the grid as it stands; only `--resnap` redraws it from the master.
+`favicon/favicon.svg` is the 32 px grid as one path per colour, and
+`favicon/favicon.ico` holds 16 and 32 from the grids plus 48 from a direct
+render. `palette` must list exactly the colours the master uses, or the grids
+pick up colours that are not in the brand.
+
+## Clear space and minimum size
+
+Not yet specified. This repository has no design document, so no clear space or
+minimum size has been recorded. The symbol's viewBox is tight to the artwork:
+add clearance where the mark is placed, not inside the file.
+
+## Do / Don't
+
+Not yet specified beyond what the files themselves fix: the mark is never
+recoloured, so the three accent colours above are the only ones (the one-colour
+files are the exception, for print); and the two document lines stay visible in
+every version, which in the one-colour files means they are cut out of the lock
+body rather than painted over it. The rounded square belongs to the app icon
+alone — for anything that crops or masks, use the full-bleed `lockdocs-maskable`.
+
+## Surfaces
+
+| Surface | Is a copy of |
+|---|---|
+| `docs/public/logo.svg` (site header) | `svg/lockdocs-symbol.svg` |
+| `docs/public/favicon.svg` | `favicon/favicon.svg` |
+| `docs/public/favicon.ico` | `favicon/favicon.ico` |
+
+`python3 brand/build.py --check` fails if any of these stops being a
+byte-for-byte copy. The website files are plain copies under `docs/public/`, so
+a regenerated icon is picked up by the next docs build and deploy; the deploy
+workflow runs on changes under `docs/`.
+
+### Surfaces still to move
+
+- `docs/.vitepress/theme/custom.css` keeps four brand tints as literal
+ `rgba()`: `--vp-c-brand-soft` (`rgba(124, 156, 255, 0.14)`), the hero
+ radial gradient (`rgba(124,156,255,.35)`, `rgba(66,214,164,.2)`) and the
+ hero-shot border and shadow (`rgba(124,156,255,.08)`). They are alpha tints
+ of `--brand-color-accent` and `--brand-color-mint`; writing them as tokens
+ needs `color-mix()`, which drops the whole declaration on browsers older
+ than 2023, so they stay literals.
+- `README.md` badges: the shields.io URLs carry `color=7c9cff` (npm badge) and
+ `42d6a4` (MCP Registry badge). shields.io renders from its own servers and
+ cannot read a local token.
+- Social preview: `docs/.vitepress/config.ts` sets `og:image` to
+ `docs/public/img/demo.gif`, a 1400×780 terminal recording, not a 1200×630
+ still. There is no `og/` master. The audit of 2026-09-28 recorded this as a
+ gap to fill later rather than draw one now.
+
+## Provenance
+
+- `svg/lockdocs-symbol.svg` is `docs/public/logo.svg` moved here; git history
+ for that path starts at `d4c4d54` (2026-09-25, "feat: lockdocs 0.1 —
+ exact-version library docs from your lockfile, offline (#1)") and the drawing
+ has not changed since. The website file is now generated back as a copy.
+- `svg/lockdocs-symbol-black.svg` and `-white.svg` are that drawing in one
+ colour: every fill and stroke set to `#000000` / `#FFFFFF`, with the two
+ document lines cut out of the lock body by `fill-rule="evenodd"` so they stay
+ visible on a black or white ground.
+- `svg/lockdocs-app-icon.svg` was specified by the brand lead in the audit of
+ 2026-09-28: the symbol on a `#06080C` rounded square (`rx` 22% of the side),
+ 1024 viewBox, symbol at 70% width, centred. `svg/lockdocs-maskable.svg` is the
+ same construction without the corner radius, so Apple's and Android's masks
+ have a full-bleed square to work with.
+- `tokens.json` records the colours and the type face the product already
+ shipped: the logo's three colours, the docs theme's background and accent
+ roles, and the theme's Inter stack.
+- Every file's SHA-256 is in `provenance.json`; the entry for each file says
+ where it came from, and `build.py` refreshes the hashes.
+
+## Trademark
+
+Not registered. Owner decision owner#781: no trademark filings before the
+product earns money. Use ™ at most, never ®.
+
+Checked 2026-09-28. **Same name, different category:** LockDocs (lockdocs.com) is a client-document and smart-forms platform for wealth and fund managers, listed on Microsoft AppSource. Not a developer tool. Noted once, not blocking. Mark (a padlock over document lines): padlock marks are common; no close match found among AI docs tools (Context7).
diff --git a/brand/app-icon/apple-touch-icon-180.png b/brand/app-icon/apple-touch-icon-180.png
new file mode 100644
index 0000000..895bb67
Binary files /dev/null and b/brand/app-icon/apple-touch-icon-180.png differ
diff --git a/brand/app-icon/icon-1024.png b/brand/app-icon/icon-1024.png
new file mode 100644
index 0000000..067663a
Binary files /dev/null and b/brand/app-icon/icon-1024.png differ
diff --git a/brand/app-icon/icon-192.png b/brand/app-icon/icon-192.png
new file mode 100644
index 0000000..040aff1
Binary files /dev/null and b/brand/app-icon/icon-192.png differ
diff --git a/brand/app-icon/icon-512.png b/brand/app-icon/icon-512.png
new file mode 100644
index 0000000..acbba0d
Binary files /dev/null and b/brand/app-icon/icon-512.png differ
diff --git a/brand/app-icon/icon-maskable-192.png b/brand/app-icon/icon-maskable-192.png
new file mode 100644
index 0000000..b022d48
Binary files /dev/null and b/brand/app-icon/icon-maskable-192.png differ
diff --git a/brand/app-icon/icon-maskable-512.png b/brand/app-icon/icon-maskable-512.png
new file mode 100644
index 0000000..80706bc
Binary files /dev/null and b/brand/app-icon/icon-maskable-512.png differ
diff --git a/brand/brand.json b/brand/brand.json
new file mode 100644
index 0000000..0de6f27
--- /dev/null
+++ b/brand/brand.json
@@ -0,0 +1,18 @@
+{
+ "name": "lockdocs",
+ "icon": {
+ "master": "svg/lockdocs-app-icon.svg",
+ "small": "svg/lockdocs-app-icon.svg",
+ "apple_touch": "svg/lockdocs-maskable.svg",
+ "maskable": "svg/lockdocs-maskable.svg",
+ "palette": ["#06080C", "#7C9CFF", "#42D6A4"],
+ "snap_threshold": 0.5,
+ "sizes": [180, 192, 512, 1024],
+ "maskable_sizes": [192, 512]
+ },
+ "outputs": {
+ "docs/public/logo.svg": "svg/lockdocs-symbol.svg",
+ "docs/public/favicon.svg": "favicon/favicon.svg",
+ "docs/public/favicon.ico": "favicon/favicon.ico"
+ }
+}
diff --git a/brand/build.py b/brand/build.py
new file mode 100644
index 0000000..3b92f91
--- /dev/null
+++ b/brand/build.py
@@ -0,0 +1,291 @@
+#!/usr/bin/env python3
+"""Brand home generator: rebuilds every derived file in brand/ from the SVG masters.
+
+ python3 brand/build.py # regenerate (needs: pip install pillow resvg-py numpy)
+ python3 brand/build.py --resnap # also redraw the 16/32 px grids from the master
+ python3 brand/build.py --check # verify hashes and surface copies (stdlib only; for CI)
+
+brand/brand.json says which master feeds which output and which surface files
+are copies of brand files. brand/README.md is the usage sheet.
+
+Small sizes are pixel-snapped: the master is rendered at 8x and every sample
+takes the nearest brand colour from tokens.json (or empty). A pixel takes the
+colour most of its samples agree on, and stays empty when fewer than the
+threshold of its samples are filled. The result
+is stored as a character grid (favicon/grid-16.txt, grid-32.txt) that a
+person may hand-edit; later runs draw from the grid until --resnap.
+"""
+import hashlib, io, json, os, shutil, struct, sys
+
+HERE = os.path.dirname(os.path.abspath(__file__))
+ROOT = os.path.dirname(HERE)
+SPEC = json.load(open(os.path.join(HERE, 'brand.json')))
+TOKENS = json.load(open(os.path.join(HERE, 'tokens.json')))
+GRID_CHARS = '.ABCDEFGHIJKLMNOPQRSTUVWXYZ'
+
+
+def p(rel):
+ return os.path.join(HERE, rel)
+
+
+def sha(path):
+ return hashlib.sha256(open(path, 'rb').read()).hexdigest()
+
+
+def colour_tokens():
+ out = {}
+ for name, tok in TOKENS.get('color', {}).items():
+ out[name] = tok['$value'] if isinstance(tok, dict) else tok
+ return out
+
+
+# ---------------------------------------------------------------- tokens.css
+def write_tokens_css():
+ lines = ['/* Generated by brand/build.py from brand/tokens.json. Do not edit. */', ':root {']
+ for group, items in TOKENS.items():
+ if group.startswith('$') or not isinstance(items, dict):
+ continue
+ for name, tok in items.items():
+ v = tok['$value'] if isinstance(tok, dict) else tok
+ if isinstance(v, list):
+ v = ', '.join(v)
+ lines.append(f' --brand-{group}-{name}: {v};')
+ lines.append('}')
+ open(p('tokens.css'), 'w').write('\n'.join(lines) + '\n')
+
+
+# ---------------------------------------------------------------- rendering
+def render(svg_rel, w, h=None):
+ import resvg_py
+ from PIL import Image
+ h = h or w
+ png = resvg_py.svg_to_bytes(svg_path=p(svg_rel), width=w, height=h)
+ img = Image.open(io.BytesIO(bytes(png))).convert('RGBA')
+ if img.size != (w, h): # a non-square master keeps its aspect ratio: centre it on the canvas
+ img.thumbnail((w, h), Image.LANCZOS)
+ canvas = Image.new('RGBA', (w, h), (0, 0, 0, 0))
+ canvas.paste(img, ((w - img.width) // 2, (h - img.height) // 2))
+ img = canvas
+ return img
+
+
+def save_png(img, rel):
+ """Save by extension: .png (optimised), .jpg/.jpeg (quality 85), .webp."""
+ os.makedirs(os.path.dirname(p(rel)), exist_ok=True)
+ if rel.lower().endswith(('.jpg', '.jpeg')):
+ img.convert('RGB').save(p(rel), quality=85, optimize=True, progressive=True)
+ else:
+ img.save(p(rel), optimize=True)
+
+
+def hex_rgb(h):
+ h = h.lstrip('#')
+ return tuple(int(h[i:i + 2], 16) for i in (0, 2, 4))
+
+
+def snap(svg_rel, size, threshold, palette):
+ """Render at 8x and quantise every sample to the nearest palette colour (or empty when
+ under half opaque). A pixel takes the colour most of its samples agree on, and stays
+ empty when fewer than `threshold` of its samples are filled."""
+ import numpy as np
+ ss = 8
+ a = np.asarray(render(svg_rel, size * ss), dtype=np.float64) / 255.0
+ pal = np.array([hex_rgb(c) for c in palette], dtype=np.float64) / 255.0
+ d = ((a[..., None, :3] - pal[None, None]) ** 2).sum(axis=-1)
+ idx = d.argmin(axis=-1) + 1
+ idx[a[..., 3] < 0.5] = 0
+ idx = idx.reshape(size, ss, size, ss).transpose(0, 2, 1, 3).reshape(size, size, ss * ss)
+ rows = []
+ for y in range(size):
+ row = ''
+ for x in range(size):
+ counts = np.bincount(idx[y, x], minlength=len(palette) + 1)
+ filled = counts[1:].sum() / (ss * ss)
+ row += '.' if filled < threshold else GRID_CHARS[1 + int(counts[1:].argmax())]
+ rows.append(row)
+ return rows
+
+
+def grid_path(size):
+ return f'favicon/grid-{size}.txt'
+
+
+def write_grid(size, rows, palette):
+ header = [f'# {size}x{size} pixel grid for the favicon. "." is empty; letters index the palette below.',
+ '# Hand edits are kept: build.py draws from this file until --resnap.']
+ header += [f'# {GRID_CHARS[i + 1]} = {c}' for i, c in enumerate(palette)]
+ open(p(grid_path(size)), 'w').write('\n'.join(header + rows) + '\n')
+
+
+def read_grid(size):
+ rows, pal = [], {}
+ for line in open(p(grid_path(size))):
+ line = line.rstrip('\n')
+ if line.startswith('# ') and ' = #' in line:
+ k, v = line[2:].split(' = ')
+ pal[k.strip()] = v.strip()
+ elif line and not line.startswith('#'):
+ rows.append(line)
+ assert len(rows) == size and all(len(r) == size for r in rows), f'grid-{size} must be {size}x{size}'
+ return rows, pal
+
+
+def grid_png(rows, pal, size):
+ from PIL import Image
+ img = Image.new('RGBA', (size, size), (0, 0, 0, 0))
+ for y, r in enumerate(rows):
+ for x, ch in enumerate(r):
+ if ch != '.':
+ img.putpixel((x, y), hex_rgb(pal[ch]) + (255,))
+ return img
+
+
+def grid_svg(rows, pal, size, dark_map=None):
+ """Pixel grid as one path per colour, run-length merged along rows. dark_map swaps
+ colours when the browser is in a dark theme."""
+ paths = {}
+ for y, r in enumerate(rows):
+ x = 0
+ while x < size:
+ ch = r[x]
+ if ch == '.':
+ x += 1
+ continue
+ x0 = x
+ while x < size and r[x] == ch:
+ x += 1
+ paths.setdefault(ch, []).append(f'M{x0} {y}h{x - x0}v1h-{x - x0}z')
+ dark = {ch: dark_map[c] for ch, c in pal.items() if dark_map and c in dark_map}
+ style = ''
+ if dark:
+ rules = ''.join(f'.{ch}{{fill:{c}}}' for ch, c in sorted(dark.items()))
+ style = f''
+ body = ''.join(f''
+ for ch, d in sorted(paths.items()))
+ return (f'\n')
+
+
+def write_ico(images, rel):
+ """ICO holding PNG entries (one per size, drawn separately)."""
+ blobs = []
+ for img in images:
+ b = io.BytesIO()
+ img.save(b, format='PNG', optimize=True)
+ blobs.append((img.size[0], b.getvalue()))
+ out = struct.pack('
diff --git a/brand/favicon/grid-16.txt b/brand/favicon/grid-16.txt
new file mode 100644
index 0000000..a23df71
--- /dev/null
+++ b/brand/favicon/grid-16.txt
@@ -0,0 +1,21 @@
+# 16x16 pixel grid for the favicon. "." is empty; letters index the palette below.
+# Hand edits are kept: build.py draws from this file until --resnap.
+# A = #06080C
+# B = #7C9CFF
+# C = #42D6A4
+..AAAAAAAAAAAA..
+.AAAAAAAAAAAAAA.
+AAAAAAAAAAAAAAAA
+AAAAAAACCAAAAAAA
+AAAAAACAACAAAAAA
+AAAAACAAAACAAAAA
+AAAAACAAAACAAAAA
+AAAAABBBBBBAAAAA
+AAAAABAAAABAAAAA
+AAAAABBBBBBAAAAA
+AAAAABAAABBAAAAA
+AAAAABBBBBBAAAAA
+AAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAA
+.AAAAAAAAAAAAAA.
+..AAAAAAAAAAAA..
diff --git a/brand/favicon/grid-32.txt b/brand/favicon/grid-32.txt
new file mode 100644
index 0000000..9c86a26
--- /dev/null
+++ b/brand/favicon/grid-32.txt
@@ -0,0 +1,37 @@
+# 32x32 pixel grid for the favicon. "." is empty; letters index the palette below.
+# Hand edits are kept: build.py draws from this file until --resnap.
+# A = #06080C
+# B = #7C9CFF
+# C = #42D6A4
+....AAAAAAAAAAAAAAAAAAAAAAAA....
+...AAAAAAAAAAAAAAAAAAAAAAAAAA...
+..AAAAAAAAAAAAAAAAAAAAAAAAAAAA..
+.AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA.
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAACCCCCCAAAAAAAAAAAAA
+AAAAAAAAAAAACCCCCCCCAAAAAAAAAAAA
+AAAAAAAAAAACCCAAAACCCAAAAAAAAAAA
+AAAAAAAAAAACCAAAAAACCAAAAAAAAAAA
+AAAAAAAAAAACCAAAAAACCAAAAAAAAAAA
+AAAAAAAAAAACCAAAAAACCAAAAAAAAAAA
+AAAAAAAAAAACCAAAAAACCAAAAAAAAAAA
+AAAAAAAAAAACCAAAAAACCAAAAAAAAAAA
+AAAAAAAAABBBBBBBBBBBBBBAAAAAAAAA
+AAAAAAAAABBBBBBBBBBBBBBAAAAAAAAA
+AAAAAAAAABBBBAAAAAABBBBAAAAAAAAA
+AAAAAAAAABBBBAAAAAABBBBAAAAAAAAA
+AAAAAAAAABBBBBBBBBBBBBBAAAAAAAAA
+AAAAAAAAABBBBAAAABBBBBBAAAAAAAAA
+AAAAAAAAABBBBAAAABBBBBBAAAAAAAAA
+AAAAAAAAABBBBBBBBBBBBBBAAAAAAAAA
+AAAAAAAAABBBBBBBBBBBBBBAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+.AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA.
+..AAAAAAAAAAAAAAAAAAAAAAAAAAAA..
+...AAAAAAAAAAAAAAAAAAAAAAAAAA...
+....AAAAAAAAAAAAAAAAAAAAAAAA....
diff --git a/brand/provenance.json b/brand/provenance.json
new file mode 100644
index 0000000..b903435
--- /dev/null
+++ b/brand/provenance.json
@@ -0,0 +1,102 @@
+{
+ "about": "Where every file in brand/ came from, with its SHA-256. build.py refreshes the hashes.",
+ "files": {
+ "README.md": {
+ "source": "the usage sheet for this brand home, written for the audit of 2026-09-28",
+ "sha256": "16ec0dd668209e524aa872750e84e3b5a4a4015066681156fa678fe7149b1efb"
+ },
+ "app-icon/apple-touch-icon-180.png": {
+ "source": "generated by build.py",
+ "sha256": "d6fc00b6744c5b9586f685cf22bd7442e8ae6e3628c970bb4a6786d7164c8100"
+ },
+ "app-icon/icon-1024.png": {
+ "source": "generated by build.py",
+ "sha256": "b33a02acd9c0a0bf0b99bf6bf3423be21c89d4400dfcee88b807e447448eeb9a"
+ },
+ "app-icon/icon-192.png": {
+ "source": "generated by build.py",
+ "sha256": "49a272fafe59281a8b7263a90721e92f270f00fd09d27b3fd0593bb646c31bae"
+ },
+ "app-icon/icon-512.png": {
+ "source": "generated by build.py",
+ "sha256": "5e885a3260b5a307cc0045abbd1add2eeb0ae529986b3fdd62c81e4abafc1af1"
+ },
+ "app-icon/icon-maskable-192.png": {
+ "source": "generated by build.py",
+ "sha256": "ce140eb07785347537d1c84bb804ad28d02f45fd3b4ec0e367c1aaf5545d8368"
+ },
+ "app-icon/icon-maskable-512.png": {
+ "source": "generated by build.py",
+ "sha256": "9ec9f90aa165231ee95d7ecbe58cc368e6e7d66a8f7b083c91e92510031d3ec9"
+ },
+ "brand.json": {
+ "source": "written 2026-09-28 for this brand home: names the masters, the icon palette for the 16/32 px grids, and the surface files that are copies of brand files",
+ "sha256": "b3365e3feb4b87fb388e42c80bedea9fdfb7cd8bbf4cd715763a12f11a469e7f"
+ },
+ "build.py": {
+ "source": "copy of the shared brand-home generator, unmodified",
+ "sha256": "406f811f5e85e5969fa6905cc8f8503788b7d70d0998426922e2ba97caa1bdcf"
+ },
+ "favicon/favicon-16.png": {
+ "source": "generated by build.py",
+ "sha256": "aaf5e4a3665fd7e3578156604c4fe2f7704969a958f3593e003f1ab182edfdab"
+ },
+ "favicon/favicon-32.png": {
+ "source": "generated by build.py",
+ "sha256": "de69897a8e5ac2883b3f686cdd02365e29fc97be5ce413b144d0642001e370bd"
+ },
+ "favicon/favicon-48.png": {
+ "source": "generated by build.py",
+ "sha256": "65f5fa3987a95624088de6c0d5e626b5879793e698824e6d99cec0e3e65ccb88"
+ },
+ "favicon/favicon.ico": {
+ "source": "generated by build.py",
+ "sha256": "e020fcdb5849785fe457f6a7c98b94c42b2b556b074d1cc11c7ef2b5aa2d872e"
+ },
+ "favicon/favicon.svg": {
+ "source": "generated by build.py",
+ "sha256": "ce6571a05cdedb61b9d6db573b6ef3da06d1456ee0638fce41f37304cbb23ed1"
+ },
+ "favicon/grid-16.txt": {
+ "source": "generated by build.py",
+ "sha256": "1bbfd78ae12ec900a53e28309885563ccd64076005c02d73168c19176214b6d0"
+ },
+ "favicon/grid-32.txt": {
+ "source": "generated by build.py",
+ "sha256": "144c21fa1bc84370c3ddf07cbbf6a4a626b5d386d0d88005a32a73328c899a41"
+ },
+ "svg/lockdocs-app-icon.svg": {
+ "source": "specified by the brand lead in the audit of 2026-09-28: the symbol on a #06080C rounded square (rx 22% of the 1024 side), symbol at 70% width, centred",
+ "sha256": "a5058f927de47ed94eb9f58d44ec146cc514165a431c81d930f8b3e11be2215e"
+ },
+ "svg/lockdocs-maskable.svg": {
+ "source": "svg/lockdocs-app-icon.svg without the corner radius, so masks that crop (Android maskable, Apple touch) have a full-bleed square",
+ "sha256": "dd55b9d3e7f7912d6fe9f4f0eca1dbf0cee8d85445bd57dbc01369690d9d5b71"
+ },
+ "svg/lockdocs-symbol-black.svg": {
+ "source": "one-colour version of svg/lockdocs-symbol.svg: every fill and stroke set to #000000, with the two document lines cut out of the lock body using fill-rule=\"evenodd\" so they stay visible",
+ "sha256": "dd65b1813ff2287a8783e086998076745754014803b16e898b67a557a4377c29"
+ },
+ "svg/lockdocs-symbol-white.svg": {
+ "source": "one-colour version of svg/lockdocs-symbol.svg: every fill and stroke set to #FFFFFF, with the two document lines cut out of the lock body using fill-rule=\"evenodd\" so they stay visible",
+ "sha256": "4df58371e47dc511ccc08f2b2a696349afe8ac9fa3601e5c306000c3d8d984fb"
+ },
+ "svg/lockdocs-symbol.svg": {
+ "source": "docs/public/logo.svg moved here 2026-09-28; git history for that path starts at d4c4d54 (2026-09-25, 'feat: lockdocs 0.1 - exact-version library docs from your lockfile, offline (#1)') and the drawing is unchanged. build.py copies it back to docs/public/logo.svg.",
+ "sha256": "8d553fc7585fb61d4e788582ab250ff5af155bf14f8647258e90fbc895653721"
+ },
+ "tokens.css": {
+ "source": "generated by build.py",
+ "sha256": "760eb9d8cf89aa8b27e6089f0b2f48829a19a9cfa8132c063196ef1a4202939e"
+ },
+ "tokens.json": {
+ "source": "the colours and the type face the product already shipped: the logo's three colours, the docs theme's background and accent roles (docs/.vitepress/theme/custom.css) and the theme's Inter stack",
+ "sha256": "8bd583de4e27f9bbc5bf34f1ef0d944e1e653c6746531d5feae4264d3150d851"
+ }
+ },
+ "surfaces": {
+ "docs/public/favicon.ico": "favicon/favicon.ico",
+ "docs/public/favicon.svg": "favicon/favicon.svg",
+ "docs/public/logo.svg": "svg/lockdocs-symbol.svg"
+ }
+}
diff --git a/brand/svg/lockdocs-app-icon.svg b/brand/svg/lockdocs-app-icon.svg
new file mode 100644
index 0000000..2fb44c5
--- /dev/null
+++ b/brand/svg/lockdocs-app-icon.svg
@@ -0,0 +1 @@
+
diff --git a/brand/svg/lockdocs-maskable.svg b/brand/svg/lockdocs-maskable.svg
new file mode 100644
index 0000000..02d1a8d
--- /dev/null
+++ b/brand/svg/lockdocs-maskable.svg
@@ -0,0 +1 @@
+
diff --git a/brand/svg/lockdocs-symbol-black.svg b/brand/svg/lockdocs-symbol-black.svg
new file mode 100644
index 0000000..7ceb873
--- /dev/null
+++ b/brand/svg/lockdocs-symbol-black.svg
@@ -0,0 +1 @@
+
diff --git a/brand/svg/lockdocs-symbol-white.svg b/brand/svg/lockdocs-symbol-white.svg
new file mode 100644
index 0000000..91ccd8c
--- /dev/null
+++ b/brand/svg/lockdocs-symbol-white.svg
@@ -0,0 +1 @@
+
diff --git a/brand/svg/lockdocs-symbol.svg b/brand/svg/lockdocs-symbol.svg
new file mode 100644
index 0000000..9fddf84
--- /dev/null
+++ b/brand/svg/lockdocs-symbol.svg
@@ -0,0 +1 @@
+
diff --git a/brand/tokens.css b/brand/tokens.css
new file mode 100644
index 0000000..85aad04
--- /dev/null
+++ b/brand/tokens.css
@@ -0,0 +1,13 @@
+/* Generated by brand/build.py from brand/tokens.json. Do not edit. */
+:root {
+ --brand-color-bg: #06080C;
+ --brand-color-bg-raised: #0B0E14;
+ --brand-color-bg-elevated: #0E1118;
+ --brand-color-bg-soft: #11151D;
+ --brand-color-accent: #7C9CFF;
+ --brand-color-accent-hover: #9DB4FF;
+ --brand-color-accent-active: #6480E8;
+ --brand-color-mint: #42D6A4;
+ --brand-color-gold: #FFD166;
+ --brand-font-sans: Inter, ui-sans-serif, system-ui, sans-serif;
+}
diff --git a/brand/tokens.json b/brand/tokens.json
new file mode 100644
index 0000000..78f66d1
--- /dev/null
+++ b/brand/tokens.json
@@ -0,0 +1,16 @@
+{
+ "color": {
+ "bg": {"$value": "#06080C", "$type": "color", "$description": "Page ground; the app-icon ground; the document lines cut out of the logo's lock body."},
+ "bg-raised": {"$value": "#0B0E14", "$type": "color", "$description": "Alternating background: code blocks, sidebar, footer."},
+ "bg-elevated": {"$value": "#0E1118", "$type": "color", "$description": "Elevated surfaces: menus, dialogs, hover cards."},
+ "bg-soft": {"$value": "#11151D", "$type": "color", "$description": "Soft fills and panels."},
+ "accent": {"$value": "#7C9CFF", "$type": "color", "$description": "The lock body; links, badges and primary buttons (docs --vp-c-brand-1)."},
+ "accent-hover": {"$value": "#9DB4FF", "$type": "color", "$description": "Links, badges and primary buttons on hover (docs --vp-c-brand-2)."},
+ "accent-active": {"$value": "#6480E8", "$type": "color", "$description": "Links and buttons when pressed (docs --vp-c-brand-3)."},
+ "mint": {"$value": "#42D6A4", "$type": "color", "$description": "The shackle; second stop of the hero gradient."},
+ "gold": {"$value": "#FFD166", "$type": "color", "$description": "Third stop of the hero gradient."}
+ },
+ "font": {
+ "sans": {"$value": ["Inter", "ui-sans-serif", "system-ui", "sans-serif"], "$type": "fontFamily", "$description": "Body and interface, Latin and CJK; VitePress serves Inter with the docs theme, self-hosted from the built site."}
+ }
+}
diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts
index f4d45e1..809c775 100644
--- a/docs/.vitepress/config.ts
+++ b/docs/.vitepress/config.ts
@@ -1,11 +1,12 @@
import { defineConfig } from 'vitepress'
+import tokens from '../../brand/tokens.json'
+const base = '/lockdocs/'
const url = 'https://sylphxai.github.io/lockdocs/'
const desc = 'Exact-version library docs from your lockfile — local, offline, no rate limits.'
-const icon = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect x='6' y='14' width='20' height='14' rx='3' fill='%237c9cff'/%3E%3Cpath d='M10 14V10a6 6 0 0 1 12 0v4' fill='none' stroke='%2342d6a4' stroke-width='3'/%3E%3Crect x='11' y='18' width='10' height='2' rx='1' fill='%2306080c'/%3E%3Crect x='11' y='22' width='7' height='2' rx='1' fill='%2306080c'/%3E%3C/svg%3E"
export default defineConfig({
- base: '/lockdocs/',
+ base,
title: 'lockdocs',
description: desc,
appearance: 'force-dark',
@@ -15,8 +16,10 @@ export default defineConfig({
lastUpdated: true,
sitemap: { hostname: url },
head: [
- ['link', { rel: 'icon', href: icon }],
- ['meta', { name: 'theme-color', content: '#06080c' }],
+ // The icons are copies of the brand home's files (brand/svg, brand/favicon).
+ ['link', { rel: 'icon', href: `${base}favicon.ico`, sizes: '48x48' }],
+ ['link', { rel: 'icon', type: 'image/svg+xml', href: `${base}favicon.svg` }],
+ ['meta', { name: 'theme-color', content: tokens.color.bg.$value }],
['meta', { property: 'og:type', content: 'website' }],
['meta', { property: 'og:site_name', content: 'lockdocs' }],
['meta', { property: 'og:image', content: `${url}img/demo.gif` }],
diff --git a/docs/.vitepress/theme/custom.css b/docs/.vitepress/theme/custom.css
index e8cc156..09189a0 100644
--- a/docs/.vitepress/theme/custom.css
+++ b/docs/.vitepress/theme/custom.css
@@ -1,15 +1,17 @@
+@import '../../../brand/tokens.css';
+
:root, .dark {
- --vp-c-bg: #06080c;
- --vp-c-bg-alt: #0b0e14;
- --vp-c-bg-elv: #0e1118;
- --vp-c-bg-soft: #11151d;
+ --vp-c-bg: var(--brand-color-bg);
+ --vp-c-bg-alt: var(--brand-color-bg-raised);
+ --vp-c-bg-elv: var(--brand-color-bg-elevated);
+ --vp-c-bg-soft: var(--brand-color-bg-soft);
--vp-c-divider: rgba(255, 255, 255, 0.08);
- --vp-c-brand-1: #7c9cff;
- --vp-c-brand-2: #9db4ff;
- --vp-c-brand-3: #6480e8;
+ --vp-c-brand-1: var(--brand-color-accent);
+ --vp-c-brand-2: var(--brand-color-accent-hover);
+ --vp-c-brand-3: var(--brand-color-accent-active);
--vp-c-brand-soft: rgba(124, 156, 255, 0.14);
--vp-home-hero-name-color: transparent;
- --vp-home-hero-name-background: linear-gradient(120deg, #7c9cff 10%, #42d6a4 60%, #ffd166 95%);
+ --vp-home-hero-name-background: linear-gradient(120deg, var(--brand-color-accent) 10%, var(--brand-color-mint) 60%, var(--brand-color-gold) 95%);
--vp-home-hero-image-background-image: radial-gradient(circle, rgba(124,156,255,.35), rgba(66,214,164,.2) 50%, transparent 70%);
--vp-home-hero-image-filter: blur(56px);
}
diff --git a/docs/public/favicon.ico b/docs/public/favicon.ico
new file mode 100644
index 0000000..55453a7
Binary files /dev/null and b/docs/public/favicon.ico differ
diff --git a/docs/public/favicon.svg b/docs/public/favicon.svg
new file mode 100644
index 0000000..70b5975
--- /dev/null
+++ b/docs/public/favicon.svg
@@ -0,0 +1 @@
+