docs(design): add PRODUCT.md + DESIGN.md, drop the unused Geist Sans - #585
Merged
Conversation
Establishes the two design-context files every /impeccable command reads before doing any work, and fixes the one live defect the scan turned up. PRODUCT.md (strategy): register is `product`, with `(public)/*` and the Puck landing blocks flagged as `brand` exceptions. Four first-class user groups, anti-references, and five principles, the load-bearing ones being "momentum without candy" (Duolingo's pedagogy, not its visual register) and "density is a property of the surface, not the system". DESIGN.md (visual): Stitch six-section format with OKLCH frontmatter, since the token layer is OKLCH-native and hex would split the source of truth. Documents what globals.css and base-mira actually do today. Three things were undocumented anywhere: - base-mira ships dense (h-7 buttons, text-xs bodies). That is the STAFF register and it is correct there; learner surfaces must now explicitly opt into a LARGER register. Otherwise a student reads 12px prose for an hour on a mid-range Android because nobody chose otherwise. - Tenants override --radius and --font-sans, not just colours (components/tenant/tenant-css-vars.tsx). Shape and typeface are tenant-variable, so no layout may depend on Noto Sans metrics. - Neutrals sit at hue ~285 while the brand sits at 223, i.e. they are NOT tinted toward the brand. That inverts the usual advice and is correct here: the brand hue is tenant-variable, so a brand-tinted neutral would change the product's temperature per school. Written up as The Constant Neutral Rule so it does not get "fixed" later. CLAUDE.md: Design Context now points at PRODUCT.md as canonical, and the default primary is corrected from "~293 hue" to the shipped teal-cyan oklch(0.52 0.105 223.128). Anything trusting that line was designing against a hue the app has never used. layout.tsx: Geist Sans was loaded on every page as --font-geist-sans with zero consumers, so it shipped font weight without doing work. Dropped rather than bound to a display role: DESIGN.md commits to one sans differentiated by weight, and because tenants override --font-sans, a hardcoded second face would pair a school's chosen font against one they never picked. Geist Mono stays; it is genuinely used. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Kmgym3WWdUV5CAP4hjWwH1
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Adds the two design-context files every
/impeccablecommand reads before doing any work (PRODUCT.mdfor strategy,DESIGN.mdfor the visual system), plus the machine-readable sidecar. Fixes the one live defect the scan turned up: Geist Sans was loaded on every page with zero consumers.Closes #587
Why
There was no written design context, so every design pass re-derived the palette, the type scale, and the register from scratch, and drifted. Three things were true in the code and documented nowhere:
base-miraships dense.h-7buttons,text-xsbodies,text-xs/relaxedcards. That is correct for dashboards and grading queues. It is wrong for a student reading an exercise for an hour on a mid-range Android, which is the baseline device this product targets. Nothing said so, so the dense default was silently winning everywhere.DESIGN.mdnames the two registers and requires learner surfaces to opt into the larger one.--radiusand--font-sans, not just colours (components/tenant/tenant-css-vars.tsx:34-39). Shape and typeface are tenant-variable. Any layout depending on Noto Sans metrics is a latent bug.CLAUDE.md's Design Context also claimed the default primary was "~293 hue OKLCH". The app shipsoklch(0.52 0.105 223.128), a teal-cyan at hue 223. Anything trusting that line was designing against a purple this codebase has never used. Corrected, and the section now points atPRODUCT.mdas canonical.On the font:
Geist Sanswas declared inapp/[locale]/layout.tsxand attached to<body>as--font-geist-sans, but nothing referenced it — no class, no CSS, no@themebinding. It shipped weight and did no work. Dropped rather than bound to a display role, because:DESIGN.mdcommits to one sans differentiated by weight, not family.--font-sans. A hardcoded display face would pair a school's chosen font against one they never picked.Geist Monois untouched — it is genuinely used by--font-mono, thefont-monoutility, and direct[font-family:var(--font-geist-mono)]refs innot-found.tsx.How to QA
Mostly documentation; the only behavioural change is the font removal, and its correctness claim is "nothing changes".
npm run build— passes.default.lvh.me:3000/en/dashboard/student), open DevTools, inspect<body>and any heading. Computedfont-familyshould resolve through--font-sans(Noto Sans) exactly as before.Geistshould not appear in any computed style, and no Geist Sans file should appear in the Network tab.font-monoelement still renders in Geist Mono, e.g. the code stamp on/en/not-foundor a slug cell in/en/platform/tenants.PRODUCT.mdandDESIGN.mdand push back on anything that misstates the product. They are the input to every future design pass, so a wrong line here propagates.Screenshots / GIF
No visual diff, by construction. The removed font had zero consumers:
next/fontwith thevariableoption only declares a CSS custom property, it does not apply afont-family, and nothing consumed--font-geist-sans. Rendering is byte-identical. Step 3 above is the verification rather than a screenshot pair, since two identical screenshots would prove nothing either way.Checklist
npm run typecheckandnpm run test:unitpass (54 files, 675 tests)npm run buildpassestenant_id(or the table genuinely has no such column) — no queries in this PRmessages/en.jsonandmessages/es.json— no new stringsnpm run db:reset, has RLS policies, andlib/database.types.tswas regenerated — no migrationNotes for the reviewer
master, not stacked on Student-facing AI task results: contrast, hierarchy and a broken sticky column on mobile + desktop #581/Exercise view: the task is two screens down on a phone, behind links to other exercises #583, though it was authored while those were in flight..impeccable/design.jsonis the machine-readable sidecar the impeccable live panel renders from..impeccable/config.local.json,hook.cache.json, andcritique/are local artifacts and stay out of the repo.PRODUCT.mdrecords four first-class user groups rather than picking one, with an explicit surface-level tiebreak, because that is what was actually decided. If you disagree with the tiebreak, that is the line to argue with.