Summary

Consolidated theme token system for Flow Mobile. Single source of truth for all AppTheme constants. Branch: edit/mobile-audit-ux-polish.


Canonical Color Tokens

Use these in all new code. Legacy aliases (below) still compile but are deprecated.

// Brand
static const Color electricCoral   = Color(0xFFFF5E57); // primary CTA, accent
static const Color electricViolet  = Color(0xFF8A2BE2); // brand complement
static const Color mintGreen       = Color(0xFF00C896); // success, gamification
static const Color inkBlack        = Color(0xFF18181B); // headlines, dark bg
static const Color pureWhite       = Color(0xFFFFFFFF);
 
// Text hierarchy
static const Color textPrimary     = Color(0xFF18181B); // Zinc 900
static const Color textSecondary   = Color(0xFF52525B); // Zinc 600
static const Color textMuted       = Color(0xFF9E9E9E); // captions, placeholders
 
// Dark mode text
static const Color textPrimaryDark   = Color(0xFFFFFFFF);
static const Color textSecondaryDark = Color(0xFFA1A1AA);
 
// Dividers / surfaces
static const Color dividerLight    = Color(0xFFE4E4E7);
 
// Brand gradient (coral → violet)
static const LinearGradient brandGradient = LinearGradient(
  colors: [electricCoral, electricViolet],
);

warmCanvas retoned 2026-05-08

warmCanvas was originally #F0EEE9 — a warm off-white with a yellow tint. It clashed with the modern editorial direction, especially in the redesigned event details + crew wizard surfaces. Retoned to #FAFAFA (zinc-50, neutral light gray) at the token level. The token name is kept for back-compat across ~50 call-sites; new code should still prefer pureWhite for primary surfaces.

static const Color warmCanvas = Color(0xFFFAFAFA); // was 0xFFF0EEE9

Legacy Aliases (do not use in new code)

static const Color textPrimaryLight   = textPrimary;    // → textPrimary
static const Color textSecondaryLight = textSecondary;  // → textSecondary
static const Color textSecondaryWarm  = textMuted;      // → textMuted

These were not removed to avoid churn across 147 call-sites. New screens must use canonical names only.


Radius Tokens

TokenValueRule
radius88.0Chips, small elements
radius1212.0Snackbars, small cards
radius1414.0Form card corners
radius1616.0Standard cards
radius2424.0Hero cards, large containers
radius2828.0Bottom sheet tops (matches bottomSheetTheme)
radiusFull9999.0Pill buttons, social buttons

radius32 was removed — it had value 24.0 behind a lying name. All former call-sites migrated to radius24 (cards) or radius28 (bottom sheets). Never re-add it.


ColorScheme Wiring

colorScheme: ColorScheme(
  primary: electricCoral,     // main CTAs, brand signature
  secondary: electricViolet,  // selected chips, highlights
  onSecondary: pureWhite,
  tertiary: mintGreen,        // success accents, gamification
  onTertiary: inkBlack,
  ...
);

Widgets automatically affected (no code change required):

  • chat_screen.dart typing indicator → violet
  • profile_avatar.dart color-from-hash → picks from coral / violet / mint

Page-Title Typography

AppTheme.pageTitleStyle

All screen page titles — navbar tabs AND secondary/pushed screens — must use AppTheme.pageTitleStyle. This is the single source of truth for header typography.

// static final (not const) — GoogleFonts.inter() bakes in the fontFamily path
// so the font is always Inter regardless of DefaultTextStyle context.
static final TextStyle pageTitleStyle = GoogleFonts.inter(
  fontSize: 30,            // = 22 × 1.35 (FlowBrandLockup height × scale)
  fontWeight: FontWeight.w900,  // Black — matches official brand-book lockup spec
  letterSpacing: -0.6,     // = -(30 × 0.02) — brand-book SVG uses letter-spacing="-0.02em"
  color: textPrimary,
  height: 1.0,
);

Why final not const: const TextStyle without a fontFamily inherits font from DefaultTextStyle. Inside SliverAppBar.title, DefaultTextStyle is AppBarTheme.titleTextStyle (Inter). In a plain Row, it resolves differently — causing the visual mismatch. GoogleFonts.inter() always returns a TextStyle with fontFamily: 'packages/google_fonts/Inter' baked in.

Letter-spacing formula: brand-book lockup SVGs specify letter-spacing="-0.02em". In Flutter, letterSpacing is logical pixels (not em), so the conversion is -(fontSize × 0.02). For pageTitleStyle at 30px → -0.6. For FlowBrandLockup at variable height: -(height × 1.35 × 0.02).

Font parity rule: FlowBrandLockup’s wordmark also uses GoogleFonts.inter() (not bare TextStyle()). Both must use the same GoogleFonts call so they render identically regardless of DefaultTextStyle context. Never use bare TextStyle(fontFamily: ...) for brand or title text — always go through GoogleFonts.inter().

Canonical source: flow_docs/content/project/brand-book.md. The lockup SVGs in that file define the authoritative weight (900) and letter-spacing (-0.02em). If the brand spec ever changes, update both pageTitleStyle and FlowBrandLockup together.

Never use const Text(style: AppTheme.pageTitleStyle)pageTitleStyle is a final (runtime) value, not a compile-time constant. The const on Text will cause an invalid_constant analysis error.

Brand Logo Rule

FlowBrandLockup (mark + “flow” wordmark) appears only on the Feed (Home) screen.

All other navbar tab screens use FlowBrandMark(height: 18) + Text(..., style: AppTheme.pageTitleStyle) side-by-side — the mark echoes the logo identity while the text changes per screen. Pushed (secondary) screens use plain Text only.

// Navbar tab screens — mark + title
Row(
  mainAxisSize: MainAxisSize.min,
  crossAxisAlignment: CrossAxisAlignment.center,
  children: [
    FlowBrandMark(height: 18),
    const SizedBox(width: 10),
    Text(l10n.events, style: AppTheme.pageTitleStyle),
  ],
)
ScreenHeader widgetLeading
Feed / HomeFlowBrandLockup(height: 22)none (root tab)
EventsFlowBrandMark(18) + Text(l10n.events)none (root tab)
Social / FriendsFlowBrandMark(18) + Text(l10n.socialLabel)none (root tab)
ChatsFlowBrandMark(18) + Text('Messaggi')none (embedded)
ProfileFlowBrandMark(18) + Text(l10n.profile)none (root tab)
SettingsText(l10n.settings, style: AppTheme.pageTitleStyle)AppIcons.back
All settings sub-screensText(l10n.x, style: AppTheme.pageTitleStyle)AppIcons.back
NotificationsText(l10n.notificationsTitle, style: AppTheme.pageTitleStyle)AppIcons.back (custom header)
LeaderboardText(l10n.leaderboard, style: AppTheme.pageTitleStyle)AppIcons.back
Edit ProfileText(l10n.editProfile, style: AppTheme.pageTitleStyle)AppIcons.close (dismiss)

Back Button Rule

Every pushed route must have a dedicated back/close button in the leading position:

  • Use AppIcons.back (chevron-left) for standard back navigation
  • Use AppIcons.close (×) for modal/form dismissal (edit screens, creation screens)
  • Use context.pop() (GoRouter) for the onPressed handler
  • Screens with a custom header (no AppBar) must add the back IconButton manually — Flutter cannot auto-add leading to non-AppBar layouts

Top-Bar Icon Button Standard

All action icons in page headers (AppBar actions or custom header Rows) MUST use IconButton directly — no Container wrappers, no custom BoxDecoration, no GestureDetector around a styled box.

// ✅ Correct — transparent bg, consistent touch target, accessible
IconButton(
  icon: const Icon(AppIcons.share, size: 22),
  onPressed: _shareProfile,
  color: AppTheme.inkBlack,          // explicit on non-AppBar headers
  visualDensity: VisualDensity.compact, // tighter layout when needed
)
 
// ❌ Wrong — warmCanvas Container was the old pattern, now removed
Container(
  width: 38, height: 38,
  decoration: BoxDecoration(color: AppTheme.warmCanvas, ...),
  child: Icon(icon, size: 20, ...),
)

Dark screens (e.g. TicketWalletScreen): use color: Colors.white on the IconButton.
Brand-colored actions (e.g. new-chat button): use color: AppTheme.electricCoral — the accent is on the icon, not a background fill.
Notification bell badge: keep the Stack wrapper for the badge overlay; replace only the inner Container+Icon with IconButton.

Navbar-tab screens (reachable from the bottom nav) must NOT use FloatingActionButton — the FAB is visually obscured by the bottom navbar. Primary actions go in SliverAppBar.actions as IconButtons instead:

ScreenAction movedPrevious location
EventsAppIcons.add → create eventFloatingActionButton

Never add FlowBrandLockup to non-feed screens. The brand mark is a unique identifier for the discovery feed, not a generic title widget.


Icon Packages

Three icon packages are active. Do not collapse them — each serves a different purpose.

PackageImportUsed for
lucide_iconsLucideIcons.*All navigation / top-bar chrome via AppIcons constants
font_awesome_flutterFontAwesomeIcons.apple, .googleFull-width social buttons on welcome screen
material_design_icons_flutterMdiIcons.google, .apple, .facebook3-icon divider row on login / forgot screens

AppIcons — The Nav Icon Contract

lib/core/theme/app_icons.dart is the single point of truth for all navigation and top-bar icons. Always use AppIcons.* constants — never import LucideIcons.* directly in screen files.

// Bottom nav
AppIcons.navHome    // LucideIcons.home
AppIcons.navSearch  // LucideIcons.search
AppIcons.navCreate  // LucideIcons.plus
AppIcons.navSocial  // LucideIcons.zap
AppIcons.navProfile // LucideIcons.user
 
// Top bar
AppIcons.back       // LucideIcons.chevronLeft  ← replaces all arrow_back variants
AppIcons.close      // LucideIcons.x
AppIcons.settings   // LucideIcons.settings
AppIcons.notifications // LucideIcons.bell
AppIcons.share      // LucideIcons.share2
AppIcons.more       // LucideIcons.moreVertical

Lucide active-state rule: Lucide icons are line-only (no filled variant). Active bottom-nav tabs use electricCoral color + a 4px dot indicator below the icon — not a filled icon switch.

MDI and FA icons are rendered monochrome at AppTheme.textSecondary — never brand-colored.

Icon Migration Status — Completed 2026-05-06

All 9 queued screens have been migrated to AppIcons.* for nav chrome:

  • events/create_event_screen.dart
  • events/event_details_screen.dart
  • events/series_detail_screen.dart
  • social/create_crew_screen.dart
  • social/crew_matchmaking_wizard.dart ✓ (3 inline nav buttons; import app_icons.dart added)
  • messaging/chat_screen.dart
  • messaging/chats_screen.dart
  • reviews/create_review_screen.dart
  • profile/edit_profile_screen.dart

Body-content icons (chip delete, list-row chevron_right, star ratings, Icons.groups) are intentionally excluded from the Lucide migration — they are content icons, not nav chrome. The chats_screen.dart search-bar prefix/clear and chat_screen.dart reply-cancel were migrated on 2026-05-06 as they are inline UI chrome, not semantic content indicators.

AppBar Consistency — Completed 2026-05-06

The following screens were missing scrolledUnderElevation: 0 and/or using scaffoldBackgroundColor instead of AppTheme.pureWhite. Fixed:

  • chats_screen.dart — added scrolledUnderElevation: 0
  • edit_profile_screen.dart — added scrolledUnderElevation: 0; backgroundColorAppTheme.pureWhite
  • create_crew_screen.dart — added scrolledUnderElevation: 0; backgroundColorAppTheme.pureWhite; surfaceTintColorColors.transparent
  • create_event_screen.dart — added scrolledUnderElevation: 0 to both AppBars (preset picker + wizard)
  • settings_screen.dart, about_screen.dart, notifications_settings_screen.dart, privacy_settings_screen.dartscaffoldBackgroundColorAppTheme.pureWhite; scrolledUnderElevation: 0; surfaceTintColor: Colors.transparent
  • leaderboard_screen.dart, create_review_screen.dart, series_detail_screen.dart — same tokens applied
  • event_details_screen.dart — error/not-found AppBars: pageTitleStyle, AppIcons.back leading, pureWhite tokens

Deferred Technical Debt

  • Shadow dark-mode: shadowSM/MD/LG use Colors.black, invisible on inkBlack backgrounds. Requires brightness-aware shadows or Material 3 surfaceTint. Low priority.
  • 147 legacy alias call-sites: Mechanical rename to canonical tokens — separate commit, low regression risk but high churn.
  • Violet-tinted shadows: Evaluate after visual consistency is achieved.

auth-screens — auth-specific token usage style-ui-redesign-pass-1 — first redesign pass context