# UI Phase 0 — Design language & theme foundation — Report (2026-07-17) ## What was built ### `theme.components` pass (the highest-leverage change) `client/src/theme/theme.ts` now carries a `components` block covering `MuiCssBaseline` (global `:focus-visible` → 2px `var(--bal-focus-ring)`), `MuiButton` (`disableElevation`, `--bal-radius-sm`, comfortable padding), `MuiPaper` (hairline `var(--bal-divider)` border, `backgroundImage: none`), `MuiAppBar` (cream/paper surface + hairline bottom border, `color="transparent"` default — no more solid teal slab), `MuiOutlinedInput` (house radius + calmer resting border + primary hover), `MuiChip` (soft `--bal-primary-soft` fill for default-color filled chips only — `ownerState`-scoped so `color="success"`/`"error"` chips are untouched), `MuiToggleButton`/`MuiListItemButton` (`.Mui-selected` → primary-soft fill + primary text), `MuiDialog` (`--bal-radius-lg`, tighter mobile margins), `MuiTabs`/`MuiTab` (3px indicator, 48px min-height), `MuiAlert` (house radius — severities now come from the palette, see below), `MuiStepIcon` (brand active/completed), `MuiSkeleton` (warm `primary-soft` tint), `MuiTooltip` (ink/cream inversion). Every value is `var(--bal-*)` or a logical CSS property — zero raw hex, zero physical direction props. A `theme.shadows` array (`TEAL_SHADOWS`, 25 entries tiered onto `--bal-shadow-1/2/3`) replaces MUI's default grey stack **globally** — every component that reads `theme.shadows[n]` (Dialog, Menu, Popover, AppBar elevation, Autocomplete…) gets teal-tinted shadows for free, not just the ones this file overrides directly. Both themes are wrapped in `responsiveFontSizes()` for per-breakpoint heading scaling. ### Semantic palette `LIGHT_PALETTE`/`DARK_PALETTE` (`colors.ts`) now define `success`/`error`/`warning`/`info` (main + contrastText), sourced from the same values as the `--bal-*` feedback tokens — an inline `` and a success toast are now the same color family in both schemes. `AppAlert`'s defaults flipped from the footgun (`severity="error" variant="filled"`) to a calm baseline (`severity="info" variant="standard"`) — every existing call-site already passed an explicit severity+variant, so this was a safe default change with zero call-site migration needed (verified by grep before changing). ### Persian type scale `typography.ts` rewritten: `letterSpacing: 0` on every `TYPOGRAPHY_RTL` variant, a shared size/line-height scale (`SIZE_SCALE` — body line-height 1.7, headings 1.4–1.5, no more 6rem default h1), and the weight decision: **700 for headings/buttons, 500 for in-text emphasis, 400 body — never 600** (neither Mikhak nor Space Grotesk loads a 600 face; a requested 600 silently rendered full Bold). Swept **64** `fontWeight: 600` sx call-sites across 42 files to 500/700 by this rule (9→700, 55→500; judgment calls documented in the commit — e.g. selection-card titles → 700, chip/row labels → 500). LTR keeps its original split (Space Grotesk headings, system-font body); RTL keeps Mikhak everywhere (full glyph coverage). Space Grotesk is now genuinely wired via `next/font/google` in `app/[locale]/layout.tsx` (self-hosted at build time, `preload: false`, attached only on `en` — mirrors the Mikhak/`fa` discipline exactly); the dead "not currently wired" comment is gone. ### Token extension (`tokens.css` + `colors.ts` mirror where palette-level) Added, both scheme blocks: `--bal-radius-sm/md/lg` (4/10/16 — controls/cards/dialogs), `--bal-motion-fast/base/slow` + `--bal-easing-standard` (for phase 12), `--bal-shadow-1/2/3` (teal-tinted light, black-teal dark), `--bal-focus-ring`, `--bal-rating`/`--bal-rating-empty` (retiring the muddy `--bal-warning` star fill — phase 1 wires this into `RatingInput`), `--bal-trust`/`--bal-trust-soft` (a distinct blue identity for verified marks, separate from primary/success — phases 1/4/8 consume this), `--bal-money-emphasis` (AA-contrast-safe; `--bal-secondary` fails AA at small sizes on white, ~2:1). `tokens.css`'s header now points at the frontend-designer skill instead of the deleted `product/balinyaar.html`. ### Brand mark — the pencil is gone Two new SVGs under `AppIcon/icons/`: **`LogoMark.tsx`** (monochrome `currentColor` "b" glyph — stem + a true evenodd-ring bowl, no background square — registered as `ICONS.logo`, used anywhere via ``, recolors for free in dark mode) and **`LogoLockup.tsx`** (the full-color mark: deep-teal rounded-square ground, cream glyph, terracotta dot, all `var(--bal-*)`-driven) used directly by `BrandMark.tsx` next to the (still translated, never-baked-into-SVG) wordmark. `PencilIcon.tsx` (the Twemoji starter pencil, the app's last hard-coded-hex SVG) is deleted. Favicon + webmanifest regenerated from the same construction via a one-off `sharp` script (fixed brand hex — the one place a literal hex is correct, since it's a static binary asset, not app code): `src/app/favicon.ico` (real multi-size PNG-in-ICO, 16/32/48) and `public/img/favicon/{16,32,48,180,192,512}.png` all now render the actual mark; `site.webmanifest`'s `theme_color`/`background_color` are the real brand teal/cream (were starter `#000000`/`#ffffff`), and every referenced icon file now genuinely exists. ### Icon system - **Size bug fixed**: `AppIcon.tsx` drives size via `style.fontSize` (which MUI SvgIcon's `1em`-based CSS actually respects) instead of `width`/`height` *attributes* (which that same CSS silently beat) — all ~40 existing `size={14..56}` call-sites now render at the requested size. The invalid `size` DOM attribute is no longer spread onto the SVG; the unknown-icon `console.warn` is dev-only. - **One visual family**: the ~90-entry registry (`AppIcon/config.ts`) is now 100% MUI `*Rounded` — every filled/outlined starter icon was swapped. The 8 dead starter entries (`daynight`/`night`/`day`/`visibilityon`/`visibilityoff`/`signup`/`login`/`settings`) are deleted (re-verified zero usages before deleting). - **New vocabulary**: `back`/`chevron_start`, `share`, `copy`, `phone`, `navigate`, `sort`, `attachment`, `star_half`. - **Directional mirroring**: `back`/`chevron_start` are registered in a new `DIRECTIONAL_ICONS` set; `AppIcon` stamps `data-icon-directional` on those, and one CSS rule (`app/globals.css`) does the flip (`[dir='rtl'] [data-icon-directional] { transform: scaleX(-1) }`) — adding a future directional icon is a one-line registry addition, never a per-component flip. ### AppButton de-startering Removed `DEFAULT_SX_VALUES = { margin: 1 }` and the dead `underline`/`label`/`text` prop cruft (`text`/`label` duplicate props stay — 238 call-sites use them — only the invalid `underline` spread onto non-link buttons and the false "Box around to specify margins" JSDoc were removed). Swept the now-redundant `sx={{ m: 0 }}` neutralization at **192** call sites across ~68 files (6 parallel batches, verified against the original 213-occurrence/82-file audit count — the ~21 difference is re-count timing, not missed sites); **3** genuine non-AppButton `m: 0` uses (on `FormControlLabel`, an unrelated real layout decision) were correctly left alone. Outer spacing is now the parent's job (Stack/Box gaps), as intended. ### Starter residue purge Deleted `theme/light.ts` + `theme/dark.ts` (+ their `theme/index.ts` exports: `APP_THEME`/`LIGHT_THEME`/`DARK_THEME`/`LIGHT_THEME as default` — nothing imported them) and the deprecated `TYPOGRAPHY` alias. Deleted the unused `AppImage` component + its test + barrel exports (zero product usages, verified). Rewrote `globals.css` as an intentional minimal base: kept the `box-sizing` reset, dropped `max-height: 100vh` and the `max-width: 100vw; overflow-x: hidden` mask, added `::selection` in brand colors + the directional-icon CSS rule, and a commented decision on the `a { color: inherit }` reset. Fixed the false `CONTENT_MIN_WIDTH` comment and the starter commented-alternatives style in `components/config.ts`. ### No-flash color-scheme boot — CSS only, no boot script The original phase spec called for an inline pre-paint `