Foundations
Amoleo Light
Amoleo Dark
The base theme — Amoleo Light and Amoleo Dark
The neutral palette three of the five products' own light/dark themes are
built on: page background, surface background, body text, muted text,
borders. Every product then puts its own accent on top — --brand-pets,
--brand-connections, and so on, already governed by
tools/tokens/base.tokens.json and docs/family-footer.md. This document is about the
layer underneath that: the part that isn't supposed to vary by product at
all.
This was never written down anywhere before 5 August 2026. It existed —
Website, Pets and Connections' neutral greys are already byte-identical to
each other — but only as an informal copy-by-hand. Website's own styles.css
says so in a comment: "Values are copied from Connections' styles.css rather
than re-derived, and should stay in step with it." That sentence is a
promise with nothing enforcing it, which is exactly the shape every other
piece of drift in this family started as before it got a JSON file and a
checker. This is that JSON file and that checker, for this one.
The values live in tools/tokens/theme-light.tokens.json
and theme-dark.tokens.json —
real DTCG token files, since 8 Sept 2026 (see
docs/family-css-system.md). Per-repo aliases,
onlyThemes and exemptions (governance, not values) live in
tools/brand-policy.json's baseTheme
section instead —
tools/check-brand.mjs's parser already does
everything this needs: per-theme scoping, var() resolution, per-repo
exemptions. The same values also compile into family-css/amoleo-light.css
/ amoleo-dark.css — the literal, copied-not-retyped files — see
docs/family-css-system.md. Run this after touching
a repo's base theme tokens:
node tools/check-brand.mjs
1. What's shared and what isn't
Shared — governed, must match:
| Concept | Light | Dark |
|---|---|---|
bg — page background |
#f7f7fb |
#101013 |
panel — surface/card background |
#ffffff |
#191920 |
text — body text |
#3a3a4a |
#e9e9ee |
muted — secondary/meta text |
#6b6b7a |
#9a9aa8 |
border — dividers, subtle rules |
#e0e0e6 |
#2b2b33 |
Not shared — each site's own, by design:
--accent— the product's own colour. Terracotta in Pets, cyan-teal in Connections, navy in Website's own case (it has no sub-brand of its own, so its--accentis the colour of the AMOLEO half of its own wordmark). This was always meant to differ; it's the entire point of "one brand, five products."--text-strong— a per-site token, explicitly, perfamily-wordmark.md: "the wordmark's own colour comes from--brand-amoleo... never--text-strong... an app whose own brand colour is also its darkest ink tints the shared half of the mark." Rissbrook did exactly that until 1 Aug 2026 — its--text-strongwas his dark teal, and the wordmark rendered in it by accident because the two happened to share a name. Website, Pets and Connections'--text-strongcurrently does coincide with the shared ink (#1c1c2e/#ffffff— the same value--brand-amoleoholds), but that's convenience, not a rule; nothing requires it to stay that way, and it isn't checked here.
Why the distinction matters going forward: a future product is free to
pick --text-strong to suit its own content (a photo-heavy app might want a
warmer near-black than #1c1c2e) without that being brand drift. Changing
bg, panel, text, muted or border without a documented reason,
however, is drift — those are what "the same family" actually looks like at
the level nobody consciously notices, the way a mismatched line-height reads
as "off" before a viewer could say why.
2. Naming isn't shared, even where the values are
The three repos that share this base don't agree on what to call it:
| Concept | Website | Connections | Pets |
|---|---|---|---|
bg |
--bg |
--bg |
--page-bg |
panel |
--panel |
--panel |
--bg |
text |
--text |
--text |
--text |
muted |
--muted |
--muted |
--text-light |
border |
--border |
--border (was --panel-2 until the 6 Aug 2026 split — family-css-system.md §4) |
--border |
Pets calling its page background --page-bg and its surface background
--bg is the one worth remembering — it inverts Website/Connections' naming,
where --bg is the page. check-brand.mjs resolves each repo's own alias
map (baseTheme.repos.<name>.aliases) rather than matching a fixed property
name, for exactly this reason.
3. Two repos that legitimately don't follow this
Rissbrook still loads the shared files (docs/family-css-system.md §3) —
"exempt" below describes the values it ends up with, not a reason to skip
amoleo-light.css/amoleo-dark.css. It's the repo that overrides the base
the most, not the one that opts out of it.
Rissbrook is exempt in full, not failing quietly. Every colour on that
site — including what would be its base theme — is Robert Bickley's own,
documented in the repo's own tokens.css header: "Every colour here comes
from Robert Bickley's own our-family-blue.css." Its page background is
#ffffff, not the shared #f7f7fb; its ink is var(--rb-dk-teal), not the
shared #1c1c2e; its muted text is #566672, not #6b6b7a. None of that is
oversight — it's the same category of deliberate exception as Pets' own
thirteen cat/dog accent themes, just at the base-theme level instead of the
accent level. Rissbrook is also light-only, so there's no dark side to have
diverged in the first place.
One token needs its own fixed override even so: --brand-amoleo.
Rissbrook's other neutral tokens (--bg/--panel/--muted) are safe to
pin unconditionally because nothing else ever sets them for this repo —
but --brand-amoleo is genuinely theme-split family-wide (white in dark,
#1c1c2e in light, §1), and the shared files only ever apply the light
value behind an explicit signal — [data-theme="light"] or an OS light
preference — never as the silent default. Rissbrook sets neither. Deleting
its own --brand-amoleo the same way the four theme-independent
--brand-* tokens were correctly deleted (family-css-system.md §5) left
a visitor with their OS set to dark seeing dark's white ink against this
site's own permanently-white --bg — invisible "AMOLEO" text, on both the
header and the footer lockup (found and fixed 6 Aug 2026). The fix is the
same shape as the neutral base tokens: a fixed, unconditional override in
Rissbrook's own :root, matching amoleo-light.css's value, because this
site is light-only regardless of the visitor's OS — every token that
varies by theme needs its own pin here, not just the ones that happen to
share a name with the neutral base.
Pets' midnight theme is only partly governed. Its light default
(amoleo) matches the shared base exactly, under Pets' own names. Its dark
theme (midnight) exists in shipped CSS but — per Pets' own comment — is
"unreachable from theme swatches, kept for future dark-mode work." A theme
nobody can select isn't shipped, so it isn't held to this yet
(baseTheme.repos["Amoleo-Pets"].onlyThemes: ["light"]). Extend that the day
midnight becomes selectable.
Pets' other twelve themes (ginger, tabby, and so on) are correctly not
checked at all. They're accent themes, the same category as --accent
itself — never meant to match the neutral base, any more than Website's navy
is meant to match Pets' terracotta. The checker distinguishes these from the
genuine default by looking for a [data-theme="…"] qualifier the parser
doesn't recognise as light/dark/midnight, and skipping — not by name-matching
each of the thirteen, which would need editing every time a theme is added or
renamed.
3.5 The reciprocal rule: extra themes are free, except the wordmark
A product may add as many additional user-selectable themes as it wants —
Pets' thirteen cat and dog palettes are the existing example — with no
constraint beyond one: whichever theme is active, AMOLEO and · Product
still have to render in the family's actual colours. Extra themes are exempt
from matching the shared neutral base (§3); they are not exempt from that.
This is the mirror image of §3's exemption, not the same rule again: §3 says a custom theme's own background, text, panel colours can be anything; this says the wordmark specifically can't be, in any theme, custom or not.
check-brand.mjs's checkSuffixColour enforces the half of this that
wasn't previously checked at all (checkWordmarkColour already covered the
AMOLEO name, unconditionally, in every scope). It found a live bug the day
it was written: Connections' own header wordmark had no --wordmark-accent
override — only the footer's .family-mark .connections sets one — so it
fell through to var(--accent), Connections' generic UI accent, which still
carried a light/dark split (#0b6e8f / #3fb6dc) left over from before the
4 Aug single-colour policy retired --brand-connections' own split. Its own
name rendered in the wrong blue in dark mode. Fixed by falling back to
var(--brand-connections) directly, matching Pets' pattern, which never had
this problem because it was already wired this way.
Rissbrook had the same latent shape, without the visible symptom. Its
fallback was var(--accent) too — copied verbatim from Website's own
pattern, where it's actually correct (Website has no sub-brand, so its own
--accent is its wordmark ink). Rissbrook's --accent happens to resolve
to the same value as --brand-rissbrook today (both trace back to his own
--rb-mid-teal), so nothing looked wrong — but that was a coincidence of
provenance, not a guarantee, and a future redesign of his site's nav colour
could have silently broken the wordmark with nothing watching. Fixed the same
way, to the same fallback pattern, before it ever became visible.
3.6 Typography joined the shared base 6 August 2026
Not one of the five values §1 lists, but the same idea: Website's own body
typography — system-ui stack, 17px, line-height: 1.65 — is now the
family's shared default, compiled into
tools/family-css/components/primitives.css
(docs/family-css-system.md §9). It was always
just Website's own choice before; it's a real decision now, since Pets and
Connections inherit it the moment they wire into that system, the same way
they'll inherit bg/panel/text/muted/border. No checker enforces
it yet, same gap family-css-system.md §8 records for radii and shadows.
4. Building a new product on this base
A new product (Accounts, Todo) that wants "an Amoleo Light and an Amoleo Dark
theme" gets one for free by using the five shared values under whatever
variable names fit its own codebase, then adding one --accent and telling
check-brand.mjs its alias map. It does not need to invent its own
neutral palette the way Rissbrook's heritage requirement forced — that's the
exception, not the template. Register the repo in baseTheme.repos with its
aliases the day its CSS ships; until then it's absent from the section
entirely (not "exempt" — there's nothing to be exempt from yet).
5. What the checker actually checks
Reuses check-brand.mjs's existing declaration parser (checkTokens's own
machinery: theme scoping via data-theme/prefers-color-scheme, var()
resolution, short-hex normalisation) rather than a second parser. For each
repo with a baseTheme.repos entry that isn't exempt:
- Every declaration whose property matches one of that repo's five aliases, scoped to light, dark, or the bare default, is compared against the matching canonical value.
- A declaration scoped to a different named theme (Pets'
[data-theme= "ginger"]and its twelve siblings) is skipped, not checked and not failed — see §3. onlyThemesrestricts which side of light/dark is actually governed — today, only Pets, only light, for the reason in §3.
A separate function, checkSuffixColour, covers §3.5: for a repo's own
self-referencing wordmark instance (never one showing another product), it
resolves .brand-suffix/.brand-dot's colour — following its fallback
across every stylesheet in the repo, not just the one declaring it, since
Rissbrook splits the token from the rule across two files — and requires it
to land on that repo's own --brand-<product> in both light and dark. An
unresolvable fallback (a token this checker can't trace to a literal colour
anywhere in the repo) is reported as a note to check by eye, not a failure —
a wrong guess here would be worse than an honest "can't tell."
Not checked: whether the values themselves are good design (this is a
consistency checker, not a design reviewer, same limit every other checker in
this family has); and --text-strong/--accent against the base theme,
deliberately, per §1 — though --accent specifically feeding the wordmark's
own colour is exactly what §3.5 exists to catch.
Generated from docs/family-theme.md + tools/tokens/theme-{light,dark}.tokens.json. Edit there and re-run npm run build in docs-site-src/ — never hand-edit a page.