Skip to content

docs(design): add PRODUCT.md + DESIGN.md, drop the unused Geist Sans - #585

Merged
guillermoscript merged 1 commit into
masterfrom
chore/impeccable-design-context
Jul 28, 2026
Merged

docs(design): add PRODUCT.md + DESIGN.md, drop the unused Geist Sans#585
guillermoscript merged 1 commit into
masterfrom
chore/impeccable-design-context

Conversation

@guillermoscript

@guillermoscript guillermoscript commented Jul 28, 2026

Copy link
Copy Markdown
Owner

What

Adds the two design-context files every /impeccable command reads before doing any work (PRODUCT.md for strategy, DESIGN.md for 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-mira ships dense. h-7 buttons, text-xs bodies, text-xs/relaxed cards. 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.md names the two registers and requires learner surfaces to opt into the larger one.
  • Tenants override --radius and --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.
  • The neutrals are deliberately not tinted toward the brand. They sit at hue ~285 while the brand sits at 223. That inverts the usual "tint neutrals toward the brand hue" advice, and it 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 specifically so it does not get "fixed" later.

CLAUDE.md's Design Context also claimed the default primary was "~293 hue OKLCH". The app ships oklch(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 at PRODUCT.md as canonical.

On the font: Geist Sans was declared in app/[locale]/layout.tsx and attached to <body> as --font-geist-sans, but nothing referenced it — no class, no CSS, no @theme binding. It shipped weight and did no work. Dropped rather than bound to a display role, because:

  1. DESIGN.md commits to one sans differentiated by weight, not family.
  2. Tenants override --font-sans. A hardcoded display face would pair a school's chosen font against one they never picked.
  3. It loaded on every page for the LATAM mid-range-Android baseline.

Geist Mono is untouched — it is genuinely used by --font-mono, the font-mono utility, and direct [font-family:var(--font-geist-mono)] refs in not-found.tsx.

How to QA

Mostly documentation; the only behavioural change is the font removal, and its correctness claim is "nothing changes".

  1. npm run build — passes.
  2. Confirm the dropped font has no consumers left:
    grep -rn "geist-sans\|geistSans" --include="*.tsx" --include="*.ts" --include="*.css" . | grep -v node_modules
    Expected: no matches.
  3. Load any page (default.lvh.me:3000/en/dashboard/student), open DevTools, inspect <body> and any heading. Computed font-family should resolve through --font-sans (Noto Sans) exactly as before. Geist should not appear in any computed style, and no Geist Sans file should appear in the Network tab.
  4. Check a font-mono element still renders in Geist Mono, e.g. the code stamp on /en/not-found or a slug cell in /en/platform/tenants.
  5. Read PRODUCT.md and DESIGN.md and 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/font with the variable option only declares a CSS custom property, it does not apply a font-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 typecheck and npm run test:unit pass (54 files, 675 tests)
  • npm run build passes
  • Every new tenant-scoped query filters by tenant_id (or the table genuinely has no such column) — no queries in this PR
  • Tested with every relevant role (student / teacher / admin) — no role-dependent behaviour; the font change is global and inert
  • Loading and error states handled — no new UI
  • New UI strings added to both messages/en.json and messages/es.json — no new strings
  • Migration (if any) applies cleanly on npm run db:reset, has RLS policies, and lib/database.types.ts was regenerated — no migration

Notes for the reviewer

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
@guillermoscript guillermoscript added the documentation Improvements or additions to documentation label Jul 28, 2026
@guillermoscript
guillermoscript merged commit 14a7832 into master Jul 28, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

No written design context, and CLAUDE.md documents a primary hue the app never ships

1 participant