diff --git a/docs/superpowers/specs/2026-04-07-fuelfinder-subcomponent-split-design.md b/docs/superpowers/specs/2026-04-07-fuelfinder-subcomponent-split-design.md
new file mode 100644
index 0000000..d52ae61
--- /dev/null
+++ b/docs/superpowers/specs/2026-04-07-fuelfinder-subcomponent-split-design.md
@@ -0,0 +1,211 @@
+# FuelFinder Sub-Component Split
+
+**Date:** 2026-04-07
+**Status:** Approved
+**Scope:** Split the monolithic `FuelFinder` Livewire component into focused sub-components communicating via browser events.
+
+---
+
+## Motivation
+
+`FuelFinder` currently owns search state, API calls, map data, station results, and prediction — all in one component. Every search re-renders the entire page including the map container. Splitting into focused components allows the map, station list, and recommendation to update independently without re-mounting Leaflet or triggering unnecessary re-renders.
+
+---
+
+## Architecture
+
+```
+FuelFinder (shell) — layout only, no state
+├── — search form, filters, geolocation, API calls
+├── — relays stations-found to Alpine/Leaflet
+├── — renders station cards
+├── — renders prediction card
+└── — Blade component, unchanged
+```
+
+---
+
+## Components
+
+### FuelFinder (shell)
+
+**Class:** `App\Livewire\Public\FuelFinder`
+**View:** `resources/views/livewire/public/fuel-finder.blade.php`
+
+Retains `#[Layout('layouts.shell')]`. Holds no state and no methods. The view contains only the page chrome (header, main scroll area, bottom nav) and the four `` tags.
+
+---
+
+### fuel.search
+
+**Class:** `App\Livewire\Public\Fuel\Search`
+**View:** `resources/views/livewire/public/fuel/search.blade.php`
+
+Owns all search state and logic migrated from `FuelFinder`:
+
+**Properties:**
+- `search: string` — postcode/town input
+- `fuelType: string` — default `'petrol'`
+- `radius: int` — default `5`
+- `sort: string` — default `'reliable'`
+- `apiError: ?string`
+- `hasSearched: bool`
+
+**Methods:**
+- `findStations()` — validates, calls `/api/stations`, calls `/api/prediction`, dispatches `stations-found`
+- `updatedFuelType()`, `updatedRadius()`, `updatedSort()` — re-run `findStations()` if `hasSearched`
+
+**Dispatches:**
+```php
+$this->dispatch('stations-found',
+ results: $results,
+ meta: $meta,
+ prediction: $prediction,
+ radius: $this->radius, // needed by Map for Leaflet zoom level
+);
+```
+
+The view contains the search input, geolocation Alpine component, filter selects, and error display — extracted from the current `fuel-finder.blade.php`.
+
+---
+
+### fuel.map
+
+**Class:** `App\Livewire\Public\Fuel\Map`
+**View:** `resources/views/livewire/public/fuel/map.blade.php`
+
+Thin relay. Listens for `stations-found` on the PHP side and re-dispatches as a browser event for Alpine/Leaflet to consume. No re-render required.
+
+```php
+#[On('stations-found')]
+public function handle(array $results, array $meta, int $radius = 5, ?array $prediction = null): void
+{
+ $this->dispatch('map-update', results: $results, meta: $meta, radius: $radius);
+}
+```
+
+The view contains the `` component wrapped in `wire:ignore`. The Alpine `stationMap` function in `station-map.js` is updated to listen for the `map-update` browser event instead of `$watch`-ing an entangled Livewire property.
+
+**station-map.js change:**
+```js
+init() {
+ // ...existing Leaflet init...
+ window.addEventListener('map-update', (e) => {
+ this.results = e.detail.results;
+ this.meta = e.detail.meta;
+ this.radius = e.detail.radius;
+ this._plotMarkers();
+ });
+}
+```
+
+`station-map.blade.php` props change from `@entangle(...)` to static defaults — `Map` holds no state:
+```blade
+
+```
+
+---
+
+### fuel.station-list
+
+**Class:** `App\Livewire\Public\Fuel\StationList`
+**View:** `resources/views/livewire/public/fuel/station-list.blade.php`
+
+Listens for `stations-found`, stores results, re-renders station cards.
+
+**Properties:**
+- `results: array`
+- `meta: array`
+- `hasSearched: bool`
+
+**Methods:**
+```php
+#[On('stations-found')]
+public function handle(array $results, array $meta): void
+{
+ $this->results = $results;
+ $this->meta = $meta;
+ $this->hasSearched = true;
+}
+```
+
+The view contains the "Stations Nearby" heading, result count, `@forelse` station card loop, and the empty-state message — extracted from `fuel-finder.blade.php`.
+
+---
+
+### fuel.recommendation
+
+**Class:** `App\Livewire\Public\Fuel\Recommendation`
+**View:** `resources/views/livewire/public/fuel/recommendation.blade.php`
+
+Listens for `stations-found`, stores prediction, re-renders.
+
+**Properties:**
+- `prediction: ?array`
+
+**Methods:**
+```php
+#[On('stations-found')]
+public function handle(array $results, array $meta, ?array $prediction = null): void
+{
+ $this->prediction = $prediction;
+}
+```
+
+The view renders `` when prediction is set. Currently commented out in the parent view — this component makes it easy to uncomment when the scoring engine is wired.
+
+---
+
+## Event Contract
+
+**Event name:** `stations-found`
+
+**Payload:**
+```php
+[
+ 'results' => array, // station records from /api/stations
+ 'meta' => array, // lat, lng, count from API response
+ 'prediction' => ?array, // from /api/prediction, nullable
+ 'radius' => int, // user-selected radius in miles, for Leaflet zoom
+]
+```
+
+All listener components must accept `?array $prediction = null` and `int $radius = 5` as defaults to avoid errors when optional fields are absent.
+
+---
+
+## File Changes
+
+**New files:**
+- `app/Livewire/Public/Fuel/Search.php`
+- `app/Livewire/Public/Fuel/Map.php`
+- `app/Livewire/Public/Fuel/StationList.php`
+- `app/Livewire/Public/Fuel/Recommendation.php`
+- `resources/views/livewire/public/fuel/search.blade.php`
+- `resources/views/livewire/public/fuel/map.blade.php`
+- `resources/views/livewire/public/fuel/station-list.blade.php`
+- `resources/views/livewire/public/fuel/recommendation.blade.php`
+
+**Modified files:**
+- `app/Livewire/Public/FuelFinder.php` — strip all state/methods, keep layout attribute
+- `resources/views/livewire/public/fuel-finder.blade.php` — replace inline sections with `` tags
+- `resources/js/maps/station-map.js` — replace `$watch` with `map-update` event listener
+- `resources/views/components/fuel/station-map.blade.php` — remove `@entangle` props
+
+**Moved tests:**
+- `FuelFinderTest` search/API logic tests → `SearchTest`
+
+**New tests:**
+- `SearchTest` — validates, dispatches `stations-found`, handles API errors
+- `StationListTest` — responds to `stations-found`, renders cards, handles empty state
+- `RecommendationTest` — responds to `stations-found`, renders when prediction set, hidden when null
+
+---
+
+## What Does Not Change
+
+- `x-fuel.forecast` — remains a Blade component
+- `station-map.js` map logic — only the data-binding mechanism changes
+- All `fuel/` Blade components (`station-card`, `recommendation`, `forecast`, etc.)
+- The visual layout in `fuel-finder.blade.php`
+- Route definition