28 KiB
phase: 5 slug: routing-route-display status: approved reviewed_at: 2026-10-24 (checker: APPROVED, 2 spacing flags + 1 wording note folded) shadcn_initialized: false preset: none created: 2026-10-24
Phase 5 — UI Design Contract (Routing + Route Display)
Visual and interaction contract for the route experience. Generated by gsd-ui-researcher, verified by gsd-ui-checker.
Source posture: the design system already exists and is IN USE —
theme/dist/rams.css+ tokens, and the live shell (strip, card stack, recenter control, gantry, destination card) shipped in Phase 3 and passed its own UI-SPEC + device gates. This contract specifies extension of that shell: route line layers, the alternatives card + chips surface, the router-pending state, and camera behaviors. It invents no new token values, no new component vocabulary, no new colors. Binding upstream: 05-CONTEXT.md D-01..D-12 (fixture backend, router-pending state, bottom card + chips, orange casing, display-only terminal), the approved 03-UI-SPEC (geometry tables, theme mapping, strip/card/control components, copy discipline, a11y rules — ITS CONVENTIONS BIND), 04-CONTEXT.md (destination hand-off), Phase 1 D-08 (orange = navigation semantics only). Thenavigate_map_overviewmockup is layout reference only; rounded/blur/shadow residue is scrubbed as before.Map-law restated (05-CONTEXT canonical refs): the MapLibre style is never re-set. Route rendering is
addLayer+setDataon sources added once — never re-set; additions only (addLayer/addSource/setData) — zero style re-creation.
Design System
| Property | Value |
|---|---|
| Tool | none — shadcn gate N/A (TypeScript + Vite, no UI framework; unchanged from Phase 3) |
| Preset | not applicable |
| Component library | @araharah/theme — theme/dist/rams.css (already imported via shell.css; nothing new to wire) |
| Icon library | inline SVG — 2px uniform stroke, square linecaps, orthogonal/45° baselines (unchanged; the destination pin glyph joins this family) |
| Font | self-hosted Geist + Space Grotesk via theme.css @font-face (unchanged) |
| Basemap | theme/basemap/liberty-rams.json — Rams-constant under both Telegram themes (unchanged) |
Consumed component classes (all pre-existing; none restyled locally):
| Class | Used for in Phase 5 |
|---|---|
.card, .card-header, .card-ref |
Route card + router-pending card (same builders' discipline as ui/cards.ts / search/destination-card.ts) |
.btn-secondary |
SEARCH AGAIN affordance (route card + pending card) — deliberately not .btn-primary: the route state has no forward action (D-10 forbids a start-guidance button), so no orange CTA fill appears in any Phase 5 surface |
.text-headline-md, .text-body-md, .text-body-sm, .text-label-sm, .text-telemetry-num |
Typography helpers (telemetry-num's sanctioned arrival — see Typography) |
New CSS for this phase (route card body, chips, marker chrome) lives in shell.css and references var(--color-*) / var(--spacing-*) / var(--type-*) only. The machined-geometry literal list grows by exactly: the chip plate (visible 28px tall, hit-slop to 44), the origin ring (24px, 2px stroke), the destination pin glyph (12px square + 8px tail), and the route line widths (map-space px, table below) — 4-multiples, 1px/2px rules, machined glyph geometry, and map-space paint arithmetic (line widths 9/5/3) — the established exception classes.
Shell Layout — Register Bands (extended, D-08)
The Phase 3 partition is unchanged. Phase 5 populates the bottom register persistently for the first time:
| Band | Content in Phase 5 | Geometry |
|---|---|---|
| Top register | Status strip — unchanged (location register only; see Strip note below) | as Phase 3 |
| Center register | Rams basemap + position marker + route line layers + origin ring + destination pin (map content); boot/denied/pending/destination cards (card stack, center placement) | Map stays the dominant surface — the route is drawn ON it, never as a panel |
| Bottom register | Route card (persistent, D-08); signal-lost card (existing, transient); recenter control bottom-right; attribution chip bottom-left | Route card clears the recenter control — see exact offset below |
- Status strip: NO new states. The strip marks the session/location state machine (Phase 3 table stands). Route calculation is a local fixture resolve — no waiting surface, no strip lamp change. The router-pending state is a routing condition, not a location degradation: it gets no red lamp, no strip entry. The route's register is its card.
- No new controls — no layer toggles, no start-guidance button, no progress UI (D-10). The recenter control keeps its Phase 3 behavior exactly (it recenters on the user; the route stays drawn regardless of camera).
- Route card placement: bottom register,
bottom: calc(var(--safe-bottom) + 64px)— the 64px control-clearance composite =space-sm(8, the control's own bottom offset) + 40px (machined control plate) +space-md(16, breathing). Card max-width 320px unchanged; on a 390×844 viewport the card never intersects the control (card bottom edge sits above the control's top edge). - Attribution chip stays flush bottom-left, below the route card's bottom edge — never covered.
Spacing Scale
Bound to tokens (--spacing-*), consumption not invention. Phase 3 table stands; Phase 5 adds these exact numbers:
| Element | Number | Token source |
|---|---|---|
| Route card bottom offset | 64px = space-sm + 40px machined + space-md (composite, documented) |
tokens + machined control |
| Route card padding | space-md space-md (16/16); actions block top padding space-sm |
tokens (unchanged pattern) |
| Chip padding | space-xs space-sm (4/8) → visible height 28px (label-sm 12px line + 2×8) |
tokens |
| Chip hit target | ≥ 44×44px via ::after { inset: -8px } transparent slop (28+16=44) |
a11y exception (4-multiple, Phase 3 precedent) |
| Chip row gap | space-xs (4); chips never wrap (2 max, one row) |
tokens |
| Telemetry row internal gap | space-xs (4) |
tokens |
| Origin ring | 24px diameter, 2px matte-black stroke, no fill |
machined geometry |
| Destination pin glyph | 12×12px square + 8px triangular tail (20px total height); name chip above it, offset space-sm |
machined geometry |
| Camera fit-bounds padding | { top: 96, bottom: 320, left: 64, right: 64 } — map-space px (MapLibre padding, not CSS; same class as Phase 3's 96px = 12×space-sm). Bottom 320 = 64 offset + ≤240 card height budget + 16 slack |
spec geometry (8-multiples) |
Exceptions: none beyond the 44px chip hit target and the map-space camera/line arithmetic (both established exception classes).
Typography
The Phase 3 scale stands. telemetry-num joins exactly as 03-UI-SPEC anticipated ("not used this phase — Phase 5/6"): the route time is its first consumer. Zero new sizes, zero new weights.
| Role | Token | Spec | Usage in Phase 5 |
|---|---|---|---|
| Route time | --type-telemetry-num |
28px Space Grotesk 700 / 32px, tracking −0.03em | Route card time counter (22), the card's data point |
| Card headline | --type-headline-md |
20px Space Grotesk 600 / 26px, tracking −0.02em | Destination name (verbatim gazetteer name — Phase 4 display rule); pending-card headline |
| Card body | --type-body-md |
14px Geist 400 / 20px | Pending-card body |
| Difference / hint | --type-body-sm |
12px Geist 400 / 16px | One-line difference vs best; pending-card hint |
| Chips / units / register | --type-label-sm |
10px Geist 700 / 12px, tracking +0.08em, uppercase | Chip text, MIN/KM unit words, card-header registers, pin name chip |
| CTA label | --type-label-lg (inside .btn-secondary) |
14px Space Grotesk 700 / 18px, uppercase | SEARCH AGAIN |
Unit formats are instrument constants, code-composed like Phase 3's ±12 M telemetry: time 22 MIN (integer minutes), distance 8.4 KM (one decimal), chip {nn} // {min} MIN · {km} KM. Units unchanged under ms (Phase 3 precedent). Body line-height ~1.43, heading 1.3 — token ratios verbatim.
Color
60 / 30 / 10 split unchanged. The route line becomes signal-orange's primary consumer — the Phase 1 reservation fulfilled. The reserved-for list is restated and extended:
| Role | Value | Usage in Phase 5 |
|---|---|---|
| Dominant (60%) | Rams basemap grayscale + shell chrome (unchanged) | The route is drawn over the grayscale world; nothing else competes |
| Secondary (30%) | Instrument surfaces (unchanged) | Route card + pending card: light soft-beige / dark inverse-surface via the existing theme slots; chips idle = matte-black plate |
| Accent (10%) | signal-orange #EB8008 |
Reserved-for list (exact, Phase 5): the route center line (selected route only — the primary consumer); position-marker heading cone; follow-active recenter notch; focus brackets (chips inherit the 3px orange focus rule); boot lamp + gantry; primary CTA fill on Phase 3 state cards (denied card — unchanged); :active press inverts (chips, recenter — existing law); strip boot-stage lamps. |
| Destructive | functional-red #ED3F1C |
Unchanged and NOT extended: denied/signal-lost lamps only. The router-pending state is a documented deferral, not an error — it renders in standard card ink with no red anywhere. |
| Operational | olive-green #736B1E |
LIVE lamp only (unchanged) |
Route-line paint (the D-09 contract):
| Layer (bottom→top) | Color | Width | Opacity | Geometry |
|---|---|---|---|---|
route-casing (selected) |
matte-black #1A1A1A |
9px | 1.0 | line-cap square, line-join bevel (bevel: no miter spikes on sharp OSM angles; square caps match Rams glyph caps) |
route-line (selected) |
signal-orange #EB8008 |
5px | 1.0 | same caps/joins; solid — the mockup's dashed centerline is decorative residue, scrubbed |
route-alt-casing (unselected) |
matte-black #1A1A1A |
5px | 1.0 | half-weight echo of the selected stack |
route-alt-line (unselected) |
soft-beige #D9D2C6 |
3px | 1.0 | reads as the basemap's own road language (dark casing + light fill): quiet, route-shaped, zero accent competition |
- Opacity 1.0 everywhere — physical planarity law: surfaces are opaque; a translucent route is haze, not an instrument.
- Unselected treatment: the beige/black mini-stack. Selected vs unselected differ by weight AND the accent — orange appears exactly once on the map, on the chosen route. Chip tap swaps geometries between the selected and alt source pairs (
setData, instant); layers themselves are static. - Layer ordering: all four layers added inside the existing
map.on("load")handler, AFTER thebuilding-3dblock, eachaddLayerwith nobeforeId→ order bottom→top:route-alt-casing,route-alt-line,route-casing,route-line. Result: the route paints above every basemap layer including building-3d — at pitch 35 the extrusions never clip the product's one accent, and labels/shields are already beneath building-3d (existing Phase 1/3 condition, not ours to reorder). DOM markers (position marker, origin ring, destination pin) always paint above all canvas layers. - DSGN-01 audit note: MapLibre paint cannot consume CSS variables — the three hex values are copied verbatim from
tokens.cssas build-time constants (thebuilding-3dprecedent: "zero invented colors"); the Phase 1 off-token sweep pattern covers them. - Sources: one GeoJSON source per role —
route-altandroute(or four sources if the executor prefers one per layer) — added empty at load, filled/cleared viasetData. Sources are data, not style: this is not style mutation.
Origin Ring + Destination Pin (map furniture, D-09)
| Element | Geometry | Color / ink | A11y |
|---|---|---|---|
| Origin ring | 24px diameter circle, 2px solid matte-black stroke, no fill, centered on the route geometry's first vertex — the route's actual origin, which may sit meters from the live fix (fixture world). The position marker (dot + cone + accuracy circle) renders unchanged beneath it |
matte-black, subdued by design — no accent, no fill |
decorative, aria-hidden |
| Destination pin glyph | 12×12px matte-black filled square + 3px triangular tail pointing down at the destination coordinate (square linecaps, orthogonal family — the recenter-glyph's sibling). Never orange — the pin is venue semantics, orange is route-only (D-09) |
matte-black fill, 1px pure-white inset rule (reads on dark map tones) |
glyph aria-hidden |
| Destination name chip | matte-black plate above the pin, offset space-sm; pure-white label-sm uppercase text = venue name verbatim; single line, max-width 160px, ellipsis overflow (the card carries the full name) |
instrument-plate precedent (attribution chip, card-ref) |
real text — readable as-is, no aria needed |
| Endpoint join | route line terminates under the pin; origin line starts inside the ring — no floating line ends | — | — |
Route Card + Alternatives Chips (D-08, exact geometry)
The card is the selected route's surface; the chips are the alternates. Built via the existing card-stack showCard(card, "bottom") mechanism — one card visible at a time, instant swaps.
Structure (top→bottom):
| Slot | Class | Content |
|---|---|---|
| Register | .card-header › .card-ref |
ROUTE — or ROUTE {n}/{N} when alternatives exist (selected index of count). Instrument vocabulary, en under both locales |
| Headline | .text-headline-md |
Destination name, verbatim (the continuity anchor from the destination card) |
| Telemetry row | flex, gap space-xs |
Time number in .text-telemetry-num + MIN in label-sm; distance 8.4 KM in label-sm, baseline-aligned |
| Difference line | .text-body-sm |
One-line difference vs best (copy table below) |
| Chips row | role="group", flex, gap space-xs |
Up to 2 alternate chips (N−1 when N routes; single-route draws render no chip row at all) |
| Actions | .card-actions |
One .btn-secondary: SEARCH AGAIN (the D-10 restart affordance) |
Height budget: ≤ 240px (content contract enforces it: one-line register, one-line headline, one row of chips). This budget is what the 320px camera bottom padding derives from.
Chip contract:
| Property | Value |
|---|---|
| Element | <button type="button"> — never a div-with-onclick |
| Visible plate | matte-black background, pure-white label-sm uppercase text, padding 4/8, visible height 28px, zero radius |
| Text | {nn} // {min} MIN · {km} KM (e.g. 02 // 24 MIN · 9.1 KM) — one line, fixed format, never truncates |
| Hit target | ≥ 44×44px via ::after { inset: -8px } transparent slop |
Press (:active) |
instant invert to signal-orange field + matte-black text (mockup pill precedent, tactile-depression law) — transient press feedback only; there is no persistent "selected chip" state: the selected route lives in the card, the tapped route becomes the card, the previously-selected becomes a chip. Chip count is invariant (N−1) |
Focus (:focus-visible) |
3px signal-orange bracket (.toggle-option precedent) |
| Selection behavior | tap → geometries swap between selected/alt sources (instant setData) → card re-renders with the new route's time/distance/difference → camera refits the new route's bounds (400ms, below). No easing, no crossfade |
| A11y | aria-label = clean localized form (ariaRouteChip template — copy table); the group carries aria-label (ariaRouteAlternatives) |
Map is not the a11y surface for route data: the line layers and ring/pin are canvas/DOM decoration; the card is the semantic statement of the route (time, distance, difference, name). Chips are the only interactive route elements.
Camera Behavior (D-09)
| Event | Camera action | Parameters |
|---|---|---|
| Route drawn (auto: live fix + resolved destination → fixture resolves) | fitBounds over the selected route geometry |
padding { top: 96, bottom: 320, left: 64, right: 64 }, duration 400ms, default easing family — identical discipline to follow.ts (CAMERA_PADDING_PX/RECENTER_DURATION_MS constants; the route adds its own padded constants beside them) |
| Chip select (alternative chosen) | fitBounds over the newly selected geometry — always refits (D-10: selection redraws + camera refits), idempotent when bounds are near-identical |
same parameters |
| SEARCH AGAIN | clears layers, returns camera to the user: flyTo latest fix via the existing follow controller, following resumes |
Phase 3 recenter parameters (96px, 400ms) |
| User pans/zooms during route display | existing movestart rule → free mode; the route stays drawn |
no change |
| Recenter tap during route display | recenters on user; route stays drawn | no change |
| Pitch/bearing | untouched by fitBounds — center+zoom only; pitch stays 35, bearing stays as the user left it | — |
Camera moves are map-viewport behavior — permitted motion; the chrome never animates (Phase 3 Micro-Behaviors law stands).
State Coexistence — Destination Card ↔ Route Card (04 hand-off)
The card stack renders exactly one card per state; transitions are instant hard swaps. Route calculation triggers automatically — no route button exists (D-10): once the location chain is live AND the destination resolved, the fixture RoutingSource is consulted.
| App state | Center register | Bottom register | Map layers | Camera |
|---|---|---|---|---|
| Live, destination resolved, route not yet consulted | destination card (Phase 4, unchanged) | — | empty | follow user |
| Live + route resolved | (empty — the map is the surface) | route card | selected stack + alt mini-stacks + ring + pin | fitBounds route |
| Live + route pending (D-03) | router-pending card | — | empty (nothing fake is ever drawn) | follow user (unchanged) |
| Destination unavailable (Phase 4 card) | unavailable card | — | empty | follow user — no route request is ever made |
No destination (?dest absent) |
(Phase 3 behavior byte-identical) | — | empty | follow user — no route request |
| Location stale during route display | — | route card persists | route stays drawn; position marker dims 40%, strip lamp red (Phase 3 signals) | no refit |
| Location stale during pending | pending card | signal-lost card (coexists — different registers) | empty | no change |
Staleness never evicts the route card: the drawn route is static fixture data; staleness is a live-position condition and keeps its Phase 3 signals (dimmed marker, red lamp). Evicting the terminal surface for a location note would break D-10.
Router-Pending Card (D-03 — the denied-card class, full copy table)
A deliberate Rams surface for arbitrary origins/destinations the fixture corpus does not cover. Same anatomy as the denied card: register, headline, body, hint, CTA. Center register. Clinical, honest, calm — the app states that routing isn't wired for this trip, offers the way back, and never apologizes theatrically. Never a blank map, never a fake route, never red (it is a deferral, not a failure).
| Element | en | ms (Bahasa Melayu) |
|---|---|---|
| Register | ROUTE // ROUTER |
ROUTE // ROUTER (instrument vocabulary) |
| Headline | Routing not wired up yet |
Penghalaan belum dipasang |
| Body | Routes currently cover known KL landmarks only. The general router ships later — your coordinates were never sent anywhere. |
Laluan buat masa ini meliputi mercu tanda KL yang diketahui sahaja. Penghala am akan datang kemudian — koordinat anda tidak pernah dihantar ke mana-mana. |
| Hint (body-sm) | Send /cari in the chat and pick another landmark. |
Hantar /cari dalam chat dan pilih mercu tanda lain. |
CTA (.btn-secondary) |
SEARCH AGAIN |
CARI SEMULA |
The body's privacy clause is the brand speaking (D-01: origin+destination never go to a third party — the fixture world makes this literally true, and the card says so once, without preaching).
Copywriting Contract (copy tone: clinical, honest — D-01; en + ms; all keys join web/src/i18n/strings.ts, never inline — COPY5 gate)
New ShellStrings keys (compose with the Phase 3 keys; register labels and unit formats are instrument constants in both locales):
| Key | en | ms |
|---|---|---|
routeRegister |
ROUTE |
ROUTE |
routeDiffFastest |
Fastest route |
Laluan terpantas |
routeDiffSlower (template) |
+{min} MIN VS ROUTE {n} |
+{min} MIN VS LALUAN {n} |
routePendingRegister |
ROUTE // ROUTER |
ROUTE // ROUTER |
routePendingHeadline |
Routing not wired up yet |
Penghalaan belum dipasang |
routePendingBody |
(table above) | (table above) |
routePendingHint |
(table above) | (table above) |
ctaSearchAgain |
SEARCH AGAIN |
CARI SEMULA |
ariaRouteAlternatives |
Alternative routes |
Laluan alternatif |
ariaRouteChip (template) |
Route {n}: {min} minutes, {km} kilometres |
Laluan {n}: {min} minit, {km} kilometer |
Copy formats composed in code (Phase 3 telemetry precedent, not strings): time {n} MIN, distance {n.n} KM, chip {nn} // {min} MIN · {km} KM, register ROUTE {n}/{N}.
- Primary CTA: none this phase — deliberate (D-10). The strongest affordance is
.btn-secondarySEARCH AGAIN; orange CTA fill is absent from every Phase 5 surface. - Empty state: the empty map with no route is not an empty state — it is the Phase 3/4 live state (unchanged). The only new "nothing to show" surface is the pending card, fully specified above.
- Error state: router-pending IS the honest error-adjacent state; fixture resolves cannot fail at runtime (committed geometry + tolerance gates). A route-source exception renders the pending card (fail-honest, never blank, never a half-drawn line).
- Destructive actions: none. SEARCH AGAIN discards nothing but the view (routes are in-memory; nothing persists to delete — D-12). No confirmation surfaces.
Copy gate: the strings-module test asserts exactly these keys in en + ms; no route copy inline in web/src/route/* or shell.css.
Transitions & Micro-Behaviors (inside Rams rules)
- No CSS
transition/animationanywhere in chrome — unchanged law. Destination card → route card, route card → chip-swap re-render, route → pending: instant hard swaps. - Route draw appears as: layers populated (
setData) + camerafitBoundsin the same tick — the line arrives with the camera glide (map motion, permitted), chrome never moves. - Chip select: instant geometry/card swap + 400ms camera refit. Press invert on the chip is instant (
:active), no transition. - SEARCH AGAIN: sources emptied + markers removed + card swap +
flyTouser, all instant except the camera glide. - No blur, no shadow, no glass, no pulse on the route line. A drawn route is static; only selection changes it.
BackButton Contract (Phase 5 — first real two-state walk, PX-01 groundwork)
Phase 3's single-state wire (overlayPresent() => false → close()) extends. The route state is the app's first pushed state; the walk-back is the same total function SEARCH AGAIN invokes.
| App state | BackButton | Click behavior |
|---|---|---|
| Boot stages | hide() (unchanged) |
— |
| Destination card (root state) | hidden — Telegram-native swipe-down close remains the exit | — |
| Route state | show(); onClick → walk back |
clears route layers + ring + pin, card swaps to destination card, camera refollows user, hide() |
| Router-pending state | show(); onClick → walk back |
card swaps to destination card, hide() (no layers to clear) |
| Destination-unavailable | hidden (root-equivalent; nothing pushed) | — |
No dead ends: the button is visible exactly when a pushed state exists, and its handler always walks to the root — never a no-op, never close() from a pushed state. The overlayPresent predicate becomes "route or pending state active"; the handler and predicate ship as one change so Phase 6 extends the graph rather than reinventing the wire.
Capture Profiles (extends web/scripts/capture.mjs + the Mock-Telegram harness)
New harness knob and legs; all existing legs unchanged:
| Artifact | Profile | Asserts |
|---|---|---|
Harness knob ?fixpair=<pair-id> |
the mock's Telegram fix delivers the fixture pair's pinned public origin coordinate (fixtures are build-time public data — no user coordinate involved, D-12) | pairs with the existing ?dest=<golden-id> carrier |
shell-route-fixture.png |
mock=android&dest=<golden-id>&fixpair=<pair>, light theme: route drawn, route card, alt chip(s), ring + pin |
ARAHARAH-ROUTE state=drawn signal (state token only — no coordinates in console, D-15 hygiene) + DOM check on rendered card text |
shell-route-alt.png |
same, after a scripted chip tap | selected geometry swapped: orange line follows the alt corridor; card re-rendered; ROUTE {n}/{N} register updated |
shell-route-pending.png |
mock=android&dest=<supported-id-outside-corpus>&fixpair=<pair> |
ARAHARAH-ROUTE state=pending + pending card visible |
shell-route-dark.png |
route-fixture profile, scheme=dark |
PX-02: chrome re-slots, route + basemap Rams-constant |
route-gallery-compare.png |
DSGN-02 side-by-side vs navigate_map_overview/screen.png (route visible in both columns) |
fidelity artifact for the phase gallery |
Bundle-budget note (D-11, planner-facing): the fixture geometry + route-model code joins the web bundle under the ≤ ~200KB gzipped BA gate — the capture harness is not the enforcement point, but the route legs must run against the production-shaped bundle (harness build, same as Phase 3).
Fidelity Artifact (DSGN-02 obligation)
Phase exit extends the Phase 1/3 gallery pattern: the five route captures above beside navigate_map_overview/screen.png, reviewed for fidelity before phase close. Where this document, the mockup markup, and tokens.css disagree: tokens.css wins, the mockup loses, this spec records the call. Recorded calls: mockup's dashed route centerline → scrubbed (solid orange per D-09); mockup's 9×36px control plates → existing 40×40 machined control (unchanged); mockup's telemetry-deck/speedometer furniture → Phase 6+ scope, not this phase (D-10 display-only).
Registry Safety
| Registry | Blocks Used | Safety Gate |
|---|---|---|
| shadcn official | none (not initialized; gate N/A for the no-framework stack) | not required |
| third-party | none — fixtures ride in the web bundle; map tiles remain an OpenFreeMap runtime data fetch, not a code dependency | not required |
Checker Sign-Off
- Dimension 1 Copywriting: PASS
- Dimension 2 Visuals: PASS
- Dimension 3 Color: PASS
- Dimension 4 Typography: PASS
- Dimension 5 Spacing: PASS
- Dimension 6 Registry Safety: PASS
Approval: pending
Phase: 05-routing-route-display Contract drafted: 2026-10-24 by gsd-ui-researcher Consumes: theme tokens (binding), liberty-rams.json layer order, 05-CONTEXT.md D-01..D-12, 03-UI-SPEC.md (binding conventions), 04-CONTEXT.md (destination hand-off), Phase 1 D-08 orange reservation, telemetry_rams/DESIGN.md
Post-approval addendum (2026-10-24, user feedback)
Card body padding fixed in the live shell (web/src/shell.css, deploy d5a224d5):
.card direct text children now carry token-only inline padding (space-md) + vertical
space-sm rhythm; the header plate keeps its own. The route card + chips + pending card
inherit this — the executor MUST NOT add per-card padding overrides; compose from the
existing classes.