diff --git a/docs/superpowers/specs/2026-03-26-wifi-scanner-redesign-design.md b/docs/superpowers/specs/2026-03-26-wifi-scanner-redesign-design.md
index 0fc00ec..ca1d9f2 100644
--- a/docs/superpowers/specs/2026-03-26-wifi-scanner-redesign-design.md
+++ b/docs/superpowers/specs/2026-03-26-wifi-scanner-redesign-design.md
@@ -17,112 +17,344 @@ Redesign the WiFi scanner's main content area for better space utilisation, visu
| Channel analysis | Heatmap (channels × time) | Shows congestion history, more useful than a snapshot bar chart |
| Row click behaviour | Right panel takeover (detail replaces heatmap) | Keeps spatial layout stable; no scroll disruption |
+## CSS Tokens
+
+Tokens confirmed in `index.css :root`: `--bg-primary`, `--bg-secondary`, `--bg-tertiary`, `--bg-card`, `--border-color`, `--border-light`, `--text-primary`, `--text-secondary`, `--text-dim`, `--accent-cyan`, `--accent-cyan-dim`, `--accent-green`, `--accent-green-dim`, `--accent-red`, `--accent-red-dim`, `--accent-orange`, `--accent-amber`, `--accent-amber-dim`.
+
+**Not defined in `index.css :root`** — do not use:
+- `--accent-yellow` (used in some existing WiFi rules but undefined — use `--accent-orange` instead)
+- `--accent-cyan-rgb` (undefined — use the literal `rgba(74, 163, 255, 0.07)` for the selected row tint)
+
## Component Designs
### 1. Status Bar
Enhanced version of the existing `wifi-status-bar`:
-- Existing fields: Networks · Clients · Hidden
-- **New:** Open count in `var(--accent-red)` — instant threat count
-- **New:** Scan indicator (pulsing dot + "SCANNING · ch N" text) floated right, updates to "IDLE" when stopped
-- Minor spacing/typography polish
+- Existing fields preserved: Networks · Clients · Hidden (IDs: `wifiNetworkCount`, `wifiClientCount`, `wifiHiddenCount`)
+- **New — Open count:** Add `
Open0
` after the Hidden item. Populated by `renderNetworks()` counting networks where `security === 'Open'`.
+- **Scan indicator — HTML:** Replace the existing `
` with:
+ ```html
+
+
+ IDLE
+
+ ```
+ CSS: `.wifi-scan-dot` is a 7×7px circle with `animation: wifi-pulse 1.2s ease-in-out infinite` (opacity 1→0.4→1, scale 1→0.7→1). The dot is hidden (`display:none`) when idle; shown when scanning. `.wifi-scan-indicator` is floated/pushed to `margin-left: auto`.
+- `updateScanningState(scanning, scanMode)` — revised body:
+ ```js
+ const dot = elements.scanIndicator?.querySelector('.wifi-scan-dot');
+ const text = elements.scanIndicator?.querySelector('.wifi-scan-text');
+ if (dot) dot.style.display = scanning ? 'inline-block' : 'none';
+ if (text) text.textContent = scanning
+ ? `SCANNING (${scanMode === 'quick' ? 'Quick' : 'Deep'})`
+ : 'IDLE';
+ ```
+ `elements.scanIndicator` replaces `elements.scanStatus` in the `elements` map, pointing to `#wifiScanIndicator`.
### 2. Networks Table
-**Row structure (two visual lines per row):**
+**Structural change:** Remove `
` (including `` and ``). Replace with `
`. Update `elements.networkTable → elements.networkList` and `elements.networkTableBody → elements.networkList` in the `elements` map in `wifi.js`.
+**Sort controls:** The `th[data-sort]` click listener on the old table is removed. Add three small text-style sort buttons to the `.wifi-networks-header`:
+```html
+
+ Sort:
+
+
+
+
```
-[left-border] SSID name / [Hidden] italic [SECURITY BADGE] [HIDDEN TAG]
- [━━━━━━━━━▓░░░░░░░] signal bar ch 6 · 4 ↔ · −48
+Clicking a button sets the existing `sortBy` state variable and calls `renderNetworks()`. Active button gets `.active` class (cyan text).
+
+**Filter buttons:** `#wifiNetworkFilters` / `.wifi-filter-btn` bar (All / 2.4G / 5G / Open / Hidden) is kept as-is in HTML. The JS `applyFilters()` function is adapted to operate on `
` elements using their `data-band` and `data-security` attributes (same logic, different element type).
+
+**Row HTML structure:**
+```html
+
+
+ SSID or [Hidden] BSSID
+
+ LABEL
+ HIDDEN
+
+
+
+
+
+
+
+
+
+ ch N
+ N ↔
+ −N
+
+
+
```
-**Left border colour coding:**
-- `var(--accent-red)` — Open security
-- `var(--accent-green)` — WPA2 / WPA3
-- `var(--border-color)` (dim) — Hidden network with unknown security
+**Left border colour (CSS class on `.network-row`):**
+- `.threat-open` → `border-left: 3px solid var(--accent-red)`
+- `.threat-safe` → `border-left: 3px solid var(--accent-green)` (WPA2, WPA3)
+- `.threat-hidden` → `border-left: 3px solid var(--border-color)`
-**Signal bar:** gradient `green → amber → red` based on dBm value:
-- Strong (> −55): green fill
-- Medium (−55 to −70): green→amber gradient
-- Weak (< −70): amber→red gradient
+**Signal bar fill width:** `pct = Math.max(0, Math.min(100, (rssi + 100) / 80 * 100))` where −100 dBm → 0%, −20 dBm → 100%.
-**Security badge pills** (replacing current flat badges):
-- Open: red pill with red border
-- WPA2: green pill with green border
-- WPA3: cyan pill with cyan border
-- WPA/WPA2: amber pill
+**Signal fill classes:**
+- `.strong` (rssi > −55): `background: linear-gradient(90deg, var(--accent-green), #88d49b)`
+- `.medium` (−55 ≥ rssi > −70): `background: linear-gradient(90deg, var(--accent-green), var(--accent-orange))`
+- `.weak` (rssi ≤ −70): `background: linear-gradient(90deg, var(--accent-orange), var(--accent-red))`
-**Hidden tag:** small dim pill "HIDDEN" next to security badge.
+**Security string → badge class mapping** (mirrors existing `securityClass` logic in `wifi.js`):
+```js
+function securityBadgeClass(security) {
+ const s = (security || '').toLowerCase();
+ if (s === 'open' || s === '') return 'open';
+ if (s.includes('wpa3')) return 'wpa3';
+ if (s.includes('wpa')) return 'wpa2'; // covers WPA2, WPA/WPA2, WPA PSK, etc.
+ if (s.includes('wep')) return 'wep';
+ return 'wpa2'; // fallback for unknown encrypted
+}
+```
+
+**Security badge colours:**
+- `.badge.open` — `var(--accent-red)` text + border, `var(--accent-red-dim)` background
+- `.badge.wpa2` — `var(--accent-green)` text + border, `var(--accent-green-dim)` background
+- `.badge.wpa3` — `var(--accent-cyan)` text + border, `var(--accent-cyan-dim)` background
+- `.badge.wep` — `var(--accent-orange)` text + border, `var(--accent-amber-dim)` background
+- `.badge.hidden-tag` — `var(--text-dim)` text, `var(--border-color)` border, transparent background
+
+**Radar dot colours (same semantic mapping):**
+- Open: `var(--accent-red)` / `#e25d5d`
+- WPA2 / WPA3: `var(--accent-green)` / `#38c180`
+- WEP: `var(--accent-orange)` / `#d6a85e`
+- Unknown / Hidden: `#484f58`
+
+**Selected state persistence across re-renders:** `WiFiMode` stores the selected BSSID in module-level `let selectedBssid = null`. After `renderNetworks()` rebuilds the list, if `selectedBssid` is set, find the matching row by `data-bssid` and add `.selected` to it; also refresh `#wifiDetailView` with updated data for that network.
+
+**Empty state:** When network list is empty, insert `
No networks detected. Start a scan to begin.
`.
**Row states:**
-- Default: transparent background
-- Hover: `var(--bg-tertiary)`
-- Selected: `rgba(0,180,216,0.07)` + cyan left border override
+- Hover: `background: var(--bg-tertiary)`
+- Selected: `background: rgba(74, 163, 255, 0.07)` + `border-left-color: var(--accent-cyan)` (via `.selected` class, overrides threat colour)
### 3. Proximity Radar
-**Animation:** Pure CSS — a `` element wrapping the sweep line + trailing arc rotates with `animation: radar-sweep 3s linear infinite`. No canvas required.
+**Existing `#wifiProximityRadar` div** contents replaced with an inline SVG. `wifi.js` adds `renderRadar(networks)`.
-**Sweep elements (inside rotating ``):**
-- Trailing arc: a pie-slice `` filling ~60° behind the sweep line, filled `#00b4d8` at ~8% opacity
-- Sweep line: `` from centre to edge, cyan, with a green highlight layer at low opacity
+**SVG:** `width="100%" viewBox="0 0 210 210"`, centre `(105, 105)`. A `` is applied to the rotating group to prevent arc overflow.
-**Network dots:**
-- Positioned by signal strength (stronger = closer to centre)
-- Sized: near=6px, mid=4.5px, far=3px radius
-- Coloured by security: red=Open, green=WPA2/3, amber=WEP/weak, grey=hidden
-- Each dot has a soft radial glow halo (second circle at ~1.5× radius, 10–15% opacity)
-- Dots are outside the rotating group — they stay stationary
+**Rings (static, outside rotating group):**
+```html
+
+
+
+
+```
-**Zone summary row** below radar: Near / Mid / Far counts, coloured red / amber / green respectively.
+**Sweep animation — CSS:**
+```css
+.wifi-radar-sweep {
+ transform-origin: 105px 105px;
+ animation: wifi-radar-rotate 3s linear infinite;
+}
+@keyframes wifi-radar-rotate {
+ from { transform: rotate(0deg); }
+ to { transform: rotate(360deg); }
+}
+```
-### 4. Right Panel — Channel Heatmap (default)
+**Sweep group** ``:
+- Trailing arc 60°: ``
+ _(endpoint derived: x = 105 + 100·sin(60°) ≈ 191.6, y = 105 − 100·cos(60°) + 100 = 155)_
+- Trailing arc 90°: ``
+ _(endpoint: x=205, y=105 — 90° clockwise from top)_
+- Sweep line: ``
-**Layout:** Vertical stack:
-1. Band label ("2.4 GHz · Last N scans")
-2. Channel number labels (1–11)
-3. Heatmap grid (JS-generated `
` cells)
-4. Legend gradient bar (Low → High)
-5. Security ring chart (always visible below heatmap)
+**Network dots** (rendered as SVG `` elements outside the rotating group, replaced by `renderRadar(networks)`):
+- Angle per dot: determined by `bssidToAngle(bssid)` — a simple hash of the BSSID string modulo 2π. This produces stable positions across re-renders for the same network.
+- Radius from centre: `dotR = 5 + (1 - Math.max(0, Math.min(1, (rssi + 100) / 80))) * 90` — stronger signal → smaller radius
+- Zone thresholds: dotR < 35 = Near, 35–70 = Mid, >70 = Far
+- Visual radius: Near → 6, Mid → 4.5, Far → 3 (px)
+- Each dot: a main `` + a glow halo `` at 1.5× visual radius, same fill colour, 12% opacity
+- Colour: see Section 2 "Radar dot colours"
-**Heatmap grid:**
-- Rows = time steps (newest at top, oldest at bottom, ~10 rows)
-- Columns = channels 1–11 for 2.4 GHz, with a tab toggle for 5 GHz
-- Cell background colour maps congestion → colour: `#0d1117` (none) → dark blue → `#0ea5e9` (medium) → `#f97316` (high) → `#ef4444` (congested)
-- Grid generated by `WiFiMode` using existing channel utilisation data
+**Zone counts:** `renderRadar()` updates `#wifiZoneImmediate`, `#wifiZoneNear`, `#wifiZoneFar` (IDs unchanged).
-**Security ring chart (below heatmap):**
-- SVG ring using `stroke-dasharray` arcs
-- Segments: WPA2 (green), Open (red), WPA3 (cyan), WEP (amber), proportional to counts
-- Counts shown in a legend list to the right of the ring
+### 4. Right Panel — Channel Heatmap + Security Ring
-### 5. Right Panel — Network Detail (on row click)
+**Removed elements:** The existing `.wifi-channel-section` (containing `.wifi-channel-tabs` and `#wifiChannelChart`), `.wifi-security-section`, and the IDs `openCount`, `wpa2Count`, `wpa3Count`, `wepCount` are all removed from the HTML. Any JS references to these IDs are also removed.
-Replaces heatmap content in the same panel. Header changes from "Channel Heatmap" to "Network Detail" and a "← Back" button appears.
+**New `.wifi-analysis-panel` inner HTML:**
+```html
+
+ Channel Heatmap
+
+
-**Detail panel contents:**
-- SSID (large, bold) + BSSID (monospace, dim)
-- Signal strength bar (same gradient as table rows, wider)
-- 2×3 stat grid: Channel · Band · Security · Cipher · Clients · Vendor
-- Action buttons: "⊕ Locate AP" (handoff to `WiFiLocate`) · "Close" (returns to heatmap)
+
+
+
2.4 GHz · Last 0 scans
+
+
+
+
+
+ Low
+
+ High
+
+
+
+
+
+
+
+
+
-Closing via "← Back" or "Close" restores the heatmap and clears row selection.
+
+
+
+```
+
+**5 GHz heatmap:** Tab toggle removed. Heatmap always shows 2.4 GHz. **5 GHz networks are excluded from `channelHistory` snapshots** — only networks with `band === '2.4'` are counted when building each snapshot.
+
+**Heatmap data source:** Module-level `let channelHistory = []` (max 10 entries). Each `renderNetworks()` prepends `{ timestamp: Date.now(), channels: { 1:N, …, 11:N } }` built from 2.4 GHz networks only. Entries beyond 10 are dropped. `wifiHeatmapCount` span is updated with `channelHistory.length`.
+
+**Heatmap grid DOM structure:** `#wifiHeatmapGrid` is a CSS grid container (`display: grid; grid-template-columns: 26px repeat(11, 1fr); gap: 2px`). `renderHeatmap()` clears and rebuilds it on every call. For each of the up to 10 history entries (newest first), it creates one row of 12 elements: a time-label `
` (text "now" for index 0, empty for others) followed by 11 cell `
` elements whose `background` is set by `congestionColor()`. Total DOM nodes: up to 10 × 12 = 120 divs, fully rebuilt on each render call.
+
+**Cell colour — `congestionColor(value, maxValue)`:**
+```js
+function congestionColor(value, maxValue) {
+ if (value === 0 || maxValue === 0) return '#0d1117';
+ const ratio = value / maxValue;
+ if (ratio < 0.05) return '#0d1117';
+ if (ratio < 0.25) return `rgba(13,74,110,${(ratio * 4).toFixed(2)})`;
+ if (ratio < 0.5) return `rgba(14,165,233,${ratio.toFixed(2)})`;
+ if (ratio < 0.75) return `rgba(249,115,22,${ratio.toFixed(2)})`;
+ return `rgba(239,68,68,${ratio.toFixed(2)})`;
+}
+```
+`maxValue` = the maximum cell value across the entire `channelHistory` array (for consistent colour scale).
+
+**Empty/loading state:** When `channelHistory.length === 0`, `#wifiHeatmapGrid` shows a single placeholder div: `
Scan to populate channel history
`.
+
+**Security ring — `renderSecurityRing(networks)`:**
+
+Circumference `C = 2 * Math.PI * 15 ≈ 94.25`. Each segment is a `` element with `stroke-dasharray="${arcLen} ${C - arcLen}"` and a `stroke-dashoffset` that positions it after the previous segments. The ring starts at the top by applying `transform="rotate(-90 24 24)"` to each arc.
+
+Standard SVG donut-segment technique — `dashoffset` for segment N = `-(sum of all preceding arcLengths)`:
+
+```js
+const C = 2 * Math.PI * 15; // ≈ 94.25
+const segments = [
+ { label: 'WPA2', color: 'var(--accent-green)', count: wpa2 },
+ { label: 'Open', color: 'var(--accent-red)', count: open },
+ { label: 'WPA3', color: 'var(--accent-cyan)', count: wpa3 },
+ { label: 'WEP', color: 'var(--accent-orange)',count: wep },
+];
+const total = segments.reduce((s, seg) => s + seg.count, 0) || 1;
+let offset = 0;
+segments.forEach(seg => {
+ const arcLen = (seg.count / total) * C;
+ //
+ offset += arcLen;
+});
+// Worked example: total=10, WPA2=7, Open=3:
+// WPA2: arcLen=66.0, dasharray="66 28.2", dashoffset="0"
+// Open: arcLen=28.2, dasharray="28.2 66", dashoffset="-66"
+```
+
+- Centre hole: `` injected last (on top)
+- Legend rows injected into `#wifiSecurityRingLegend`: coloured square + name + count
+
+### 5. Right Panel — Network Detail
+
+**`#wifiDetailDrawer` deletion:** Delete the entire `
` block (~lines 940–1000 of `index.html`). Also remove all associated CSS from `index.css`: `.wifi-detail-drawer`, `.wifi-detail-drawer.open`, `.wifi-detail-header`, `.wifi-detail-title`, `.wifi-detail-essid`, `.wifi-detail-bssid`, `.wifi-detail-close`, `.wifi-detail-content`, `.wifi-detail-grid`, `.wifi-detail-stat`. In `wifi.js`, remove `detailDrawer` from the `elements` map and remove its usage in the existing `closeDetail()` (`detailDrawer.classList.remove('open')`) since `closeDetail()` is being replaced.
+
+**`#wifiDetailView` inner HTML:**
+```html
+
+
+
—
+
—
+
+
+
+
+ Signal
+ —
+
+
+
+
+
+
+
+
Channel—
+
Band—
+
Security—
+
Cipher—
+
Clients—
+
First Seen—
+
Vendor—
+
+
+
+
+
Connected Clients
+
+
+
+
+
+
+
+
+
+```
+
+**Signal bar fill width:** Same formula as table rows — `pct = Math.max(0, Math.min(100, (rssi + 100) / 80 * 100))`.
+
+**Show/hide:**
+- `WiFiMode.selectNetwork(bssid)`: hides `#wifiHeatmapView`, shows `#wifiDetailView`, sets `#wifiRightPanelTitle` text to "Network Detail", shows `#wifiDetailBackBtn`, stores `selectedBssid = bssid`
+- `WiFiMode.closeDetail()`: reverses — shows heatmap, hides detail, restores title to "Channel Heatmap", hides back button, sets `selectedBssid = null`
+
+**"← Back" vs "Close":** Both call `WiFiMode.closeDetail()`. "← Back" is in the panel header (always reachable at top); "Close" is at the bottom of the detail body for users who scrolled down.
+
+**`wifiClientCountBadge`** is preserved inside `#wifiDetailClientList` — no changes to the client list sub-panel.
## File Changes
| File | Change |
|---|---|
-| `static/css/index.css` | Update/add WiFi section CSS (rows, radar, heatmap, detail panel, status bar) |
-| `templates/index.html` | Update WiFi layout HTML structure |
-| `static/js/modes/wifi.js` | Update `renderNetworks()`, add heatmap renderer, radar dot positioning, detail panel logic |
-
-No new files needed. No backend changes.
+| `static/css/index.css` | Update WiFi section CSS (~line 3515): add `.wifi-scan-indicator`, `.wifi-scan-dot` + keyframes; replace table CSS with `.wifi-network-list`, `.network-row`, `.row-top`, `.row-bottom`, `.signal-bar-*`, `.badge.*`, `.wifi-sort-*`; add `.wifi-radar-sweep` + `@keyframes wifi-radar-rotate`; replace channel/security section CSS with `.wifi-heatmap-*`, `.wifi-security-ring-*`, `.wifi-analysis-panel-header`, `.wifi-detail-back-btn`; add `.wifi-detail-inner`, `.wifi-detail-*` |
+| `templates/index.html` | WiFi section (~line 820): replace `
` with `
`, add sort controls to header, add `#wifiOpenCount` to status bar, replace `#wifiScanStatus` with `#wifiScanIndicator`, replace right panel contents with `#wifiHeatmapView` / `#wifiDetailView`, add `#wifiRightPanelTitle` + `#wifiDetailBackBtn` to panel header, inline radar SVG shell (static rings + empty dot group), remove `#wifiDetailDrawer` |
+| `static/js/modes/wifi.js` | Update `elements` map (remove `networkTable`, `networkTableBody`, `detailDrawer`, `wpa3Count`, `wpa2Count`, `wepCount`, `openCount`; add `networkList`, `openCount` → `wifiOpenCount`, `scanIndicator`, `heatmapGrid`, `heatmapCount`, `securityRingSvg`, `securityRingLegend`, `heatmapView`, `detailView`, `rightPanelTitle`, `detailBackBtn`, `detailSignalFill`); update `renderNetworks()` (div rows, filter + sort, persistence of `selectedBssid`); update `updateScanningState()`; add `renderRadar(networks)`; add `renderHeatmap()`; add `renderSecurityRing(networks)`; add `selectNetwork(bssid)` / `closeDetail()`; remove `th[data-sort]` listener |
+| `templates/partials/modes/wifi.html` | No changes — sidebar out of scope |
## Out of Scope
-- WiFi locate mode (separate mode, untouched)
-- Sidebar panel (signal source, scan settings, etc.) — untouched
-- Attack options, handshake capture — untouched
+- WiFi locate mode (separate)
+- Sidebar panel (signal source, scan settings, attack options, handshake capture)
- Mobile/responsive layout changes
-- 5 GHz channel heatmap data (tab exists, data hookup is a follow-on)
+- 5 GHz channel heatmap data population
+- Any backend / route changes