# UI Modernization Plan — EPower Maps Flutter App
_Target: Clean, modern, Next.js-inspired aesthetic using only existing Flutter Material 3 widgets_

---

## Design System Foundation

### Spacing Grid
| Token | Value | Use |
|-------|-------|-----|
| `xs` | 4dp | Icon padding, tight gaps |
| `sm` | 8dp | Between related elements |
| `md` | 12dp | Card internal padding (tight) |
| `base` | 16dp | Standard padding, page margins |
| `lg` | 24dp | Between sections |
| `xl` | 32dp | Major section breaks |

### Color Roles (Material 3 mapping)
```
Primary:       #F15A23   → AppBar, FAB, active nav, key CTA buttons
PrimaryDark:   #B72500   → Pressed states, dark header gradient
PrimaryLight:  #FFCCBC   → Soft badges, chip backgrounds
Accent:        #03A9F4   → Info chips, GPS indicator, secondary actions
Surface:       #FFFFFF   → Card backgrounds
SurfaceVariant: #F5F5F5  → Page background, subtle card fill
OnSurface:     #212121   → Primary text
SecondaryText: #757575   → Subtitles, hints
Success:       #4CAF50   → Collected status, sync done
Warning:       #FF9800   → In-progress sync
Error:         #F44336   → Error states
```

### Typography Hierarchy
| Role | Style | Use |
|------|-------|-----|
| `headlineSmall` | 24sp, medium | Page titles (card-based pages) |
| `titleLarge` | 22sp, medium | Section titles |
| `titleMedium` | 16sp, medium | Card titles, list item primary |
| `titleSmall` | 14sp, medium | Section headers, form labels |
| `bodyMedium` | 14sp, regular | Body text, list subtitles |
| `bodySmall` | 12sp, regular | Hints, captions, timestamps |
| `labelMedium` | 12sp, medium | Chip labels, badges |

---

## Phase 0 — Theme Overhaul (`lib/app.dart`)

**Goal:** Replace the basic orange seed with a hand-crafted Material 3 scheme that controls Card, AppBar, InputDecoration, FAB, Chip, and Divider globally. This means all later pages automatically inherit correct base styling.

### Changes to `app.dart` ThemeData:

```dart
ThemeData(
  useMaterial3: true,
  colorScheme: ColorScheme.fromSeed(
    seedColor: AppColors.primary,
    primary: AppColors.primary,
    secondary: AppColors.accent,
    surface: Colors.white,
    surfaceContainerHighest: const Color(0xFFF5F5F5),
  ),

  // AppBar: white surface, shadow on scroll, not colored
  appBarTheme: AppBarTheme(
    backgroundColor: Colors.white,
    foregroundColor: AppColors.primaryText,
    elevation: 0,
    scrolledUnderElevation: 2,
    centerTitle: false,
    titleTextStyle: TextStyle(
      color: AppColors.primaryText,
      fontSize: 18,
      fontWeight: FontWeight.w600,
    ),
    iconTheme: IconThemeData(color: AppColors.primaryText),
    actionsIconTheme: IconThemeData(color: AppColors.primary),
    systemOverlayStyle: SystemUiOverlayStyle.dark,
  ),

  // Cards: very subtle border, no heavy shadow
  cardTheme: CardThemeData(
    elevation: 0,
    shape: RoundedRectangleBorder(
      borderRadius: BorderRadius.circular(12),
      side: BorderSide(color: Color(0xFFE5E7EB), width: 1),
    ),
    margin: EdgeInsets.zero,
    color: Colors.white,
  ),

  // Inputs: filled style (modern look)
  inputDecorationTheme: InputDecorationTheme(
    filled: true,
    fillColor: const Color(0xFFF9FAFB),
    border: OutlineInputBorder(
      borderRadius: BorderRadius.circular(10),
      borderSide: BorderSide(color: Color(0xFFE5E7EB)),
    ),
    enabledBorder: OutlineInputBorder(
      borderRadius: BorderRadius.circular(10),
      borderSide: BorderSide(color: Color(0xFFE5E7EB)),
    ),
    focusedBorder: OutlineInputBorder(
      borderRadius: BorderRadius.circular(10),
      borderSide: BorderSide(color: AppColors.primary, width: 1.5),
    ),
    errorBorder: OutlineInputBorder(
      borderRadius: BorderRadius.circular(10),
      borderSide: BorderSide(color: Colors.red),
    ),
    contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 14),
    labelStyle: TextStyle(color: AppColors.secondaryText),
  ),

  // FAB: extended style default
  floatingActionButtonTheme: FloatingActionButtonThemeData(
    backgroundColor: AppColors.primary,
    foregroundColor: Colors.white,
    elevation: 2,
    shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
  ),

  // Chips: subtle bordered style
  chipTheme: ChipThemeData(
    shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
    padding: EdgeInsets.symmetric(horizontal: 8, vertical: 2),
  ),

  // Divider: lighter
  dividerTheme: DividerThemeData(
    color: Color(0xFFE5E7EB),
    thickness: 1,
    space: 1,
  ),

  // Drawer: white, matches surface
  drawerTheme: DrawerThemeData(
    backgroundColor: Colors.white,
    elevation: 4,
    shape: RoundedRectangleBorder(
      borderRadius: BorderRadius.only(
        topRight: Radius.circular(0),
        bottomRight: Radius.circular(0),
      ),
    ),
  ),

  // Scaffold background: off-white
  scaffoldBackgroundColor: const Color(0xFFF5F5F5),
)
```

**Implementation note:** Keep the `BlocBuilder<AppBloc>` wrapper unchanged. Only modify the `ThemeData` block.

---

## Phase 1 — Shared Widget Toolkit (`lib/core/widgets/`)

Create 6 reusable components. All files go in `lib/core/widgets/`. No new packages — pure Flutter.

---

### 1a. `lib/core/widgets/app_card.dart`
A standardized card container used everywhere.

```dart
/// AppCard — consistent card with optional padding and title bar.
/// Usage:
///   AppCard(child: Text('content'))
///   AppCard(title: 'Section', child: ...)
///   AppCard(padding: EdgeInsets.zero, child: ListView(...))

class AppCard extends StatelessWidget {
  final Widget child;
  final String? title;         // Optional section heading inside card
  final EdgeInsets? padding;   // defaults to EdgeInsets.all(16)
  final VoidCallback? onTap;   // Makes entire card tappable

  // Build: Card (from theme) → InkWell (if onTap) → padding → Column(title? + child)
}
```

---

### 1b. `lib/core/widgets/list_item_card.dart`
A card-shaped list item replacing bare `ListTile`.

```dart
/// ListItemCard — single-item card for list pages.
/// Renders as Card with 12dp horizontal + 4dp vertical outer margin,
/// InkWell inside for tap ripple, 16dp internal padding.

class ListItemCard extends StatelessWidget {
  final String title;
  final String? subtitle;
  final Widget? leading;       // Icon badge circle or avatar
  final Widget? trailing;      // Chip, icon, etc.
  final VoidCallback? onTap;
  final Color? leadingColor;   // background for leading circle badge
}

/// LeadingIconBadge — optional leading for ListItemCard
/// Renders as CircleAvatar(radius:20) with icon inside
class LeadingIconBadge extends StatelessWidget {
  final IconData icon;
  final Color? color;  // defaults to AppColors.primary with opacity 0.1
  final Color? iconColor; // defaults to AppColors.primary
}
```

---

### 1c. `lib/core/widgets/empty_state.dart`
Modern empty state with icon, message, and optional action.

```dart
/// EmptyState — full centered empty state view
/// Layout: Column(mainAxisAlignment.center) with:
///   - Container(60dp circle with icon, surfaceVariant bg)
///   - 16dp gap
///   - Text(title, titleMedium, center)
///   - 8dp gap  
///   - Text(subtitle, bodyMedium, secondaryText, center)
///   - 24dp gap (if action)
///   - OutlinedButton(label, onTap) (if action provided)

class EmptyState extends StatelessWidget {
  final IconData icon;
  final String title;
  final String subtitle;
  final String? actionLabel;
  final VoidCallback? onAction;
}
```

---

### 1d. `lib/core/widgets/search_field.dart`
Top-of-list search bar, no new packages.

```dart
/// SearchField — styled search TextField
/// Uses theme's inputDecorationTheme, prefixIcon=Icons.search,
/// suffixIcon=clear button (visible when non-empty)
/// Layout: Container(height:44) → TextField with compact styling

class SearchField extends StatelessWidget {
  final TextEditingController controller;
  final String hint;           // e.g., 'Search accessories...'
  final ValueChanged<String> onChanged;

  // Build: TextField using filled decoration from theme,
  //        override fillColor to Colors.white for contrast on grey page bg
}
```

---

### 1e. `lib/core/widgets/status_chip.dart`
Colored status chip for collected/sync/active states.

```dart
/// StatusChip — small label chip with semantic color
/// Variants: collected, notCollected, active, inactive, syncing, error

enum ChipVariant { collected, notCollected, active, inactive, syncing, error }

class StatusChip extends StatelessWidget {
  final ChipVariant variant;
  final String? label;  // if null, uses default label for variant

  // Colors per variant:
  //   collected:    bg=#E8F5E9, text=#2E7D32, icon=check_circle
  //   notCollected: bg=#FAFAFA, text=#757575, icon=radio_button_unchecked  
  //   active:       bg=#E3F2FD, text=#1565C0, icon=circle
  //   inactive:     bg=#FFEBEE, text=#C62828, icon=circle
  //   syncing:      bg=#FFF3E0, text=#E65100, icon=sync
  //   error:        bg=#FFEBEE, text=#C62828, icon=error_outline

  // Build: Container(padding h:8 v:4, borderRadius:6, decoration:fill+border)
  //        Row(icon(14dp) + 4dp + Text(labelSmall))
}
```

---

### 1f. `lib/core/widgets/section_header.dart`
Visual separator with title text for forms and grouped lists.

```dart
/// SectionHeader — bold section title with optional bottom border
/// Usage in forms: SectionHeader('Connection Settings')
/// Usage in lists: SectionHeader('AREA NAME', isAreaHeader: true)

class SectionHeader extends StatelessWidget {
  final String title;
  final bool isAreaHeader;  // if true: uppercase, grey bg for list area rows
  final Widget? trailing;  // optional action widget on right

  // Normal: Padding(h:16, v:12) + Text(titleSmall, primaryText, bold)
  //         + 1dp divider line
  // Area:   Container(grey100 bg, h:full) + Text(uppercase, medium weight)
}
```

---

### 1g. `lib/core/widgets/page_loading.dart`
Skeleton loading without third-party packages.

```dart
/// PageLoading — animated skeleton placeholder
/// Uses AnimatedOpacity pulsing between 0.3 and 1.0 on a
/// 800ms repeating animation (no external shimmer package needed)

class SkeletonBox extends StatefulWidget {
  final double width;
  final double height;
  final BorderRadius? borderRadius;
  // Renders grey Container with pulsing opacity animation
}

class SkeletonListItem extends StatelessWidget {
  // Renders a ListItemCard-shaped skeleton:
  // leading circle + two lines (80% width title, 50% width subtitle)
}
```

---

### 1h. `lib/core/widgets/error_view.dart`
Inline error banner for form pages.

```dart
/// ErrorView — collapsible error banner for form/detail pages
/// Shown only when errorMessage != null

class ErrorView extends StatelessWidget {
  final String? message;
  // If message null: returns SizedBox.shrink()
  // Otherwise: Container(red.50 bg, red.200 border, 12dp padding, rounded)
  //            Row(icon=error_outline + Expanded(Text))
}
```

---

## Phase 2 — Shell Modernization

### `lib/core/widgets/app_scaffold.dart`

**Current state:** Standard Drawer with ListTiles, `_L10nStrings` helper class.

**Changes:**
1. Replace `DrawerHeader` with a modern header:
   - Keep orange bg but add gradient (`PrimaryDark` to `Primary`)
   - Add version number loaded via `PackageInfo`
   - Keep bolt icon + app name text
2. Replace `_DrawerItem` bare `ListTile` with a styled version:
   - Highlight the **active route** with orange left border + `PrimaryLight` background
   - `selectedTileColor: AppColors.primaryLight.withValues(alpha: 0.3)`
   - `selectedColor: AppColors.primary`
   - Detect active route via `GoRouterState.of(context).uri.toString().startsWith(route)`
3. Keep all gesture logic and `_L10nStrings` unchanged
4. Add `const Color(0xFFF5F5F5)` scaffold background to Scaffold in `app_scaffold.dart`

**Note:** Do NOT touch BLoC wiring, language toggle logic, or exit dialog.

---

## Phase 3 — Admin Hub (`lib/features/admin/ui/admin_page.dart`)

**Current:** 4 ListTiles in a ListView.

**Target:** 2×2 grid of nav cards. Each card = rounded rect with:
- Large icon in colored circle
- Title text
- Subtitle text
- Arrow indicator

**Layout:**
```
Scaffold + AppBar('Admin')
  → Padding(16)
      → GridView.count(crossAxisCount:2, mainAxisSpacing:12, crossAxisSpacing:12)
           → _AdminNavCard × 4
```

**`_AdminNavCard` widget:**
```dart
// Card (from theme) → InkWell → Padding(20)
//   → Column(crossAxis.start)
//       → Container(48dp, borderRadius:12, color: color.withOpacity(0.1))
//            → Icon(icon, color: color, size:24)
//       → SizedBox(16)
//       → Text(title, titleMedium, bold)
//       → SizedBox(4)
//       → Text(subtitle, bodySmall, secondaryText)
//       → Spacer
//       → Align(bottomRight) → Icon(chevron_right, secondaryText, 16)
```

**Accent colors per card:**
- Accessories: `#F15A23` (primary)
- Packages: `#03A9F4` (accent)
- Places: `#4CAF50` (green)
- Transformers: `#9C27B0` (purple)

---

## Phase 4 — Admin List Pages

Apply identical modernization to all 4 list pages.

### Pattern for every list page:

**Scaffold structure:**
```
Scaffold
  → AppBar(title, 0 elevation)
  → Column
      → Padding(h:16, v:12) → SearchField(controller, hint, onChanged)
      → Expanded → BlocBuilder → [Loading | Empty | List]
  → FAB: ExtendedFloatingActionButton(icon+label) [where applicable]
```

**List rendering:**
```dart
// Replace ListView.separated with:
ListView.builder(
  padding: EdgeInsets.fromLTRB(16, 8, 16, 96), // 96 bottom for FAB clearance
  itemCount: filtered.length,
  itemBuilder: (_, i) => Padding(
    padding: EdgeInsets.only(bottom: 8),
    child: ListItemCard(
      title: ...,
      subtitle: ...,
      leading: LeadingIconBadge(icon:..., color:...),
      trailing: StatusChip(...) or Icon(chevron_right),
      onTap: () => context.go(...),
    ),
  ),
)
```

**Loading state:** Replace `CircularProgressIndicator()` center with:
```dart
ListView.builder(
  padding: EdgeInsets.fromLTRB(16, 8, 16, 0),
  itemCount: 5,
  itemBuilder: (_, __) => Padding(
    padding: EdgeInsets.only(bottom: 8),
    child: SkeletonListItem(),
  ),
)
```

**Empty state:** Replace plain `Text` center with `EmptyState(...)`.

**FAB:** Replace bare `FloatingActionButton` with `FloatingActionButton.extended`:
```dart
FloatingActionButton.extended(
  onPressed: () => context.go('${AppRoutes.xList}/new'),
  icon: const Icon(Icons.add),
  label: const Text('New ...'),
)
```

### Page-specific changes:

#### `accessory_list_page.dart`
- SearchField hint: `'Search accessories...'`
- Filter: `state.accessories.where((a) => a.name?.toLowerCase().contains(q) ?? false)`
- Leading badge icon: `Icons.inventory_2_outlined`, color: `AppColors.primary`
- Trailing: `Icon(Icons.chevron_right)` (no FAB per spec)
- Empty state: icon=`Icons.inventory_2_outlined`, title=`'No accessories'`, subtitle=`'Accessories are synced from the desktop app.'`

#### `package_list_page.dart`
- SearchField hint: `'Search packages...'`
- Leading badge icon: `Icons.category_outlined`, color: `AppColors.accent`
- Empty state: icon=`Icons.category_outlined`, title=`'No packages'`, subtitle=`'Tap + to create your first package.'`, actionLabel=`'Create Package'`, onAction=navigate to new
- FAB label: `'New Package'`

#### `place_list_page.dart`
- SearchField hint: `'Search poles...'`
- Filter applies to both `pole` and `area` fields
- Keep area grouping: use `SectionHeader(area, isAreaHeader: true)` as header rows
- Each place row: `ListItemCard` with `StatusChip(variant: place.isCollected==1 ? ChipVariant.collected : ChipVariant.notCollected)`
- Empty state: icon=`Icons.location_on_outlined`, title=`'No poles found'`, subtitle=`'Poles are synced from the desktop app.'`
- FAB label: `'New Pole'`

#### `transformer_list_page.dart`
- SearchField hint: `'Search transformers...'`
- Leading badge icon: `Icons.bolt_outlined`, color: `Color(0xFF9C27B0)`
- Show note as subtitle in `ListItemCard`
- FAB label: `'New Transformer'`

---

## Phase 5 — Admin Detail Forms

Apply identical modernization to all 4 detail pages.

### Pattern for every detail page:

**AppBar:** Keep, but add leading back arrow explicitly (consistent with white AppBar):
```dart
AppBar(
  title: Text(isNew ? 'New X' : 'Edit X'),
  // Save button icon → keep as checkmark IconButton
)
```

**Body structure:**
```
SingleChildScrollView(padding: EdgeInsets.all(16))
  → Column
      → ErrorView(message: state.errorMessage)  ← always present, only visible on error
      → SectionHeader('Basic Information')
      → SizedBox(12)
      → [fields...]
      → SizedBox(24)
      → [additional sections if needed]
```

**Text fields:** Use theme's `InputDecorationTheme` (filled, rounded). No changes to `onChanged` callbacks.

**Field spacing:** `SizedBox(height: 16)` between fields within a section, `SizedBox(height: 24)` between sections.

### Page-specific changes:

#### `accessory_detail_page.dart`
- Add `SectionHeader('Accessory Information')` above fields
- Name field: add `helperText: 'Required'`
- Note field: keep `maxLines: 3`
- Replace error `Container` with `ErrorView(message: state.errorMessage)`

#### `package_detail_page.dart`
- Add `SectionHeader('Package Name')` above name field
- Add `SectionHeader('Accessories', trailing: TextButton.icon('Add', ...))` above accessories section
- Each accessory row in the list: use `ListItemCard` variant:
  ```
  ListItemCard(
    title: accName,
    leading: LeadingIconBadge(icon: Icons.inventory_2_outlined),
    trailing: Row[ qty TextField + delete IconButton ]
  )
  ```
- Replace `ListView.builder` (accessories) with `Column` (shrink-wrapped inside SingleChildScrollView)
- Replace error Container with `ErrorView`

#### `place_detail_page.dart`
- Add `SectionHeader('Pole Information')` above fields
- Pole name field with `helperText: 'Will be stored in uppercase'`
- Area field/dropdown: same logic, just uses theme styling
- Replace error Container with `ErrorView`

#### `transformer_detail_page.dart`
- Add `SectionHeader('Transformer Information')` above fields
- Same pattern as Accessory detail
- Replace error Container with `ErrorView`

---

## Phase 6 — Collect Page (`lib/features/collect/ui/collect_page.dart`)

The collect page is the most used screen. It is 612 lines. **Focus: restructure into distinct visual cards without touching any BLoC events.**

### Target layout (sectioned cards):

```
Scaffold
  appBar: AppBar('Collect', actions: [GPS accuracy chip])
  body: Column
    ├── [GPS Status Banner] — animated appear/dismiss
    └── Expanded → SingleChildScrollView
          → Column(padding: EdgeInsets.all(16), children:
              ├── _PoleSearchCard       (pole autocomplete + nav arrows)
              ├── SizedBox(12)
              ├── _PoleDetailsCard      (style, ownership toggle)
              ├── SizedBox(12)
              ├── _GPSCoordinatesCard   (lat/lng/accuracy display)
              ├── SizedBox(12)
              ├── _AccessoriesCard      (list + add button)
              └── SizedBox(88)          (FAB clearance)
  floatingActionButton: ExtendedFAB('Save')
```

### Card designs:

**`_PoleSearchCard`:**
```
AppCard(title: 'Pole Selection')
  → Column:
      → SearchField-style TextField (pole autocomplete)
      → if (showDropdown) _AutocompleteDropdown(...)  ← keep existing logic
      → SizedBox(12)
      → Row(prev arrow | counter text | next arrow)
```

**`_PoleDetailsCard`:**
```
AppCard(title: 'Pole Details')
  → Column:
      → DropdownButtonFormField for style (uses theme inputDecoration)
      → SizedBox(12)
      → Row(Text('EPCL Owned') | Switch)  ← replaces Checkbox pattern
```

**`_GPSCoordinatesCard`:**
```
AppCard(
  title: 'GPS Coordinates',
  trailing: _AccuracyChip(accuracy)  ← StatusChip variant
)
  → Row(3 even columns):
      → _CoordItem('Latitude', value)
      → _VertDivider
      → _CoordItem('Longitude', value)  
      → _VertDivider
      → _CoordItem('Accuracy', '${acc}m')
```

**`_AccessoriesCard`:**
```
AppCard(
  title: 'Accessories',
  trailing: TextButton.icon(Icons.add, 'Add')  ← opens bottom sheet
)
  → if empty: small centered empty hint
  → else: Column of accessory rows
      → each row: ListItemCard(title, subtitle:qty, trailing:delete)
```

**Save FAB:**
```dart
FloatingActionButton.extended(
  onPressed: isSaving ? null : () => bloc.add(SavePlace()),
  icon: isSaving ? SizedBox(16,16, CircularProgressIndicator(strokeWidth:2)) : Icon(Icons.save),
  label: Text('Save'),
  backgroundColor: canSave ? AppColors.primary : Colors.grey,
)
```

**GPS Banner** (top, animated):
```dart
AnimatedContainer(
  duration: Duration(milliseconds: 300),
  height: hasGps ? 0 : 44,
  child: Container(
    color: Colors.orange[50],
    child: Row(Icon(location_searching) + Text('Acquiring GPS...'))
  )
)
```

**Keep unchanged:** All BLoC event dispatching, `_startLocationStream`, `StreamSubscription`, autocomplete filter logic, `_showAddAccessorySheet`, prev/next navigation logic.

---

## Phase 7 — Communication Page (`lib/features/communication/ui/communication_page.dart`)

204 lines. Restructure into cards without touching BLoC.

### Target layout:

```
Scaffold
  AppBar('Communication / Sync')
  → SingleChildScrollView → Column(padding:16):
      → _ConnectionCard (baseUrl + token fields)
      → SizedBox(12)
      → _SyncStatusCard (current phase display)
      → SizedBox(12)  
      → _SyncActionsCard (Import / Export buttons)
      → SizedBox(16)
      → _SyncLogCard (if messages exist)
```

**`_ConnectionCard`:**
```
AppCard(title: 'Desktop Connection')
  → Column:
      → TextField(label:'Desktop IP Address', controller:_urlCtrl)
      → SizedBox(12)
      → TextField(label:'Access Token', controller:_tokenCtrl, obscureText)
```

**`_SyncStatusCard`:**
```
AppCard(title: 'Sync Status')
  → Row:
      → _PhaseIcon(phase)  ← Icon in colored circle
      → SizedBox(12)
      → Column(
           Text(phaseLabel, titleMedium),
           Text(phaseSubtitle, bodySmall, secondaryText)
         )
      → Spacer
      → StatusChip(variant: _variantForPhase(phase))
```

Phase → label mapping:
- `idle`: 'Ready', 'Waiting for connection'
- `connecting`: 'Connecting', 'Establishing connection...'
- `importing`: 'Importing', 'Receiving data from desktop'
- `exporting`: 'Exporting', 'Sending collected data'
- `completed`: 'Completed', 'Sync successful'
- `failed`: 'Failed', errorMessage

**`_SyncActionsCard`:**
```
AppCard(title: 'Actions')
  → Row(spaced):
      → Expanded → OutlinedButton.icon(Icons.download, 'Import Data')
      → SizedBox(12)
      → Expanded → FilledButton.icon(Icons.upload, 'Export Data')
```
Disable buttons when phase is not idle.

---

## Phase 8 — About Page (`lib/features/about/ui/about_page.dart`)

Simple redesign as a single centered card.

### Target layout:
```
Scaffold(AppBar('About'))
  → SingleChildScrollView → Padding(24):
      → AppCard → Column(center):
          → CircleAvatar(48, backgroundColor:primary)
               → Icon(electric_bolt, white, 28)
          → SizedBox(16)
          → Text(appName, headlineSmall, bold, center)
          → SizedBox(8)
          → Text('Version $_version', bodyMedium, secondaryText, center)
          → SizedBox(24)
          → Divider
          → SizedBox(16)
          → _InfoRow(Icons.business, '© E-Power CCL Co., Ltd')
          → SizedBox(12)
          → _InfoRow(Icons.phone, '(+855) 90 65 99 00')
          → _InfoRow(Icons.phone, '(+855) 10 65 99 00')
```

**`_InfoRow`:**
```dart
Row(
  Icon(icon, 16dp, secondaryText) + SizedBox(8) + Text(label, bodyMedium)
)
```

---

## Phase 9 — Maps Page (minor polish)

The maps page mostly works well. Minor changes only:

1. **AppBar:** Add subtitle with place count: `Text('${state.places.length} poles loaded', style: bodySmall)` via `flexibleSpace` or `bottom: PreferredSize`.
2. **Loading state:** Already acceptable. Can keep `CircularProgressIndicator`.
3. **No data empty state:** Replace plain Text with `EmptyState(Icons.map_outlined, 'No map data', 'Sync data from desktop to populate the map.')`.
4. **Map controls:** Consider a `FloatingActionButton`(mini) for re-center to current GPS if already available from CollectBloc. _(Optional stretch goal)_

---

## Implementation Order

| # | Phase | Files | Effort |
|---|-------|-------|--------|
| 0 | Theme overhaul | `app.dart` | Small |
| 1 | Widget toolkit | `core/widgets/app_card.dart`, `list_item_card.dart`, `empty_state.dart`, `search_field.dart`, `status_chip.dart`, `section_header.dart`, `page_loading.dart`, `error_view.dart` | Medium |
| 2 | Shell | `app_scaffold.dart` | Small |
| 3 | Admin hub | `admin_page.dart` | Small |
| 4 | Admin lists | `accessory_list_page.dart`, `package_list_page.dart`, `place_list_page.dart`, `transformer_list_page.dart` | Medium |
| 5 | Admin forms | `accessory_detail_page.dart`, `package_detail_page.dart`, `place_detail_page.dart`, `transformer_detail_page.dart` | Medium |
| 6 | Collect | `collect_page.dart` | Large |
| 7 | Communication | `communication_page.dart` | Medium |
| 8 | About | `about_page.dart` | Small |
| 9 | Maps | `maps_page.dart` | Small |

**Total: ~10 sessions, each phase independently runnable.**

---

## Constraints & Rules

1. **BLoC events are sealed** — every `bloc.add(XEvent())` call must be preserved exactly as-is.
2. **No new pub packages** — all animations use Flutter's built-in `AnimatedContainer`, `AnimatedOpacity`, `AnimatedSwitcher`.
3. **No generated l10n** — keep `_L10nStrings` in `app_scaffold.dart`. Hardcoded English is acceptable for now in new component text.
4. **`flutter analyze lib/` must report 0 issues** after each phase.
5. **Backward compat** — `app_scaffold.dart` Scaffold body stays as `child` (the ShellRoute child), only drawer changes.
6. **SliverApp/CustomScrollView** — do NOT convert AppBar to SliverAppBar (adds complexity, not needed at this scale).
7. **`DropdownButtonFormField` uses `initialValue:`** not `value:` (Flutter 3.38.7 deprecation).
