title: Design-tokens package — extract Mars palette into @rocky-hq/design-tokens
date: 2026-07-01
status: Proposed
phase: fe-a-design-tokens (foundation for shared visual identity across pantheon)
predecessor: docs/decisions/2026-06-29-phase-7c-c-a-hearth-close.md
spec: DESIGN.md (parent) + DESIGN.md (console)
plan: (this ADR is the plan)
external_repo: rocky-hq/design-tokens (proposed, not yet created)
external_pr: (none yet — draft lives at parent design-tokens/ until submodule cutover)
Design-tokens package (proposed)
Problem
The Mars mineral palette (void, oxide, signal, regolith, dust) is currently defined six times, once per app that needs it:
| File | Palette shape |
|---|---|
DESIGN.md (parent) |
canonical, OKLCH-annotated |
DESIGN.json (parent) |
schema mirror of the above |
console/src/app/globals.css:112-134 |
full Mars palette + --mars-* prefix, 4 void steps, 4 oxide, 4 signal, 3 regolith, 3 dust, 4 status, animations |
apps/erid-apex/src/app/globals.css:1-9 |
flat 7-color subset (--void, --void-deep, --dust, --regolith, --oxide, --signal, --border) |
apps/erid-docs/src/app/globals.css:1-32 |
same 7-color subset |
apps/erid-coming-soon/src/app/globals.css:1-87 |
same 7-color subset |
Hatch (devarno-cloud/hatch, external repo) is a seventh copy of the console-shaped superset (dust/dust-muted/dust-faint, void/void-surface/void-elevated, status-* badges).
Two failure modes flow from this:
- Drift — Console prefixes its palette
--mars-*; erid apps do not. Same colors, incompatible token names. A shared component cannot reference both. A palette edit inDESIGN.mdpropagates through zero surfaces automatically. - Split identity — Console and hatch (
rocky.erid.techandhatch.devarno.cloud) are two operator citizens under Erid. They currently look like two products because their token vocabularies were assembled independently.
Decision
Extract the Mars palette (plus typography, spacing, radii, animations, utilities) into a package at @rocky-hq/design-tokens, published to GitHub Packages under the same pattern as @rocky-hq/contracts (publishConfig.registry: npm.pkg.github.com, access: restricted, subpath exports).
Package publishes three artifacts, each with a subpath export:
@rocky-hq/design-tokens/css— raw CSS custom properties in a:rootblock (primitives layer)@rocky-hq/design-tokens/tailwind— Tailwind v4@theme inlineblock mapping primitives to color/font/animation utilities@rocky-hq/design-tokens/json— flat JSON export for cross-runtime consumers (Go, Python, Figma, docs generators)
Consumers pick the subpaths they need. Erid brand apps pull /css only (no Tailwind, no animations, no status colors — per the erid "No Status Color Rule"). Console and hatch pull /css + /tailwind and consume the full palette.
Locked decisions
1. Package lives at parent-root design-tokens/ as a draft, promoted to submodule after this ADR merges
Contracts is a git submodule at contracts/. Design-tokens will follow the same pattern (external repo rocky-hq/design-tokens, submodule-mounted at parent-root). Creating the external repo requires a push to GitHub, which is out of scope for this ADR. Draft contents live at parent design-tokens/ until the submodule cutover PR — this keeps the source of truth reviewable in-band without pretending the package is already published.
The contracts/CLAUDE.md push-down policy applies to design-tokens/ from day one: build/test instructions live in the package, not the parent.
2. Token names drop the --mars-* prefix
Console currently prefixes every Mars token with --mars-* (--mars-void, --mars-oxide) and bridges to --color-void via @theme inline. Erid apps use bare names (--void, --oxide).
The package emits bare names (--void-deep, --oxide, --regolith). Rationale:
- Erid apps' naming is closer to the canonical DESIGN.md vocabulary.
--mars-voidis a Console-internal artifact of the shadcn bridge. - The
mars-prefix is redundant — the palette IS Mars. Prefixing is defensive against a hypothetical clash with a second palette, and there is no second palette. - Shorter names read better in Tailwind classes:
bg-void-deepbeatsbg-mars-void-deep.
Console migrates by dropping the prefix in globals.css and updating the @theme inline bridge to read bare names. No .tsx changes required — the Tailwind class names (bg-void, text-oxide) do not change.
3. OKLCH is the primary color space; hex is a fallback
DESIGN.md specifies OKLCH values (oklch(11% 0.04 268) etc.) and the "Tinted-Neutral Rule" (no #000, no #fff). Package emits OKLCH as the primary value. Where hex is retained for a comment or fallback, chroma is verified to be tinted toward the brand hue.
Modern browser support (Chrome 111+, Safari 15.4+, Firefox 113+) covers every operator target. No legacy shim needed.
4. Status colors ship in the package but the erid brand apps do not consume them
Console/hatch operator surfaces render status pills (--status-nominal, --status-warning, --status-critical, --status-offline). Erid brand apps deliberately do not — the "No Status Color Rule" in DESIGN.md (Section 2) reserves status vocabulary for the operator console.
The package includes status colors in /css so operator surfaces can consume them, and documents the "brand surfaces don't import status" convention in the README. The alternative — a separate /css-operator and /css-brand — is a layer boundary that requires enforcement machinery to be worth its weight. A README convention plus a CSS-lint rule is enough for now; split subpaths if the convention starts to leak.
5. Typography tokens name families and scales; font loading stays app-local
Package emits --font-sans = "Inter, system-ui, sans-serif" and --font-mono = "JetBrains Mono, ui-monospace, monospace", plus a type scale (--type-display, --type-headline, --type-body, --type-scene, --type-label).
Font loading — Next.js next/font/google calls, @font-face blocks, preloading — stays in each consumer app. The package does not ship font files or fetch calls. Rationale: font loading is coupled to the framework (Next.js Font, Astro, Remix all differ), and shipping fonts from the tokens package would force a runtime choice on every consumer.
6. DataTable / Badge / Button primitives are NOT extracted in this ADR
Console has 3+ custom badge components (RegistryBadge, EarlyStopBadge, FingerprintBadge, HatchScopeBadge) and 3+ custom table components (AuditLogTable, HatchFeedTable, TablePanel). Hatch has its own badge and table primitives.
Consolidating these is a natural next step, but this ADR scopes to tokens only. Component extraction lands in a follow-up ADR (fe-c-primitive-components) once the token layer is in production and the semantic vocabulary is stable. Premature abstraction of a Badge component before the token names settle would guarantee a rewrite.
7. No animations helper library, no CSS-in-JS, no design-tokens build step for consumers
Package ships static CSS + JSON. No tsc, no PostCSS, no Rollup. Consumers @import the CSS or import the JSON. This mirrors the contracts pattern (contracts ships prebuilt dist/; design-tokens has no build to run).
animations.css ships keyframes (dustDrift, pulseSlow, pulseCritical, telemetryFlicker) as static CSS. Consumers reference them by name; the package does not enforce reduced-motion suppression (that is a consumer concern per DESIGN.md's Accessibility section).
Migration plan
Console (rocky-hq/console)
- Add
@rocky-hq/design-tokensas a localfile:../design-tokensdependency inpackage.json(draft phase). - Replace
globals.css:112-134(raw--mars-*palette) with@import "@rocky-hq/design-tokens/css";. - Rewrite
globals.css:14-84@theme inlineblock to read bare names (var(--void)instead ofvar(--mars-void)), or@import "@rocky-hq/design-tokens/tailwind";if the bridge maps cleanly. - Verify:
npm run buildclean,npm run test:runclean, Playwright E2E (npm run e2e) clean. Visual parity viaplaywright screenshoton all 11 top-level routes before/after.
Zero .tsx changes expected — Tailwind class names (bg-void, text-oxide-bright) preserved by name.
Erid apps (apps/erid-apex, apps/erid-docs, apps/erid-coming-soon)
- Add dependency same as Console.
- Replace each app's raw palette block in
globals.csswith@import "@rocky-hq/design-tokens/css";. - Keep app-local styling (
.citizen { ... },.scene { ... }, etc.) — those are surface-specific, not tokens. - Verify: build clean,
npm run buildproduces bit-identicalout/forerid-apex(static export).
Hatch (devarno-cloud/hatch, external)
Cannot land in this rocky-hq PR — hatch lives in a separate repo. This ADR documents the target state:
- Hatch adds
@rocky-hq/design-tokensas a package dependency (npm workspace or GitHub Packages install, depending on hatch's package.json). - Hatch's
globals.css(or equivalent) replaces its raw palette block with the package import. - Hatch keeps its
dust-muted/dust-faint/void-surface/void-elevatedsemantic tokens locally IF they diverge from the package's semantic layer; otherwise consumes the shared ones directly.
Hatch alignment PR opened after rocky-hq/design-tokens is a published submodule.
Contracts alignment
@rocky-hq/contracts is unrelated to design-tokens and stays untouched. The two packages coexist as siblings under the pantheon npm scope.
What this ADR does NOT lock
- Component extraction — deferred to
fe-c-primitive-components. Badges and tables stay app-local until the token layer stabilizes. - Sidebar / nav grouping by subsystem — deferred to
fe-d-console-shell(highest-leverage frontend item after tokens). - Gate-denial UX (402/403 inline surface) — deferred to
fe-e-gate-denial-ux. - Console
/agentsprojection page — deferred tofe-f-console-agents-surface. - Cross-app auth session preview in headers — out of scope; different subsystem (airlock/handoff).
- Design-tokens submodule promotion — separate PR after this ADR merges. Creates external repo
rocky-hq/design-tokens, moves parentdesign-tokens/into a submodule mount at the same path.
Acceptance (this ADR)
design-tokens/package exists at parent root withpackage.json,README.md,src/tokens.css,src/theme.css,src/tokens.json, and animation/utility CSS.- Package
package.jsonmirrors the contracts pattern (publishConfig.registry, subpathexports, GitHub Packages access). - Draft content passes an eyeball diff against
DESIGN.md(OKLCH values match, palette names match). - This ADR file exists at the path in its
title:frontmatter.
Sources
DESIGN.md(parent root) — canonical palette + Section 2 "The Ten Percent Oxide Rule", "The No Status Color Rule", "The Tinted-Neutral Rule".console/src/app/globals.css:14-134— current Console--mars-*palette + Tailwind v4 bridge.apps/erid-apex/src/app/globals.css:1-26,apps/erid-docs/src/app/globals.css:1-32,apps/erid-coming-soon/src/app/globals.css:1-87— current erid app palette duplicates.contracts/package.json:23-39— publish-pattern reference (subpath exports, restricted GitHub Packages access).docs/decisions/2026-06-29-phase-7c-c-a-hearth-close.md— most recent ADR (structural precedent).