# UI Phase 0 — Design language & theme foundation > **Mission:** kill the default-MUI/starter look at the token + theme layer in one pass and establish the > brand's visual system — a `theme.components` pass, a semantic palette, a real Persian type scale, one icon > family, a designed Balinyaar logomark, extended tokens (elevation/motion/focus/rating/trust/money), and a > purge of starter residue — so every later phase composes on a system that already looks designed. **No page > redesigns here**: this phase changes what every screen inherits, not any screen's layout. > > **Track:** frontend · **Depends on:** — (first phase of the [UI chain](README.md)) · **Unlocks:** > [Phase 1](ui-phase-1-primitives-and-states.md), [Phase 2](ui-phase-2-shells-and-navigation.md), and every > later phase — they all inherit the de-startered look built here. > **Before you start, read [../../phases/_shared/agent-operating-rules.md](../../phases/_shared/agent-operating-rules.md) > and invoke the frontend-designer skill — both are mandatory.** ## 1. Context — where this sits Balinyaar's feature layer is unusually disciplined — 331 `var(--bal-*)` usages across 103 files, effectively zero hard-coded hexes, correct RTL/Shamsi/BigInt-money plumbing — but the *visual system underneath it is still the karpolan react-mui starter*. Diagnosed root causes (all verified in code): 1. **`createTheme` has zero `components` overrides** — `client/src/theme/theme.ts:19-36` passes only `cssVariables`/`colorSchemes`/`typography`/`shape`/`direction`; every Button/Card/TextField/AppBar/Chip/ Dialog/Alert renders stock Material with recolored primaries. The single biggest cause of the starter feel. 2. **No semantic palette** — `theme/colors.ts` defines only primary/secondary/background/text/divider, so inline `` surfaces render stock MUI green/red while toasts (`lib/toast/NotistackProvider.tsx`) use the brand `--bal-*` feedback tokens — two feedback systems for one semantic state. Worse, a bare `` defaults to a **filled stock-red error** (`components/config.ts:10-11`). 3. **Persian typography is Roboto metrics** — `TYPOGRAPHY_RTL` (`theme/typography.ts:43-52`) sets only `fontFamily` + weights: non-zero letter-spacing on a joined script, tight heading leading, and a requested weight **600** (h6, button) that the Mikhak loader (`app/[locale]/layout.tsx:39-48`, weights 400/500/700) renders as full Bold — repeated in 68 `fontWeight: 600` sx usages across 42 files (verified). Space Grotesk (`--font-space-grotesk`) is declared for EN but never loaded. 4. **The brand mark is the starter's Twemoji cartoon pencil** — `AppIcon/config.ts:115` registers `logo: PencilIcon`; `components/auth/BrandMark.tsx:21` renders it at 56px on every auth screen and passes `color="var(--bal-primary)"`, which the hard-coded fills (`#EA596E`, `#FFCC4D`, …) silently ignore. 5. **The icon system is broken and incoherent** — `AppIcon.tsx:38-46` passes `size` as SVG `width`/`height` *attributes*, which MUI `SvgIcon`'s `1em` class CSS overrides, so every `size={14..56}` call-site renders 24px. The ~87-entry registry mixes filled starter icons against `*Outlined` feature icons, carries eight dead entries (zero usages, verified), and has **no back/chevron icon at all** (only `expand`). 6. **Starter residue everywhere else:** `AppButton`'s `DEFAULT_SX_VALUES = { margin: 1 }` neutralized by **213 `m: 0` occurrences across 82 files**; dead `theme/light.ts`/`dark.ts` exported from `theme/index.ts` incl. `LIGHT_THEME as default` (nothing else imports them); the 17-line starter `globals.css`; the wrong `CONTENT_MIN_WIDTH` comment (`components/config.ts:5`); unused `AppImage` (all verified); a cookie-less OS-dark first visit paints light first (`lib/cookies/server.ts:46` returns `'light'`, no pre-paint script exists — acknowledged doc drift in `client/CLAUDE.md`); `viewport.themeColor` hard-coded to light teal (`layout.tsx:50-52`); `public/site.webmanifest` still ships starter `#000000`/`#ffffff` and references `img/favicon/*.png` files that don't exist. **What already exists (do not rebuild):** - The **two-layer token system** — `theme/tokens.css` (`--bal-*`, scheme-keyed on `data-mui-color-scheme`) mirrored by `theme/colors.ts`, sync rule in both headers. Extend it; never bypass it. - The **MUI v9 RTL dual-theme setup** (`APP_THEME_LTR`/`APP_THEME_RTL` built once at module load, `stylis-plugin-rtl` Emotion cache in `ThemeProvider.tsx`) and the **cookie-SSR color-scheme sync** (`getThemeMode()` → `data-mui-color-scheme` stamped server-side; explicit `colorSchemeSelector`). - **Per-locale font loading** (Mikhak via `next/font/local`, `preload: false`, attached only on `fa`), the **AppIcon string registry** with its snake_case domain names (the exact indirection that makes this phase's icon swap a config-level change), **brand-styled notistack toasts**, and the correct global decisions already made: `shape.borderRadius: 10`, `textTransform: 'none'`, the lifted dark-teal palette. ## 2. Required reading (do this first) - The audits: [audit/theme-and-brand.md](audit/theme-and-brand.md), [audit/component-primitives.md](audit/component-primitives.md), [audit/cross-cutting-ux.md](audit/cross-cutting-ux.md) — the full file/line evidence and the **Keep (do not regress)** lists this phase must honor. - [../../../.claude/skills/frontend-designer/SKILL.md](../../../.claude/skills/frontend-designer/SKILL.md) — the design contract you are extending (invoke the skill; this phase also *updates* it — see 3.10). - `client/CLAUDE.md` — "Golden rules", "Theme System" (the cookie/no-flash machinery and the MUI v9 traps: explicit `colorSchemeSelector`, never `InitColorSchemeScript`, never `storageWindow`), and "Fonts". - Code, in this order: all of `client/src/theme/`, `components/config.ts`, `components/common/AppIcon/` (+ `icons/PencilIcon.tsx`), `components/common/AppButton/AppButton.tsx`, `components/auth/BrandMark.tsx`, `lib/toast/NotistackProvider.tsx`, `app/[locale]/layout.tsx`, `app/globals.css`, `lib/cookies/server.ts` (`getThemeMode`), `public/site.webmanifest`. - `product/overview/platform-summary.md` — the tone (trust-first, calm, clinical-but-human) every visual decision here must serve. ## 3. Scope — build this ### 3.1 The `theme.components` pass (the highest-leverage change in the app) Add a `components` block inside `createAppTheme` (`theme/theme.ts`) that encodes the brand once. Reference colors as `var(--bal-*)` tokens (or `theme.vars.palette.*`) so every override is scheme-correct — never a hex. Overrides must be direction-safe (logical properties only — both theme directions share them). Cover at least: **MuiButton** (`disableElevation`, comfortable `paddingInline`, weight per the 3.3 decision, keep `textTransform: 'none'`); **MuiPaper/MuiCard** (hairline `1px solid var(--bal-divider)` border + soft **teal-tinted** shadows from the 3.4 elevation tokens instead of MUI's grey stack on warm cream); **MuiAppBar** (paper/cream surface + hairline bottom divider, **not** solid primary — shells are rebuilt in phase 2, this stops the default bar screaming "starter" now); **MuiOutlinedInput/MuiTextField** (house radius, calmer resting border, brand focus ring); **MuiChip / MuiToggleButton(Group) / MuiListItemButton** (soft `--bal-primary-soft`/`--bal-secondary-soft` tonal fills; `selected` = primary-soft + primary text — tokens already exist in both schemes); **MuiDialog** (radius 16, sane mobile margins); **MuiTabs** (thicker indicator, comfortable min-height); **MuiAlert** (severities from the 3.2 semantic palette so inline alerts match toasts); **MuiStepper/MuiStepIcon** (brand-colored active/completed steps — the trust flows live on these); **MuiSkeleton** (warm tint per scheme); **MuiTooltip** (ink/cream inversion, house radius); and **MuiCssBaseline** carrying a global `:focus-visible` treatment off `--bal-focus-ring` (2px ring + offset — today exactly **one** `:focus-visible` style exists in the app, in `NurseResultCard.tsx`; after this, keyboard focus is uniform everywhere). ### 3.2 Semantic palette — one feedback language - Add `success`/`error`/`warning`/`info` (main + contrastText, light **and** dark) to `LIGHT_PALETTE`/`DARK_PALETTE` in `colors.ts`, sourced from the existing `--bal-*` feedback values in `tokens.css` (light: `#1f6b50`/`#a8392a`/`#8a6418`/`#1d4a40`; dark: the lifted set). Keep the two files mirror-synced. Alert, `Chip color="success"`, Badge, LinearProgress become brand-harmonized automatically. - Fix the `AppAlert` footgun in `components/config.ts`: first make severity explicit at any call-site that genuinely means error, then change the defaults to a calm baseline (`severity="info"`, `variant="standard"` on the new tinted severities — decide and document in the config file). ### 3.3 A real Persian type scale In `theme/typography.ts` `TYPOGRAPHY_RTL`: - **`letterSpacing: 0` on every variant** — Persian is a joined script; tracking breaks Mikhak's glyph connections (MUI's Latin defaults set non-zero spacing on body1/body2/button/caption/overline). - **Body line-height ≥ 1.7; headings ~1.4–1.5** (room for Persian ascenders/descenders) plus **responsive heading sizes** (MUI's default 6rem h1 is unusable; grep confirms zero h1/h2 usages today). - **Resolve the weight-600 problem.** Mikhak loads 400/500/700 only, so every requested 600 silently renders 700. Decide: (a) adopt **500/700 semantics** — map the theme's h6/button and the 68 `fontWeight: 600` sx usages across 42 files to loaded weights (recommended: 700 headings/buttons, 500 in-text emphasis; the sweep is mechanical) — or (b) add a genuine Mikhak DemiBold face to `src/app/fonts/` + the loader. Pick one, do it fully, **document the decision in `typography.ts` and the skill (3.10)**. - **Settle the EN display font**: wire Space Grotesk via `next/font` in `app/[locale]/layout.tsx` (attached only on `en`, mirroring the Mikhak pattern, `preload: false`) **or** delete the dead `BRAND_FONT_VARIABLE_EN` variable and its fallback stack. Decide and do it — no vaporware comment left. ### 3.4 Token extension — beyond color Extend `theme/tokens.css` (**both scheme blocks — always**), mirroring palette-level values in `colors.ts`: - **Elevation/shadow tokens** — 2–3 steps of teal-tinted shadows (light: `rgba(29,74,64,α)` stacks; dark: black-teal), consumed by 3.1's Paper/Card/Dialog overrides. - **Radius scale** — document the house scale (e.g. 4 / 10 / 16: controls / cards / dialogs) as tokens or constants next to `shape.borderRadius`; stop inventing radii per component. - **Motion tokens** — durations (e.g. 120/200/300ms) + easings; defined once here, consumed by phase 12 — and **`--bal-focus-ring`**, the global focus ring used by 3.1's `:focus-visible` treatment. - **`--bal-rating` + `--bal-rating-empty`** — proper star gold + empty color per scheme (`RatingInput`'s muddy `--bal-warning` stars are fixed in **phase 1** using these); **`--bal-money-emphasis`** — a contrast-safe money-text emphasis per scheme, retiring terracotta as a small-text money color (`#d98c6a` on white fails AA contrast); **`--bal-trust` + `--bal-trust-soft`** — a distinct trust identity for verified marks (not generic primary/success), consumed by phases 1/4/8. All define-only here. - Update the stale `tokens.css` header (it cites `product/balinyaar.html`, which no longer exists in the repo) to point at the frontend-designer skill as the brand source of truth (3.10). ### 3.5 A real brand mark — kill the pencil - Design the Balinyaar logomark per the identity the skill records (the `product/balinyaar.html` seed deck is **gone from the repo** — recreate from the tokens + the skill's description): deep-teal ground, cream lowercase glyph, a single terracotta dot. Build two SVGs under `AppIcon/icons/`: a **monochrome `currentColor` logomark** (so `color="var(--bal-primary)"` finally works and it recolors in dark mode for free) and a **full lockup** (mark + wordmark) for `BrandMark`/auth. - Register the mark as `ICONS.logo` (replacing `PencilIcon`), update `components/auth/BrandMark.tsx` to the new lockup, **delete `icons/PencilIcon.tsx`** (the last hard-coded-hex SVG in the app), regenerate `src/app/favicon.ico` from the mark, and fix `public/site.webmanifest` — brand `theme_color`/`background_color` (currently starter `#000000`/`#ffffff`) and icon entries that actually exist (generate the referenced PNG sizes from the mark, or trim the manifest — today it points at missing `img/favicon/*.png`). ### 3.6 Icon system — one family, working sizes, full vocabulary - **Fix the size bug** in `AppIcon.tsx`: drive `fontSize` via `style`/`sx` instead of `width`/`height` attributes (which MUI `SvgIcon`'s `1em` class CSS beats) — all ~40 existing `size={14..56}` call-sites start working simultaneously. Stop spreading the invalid `size` attribute onto the DOM ``; make the unknown-name `console.warn` **dev-only**; keep the custom-SVG path (the new logomark) scaling correctly. - **Normalize the ~87-entry registry to ONE visual family** — recommend the **Rounded** variants of `@mui/icons-material` (zero new deps, warmer than the current filled/outlined mix, fits clinical-but-human). A `config.ts`-only sweep thanks to the registry indirection. Delete the eight dead starter entries (`daynight`/`night`/`day`/`visibilityon`/`visibilityoff`/`signup`/`login`/`settings` — re-verify zero usages before each delete). - **Add the missing vocabulary**: `back`/`chevron_start` (no directional nav glyph exists today except `expand`), `share`, `copy`, `phone` (non-emergency call), `navigate` (directions), `sort`, `attachment`, `star_half`. Note `camera`, `calendar`, `wallet`, and `tune` (filter) are **already registered** — they only need family normalization. **Directional icons auto-mirror**: register `back`/`chevron_start` as the LTR glyph, mirrored via a `[dir="rtl"]` `scaleX(-1)` rule applied by `AppIcon` for a declared `DIRECTIONAL_ICONS` set in `config.ts`; write the rule into the registry comment and the skill — later phases must not hand-roll flips. ### 3.7 AppButton de-startering - Remove `DEFAULT_SX_VALUES` (`margin: 1`), then sweep the now-no-op **213 `sx={{ m: 0 }}` neutralizations across 82 files** (mechanical: remove the key; drop `sx` when it becomes empty). Outer spacing becomes the parent's job (Stack/Box gaps). Clean the starter prop cruft: duplicate `label`/`text` props (pick one, migrate the loser's call-sites), the false "Box around to specify margins" JSDoc, the `// Missing props` comment, and the `underline` spread onto non-link buttons (`AppButton.tsx:85`). - **Do not break the public API**: `to`/`href` auto-link composition, icon-name `startIcon`/`endIcon`, and non-MUI-color-becomes-text-color all stay (238 call-sites). Leave the `color='inherit'` default as-is — changing every unspecified button's color is a per-screen decision for later phases. Update `AppButton.test.tsx` (and every other touched shared component's test) in the same change. ### 3.8 Starter residue purge - Delete `theme/light.ts` + `theme/dark.ts` and their exports from `theme/index.ts` (verified: nothing else imports them; drop `LIGHT_THEME`/`DARK_THEME`/`APP_THEME`/`LIGHT_THEME as default` — keep `ThemeProvider`, `getDirection`, `APP_THEME_LTR/RTL`). Delete the deprecated `TYPOGRAPHY` alias in `typography.ts` once nothing imports it. - Rewrite `app/globals.css` as an intentional minimal base: keep the `box-sizing` reset, **drop `max-height: 100vh`** and the `max-width: 100vw; overflow-x: hidden` mask, add `::selection` in brand colors, and a commented decision on the `a { color: inherit }` reset (AppLink owns link affordance). - Fix the wrong `CONTENT_MIN_WIDTH` comment (`components/config.ts:5`), prune the starter commented-out-alternatives style there into owned decisions, and delete the unused `AppImage` + its test + barrel export (verified: zero product usages). ### 3.9 No-flash color-scheme boot - A cookie-less OS-dark first visit paints light, then flips. Implement the **`ColorSchemeScript`** that `client/CLAUDE.md`'s Theme System section already documents (acknowledged doc drift): an inline `` script in `app/[locale]/layout.tsx` that reads the `color-scheme` cookie, falls back to `matchMedia('(prefers-color-scheme: dark)')` when absent, and sets `data-mui-color-scheme` before first paint — plus the documented `Storage.prototype` patch. **Never** MUI's `InitColorSchemeScript` (banned in `client/CLAUDE.md` — it reads localStorage, which diverges from the cookie). The script must agree with `getThemeMode()` on every path. Switch `viewport.themeColor` (`layout.tsx:50-52`) to the media-query array form: light → `BRAND.teal`, `(prefers-color-scheme: dark)` → `BRAND.tealDeep`, so browser chrome matches the page in both schemes. ### 3.10 Keep the design contract honest Update `.claude/skills/frontend-designer/SKILL.md` **in the same change**: the new tokens (elevation, radius, motion, focus, rating, trust, money-emphasis) and when to use each; the type-scale + weight decision (its "buttons weight 600" rule becomes wrong the moment 3.3 lands); the normalized icon family + directional- mirroring rule + a corrected registered-icons note (it lists ~19 of ~87 icons and still names the dead starter entries); and the brand-mark construction as the written source of truth now that `product/balinyaar.html` is gone (fix its reference; `tokens.css`'s header points here too). **(DEFERRED → phase 1):** all new shared primitives — EmptyState/ErrorState, PageHeader, ``, Jalali picker, StatusChip v2, RatingInput's star fix, skeleton twins, route-level `loading.tsx`/`error.tsx`/404. **(DEFERRED → phase 2):** the shells — TopBar/SideBar chrome, `UserInfo`, nav grouping, SSR mobile-first flash. **(DEFERRED → phase 12):** the app-wide motion pass that consumes 3.4's motion tokens. ## 4. Mocks & seams in this phase None. This phase is pure client theming — no service seams, no mock flags. The chain's REQ posture: if a backend gap surfaces (none is expected here), append a REQ entry to [../../shared-working-context/frontend/requests/for-backend.md](../../shared-working-context/frontend/requests/for-backend.md) — REQ-001…038 are taken (verified: the tracker ends at REQ-038); number onward from **REQ-039**. UI stays mock-tolerant behind the existing `services/{domain}` seams; never edit `server/`. ## 5. Critical rules you must not get wrong - **The two-layer token sync is law** — every color added/changed lands in `tokens.css` (**both** scheme blocks) *and* `colors.ts` in the same commit — and **zero hex regressions**: feature code has effectively no hard-coded hexes; the theme pass must not reintroduce literals outside the theme layer (deleting `PencilIcon` removes the last offender). - **Do not break the RTL dual-theme/Emotion setup** (`APP_THEME_LTR`/`APP_THEME_RTL` stay module-load-built; `createTheme()` never in a component; overrides use logical properties only) **or the cookie-SSR color-scheme sync** (`colorSchemeSelector` stays the explicit `'data-mui-color-scheme'`, never `'data'`; no `InitColorSchemeScript`; no `storageWindow`). - **The AppIcon registry indirection stays** — keep the `` API, the snake_case domain names, the lowercase keys. **AppButton's composition API stays** (`to`/`href` → AppLink, icon-name `startIcon`/`endIcon`) — 238 call-sites; the margin removal is a default change, not an API change. - **Keep the keep-lists.** Notably: `textTransform: 'none'`, radius 10 as the house radius, the lifted dark-teal palette, per-locale font loading (Mikhak never ships to `/en`; mirror that discipline if you wire Space Grotesk), and `NotistackProvider` staying token-driven. - **Design-contract non-negotiables:** any new user-facing string (e.g. the logo's aria/alt) goes in **both** `messages/en.json` and `messages/fa.json`; MUI v9 API only; co-located `*.test.tsx` updated for every touched shared component (`AppIcon`, `AppButton`, `BrandMark`, …); fetch/cookies rules untouched. - **Update the skill and `client/CLAUDE.md` in the same change** (3.10 + §8) — stale design docs are how the next phase reintroduces the starter. ## 6. Definition of Done On top of the shared [definition-of-done.md](../../phases/_shared/definition-of-done.md): - [ ] `npm run check` green; `npm run test:ci` green incl. updated tests for every touched shared component; `en.json`/`fa.json` in sync. - [ ] `theme.ts` has a `components` block covering at least the 3.1 list; no override uses a raw hex or a physical direction property. - [ ] `LIGHT_PALETTE`/`DARK_PALETTE` define success/error/warning/info; an inline `` and a success toast are visibly the same color family in both schemes. - [ ] `TYPOGRAPHY_RTL` has `letterSpacing: 0` everywhere + the new line-heights and responsive heading sizes; no `fontWeight: 600` remains in `src` (or a real 600 face is loaded — per the documented decision); the Space Grotesk question is resolved (wired or deleted). - [ ] `tokens.css` (both blocks) carries elevation, radius, motion, focus-ring, rating, money-emphasis, and trust tokens, mirrored in `colors.ts` where palette-level. - [ ] `ICONS.logo` is the new Balinyaar mark; `PencilIcon.tsx`, `light.ts`, `dark.ts`, `AppImage`, and the eight dead icon entries are deleted; favicon + webmanifest rebuilt with brand colors and only real files referenced. - [ ] `` actually renders 48px; the registry is one visual family; `back`/`chevron_start` mirror correctly under `dir="rtl"`; `AppButton` has no default margin and **zero** `sx={{ m: 0 }}` neutralizations remain in `src`. - [ ] First visit with OS dark preference and no cookie paints dark with no light flash; browser `theme-color` matches the scheme in both modes. - [ ] Visual verification on the four axes — `/fa` + `/en` × light + dark — and mobile + desktop widths on the §7 walk, with screenshots in the report; skill + `client/CLAUDE.md` updated to match reality. ## 7. How to test (what a human can verify after this phase) 1. Open `/fa/login` (private window, OS set to dark, no cookies): the page paints **dark immediately** — no light flash — and shows the **new Balinyaar logomark** (not a pencil), recoloring correctly in dark mode. 2. On the customer home `/fa`: buttons are flat (no elevation) with comfortable padding; cards show hairline borders + soft teal-tinted shadows (not grey); chips are soft-tinted; nothing renders stock-MUI grey/blue. Tab through: every focusable element shows the same 2px brand focus ring. 3. Visit `/fa/nurse` and `/fa/admin` (seeded accounts): the top bar is a cream/paper surface with a hairline divider — **not** a solid teal slab; sidebar icons are one visual family (all Rounded); the selected nav item uses the soft-primary fill. 4. Trigger an inline alert (e.g. a form error state) and a toast on the same screen: both use the same brand-harmonized semantic colors, in light and dark. 5. On `/fa`, inspect Persian text: no letter-spacing gaps inside joined words, headings don't clip ascenders, bold shows a real weight hierarchy (not everything Bold). Compare `/en`: headings render per the 3.3 decision (Space Grotesk or the documented system stack). 6. Check the browser tab: the favicon is the new mark; toggling OS dark mode flips the browser chrome (`theme-color`) to the deep-teal value. ## 8. Hand off & document (close the phase) - Update `client/CLAUDE.md`: the Theme System section (`ColorSchemeScript` now real — remove the doc-drift note; new tokens; the weight decision), the theme entries in "Project Structure" (`light.ts`/`dark.ts` removed, new icon assets), and the Fonts table if Space Grotesk was wired. Update `.claude/skills/frontend-designer/SKILL.md` per 3.10 (part of the phase, not optional). - Write the frontend report at `dev/shared-working-context/reports/ui-phase-0-report.md`: what changed at the theme layer, the type-scale + weight + icon-family decisions, before/after screenshots on the four axes, what later phases must know (new token names, directional-icon rule), and any REQs filed (expected: none). - Save a memory note per operating-rules §8: the theme/token/icon/brand decisions, the size-bug fix, the margin-default removal, and the no-flash boot mechanism — later UI phases build on all of them.