Files

@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.


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; static forces 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 is 0px. The deprecated bare --radius key is set too, because that is the key v4 drives bare rounded from.
  • Rams-forbidden primitives disabled at the key — --shadow, --shadow-inner, --drop-shadow, and --blur are all set to initial, so shadow-* / blur-* utilities cannot compile from any source.
  • Typography — --type-* compound tokens follow the grammar weight size/line-height family…, each paired with a -tracking companion.
  • --shell-bg / --shell-on-surface — declared in src/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 / gap that 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-none shadow, any blur or backdrop-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 is mix(soft-beige, chassis-subtle).
  • Accent guard — signal-orange, functional-red, and olive-green are excluded by token name and by hex value; the build fails if any of the three (hex or rgb() 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-3d removed — fill-extrusion is faux 3D, rejected by the design system. The removal is recorded in style.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-substitutions as 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/

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/ and screenshots/ are gitignored build output. After a fresh checkout, run npm run build -w theme before opening src/sheet.html or gallery.html.
  • Typefaces are self-hosted woff2 subsets under fonts/ — no Google Fonts CDN. font-display: block is 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.Identifier metadata files may be present in the tree — exclude them from any tooling.