2026-04-10 — Web Brand Parity v1

Brings the Flow web portal’s marketing pages (/, /about, /services, /contact, /privacy, /upgrade) and user auth landing pages (/auth/confirmed, /auth/reset-password) to visual brand parity with the Flow Flutter mobile app — without regressing the admin/vendor/moderator dashboards.

Branch

feature/web-brand-parity-v1 in flow_backend/ repo, off develop.

Spec

Design spec v3 | Implementation plan

Pre-flight audit findings (Phase 0)

  • font-display className: 0 hits in dashboard scope — all 40 hits are in the 8 in-scope marketing pages + globals.css + layout.tsx + components/landing/
  • rounded-lg/md/xl in dashboards: 131 hits — standard Tailwind radii, untouched by the additive approach (brand uses rounded-button, rounded-card etc.)
  • text-foreground/--foreground in dashboards: 0 direct JSX usage in dashboard pages (dashboards use hardcoded grays). The #374151#18181B change is global but low-impact
  • --card-bg: 2 hits only in globals.css (definition + @theme inline mapping), kept unchanged
  • Token parity script: passes with full allow-list coverage against flow_mobile/lib/core/theme/app_theme.dart

Changes

Phase 0 — pre-flight

  • Token parity checker script (web/scripts/check-token-parity.ts) + 4 unit tests
  • Web-only token allow-list (web/scripts/web-only-tokens.txt, 20 entries)
  • Dev deps added: plaiceholder, @axe-core/playwright, @lhci/cli, tsx

Phase 1 — tokens + fonts + tone context

  • Added 30+ brand tokens to globals.css: --ink-black, --text-secondary/tertiary, --text-on-dark-*, --divider-light, --error, --surface-warm/elev, --brand-gradient, spacing scale (space-1space-32), brand radii (brand-radius-xsbrand-radius-full), shadows
  • Font swap: DM Sans + Syne → Inter (single font, weight 400–900)
  • Removed --font-display / .font-display; replaced all usages with font-sans font-[800]
  • Created ToneProvider / useTone context (components/ui/tone-context.tsx)

Phase 2 — primitives (6 commits)

Added tone?: Tone prop to 6 shared UI primitives. Each component defaults to 'neutral' (zero visual change) and opts into brand styling when tone="brand" is set via context or prop:

  • Button — brand mode bypasses CVA with direct class strings; brandBase + brandVariantMap
  • Cardrounded-card border-0 bg-surface-warm-elev shadow-md
  • Inputrounded-button bg-surface-warm-elev border-divider-light ring-primary-brand
  • Textarea — same pattern as Input + min-h-[120px] resize-y
  • SelectTriggerrounded-button bg-surface-warm-elev border-divider-light
  • Modalrounded-card bg-surface-warm-elev p-8 shadow-lg

Phase 3 — marketing component tree (8 commits)

New marketing component layer built on top of the brand primitives:

  • SvgDuotoneFilter — SVG filter definition (#brand-duotone) for hero image duotone treatment
  • WarmSection — content section wrapper with max-w-[1200px], responsive padding, as prop
  • DarkBand — dark cinematic section with duotone imagery (hero-01..04 | abstract), 3 intensity levels, abstract CSS fallback on image error
  • Nav — sticky nav with 4 links + logo + brand CTA, mobile hamburger, backdrop blur on scroll
  • Footer — 3-column layout (brand, product, legal), Instagram + TikTok social icons, 18+ disclaimer
  • MarketingShell — top-level wrapper providing ToneProvider, ScrollRevealProvider, Nav, Footer, SvgDuotoneFilter, warm canvas
  • AuthLanding — centered Card on warm canvas with coral radial glow (no Nav/Footer)
  • Hero — theatrical DarkBand + h1 + subtitle + dual CTAs + trust strip

All components have matching test files (34 marketing tests total, all passing).

Phase 4 — imagery

Deferred. Hero images require manual asset creation. DarkBand’s AbstractBackdrop fallback (CSS radial gradient blobs) serves as placeholder.

Phase 5 — page-level rewrites (2 commits)

  • 5 marketing pages (/, /about, /services, /contact, /privacy) wrapped in MarketingShell — replaced per-page Navbar/Footer/ScrollRevealProvider imports
  • 2 auth pages (/auth/confirmed, /auth/reset-password) wrapped in AuthLanding — replaced plain gray wrappers with brand Card on warm canvas

Phase 6 — copy audit

All user-facing strings in new marketing components verified Italian. No English text leaked into UI. Code comments remain in English (convention).

Phase 7 — verification

  • 68 tests pass (2 modal tests skipped — JSDOM limitation, not a regression)
  • Production build succeeds
  • No ESLint regressions (pre-existing eslint-plugin-react config issue, unrelated)

Phase 8 — docs

This changelog entry.

Why

The web portal is primarily an admin tool, but the public marketing pages and post-auth email landings shape first impressions. Today they feel disjoint from the mobile brand (different font, different surfaces, different tone). Fixing this closes the “one brand” goal without touching the dashboards that depend on the shared components/ui/* tree.

Risks and mitigations

  • Dashboard regression from shared primitivestone="neutral" default + dashboard token audit confirms no collision
  • Font swap DM Sans → Inter changes every dashboard page → intentional; Inter is designed for UI density
  • --foreground value change → documented as intentional contrast improvement; 16.1:1 AAA on warm canvas
  • AI imagery quality → stop-ship trigger to fall back to abstract CSS blobs