# Responsive UX/UI strategy This document describes the responsive design system added to the CurrenciCombo portal. It is intended to be the source of truth for future responsive work. ## Goals The portal ships to a mixed audience that includes treasury operators at desks (≥ 1440 px displays), branch-office users on 13" laptops, tablets in landscape, and mobile phones for on-call auditors. The system has to adapt across all of those without: - horizontal scroll at any viewport ≥ 320 px, - layout shift on resize (no CLS), - unusable tap targets on touch devices, - losing keyboard navigation on desktop, or - degrading desktop performance. ## Architecture The responsive system is CSS-first and token-driven. There is **one** runtime hook (`useBreakpoint`) used by the handful of components that have to swap structure (nav, workspace gate) — everything else is pure CSS. ### Layered stylesheet order `src/main.tsx` imports styles in this order: 1. `src/styles/tokens.css` — design tokens (no visual side effects). 2. `src/index.css` — the original desktop baseline stylesheet. 3. `src/styles/responsive.css` — media-query overrides. 4. `src/styles/a11y.css` — focus ring, skip-to-content, sr-only, touch-target enforcement. Order matters: overrides must appear after the baseline so that rules at equal specificity win. Nothing in `responsive.css` or `a11y.css` changes behavior at `≥ lg` — every rule is wrapped in a `@media (max-width: …)` or `@media (pointer: coarse)` query. ### Tokens All sizing values that change with viewport live in `src/styles/tokens.css`: | Category | Tokens | | ------------- | -------------------------------------------- | | Breakpoints | `--bp-xs 0`, `--bp-sm 480`, `--bp-md 768`, `--bp-lg 1024`, `--bp-xl 1440` (px) | | Fluid type | `--fs-2xs` through `--fs-4xl`, all `clamp()` | | Fluid spacing | `--space-0` through `--space-12`, all `clamp()` | | Line height | `--lh-tight`, `--lh-snug`, `--lh-normal`, `--lh-relaxed` | | Motion | `--motion-fast 120ms`, `--motion-base 200ms`, `--motion-slow 320ms`, `--motion-ease` | | Z-index | `--z-base`, `--z-sticky`, `--z-drawer-backdrop`, `--z-drawer`, `--z-dropdown`, `--z-modal`, `--z-toast`, `--z-tooltip`, `--z-focus` | | Focus ring | `--focus-ring-color #60a5fa`, `--focus-ring-offset 2px`, `--focus-ring-width 2px` | | Safe area | `--safe-top/right/bottom/left` via `env()` | | Tap target | `--tap-min 44px` | ### Fluid scaling formula Typography and spacing use `clamp(min, preferred, max)` where `preferred` has a `vw` term so values interpolate continuously between the smallest design viewport (320 px) and the largest (1440 px). This is what the spec calls "avoid hard breakpoints where possible; prefer fluid responsiveness" — between breakpoints, sizes grow continuously rather than snapping. Example, `--fs-base: clamp(0.8125rem, 0.77rem + 0.23vw, 1rem)`: - at 320 px → 13 px (minimum), - at 1024 px → ~15.1 px, - at 1440 px → 16 px (maximum clamp). ### Breakpoints | Name | CSS range | Semantic | | ---- | ----------------------- | ---------------------- | | xs | 0 – 479 px | Small phones, portrait | | sm | 480 – 767 px | Larger phones | | md | 768 – 1023 px | Tablets | | lg | 1024 – 1439 px | Laptops | | xl | ≥ 1440 px | External displays | Semantic aliases exposed by `useBreakpoint()`: - `isMobile` = `< md` (phones) - `isTablet` = `md – lg` (tablets, portrait iPads) - `isDesktop` = `≥ lg` (laptops, external monitors) ### Hooks - **`useMediaQuery(query)`** — subscribes to a matchMedia `change` event via `useSyncExternalStore`. No resize listener — re-renders only when the query result actually flips. - **`useBreakpoint()`** — returns `{ current, isXs/Sm/Md/Lg/Xl, isMobile/Tablet/Desktop }`. Composed of four `useMediaQuery` calls using the `--bp-*` values from the tokens. - **`useReducedMotion()`** — wraps `(prefers-reduced-motion: reduce)` so components that animate in JS can disable animations. - **`useOrientation()`** — returns `'portrait'` or `'landscape'`. All four hooks are SSR-safe (return `false` / default in a non-DOM environment). ## Component inventory and strategy ### Global primitives - **Skip-to-content link** — rendered at the top of the tree via ``. Visually hidden until `:focus`, then slides in at top-left. Target is `
` in `PortalLayout`. - **VisuallyHidden** — `sr-only` wrapper, for adding accessible text without visual presence. ### Portal chrome - **Topbar** (`.portal-topbar`): fixed 48 px height at all sizes. Below `md`, the "Production" environment badge and the "Solace Bank Group" logo text are hidden so the hamburger, logo mark and user avatar fit on a phone without wrapping. - **Hamburger toggle** (`.portal-menu-toggle`): new `