@araharah/theme
The Telemetry Rams design system as a Tailwind v4 CSS-first token package: every color, spacing, type, and radius value in arah² is defined once in src/tokens.css, compiled to dist/rams.css, and consumed by @araharah/web. The package also hosts the MapLibre basemap style fork, the self-hosted typefaces, and the audit / screenshot evidence tooling.
Part of the arah² monorepo — see the root README and docs/ for bot and Mini App context.
Contents
| Path | What it is |
|---|---|
src/tokens.css |
Single source of truth for every numeric design value: @theme static color / spacing / font / radius tokens plus the --type-* typography scale |
src/theme.css |
Entry stylesheet — imports Tailwind, tokens.css, components.css; declares the self-hosted @font-face rules and the Telegram light/dark shell slots |
src/components.css |
Component layer (@layer components): buttons, inputs, telemetry cards, event chips, maneuver banner, checkbox, toggle, and the .text-* typography helpers |
src/sheet.html |
Component sheet — the rendered proof that every component derives from tokens |
gallery.html |
DSGN-02 approval gallery: mockup reference render vs. implementation capture, side by side |
basemap/build.mjs |
Builds basemap/liberty-rams.json by forking the OpenFreeMap liberty style and deriving every cartographic color from tokens.css |
basemap/liberty-rams.json |
The generated MapLibre style — imported directly by web/src/map/map.ts |
basemap/index.html |
MapLibre GL proof page over KL, with a placeholder route vector |
scripts/audit.mjs |
Off-token audit: scans rendered CSS and flags any value that does not derive from tokens.css |
scripts/audit.test.mjs |
TDD harness for the audit (12 contract tests, zero dependencies) |
scripts/screenshot.mjs |
Playwright capture toolchain — writes screenshots/ |
fonts/geist/, fonts/space-grotesk/ |
Self-hosted woff2 subsets (SIL Open Font License 1.1) |
graph TD
TOK["src/tokens.css — colors, spacing, type, radius"]
COMP["src/components.css — component classes"]
ENTRY["src/theme.css — entry stylesheet"]
CLI["@tailwindcss/cli"]
DIST["dist/rams.css — built stylesheet"]
BUILD["basemap/build.mjs"]
STYLE["basemap/liberty-rams.json"]
WEB["@araharah/web — Mini App shell"]
AUDIT["scripts/audit.mjs"]
SHOTS["scripts/screenshot.mjs"]
GAL["gallery.html + src/sheet.html + basemap/index.html"]
TOK --> COMP
TOK --> ENTRY
COMP --> ENTRY
ENTRY --> CLI
CLI --> DIST
TOK --> BUILD
BUILD --> STYLE
DIST --> WEB
STYLE --> WEB
TOK --> AUDIT
GAL --> AUDIT
GAL --> SHOTS
SHOTS --> GAL
classDef tokens fill:#EB8008,stroke:#1A1A1A,stroke-width:2px,color:#1A1A1A
classDef build fill:#D9D2C6,stroke:#1A1A1A,stroke-width:2px,color:#1A1A1A
classDef out fill:#F5EDE0,stroke:#736B1E,stroke-width:2px,color:#1E1B14
class TOK,COMP,ENTRY tokens
class CLI,BUILD,AUDIT,SHOTS build
class DIST,STYLE,WEB,GAL out
Installation
@araharah/theme is a workspace package — it is never published. Consumers depend on it by name and install from the monorepo root:
git clone http://gitmirah:3000/c-fu/arah2.git
cd arah2
npm install
Requires Node.js and npm. basemap/build.mjs needs Node >= 18 (global fetch), and basemap:preview shells out to python3.
⚠ VERIFY: Node.js minimum version — no
enginesfield or.nvmrcexists in the repository;basemap/build.mjsdocumentsNode >= 18in its header comment.
Quick start
npm run build -w theme # src/theme.css -> dist/rams.css (minified)
npm run sheet # build, then open theme/src/sheet.html in a browser
npm run audit # audit the rendered CSS for off-token values
Scripts
| Command | Description |
|---|---|
npm run build -w theme |
tailwindcss -i ./src/theme.css -o ./dist/rams.css --minify |
npm run send -w theme |
Copy fonts/geist and fonts/space-grotesk into dist/fonts/ so the built stylesheet ships with its typefaces |
npm run sheet -w theme |
build, then print the path to src/sheet.html (open it in a browser) |
npm run gallery -w theme |
build + capture, then serve the workspace on :8123 and block on basemap:preview |
npm run basemap -w theme |
node basemap/build.mjs — fetch OpenFreeMap liberty, derive the Rams cartography, write basemap/liberty-rams.json |
npm run basemap:preview -w theme |
python3 -m http.server 8123 — serve theme/ as docroot so dist/rams.css, the woff2 fonts, and basemap/ resolve |
npm run audit -w theme |
node ./scripts/audit.mjs ./dist/rams.css — off-token audit |
npm run capture -w theme |
node ./scripts/screenshot.mjs — Playwright captures into theme/screenshots/ |
npm test -w theme |
node ./scripts/audit.test.mjs — the audit's contract tests |
The root package.json delegates sheet, gallery, audit, and capture to this workspace, so npm run audit from the root is the same command.
Testing
npm test runs a plain Node harness — no test framework, no dev-server dependency. audit.test.mjs spawns audit.mjs against fixtures under /tmp/opencode/audit-test/ and asserts exit codes:
| Contract | Expectation |
|---|---|
| T1 | Stylesheet using only token values → exit 0 |
| T2 | Off-token color / non-zero radius / off-token font / shadow / blur / backdrop-filter → exit 1, each listed as file:line: declaration |
| T3 | tokens.css drifting below 40 colors → exit 2 |
| T4 | Any --radius-* token other than zero → exit 2 |
| T5 | The real dist/rams.css build → exit 0 |
| T6 | Off-token values inside @layer components → flagged |
| T7 | @layer utilities only audits classes the implementation pages actually wear |
| T8 | Inline style="…" attributes are audited too |
| T9 | The deprecated bare --radius key drives bare rounded at zero |
| T10–T12 | Literal font: shorthands, named colors / oklch() and friends, and spacing + typography numerics are flagged |
audit.mjs exit codes: 0 clean, 1 violations (each printed as file:line: declaration), 2 token-sheet drift or an error.
Coverage
No coverage threshold configured. The audit itself is the enforcement mechanism: it is a token-conformance gate, not a line-coverage gate.
Usage
Importing the built stylesheet
@araharah/web pulls the package in from its shell stylesheet:
/* web/src/shell.css */
@import "@araharah/theme/dist/rams.css";
That single import brings the full token layer (@theme static, so every token survives compilation as a plain CSS custom property), the @layer components class API, the self-hosted @font-face rules, and the Telegram light/dark shell slots --shell-bg / --shell-on-surface.
Consuming tokens in your own CSS
Because @theme static forces every token into the output, tokens are addressable as custom properties from anywhere — no Tailwind build step needed:
.surface {
background: var(--color-surface);
border: 1px solid var(--color-matte-black);
padding: var(--spacing-space-md);
border-radius: var(--radius-md); /* resolves to 0 — Rams forbids roundedness */
}
.headline {
font: var(--type-headline-md);
letter-spacing: var(--type-headline-md-tracking);
}
Note the convention: font: always takes the compound --type-* token and letter-spacing takes the -tracking companion, because the CSS font shorthand cannot carry tracking.
Component classes
src/components.css is the public component API. Every declaration in it is a token reference; the only literals are the machined border geometry (1px / 2px rules, 3px focus brackets) and the exact 18×18 checkbox square.
<button class="btn-primary">Recenter</button>
<button class="btn-secondary">Layers</button>
<label class="checkbox"><input type="checkbox" checked></label>
<div class="toggle">
<button class="toggle-option" aria-pressed="true">Off</button>
<button class="toggle-option" aria-pressed="false">On</button>
</div>
<div class="card">
<div class="card-header"><span>Traffic</span><span class="card-ref">KL-01</span></div>
<div class="card-content"><span class="text-telemetry-num">2.4</span><span class="text-label-sm">KM AHEAD</span></div>
</div>
<span class="chip-traffic">Congestion</span>
<span class="chip-hazard">Crash</span>
<span class="chip-safe">Clear</span>
<div class="maneuver-banner">
<span class="maneuver-arrow">→</span>
<span class="text-label-sm">STEP</span>
<span class="text-telemetry-num">320 m</span>
</div>
Importing the basemap style
// web/src/map/map.ts
import ramsStyle from "@araharah/theme/basemap/liberty-rams.json";
The style object is never re-tinted at runtime — MapLibre receives it as-is.
How the tokens are defined
src/tokens.css holds every value and nothing else does. Its header records the vocabulary as copied verbatim from the frontmatter of stitch_minimalist_route_navigator/telemetry_rams/DESIGN.md, so mockup markup maps 1:1 onto utilities (bg-signal-orange, px-margin, gap-space-md).
@theme static— Tailwind v4 tree-shakes unused theme variables by default;staticforces every token into the compiled output so the package stays consumable as plain CSS custom properties, including by the basemap derivation script.- Radius at absolute zero — every
--radius*key is0px. The deprecated bare--radiuskey is set too, because that is the key v4 drives bareroundedfrom. - Rams-forbidden primitives disabled at the key —
--shadow,--shadow-inner,--drop-shadow, and--blurare all set toinitial, soshadow-*/blur-*utilities cannot compile from any source. - Typography —
--type-*compound tokens follow the grammarweight size/line-height family…, each paired with a-trackingcompanion. --shell-bg/--shell-on-surface— declared insrc/theme.css, mapping the OS color scheme onto shell-level surfaces only; components and cartography stay Rams-constant under both Telegram themes.
The four mockup screens under stitch_minimalist_route_navigator/ are the read-only class vocabulary reference. src/theme.css lists them as explicit @source paths and excludes scripts/ and basemap/ from the auto-scan, so prose can never mint a utility class. The mockup files themselves are never edited.
Audit
scripts/audit.mjs treats "zero off-token values" as evidence rather than an eyeball claim. It parses src/tokens.css into an allowlist, then scans rendered CSS — dist/rams.css plus the inline <style> blocks and style="…" attributes of every registered implementation page — and flags:
- hex /
rgb()/hsl()/ named-keyword /oklch()-family color literals that are not token values - font families outside the sanctioned stacks, and any literal
font:shorthand - spacing numerics on
padding/margin/gapthat do not equal a--spacing-*token - typography numerics (
font-size,font-weight,line-height,letter-spacing) outside the--type-*scale - any non-zero
border-radius, any non-noneshadow, any blur orbackdrop-filter
Seven scope decisions are documented in the script header; the notable ones: Tailwind's own @layer properties / theme / base are plumbing and skipped, @layer utilities rules are only audited when the selector names a class a registered implementation page actually wears, and rounded-full's full-circle primitive is treated as spec geometry like the 18×18 checkbox square.
The page registry lives at scripts/audit.mjs:118 — IMPLEMENTATION_PAGES. Any new page that renders Rams components must be appended there, or the audit will exit 2 on a missing path.
npm run audit -w theme
node scripts/audit.mjs --tokens ./src/tokens.css --pages src/sheet.html ./dist/rams.css
Basemap derivation
basemap/build.mjs fetches the OpenFreeMap liberty vector style and forks it into Telemetry Rams cartography. Every cartographic color is derived from tokens.css inside the script — there are no hand-tuned hexes in the output JSON, only the derivation parameters (saturation multipliers, per-role lightness targets).
- Derivation core —
desaturate(tokenHex, satMultiplier, lightness): hue genes come from the source token, saturation is clamped into a calibrated[0.06, 0.12]band, lightness snaps to the role target. Ground ismix(soft-beige, chassis-subtle). - Accent guard —
signal-orange,functional-red, andolive-greenare excluded by token name and by hex value; the build fails if any of the three (hex orrgb()form) appears in the output layers. Those accents are reserved for the route line and hazard semantics that later phases render. - Hierarchy by weight only — zoom-interpolated color ramps flatten to role constants while width/opacity ramps are preserved.
- Labels — matte-black at reduced opacity with a ground-tone halo and
text-halo-blur: 0. building-3dremoved — fill-extrusion is faux 3D, rejected by the design system. The removal is recorded instyle.metadata.rams-removed-layers.- Font probing — the intended Rams label stacks are probed against the OpenFreeMap glyphs endpoint at build time. When they are not hosted (the Phase 1 case), the closest hosted stack is substituted and the substitution is recorded in
style.metadata.rams-font-substitutionsas honest fallback provenance.
The generated style also carries a full derivation report in style.metadata.rams-cartography: every role with its color and the token recipe behind it, the excluded accents, and the list of untouched layers.
npm run basemap -w theme # rebuild liberty-rams.json (needs network)
npm run basemap:preview -w theme # serve theme/ on http://localhost:8123/basemap/
Gallery and captures
scripts/screenshot.mjs launches chromium, serves theme/ on an ephemeral loopback port as docroot, and writes six PNGs to theme/screenshots/ (gitignored):
| Capture | Source | Viewport |
|---|---|---|
sheet-full.png |
src/sheet.html |
390 px, full page |
mockup-<screen>.png ×4 |
the four mockup screens via file:// |
390 px |
basemap-kl.png |
basemap/index.html |
1280 × 800 |
The mockup renders need network (CDN Tailwind and Google fonts in the read-only mockups). On capture failure or an undersized / blank shot, the committed screen.png for that screen is copied in as a fallback — the mockups are reference material and are never edited. The basemap capture waits for the proof page's own RAMS-BASEMAP-READY console signal before settling on tiles. Missing, undersized (< 10 KB), or failed captures exit 1.
npm run capture -w theme
npm run gallery -w theme
# → http://localhost:8123/gallery.html and http://localhost:8123/basemap/
gallery.html is the approval-gate mechanism template: every later UI phase adds one comparison row (mockup reference render on the left, Playwright implementation capture on the right). It is CDN-free by construction — only the built stylesheet plus local captures.
npm run gallery ends with a blocking basemap:preview server. Stop it with Ctrl-C.
Notes
dist/andscreenshots/are gitignored build output. After a fresh checkout, runnpm run build -w themebefore openingsrc/sheet.htmlorgallery.html.- Typefaces are self-hosted woff2 subsets under
fonts/— no Google Fonts CDN.font-display: blockis deliberate: no fallback-font flash. - MapLibre GL v4 is loaded from a CDN on
basemap/index.html; the product build wires its own copy. - Windows
*:Zone.Identifiermetadata files may be present in the tree — exclude them from any tooling.
Links
- Root README — monorepo overview
docs/GETTING-STARTED.md·docs/DEVELOPMENT.md·docs/ARCHITECTURE.md·docs/TESTING.mdstitch_minimalist_route_navigator/telemetry_rams/DESIGN.md— the design-system document the token vocabulary is copied fromDESIGN.md— Telemetry Rams philosophy
⚠ VERIFY: the private Gitea clone URL (
http://gitmirah:3000/c-fu/arah2.git) is taken from the user-provided facts and is not verifiable from repository contents alone.
arah²
Packages
Repo
- Source tree
- API — API
- Deployment — Deployment
- Contributing — Contributing
- Core concepts — Core-Concepts