@@ -212,6 +210,7 @@
Fruit Slash
+
diff --git a/css/CLAUDE.md b/css/CLAUDE.md
index e867c5f..8531b85 100644
--- a/css/CLAUDE.md
+++ b/css/CLAUDE.md
@@ -2,7 +2,7 @@
Brief overview: 3 CSS files totaling ~3,900 lines. Five themes (bold, cinematic, brutalist, retro, neon). Pattern: base component styles followed by theme override blocks.
-## shared.css (2576 lines)
+## shared.css (2570 lines)
### Section Map
@@ -11,10 +11,9 @@ Brief overview: 3 CSS files totaling ~3,900 lines. Five themes (bold, cinematic,
| Universal reset + :root variables | 1-26 | --bg, --bg-warm, --bg-card, --border, --border-hover, --text-primary, --text-secondary, --text-muted, --accent, --accent-soft, --accent-mid, --pop, --pop-soft, --cream, --accent-glow, --font-serif, --font-body, --font-mono, --ease-out, --ease-spring, --radius, --radius-sm |
| [data-theme="neon"] variables | 28-47 | Dark grays, lime accent (#c9f059), blue pop (#59b8f0), Sora/Space Mono fonts |
| Global transitions | 49-51 | body, .work-card, .approach-card transitions |
-| .theme-switcher | 53-74 | Fixed positioning, sizing, hover states |
-| PLANE ACTIVE | 76-81 | html.plane-active styles (user-select: none) |
-| PLANE TOGGLE | 83-127 | .plane-toggle button, attractor state, bounce animation |
-| Global html/body/links | 128-148 | Base typography, .container max-width 1140px |
+| RIGHT-EDGE TOOLBAR | 59-183 | .toolbar fixed-right container, .swatches stack, .swatch-btn[data-theme-id] dots, .plane-btn active state, .preview-panel (desktop hover only), responsive @768px shrink, prefers-reduced-motion disable |
+| PLANE ACTIVE | ~185 | html.plane-active styles (user-select: none) |
+| Global html/body/links | ~190 | Base typography, .container max-width 1140px |
| HERO | 150-257 | .hero min-height 100vh, .hero-badge fadeUp, .hero h1 clamp(3rem-6.5rem), .hero-body grid, .hero-tidbits |
| SECTIONS | 258-279 | section padding 7rem, .section-label, .section-heading |
| WORK CARDS | 280-491 | .work-grid, .work-card, .work-card-header, .work-icon, .work-expand, .work-image-placeholder (warm/cool/fresh), .work-details, .work-stats |
@@ -22,7 +21,7 @@ Brief overview: 3 CSS files totaling ~3,900 lines. Five themes (bold, cinematic,
| APPROACH | 564-608 | .approach-cards grid, .approach-card |
| CONTACT | 609-672 | .contact, .btn primary/secondary |
| DOODLE DECORATION | 674-707 | .doodle positioning and animation |
-| ANIMATIONS | 708-732 | @keyframes: fadeUp, pulse, nudgeBounce |
+| ANIMATIONS | ~708 | @keyframes: fadeUp, pulse |
| 3D PLANE OVERLAY | 733-740 | #plane-canvas fixed overlay |
| THEME LAYOUT TOGGLES | 741-749 | .status-bar, .hero-brutalist, .about-brutalist, .hero-panel display toggles |
| NEON THEME OVERRIDES | 750-844 | Font weights 600-700, sharp corners, grain overlay, doodle hidden |
@@ -42,6 +41,7 @@ All variables defined in lines 3-26:
- `--text-primary`, `--text-secondary`, `--text-muted` -- text colors
- `--accent`, `--accent-soft`, `--accent-mid`, `--accent-glow` -- accent color + opacity variants
- `--pop`, `--pop-soft` -- pop color (same as accent in bold theme)
+- `--theme-bold-accent`, `--theme-brutalist-accent`, `--theme-retro-accent`, `--theme-cinematic-accent`, `--theme-neon-accent` -- cross-theme swatch identity colors (used by `.toolbar .swatch-dot`, never overridden per theme)
- `--cream` -- cream background
- `--font-serif`, `--font-body`, `--font-mono` -- font stacks
- `--ease-out`, `--ease-spring` -- easing functions
@@ -57,7 +57,7 @@ All variables defined in lines 3-26:
### Responsive Breakpoints
- max-width: 900px -- hero-body 1fr, about-content 1fr
-- max-width: 768px -- plane-toggle adjustments
+- max-width: 768px -- .toolbar shrink (smaller swatches + plane icon)
- max-width: 600px -- overflow-x: hidden
## cards.css (864 lines)
diff --git a/css/shared.css b/css/shared.css
index 3127f53..0aea8be 100644
--- a/css/shared.css
+++ b/css/shared.css
@@ -23,6 +23,12 @@
--ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
--radius: 0px;
--radius-sm: 0px;
+ /* Cross-theme swatch accent identities (NEVER overridden per theme) */
+ --theme-bold-accent: #ff3d00;
+ --theme-brutalist-accent: #0000ff;
+ --theme-retro-accent: #1A5C52;
+ --theme-cinematic-accent: #D6001C;
+ --theme-neon-accent: #c9f059;
}
[data-theme="neon"] {
@@ -50,79 +56,162 @@ body, .work-card, .approach-card, .about-box, .tidbit, .chip, .btn {
transition: background-color 0.3s, color 0.3s, border-color 0.3s;
}
-.theme-switcher {
+/* ========== RIGHT-EDGE TOOLBAR ========== */
+.toolbar {
position: fixed;
- top: 4.5rem;
- right: 1.5rem;
- z-index: 99;
- font-family: var(--font-mono);
- font-size: 0.65rem;
- font-weight: 500;
- letter-spacing: 0.05em;
- padding: 0.4rem 0.9rem;
- border-radius: 100px;
+ right: 0;
+ top: 50%;
+ transform: translateY(-50%);
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ padding: 20px 10px;
+ background: var(--bg-card);
border: 1px solid var(--border);
- background: var(--cream);
- color: var(--text-secondary);
+ border-right: none;
+ border-top-left-radius: var(--radius);
+ border-bottom-left-radius: var(--radius);
+ border-top-right-radius: 0;
+ border-bottom-right-radius: 0;
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.12);
+ transition: background 0.3s, border-color 0.3s, box-shadow 0.3s, border-radius 0.3s;
+ z-index: 99;
+}
+
+.toolbar .swatches {
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+ align-items: center;
+}
+
+.toolbar .swatch-btn {
+ border: none;
cursor: pointer;
- transition: all 0.3s;
+ padding: 0;
+ background: none;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transition: transform 0.15s;
+ position: relative;
}
+.toolbar .swatch-btn:hover { transform: scale(1.15); }
-.theme-switcher:hover {
- border-color: var(--accent);
- color: var(--accent);
+.toolbar .swatch-dot {
+ width: 22px;
+ height: 22px;
+ border-radius: 4px;
+ transition: all 0.2s;
}
-/* ========== PLANE ACTIVE (disable text selection during plane mode) ========== */
-html.plane-active {
- user-select: none;
- -webkit-user-select: none;
- scroll-behavior: auto;
+.toolbar .swatch-btn.active .swatch-dot {
+ width: 26px;
+ height: 26px;
+ border-radius: 5px;
+ box-shadow: 0 0 0 2.5px var(--accent);
}
-/* ========== PLANE TOGGLE ========== */
-.plane-toggle {
- position: fixed;
- top: 7rem;
- right: 1.5rem;
- z-index: 99;
- font-family: var(--font-mono);
- font-size: 0.65rem;
- font-weight: 500;
- letter-spacing: 0.05em;
- padding: 0.4rem 0.9rem;
- border-radius: 100px;
- border: 1px solid var(--border);
- background: var(--cream);
- color: var(--text-secondary);
+/* Individual swatch dot colours (cross-theme constants) */
+.toolbar .swatch-btn[data-theme-id="bold"] .swatch-dot { background: var(--theme-bold-accent); }
+.toolbar .swatch-btn[data-theme-id="brutalist"] .swatch-dot { background: var(--theme-brutalist-accent); }
+.toolbar .swatch-btn[data-theme-id="retro"] .swatch-dot { background: var(--theme-retro-accent); }
+.toolbar .swatch-btn[data-theme-id="cinematic"] .swatch-dot { background: var(--theme-cinematic-accent); }
+.toolbar .swatch-btn[data-theme-id="neon"] .swatch-dot { background: var(--theme-neon-accent); }
+
+.toolbar .divider {
+ width: 18px;
+ height: 1px;
+ background: var(--border);
+ margin: 14px auto;
+}
+
+.toolbar .plane-btn {
+ border: none;
cursor: pointer;
- transition: border-color 0.3s, color 0.3s, background 0.3s;
display: flex;
align-items: center;
- gap: 5px;
+ justify-content: center;
+ background: transparent;
+ color: var(--text-secondary);
+ padding: 8px 10px;
+ border-radius: 6px;
+ transition: background 0.2s, color 0.2s;
}
-
-.plane-toggle:hover {
- border-color: var(--accent);
+.toolbar .plane-btn svg { width: 22px; height: 22px; transition: color 0.2s; }
+.toolbar .plane-btn:hover:not(.active) { color: var(--text-primary); }
+.toolbar .plane-btn.active {
+ background: var(--accent-soft);
color: var(--accent);
}
-.plane-toggle.attractor {
- border-color: var(--accent);
- color: var(--accent);
- background: var(--cream);
- will-change: transform;
+/* Preview panel — desktop only (enabled via @media hover below) */
+.toolbar .preview-panel {
+ position: absolute;
+ right: calc(100% + 12px);
+ pointer-events: none;
+ z-index: 20;
+ width: 170px;
+ padding: 12px;
+ background: var(--bg-card);
+ border: 1px solid var(--border);
+ border-radius: var(--radius);
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.12);
+ opacity: 0;
+ transform: translateX(-8px);
+ transition: opacity 0.2s, transform 0.2s;
+ display: none; /* hidden unless desktop hover media matches */
+}
+.toolbar .preview-panel.visible { opacity: 1; transform: translateX(0); }
+.toolbar .preview-stripe { height: 4px; border-radius: 2px; margin-bottom: 8px; }
+.toolbar .preview-name {
+ font-family: var(--font-mono);
+ font-size: 10px;
+ text-transform: uppercase;
+ letter-spacing: 1.5px;
+ color: var(--text-primary);
+ margin-bottom: 2px;
+}
+.toolbar .preview-sub {
+ font-family: var(--font-mono);
+ font-size: 8px;
+ color: var(--text-secondary);
+ margin-bottom: 8px;
+ letter-spacing: 0.5px;
+}
+.toolbar .preview-chips { display: flex; gap: 4px; }
+.toolbar .preview-chip {
+ width: 16px;
+ height: 16px;
+ border-radius: 3px;
+ border: 1px solid rgba(128, 128, 128, 0.2);
}
-.plane-toggle.attractor.bouncing {
- animation: nudgeBounce 1.2s ease;
+@media (hover: hover) and (pointer: fine) {
+ .toolbar .preview-panel { display: block; }
}
-.plane-toggle.attractor:hover {
- animation: none;
- background: var(--accent-soft);
- border-color: var(--accent);
- color: var(--accent);
+@media (prefers-reduced-motion: reduce) {
+ .toolbar,
+ .toolbar .swatch-btn,
+ .toolbar .preview-panel,
+ .toolbar .plane-btn { transition: none; }
+}
+
+/* Mobile: shrink + hide while dragging a card */
+@media (max-width: 768px) {
+ .toolbar { padding: 14px 8px; }
+ .toolbar .swatch-dot { width: 18px; height: 18px; }
+ .toolbar .swatch-btn.active .swatch-dot { width: 22px; height: 22px; }
+ .toolbar .plane-btn svg { width: 18px; height: 18px; }
+ .toolbar .swatches { gap: 10px; }
+}
+
+/* ========== PLANE ACTIVE (disable text selection during plane mode) ========== */
+html.plane-active {
+ user-select: none;
+ -webkit-user-select: none;
+ scroll-behavior: auto;
}
html { scroll-behavior: smooth; }
@@ -711,14 +800,6 @@ section { padding: 7rem 0; }
to { opacity: 1; transform: translateY(0); }
}
-@keyframes nudgeBounce {
- 0%, 100% { transform: translateY(0); }
- 20% { transform: translateY(-5px); }
- 40% { transform: translateY(0); }
- 55% { transform: translateY(-3px); }
- 70% { transform: translateY(0); }
-}
-
.reveal {
opacity: 0;
transform: translateY(24px);
@@ -785,23 +866,6 @@ section { padding: 7rem 0; }
[data-theme="neon"] .tidbit { border-radius: 8px; }
[data-theme="neon"] .about-box { border-radius: 8px; }
-/* Theme switcher + plane toggle */
-[data-theme="neon"] .theme-switcher {
- top: 4.5rem;
- border-radius: 4px;
- background: var(--bg-card);
- border-color: var(--border);
-}
-[data-theme="neon"] .theme-switcher:hover {
- border-color: var(--accent);
- color: var(--accent);
- box-shadow: 0 0 12px #c9f05940;
-}
-[data-theme="neon"] .plane-toggle { border-radius: 4px; background: var(--bg-card); border-color: var(--border); }
-[data-theme="neon"] .plane-toggle:hover { border-color: var(--accent); color: var(--accent); box-shadow: 0 0 12px #c9f05940; }
-[data-theme="neon"] .plane-toggle.attractor { border-radius: 4px; background: var(--bg-card); border-color: var(--accent); color: var(--accent); box-shadow: none; }
-[data-theme="neon"] .plane-toggle.attractor:hover { background: var(--bg-card); border-color: var(--accent); color: var(--accent); box-shadow: 0 0 12px #c9f05940; }
-
/* Grain overlay */
[data-theme="neon"] body::before {
content: '';
@@ -863,23 +927,21 @@ section { padding: 7rem 0; }
display: inline;
}
-@media (max-width: 768px) {
- .plane-toggle {
- top: 7rem;
- right: 1.5rem;
- }
-}
-
@media (max-width: 600px) {
html { overflow-x: hidden; }
+}
- .plane-toggle {
- top: 3.25rem;
- }
+[data-theme="neon"] .toolbar {
+ border-radius: 16px 0 0 16px;
+ border-color: var(--accent);
+ box-shadow: 0 0 20px var(--accent-glow, rgba(201, 240, 89, 0.15)),
+ 0 4px 16px rgba(0, 0, 0, 0.4);
+}
- [data-theme="neon"] .theme-switcher {
- top: 0.75rem;
- }
+[data-theme="neon"] .toolbar .preview-panel {
+ border-radius: 16px;
+ border-color: var(--accent);
+ box-shadow: 0 0 20px var(--accent-glow, rgba(201, 240, 89, 0.15));
}
/* ========== BOLD THEME OVERRIDES ========== */
@@ -1142,17 +1204,6 @@ section { padding: 7rem 0; }
box-shadow: none;
}
-/* ── Theme Switcher & Plane Toggle ── */
-[data-theme="bold"] .theme-switcher {
- border-radius: 0;
-}
-[data-theme="bold"] .plane-toggle {
- border-radius: 0;
-}
-[data-theme="bold"] .plane-toggle.attractor {
- border-radius: 0;
-}
-
/* ── Hide neon grid bg / doodles ── */
[data-theme="bold"] .hero::before { display: none; }
@@ -1166,9 +1217,6 @@ section { padding: 7rem 0; }
}
@media (max-width: 600px) {
- [data-theme="bold"] .theme-switcher {
- top: 0.75rem;
- }
[data-theme="bold"] .hero-desc {
color: #000;
}
@@ -1180,6 +1228,10 @@ section { padding: 7rem 0; }
}
}
+[data-theme="bold"] .toolbar {
+ border-radius: 12px 0 0 12px;
+}
+
/* ========== CINEMATIC THEME OVERRIDES ========== */
[data-theme="cinematic"] {
@@ -1441,37 +1493,6 @@ section { padding: 7rem 0; }
box-shadow: none;
}
-/* ── Theme Switcher & Plane Toggle ── */
-[data-theme="cinematic"] .theme-switcher {
- border-radius: 0;
- background: var(--bg-card);
- border-color: var(--border);
-}
-[data-theme="cinematic"] .theme-switcher:hover {
- border-color: var(--accent);
- color: var(--accent);
-}
-[data-theme="cinematic"] .plane-toggle {
- border-radius: 0;
- background: var(--bg-card);
- border-color: var(--border);
-}
-[data-theme="cinematic"] .plane-toggle:hover {
- border-color: var(--accent);
- color: var(--accent);
-}
-[data-theme="cinematic"] .plane-toggle.attractor {
- border-radius: 0;
- background: var(--bg-card);
- border-color: var(--accent);
- color: var(--accent);
-}
-[data-theme="cinematic"] .plane-toggle.attractor:hover {
- background: var(--bg-card);
- border-color: var(--accent);
- color: var(--accent);
-}
-
/* ── Blur overlay ── */
[data-theme="cinematic"] .blur-overlay.active {
background: rgba(0, 0, 0, 0.5);
@@ -1492,14 +1513,21 @@ section { padding: 7rem 0; }
/* ── Responsive ── */
@media (max-width: 600px) {
- [data-theme="cinematic"] .theme-switcher {
- top: 0.75rem;
- }
[data-theme="cinematic"] .hero h1 {
font-size: clamp(2rem, 10vw, 3rem);
}
}
+[data-theme="cinematic"] .toolbar {
+ border-radius: 14px 0 0 14px;
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
+}
+
+[data-theme="cinematic"] .toolbar .preview-panel {
+ border-radius: 14px;
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
+}
+
/* ========== BRUTALIST THEME OVERRIDES ========== */
[data-theme="brutalist"] {
@@ -1601,7 +1629,8 @@ section { padding: 7rem 0; }
text-transform: uppercase;
font-size: 1.1rem;
letter-spacing: -0.02em;
- padding: 0.4rem clamp(1.5rem, 5vw, 3.5rem);
+ /* Extra right padding reserves space for the fixed toolbar (~60px wide, vertically centered) */
+ padding: 0.4rem clamp(5rem, 8vw, 6.5rem) 0.4rem clamp(1.5rem, 5vw, 3.5rem);
border-bottom: 1px solid var(--border);
display: flex;
justify-content: space-between;
@@ -2086,35 +2115,6 @@ section { padding: 7rem 0; }
box-shadow: none;
}
-[data-theme="brutalist"] .theme-switcher {
- border-radius: 0;
- border: 2px solid var(--border);
- background: var(--bg);
-}
-
-[data-theme="brutalist"] .theme-switcher:hover {
- border-color: var(--accent);
- color: var(--accent);
-}
-
-[data-theme="brutalist"] .plane-toggle {
- border-radius: 0;
- border: 2px solid var(--border);
- background: var(--bg);
-}
-
-[data-theme="brutalist"] .plane-toggle:hover {
- border-color: var(--accent);
- color: var(--accent);
-}
-
-[data-theme="brutalist"] .plane-toggle.attractor {
- border-radius: 0;
- border-color: var(--accent);
- color: var(--accent);
- background: var(--bg);
-}
-
[data-theme="brutalist"] .work-image-placeholder {
border: 1px solid var(--border);
}
@@ -2177,9 +2177,19 @@ section { padding: 7rem 0; }
[data-theme="brutalist"] #page-home {
padding-top: 0; /* status bar is now in flow, not fixed */
}
- [data-theme="brutalist"] .theme-switcher {
- top: 0.75rem;
- }
+}
+
+[data-theme="brutalist"] .toolbar {
+ border-width: 2px;
+ border-color: var(--border);
+ border-radius: 0;
+ box-shadow: 4px 4px 0 var(--border);
+}
+
+[data-theme="brutalist"] .toolbar .preview-panel {
+ border-width: 2px;
+ border-radius: 0;
+ box-shadow: 4px 4px 0 var(--border);
}
/* ========== RETRO THEME OVERRIDES ========== */
@@ -2457,39 +2467,6 @@ section { padding: 7rem 0; }
color: #F0EBE0;
}
-/* Theme Switcher */
-[data-theme="retro"] .theme-switcher {
- border-radius: 0;
- border: 2px solid #1A5C52;
- background: var(--bg);
- color: #1A5C52;
-}
-[data-theme="retro"] .theme-switcher:hover {
- border-color: var(--accent);
- color: var(--accent);
-}
-
-/* Plane Toggle */
-[data-theme="retro"] .plane-toggle {
- border-radius: 0;
- background: var(--bg);
- border: 2px solid #1A5C52;
- color: #1A5C52;
-}
-[data-theme="retro"] .plane-toggle:hover {
- border-color: var(--accent);
- color: var(--accent);
-}
-[data-theme="retro"] .plane-toggle.attractor {
- border-radius: 0;
- background: var(--bg);
- border-color: var(--accent);
- color: var(--accent);
-}
-[data-theme="retro"] .plane-toggle.attractor:hover {
- background: #D9423018;
-}
-
/* Retro teal panel — right side of split hero */
[data-theme="retro"] .hero-panel {
display: flex;
@@ -2555,6 +2532,29 @@ section { padding: 7rem 0; }
font-size: 0.95rem;
}
+[data-theme="retro"] .toolbar {
+ border-width: 2px;
+ border-color: var(--pop);
+ border-radius: 8px 0 0 8px;
+ box-shadow: 3px 3px 0 var(--pop);
+}
+
+[data-theme="retro"] .toolbar .swatch-btn.active .swatch-dot {
+ box-shadow: 0 0 0 2.5px var(--pop);
+}
+
+[data-theme="retro"] .toolbar .plane-btn.active {
+ color: var(--pop);
+ background: var(--pop-soft, rgba(26, 92, 82, 0.1));
+}
+
+[data-theme="retro"] .toolbar .preview-panel {
+ border-width: 2px;
+ border-color: var(--pop);
+ border-radius: 8px;
+ box-shadow: 3px 3px 0 var(--pop);
+}
+
/* Responsive */
@media (max-width: 900px) {
[data-theme="retro"] .hero {
@@ -2569,8 +2569,3 @@ section { padding: 7rem 0; }
}
}
-@media (max-width: 600px) {
- [data-theme="retro"] .theme-switcher {
- top: 0.75rem;
- }
-}
diff --git a/docs/superpowers/plans/2026-04-11-right-edge-toolbar-HANDOFF.md b/docs/superpowers/plans/2026-04-11-right-edge-toolbar-HANDOFF.md
new file mode 100644
index 0000000..2e83a4f
--- /dev/null
+++ b/docs/superpowers/plans/2026-04-11-right-edge-toolbar-HANDOFF.md
@@ -0,0 +1,91 @@
+# Handoff: Execute Right-Edge Toolbar Plan (Subagent-Driven)
+
+You are picking up a fully-scoped implementation plan. A prior session did the research, resolved ambiguities with the user, and wrote the plan. Your job is to execute it task-by-task using the subagent-driven-development workflow.
+
+## What to read first (in this order)
+
+1. **Invoke the skill:** `superpowers:subagent-driven-development` via the Skill tool. Follow it exactly. This is the workflow.
+2. **Read the plan:** `docs/superpowers/plans/2026-04-11-right-edge-toolbar.md`. Read it top-to-bottom once before dispatching any subagent, so you understand cross-task dependencies (especially name consistency: `#toolbar`, `#toolbar-plane-slot`, `.plane-btn`, `.swatch-btn`, `data-theme-id`, `window.setTheme`, `window.Toolbar.syncActive`).
+3. **Project conventions:** `CLAUDE.md` at repo root. Two rules matter most for this work:
+ - Never hardcode colour values in element CSS — use `var(--...)`. The plan already handles this via `--theme-*-accent` constants (Task 2).
+ - Be surgical with file reads — always use `offset`+`limit`, never read whole files. Pass this instruction to every subagent you dispatch.
+
+## Ground truth
+
+- All six ambiguities flagged in the original plan draft have been **resolved** by the user. The plan's "Resolved Decisions" section (top of the file) is authoritative — do not re-ask the user about them. In particular:
+ - Swatch dots use `--theme-*-accent` constants in `:root` (not hex in element styles).
+ - Plane icon is paper-plane SVG only. No stop-square swap. Active state is pure CSS recolour via `.plane-btn.active { color: var(--accent); ... }` + SVG `currentColor`.
+ - Retro `var(--pop)` override applies to the live toolbar chrome only; the hover preview panel still shows each target theme's identity colours via the PREVIEW object in toolbar.js.
+ - Mobile: shrink only at ≤768px. No hide-on-drag.
+ - Delete `toolbar-variation-d.html` in the final task.
+ - `lost-satellite copy for games.html` is in a backup folder, out of scope.
+
+## Execution rules
+
+- **One subagent per task.** 12 tasks in the plan. Dispatch each as a fresh subagent via the Agent tool with the `general-purpose` subagent type (or whichever subagent-driven-development recommends). The subagent should implement that one task's steps and commit.
+- **Two-stage review between tasks.** After each subagent finishes:
+ 1. Read the diff yourself (`git diff HEAD~1`) and verify it matches the plan's intent.
+ 2. Dispatch a fresh `superpowers:code-reviewer` subagent to audit the task against the plan section. Only proceed to the next task if both pass.
+- **Line numbers drift.** `css/shared.css` is ~2576 lines and will shrink significantly during Task 9. Any task after Task 3 must re-grep for line numbers before editing. Tell each subagent: "Line numbers in the plan are snapshots from when it was written — re-grep before deleting or inserting."
+- **Do NOT batch tasks.** The plan is designed for isolated, sequential, reviewable commits. Resist the urge to combine Tasks 3+4, or 9+10, etc.
+- **Verification task (Task 11) is manual + Playwright.** You can dispatch a subagent to run the Playwright commands, but you personally need to look at screenshots and confirm visual correctness across all five themes. Do not claim success without seeing the images.
+- **Don't touch out-of-scope systems:** card-hand attractor (separate from the retired plane attractor), the plane overlay's Three.js rendering, destruction.js internals, text-rearrange. The plan only touches specific lines — respect the scope.
+
+## Subagent prompt template
+
+When dispatching each task, use a briefing like:
+
+> You are implementing Task N of the right-edge toolbar plan at `docs/superpowers/plans/2026-04-11-right-edge-toolbar.md`. Read only that task's section (use offset+limit — do not read the whole file). Also read `CLAUDE.md` for the "no hardcoded colours" and "surgical file reads" rules.
+>
+> Implementation requirements:
+> - Follow every step in the task in order. Do not skip steps or batch them.
+> - Line numbers in the plan may have drifted — re-grep before deleting/inserting into `css/shared.css`.
+> - Use Read with offset+limit for every file access. Never read a full file.
+> - Commit when the task says to commit. Use the exact commit message from the plan.
+> - If a step's expected result doesn't match (e.g. grep returns zero matches when the plan expected some), STOP and report back instead of improvising.
+>
+> When done, report: (1) what you changed, (2) what you committed, (3) any deviations from the plan and why. Under 200 words.
+
+## Order of operations
+
+1. Task 1 — pre-flight grep sweep (no commits, produces an audit note)
+2. Task 2 — `--theme-*-accent` constants → commit
+3. Task 3 — `.toolbar` base CSS → commit
+4. Task 4 — per-theme overrides → commit
+5. Task 5 — `js/toolbar.js` (new file) → commit
+6. Task 6 — `js/shared.js` refactor → commit
+7. Task 7 — `js/plane.js` rework (drop attractor, new icon, adopt into slot) → commit
+8. Task 8 — analytics/destruction/page-transition selector updates → commit
+9. Task 9 — delete obsolete CSS (reverse line order, re-grep between deletions) → commit
+10. Task 10 — HTML changes across 4 live pages + script tag → commit
+11. Task 11 — Playwright verification across all 5 themes + mobile + a11y + cross-page → fix-up commit if needed
+12. Task 12 — delete prototype + update CLAUDE.md docs → commit
+
+**Reviewable checkpoint moments:** After Tasks 5 (new JS), 7 (plane rework), 9 (big CSS deletion), and 11 (verification) — pause and let the user eyeball things before continuing. Everything else can flow through subagent+reviewer without interruption.
+
+## If something goes wrong
+
+- **Grep mismatch:** A task expects to find `.theme-switcher` at a line that no longer contains it. STOP. Re-grep with fresh line numbers and update the plan's line references, then resume.
+- **Subagent exceeds scope:** Reject its work, reset to `HEAD~1`, and dispatch again with a stricter prompt.
+- **A visual in Task 11 looks wrong in one theme:** Do not patch blindly. Identify which per-theme override (Task 4) is wrong and fix it there, so the fix lives in the right section.
+- **The user's earlier instructions conflict with the plan:** The user's explicit instructions always win. Pause and ask.
+
+## Known risks
+
+- Task 9 (CSS deletion) is the highest-risk task because line numbers shift after every deletion. The plan tells you to delete in reverse line order and re-grep between each — follow that discipline.
+- `js/plane.js` has attractor state scattered across several functions (constructor vars, `createToggleButton`, `toggle()`, possibly `pointerenter` handlers). Task 7 lists them, but grep for `attractor` and `plane-attractor-seen` one more time at the end of the task to make sure nothing lingers.
+- `page-transition.js` has both a `removeSelectors` list AND an `infraIds` list AND a button-skip check — all three must be updated (Task 8). Missing one will break the SPA navigation when leaving a project page.
+
+## Success criteria
+
+- All five themes show a visually distinct, pinned right-edge toolbar with five swatches + plane button
+- Clicking a swatch switches themes directly (not cycled)
+- Hover preview appears on desktop, not on mobile/touch
+- Plane button still toggles the plane overlay; active state is theme-coloured
+- Mobile ≤768px: toolbar shrinks, does not visibly collide with card hand
+- `role="toolbar"`, `aria-label`s on each swatch and the plane button, `aria-pressed` on plane button
+- Zero console errors on all four live pages
+- Cross-page SPA navigation still works (card drag from index → project page, theme still switchable there)
+- Zero remaining references to `.theme-switcher` / `.plane-toggle` / `themeSwitcher` / `plane-attractor-seen` in `css/`, `js/`, and live HTML (tests and `backup/` are out of scope)
+
+Good luck. Read the plan first, then invoke `superpowers:subagent-driven-development`, then dispatch Task 1.
diff --git a/docs/superpowers/plans/2026-04-11-right-edge-toolbar.md b/docs/superpowers/plans/2026-04-11-right-edge-toolbar.md
new file mode 100644
index 0000000..ac803a2
--- /dev/null
+++ b/docs/superpowers/plans/2026-04-11-right-edge-toolbar.md
@@ -0,0 +1,1058 @@
+# Right-Edge Toolbar 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:** Replace `.theme-switcher` (cycling button) and `.plane-toggle` (fixed button + attractor) with a single fixed right-edge `.toolbar` component containing direct-select theme swatches, a hover preview panel (desktop only), and a plane toggle, on all four live pages across all five themes.
+
+**Architecture:** One fixed-position toolbar element injected by JS into each page (mirrors the prototype DOM in `toolbar-variation-d.html`). Base + per-theme styles live in `css/shared.css`. Swatch click wires directly into the existing `setTheme(name)` logic in `js/shared.js`. Plane button reuses the existing `toggle()` function in `js/plane.js` (unchanged behaviour) but is now adopted into the toolbar DOM instead of being appended to `document.body` standalone. All old `.theme-switcher` / `.plane-toggle` CSS, HTML, and the attractor/bounce code are deleted.
+
+**Tech Stack:** Static HTML/CSS/JS, no build step. CSS custom properties + per-theme `[data-theme="..."]` blocks. `data-theme` lives on ``.
+
+---
+
+## Resolved Decisions (locked in before implementation)
+
+1. **Swatch dot colours:** use `--theme-*-accent` cross-theme constants in `:root` (Task 2).
+2. **Plane button icon:** use the paper-plane SVG only (rest state). Drop the crosshair, drop the `"Fly"/"Stop"` text label, and do NOT swap to a "stop" square when active. The active state is conveyed purely by CSS recolouring the same SVG via `.plane-btn.active` → `color: var(--accent)` (+ `background: var(--accent-soft)`), which picks up a theme-appropriate colour automatically.
+3. **Retro `var(--pop)` scope:** applies only to live toolbar chrome (border, box-shadow, active swatch ring, plane-btn active colour). The preview panel's stripe/name/chip colours continue to show the *target* theme's identity colours from the `PREVIEW` object in toolbar.js — they are not affected by the retro override.
+4. **Mobile:** shrink only (≤768px padding + swatch + icon size). No hide-on-drag rule. Revisit later if overlap becomes a visual issue.
+5. **`toolbar-variation-d.html`:** delete in the final task.
+6. **`lost-satellite copy for games.html`:** user has moved it into the backup folder — out of scope.
+
+---
+
+## File Structure
+
+**Modified:**
+- `css/shared.css` — add `.toolbar` base rules + per-theme overrides + responsive rules; delete all `.theme-switcher` and `.plane-toggle` rules (base and per-theme).
+- `js/shared.js` — replace the cycling `themeSwitcher` button handler with a `setTheme(name)` function callable from any swatch click. Keep the three theme-change hook calls.
+- `js/plane.js` — delete `startBounce/stopBounce/triggerBounce`, delete the attractor class + `sessionStorage('plane-attractor-seen')` logic, change `createToggleButton()` to build a `.plane-btn` element whose parent is the toolbar (not `document.body`), swap crosshair SVG for paper-plane SVG, drop text label. Keep everything else.
+- `js/analytics.js` — update selectors from `.plane-toggle` / `.theme-switcher` to `.plane-btn` / `.swatch-btn`.
+- `js/destruction.js` — update exclusion selector at L59 from `.theme-switcher, .plane-toggle` to `.toolbar`.
+- `js/page-transition.js` — update removeSelectors list (L39) and infraIds list (L73) and button-skip check (L81) from `.theme-switcher` / `.plane-toggle` to `.toolbar` / `#toolbar`.
+- `index.html`, `casino_games.html`, `my_games.html`, `eve_of_destruction.html` — delete the `
` line. The toolbar is injected by JS (single source of truth), so pages get no new markup.
+
+**Created:**
+- `js/toolbar.js` — new file. Owns toolbar DOM creation, swatch rendering, hover-preview panel, and plane-btn adoption. Loaded before `shared.js` and `plane.js` via a new `` BEFORE the existing `shared.js` script**
+
+Grep each file for `` immediately above the `shared.js` tag.
+
+- [ ] **Step 3: Repeat for all four pages**
+
+Apply identical changes to:
+- `index.html`
+- `casino_games.html`
+- `my_games.html`
+- `eve_of_destruction.html`
+
+Do NOT modify `lost-satellite copy for games.html` (backup file).
+
+- [ ] **Step 4: Final grep sweep — nothing should reference the old names**
+
+```
+Grep pattern: theme-switcher → expect only CLAUDE.md, the backup html, plans/*, and tests/* (audit each result)
+Grep pattern: plane-toggle → expect only tests/* + plans/*
+Grep pattern: themeSwitcher → expect only the backup html
+Grep pattern: id="themeSwitcher" → expect only the backup html
+```
+
+If any live file still references the old names, stop and fix.
+
+- [ ] **Step 5: Commit**
+
+```
+git add index.html casino_games.html my_games.html eve_of_destruction.html
+git commit -m "html: remove theme-switcher button, load toolbar.js"
+```
+
+---
+
+## Task 11: Verification — load + basic interaction
+
+**Prerequisite:** start the dev server yourself.
+
+- [ ] **Step 1: Ensure server is running**
+
+```bash
+curl -s -o /dev/null -w "%{http_code}" http://localhost:8080
+```
+
+If 000 / connection refused, start in the background:
+```bash
+npx http-server -p 8080 -c-1
+```
+
+- [ ] **Step 2: Manual smoke test via Playwright**
+
+```bash
+npx playwright-cli open http://localhost:8080/index.html
+npx playwright-cli eval "() => document.querySelectorAll('.toolbar .swatch-btn').length"
+# Expected: 5
+npx playwright-cli eval "() => document.documentElement.getAttribute('data-theme')"
+# Expected: whatever was in localStorage, default 'bold'
+```
+
+- [ ] **Step 3: Click each swatch in sequence, screenshot each**
+
+```bash
+for theme in bold brutalist retro cinematic neon; do
+ npx playwright-cli eval "() => window.setTheme('$theme')"
+ sleep 1
+ npx playwright-cli screenshot --filename=toolbar-$theme.png
+done
+```
+
+Open each screenshot visually. For each: toolbar pinned to right edge, vertically centred, five swatches visible, active swatch has accent ring, plane button visible below divider, theme-appropriate colours/radius/border.
+
+- [ ] **Step 4: Hover preview test (desktop)**
+
+```bash
+npx playwright-cli eval "() => { const b = document.querySelector('.toolbar .swatch-btn:not(.active)'); b.dispatchEvent(new MouseEvent('mouseover', { bubbles: true })); return document.querySelector('.toolbar .preview-panel').classList.contains('visible'); }"
+# Expected: true
+```
+
+- [ ] **Step 5: Plane button test**
+
+```bash
+npx playwright-cli eval "() => document.querySelector('.toolbar .plane-btn').click()"
+sleep 1
+npx playwright-cli eval "() => document.documentElement.classList.contains('plane-active')"
+# Expected: true
+npx playwright-cli screenshot --filename=toolbar-plane-active.png
+npx playwright-cli eval "() => document.querySelector('.toolbar .plane-btn').click()"
+npx playwright-cli eval "() => document.documentElement.classList.contains('plane-active')"
+# Expected: false
+```
+
+- [ ] **Step 6: Mobile viewport test**
+
+```bash
+# Playwright CLI doesn't set viewport on-the-fly; if needed write a tiny node script using
+# playwright directly. For now, use browser devtools emulation via page.setViewportSize in a
+# test file tests/toolbar-mobile-test.js if one is introduced.
+```
+
+For each mobile viewport (375x812, 812x375, 768x1024) manually:
+- Toolbar is visible, does not overlap the card hand
+- Swatch dots are smaller (18px)
+- Plane button icon is 18px
+
+- [ ] **Step 7: Reduced motion test**
+
+```bash
+# Emulate prefers-reduced-motion: reduce. Verify no transitions on toolbar/swatch/preview.
+# Manual check — inspect computed style for .toolbar and confirm transition is 'none'.
+```
+
+- [ ] **Step 8: Accessibility spot-check**
+
+```bash
+npx playwright-cli eval "() => ({
+ role: document.querySelector('.toolbar').getAttribute('role'),
+ label: document.querySelector('.toolbar').getAttribute('aria-label'),
+ swatchLabels: Array.from(document.querySelectorAll('.toolbar .swatch-btn')).map(b => b.getAttribute('aria-label')),
+ planeLabel: document.querySelector('.toolbar .plane-btn').getAttribute('aria-label'),
+ planePressed: document.querySelector('.toolbar .plane-btn').getAttribute('aria-pressed')
+})"
+```
+Expected: `role: "toolbar"`, `label: "Site controls"`, five labels of the form "Switch to Foo theme", plane label "Toggle paper plane overlay", plane pressed "true"/"false".
+
+- [ ] **Step 9: Keyboard navigation**
+
+Tab into the toolbar, confirm each swatch and the plane button receive focus in DOM order, Enter/Space activates each. If focus outlines are invisible in some themes, add `:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }` on `.toolbar .swatch-btn, .toolbar .plane-btn` in shared.css.
+
+- [ ] **Step 10: Cross-page test**
+
+Navigate from `index.html` via a card drag to `casino_games.html`. Confirm:
+- Toolbar is still visible and functional on project page
+- `setTheme('retro')` on project page switches theme without errors
+- Plane button on project page still toggles plane overlay
+- Page transition still runs (page-transition.js's toolbar exclusion works)
+
+Repeat from index → `my_games.html` and index → `eve_of_destruction.html`.
+
+- [ ] **Step 11: Console clean**
+
+Open devtools console on each page; zero errors or warnings (aside from pre-existing unrelated messages).
+
+- [ ] **Step 12: Delete screenshot files**
+
+```bash
+rm toolbar-*.png
+```
+
+- [ ] **Step 13: Commit if any follow-ups needed**
+
+Only if Step 9 or 11 turned up small CSS/JS fixes, commit them:
+```
+git add -p
+git commit -m "fix: toolbar verification follow-ups"
+```
+
+---
+
+## Task 12: Delete the prototype + update CLAUDE.md
+
+**Files:**
+- Delete: `toolbar-variation-d.html`
+- Modify: `CLAUDE.md`, `css/CLAUDE.md`, `js/CLAUDE.md`
+
+- [ ] **Step 1: Delete the prototype**
+
+```bash
+rm toolbar-variation-d.html
+```
+
+- [ ] **Step 2: Update root `CLAUDE.md`**
+
+Find the `- **Attractor (drag-me hint)**:` bullet — it describes the **card-hand** attractor and stays. But any reference to the **plane-toggle** attractor must be removed. Grep CLAUDE.md for `plane-attractor` / `plane-toggle` / `theme-switcher`.
+
+- [ ] **Step 3: Update `css/CLAUDE.md`**
+
+Remove table rows / bullets for:
+- `.theme-switcher` (line 53 reference)
+- `.plane-toggle` (line 83 reference)
+- Responsive `plane-toggle` mentions
+
+Add a one-line entry for the new `.toolbar` section.
+
+- [ ] **Step 4: Update `js/CLAUDE.md`**
+
+- Under `plane.js`: remove mentions of `startBounce/stopBounce/triggerBounce`, update "Toggle button UI" section, remove attractor-related lines.
+- Under `shared.js`: replace the cycling-handler description with the new `setTheme(name)` function signature and `window.setTheme` export.
+- Add a new `toolbar.js` section in the dependency graph and section map with constants table + API (`window.Toolbar.syncActive`).
+
+- [ ] **Step 5: Commit**
+
+```
+git add CLAUDE.md css/CLAUDE.md js/CLAUDE.md
+git rm toolbar-variation-d.html
+git commit -m "docs: update CLAUDE.md for toolbar; remove prototype"
+```
+
+---
+
+## Self-Review Checklist (run before handing off)
+
+- [ ] Every spec requirement mapped to a task? (HTML ✓ T10, CSS base ✓ T3, CSS per-theme ✓ T4, CSS deletion ✓ T9, JS direct-select ✓ T5/T6, JS hover-preview desktop-only ✓ T5, JS plane unchanged ✓ T7 retains toggle(), attractor retired ✓ T7, mobile ✓ T3 Step 1 responsive block, a11y ✓ T5 + T11 Step 8, grep sweep ✓ T1 + T10 Step 4, verification ✓ T11)
+- [ ] No placeholders / TODO / "similar to" without code
+- [ ] File paths exact, line numbers verified against current shared.css (≈2576 lines) — if the file has shifted since this plan was written, re-grep
+- [ ] Type/name consistency: `window.setTheme`, `window.Toolbar.syncActive`, `#toolbar`, `#toolbar-plane-slot`, `.plane-btn`, `.swatch-btn`, `data-theme-id` — all used consistently across tasks
+- [ ] Grep pre-check done in T1, grep post-check done in T9 Step 10 and T10 Step 4
+- [ ] Don't touch: card-hand attractor, plane overlay logic, destruction system internals, text-rearrange
+- [ ] "No hardcoded hex in element CSS" respected via `--theme-*-accent` constants (T2). The PREVIEW object in toolbar.js is JS data, not element CSS; the prototype pattern is preserved but flagged in T5 comment.
diff --git a/eve_of_destruction.html b/eve_of_destruction.html
index 5122525..95e2029 100644
--- a/eve_of_destruction.html
+++ b/eve_of_destruction.html
@@ -20,7 +20,8 @@
@@ -40,9 +41,6 @@
-
-
-
@@ -195,6 +193,7 @@
Movement
+
diff --git a/index.html b/index.html
index c700902..4f24d37 100644
--- a/index.html
+++ b/index.html
@@ -20,7 +20,8 @@
@@ -42,9 +43,6 @@
-
-
-
STATUS: OPEN TO OPPORTUNITIES
@@ -276,6 +274,7 @@
GET IN TOUCH
+
diff --git a/js/CLAUDE.md b/js/CLAUDE.md
index 1eceae1..48ed1d8 100644
--- a/js/CLAUDE.md
+++ b/js/CLAUDE.md
@@ -5,7 +5,13 @@ Overview: 6 JS files (~3,100 lines). No build step, no modules — all globals.
## Dependency Graph
```
-shared.js (theme switcher)
+toolbar.js (DOM: swatches, plane slot, hover preview)
+ ├─ wires → window.setTheme(t) on swatch click
+ └─ exports → window.Toolbar.syncActive(t)
+
+shared.js (setTheme(name) — direct theme switch)
+ ├─ called by → toolbar.js swatch click
+ ├─ fires → window.Toolbar.syncActive(name)
├─ fires → TextDestruction.onThemeChange()
├─ fires → window._planeOnThemeChange()
└─ fires → window._cardHandOnThemeChange()
@@ -21,25 +27,53 @@ External: GSAP core, SplitText, Physics2DPlugin, Three.js r128, GLTFLoader
---
-## shared.js (37 lines)
+## shared.js (27 lines)
### Complete Reference
| Section | Lines | Notes |
|---------|-------|-------|
-| Scroll reveal (IntersectionObserver) | 1-11 | threshold 0.1, rootMargin '0px 0px -40px 0px', staggered 60ms delay, unobserves after visible |
-| Theme switcher setup | 14-17 | themes = ['bold','brutalist','retro','cinematic','neon'], reads data-theme attr |
-| updateLabel() | 21-25 | Shows NEXT theme name on button |
-| Click handler | 27-35 | Cycles index, sets data-theme, persists to localStorage('portfolio-theme'), fires 3 callbacks |
+| Scroll reveal (IntersectionObserver) | 1-12 | threshold 0.1, rootMargin '0px 0px -40px 0px', staggered 60ms delay, unobserves after visible |
+| Theme list | 15 | themes = ['bold','brutalist','retro','cinematic','neon'] |
+| setTheme(name) | 17-26 | Validates name, sets data-theme, persists to localStorage('portfolio-theme'), fires Toolbar.syncActive + 3 theme-change callbacks |
+| window.setTheme export | 27 | Global accessor invoked by toolbar.js swatch clicks |
### Global Exports
-- `window.revealObserver` (line 10) — IntersectionObserver instance
+- `window.revealObserver` (line 11) — IntersectionObserver instance
+- `window.setTheme(name)` (line 27) — direct theme switch; no cycling logic, name must be in `themes` array
+
+### Theme Change Hooks Fired (in order, inside setTheme)
+1. `window.Toolbar.syncActive(name)` — updates swatch `.active` class + aria-pressed (null-checked)
+2. `window.TextDestruction.onThemeChange()` (null-checked)
+3. `window._planeOnThemeChange()` (null-checked)
+4. `window._cardHandOnThemeChange()` (null-checked)
+
+---
-### Theme Change Hooks Fired (in order)
-1. `window.TextDestruction.onThemeChange()` (line 32)
-2. `window._planeOnThemeChange()` (line 33)
-3. `window._cardHandOnThemeChange()` (line 34)
+## toolbar.js (147 lines)
+
+### Purpose
+Builds the fixed right-edge `.toolbar` element injected into `document.body` on every live page. Contains five theme swatch buttons (direct-select, not cycling), a divider, a `#toolbar-plane-slot` marker div that plane.js adopts its button into, and a hover preview panel (desktop-only via `(hover: hover) and (pointer: fine)` media query).
+
+### Constants
+| Name | Value | Notes |
+|------|-------|-------|
+| `THEMES` | `['bold','brutalist','retro','cinematic','neon']` | Rendered in this order top-to-bottom |
+| `LABELS` | `{bold: 'Bold', ...}` | aria-label + preview panel name |
+| `PREVIEW` | Per-theme `{accent, bg, text, sub, chips[4]}` | Inline hex values for preview panel chips — mirrors the `--theme-*-accent` CSS constants (keep in sync if CSS changes) |
+
+### Functions
+| Function | Notes |
+|----------|-------|
+| `buildToolbar()` | Creates `#toolbar` with role=toolbar, swatches, divider, plane slot, preview panel; wires click + hover; appends to body |
+| `wireSwatchClicks(swatches)` | Delegates click on `.swatch-btn`, reads `data-theme-id`, calls `window.setTheme(t)` |
+| `wireHoverPreview(...)` | Desktop-only — updates preview panel content on mouseover of non-active swatches, hides on mouseleave |
+| `syncActive(name)` | Toggles `.active` class + `aria-pressed` on swatch buttons; hides preview — exported as `window.Toolbar.syncActive` |
+
+### Global Exports
+- `window.Toolbar = { syncActive }` — called by `setTheme()` in shared.js
-These are called without null checks — would throw if undefined and script loads before the others.
+### Load Order Requirement
+Must be loaded **before** `shared.js` (so `window.Toolbar.syncActive` exists when setTheme first fires) **and before** `plane.js` (so `#toolbar-plane-slot` exists when `createToggleButton()` runs). All four live HTML pages load scripts in this order: `toolbar.js` → `shared.js` → `plane.js`.
---
@@ -112,7 +146,7 @@ GSAP core, SplitText plugin, Physics2DPlugin (all CDN, no npm)
| Mobile detection | 29-35 | isMobile() with _isMobileCached |
| State variables | 37-80 | enabled, scene/camera/renderer, plane model, mouse/touch, projectiles[], fire interval |
| Iframe avoidance | 82-125 | getAvoidanceTarget() keeps plane outside iframe buffered regions |
-| Toggle button UI | 127-214 | createToggleButton(), updateButtonLabel(), attractor bounce |
+| Toggle button UI | ~125-161 | `planeIcon` SVG (paper plane), `createToggleButton()` builds `.plane-btn` with aria-label + aria-pressed, adopts into `#toolbar-plane-slot` (body fallback). `updateButtonLabel()` toggles `.active` + aria-pressed on state change. No icon swap — active state is pure CSS recolor via `.plane-btn.active { color: var(--accent); background: var(--accent-soft); }` |
| Toggle handler | 216-247 | SessionStorage persist, syncs TextDestruction.init/destroy, plane-active class |
| start() / stop() | 249-271 | Control animation loop + canvas visibility |
| Dynamic script loading | 273-298 | CDN Three.js r128 + GLTFLoader |
diff --git a/js/analytics.js b/js/analytics.js
index 6ab6066..44c52fc 100644
--- a/js/analytics.js
+++ b/js/analytics.js
@@ -8,7 +8,7 @@
// 1. Plane mode toggle. The state read is deferred so plane.js's own click
// handler runs first and flips the `plane-active` class on .
document.addEventListener('click', function (e) {
- if (!e.target.closest('.plane-toggle')) return;
+ if (!e.target.closest('.toolbar .plane-btn')) return;
setTimeout(function () {
var on = document.documentElement.classList.contains('plane-active');
gtag('event', 'plane_mode_toggle', { state: on ? 'on' : 'off' });
@@ -17,7 +17,7 @@
// 2. Theme switch. Same deferral so we read the theme after shared.js sets it.
document.addEventListener('click', function (e) {
- if (!e.target.closest('.theme-switcher')) return;
+ if (!e.target.closest('.toolbar .swatch-btn')) return;
setTimeout(function () {
var theme = document.documentElement.getAttribute('data-theme') || 'unknown';
gtag('event', 'theme_change', { theme: theme });
diff --git a/js/destruction.js b/js/destruction.js
index 3fe838a..74fdb3d 100644
--- a/js/destruction.js
+++ b/js/destruction.js
@@ -56,7 +56,7 @@ function splitAllText() {
var elements = document.querySelectorAll(DESTRUCTIBLE_SELECTOR);
elements.forEach(function(el) {
- if (el.closest('.theme-switcher, .plane-toggle, .btn, .work-expand')) return;
+ if (el.closest('.toolbar, .btn, .work-expand')) return;
if (el.offsetParent === null) return;
var instance = SplitText.create(el, {
diff --git a/js/page-transition.js b/js/page-transition.js
index a085f81..476fd4f 100644
--- a/js/page-transition.js
+++ b/js/page-transition.js
@@ -36,7 +36,7 @@ function extractPageContent(doc) {
var clone = body.cloneNode(true);
var removeSelectors = [
- 'nav', '.theme-switcher', 'footer', 'script',
+ 'nav', '.toolbar', 'footer', 'script',
'#plane-canvas', '.blur-overlay', '.project-nav',
'#handContainer', '#dragBlurOverlay', '#pageContainer',
'#perspectiveContainer', '#flyOverlay'
@@ -70,7 +70,7 @@ function bootstrapFromProjectPage() {
// Collect project content nodes (everything that isn't SPA infrastructure)
var infraIds = ['plane-canvas', 'dragBlurOverlay', 'pageContainer',
'handContainer', 'perspectiveContainer', 'flyOverlay',
- 'themeSwitcher', 'statusBar'];
+ 'toolbar', 'statusBar'];
var body = document.body;
var nodesToMove = [];
@@ -78,7 +78,7 @@ function bootstrapFromProjectPage() {
var child = body.children[i];
if (child.tagName === 'SCRIPT') continue;
if (child.id && infraIds.indexOf(child.id) !== -1) continue;
- if (child.tagName === 'BUTTON' && (child.classList.contains('theme-switcher') || child.classList.contains('plane-toggle'))) continue;
+ if (child.id === 'toolbar') continue;
nodesToMove.push(child);
}
diff --git a/js/plane.js b/js/plane.js
index 750de32..2e8367d 100644
--- a/js/plane.js
+++ b/js/plane.js
@@ -124,91 +124,40 @@
var canvasContainer = document.getElementById('plane-canvas');
var toggleBtn = null;
- var crosshairIcon = null;
- var labelSpan = null;
- var bounceInterval = null;
- var bounceTimeout = null;
- var bounceDelayId = null;
-
- function startBounce() {
- stopBounce();
- triggerBounce();
- bounceInterval = setInterval(triggerBounce, 3000);
- }
- function triggerBounce() {
- toggleBtn.classList.add('bouncing');
- bounceTimeout = setTimeout(function() {
- toggleBtn.classList.remove('bouncing');
- }, 1200);
- }
-
- function stopBounce() {
- if (bounceDelayId) clearTimeout(bounceDelayId);
- if (bounceInterval) clearInterval(bounceInterval);
- if (bounceTimeout) clearTimeout(bounceTimeout);
- bounceDelayId = null;
- bounceInterval = null;
- bounceTimeout = null;
- if (toggleBtn) toggleBtn.classList.remove('bouncing');
- }
+ var planeIcon =
+ '
';
// --- Toggle Button ---
function createToggleButton() {
toggleBtn = document.createElement('button');
- toggleBtn.className = 'plane-toggle';
- toggleBtn.innerHTML =
- '
' +
- '
';
-
- crosshairIcon = toggleBtn.querySelector('.plane-toggle-icon');
- labelSpan = toggleBtn.querySelector('.plane-toggle-label');
-
- if (enabled) {
- crosshairIcon.style.display = 'none';
- labelSpan.textContent = 'Stop';
- } else {
- labelSpan.textContent = 'Fly';
- if (!sessionStorage.getItem('plane-attractor-seen')) {
- toggleBtn.classList.add('attractor');
- bounceDelayId = setTimeout(function() { startBounce(); }, 1500);
- }
- }
-
- // If nav is already scroll-hidden (e.g. page reloaded while scrolled), start hidden
- var nav = document.querySelector('nav');
- if (nav && nav.classList.contains('scroll-hidden')) {
- toggleBtn.classList.add('scroll-hidden');
- }
+ toggleBtn.type = 'button';
+ toggleBtn.className = 'plane-btn';
+ toggleBtn.setAttribute('aria-label', 'Toggle paper plane overlay');
+ toggleBtn.setAttribute('aria-pressed', enabled ? 'true' : 'false');
+ toggleBtn.innerHTML = planeIcon;
+ if (enabled) toggleBtn.classList.add('active');
toggleBtn.addEventListener('click', toggle);
- toggleBtn.addEventListener('pointerenter', function() {
- if (toggleBtn.classList.contains('attractor')) {
- stopBounce();
- toggleBtn.classList.remove('attractor');
- sessionStorage.setItem('plane-attractor-seen', '1');
- }
- });
-
- document.body.appendChild(toggleBtn);
+ // Adopt into toolbar slot (toolbar.js runs first). Fallback: append to body.
+ var slot = document.getElementById('toolbar-plane-slot');
+ if (slot) {
+ slot.appendChild(toggleBtn);
+ } else {
+ document.body.appendChild(toggleBtn);
+ }
}
function updateButtonLabel() {
if (!toggleBtn) return;
- if (enabled) {
- crosshairIcon.style.display = 'none';
- labelSpan.textContent = 'Stop';
- } else {
- crosshairIcon.style.display = '';
- labelSpan.textContent = 'Fly';
- }
+ toggleBtn.classList.toggle('active', enabled);
+ toggleBtn.setAttribute('aria-pressed', enabled ? 'true' : 'false');
}
function toggle() {
@@ -217,8 +166,6 @@
updateButtonLabel();
if (enabled) {
- toggleBtn.classList.remove('attractor');
- stopBounce();
document.documentElement.classList.add('plane-active');
if (!initialized) {
init();
@@ -438,7 +385,7 @@
function onMobileMouseDown(e) {
if (!running || !planeGroup || !planeGroup.visible) return;
if (e.button !== 0) return;
- if (e.target.closest('.plane-toggle, nav, .theme-switcher')) return;
+ if (e.target.closest('.toolbar')) return;
var planeScreen = worldToScreen(planeGroup.position.x, planeGroup.position.z);
var dx = e.clientX - planeScreen.x;
@@ -518,7 +465,7 @@
function onMouseDown(e) {
if (!running || !planeGroup || !planeGroup.visible) return;
if (e.button !== 0) return;
- if (e.target.closest('.plane-toggle, nav, .theme-switcher')) return;
+ if (e.target.closest('.toolbar')) return;
fireProjectiles();
clearInterval(fireIntervalId);
fireIntervalId = setInterval(fireProjectiles, FIRE_INTERVAL * 1000);
diff --git a/js/shared.js b/js/shared.js
index 39c9f97..dfcd8ea 100644
--- a/js/shared.js
+++ b/js/shared.js
@@ -11,27 +11,17 @@ const observer = new IntersectionObserver((entries) => {
window.revealObserver = observer;
reveals.forEach(el => observer.observe(el));
-// Theme switcher
-const themeSwitcher = document.getElementById('themeSwitcher');
+// Theme switcher (direct-select via toolbar swatches)
const themes = ['bold', 'brutalist', 'retro', 'cinematic', 'neon'];
-let currentIndex = themes.indexOf(document.documentElement.getAttribute('data-theme') || 'bold');
-if (currentIndex === -1) currentIndex = 0;
-
-// Update button label to show the *other* theme
-const updateLabel = () => {
- const next = themes[(currentIndex + 1) % themes.length];
- themeSwitcher.textContent = next.charAt(0).toUpperCase() + next.slice(1);
-};
-updateLabel();
-
-themeSwitcher.addEventListener('click', () => {
- currentIndex = (currentIndex + 1) % themes.length;
- document.documentElement.setAttribute('data-theme', themes[currentIndex]);
- localStorage.setItem('portfolio-theme', themes[currentIndex]);
- updateLabel();
+function setTheme(name) {
+ if (themes.indexOf(name) === -1) return;
+ document.documentElement.setAttribute('data-theme', name);
+ localStorage.setItem('portfolio-theme', name);
+ if (window.Toolbar && window.Toolbar.syncActive) window.Toolbar.syncActive(name);
if (window.TextDestruction) TextDestruction.onThemeChange();
if (window._planeOnThemeChange) window._planeOnThemeChange();
if (window._cardHandOnThemeChange) window._cardHandOnThemeChange();
-});
+}
+window.setTheme = setTheme;
diff --git a/js/toolbar.js b/js/toolbar.js
new file mode 100644
index 0000000..2e2a62e
--- /dev/null
+++ b/js/toolbar.js
@@ -0,0 +1,152 @@
+// js/toolbar.js — right-edge toolbar with theme swatches + plane button slot
+(function () {
+ 'use strict';
+
+ // Desktop default is brutalist, mobile default is bold — default theme sits first in the bar.
+ var IS_MOBILE = window.matchMedia('(max-width: 768px)').matches;
+ var THEMES = IS_MOBILE
+ ? ['bold', 'brutalist', 'retro', 'cinematic', 'neon']
+ : ['brutalist', 'bold', 'retro', 'cinematic', 'neon'];
+ var LABELS = {
+ bold: 'Bold',
+ brutalist: 'Brutalist',
+ retro: 'Retro',
+ cinematic: 'Cinematic',
+ neon: 'Neon'
+ };
+
+ // Preview panel content (uses the cross-theme accent constants declared in :root).
+ // Hex values mirror the CSS vars; kept in JS purely for the preview chips since we
+ // render them inline for speed. NOTE: if you change a --theme-*-accent in shared.css,
+ // update the matching entry here.
+ var PREVIEW = {
+ bold: { accent: '#ff3d00', bg: '#f4f4f4', text: '#111', sub: '#555', chips: ['#ff3d00', '#111', '#f4f4f4'] },
+ brutalist: { accent: '#0000ff', bg: '#fff', text: '#000', sub: '#333', chips: ['#0000ff', '#ff0000', '#000', '#fff'] },
+ retro: { accent: '#1A5C52', bg: '#FDF6EC', text: '#1A1A16', sub: '#6D6A5E', chips: ['#1A5C52', '#D94230', '#1A1A16', '#FDF6EC'] },
+ cinematic: { accent: '#D6001C', bg: '#0A0A0A', text: '#E8E6E1', sub: '#9A9590', chips: ['#D6001C', '#E8E6E1', '#9A9590', '#0A0A0A'] },
+ neon: { accent: '#c9f059', bg: '#0a0a0c', text: '#e8e6e3', sub: '#8a8a99', chips: ['#c9f059', '#e8e6e3', '#0a0a0c'] }
+ };
+
+ function buildToolbar() {
+ var current = document.documentElement.getAttribute('data-theme') || 'bold';
+
+ var toolbar = document.createElement('div');
+ toolbar.className = 'toolbar';
+ toolbar.id = 'toolbar';
+ toolbar.setAttribute('role', 'toolbar');
+ toolbar.setAttribute('aria-label', 'Site controls');
+
+ var swatches = document.createElement('div');
+ swatches.className = 'swatches';
+ toolbar.appendChild(swatches);
+
+ THEMES.forEach(function (t) {
+ var btn = document.createElement('button');
+ btn.type = 'button';
+ btn.className = 'swatch-btn' + (t === current ? ' active' : '');
+ btn.setAttribute('data-theme-id', t);
+ btn.setAttribute('aria-label', 'Switch to ' + LABELS[t] + ' theme');
+ btn.setAttribute('aria-pressed', t === current ? 'true' : 'false');
+ var dot = document.createElement('span');
+ dot.className = 'swatch-dot';
+ btn.appendChild(dot);
+ swatches.appendChild(btn);
+ });
+
+ var divider = document.createElement('div');
+ divider.className = 'divider';
+ toolbar.appendChild(divider);
+
+ // Plane button is injected here by plane.js — leave a marker slot.
+ var slot = document.createElement('div');
+ slot.id = 'toolbar-plane-slot';
+ toolbar.appendChild(slot);
+
+ // Preview panel (hover, desktop only)
+ var preview = document.createElement('div');
+ preview.className = 'preview-panel';
+ preview.id = 'toolbar-preview';
+ preview.innerHTML =
+ '
' +
+ '
' +
+ '
Theme preview
' +
+ '
';
+ toolbar.appendChild(preview);
+
+ document.body.appendChild(toolbar);
+
+ wireSwatchClicks(swatches);
+ wireHoverPreview(toolbar, swatches, preview);
+
+ return toolbar;
+ }
+
+ function wireSwatchClicks(swatches) {
+ swatches.addEventListener('click', function (e) {
+ var btn = e.target.closest('.swatch-btn');
+ if (!btn) return;
+ var t = btn.getAttribute('data-theme-id');
+ if (!t) return;
+ if (typeof window.setTheme === 'function') {
+ window.setTheme(t);
+ }
+ });
+ }
+
+ function wireHoverPreview(toolbar, swatches, preview) {
+ // Only run hover wiring on devices with true hover + fine pointer
+ var mq = window.matchMedia('(hover: hover) and (pointer: fine)');
+ if (!mq.matches) return;
+
+ swatches.addEventListener('mouseover', function (e) {
+ var btn = e.target.closest('.swatch-btn');
+ if (!btn) return;
+ if (btn.classList.contains('active')) {
+ preview.classList.remove('visible');
+ return;
+ }
+ var t = btn.getAttribute('data-theme-id');
+ var p = PREVIEW[t];
+ if (!p) return;
+ preview.querySelector('.preview-stripe').style.background = p.accent;
+ var nameEl = preview.querySelector('.preview-name');
+ nameEl.textContent = LABELS[t];
+ nameEl.style.color = p.text;
+ preview.querySelector('.preview-sub').style.color = p.sub;
+ preview.style.background = p.bg;
+ preview.querySelector('.preview-chips').innerHTML =
+ p.chips.map(function (c) {
+ return '
';
+ }).join('');
+ var tr = toolbar.getBoundingClientRect();
+ var br = btn.getBoundingClientRect();
+ preview.style.top = Math.max(4, br.top - tr.top + btn.offsetHeight / 2 - 40) + 'px';
+ preview.classList.add('visible');
+ });
+
+ swatches.addEventListener('mouseleave', function () {
+ preview.classList.remove('visible');
+ });
+ }
+
+ // Called by shared.js after it changes data-theme
+ function syncActive(themeName) {
+ document.querySelectorAll('#toolbar .swatch-btn').forEach(function (b) {
+ var on = b.getAttribute('data-theme-id') === themeName;
+ b.classList.toggle('active', on);
+ b.setAttribute('aria-pressed', on ? 'true' : 'false');
+ });
+ // Hide preview if it was showing the now-active theme
+ var preview = document.getElementById('toolbar-preview');
+ if (preview) preview.classList.remove('visible');
+ }
+
+ // Build immediately if body exists, else wait
+ if (document.body) {
+ buildToolbar();
+ } else {
+ document.addEventListener('DOMContentLoaded', buildToolbar);
+ }
+
+ window.Toolbar = { syncActive: syncActive };
+})();
\ No newline at end of file
diff --git a/my_games.html b/my_games.html
index cb79603..0463994 100644
--- a/my_games.html
+++ b/my_games.html
@@ -20,7 +20,8 @@
@@ -40,9 +41,6 @@
-
-
-
@@ -204,6 +202,7 @@
Calm Before the Storm
+