AmoleoFamily

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.

tokens/*.tokens.json + family-css/components/*.css → style-dictionary.config.mjs + build-family-css.mjs → family-css/*.css (canonical) → sync-family-css.mjs, or the npm package pilot → every familyCss:true repo

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):

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:

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. familyCssDistribution: "npm" since 8 Sept 2026 — this repo is the pilot for fetching the three files via @valadamoleo/family-css (GitHub Packages) instead of sync-family-css.mjs's plain git copy; see "Distribution: git copy vs. npm package" below. Its own copy is never committed (rebuilt at Cloudflare Pages deploy time) — a constraint specific to Website's own hosting, not a property of the npm mechanism itself (contrast Amoleo-Images below, which commits the fetched output).
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. familyCssDistribution: "npm" since 30 Sept 2026 (Amoleo-Accounts#64) — the same isolated-fetcher shape as Pets and Connections (scripts/family-css/, Dockerfile BuildKit secret, Dependabot, output gitignored), fetching family-css and family-ui (see "Shared components: @valadamoleo/family-ui"); moved off git copy in the same change that adopted AuthRedirectCard.
Amoleo-Images true — wired 9 Sept 2026 (#17) Plain Node/Express app, static assets served from api/public/ — familyCssDest: "api/public/family-css" (appDir: "api" in tools/repos.json, since the repo root also holds worker/, which has no static assets at all). No client-side build (noClient: true) — #6 decided plain server-rendered HTML for this repo's minimal admin UI, not React/Vite. familyCssDistribution: "npm" since 16 Sept 2026 (Amoleo-Images#22) — the second repo onto the npm mechanism after Website, but a DIFFERENT shape: see "Distribution: git copy vs. npm package" below for why Images commits the fetched output (Website doesn't) and needs no changes to its own Dockerfile/CI/deploy runbooks as a result.
Amoleo-Todo true — wired 2 Oct 2026 (#2) Transactional-tier scaffold. Vite serves static assets from client/public/, so familyCssDest: "client/public/family-css". Uses familyCssDistribution: "npm" from the start: an isolated scripts/family-css/ fetcher installs @valadamoleo/family-css, generated output is gitignored, and the Docker build fetches it with a BuildKit secret.
Amoleo-Reviews true — Reviews PR #7 (Reviews #1/#6; Family #15) Transactional-tier foundation landed via Reviews PR #7 on 7 Oct 2026. Vite serves static assets from client/public/, so familyCssDest: "client/public/family-css". Uses familyCssDistribution: "npm" from the start via the isolated scripts/family-css/ fetcher, with generated CSS/UI assets gitignored and fetched in Docker through a BuildKit secret. Also consumes the shared AuthRedirectCard from @valadamoleo/family-ui; the local shell maps --accent to the already-canonical --brand-reviews token rather than duplicating its value.
Amoleo-Home true — Home #1 foundation Transactional-tier scaffold. Vite serves from client/public/; Home uses familyCssDistribution: "npm" with the same isolated fetch-at-build/gitignored-output shape as Todo. The Home repo keeps #515AD6 as its local product alias/fallback while consuming the canonical generated family base. Package Manage Actions access still has to grant Amoleo-Home Read before its manual CI can fetch the private package.

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.

Distribution: git copy vs. npm package

Every familyCss:true repo gets the three files one of two ways, tracked by tools/repos.json's own familyCssDistribution field (documented in that file's own _comment block):

The npm mechanism answers one question — "where do the three files come from" — and each adopting repo separately decides whether the RESULT gets committed, which is a property of that repo's own deploy shape, not the npm package:

A future third repo adopting this mechanism should read both rows above before assuming either shape — "npm-distributed" alone doesn't say whether the output is committed.

Required manual step, easy to miss: grant the new repo Actions access on the package itself. @valadamoleo is a personal GitHub user account, not an organization — GITHUB_TOKEN's "trusts same-account packages" behaviour is real, but it's an organization feature, not a personal- account one. A repo's own GITHUB_TOKEN gets NO automatic read access to @valadamoleo/family-css just because it's owned by the same person who owns this repo. Each consuming repo must be added explicitly: from this repo's own Packages sidebar link → family-css → Package settings → Manage Actions access → Add Repository, at least Read role.

Found live twice already, both times the hard way: neither Website's original ci.yml (9 Sept 2026) nor Images' new family-css-drift.yml (16 Sept 2026) had this grant when first wired in, and both 403'd with Permission permission_denied: read_package on their first real run — Website's failure was masked for a week by an unrelated npm ci no-lockfile error that happened to fail first (see that repo's own row above), so this gap went unnoticed even longer than the drift-guard workflow's own first run took to surface it for Images. Do this before trusting a green CI run on a newly-wired npm-distributed repo, not after assuming one green run means the mechanism is proven — a workflow that never got far enough to hit the registry call (failing earlier, on something else) proves nothing about whether this grant exists.

Shared components: @valadamoleo/family-ui

Added 30 Sept 2026 (Amoleo-Accounts#63). family-css is CSS-only; the first shared React component, AuthRedirectCard (the redirect variant of the auth box, family-auth-box.md §0), lives in its own package so family-css stays CSS-only and Website, Images and Rissbrook never see a React peer dependency. Source in family-ui/, published by .github/workflows/publish-family-ui.yml (manual, same shape as publish-family-css.yml, no build step: it ships .jsx source).

How an app takes it — not as a client/ dependency. Pets' and Connections' scripts/family-css/package.json are "isolated on purpose" so the app's own package.json never needs a GitHub Packages token. The new package rides that route: it becomes a second dependency of the isolated fetcher, whose copy script also copies AuthRedirectCard.jsx into a gitignored client/src/family/. The Dockerfile's BuildKit-secret step and the Dependabot entry for /scripts/family-css already cover it. The app's own Vite compiles the JSX, so the package needs no build and no createElement workaround. Checklist: family-ui/README.md.

Version skew is a peer dependency, not a hope. family-ui declares peerDependencies: { "@valadamoleo/family-css": ">=0.2.0" } (the component needs the redirect-variant classes that 0.2.0 introduced). Installing it next to an older family-css fails with ERESOLVE — tested by installing family-ui beside a family-css 0.1.0 tarball. React is an optional peer, so the isolated fetcher (which has no React) does not pull it in. 0.x caret caveat as ever: ^0.1.0 means >=0.1.0 <0.2.0, so a breaking prop or markup change bumps the minor.

Same manual grant as family-css (§5's "Manage Actions access"): every repo whose CI fetches family-ui must be added under this package's own Package settings, separately from family-css — packages are granted per package.

tools/auth-box.json's per-repo familyUi flag records who has adopted it; check-auth-box.mjs fails a repo flagged familyUi that still carries its own redirect-card markup (.auth-accounts-caption in its own JSX).

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:

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

Generated from docs/family-css-system.md. Edit there and re-run npm run build in docs-site-src/ — never hand-edit a page.