diff --git a/docs/superpowers/plans/2026-03-22-brave-search-frontend-redesign.md b/docs/superpowers/plans/2026-03-22-brave-search-frontend-redesign.md
deleted file mode 100644
index 5b6ad9b..0000000
--- a/docs/superpowers/plans/2026-03-22-brave-search-frontend-redesign.md
+++ /dev/null
@@ -1,1222 +0,0 @@
-# Brave Search Frontend Redesign β Implementation Plan
-
-> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
-
-**Goal:** Redesign the samsa frontend to match Brave Search's layout: three-column results page, category tiles on homepage, and a hybrid preferences system with full-page `/preferences` route.
-
-**Architecture:** CSS Grid for page-level layouts (three-column results, two-column preferences). JavaScript popover for quick settings (theme + engines only). Server-rendered full preferences page with localStorage persistence. Category tiles are static links with category query params.
-
-**Tech Stack:** Go (Go templates), CSS Grid/Flexbox, Vanilla JavaScript (HTMX for search), localStorage for preferences
-
----
-
-## File Map
-
-| File | Responsibility |
-|------|----------------|
-| `internal/views/static/css/samsa.css` | Add layout grids, category tiles, sidebar styles, mobile breakpoints |
-| `internal/views/templates/index.html` | Add category tiles below search box |
-| `internal/views/templates/results.html` | Add left sidebar, restructure for three-column grid |
-| `internal/views/templates/preferences.html` | **New** β full preferences page with nav |
-| `internal/views/templates/base.html` | No structural changes needed |
-| `internal/views/static/js/settings.js` | Reduce popover to theme + engines; add preferences page JS |
-| `internal/httpapi/handlers.go` | Add `GET /preferences` and `POST /preferences` handlers |
-| `internal/views/views.go` | Add `RenderPreferences` and `tmplPreferences` template |
-
----
-
-## PHASE 1: CSS Layout Framework
-
-### Task 1: Add CSS Grid Layouts and Breakpoints
-
-**Files:**
-- Modify: `internal/views/static/css/samsa.css`
-
-- [ ] **Step 1: Add three-column results layout CSS**
-
-Append to end of `samsa.css`, before the `@media print` block:
-
-```css
-/* ============================================================
- Three-Column Results Layout
- ============================================================ */
-
-.results-layout {
- display: grid;
- grid-template-columns: 200px 1fr 240px;
- gap: 2rem;
- align-items: start;
-}
-
-.results-layout .left-sidebar {
- position: sticky;
- top: calc(var(--header-height) + 1.5rem);
- max-height: calc(100vh - var(--header-height) - 3rem);
- overflow-y: auto;
-}
-
-.results-layout .right-sidebar {
- position: sticky;
- top: calc(var(--header-height) + 1.5rem);
- max-height: calc(100vh - var(--header-height) - 3rem);
- overflow-y: auto;
-}
-
-.results-layout .results-column {
- min-width: 0;
-}
-```
-
-- [ ] **Step 2: Add mobile breakpoints**
-
-```css
-/* Tablet: hide left sidebar, two columns */
-@media (min-width: 769px) and (max-width: 1024px) {
- .results-layout {
- grid-template-columns: 1fr 220px;
- }
- .results-layout .left-sidebar {
- display: none;
- }
-}
-
-/* Mobile: single column, no sidebars */
-@media (max-width: 768px) {
- .results-layout {
- grid-template-columns: 1fr;
- }
- .results-layout .left-sidebar,
- .results-layout .right-sidebar {
- display: none;
- }
-}
-```
-
-- [ ] **Step 3: Add preferences page layout CSS**
-
-```css
-/* ============================================================
- Preferences Page Layout
- ============================================================ */
-
-.preferences-layout {
- display: grid;
- grid-template-columns: 200px 1fr;
- gap: 2rem;
- align-items: start;
- padding: 2rem 0;
-}
-
-.preferences-nav {
- position: sticky;
- top: calc(var(--header-height) + 1.5rem);
-}
-
-.preferences-nav-item {
- display: flex;
- align-items: center;
- gap: 0.6rem;
- padding: 0.6rem 0.75rem;
- border-radius: var(--radius-sm);
- color: var(--text-secondary);
- text-decoration: none;
- font-size: 0.9rem;
- transition: background 0.15s, color 0.15s;
- cursor: pointer;
-}
-
-.preferences-nav-item:hover {
- background: var(--bg-tertiary);
- color: var(--text-primary);
-}
-
-.preferences-nav-item.active {
- background: var(--accent-soft);
- color: var(--accent);
- font-weight: 500;
-}
-
-.preferences-content {
- background: var(--bg);
- border: 1px solid var(--border);
- border-radius: var(--radius-lg);
- padding: 1.5rem;
-}
-
-@media (max-width: 768px) {
- .preferences-layout {
- grid-template-columns: 1fr;
- }
- .preferences-nav {
- position: static;
- display: flex;
- overflow-x: auto;
- gap: 0.5rem;
- padding-bottom: 0.5rem;
- }
- .preferences-nav-item {
- white-space: nowrap;
- }
-}
-```
-
-- [ ] **Step 4: Add category tiles CSS**
-
-```css
-/* ============================================================
- Category Tiles
- ============================================================ */
-
-.category-tiles {
- display: grid;
- grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
- gap: 1rem;
- margin-top: 2rem;
-}
-
-.category-tile {
- display: flex;
- flex-direction: column;
- align-items: center;
- gap: 0.5rem;
- padding: 1rem 0.5rem;
- border-radius: var(--radius-md);
- text-decoration: none;
- color: var(--text-secondary);
- font-size: 0.85rem;
- transition: background 0.15s, color 0.15s, transform 0.15s, box-shadow 0.15s;
-}
-
-.category-tile:hover {
- background: var(--bg-tertiary);
- color: var(--text-primary);
- transform: translateY(-2px);
- box-shadow: var(--shadow-sm);
-}
-
-.category-tile-icon {
- font-size: 1.5rem;
- line-height: 1;
-}
-
-.category-tile.disabled {
- opacity: 0.5;
- cursor: not-allowed;
- pointer-events: none;
-}
-
-@media (max-width: 768px) {
- .category-tiles {
- grid-template-columns: repeat(auto-fill, minmax(80px, 1fr));
- gap: 0.75rem;
- }
- .category-tile {
- padding: 0.75rem 0.25rem;
- font-size: 0.75rem;
- }
- .category-tile-icon {
- font-size: 1.25rem;
- }
-}
-```
-
-- [ ] **Step 5: Add left sidebar navigation styles**
-
-```css
-/* ============================================================
- Left Sidebar (Results Page)
- ============================================================ */
-
-.left-sidebar {
- padding: 0;
-}
-
-.sidebar-nav {
- display: flex;
- flex-direction: column;
- gap: 0.25rem;
-}
-
-.sidebar-nav-title {
- font-size: 0.7rem;
- font-weight: 600;
- text-transform: uppercase;
- letter-spacing: 0.06em;
- color: var(--text-muted);
- padding: 0.5rem 0.75rem;
- margin-top: 0.5rem;
-}
-
-.sidebar-nav-item {
- display: flex;
- align-items: center;
- gap: 0.5rem;
- padding: 0.5rem 0.75rem;
- border-radius: var(--radius-sm);
- color: var(--text-secondary);
- text-decoration: none;
- font-size: 0.875rem;
- transition: background 0.15s, color 0.15s;
-}
-
-.sidebar-nav-item:hover {
- background: var(--bg-tertiary);
- color: var(--text-primary);
-}
-
-.sidebar-nav-item.active {
- background: var(--accent-soft);
- color: var(--accent);
- font-weight: 500;
-}
-
-.sidebar-nav-item-icon {
- font-size: 1rem;
- width: 20px;
- text-align: center;
-}
-
-.sidebar-filters {
- margin-top: 1rem;
- padding-top: 1rem;
- border-top: 1px solid var(--border);
-}
-
-.sidebar-filter-group {
- margin-bottom: 0.75rem;
-}
-
-.sidebar-filter-label {
- font-size: 0.75rem;
- font-weight: 500;
- color: var(--text-muted);
- padding: 0 0.75rem;
- margin-bottom: 0.25rem;
-}
-
-.sidebar-filter-option {
- display: flex;
- align-items: center;
- gap: 0.5rem;
- padding: 0.35rem 0.75rem;
- font-size: 0.8rem;
- color: var(--text-secondary);
- cursor: pointer;
- border-radius: var(--radius-sm);
- transition: background 0.15s;
-}
-
-.sidebar-filter-option:hover {
- background: var(--bg-tertiary);
-}
-
-.sidebar-filter-option input[type="radio"] {
- accent-color: var(--accent);
-}
-
-/* Mobile filter chips */
-.mobile-filter-chips {
- display: none;
- overflow-x: auto;
- gap: 0.5rem;
- padding: 0.75rem 0;
- -webkit-overflow-scrolling: touch;
-}
-
-.mobile-filter-chips::-webkit-scrollbar {
- display: none;
-}
-
-.mobile-filter-chip {
- display: inline-flex;
- align-items: center;
- padding: 0.4rem 0.75rem;
- border: 1px solid var(--border);
- border-radius: var(--radius-full);
- font-size: 0.8rem;
- color: var(--text-secondary);
- white-space: nowrap;
- text-decoration: none;
- transition: background 0.15s, border-color 0.15s;
-}
-
-.mobile-filter-chip:hover,
-.mobile-filter-chip.active {
- background: var(--accent-soft);
- border-color: var(--accent);
- color: var(--accent);
-}
-
-@media (max-width: 768px) {
- .mobile-filter-chips {
- display: flex;
- }
-}
-```
-
-- [ ] **Step 6: Verify Go compilation**
-
-Run: `go build ./...`
-Expected: No errors
-
-Note: CSS is embedded as static files and not processed by the Go compiler. CSS changes must be tested manually in a browser.
-
-- [ ] **Step 7: Commit**
-
-```bash
-git add internal/views/static/css/samsa.css
-git commit -m "feat(frontend): add CSS layout framework for three-column results and preferences page"
-```
-
----
-
-## PHASE 2: Results Page Three-Column Layout
-
-### Task 2: Restructure Results Template
-
-**Files:**
-- Modify: `internal/views/templates/results.html`
-- Modify: `internal/views/views.go`
-
-- [ ] **Step 1: Read current results.html to understand exact content**
-
-Current structure has `.results-layout` grid with `.search-compact` spanning full width, `.results-column`, and `.sidebar`. Need to add left sidebar and restructure grid.
-
-- [ ] **Step 2: Replace results.html content**
-
-Replace the entire file content:
-
-```html
-{{define "title"}}{{if .Query}}{{.Query}} β {{end}}{{end}}
-{{define "content"}}
-
-
-
-
-
-
-
-
-
-
-
-
All
- {{range .Categories}}
-
{{.}}
- {{end}}
-
-
-
- {{template "results_inner" .}}
-
-
-
-
-
-{{end}}
-```
-
-- [ ] **Step 3: Add FilterOption struct and update PageData struct**
-
-Add `FilterOption` struct at package level in `views.go` (near `PageNumber` struct):
-
-```go
-// FilterOption represents a filter radio option for the sidebar.
-type FilterOption struct {
- Label string
- Value string
-}
-```
-
-Then update `PageData` struct to include new fields:
-
-```go
-type PageData struct {
- // ... existing fields (SourceURL, Query, Pageno, etc.) ...
-
- // New fields for three-column layout
- Categories []string
- CategoryIcons map[string]string
- DisabledCategories []string
- ActiveCategory string
- TimeFilters []FilterOption
- TypeFilters []FilterOption
- ActiveTime string
- ActiveType string
-}
-```
-
-- [ ] **Step 4: Update FromResponse signature and body**
-
-Update `FromResponse` signature to accept filter params and set defaults:
-
-```go
-func FromResponse(resp contracts.SearchResponse, query string, pageno int, activeCategory, activeTime, activeType string) PageData {
- // Set defaults
- if activeCategory == "" {
- activeCategory = "all"
- }
-
- pd := PageData{
- // ... existing initialization (NumberOfResults, Results, etc.) ...
-
- // New: categories with icons
- Categories: []string{"all", "news", "images", "videos", "maps"},
- DisabledCategories: []string{"shopping", "music", "weather"},
- CategoryIcons: map[string]string{
- "all": "π",
- "news": "π°",
- "images": "πΌοΈ",
- "videos": "π¬",
- "maps": "πΊοΈ",
- "shopping": "π",
- "music": "π΅",
- "weather": "π€οΈ",
- },
- ActiveCategory: activeCategory,
-
- // Time filters
- TimeFilters: []FilterOption{
- {Label: "Any time", Value: ""},
- {Label: "Past hour", Value: "h"},
- {Label: "Past 24 hours", Value: "d"},
- {Label: "Past week", Value: "w"},
- {Label: "Past month", Value: "m"},
- {Label: "Past year", Value: "y"},
- },
- ActiveTime: activeTime,
-
- // Type filters
- TypeFilters: []FilterOption{
- {Label: "All results", Value: ""},
- {Label: "News", Value: "news"},
- {Label: "Videos", Value: "video"},
- {Label: "Images", Value: "image"},
- },
- ActiveType: activeType,
- }
- // ... rest of function ...
-}
-```
-
-Update the `Search` handler in `handlers.go` to pass filter params:
-
-```go
-pd := views.FromResponse(resp, req.Query, req.Pageno,
- r.FormValue("category"), r.FormValue("time"), r.FormValue("type"))
-```
-
-- [ ] **Step 5: Update results.html sidebar to show disabled state**
-
-Update the sidebar category loop to conditionally apply `disabled` class:
-
-```html
-{{range .Categories}}
-
-{{end}}
-
-{{range .DisabledCategories}}
-
-{{end}}
-```
-
-- [ ] **Step 6: Test compilation**
-
-Run: `go build ./...`
-Expected: No errors
-
-- [ ] **Step 7: Commit**
-
-```bash
-git add internal/views/views.go internal/views/templates/results.html
-git commit -m "feat(frontend): add three-column results layout with left sidebar navigation"
-```
-
----
-
-## PHASE 3: Homepage Category Tiles
-
-### Task 3: Add Category Tiles to Homepage
-
-**Files:**
-- Modify: `internal/views/templates/index.html`
-
-- [ ] **Step 1: Read current index.html**
-
-- [ ] **Step 2: Replace index.html with tiles**
-
-```html
-{{define "title"}}{{end}}
-{{define "content"}}
-
-
-
Search the web privately, without tracking or censorship.
-
-
-
-
-
-
-{{end}}
-```
-
-- [ ] **Step 3: Test compilation**
-
-Run: `go build ./...`
-Expected: No errors
-
-- [ ] **Step 4: Commit**
-
-```bash
-git add internal/views/templates/index.html
-git commit -m "feat(frontend): add category tiles to homepage"
-```
-
----
-
-## PHASE 4: Preferences Page
-
-### Task 4: Create Preferences Template
-
-**Files:**
-- Create: `internal/views/templates/preferences.html`
-
-- [ ] **Step 1: Create preferences.html**
-
-```html
-{{define "title"}}Preferences{{end}}
-{{define "content"}}
-
-
-
-
-
-
-
-
- Search
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Privacy
-
-
-
-
Block trackers and scripts that follow you across the web.
-
-
-
-
-
-
-
Ask websites not to track you.
-
-
-
-
-
-
-
- Tabs
-
-
-
-
Choose what happens when you open a new tab.
-
-
-
-
-
-
-
- Appearance
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Content
-
-
-
-
Hide explicit content from search results (SafeSearch).
-
-
-
-
-
-
-
Automatically play video content when visible.
-
-
-
-
-
-
-
- Languages
-
-
-
-
-
-
-
-
-
-
-
-
- Regional
-
-
-
-
-
-
-
-
-
-
-
-{{end}}
-```
-
-- [ ] **Step 2: Add preferences section CSS styles**
-
-Append to `samsa.css`:
-
-```css
-/* ============================================================
- Preferences Page Styles
- ============================================================ */
-
-.pref-section {
- margin-bottom: 2rem;
-}
-
-.pref-section:last-child {
- margin-bottom: 0;
-}
-
-.pref-section-title {
- font-size: 1rem;
- font-weight: 600;
- color: var(--text-primary);
- margin-bottom: 1rem;
- padding-bottom: 0.5rem;
- border-bottom: 1px solid var(--border);
-}
-
-.pref-row {
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 1rem;
- padding: 0.75rem 0;
- border-bottom: 1px solid var(--border);
-}
-
-.pref-row:last-child {
- border-bottom: none;
-}
-
-.pref-row label {
- font-size: 0.9rem;
- color: var(--text-primary);
-}
-
-.pref-row-info {
- flex: 1;
-}
-
-.pref-row-info label {
- font-weight: 500;
-}
-
-.pref-desc {
- font-size: 0.8rem;
- color: var(--text-muted);
- margin-top: 0.25rem;
-}
-
-.pref-row select {
- padding: 0.5rem 0.75rem;
- font-size: 0.85rem;
- font-family: inherit;
- border: 1px solid var(--border);
- border-radius: var(--radius-sm);
- background: var(--bg);
- color: var(--text-primary);
- cursor: pointer;
- min-width: 150px;
-}
-
-.pref-row select:focus {
- outline: none;
- border-color: var(--accent);
-}
-
-.pref-row input[type="checkbox"] {
- width: 18px;
- height: 18px;
- accent-color: var(--accent);
- cursor: pointer;
- flex-shrink: 0;
-}
-
-.pref-row input[type="checkbox"]:disabled {
- opacity: 0.6;
- cursor: not-allowed;
-}
-```
-
-- [ ] **Step 3: Register preferences template in views.go**
-
-Add `tmplPreferences` variable and initialize it in `init()`. Also add `RenderPreferences` function:
-
-```go
-// In views.go, add to var block:
-var (
- tmplFull *template.Template
- tmplIndex *template.Template
- tmplFragment *template.Template
- tmplPreferences *template.Template
-)
-
-// In init(), after existing template parsing, add:
-tmplPreferences = template.Must(template.New("").Funcs(funcMap).ParseFS(tmplFS,
- "base.html", "preferences.html",
-))
-
-// Add RenderPreferences function:
-func RenderPreferences(w http.ResponseWriter, sourceURL string) error {
- w.Header().Set("Content-Type", "text/html; charset=utf-8")
- return tmplPreferences.ExecuteTemplate(w, "base", PageData{ShowHeader: true, SourceURL: sourceURL})
-}
-```
-
-- [ ] **Step 4: Commit**
-
-```bash
-git add internal/views/templates/preferences.html internal/views/static/css/samsa.css internal/views/views.go
-git commit -m "feat(frontend): add preferences page template and styles"
-```
-
----
-
-### Task 5: Add Preferences Route
-
-**Files:**
-- Modify: `internal/httpapi/handlers.go`
-- Modify: `cmd/samsa/main.go`
-
-- [ ] **Step 1: Add GET and POST handlers for /preferences**
-
-Add to `handlers.go`:
-
-```go
-// Preferences renders the preferences page.
-func (h *Handler) Preferences(w http.ResponseWriter, r *http.Request) {
- if r.URL.Path != "/preferences" {
- http.NotFound(w, r)
- return
- }
- if err := views.RenderPreferences(w, h.sourceURL); err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- }
-}
-
-// PreferencesPOST handles form submission from the preferences page.
-// NOTE: This is a no-op. All preferences are stored in localStorage on the client
-// via JavaScript. This handler exists only for form submission completeness (e.g.,
-// if a form POSTs without JS). The JavaScript in settings.js handles all saves.
-func (h *Handler) PreferencesPOST(w http.ResponseWriter, r *http.Request) {
- if r.URL.Path != "/preferences" {
- http.NotFound(w, r)
- return
- }
- http.Redirect(w, r, "/preferences", http.StatusFound)
-}
-```
-
-- [ ] **Step 2: Register the route in main**
-
-Find where routes are registered (likely in `cmd/samsa/main.go`) and add:
-
-```go
-mux.HandleFunc("GET /preferences", handler.Preferences)
-mux.HandleFunc("POST /preferences", handler.PreferencesPOST)
-```
-
-- [ ] **Step 3: Test compilation**
-
-Run: `go build ./...`
-Expected: No errors
-
-- [ ] **Step 4: Commit**
-
-```bash
-git add internal/httpapi/handlers.go cmd/samsa/main.go
-git commit -m "feat: add GET and POST /preferences route"
-```
-
----
-
-### Task 6: Update Settings JavaScript
-
-**Files:**
-- Modify: `internal/views/static/js/settings.js`
-
-- [ ] **Step 1: Reduce popover to theme + engines only**
-
-Update the `renderPanel` function to remove SafeSearch and Format options. Keep only theme buttons and engine toggles.
-
-- [ ] **Step 2: Add preferences page navigation JavaScript**
-
-Add to end of `settings.js`:
-
-```javascript
-// Preferences page navigation
-function initPreferences() {
- var nav = document.getElementById('preferences-nav');
- if (!nav) return;
-
- var sections = document.querySelectorAll('.pref-section');
- var navItems = nav.querySelectorAll('.preferences-nav-item');
-
- function showSection(id) {
- sections.forEach(function(sec) {
- sec.style.display = sec.id === 'section-' + id ? 'block' : 'none';
- });
- navItems.forEach(function(item) {
- item.classList.toggle('active', item.getAttribute('data-section') === id);
- });
- }
-
- navItems.forEach(function(item) {
- item.addEventListener('click', function() {
- showSection(item.getAttribute('data-section'));
- });
- });
-
- // Load saved preferences
- var prefs = loadPrefs();
- var themeEl = document.getElementById('pref-theme');
- if (themeEl) themeEl.value = prefs.theme || 'system';
-
- var ssEl = document.getElementById('pref-safesearch');
- if (ssEl) ssEl.value = prefs.safeSearch || 'moderate';
-
- var fmtEl = document.getElementById('pref-format');
- if (fmtEl) fmtEl.value = prefs.format || 'html';
-
- // Save handlers
- if (themeEl) {
- themeEl.addEventListener('change', function() {
- prefs.theme = themeEl.value;
- savePrefs(prefs);
- applyTheme(prefs.theme);
- });
- }
-
- if (ssEl) {
- ssEl.addEventListener('change', function() {
- prefs.safeSearch = ssEl.value;
- savePrefs(prefs);
- });
- }
-
- if (fmtEl) {
- fmtEl.addEventListener('change', function() {
- prefs.format = fmtEl.value;
- savePrefs(prefs);
- });
- }
-
- // Show first section by default
- showSection('search');
-}
-
-document.addEventListener('DOMContentLoaded', initPreferences);
-```
-
-- [ ] **Step 3: Test with browser**
-
-Manual verification needed β cannot test browser JS with `go build`
-
-- [ ] **Step 4: Commit**
-
-```bash
-git add internal/views/static/js/settings.js
-git commit -m "feat(frontend): reduce popover to theme+engines, add preferences page JS"
-```
-
----
-
-## PHASE 5: Polish and Mobile Responsiveness
-
-### Task 7: Mobile Filter Chips Integration
-
-**Files:**
-- Modify: `internal/views/templates/results.html`
-
-- [ ] **Step 1: Ensure mobile filter chips have working category links**
-
-The current results.html has mobile filter chips with category links. These should preserve existing query params for pagination/HTMX navigation.
-
-- [ ] **Step 2: Add filter form submission via HTMX**
-
-Update the filter radio buttons to submit via HTMX when changed.
-
-- [ ] **Step 3: Commit**
-
-```bash
-git add internal/views/templates/results.html
-git commit -m "fix(frontend): add HTMX filter submission"
-```
-
----
-
-### Task 8: Final Mobile Responsiveness Audit
-
-**Files:**
-- Review: `internal/views/static/css/samsa.css`
-
-- [ ] **Step 1: Test all breakpoints manually**
-
-- [ ] **Step 2: Fix any layout issues found**
-
-- [ ] **Step 3: Commit any fixes**
-
-```bash
-git add internal/views/static/css/samsa.css
-git commit -m "fix(frontend): improve mobile responsiveness"
-```
-
----
-
-## Summary
-
-| Phase | Task | Files |
-|-------|------|-------|
-| 1 | CSS Layout Framework | `samsa.css` |
-| 2 | Results Three-Column | `results.html`, `views.go` |
-| 3 | Homepage Tiles | `index.html` |
-| 4 | Preferences Page | `preferences.html` (new), `handlers.go`, `settings.js` |
-| 5 | Polish | Various |
-
-**Total: 8 tasks across 5 phases**
-
-Run `go test ./...` after each phase to verify nothing is broken.
diff --git a/docs/superpowers/plans/2026-03-22-settings-ui.md b/docs/superpowers/plans/2026-03-22-settings-ui.md
deleted file mode 100644
index 8b68536..0000000
--- a/docs/superpowers/plans/2026-03-22-settings-ui.md
+++ /dev/null
@@ -1,747 +0,0 @@
-# Settings UI Implementation Plan
-
-> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
-
-**Goal:** A preferences popover panel (top-right on desktop, bottom sheet on mobile) that lets users set theme, enabled engines, safe search, and default format. All changes auto-save to `localStorage` and apply immediately to the DOM.
-
-**Architecture:** Pure client-side JS + CSS added alongside existing templates. No Go changes. Settings persist via `localStorage` key `samsa_prefs`. Theme applies via `data-theme` attribute on ``.
-
-**Tech Stack:** Vanilla JS (no framework), existing `samsa.css` custom properties, HTMX for search.
-
----
-
-## File Map
-
-| Action | File |
-|--------|------|
-| Create | `internal/views/static/js/settings.js` |
-| Modify | `internal/views/static/css/samsa.css` |
-| Modify | `internal/views/templates/base.html` |
-| Modify | `internal/views/templates/index.html` |
-| Modify | `internal/views/templates/results.html` |
-| Modify | `internal/views/views.go` |
-
-**Key insight on engine preferences:** `ParseSearchRequest` reads `engines` as a CSV form value (`r.FormValue("engines")`). The search forms in `index.html` and `results.html` will get a hidden `#engines-input` field that is kept in sync with localStorage. On submit, the engines preference is sent as a normal form field. HTMX `hx-include="this"` already includes the form element, so the hidden input is automatically included in the request.
-
----
-
-## Task 1: CSS β Popover, toggles, bottom sheet
-
-**Files:**
-- Modify: `internal/views/static/css/samsa.css`
-
-- [ ] **Step 1: Add CSS for popover, triggers, toggles, bottom sheet**
-
-Append the following to `samsa.css`:
-
-```css
-/* ============================================
- Settings Panel
- ============================================ */
-
-/* Header */
-.site-header {
- display: flex;
- align-items: center;
- justify-content: space-between;
- padding: 0.6rem 1rem;
- background: var(--color-header-background);
- border-bottom: 1px solid var(--color-header-border);
-}
-.site-title {
- font-size: 1rem;
- font-weight: 600;
- color: var(--color-base-font);
-}
-
-/* Gear trigger button */
-.settings-trigger {
- background: none;
- border: none;
- font-size: 1.1rem;
- cursor: pointer;
- padding: 0.3rem 0.5rem;
- border-radius: var(--radius);
- color: var(--color-base-font);
- opacity: 0.7;
- transition: opacity 0.2s, background 0.2s;
- line-height: 1;
-}
-.settings-trigger:hover,
-.settings-trigger[aria-expanded="true"] {
- opacity: 1;
- background: var(--color-sidebar-background);
-}
-
-/* Popover panel */
-.settings-popover {
- position: absolute;
- top: 100%;
- right: 0;
- width: 280px;
- max-height: 420px;
- overflow-y: auto;
- background: var(--color-base-background);
- border: 1px solid var(--color-sidebar-border);
- border-radius: var(--radius);
- box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
- z-index: 200;
- display: none;
- flex-direction: column;
-}
-.settings-popover[data-open="true"] {
- display: flex;
- animation: settings-slide-in 0.2s ease;
-}
-@keyframes settings-slide-in {
- from { opacity: 0; transform: translateY(-8px); }
- to { opacity: 1; transform: translateY(0); }
-}
-
-.settings-popover-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding: 0.75rem 1rem;
- border-bottom: 1px solid var(--color-sidebar-border);
- font-weight: 600;
- font-size: 0.9rem;
- flex-shrink: 0;
-}
-.settings-popover-close {
- background: none;
- border: none;
- font-size: 1.2rem;
- cursor: pointer;
- color: var(--color-base-font);
- opacity: 0.6;
- padding: 0 0.25rem;
- line-height: 1;
-}
-.settings-popover-close:hover { opacity: 1; }
-
-.settings-popover-body {
- padding: 0.8rem;
- display: flex;
- flex-direction: column;
- gap: 1rem;
-}
-
-.settings-section-title {
- font-size: 0.7rem;
- font-weight: 600;
- text-transform: uppercase;
- letter-spacing: 0.05em;
- color: var(--color-suggestion);
- margin-bottom: 0.5rem;
-}
-
-/* Theme buttons */
-.theme-buttons {
- display: flex;
- gap: 0.4rem;
-}
-.theme-btn {
- flex: 1;
- padding: 0.35rem 0.5rem;
- border: 1px solid var(--color-sidebar-border);
- border-radius: var(--radius);
- background: var(--color-btn-background);
- color: var(--color-base-font);
- cursor: pointer;
- font-size: 0.75rem;
- text-align: center;
- transition: background 0.15s, border-color 0.15s;
-}
-.theme-btn:hover { background: var(--color-btn-hover); }
-.theme-btn.active {
- background: var(--color-link);
- color: #fff;
- border-color: var(--color-link);
-}
-
-/* Engine toggles β 2-column grid */
-.engine-grid {
- display: grid;
- grid-template-columns: 1fr 1fr;
- gap: 0.4rem;
-}
-.engine-toggle {
- display: flex;
- align-items: center;
- gap: 0.4rem;
- padding: 0.3rem 0.5rem;
- border-radius: var(--radius);
- background: var(--color-sidebar-background);
- font-size: 0.78rem;
- cursor: pointer;
-}
-.engine-toggle input[type="checkbox"] {
- width: 15px;
- height: 15px;
- margin: 0;
- cursor: pointer;
- accent-color: var(--color-link);
-}
-.engine-toggle span {
- flex: 1;
- min-width: 0;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
-}
-
-/* Search defaults */
-.setting-row {
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 0.5rem;
- margin-top: 0.4rem;
-}
-.setting-row label {
- font-size: 0.85rem;
- flex: 1;
-}
-.setting-row select {
- width: 110px;
- padding: 0.3rem 0.4rem;
- font-size: 0.8rem;
- border: 1px solid var(--color-sidebar-border);
- border-radius: var(--radius);
- background: var(--color-base-background);
- color: var(--color-base-font);
- cursor: pointer;
-}
-
-/* Mid-search notice */
-.settings-notice {
- font-size: 0.72rem;
- color: var(--color-suggestion);
- margin-top: 0.3rem;
- font-style: italic;
-}
-
-/* Dark theme via data-theme attribute */
-html[data-theme="dark"] {
- --color-base: #222;
- --color-base-font: #dcdcdc;
- --color-base-background: #2b2b2b;
- --color-header-background: #333;
- --color-header-border: #444;
- --color-search-border: #555;
- --color-search-focus: #5dade2;
- --color-result-url: #8ab4f8;
- --color-result-url-visited: #b39ddb;
- --color-result-content: #b0b0b0;
- --color-result-title: #8ab4f8;
- --color-result-title-visited: #b39ddb;
- --color-result-engine: #999;
- --color-result-border: #3a3a3a;
- --color-link: #5dade2;
- --color-link-visited: #b39ddb;
- --color-sidebar-background: #333;
- --color-sidebar-border: #444;
- --color-infobox-background: #333;
- --color-infobox-border: #444;
- --color-pagination-current: #5dade2;
- --color-pagination-border: #444;
- --color-error: #e74c3c;
- --color-error-background: #3b1a1a;
- --color-suggestion: #999;
- --color-footer: #666;
- --color-btn-background: #333;
- --color-btn-border: #555;
- --color-btn-hover: #444;
-}
-
-/* Mobile: Bottom sheet + FAB trigger */
-@media (max-width: 768px) {
- /* Hide desktop trigger, show FAB */
- .settings-trigger-desktop {
- display: none;
- }
- .settings-trigger-mobile {
- display: block;
- }
- .settings-popover {
- position: fixed;
- top: auto;
- bottom: 0;
- left: 0;
- right: 0;
- width: 100%;
- max-height: 70vh;
- border-radius: var(--radius) var(--radius) 0 0;
- border-bottom: none;
- }
- /* FAB: fixed bottom-right button visible only on mobile */
- .settings-trigger-mobile {
- display: block;
- position: fixed;
- bottom: 1.5rem;
- right: 1.5rem;
- width: 48px;
- height: 48px;
- border-radius: 50%;
- background: var(--color-link);
- color: #fff;
- border: none;
- box-shadow: 0 4px 12px rgba(0,0,0,0.2);
- font-size: 1.2rem;
- z-index: 199;
- opacity: 1;
- }
-}
-```
-
-Note: The existing `:root` and `@media (prefers-color-scheme: dark)` blocks provide the "system" theme. `html[data-theme="dark"]` overrides only apply when the user explicitly picks dark mode. When `theme === 'system'`, the `data-theme` attribute is removed and the browser's `prefers-color-scheme` media query kicks in via the existing CSS.
-
-- [ ] **Step 2: Verify existing tests still pass**
-
-Run: `go test ./...`
-Expected: all pass
-
-- [ ] **Step 3: Commit**
-
-```bash
-git add internal/views/static/css/samsa.css
-git commit -m "feat(settings): add popover, toggle, and bottom-sheet CSS"
-```
-
----
-
-## Task 2: JS β Settings logic
-
-**Files:**
-- Create: `internal/views/static/js/settings.js`
-
-- [ ] **Step 1: Write the settings JS module**
-
-Create `internal/views/static/js/settings.js`:
-
-```javascript
-'use strict';
-
-var ALL_ENGINES = [
- 'wikipedia', 'arxiv', 'crossref', 'braveapi',
- 'qwant', 'duckduckgo', 'github', 'reddit', 'bing'
-];
-
-var DEFAULT_PREFS = {
- theme: 'system',
- engines: ALL_ENGINES.slice(),
- safeSearch: 'moderate',
- format: 'html'
-};
-
-var STORAGE_KEY = 'samsa_prefs';
-
-// ββ Persistence ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
-
-function loadPrefs() {
- try {
- var raw = localStorage.getItem(STORAGE_KEY);
- if (!raw) return { theme: DEFAULT_PREFS.theme, engines: DEFAULT_PREFS.engines.slice(), safeSearch: DEFAULT_PREFS.safeSearch, format: DEFAULT_PREFS.format };
- var saved = JSON.parse(raw);
- return { theme: saved.theme || DEFAULT_PREFS.theme, engines: saved.engines || DEFAULT_PREFS.engines.slice(), safeSearch: saved.safeSearch || DEFAULT_PREFS.safeSearch, format: saved.format || DEFAULT_PREFS.format };
- } catch (e) {
- return { theme: DEFAULT_PREFS.theme, engines: DEFAULT_PREFS.engines.slice(), safeSearch: DEFAULT_PREFS.safeSearch, format: DEFAULT_PREFS.format };
- }
-}
-
-function savePrefs(prefs) {
- try {
- localStorage.setItem(STORAGE_KEY, JSON.stringify({ theme: prefs.theme, engines: prefs.engines, safeSearch: prefs.safeSearch, format: prefs.format }));
- } catch (e) { /* quota or private mode */ }
-}
-
-// ββ Theme application ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
-
-function applyTheme(theme) {
- if (theme === 'system') {
- document.documentElement.removeAttribute('data-theme');
- } else {
- document.documentElement.setAttribute('data-theme', theme);
- }
-}
-
-// ββ Engine input sync βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
-
-function syncEngineInput(prefs) {
- var input = document.getElementById('engines-input');
- if (input) input.value = prefs.engines.join(',');
-}
-
-// ββ Panel open / close ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
-
-function closePanel() {
- var panel = document.getElementById('settings-popover');
- var trigger = document.getElementById('settings-trigger');
- if (!panel) return;
- panel.setAttribute('data-open', 'false');
- if (trigger) trigger.setAttribute('aria-expanded', 'false');
- if (trigger) trigger.focus();
-}
-
-function openPanel() {
- var panel = document.getElementById('settings-popover');
- var trigger = document.getElementById('settings-trigger');
- if (!panel) return;
- panel.setAttribute('data-open', 'true');
- if (trigger) trigger.setAttribute('aria-expanded', 'true');
- var focusable = panel.querySelector('button, input, select');
- if (focusable) focusable.focus();
-}
-
-// ββ Escape key βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
-
-document.addEventListener('keydown', function(e) {
- if (e.key !== 'Escape') return;
- var panel = document.getElementById('settings-popover');
- if (!panel || panel.getAttribute('data-open') !== 'true') return;
- closePanel();
-});
-
-// ββ Click outside βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
-
-document.addEventListener('click', function(e) {
- var panel = document.getElementById('settings-popover');
- var trigger = document.getElementById('settings-trigger');
- if (!panel || panel.getAttribute('data-open') !== 'true') return;
- if (!panel.contains(e.target) && (!trigger || !trigger.contains(e.target))) {
- closePanel();
- }
-});
-
-// ββ Focus trap ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
-
-document.addEventListener('keydown', function(e) {
- if (e.key !== 'Tab') return;
- var panel = document.getElementById('settings-popover');
- if (!panel || panel.getAttribute('data-open') !== 'true') return;
- var focusable = Array.prototype.slice.call(panel.querySelectorAll('button, input, select, [tabindex]:not([tabindex="-1"])'));
- if (!focusable.length) return;
- var first = focusable[0];
- var last = focusable[focusable.length - 1];
- if (e.shiftKey) {
- if (document.activeElement === first) { e.preventDefault(); last.focus(); }
- } else {
- if (document.activeElement === last) { e.preventDefault(); first.focus(); }
- }
-});
-
-// ββ Render ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
-
-function escapeHtml(str) {
- return String(str).replace(/&/g, '&').replace(//g, '>');
-}
-
-function renderPanel(prefs) {
- var panel = document.getElementById('settings-popover');
- if (!panel) return;
- var body = panel.querySelector('.settings-popover-body');
- if (!body) return;
-
- var themeBtns = '';
- ['light', 'dark', 'system'].forEach(function(t) {
- var icons = { light: '\u2600', dark: '\u263D', system: '\u2318' };
- var labels = { light: 'Light', dark: 'Dark', system: 'System' };
- var active = prefs.theme === t ? ' active' : '';
- themeBtns += '';
- });
-
- var engineToggles = '';
- ALL_ENGINES.forEach(function(name) {
- var checked = prefs.engines.indexOf(name) !== -1 ? ' checked' : '';
- engineToggles += '';
- });
-
- var ssOptions = [
- { val: 'moderate', label: 'Moderate' },
- { val: 'strict', label: 'Strict' },
- { val: 'off', label: 'Off' }
- ];
- var fmtOptions = [
- { val: 'html', label: 'HTML' },
- { val: 'json', label: 'JSON' },
- { val: 'csv', label: 'CSV' },
- { val: 'rss', label: 'RSS' }
- ];
- var ssOptionsHtml = '';
- var fmtOptionsHtml = '';
- ssOptions.forEach(function(o) {
- var sel = prefs.safeSearch === o.val ? ' selected' : '';
- ssOptionsHtml += '';
- });
- fmtOptions.forEach(function(o) {
- var sel = prefs.format === o.val ? ' selected' : '';
- fmtOptionsHtml += '';
- });
-
- body.innerHTML =
- '' +
- '
Appearance
' +
- '
' + themeBtns + '
' +
- '
' +
- '' +
- '
Engines
' +
- '
' + engineToggles + '
' +
- '
Engine changes apply to your next search.
' +
- '
' +
- '' +
- '
Search Defaults
' +
- '
' +
- '' +
- '' +
- '
' +
- '
' +
- '' +
- '' +
- '
' +
- '
';
-
- // Theme buttons
- var themeBtnEls = panel.querySelectorAll('.theme-btn');
- for (var i = 0; i < themeBtnEls.length; i++) {
- themeBtnEls[i].addEventListener('click', (function(btn) {
- return function() {
- prefs.theme = btn.getAttribute('data-theme');
- savePrefs(prefs);
- applyTheme(prefs.theme);
- syncEngineInput(prefs);
- renderPanel(prefs);
- };
- })(themeBtnEls[i]));
- }
-
- // Engine checkboxes
- var checkboxes = panel.querySelectorAll('.engine-toggle input[type="checkbox"]');
- for (var j = 0; j < checkboxes.length; j++) {
- checkboxes[j].addEventListener('change', (function(cb) {
- return function() {
- var checked = Array.prototype.slice.call(panel.querySelectorAll('.engine-toggle input[type="checkbox"]:checked')).map(function(el) { return el.value; });
- if (checked.length === 0) { cb.checked = true; return; }
- prefs.engines = checked;
- savePrefs(prefs);
- syncEngineInput(prefs);
- };
- })(checkboxes[j]));
- }
-
- // Safe search
- var ssEl = panel.querySelector('#pref-safesearch');
- if (ssEl) {
- ssEl.addEventListener('change', function() {
- prefs.safeSearch = ssEl.value;
- savePrefs(prefs);
- });
- }
-
- // Format
- var fmtEl = panel.querySelector('#pref-format');
- if (fmtEl) {
- fmtEl.addEventListener('change', function() {
- prefs.format = fmtEl.value;
- savePrefs(prefs);
- });
- }
-
- // Close button
- var closeBtn = panel.querySelector('.settings-popover-close');
- if (closeBtn) closeBtn.addEventListener('click', closePanel);
-}
-
-// ββ Init βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
-
-function initSettings() {
- var prefs = loadPrefs();
- applyTheme(prefs.theme);
- syncEngineInput(prefs);
-
- var panel = document.getElementById('settings-popover');
- var trigger = document.getElementById('settings-trigger');
- var mobileTrigger = document.getElementById('settings-trigger-mobile');
-
- if (panel) {
- renderPanel(prefs);
-
- function togglePanel() {
- var isOpen = panel.getAttribute('data-open') === 'true';
- if (isOpen) closePanel(); else openPanel();
- }
-
- if (trigger) trigger.addEventListener('click', togglePanel);
- if (mobileTrigger) mobileTrigger.addEventListener('click', togglePanel);
- }
-}
-
-if (document.readyState === 'loading') {
- document.addEventListener('DOMContentLoaded', initSettings);
-} else {
- initSettings();
-}
-```
-
-- [ ] **Step 2: Verify JS syntax**
-
-Run: `node --check internal/views/static/js/settings.js`
-Expected: no output (exit 0)
-
-- [ ] **Step 3: Commit**
-
-```bash
-git add internal/views/static/js/settings.js
-git commit -m "feat(settings): add JS module for localStorage preferences and panel"
-```
-
----
-
-## Task 3: HTML β Gear trigger, panel markup, header in base
-
-**Files:**
-- Modify: `internal/views/templates/base.html`
-- Modify: `internal/views/views.go`
-
-- [ ] **Step 1: Add ShowHeader to PageData**
-
-In `views.go`, add `ShowHeader bool` to `PageData` struct.
-
-- [ ] **Step 2: Set ShowHeader in render functions**
-
-In `RenderIndex` and `RenderSearch`, set `PageData.ShowHeader = true`.
-
-- [ ] **Step 3: Update base.html β add header and settings markup**
-
-In `base.html`, update the `` to:
-
-```html
-
- {{if .ShowHeader}}
-
-
-
- {{end}}
-
- {{template "content" .}}
-
-
-
-
-
-
-```
-
-**Note:** The existing autocomplete `