CLAUDE
CLAUDE.md — Claude Code guidance for this repository
Section titled “CLAUDE.md — Claude Code guidance for this repository”If you are not Claude Code, read AGENTS.md. The two files contain the same rules; this one uses Claude-specific phrasing and tool references.
What this repo is
Section titled “What this repo is”The master Rechaj design repository at design.rechaj.com. Single source of truth for every Rechaj product — designs, prototypes, PRDs, changelogs, API references, and agent skills.
⛔ Creating or modifying a flow — MANDATORY ritual (read before writing any flow HTML)
Section titled “⛔ Creating or modifying a flow — MANDATORY ritual (read before writing any flow HTML)”Every flow must match the platform’s source-of-truth app so designs stay consistent across the many people who author them. Do NOT hand-roll a shell, tabs, table, buttons, etc. from memory — you will drift and the change will be rejected by CI.
Source of truth per platform (copy these apps’ components verbatim; PATTERNS.md is the written index, these files are the ground truth):
| Platform | Canvas | Source-of-truth apps |
|---|---|---|
| Web | 1440, .sidebar+.topbar+.pg-wrap, document scrolls |
Polaris products/polaris/screens/dashboard.html (shell, .pseg tabs, .pstat KPIs, detail pages) + Caesar products/caesar/screens/dashboard.html (.tbl table system, .dlg dialogs, toast()) |
| Mobile | 390×844 .phone, dark backdrop |
Kit Assembler, Production QA (products/rechaj-kit-production-app/flows/…), Battery Onboarding Operator (products/rechaj-inventory-app/flows/battery-onboarding-operator/) — rsm-* scaffold, bottom sheets, .rsm-nav |
| Tablet | 834×1194 .tablet + viewport-fit IIFE |
Production Manager (…/rechaj-kit-production-app/flows/production-manager/), Procurement Manager, Inventory Manager (…/rechaj-inventory-app/flows/…) — pm-*/im-*/km-* over rsm-*, bottom sheets |
Full verbatim component extractions of all seven live in patterns/canon-source-extractions/.
The template is enforced, not just recommended. web-undefined-class fails any file whose markup uses a class no CSS rule defines — which is exactly what a half-transplant leaves behind: an atom’s markup copied in without its styling. It caught .td-num and .rank-rank (used on 9 cells, defined in no file in the repo, so those numbers were never aligned), .pg-back-t and .loc-name (invented wrappers, 28 uses across the repo), and 58 shell classes whose rules were left behind when a topbar/Settings/Notifications block was copied over. atlas-shell-parity greps five markers in the markup and passes every one of those. The 173 pre-existing findings are baselined; a swept flow and the starter template are at zero tolerance.
Composition is enforced too, not only atoms. A flow can use every canonical atom and still be wrong, so five rules judge the composition the atoms are arranged into: web-ranking-no-bar (a card whose own title calls it a ranking, rendered as bare digit columns — it reads the renderer that fills the card’s tbody, not just its markup, because a file-level search for .cbar passes as soon as any other card has a bar), web-table-card-no-head (a table card with no .tbl-card-head, so its title/count/controls ended up in a .section-header above it), web-card-sub-inline (a .card-sub under a .card-title that is not display:block — shipped once as the literal string “this week1 cluster”), web-colgroup-col-count (a <colgroup> whose <col> count no longer matches the <th> count, which is what puts a scrollbar under a table), and web-chart-no-hover (an svg.chart-line with no data-tip-rows anywhere). The last two are regression guards — the repo currently satisfies them, and both were proved against a synthetic bad case rather than trusted because they reported zero.
The ritual — do all seven, in order:
-
Sync to current
uatfirst — and again before you push:git fetch origin && git merge origin/uat(or run the/sync-uatskill). Everyone authors flows in parallel, so a branch that sat open is already behind canon and will collide at PR time. If a file you’re about to modify was rewritten onuat, do NOT blind-merge — open the current version and re-apply your change on top of it (see/sync-uatfor the mechanical-vs-rebuilt distinction). CI branch protection isstrict: a behind branch cannot merge.Conflicts in a design-swept flow resolve in the sweep’s favour —
uat’s structure wins, then re-apply your feature on top of it. Never the other way round: a flow reverted to its pre-sweep structure still uses canonical atoms, so the lint passes, the reviewer sees a clean file, and the loss only resurfaces as the same design feedback the sweep already answered. Losing your feature instead is obvious on first load. Take the swept side by naming the ref —git checkout origin/uat -- <path>— because--ours/--theirsinvert between a merge and a rebase (verified: ingit merge origin/uat,--theirsisuat; ingit rebase origin/uat,--theirsis your branch). Then prove both survived:flow-diffat 0 gaps, lint at 0 errors, and load the page to confirm your feature is still there. -
Start from the starter template, never a blank file:
templates/web-flow-starter.html·templates/mobile-flow-starter.html·templates/tablet-flow-starter.html. It ships the canonical shell, tokens, tabs, table, dialog/sheet, toast and buttons already wired — fill in content only.The web starter now LINKS the design system instead of inlining it:
<link rel="stylesheet" href="/tokens/tokens.css"/> <!-- generated from tokens.json --><link rel="stylesheet" href="/components/rechaj-ds.css"/> <!-- the canon components -->Keep both links, and put only flow-local CSS in your
<style>block. A new flow therefore cannot have a half-transplant — there is nothing to transplant, so the whole class of “markup copied without its CSS” defect does not exist for it. Fixing a canonical component is now one edit tocomponents/rechaj-ds.cssinstead of a sweep across 36 files. Both hrefs are root-absolute because that is how the served site resolves them (site/public/tokensandsite/public/componentsare symlinks to the repo dirs) — so view flows through the dev server or design.rechaj.com, not overfile://.Migrating an EXISTING flow onto the shared sheet is NOT a codemod — it needs A/B proof per flow.
scripts/link-ds-css.mjslifts only rules byte-identical to canon, which sounds safe and is not sufficient: linking the full sheet is additive, so a flow that diverged by omission silently acquires canon properties it never had. Measured oncluster-maintenance-manager: canon.tblsetstable-layout:fixedwhere that flow had been running on theautodefault, so its table went 1088px → 825px, a header 119px → 78px, and “Fail / 30d” began clipping. Byte-identity cannot detect this — there is no conflicting declaration, the property was simply absent. A first attempt at batch-migrating 33 flows produced 36 new layout failures for exactly this reason and was reverted.So the rule is: migrate one flow, then prove it renders identically —
Terminal window node scripts/link-ds-css.mjs <file>PLAYWRIGHT_DIR=… node site/scripts/probe-ab.mjs <file> # NEW must be 0and keep the migration only when
NEW: 0. Where it is not 0, the difference is a real pre-existing divergence to fix deliberately (usually a missing<colgroup>once the table isfixed), not something to absorb into a refactor. The first screen must be the dashboard — no login gate (see the “No login gate” hard rule below): the flow must open directly on its main app screen, not a sign-in screen. -
Read the platform’s Page Anatomy + atom specs in PATTERNS.md for the form factor you’re building.
-
Self-check every atom before you call it done (see checklist below). Fix the shell/tabs/table/buttons/badges/KPIs/overlays to canon — not “close enough”.
-
Diff against the reference, and drive it to ZERO.
Terminal window node site/scripts/flow-diff.mjs <yourfile> # vs the starter templatenode site/scripts/flow-diff.mjs <yourfile> --ref <sibling.html> # vs a swept siblingThis is the method that worked, made mechanical. Three sweeps of the same flow shape: the first took 15 rounds of the design lead’s feedback; the second used regex surgery with no reference diff and came back “so many discrepancies … not happy at all”; the third copied a known-good reference and then drove this diff to zero — right in one pass, with no agent and no judgment involved. It reports (a) every canonical atom whose markup you used but whose CSS you did not copy, and (b) every structural invariant the reference holds that yours breaks. On that redo it caught a
.card-subwith nodisplay:block(a title and count rendering as “this week1 cluster”), a 10-column<colgroup>on a 7-column table (the scrollbar), filters still in a bar above the card, and a search outside the card head — none of which the linter or a careful read had flagged. A reference that fails its own invariant is reported first: fix the reference, it is the source of truth.This BLOCKS on every PR (
lint-canon.yml, diff-scoped to the flows you changed), so a new flow with a structural gap cannot merge. Two carve-outs live in the script, not in the workflow:flow-diff-baseline.jsonrecords the flows with known remaining drift by gap identity, so those specific gaps are tolerated while anything else in the same file still fails — and placeholder pages (no shell, ~2.3KB) are skipped via the linter’s owndetectPlatform, so both tools agree on what a web flow is. When you sweep a flow, delete its baseline entry in the same commit. There is deliberately no bulk re-record flag: that is how the canon baseline once absolved 57 findings in a file the sweep never touched. -
Run the lint and get 0 errors:
node site/scripts/lint-canon.mjs <yourfile>(or justnode site/scripts/lint-canon.mjsfor changed files). CI runs this on every PR intouat/main; a file with canon errors cannot merge. The gate covers flows, standaloneproducts/*/screens/, andproducts/*/prototypes/— the same canon applies to all three. (Screens/prototypes were previously unlinted, which is how the Swap Super Admin shipped a hand-rolled shell; the lint now also flags a.sidebar/.topbarwith no canonical.sb-item/.crumb-toggle.) Sweep-staged rules are grandfathered by a baseline. They describe drift that predates the canon sweep and still sits in the unswept flows. The set grows as rules are added, so it is not listed here —node site/scripts/lint-canon.mjs --list-stagedprints the current one, and the run header prints how many there are. They are warnings up to the count recorded insite/scripts/baselines/canon/for that (file, rule), and ERRORS above it — so a swept flow (baseline 0) fails CI the moment it regresses, and a backlog flow can’t get worse, without blocking anyone on drift they didn’t introduce. After you sweep a flow, name the file explicitly:node site/scripts/lint-canon.mjs --update-baseline products/atlas/flows/<your-flow>/index.html, then commit the baseline so the flow graduates to zero-tolerance. That form merges — it only touches entries for the files you named and leaves the rest of the backlog alone. Never add--all:--all --update-baselinere-records every file in the repo and grandfathers every unrelated violation (it once silently absolved 57 in a file the sweep never touched). The script now refuses that combination outright, so this is a guard rather than a request. Never baseline a file that was already ERRORing — that silently weakens a gate which was doing its job; leave it failing. And never baseline a failure your own diff introduced. The baseline is for debt you INHERITED; recording your own regression ships the defect and teaches the gate to accept it. A clipped header wants a shorter label or one fewer column, not a wider tolerance.probe-ci.mjsnow blocks this outright — it compares your baseline against the base ref for every flow your branch changes, by string rather than by count, so widening one entry ("… — Onboarded · vs prior"→"… — Onboarded · vs prior, Account status") is caught too. If a failure genuinely only exists on the CI runner and cannot be fixed from your machine,PROBE_ALLOW_SELF_BASELINE=1allows it and announces it in the log — say why in the PR. Note also that--update-baselinemerges: it never removes an entry your machine cannot reproduce, because ~3% of font-metric difference between macOS and the runner makes that routine. Use--prune-baselinewhen you are certain something is fixed.Archived flows are exempt from the gate. A flow marked
archived: trueinsite/src/data/products.tsis skipped bylint-canon,flow-diffandprobe-ciwhenever they pick files automatically (CI’s diff scope, or--all). Naming the file yourself still lints it — the exemption is for the automatic scopes, not for a direct question. Every skip is printed, never silent.The reason: the gates are diff-scoped, so without this a one-line bug fix to retired work demands a full canon sweep of something nobody ships. That is not hypothetical —
field-support-cfsm’s sign-in screen was covering its own app, the fix was one CSS declaration, it provably changed nothing the linter measures (32 errors / 133 warnings before and after, same rules, same counts), and it would still have blocked the PR on drift that predates the sweep. The gate exists to stop drift entering LIVE work.The flag lives in the catalog, not in the file, so exempting a flow and hiding it from /flows are the same act — an in-file marker would have let a live flow dodge the gate with one comment. Un-archiving puts it straight back under the gate, which is the property that makes the exemption safe. The shared helper is
site/scripts/archived-flows.mjs.Two page-level rules worth knowing before you copy a page head. A top-level page carries no subtext — no
.pg-subunder the.pg-title(web-page-title-subtext); an inner page, identified by its.pg-back, may keep one as record identity. And a tab strip in a table card head is a filter, so it takes.filt-select, not.pseg(web-pseg-in-table) — where tabs genuinely read better, such as “By zone / By rider” over one measure, declare it on the element withdata-canon="allow-pseg-filter"so the decision is visible in review rather than argued twice. A flow’sdesign.mdis linted too — not against the canon (it’s prose), but against its own siblingindex.html. If the doc names a#pg-*page that no longer exists that is an error; a.classthat no longer exists, or prose describing a banned side drawer, is a warning. Both canon layers read the HTML and neither read the paragraph beside it, so docs went stale invisibly. When you change a flow, update itsdesign.mdin the same commit. To name a component deliberately without tripping the check (a banned class, a not-yet-adopted one), write the mention as a contrast — “never.tab-bar”, “.dw-rowwas replaced by.td-row”. -
Probe every view, then ratchet. Lint is only the file. Load the flow in the Browser pane and run the runtime/layout probe on every view — not the ones you touched:
/probe-flow products/atlas/flows/<your-flow>/index.htmlThat skill opens the flow in the Browser pane, runs the probe over every view (plus the filtered states and dark mode) and reports the failures. It costs nothing but the turn — no
design-revieweragent, no token bill. By hand it is:eval(await (await fetch('/_static/flow-probe.js')).text());flowViews().map(v => [v, flowProbe({ view: v })]); // all must be pass:trueIt asserts what no file-reader can see: a
hiddenelement a class un-hides,undefinedrendered by avarread before assignment, KPI cards that wrap or double-count, clipped nowrap headers, rows whose only target is the chevron, an unsorted or bar-less “ranking”, ragged text edges (painted glyph via aRange, not the cell box), grid-row cards of unequal height, a chart with no hover target, a popover wider than its trigger, a dialog that traps Escape, reflow on filtering. Not done while any view ispass:false.Then ratchet in the same commit: every finding either becomes a rule in
site/scripts/canon-rules.mjs(pattern-detectable —staged:true+ a baseline refresh when it is pre-existing drift) or a check insite/scripts/flow-probe.js(only visible after layout). Prove a new rule fires on a synthetic bad case and check its repo-wide count: zero hits usually means broken, not satisfied. Prose in DESIGN.md explains; it does not enforce.
Three layers, cheapest first — and the expensive one must not re-derive the cheap ones. Layer 1 —
lint-canon.mjs, ~10s, free. Runs on every PR; it prints its own rule count on every run, so don’t hardcode one here. Proves everything pattern-detectable: the half-transplant tell (web-undefined-class: markup using a class no CSS rule defines) and the composition rules (web-ranking-no-bar,web-table-card-no-head,web-card-sub-inline,web-colgroup-col-count,web-chart-no-hover) that catch a page whose atoms are all canonical but arranged wrongly. Layer 2 —/probe-flowlocally,probe-flows.ymlin CI, free. Runtime + layout: cards that wrap or double-count, clipped nowrap headers, ragged painted-text edges, unequal grid-row cards, a popover wider than its trigger, a chart with no hover, reflow on filtering,undefinedfrom a TDZ read. This now runs automatically on every PR (probe-flows.yml→site/scripts/probe-ci.mjs), headless, over every view of every changed flow in both themes, and blocks on any failure not recorded insite/scripts/baselines/probe/. Until now these checks existed but nothing ran them —/probe-flowneeded a human to remember, which is a habit, not a gate. Run the skill while you are fixing (you can see the page and drive a filter); CI is the backstop. Reproduce a CI finding without the Browser pane:PLAYWRIGHT_DIR=… node site/scripts/probe-ci.mjs <file>.A probe finding is not automatically a flow defect — check the harness first. The first full-corpus run reported 405 failures; 146 of them were bugs in the probe, not the flows.
section[id^="view-"]was tag-qualified and cluster-manager’s views are DIVs, so the selector matched nothing, fell through to a zero-height.pg-wrap, and reported “page rendered: false” on 94 views of pages that render perfectly. Another 52 were navigation misses (showView/showScr/ovTabwere absent from the switcher list) reported as JS errors in the flow. Both are fixed, and a navigation miss now reportsUNMEASUREDrather than pass or fail. The lesson mirrors the existing one about zero hits: a flood of hits often means the harness is wrong. Verify one finding against the real page before you sweep — or baseline — anything. Layer 3 — thedesign-revieweragent, run locally on demand. Judgment only, and it reads Layers 1–2 rather than repeating them: is this the right component for this content, does the page show direction or only levels, do the KPI cards partition the population, does every label describe the number under it, does every filter filter the whole page. It went from 64 checks to 14 when the other 50 moved down — restating a Layer-1 finding here costs real money for an answer already on screen.The judgment-level residue. The lint above (Layer 1) only catches pattern-detectable drift. The judgment-level issues — a custom class re-skinned to look canonical instead of the real component, congested/overflowing tables, misaligned atoms, oversized or non-circular icons, tinted status cards, wrong modal/KPI/tabs — need Layer 2: the
design-revieweragent. Run/design-review <file>on your flow before opening the PR; it sweeps every atom against the live reference apps and fixes drift with the actual canonical components (never a re-skin), without touching features. There is no CI design review. Layer 3 is a local-only step: run/design-review <file>on your branch before opening the PR. The workflow that used to run it on PRs intouatwas removed on 2026-08-25 — it billed a single shared Claude token for the whole team, and once every Atlas flow had been swept andflow-diffbegan blocking on structural gaps, Layer 1 covered what it had been relied on for.canon(lint-canon + flow-diff, ~12s, free) is the deterministic gate. Now that Layer 1 enforces structure (flow-diffblocks on any new gap against the reference) and every Atlas flow has been swept, the deterministic gate covers what this used to be relied on for; reach for Layer 3 on a genuinely new or unusual page, not on routine edits to a swept flow. It reviews the diff’s changed line ranges, not whole files, and preferspatterns/canon-source-extractions/over re-reading the 650-750 KB reference apps — running it on every push over whole files cost one PR seven runs at ~63M tokens / ~$25 each. Run the skill locally first:/design-review <file>both finds and fixes drift and bills your own subscription, where the CI job shares one token across the whole team. Then the CI has nothing to report. When a review turns up a new pattern-detectable issue, promote it to a rule insite/scripts/canon-rules.mjsso Layer 1 catches it for free next time.If your PR is flagged: in Claude Code on your branch, run
/design-review <file>— it reproduces the same findings the CI posted (same agent + same lint) and fixes them to passable code, then re-lints to 0. Or comment@claude fix the canon findingson the PR and Claude fixes them on the branch (.github/workflows/claude-fix.yml). Onlycanonfindings block the merge; the design review is advisory and local.
Layer 0 — the gate runs before the PR exists, on your own machine
Section titled “Layer 0 — the gate runs before the PR exists, on your own machine”Everything above describes checks that run after you open a PR. That is too late
twice over: the author has already context-switched, and a red check is only advice
if nothing blocks on it. So the same canon job also runs locally, and refuses to
let the work leave your machine while it is broken:
| Trigger | Mechanism | Covers |
|---|---|---|
Claude runs git push, gh pr create, gh pr ready |
.claude/hooks/pre-pr-canon.sh — a PreToolUse hook wired in the tracked .claude/settings.json |
anyone authoring with Claude Code, from the moment they clone |
Any git push — terminal, IDE, non-Claude teammate |
.githooks/pre-push |
the paths a Claude hook cannot see |
Both call one script — scripts/canon-gate.sh — which runs
the same five steps as lint-canon.yml, in the same order, over the same diff scope:
build-tokens --check → lint-canon → flow-diff → lint-flow-titles → probe-copy
parity. A local gate that checks something slightly different is worse than none,
because it teaches people that a local pass means nothing.
On a finding the Claude hook exits 2, which is Claude Code’s “block this call and hand my stderr back to the model” contract. The author’s own Claude therefore reads the findings, fixes them at source, and retries — the PR is never created in the broken state. Run it yourself any time:
bash scripts/canon-gate.shFour properties worth preserving if you touch this:
- A deleted design is invisible to every other check. Both CI and this script
are diff-scoped with
--diff-filter=d, which excludes deletions — so a branch whose only effect is that a flow is gone has nothing to lint and nothing to diff. Step 0 therefore compares the base’s file list to the branch’s and blocks on any missing design. It is not theoretical: PR #166 (approved, squash-merged intouaton 2026-08-27) carried a branch cut before two products existed and took 12 files with it — the wholeatlas/field-validation-engineerflow, the wholeproducts/bulwarkproduct, two PRDs and three share cards — of which only 3 appeared as “removed” in the PR’s own file list. Opt in per push withCANON_ALLOW_DELETE=1when a removal is genuinely intended. - Silent when it has nothing to say. It exits 0 immediately when the branch touches no canon-gated file, so infrastructure and doc work never pays for it. One changed flow costs ~2s.
- It installs itself.
scripts/install-hooks.shwas opt-in, so a fresh clone had no git hooks at all — the same failure mode as a label-gated review that nobody labels. TheSessionStarthook now setscore.hooksPathif it is unset, so the pre-push gate exists for everyone the first time they open the repo in Claude Code. - Fail-open on plumbing, fail-closed on findings. If the hook itself cannot run
(no repo, missing script, unreadable payload) it exits 0 and gets out of the way. A
hook that blocks every
Bashcall because of its own bug is a hook the team disables within the hour, and then nothing is gated. Only a real finding blocks. git stash pushis not a push. Sessions sharing a checkout stash constantly; the matcher neutralisesgit stash push,git remote …--push, andgit help pushbefore testing, so only genuine publication is gated.
Layer 2 is deliberately absent here. The runtime/layout probe drives a real browser
over every view of every changed flow in both themes — minutes, plus a Playwright
install. A pre-push hook that takes four minutes gets disabled by the whole team within
a day. probe-flows.yml remains the backstop; run /probe-flow <file> while you are
still fixing.
Emergency bypass, for when the gate itself is wrong and you need to ship:
CANON_GATE_SKIP=1 git push. It prints that it skipped, and CI still runs the same
checks — the bypass buys you a push, not a merge.
The nine gap-closers (2026-08-27)
Section titled “The nine gap-closers (2026-08-27)”Auditing atlas/revenue-assurance-lead — 8 merged PRs, canon green every time,
four kinds of wrong — showed the gate was blind in specific, nameable ways. Nine
rules close them. Each was proved on a synthetic bad case and checked repo-wide
before landing; corpus errors went 324 → 327, i.e. the only new blocking findings
are the three real defects.
| Rule | Severity | Corpus | Closes |
|---|---|---|---|
card-side-accent-inline |
error | 1 | a stripe in an inline style= — card-side-accent reads CSS rule blocks only |
card-shape-side-accent |
error | 1 | a card by shape (border-radius + padding) under any name — both card rules key off the substring card in the selector, so .leak-tile was invisible |
web-flow-default-theme |
error | 1 | <html data-theme="dark"> — nothing looked at a flow’s boot theme |
web-flow-links-ds |
staged | 56 | a new flow inlining the DS instead of linking it; flow-diff cannot tell inlined CSS from linked canon |
web-stacked-tables-not-tabbed |
staged | 2 | two table cards stacked in one page with no .pseg grouping them |
font-weight-700-label |
staged | 79 | 700 on a text label instead of a display numeral |
icon-container-not-circular |
staged | 48 | a non-circular icon container |
em-dash-in-copy |
staged (was a plain warning) | 227 | em dashes in UI copy — a hard rule that had been advisory and ignored |
spacing-off-grid |
warning only | 1836 | off-grid 7/11/15px spacing — see the caveat below |
Four things this got wrong first, all caught by linting the reference rather than trusting a count. Worth knowing because the same mistakes are easy to repeat:
- A staged rule must be declared
severity:'warn'.lint-canon.mjsonly ever upgrades a staged finding (if (seenStaged[f.id] > allowed)) — it never downgrades.severity:'error'+staged:truetherefore errors unconditionally and the baseline cannot soften it. Five rules shipped that way and broke three already-swept flows. (Note: 8 pre-existing staged rules are also declared non-warnand so are effectively hard errors — the corpus happens to satisfy them, but the declaration does not mean what it looks like.) - A rule the design system itself fails is a broken rule, not a broken corpus.
Canon ships
.pg-title{font-weight:700}and.icon-action{border-radius:var(--r-md)}on purpose; flat bans errored on 30 and 53 canonical uses and failed the starter template. Both rules now ban diverging from canon viacanonDecl(), which readscomponents/rechaj-ds.cssand exempts whatever canon declares itself. spacing-off-gridcannot block yet.components/rechaj-ds.csscarries three off-grid declarations (gap:7px,padding:0 11px,padding:11px) and the reference flow carries nine. Per the rule above it stays a warning; promote it toseverity:'warn', staged:truein the same commit that cleans those three out of the DS sheet.- Segment on real containers, and never strip lines you still need to count.
web-stacked-tables-not-tabbedfirst segmented on “any<div>with an id”, so the starter template’s four table cards in four separate.pg-views read as four stacked in one page; and stripping<script>/<style>with''deleted lines, so every reported line number was wrong (it said L16). Fixing both took the corpus count from 17 to 2 — 15 of the original “hits” were the bug. Blank tag contents to spaces, preserving newlines, and skip files with no view container at all rather than guessing.
Also surfaced and deliberately not changed here, since each needs a decision:
templates/web-flow-starter.html fails web-page-subtitle (a .pg-sub at L58, and
it is the reference); the three off-grid declarations in the DS sheet.
Atom self-check (web): sidebar = .sidebar/.sb-item.active; topbar = .crumb-toggle+.crumbs+.icon-action+.user-pill; page head = .pg-head/.pg-title; tabs = .pseg pills (NEVER .tab-bar/.ptab/.tab-group); table = .tbl-card>.tscroll>.tbl with .tbl-card-head; KPIs = .pstat; buttons = .btn-pri (green+#111 text) / .btn-sec / .btn-danger-out (outline red — solid red only in a .dlg confirm); badges = .badge b-*; form fields = .fld block (never the login’s .inp/.form-group); confirmations = centered .dlg; detail opens on row click — a centred .dlg for create/edit/compact, a full details page for a rich record; NO side drawers (web-no-drawer), never pinned open by default; toast = .toast-live/toast(). Mobile/tablet: .phone/.tablet shell, rsm-* scaffold, bottom sheets for all confirmations (never centered modals); filter tabs + chips = white active pill on a grey track + green text (never a dark/black active pill, never underline tabs); headers = centered .rsm-page-head title on every screen except the dashboard, which uses .rsm-hero (never a left-aligned title bar). All platforms: cards have no visible border; no emoji; no em dashes in copy (use ·); 700 weight only on display numbers; 4px spacing grid.
The /flows Screens canvas — verify it after touching a flow
Section titled “The /flows Screens canvas — verify it after touching a flow”The Screens tab is a Figma-style artboard: one section per sidebar page, and
inside it that page’s own states — the page, the detail page a row opens, and each
dialog it can open. None of that is written down anywhere; it is discovered at
runtime by site/public/_static/flow-canvas.js,
which loads the flow once and drives it.
That means a flow can break its own canvas without failing lint or the probe — by renaming a switcher, moving a view into a shared container, or wiring a dialog through a wrapper that needs a selected row. Audited across all 44 Atlas flows, every one of these assumptions was wrong for at least one flow:
| The flows actually do | |
|---|---|
| Nav element | .rsm-nav-tab (318 uses — the most common by far) · .sb-item (216) · .sb-subitem (25) · .nav-item (18). Counting only .sb-item/.rsm-nav-item made every mobile flow report having no navigation |
| Switcher name | showView · goView · navTo · nav · goto · showScr · go · ovTab |
| Switcher arity | navTo('x') and navTo(this,'x') and navTo(el, page) — the key is not always the first argument |
| No switcher at all | Caesar names one dedicated opener per page — openCustomers(), openCOA(), openSettings('organization') — so there is no key to read and the nav item itself is the only handle |
| Listener-bound nav | hermes-web-app is a React app: its modules are DIVs with the handler bound in JS, invisible to any attribute scan. Found structurally (siblings sharing a class, one marked active) and only when nothing else works |
| Page container | #view-x · #pg-x · #v-x · #scr-x · .app-screen · .screen[id] · or none at all (the KaaG family re-renders every page into one shared .pg-wrap) |
| Dialogs | never opened inline — a wrapper populates the dialog then calls openDlg, and some wrappers need a selection that does not exist on a cold load |
| Injected dialogs | Caesar’s 34 modals have no element to find: openModal(html, cls) writes .dlg-overlay into #modal-root. Its closeModal() empties that host on a 160ms timer, so a timer left over from one frame erases the next one’s modal |
A row is a SEQUENCE, not a bucket. A screen you reach by clicking inside another screen — the detail a card opens, the sheet a button raises — is a frame in that screen’s row, in click order, so a row reads “tap this, get this”. Only a screen the flow’s own nav names heads a row. Everything else (entry, utility, orphan, control-inferred) is a candidate that must actually drive at render time or it is dropped, because a frame showing the wrong screen under a confident label is worse than no frame. Chains follow two hops; rows cap at 8 frames and a capped row says so.
Two consequences worth knowing when you author a flow:
- Children are found by CLICKING, not by reading handlers. Forge’s inventory-manager
opens its Receive note with
openReceive('s-movement'), where the argument is the screen to come back to — every name-based guess lands on the wrong screen. So if a control opens a screen, it will be found; if it needs typed input or a prior selection first (adisabledCTA,openRejectArea('ar3')before the record store is populated), it cannot be reproduced from a cold load and is deliberately left off. - Frames grow to their content. A long screen is no longer clipped to the device height — the frame is sized to what actually overflows, capped at 4× nominal. You do not need to design for the artboard; design the screen and the board will show it all.
So after changing a flow’s navigation, views or dialogs, re-run the audit:
npm run build --prefix site && node site/scripts/canvas-audit.mjs --allIt replays every discovered state from a fresh page load and checks what is
actually on screen, exiting nonzero on any defect: a recipe that does not land
where it claims, two frames showing the same state, a modal that opens empty, or a
screen left unattributed. AUDIT_APP=<product> audits another product; name slugs
instead of --all for one flow. It needs the BUILT site served (the dev server
injects scoped styles as module scripts and leaves the page unstyled).
Two rules the canvas holds, and you should keep: never treat “the call did not throw” as successful navigation — verify the destination; and never render a frame that cannot be reproduced from a cold load — an unreachable screen or an unfillable modal is dropped, not shown hollow.
The share thumbnail is generated from the same discovery
Section titled “The share thumbnail is generated from the same discovery”A design shared out of /flows previews with its own card, generated by
scripts/og-shots.mjs into site/public/og/flows/ and
committed. So after a redesign, refresh that flow’s card — the shot is not
rebuilt by npm run build (a browser pass over 87 flows takes minutes):
PLAYWRIGHT_DIR=… node scripts/og-shots.mjs <product> <flow>It needs the BUILT site served, like the audit. Three rules it holds, each learned
from a wrong card: the frame is never the login (recipe.isLogin) and never a
splash — a splash is identified by having no chrome inside the visible view, since
a document-wide search finds the app’s own tab strip sitting under the overlay; and the
page is reloaded before the shot, because discovery has to click and those clicks
leave the page dirty (atlas/cfsm shot as an empty board with a case panel hanging off
the edge, while the flow renders perfectly on a cold load). Placeholder flows get no
card and fall back to the site’s generic image. The card itself is the homepage product
cover (ProductCard.astro) with the flow’s frame as the screen — if you change that
component, change the card with it.
Lookup order — never invent
Section titled “Lookup order — never invent”When asked to design, modify, or reason about any UI or service, follow this order. Stop at the first source that answers the question.
- PATTERNS.md — per-platform widget recipe book. Read this FIRST when starting any new design — identify the form factor (web 1440 / tablet 834 / mobile 390) and pull the canonical widget classes from that section. Eliminates 80% of iteration on widget choice.
- DESIGN.md — global tokens, typography, spacing, hard rules, cross-form-factor invariants.
- tokens/tokens.json — full machine-readable token export. Use when you need a precise value.
- components/ — per-component docs. The 2.6 MB
components/index.htmlis the production component library; grep with the Bash tool, never Read in full. Search for the specific class (.dlg,.ds-btn-primary,.tab-bar,.sb-item,.lp-*,.mhdr-*,.sh-*, …). - patterns/ — cross-product interaction patterns.
- products/<name>/DESIGN.md — product-scoped overrides.
- products/<name>/screens/ — real in-context usages. Tie-breaker for composition. NOT canon — precedent screens may have drifted from PATTERNS.md.
- Ask the user via AskUserQuestion — only if PRD is ambiguous about scope (persona, format), never about widget choice.
Hard rules
Section titled “Hard rules”These override anything you might infer from training:
- Never invent tokens, colors, components, or patterns. If it isn’t in
tokens/tokens.json,DESIGN.md, orPATTERNS.md, it doesn’t exist. - Tokens come from
tokens/tokens.json, and it is now enforced.tokens/tokens.cssis generated from it and holds the canonical:root+:root[data-theme="dark"]blocks;token-value-driftflags any flow that redeclares a DS token with a different value (same name, different colour — invisible in review, impossible to sweep). If a surface genuinely needs a different colour, give it its own name, don’t redefine--bg. This closed a real hole: tokens.json and the flows’ hand-written:rootblocks had been two independent sources of truth and had already diverged on six values. - Color — palette only (mandatory). Every color is a palette token from
tokens/tokens.json/ the color foundations page. No ad-hoc or off-palette colors. The only bare hex allowed are the neutrals#111/#fff/#000and the documented status-chip hexes; everything else must be avar(--token). - Typeface — General Sans only (mandatory). Body/UI is
General Sans(orinherit). Serial numbers / VINs / plates /<code>also stay in General Sans withfont-variant-numeric:tabular-nums; letter-spacing:.02em— no mono webfont is loaded in the DS, so anSF Monostack silently renders as Courier (enforced by themono-font-stacklint). Never introduce another font family. Enforced by thenon-canon-fontlint rule. - No decorative gradients (mandatory). Flat palette-token fills only. The only gradients allowed are functional: progress rings (
conic-gradient) and a sticky-footer scrim that fades totransparent. No multi-hue decorative backgrounds. - Brand mark = exact two-tone SVG in DESIGN.md. Never substitute, recolor, or redraw.
- Brand-green button =
#01E46Fbackground with dark#111111text. NEVER white-on-green for utility buttons. - Confirmations on mobile + tablet use bottom sheets. Confirmations on web use centered
.dlgmodals — never bottom sheets on web. - Mobile prototype shells contain ONLY the device on a dark backdrop. No titles, labels, legends, prev/next, or external chrome. Body is a centered flex container with
background:#0E0F12. - All icon containers are circular (
border-radius:50%). Rounded squares are reserved for photo/media thumbnails only. Exception: canonical.rsm-profile-row-icuses 10px rounded square (documented in Kit Assembler precedent). - All DS icons are OUTLINE style —
fill:none; stroke:currentColor(stroke-width ~1.8–2), never filled. This covers every icon: nav, KPI/tonal frames, list rows, and icons inside buttons. Give each icon<svg>an inlinestroke="currentColor"(the Polaris convention) so a size-only CSS rule can’t drop it. An icon svg with no stroke renders wrong — invisible (open paths) or a black blob — so an icon container becomes an empty frame. Every icon container must show a real glyph; avatars must show initials. The only filled marks are the two-tone brand mark and multi-color logos (e.g. the Google “G”). - Spacing on a strict 4px grid. No 7/11/15px values.
- Weight discipline. Titles and headings are semibold (600). 700 is for display numerals only (KPI/stat digits, wallet/balance amounts, hero numbers) plus splash/onboarding/welcome/success display titles and the sidebar wordmark. Never 700 on a text label — a stat-row value holding a word, a table/column header, a kanban/card title → 600.
b/strongrender at 600. Over-bolding is a common drift; default to 600 and reach for 700 only on a number. - Badges (any form factor): 12px / 500 weight / 6px radius / NO uppercase / letter-spacing -0.01em. The canonical
.ds-badgewidget. - Cards have NO borders —
box-shadow: var(--sh-card)only. Status hero on detail screens = white card + small 36×36 tonal icon, never a full-card tinted background. - No emojis anywhere in the UI.
- No login gate — the first screen is the dashboard. A persona flow must open directly on its main dashboard/app screen. Do NOT ship a login/sign-in screen as the default (first) screen — in a prototype it is dead weight the reviewer has to click through. If you build from the starter template (which demos the canonical login), make the flow boot to the dashboard: the login screen must never be the default-active screen. The ONLY flow that keeps a visible login is the starter template itself, opted out with an HTML comment containing
canon:allow-login. Enforced by theweb-flow-login-defaultlint rule (flags a login screen carrying the defaultactiveclass). The canonical login markup/pattern still lives in the starter template for the rare surface that genuinely needs one. - Mobile flows = 390px width. Tablet = 834×1194. Web/desktop = 1440px.
- Flow titles in
site/src/data/products.tsare short + single-line — aim for ~3 words, hard cap 30 chars. Long names wrap in the /flows catalog cards and sidebar. Put descriptive detail indescription, never thename. Enforced diff-scoped bysite/scripts/lint-flow-titles.mjs(in the CIcanonjob): new or changed titles over the cap fail; the existing backlog is grandfathered.
Where to put generated work
Section titled “Where to put generated work”| What | Where |
|---|---|
| New web/desktop/back-office flow | products/atlas/flows/<persona-slug>/index.html — Atlas is the umbrella product for every Rechaj web surface. Do NOT create a new top-level products/<some-web-tool>/ folder. Assign a sidebar sub-category — set category: 'onboarding' | 'swap' | 'tricycle-hardware' | 'support' on the flow’s entry in site/src/data/products.ts. If which one isn’t obvious, ask the user before placing it. |
| New web/desktop static mockup or dashboard | products/atlas/screens/<kebab-case>.html (sibling to KaaG / MEI / swap dashboards) |
| New web/desktop multi-state prototype | products/atlas/prototypes/<flow>.html (mirrors swap-support-resolution.html) |
| New mobile / tablet screen / mockup HTML | products/<product>/screens/<kebab-case>.html (per-product, NOT under atlas) |
| New mobile / tablet canonical flow | products/<product>/flows/<persona-slug>/index.html |
| New PRD section | products/<product>/docs/prd.md |
| Changelog entry | products/<product>/changelog.md (Keep a Changelog format) |
| New shared interaction pattern | patterns/<pattern>.md |
| New token | tokens/tokens.json, then node scripts/build-tokens.mjs and commit the generated tokens/tokens.css + tokens.ts + tailwind.preset.js. Those three are GENERATED — never hand-edit them; build-tokens.mjs --check blocks the PR if they drift from the source. Give the token a $extensions["com.rechaj.cssVar"] if it should surface as a CSS custom property (and com.rechaj.dark if the dark theme overrides it) — that mapping is what puts it in :root. Reference it from DESIGN.md too. |
| New feature spec | specs/<year>/<slug>/{proposal,design,tasks}.md |
| Mood/aesthetic variant | tokens/packs/<name>.json |
| New agent skill | .claude/skills/<skill>/SKILL.md — Claude Code only auto-discovers skills here, so this path is what makes /<skill> work for everyone who clones the repo |
Phase-aware routing
Section titled “Phase-aware routing”When you delegate via the Skill tool or Agent tool, pick the phase-appropriate model per agents/routing.md:
- Ideate (open exploration) → Claude
- Mockup (design-system implementation) → Gemini
- Refine (iteration / polish) → GPT
- Drift-check (verification) → any
Skills declare phase: and prefers: in their front-matter.
Working with the existing component library
Section titled “Working with the existing component library”components/index.html is 2.6 MB. Do not read it whole. Use the Bash tool to grep for specific patterns:
grep -n "\.lp-" components/index.html | head -20After you extract a pattern from the HTML, update the relevant patterns/<pattern>.md or DESIGN.md so the next task does not need to re-grep.
Figma is being phased out
Section titled “Figma is being phased out”The bet is that DESIGN.md + interactive HTML prototypes are replacing Figma. Use the Figma MCP only to read specific frames during the transition — never fetch full files. New design work should originate in this repo.
Site preview
Section titled “Site preview”The Astro+Starlight site lives in site/. To preview locally:
cd sitenpm installnpm run devThe site reads markdown straight from the repo root — there is no source-vs-build duality. AI agents can fetch the same .md from the GitHub raw URL or from https://design.rechaj.com/<path>.md.txt.