diff --git a/CHANGELOG.md b/CHANGELOG.md
index fe920b2..aa4560e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,40 @@ Keep a Changelog change-type headings, so a consumer reads only the
sections that affect its template. Version numbers follow the
`MAJOR.MINOR.PATCH` policy described in [the README](README.md#versioning).
+## Unreleased
+
+### shared
+
+- The brand assets are the **ClumL company mark**, replacing a
+ `brand.svg` that was the Clumit Security product logo — one consumer's
+ product, shipped to every consumer. A project documenting a different
+ product would have published another product's branding.
+- `brand.svg` now carries **white** lettering, for the site header, which
+ sits on the primary-coloured bar. Anything rendering it on a light
+ background needs `brand-print.svg` instead.
+- Added `brand-print.svg`, the black-lettering variant the PDF cover
+ uses, and `brand-symbol.svg`, the cube alone. A wordmark scaled to a
+ 16-pixel tab icon is unreadable; the cube is not.
+
+### manual
+
+- `theme.favicon` points at `theme/brand-symbol.svg` rather than the
+ wordmark.
+
+### api-reference
+
+- `theme.favicon` points at `theme/brand-symbol.svg` rather than the
+ wordmark.
+
+### scripts
+
+- `fetch-theme.sh` installs all three brand assets.
+- `build-docs-pdf.sh` takes the cover logo from `brand-print.svg`, and
+ accepts `extra.pdf.cover_logo` — a path relative to `docs_dir` — so a
+ project documenting a product with its own mark can put it on the
+ cover. A path that does not resolve is an error rather than a silent
+ fall back to the company mark.
+
## [0.2.0] - 2026-08-08
### manual
diff --git a/README.md b/README.md
index c991994..6cf2013 100644
--- a/README.md
+++ b/README.md
@@ -21,7 +21,9 @@ templates/ Template assets grouped by document type
release-notes/ (planned)
shared/ Assets shared across all templates
fonts/ Roboto and Pretendard web fonts
- brand.svg Brand logo
+ brand.svg ClumL wordmark, white lettering (site header)
+ brand-print.svg ClumL wordmark, black lettering (PDF cover)
+ brand-symbol.svg ClumL cube alone (favicon)
styles/ Base CSS
samples/ Per-template sample sites for previewing
index.html Landing page linking to all samples
@@ -190,6 +192,8 @@ It downloads the release named in `docs/theme.toml` and installs:
| `shared/styles/base.css` | `docs/theme/styles/base.css` |
| `shared/fonts/` | `docs/theme/fonts/` |
| `shared/brand.svg` | `docs/theme/brand.svg` |
+| `shared/brand-print.svg` | `docs/theme/brand-print.svg` |
+| `shared/brand-symbol.svg` | `docs/theme/brand-symbol.svg` |
| `scripts/build-docs-pdf.sh` | `docs/theme/build-docs-pdf.sh` |
The install directory must not be dot-prefixed: MkDocs drops every
@@ -253,6 +257,37 @@ default, and a consumer that needs its own `i18n` languages redefines
the whole `plugins` list (which replaces the default wholesale), as the
sample sites do.
+### Branding
+
+The theme ships the **ClumL company mark** in three variants, because one
+file cannot serve all three places. `brand.svg` has white lettering for
+the site header, which sits on the primary-coloured bar. `brand-print.svg`
+has black lettering for the PDF cover, which sits on white paper.
+`brand-symbol.svg` is the cube alone: a wordmark scaled to a 16-pixel tab
+icon is unreadable, and the cube is not. The base wires all three, so a
+project documenting something ClumL publishes needs no branding config at
+all.
+
+A project documenting a product with its own mark overrides them. Commit
+the product's assets under `docs/` — **not** under `docs/theme/`, which
+is covered by the `.meta` digest and reverted on the next installer run:
+
+```yaml
+theme:
+ logo: assets/product-logo.svg
+ favicon: assets/product-symbol.svg
+
+extra:
+ pdf:
+ cover_logo: assets/product-logo-print.svg
+```
+
+`theme` is a mapping, so those two keys merge into the base rather than
+replacing it — the palette, fonts, and features it defines all survive.
+`extra.pdf.cover_logo` is a path relative to `docs_dir`; a path that does
+not resolve is an error rather than a silent fall back to the company
+mark, which would otherwise ship a cover branded with the wrong name.
+
The base already wires `extra_css` to the installed stylesheets (base,
lists, and PDF guardrails, plus `api.css` for the api-reference
template), so you do not list them yourself. To review the full set of
diff --git a/scripts/build-docs-pdf.sh b/scripts/build-docs-pdf.sh
index c036751..03b66a0 100755
--- a/scripts/build-docs-pdf.sh
+++ b/scripts/build-docs-pdf.sh
@@ -303,9 +303,24 @@ if cover_subtitle is not None:
if toc_title is not None:
options["toc_title"] = toc_title
-brand_svg = os.path.join(theme_dir, "brand.svg")
-if os.path.isfile(brand_svg):
- options["cover_logo"] = brand_svg
+# The cover sits on white paper, so it needs the black-lettering variant.
+# theme/brand.svg is the white-lettering one the site header uses; putting
+# that on the cover renders an invisible logo and nothing downstream would
+# report it. A consumer documenting a product with its own mark points
+# extra.pdf.cover_logo at it, relative to docs_dir.
+cover_logo = text("cover_logo")
+if cover_logo is not None:
+ docs_dir = str(data.get("docs_dir") or "docs")
+ cover_logo = os.path.join(root, docs_dir, cover_logo)
+ if not os.path.isfile(cover_logo):
+ fail(f"{config_path}: 'extra.pdf.cover_logo' does not exist at "
+ f"{cover_logo}")
+else:
+ cover_logo = os.path.join(theme_dir, "brand-print.svg")
+ if not os.path.isfile(cover_logo):
+ fail(f"{cover_logo} is missing; the installed theme is incomplete. "
+ "Re-run ./scripts/fetch-theme.sh.")
+options["cover_logo"] = cover_logo
# cover_tagline is not a mkdocs-with-pdf option. The plugin seeds the
# cover template context from `extra` and never overwrites this name, so
diff --git a/scripts/fetch-theme.sh b/scripts/fetch-theme.sh
index 2d12872..f597694 100755
--- a/scripts/fetch-theme.sh
+++ b/scripts/fetch-theme.sh
@@ -285,10 +285,16 @@ def stage(src, staging, template, template_dir, label):
fail(f"shared/fonts/ not found in {label}")
copy_tree(shared_fonts, os.path.join(staging, "fonts"))
- brand = os.path.join(src, "shared", "brand.svg")
- if not os.path.isfile(brand):
- fail(f"shared/brand.svg not found in {label}")
- copy_file(brand, os.path.join(staging, "brand.svg"))
+ # brand.svg carries white lettering for the site header, which sits on
+ # the primary-coloured bar; brand-print.svg is the black-lettering
+ # variant the PDF cover needs on white paper; brand-symbol.svg is the
+ # cube alone, which is the only one of the three that stays legible at
+ # favicon size.
+ for name in ("brand.svg", "brand-print.svg", "brand-symbol.svg"):
+ asset = os.path.join(src, "shared", name)
+ if not os.path.isfile(asset):
+ fail(f"shared/{name} not found in {label}")
+ copy_file(asset, os.path.join(staging, name))
pdf_script = os.path.join(src, "scripts", "build-docs-pdf.sh")
if not os.path.isfile(pdf_script):
diff --git a/shared/brand-print.svg b/shared/brand-print.svg
new file mode 100644
index 0000000..eb8de33
--- /dev/null
+++ b/shared/brand-print.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/shared/brand-symbol.svg b/shared/brand-symbol.svg
new file mode 100644
index 0000000..cdb533c
--- /dev/null
+++ b/shared/brand-symbol.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/shared/brand.svg b/shared/brand.svg
index 3e05650..aced5e5 100644
--- a/shared/brand.svg
+++ b/shared/brand.svg
@@ -1,40 +1 @@
-
+
\ No newline at end of file
diff --git a/templates/api-reference/mkdocs-base.yml b/templates/api-reference/mkdocs-base.yml
index 4009e5b..274cba6 100644
--- a/templates/api-reference/mkdocs-base.yml
+++ b/templates/api-reference/mkdocs-base.yml
@@ -27,7 +27,7 @@ theme:
language: en
font: false
logo: theme/brand.svg
- favicon: theme/brand.svg
+ favicon: theme/brand-symbol.svg
features:
- navigation.tabs
- navigation.sections
diff --git a/templates/manual/mkdocs-base.yml b/templates/manual/mkdocs-base.yml
index 0ab55c5..357e5cf 100644
--- a/templates/manual/mkdocs-base.yml
+++ b/templates/manual/mkdocs-base.yml
@@ -27,7 +27,7 @@ theme:
language: en
font: false
logo: theme/brand.svg
- favicon: theme/brand.svg
+ favicon: theme/brand-symbol.svg
features:
- navigation.tabs
- navigation.sections
diff --git a/tests/installer-test.sh b/tests/installer-test.sh
index aa3db0c..8bbdca7 100755
--- a/tests/installer-test.sh
+++ b/tests/installer-test.sh
@@ -87,6 +87,8 @@ for entry in \
fonts/Roboto-Regular.woff2 \
fonts/Pretendard-Regular.woff2 \
brand.svg \
+ brand-print.svg \
+ brand-symbol.svg \
build-docs-pdf.sh \
.meta
do
diff --git a/tests/pdf-test.sh b/tests/pdf-test.sh
index e662e02..e20c349 100755
--- a/tests/pdf-test.sh
+++ b/tests/pdf-test.sh
@@ -711,4 +711,70 @@ contains "$WORK/tables.txt" "overlong-end" "a row taller than a page"
contains "$WORK/tables.txt" "after-overlong" "the row after an overlong one"
ok "a row taller than a page splits rather than losing its content"
+# --- the cover logo -----------------------------------------------------
+
+# The cover sits on white paper, so it must carry the black-lettering
+# brand-print.svg. theme/brand.svg is the white-lettering variant the
+# site header uses; rendering that on the cover produces an invisible
+# logo, which is exactly the kind of silent asset loss nothing else here
+# would catch.
+generated="$(debug_build_pdf "$full" en)"
+grep -q 'cover_logo:.*brand-print\.svg' "$generated" \
+ || die "the cover did not default to brand-print.svg"
+grep -q 'cover_logo:.*brand\.svg' "$generated" \
+ && die "the cover used the white-lettering header logo"
+ok "the cover logo defaults to the print variant, not the header one"
+
+# A consumer documenting a product with its own mark overrides it. The
+# fixture carries a element so the substitution is observable in
+# the rendered page rather than only in the generated config.
+override="$WORK/override"
+new_project "$override"
+cat > "$override/docs/product-logo.svg" <<'SVG'
+
+SVG
+cat > "$override/mkdocs.yml" <<'EOF'
+site_name: Override Fixture
+extra:
+ pdf:
+ cover_logo: product-logo.svg
+EOF
+build_pdf "$override" en
+pdf_text "$override/site/pdf/override-fixture.en.pdf" > "$WORK/override.txt"
+contains "$WORK/override.txt" "PRODUCTLOGOMARK" "the overridden cover logo"
+ok "extra.pdf.cover_logo puts a consumer's own mark on the cover"
+
+# A path that does not resolve is an error. Falling back to the theme
+# logo would hand a consumer a cover branded with the wrong company.
+cat > "$override/mkdocs.missing.yml" <<'EOF'
+site_name: Missing Logo Fixture
+extra:
+ pdf:
+ cover_logo: does-not-exist.svg
+EOF
+if (cd "$override" && ./docs/theme/build-docs-pdf.sh en mkdocs.missing.yml) \
+ > "$WORK/missing-logo.log" 2>&1
+then
+ die "a cover_logo that does not exist should be an error"
+fi
+grep -q "extra.pdf.cover_logo" "$WORK/missing-logo.log" \
+ || die "the error does not name extra.pdf.cover_logo"
+ok "a cover_logo that does not resolve fails instead of falling back"
+
+# An incomplete install is an error for the same reason. This needs a
+# config without an override, or the override would satisfy the cover and
+# the missing asset would go unnoticed.
+cat > "$override/mkdocs.default.yml" <<'EOF'
+site_name: Default Logo Fixture
+EOF
+rm -f "$override/docs/theme/brand-print.svg"
+if (cd "$override" && ./docs/theme/build-docs-pdf.sh en mkdocs.default.yml) \
+ > "$WORK/no-print-logo.log" 2>&1
+then
+ die "a missing brand-print.svg should be an error"
+fi
+grep -q "brand-print.svg" "$WORK/no-print-logo.log" \
+ || die "the error does not name the missing asset"
+ok "a missing brand-print.svg fails instead of rendering a logo-less cover"
+
echo "All PDF script checks passed."