Process & Registry
vs. Theme
Theme is the decision — which five concepts are shared (bg/panel/text/muted/border), why, and what's exempt (Rissbrook, Pets' extra accent themes). Family CSS System is the pipeline that turns that decision, plus the wordmark/footer/ auth-box/header/cookie-consent component rules, into real, distributed, checked CSS files. Theme tells you what the neutral palette is; this page tells you how that becomes the actual bytes every repo loads.
This docs site borrows the same discipline, one step further: generate, never hand-edit, and — since 8 Sept 2026 — actually deployed, not just generated locally.
The family CSS system — base.css, amoleo-light.css, amoleo-dark.css
Three generated, checked-in files — family-css/base.css, family-css/ amoleo-light.css, family-css/amoleo-dark.css — that every adopted repo
copies verbatim into its own family-css/ directory and never hand-edits.
Everything genuinely per-product (a site's own --accent, its extra themes,
Rissbrook's entire heritage palette) lives in that repo's own stylesheet,
layered on top.
Why this exists. Before this system, the shared CSS in family-wordmark.md,
family-footer.md and family-auth-box.md was prose with code fences —
each repo hand-retyped the fences into its own stylesheet, with no canonical
file to copy from. The two Amoleo-Website commits that prompted this doc
(5 Aug 2026) are the failure mode in miniature: a 6-line .family-link-pair
rule got added in the wrong place in styles.css, then moved, because there
was nothing to copy-paste — only a paragraph to remember correctly. This
doesn't replace the docs (they're still the why); it gives the what a
single literal source instead of N hand-typed ones.
The repo still doesn't link CSS across sites at runtime — Amoleo-Family/CLAUDE.md's
load-bearing rule (each site independently deployable, no first-paint
dependency on another repo being up) is unchanged. These three files are
copied, the same as everything else in this family; they just have one
real, byte-exact master copy now instead of a paragraph everyone re-types.
node tools/build-family-css.mjs # regenerate family-css/*.css from source
node tools/sync-family-css.mjs # copy them into every familyCss:true repo
node tools/check-family-css-sync.mjs # verify every copy still matches
1. Where the content actually lives
Three source locations, matching the family's existing "JSON is the
authority on values, markdown is the authority on why" split (CLAUDE.md):
tools/tokens/{base,theme-light,theme-dark}.tokens.json— the real values, since 8 Sept 2026 in real DTCG format ($value/$type/$description— https://www.designtokens.org/): every--brand-*accent anddesignTokens.radiusinbase.tokens.json(theme-independent), the 5 base-theme neutrals,--brand-amoleo, and shadow in the two theme files. Replaces the oldtools/brand-tokens.json, which turned out to be two different things mashed into one file — real values, and family governance that was never a token value itself (which repo may use a token, per-repo alias names, the wordmark colour rule). The reasoning prose that used to live in each token'snotefield now lives in its$description— a better home for it, and read directly bycheck-brand.mjsandtools/make-og-image.pyrather than duplicated.tools/brand-policy.json— the governance half of the old file:wordmarkColour,baseTheme.repos(per-repo aliases/onlyThemes/exempt), andextras— real values that never flow through this pipeline at all, like--brand-pets-ink(Website-only, never emitted intofamily-css/).style-dictionary.config.mjs— a real Style Dictionary build (added 8 Sept 2026, replacing ~140 lines of hand-writtenaccentLines/themeBlockJS) that compiles the three token files into four CSS custom-property fragments (tools/tokens/dist/{base,dark,light-media,light-attr}.css) using Style Dictionary's own built-incss/variablesformat — no custom format needed, just itsselectoroption (a string or array to nest inside)::rootfor base, the compound:root, [data-theme="dark"]for dark, and two separate files from the same light source (@media (prefers-color- scheme: light) { :root {...} }, and a plain[data-theme="light"]block) for light's two-block shape — see §2.tools/family-css/components/{primitives,wordmark,footer-lockup, auth-box,header,cookie-consent}.css— hand-authored, token-only CSS (never a hex literal) for the shared pieces, unchanged by any of the above.docs/family-wordmark.md,family-footer.md,family-auth-box.md,family-header.mdandcookie-consent.mdeach point at their matching file for the literal rule rather than restating it in their own fences — the reasoning stays in the prose, the syntax has one home.primitives.cssis the exception with no doc of its own — reset, typography, buttons, focus ring,.wrap,.skip-link,.visually-hidden, reduced-motion — see §9.tools/build-family-css.mjsruns the Style Dictionary build, then reads its four CSS fragments and the component files and writesfamily-css/base.css(accents + radii + the six component blocks, in that order —primitives.cssfirst since everything else builds on it) andfamily-css/amoleo-light.css/amoleo-dark.css(the fragments, concatenated with their own explanatory header prose). Every generated file opens with aGENERATED — do not hand-editheader.
Change a value: edit the matching tools/tokens/*.tokens.json file. Change
a shared rule: edit the matching file under tools/family-css/components/.
Either way, run
node tools/build-family-css.mjs and commit the regenerated output in the
same change — same discipline check-brand.mjs's JSON already requires,
extended to the CSS it produces.
2. Dark is the unconditioned default — a wrinkle worth knowing about
amoleo-light.css and amoleo-dark.css are not simply ":root" and
"[data-theme=\"dark\"]". Website and Connections both default to dark —
"a browser that reports no preference gets the theme the apps default to,
rather than a flash of the other one" (Website's own comment, predating this
system) — by putting the dark values on the unconditioned :root and
treating light as the thing that needs an explicit signal to activate.
amoleo-dark.css reproduces that exactly: :root, [data-theme="dark"].
amoleo-light.css only ever applies via @media (prefers-color-scheme: light)
or an explicit [data-theme="light"], never on bare :root. Get this
backwards and every visitor with no stored preference sees light instead of
the family's actual default — a real, silent regression the first version of
this generator shipped internally before being caught in verification.
3. Every adopted repo loads all three files — including Rissbrook
The original draft of this system treated Rissbrook as skipping
amoleo-light.css/amoleo-dark.css outright, on the reasoning that
family-theme.md §3 documents it as exempt from the shared neutral base.
That's the wrong shape. Rissbrook isn't exempt from loading the base —
it's the repo whose own rissbrook.css overrides the base the most, to
stay Robert Bickley's own palette, not a repo that opts out of the system.
Every adopted repo, Rissbrook included, links all three shared files and
then layers its own product stylesheet on top:
<link rel="stylesheet" href="/family-css/amoleo-dark.css">
<link rel="stylesheet" href="/family-css/amoleo-light.css">
<link rel="stylesheet" href="/family-css/base.css">
<link rel="stylesheet" href="/rissbrook.css"> <!-- or pets.css, connections.css, styles.css -->
Dark before light — not alphabetical, and not optional. Found live 6 Aug
2026 while wiring in Connections, whose theme genuinely switches at runtime
(data-theme set by boot.js/theme.js, not left to
prefers-color-scheme alone the way Website's does): amoleo-dark.css's
:root, [data-theme="dark"] rule always matches the root element, even on
a page with data-theme="light" set, since :root carries no condition of
its own. That ties in specificity with amoleo-light.css's
[data-theme="light"] rule, and a tie goes to whichever file loads later
— so light has to load second, or an explicit light theme silently renders
dark's colours instead. Verified by loading the two generated files
directly and reading getComputedStyle(--bg) under data-theme="light"
before and after swapping the order. Website carried the wrong order
harmlessly (it never sets data-theme, so the bug was never reachable)
until this fix corrected it too, 6 Aug 2026.
What differs by repo is only how much that last file overrides:
- Website (wired 5 Aug 2026, the reference for this system): its own
stylesheet defines
--accent/--on-accent/--text-strongand nothing else from the base — it takes the shared neutrals and accents as-is. - Connections (wired 6 Aug 2026 — see §4): same shape as Website, plus
its own
--accent. Its extra selectable colours (the node/edge/overlay palette — drama blue, actor teal, idol gold, and so on) were never brand colours in the first place (family-theme.md's own reasoning, restated in this repo's ownstyles.csscomment) and stay entirely its own. - Pets (once wired — see §4): same shape again, plus its own
--accentand its extra selectable themes (the cat/dog palettes) that were never meant to match the base in the first place (family-theme.md§3). - Rissbrook (once wired): overrides
--bg,--panel,--text,--muted,--borderand--accent— the full neutral set, not just the accent — to Robert Bickley's own values. Same mechanism as every other repo's override layer, just exercised completely rather than partially.family-theme.md§3's "exempt" language describes the values Rissbrook ends up with, not a reason to skip linking the files that set the defaults it then overrides.
4. The rename Pets needs before it can wire in
family-css/amoleo-light.css / amoleo-dark.css declare --bg, --panel,
--text, --muted, --border literally. For them to take effect, a
consuming repo has to actually use those names — family-theme.md §2's
existing alias table is exactly the gap:
| Concept | Website today | Connections before 6 Aug 2026 | Pets today | Canonical (this system) |
|---|---|---|---|---|
| page background | --bg |
--bg |
--page-bg |
--bg |
| surface background | --panel |
--panel |
--bg |
--panel |
| muted text | --muted |
--muted |
--text-light |
--muted |
| border | --border |
--panel-2 (doing double duty — see below) |
--border |
--border |
Decided this session: rename, don't alias. Connections did this 6 Aug
2026: --panel-2 had been carrying two unrelated concepts under one name —
genuine borders, and a real background/fill colour on chips, hover states
and buttons. Reading every one of its ~50 call sites in styles.css split
them: pure-border uses became --border, pure-background uses stayed
--panel-2 (now legitimately its own second-surface token, the same
relationship Website's --panel-2 already has to --border), and the rare
rule using --panel-2 for both border and fill on the same element (flat
chips, matching edge and fill on purpose) kept --panel-2 on both
properties rather than being decoupled — the two happen to share a value
today, but that's not a reason to force a relationship that isn't there.
Pets still owes its own version of this: renames --page-bg→--bg,
--bg→--panel (careful — the same name means a different concept before
and after), --text-light→--muted, throughout its own CSS (and any JS
that reads these custom properties). Once renamed, it wires in exactly like
Website and Connections — no ongoing alias layer to maintain.
5. Per-repo status
| Repo | familyCss |
Notes |
|---|---|---|
| Amoleo-Website | true — wired 5 Aug 2026, extended 6 Aug 2026 | Reference implementation for every component shipped so far, including the header (family-header.md §1.1) and the generic primitives (§9). Zero rename needed (names already matched). What's left in its own styles.css: --accent/--on-accent/--text-strong/--panel-2/--wrap/--step (per-site tokens), .brand-name and the compound-link hover rules (no equivalent construct exists in Pets/Connections to make them shared), and this one page's actual content. |
| Amoleo-Connections | true — wired 6 Aug 2026 | The reference implementation for the header's drop-down panel (family-header.md §6) and the whole auth box (family-auth-box.md) adopting its own reflection — both shared files were extracted from this app's CSS, so the visual result is unchanged. Did the §4 --panel-2/--border split. familyCssDest: "client/public/family-css" (a Vite app; static assets serve from client/public/, not the repo root — <link> tags added to client/index.html). Kept locally: node/edge/overlay colours (never brand colours), --accent/--on-accent/--text-strong/--panel-2 (per-site tokens), the .compact footer variant, .landing-* page content, and two real per-context overrides the shared files don't need to know about (.landing-auth .auth-card's padding delta; the reduced-motion .spinner exception, which needs !important to out-rank primitives.css's own blanket !important rule at equal specificity). One deliberate, unresolved conflict: the shared footer's 560px stacking breakpoint replaced Connections' own 1024px (reasoned as "a tablet is a thumb too") — the family value won as the default for a newly-wired repo, not silently kept; a family-wide breakpoint change is a separate discussion if tablet coverage turns out to matter in practice. |
| Amoleo-Pets | false — follow-up | Needs the §4 rename first. |
| Amoleo-Rissbrook | true — wired 6 Aug 2026 | Renamed its own neutral tokens (--page-bg→--bg, --surface→--panel, --text-muted→--muted) despite being exempt on values (family-theme.md §3) — "rename, don't alias" applied here too, the same choice Pets still owes. familyCssDest: "public/family-css" (an Astro site; static assets serve from public/). Also adopted the shared Pre-Auth Header component (family-header.md §1.1) the same session, replacing its own bespoke .site-header/.site-brand — not part of this system directly, but found and fixed alongside it, since the two migrations touched the same files. One real gap caught wiring this in: --brand-amoleo (unlike the four other --brand-* tokens) is genuinely theme-split, and deleting it here the same way the theme-independent four were deleted left a dark-OS visitor seeing white-on-white "AMOLEO" text — pinned back as Rissbrook's own fixed override, family-theme.md §3.5. Kept locally: --surface-alt, .family-mark a:hover{background:none} (counters this site's own global a:hover rule), and a 44px (not the shared 40px) mobile footer touch-target — deliberately ahead of the family default, not behind it. |
| Amoleo-Accounts | true — wired via #19 | Vite app, static assets serve from client/public/ (familyCssDest: "client/public/family-css"), same reason as Connections'. This row said "no code yet" for some time after that landed — corrected 3 Sept 2026; tools/repos.json had the current state right the whole time, only this table had drifted. |
| Amoleo-Todo | false — no code yet | Wire in as part of shipping, like every other family system. |
tools/repos.json's familyCss flag is what sync-family-css.mjs and
check-family-css-sync.mjs read — flip it only once a repo actually links
the three files and has its duplicated rules removed, same idiom as
adopted for the footer lockup.
6. What a component file is allowed to assume
Written while extracting wordmark.css: the "canonical" rule is whatever
Website (and where they agree, Pets) actually ship, not the most literal
reading of a doc's illustrative code sample. Two corrections made during
this extraction, worth remembering next time a component file is touched:
- No font-size on the bare
.brand-wordmarkselector. An earlier draft hardcoded the footer's own19pxonto the generic.brand-suffix/.brand-dotrule. That's wrong twice over: it's the footer's literal value, not a family-wide default, and the actual family-wide default (Website, Pets) is a0.79emratio off whatever size context sets on.brand-wordmarkitself — never a size of its own. Every context (header, footer, hero, product heading) sets its own explicit pixel size on both.brand-suffixand.brand-dottogether; there is no bare, unscoped instance anywhere in the family that doesn't do this. - Don't add a documented-but-unshipped rule silently.
family-wordmark.md§6 describes a footer family-name hover-underline that, as of this extraction, no adopted repo actually ships yet (check-wordmark.mjsreports itpending, not failing anywhere). The first draft offooter-lockup.cssincluded it anyway, since it's the documented target — which would have made wiring Website into this system a silent behavior change instead of a lossless move. Left out, with a comment marking where it goes once that rollout actually happens as its own tracked change.
8. Design tokens have a source, not a checker yet
tools/tokens/base.tokens.json's radius group and the two theme files'
shadow/shadow-lg (radii, shadows) exist purely because primitives.css
and header.css needed real values and the family's own rule is "the JSON
is the authority on values" — they aren't yet read by check-brand.mjs the
way the --brand-* accents and base-theme neutrals are. A repo's
radius/shadow values drifting from these wouldn't currently be caught by
anything. Website and Connections already agree on every value here; Pets
diverges on radius.md/radius.lg (16px/24px, not 14px/20px) and has no
comparable shadow system at all — its own --shadow is a bare colour
component re-tinted per accent theme (thirteen of them), not a fixed
box-shadow. Recorded here as a known gap and a known divergence, not
silently reconciled or silently enforced.
9. primitives.css has no canonical doc of its own
Every other component traces back to a doc that explains why — the
wordmark to family-wordmark.md, the footer to family-footer.md, and so
on. The base reset, typography, buttons, focus ring, .wrap, .skip-link,
.visually-hidden and the reduced-motion query don't have one, because
they were never really Website's own design decision to begin with — a
CSS reset and :focus-visible are close to universal defaults, not a
choice specific to this family the way the wordmark's 0.79em ratio is.
Website's specific values within that (17px body text, the system-ui
stack) are still a real decision, now the family's shared one — worth a
line in docs/family-theme.md for exactly that reason, even without a
dedicated primitives doc of its own.
10. What to check afterwards
node tools/check-family-css-sync.mjs— everyfamilyCss:truerepo's copy hashes identical to the canonical one.node tools/check-brand.mjs,check-wordmark.mjs,check-lockup.mjs,check-auth-box.mjs,check-header.mjs— unchanged in outcome for any repo just wired in; this system moves where a rule is declared, never what it computes to.- Load the wired repo with no
data-themeattribute and no stored preference — confirm it still renders the family's actual default theme (dark, today), notamoleo-light.css's values winning by accident (§2). - The mobile stacked footer (
docs/family-footer.md§6, ≤560px) still shows each product row's dot+suffix nudged left of centre — the literal rule this system's own extraction started from. - The header (
family-header.md§1.1): desktop shows the centred 36px wordmark with no bar or burger; ≤768px shows the sticky compact bar instead, burger opening a right-hand off-canvas drawer with a backdrop, closing on Escape/backdrop-click/nav-link-click and returning focus to the burger each time.
Generated from docs/family-css-system.md. Edit there and re-run npm run build in docs-site-src/ — never hand-edit a page.