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. 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):
- Plain git copy (the default — absent, or any value other than
"npm"):sync-family-css.mjspushes the canonical files straight into the repo's own working tree;check-family-css-sync.mjsverifies every such repo's copy still matches by hash. This is every repo in the table above except Website and Images. "npm": the repo installs@valadamoleo/family-cssfrom GitHub Packages (published by.github/workflows/publish-family-css.ymlin this repo, off a manual version bump — not on every commit here, so an npm-distributed repo's copy is expected to lag the git-copy repos' by however long it's been since the last version bump, not a defect) and copies the three files out ofnode_modules/@valadamoleo/ family-css/family-css/itself, via its own copy script.sync-family-css.mjs/check-family-css-sync.mjsboth skip any repo flagged this way entirely — there's nothing for the family-wide hash checker to compare against once a repo is pinned to a specific npm version rather than this repo's own HEAD. Each npm-distributed repo's own CI is what actually catches a stale copy instead (Website's.github/workflows/ci.yml; Images'.github/workflows/ family-css-drift.yml, added with #22) — re-running its own copy script and failing on agit diff/build-output mismatch.
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:
- Website doesn't commit it. Cloudflare Pages deploys the working tree with no other build artifact, so Website has to run the copy at Pages' own build time and can't skip committing without ALSO losing the files entirely if that build step ever failed silently.
- Images does commit it (Amoleo-Images#22, 16 Sept 2026 — the second
repo onto this mechanism). Images already has a real build artifact
(the GHCR Docker image) and already committed these files before
adopting npm distribution; npm only replaces WHERE the copy comes from
(a versioned, pinned, auditable package instead of "whatever
sync-family-css.mjslast pushed from a sibling working tree"), not whether it's committed. This is the more conservative adoption shape —api/Dockerfile,.github/workflows/build.yml, and Images' own deploy runbooks all change by zero lines, since the app still just copiespublic/into the image the same way it always did.
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:
- 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.