# Design tokens, theme, typography, brand execution

## Current state

The theme lives in client/src/theme/ as a deliberate two-layer token system: tokens.css defines ~30 `--bal-*` CSS variables (primary/secondary + light/dark/contrast, soft tints, surfaces, text, divider, and four semantic feedback pairs) keyed on `[data-mui-color-scheme='light'|'dark']`, mirrored by colors.ts (`BRAND`, `LIGHT_PALETTE`, `DARK_PALETTE`) which feeds a MUI v9 cssVariables theme built in theme.ts (`createAppTheme` produces APP_THEME_LTR/APP_THEME_RTL once at module load). ThemeProvider.tsx wires a direction-aware Emotion cache with stylis-plugin-rtl, CssBaseline enableColorScheme, and a cookie sync so the server (app/[locale]/layout.tsx + lib/cookies/server.ts getThemeMode) stamps `data-mui-color-scheme` on <html> before first paint for returning visitors. Fonts are loaded per-locale in app/[locale]/layout.tsx: Mikhak (400/500/700 woff2 via next/font/local, preload:false) attached only on fa routes; typography.ts declares a Space Grotesk variable for EN that is explicitly "not currently wired to a font loader". Adoption of the tokens in feature code is exceptionally disciplined — 331 `var(--bal-*)` usages across 103 component/page files, with raw hexes existing only in theme/colors.ts, one starter SVG, and one test.

What the theme does NOT do is the story: createTheme receives only palette, typography (family+weight only), `shape.borderRadius: 10`, and direction — there is zero `components` key anywhere in src (the only styleOverrides grep hit is ErrorBoundary's unrelated text). Every Button, Card, TextField, Chip, Dialog, Table, Tab, and Alert renders stock Material Design with recolored primaries: default elevations, default densities, default focus treatment, Roboto-tuned type metrics. The MUI palette also omits success/error/warning/info, so the 29 files using `<Alert severity>`/`color="success"` show stock MUI greens/reds while toasts (lib/toast/NotistackProvider.tsx) use the brand-harmonized `--bal-*` feedback colors — two visibly different feedback systems. Persian typography inherits MUI's Latin defaults wholesale (rem scale, tight heading line-heights, non-zero letter-spacing that is wrong for joined Persian script), and the "logo" registered in components/common/AppIcon/config.ts is still the starter's multicolor Twemoji pencil SVG, rendered at 56px on the auth screens by components/auth/BrandMark.tsx and as the TopBar logo button. The brand seed deck the tokens were extracted from (product/balinyaar.html) is referenced in tokens.css and .claude/skills/frontend-designer/SKILL.md but no longer exists in the repo. Starter residue persists: dead legacy themes light.ts/dark.ts (built on the deprecated LTR-only TYPOGRAPHY export, exported from theme/index.ts including `LIGHT_THEME as default` — never imported by the app), a raw starter globals.css, and starter-era comments in components/config.ts.

## Problems (15)

- **[high]** `client/src/theme/theme.ts` — The MUI theme has NO `components` customization at all — createTheme gets only cssVariables/colorSchemes/typography/shape/direction. No styleOverrides or defaultProps for Button, Card, Paper, TextField, Chip, Dialog, Table, Tabs, Skeleton, etc., so every surface renders stock Material Design (default elevations/shadows, ripple, densities, focus states) with recolored primaries. This is the single biggest reason the app reads as a default-MUI starter instead of the calm/warm brand.
  - evidence: Lines 19-36: the entire createTheme call — a repo-wide grep for `styleOverrides|defaultProps` finds only an unrelated hit in ErrorBoundary.tsx
- **[high]** `client/src/components/common/AppIcon/config.ts` — The registered brand `logo` is still the starter's Twemoji multicolor pencil SVG (icons/PencilIcon.tsx with hard-coded fills #D99E82/#EA596E/#FFCC4D/#CCD6DD). It is the logo button in the TopBar shell and is rendered at 56px on every auth screen by components/auth/BrandMark.tsx, where the passed `color="var(--bal-primary)"` prop is silently ignored by the hard-coded fills. The seed-deck logo (deep-teal square, cream glyph, terracotta dot) was never implemented — first-impression brand execution is a cartoon pencil.
  - evidence: config.ts line 115: `logo: PencilIcon,`; BrandMark.tsx line 21: `<AppIcon icon="logo" size={56} color="var(--bal-primary)" />`
- **[high]** `client/src/theme/colors.ts` — LIGHT_PALETTE/DARK_PALETTE never define success/error/warning/info, so all MUI severity surfaces (Alert, Chip color=success, etc. — used in 29 files including checkout, refund status, verification) render stock MUI #2e7d32 green / #d32f2f red, while notistack toasts use the brand-harmonized --bal-success #1f6b50 / --bal-error #a8392a. The same semantic state shows two different color systems depending on whether it arrives as a toast or an inline alert — directly against the skill rule 'Need success/error/warning/info → use --bal-* tokens, not MUI defaults'.
  - evidence: colors.ts lines 29-75 define only primary/secondary/background/text/divider; AppAlert.tsx defaults `severity='error' variant='filled'` to stock MUI error red
- **[high]** `client/src/theme/typography.ts` — TYPOGRAPHY_RTL sets only fontFamily and weights — the entire Persian type scale is MUI's Roboto-tuned Latin defaults: rem sizes, tight heading line-heights (e.g. h4 1.235) that clip Persian ascenders/descenders, and non-zero letterSpacing on body1/body2/button/caption/overline, which is typographically wrong for joined (cursive) Persian script — letter-spacing visually breaks glyph connections in Mikhak. There is no fa-specific size, line-height, or letter-spacing tuning anywhere, and no responsive heading sizes.
  - evidence: Lines 43-52: TYPOGRAPHY_RTL contains only fontFamily + fontWeight per variant; no fontSize/lineHeight/letterSpacing overrides exist
- **[medium]** `client/src/theme/typography.ts` — The theme requests fontWeight 600 (h6, button) but the Mikhak loader in app/[locale]/layout.tsx ships only 400/500/700 — CSS font-matching resolves 600 upward to 700, so all intended 'semibold' text renders full Bold in Persian. The same 600-vs-loaded-weights mismatch is repeated in ~68 `fontWeight: 600` sx usages across 42 component/page files, collapsing the weight hierarchy to regular-vs-bold; the loaded Medium 500 is barely used.
  - evidence: typography.ts lines 38/51 (`fontWeight: 600`), layout.tsx lines 40-44 (weights 400/500/700 only); grep `fontWeight: 600` = 68 hits in 42 files
- **[medium]** `client/src/theme/typography.ts` — The English brand font is vaporware: BRAND_FONT_VARIABLE_EN '--font-space-grotesk' is declared with a comment admitting it is 'Not currently wired to a font loader; the LTR stack falls back to the system fonts'. /en pages have no brand typeface at all — headings render in Segoe UI/Roboto, so the secondary locale has zero typographic identity.
  - evidence: Lines 3-5 comment + line 22 DISPLAY_FONT_LTR referencing the never-populated variable
- **[medium]** `client/src/theme/tokens.css` — The token system covers colors only. There are no spacing-scale tokens, no radius steps beyond the single shape.borderRadius:10, no elevation/shadow tokens (all shadows are MUI's default neutral-black stack — cold and grey against the warm cream surfaces), no motion/duration/easing tokens, and no focus-ring token. Focus styling exists exactly once in the whole app, hand-rolled in NurseResultCard.tsx (`'&:focus-visible': { outline: '2px solid var(--bal-primary)' }`) — keyboard focus everywhere else is MUI's faint default, a real accessibility + polish gap.
  - evidence: tokens.css lines 20-99 define only color variables; grep `focus-visible|outline:` yields a single hit at NurseResultCard.tsx:67
- **[medium]** `client/src/theme/index.ts` — Dead starter theme code is still exported as the public API: light.ts/dark.ts build legacy ThemeOptions on the deprecated LTR-only TYPOGRAPHY (system font, no colorSchemes, no cssVariables, no direction) and index.ts exports them plus `LIGHT_THEME as default` — importing the package default yields a broken, unbranded theme. Only ThemeProvider and getDirection are actually consumed (single import in app/[locale]/layout.tsx). Violates the repo's own no-dead-code rule and is a trap for future contributors.
  - evidence: index.ts lines 8-17 export APP_THEME/LIGHT_THEME/DARK_THEME/`LIGHT_THEME as default`; grep shows the only consumer imports `{ ThemeProvider, getDirection }`
- **[medium]** `client/src/lib/cookies/server.ts` — First-visit dark-mode flash: when no color-scheme cookie exists, getThemeMode returns `colorScheme: 'light'`, so SSR stamps data-mui-color-scheme="light" on <html>; an OS-dark first-time visitor paints the full light theme, then MUI flips to dark client-side (no InitColorSchemeScript/inline pre-paint script). On a cream-vs-deep-teal palette this flash is stark.
  - evidence: Line 46: `return { colorScheme: 'light', defaultMode: 'system' };` combined with layout.tsx line 89 `data-mui-color-scheme={colorScheme}`
- **[medium]** `client/src/theme/tokens.css` — The brand source of truth is missing: tokens.css says the palette was 'extracted from the seed-deck proposal (balinyaar.html)' and the frontend-designer skill describes the logo from 'product/balinyaar.html', but that file does not exist anywhere in the repo — the intended identity (logo lockup, imagery, tone) is unrecoverable from the codebase, so gaps between deck and implementation cannot even be checked.
  - evidence: tokens.css line 4 references balinyaar.html; Glob `**/balinyaar*.html` across the repo returns no files
- **[low]** `client/src/app/globals.css` — globals.css is an untouched starter reset with hazards: `max-width: 100vw; overflow-x: hidden; max-height: 100vh` on html/body (100vw invites scrollbar-width overflow that the hidden overflow then silently masks; max-height:100vh is fragile on mobile browsers vs dvh and makes body scrolling work only by accident) plus a global `a { color: inherit; text-decoration: none }` that strips native link affordance app-wide. Nothing brandful (selection color, scrollbar, focus) lives here.
  - evidence: Lines 7-12 and 14-17 — the entire 17-line file
- **[low]** `client/src/app/[locale]/layout.tsx` — Brand metadata is placeholder-grade and not locale-aware: a single static title 'Balinyaar | بالین‌یار' and description 'Balinyaar web application' serve both /fa and /en (no generateMetadata per locale), and viewport.themeColor is hard-coded to light teal #1d4a40 with no dark-scheme media entry, so dark-mode users get a light-teal browser chrome over a #0f1c19 page.
  - evidence: Lines 50-58: `themeColor: BRAND.teal` and `description: 'Balinyaar web application'`
- **[low]** `client/src/components/config.ts` — Starter residue in the component-defaults file: the comment `CONTENT_MIN_WIDTH = 320; // CONTENT_MAX_WIDTH - Sidebar width` is factually wrong (800 − 240 = 560) and the whole file keeps the starter's commented-out-alternatives style ('error' // 'error' | 'info' ...), signaling copy-paste config rather than owned design decisions.
  - evidence: Line 5 comment; lines 10-35
- **[low]** `client/src/components/common/AppIcon/config.ts` — The icon registry (~90 icons) mixes filled and outlined Material styles with no system: legacy starter entries are filled (Home, Settings, Star, AccountCircle, Dashboard, CheckCircle, Cancel, MedicalServices...) while everything added later is deliberately Outlined — adjacent nav/status icons visibly differ in visual weight, reinforcing the default-MUI feel the owner already dislikes. No custom/brand icon set exists (the only custom SVG is the Twemoji pencil).
  - evidence: Lines 4-33 filled imports vs lines 35-98 `*Outlined` imports registered side-by-side in ICONS
- **[low]** `client/src/layout/TopBarAndSideBarLayout.tsx` — Physical direction props in the shell (`paddingLeft`/`paddingRight` gated on `anchor.includes('left')`) instead of logical padding — currently rescued at runtime by the stylis-plugin-rtl Emotion cache flipping them, but it contradicts the skill's own RTL rule and couples shell correctness to the RTL cache implementation.
  - evidence: Lines 53-60 and line 102

## Opportunities (10)

- **A single MUI components theming pass — the highest-leverage move in the whole app** (impact: high, effort: large) — Add a `components` block to createAppTheme encoding the brand once: Button (disableElevation, weight, comfortable padding), Paper/Card (hairline divider border + soft teal-tinted shadow instead of grey elevation), TextField (calmer outline, cream-tinted filled variant), Chip (soft --bal-primary-soft/secondary-soft fills), Dialog/Drawer (radius 16, cream surfaces), Tabs (thicker indicator), Table (relaxed density, tinted header), Skeleton (warm tint), Alert (severity colors mapped to --bal-* tokens). Every screen upgrades simultaneously with zero per-page edits — this is the escape hatch from the 'default-MUI starter' look.
- **Design a real Persian type scale** (impact: high, effort: medium) — Replace inherited Roboto metrics in TYPOGRAPHY_RTL with an owned fa scale: letterSpacing: 0 on every variant (joined script), body line-height ≥ 1.7 and heading line-heights ~1.4-1.5 for Persian ascender/descender room, explicit responsive heading sizes (MUI's default 6rem h1 is unusable, which is why no page uses h1/h2 today — grep confirms zero usages), and a weight system built on the actually-loaded 400/500/700 (retire the phantom 600). Wire Space Grotesk via next/font for /en so the secondary locale gets its brand voice.
- **Implement the real brand mark and kill the pencil** (impact: high, effort: medium) — Build the seed-deck logo (deep-teal rounded square, cream lowercase glyph, single terracotta dot) as a theme-aware SVG component with sizes for TopBar, auth lockup (BrandMark), favicon, and webmanifest/PWA icons. Since product/balinyaar.html is gone, first re-establish the brand source of truth as a product/brand.md (palette, logo construction, tone words, do/don'ts) so design decisions stop living only in a skill file.
- **Unify semantic feedback into the MUI palette** (impact: high, effort: small) — Add success/error/warning/info (from the existing --bal-* values) into LIGHT_PALETTE/DARK_PALETTE so Alert, Chip, Badge, LinearProgress color props are automatically brand-harmonized, then delete per-component semantic styling. One feedback language across toasts, inline alerts, and status chips — important in a product where refund/verification/payment states are the emotional core.
- **Codify a trust-signal design language at token level** (impact: high, effort: medium) — Trust IS the product, yet 'verified' has no dedicated visual identity — TrustBadge and verification chips borrow generic primary/success styling. Introduce a `--bal-trust`/`--bal-trust-soft` token pair (both schemes), a consistent shield/checkmark mark, and a defined 'verified nurse' card treatment (badge placement, tinted ring on avatar, tooltip explaining WHAT was verified: identity, license, Shahkar). This turns the platform's core differentiator into a recognizable, repeatable visual asset instead of an ad-hoc green chip.
- **Extend tokens beyond color: shadows, radii, motion, focus** (impact: medium, effort: medium) — Add teal-tinted elevation tokens (e.g. shadows built on rgba(29,74,64,α) for light / black-teal for dark), a radius scale (4/10/16) documented next to shape.borderRadius, motion tokens (durations + easings) for consistent transitions, and a global :focus-visible ring (2px --bal-primary, offset 2) applied via theme so keyboard accessibility is uniform instead of existing in exactly one card.
- **No-flash color-scheme boot + dark browser chrome** (impact: medium, effort: small) — Render MUI's InitColorSchemeScript (or a 3-line inline script) before paint so cookie-less OS-dark visitors never see the light flash, and switch viewport.themeColor to the media-query array form ({ media: '(prefers-color-scheme: dark)', color: BRAND.tealDeep }) so the browser UI matches the page in both schemes.
- **Delete starter residue from the theme layer** (impact: medium, effort: small) — Remove light.ts, dark.ts, the deprecated TYPOGRAPHY export, and the APP_THEME/LIGHT_THEME/default exports from theme/index.ts (nothing imports them); rewrite globals.css as an intentional base (logical-property-safe reset, ::selection in brand teal/cream, dvh-safe heights, focus-visible fallback); fix the false CONTENT_MIN_WIDTH comment and prune the option-menu comments in components/config.ts.
- **Commit to one icon style and consider a warmer set** (impact: medium, effort: medium) — Normalize the registry to a single style (the Outlined majority) by swapping the ~12 legacy filled starter icons, or go further and adopt a rounded/duotone set (e.g. Material Symbols Rounded or Phosphor) rendered through the existing AppIcon registry — rounded strokes read warmer and less 'admin dashboard', matching clinical-but-human. The name-registry architecture makes this a config-file-only swap.
- **Systematize Persian numerals as a component** (impact: medium, effort: small) — The Intl plumbing (money.ts, date.ts, booking/format.ts) is correct but every call site must remember the locale parameter; add a tiny <LocalizedNumber>/<Money>/<ShamsiDate> component family (or a useFormatters() hook) so counts, pagination, phone numbers, and durations can never accidentally render Latin digits on fa — and typographic details like the Toman unit label and IRR→Toman display stay consistent.

## Keep (do not regress)

- The two-layer token architecture (tokens.css --bal-* variables scheme-keyed on data-mui-color-scheme, mirrored in colors.ts) with the sync rule documented in both file headers — a genuinely well-designed system, keep it as the foundation for any restyle.
- Outstanding token discipline in feature code: 331 var(--bal-*) usages across 103 files and effectively zero hard-coded hexes outside the theme layer (only the starter PencilIcon and one test) — do not let a design pass reintroduce literals.
- Correct MUI v9 RTL setup: dual prebuilt themes (APP_THEME_LTR/APP_THEME_RTL), direction-aware Emotion cache with stylis-plugin-rtl in ThemeProvider.tsx, and lang/dir sourced from the [locale] layout with the documented reasoning for why <html> lives there.
- Cookie-SSR color-scheme sync (lib/cookies/server.ts getThemeMode + data-mui-color-scheme stamped server-side) — returning visitors get zero dark-mode flash; also the documented colorSchemeSelector fix in theme.ts.
- Per-locale font loading done right: Mikhak via next/font/local with preload:false and a conditional className so Persian woff2 never ships to /en, and the skill rule that fonts load only in the locale layout.
- Brand-harmonized feedback tokens plus NotistackProvider styling toasts entirely from --bal-* variables, so toasts track scheme and direction for free through the portal.
- The Persian correctness utility layer: money.ts (BigInt IRR, fa-IR digit grouping, Toman-at-the-boundary), date.ts (fa-IR-u-ca-persian Shamsi via Intl, no date library), text.ts toEnglishDigits for Persian-keyboard input, booking/format.ts locale clocks — this is rare-quality i18n plumbing.
- The few global theme decisions already made are the right ones: shape.borderRadius 10, button textTransform 'none', dark palette that lifts teal to #6fc0ac on deep-teal surfaces instead of inverting to grey.
- The frontend-designer skill (.claude/skills/frontend-designer/SKILL.md) as a written, enforceable design contract — extend it with whatever the design pass adds rather than replacing it.
- The AppIcon name-registry pattern — exactly what makes a future icon-set swap a one-file change.
