diff --git a/docs/superpowers/specs/2026-05-08-js-module-split-design.md b/docs/superpowers/specs/2026-05-08-js-module-split-design.md
new file mode 100644
index 0000000..7c86512
--- /dev/null
+++ b/docs/superpowers/specs/2026-05-08-js-module-split-design.md
@@ -0,0 +1,116 @@
+# JS Module Split Design
+
+## Goal
+
+Split `public/app.js` (1025 lines, single file) into ES modules under `public/js/` — no build step, no npm. Modernize `var` to `const`/`let` in the process.
+
+## Module Structure
+
+```
+public/js/
+ state.js shared mutable state object
+ i18n.js translations, locale logic
+ api.js fetch wrapper, loadGoals(), saveGoal()
+ goals.js pure goal calculations (calc, tOff, dcls, …)
+ ui.js tpl(), showToast(), overlay helpers, updateHeader()
+ auth.js login/register/password screens
+ sheets.js openNew(), openData(), openAdmin()
+ render.js buildCard(), render(), wire()
+ app.js entry point: init, URL params, stopwatch, midnight timer
+```
+
+## State Management
+
+Single exported object in `state.js`. All modules import and mutate it directly:
+
+```js
+// state.js
+export const state = {
+ goals: [],
+ userName: '',
+ isAdmin: false,
+ prefs: {},
+ selDay: {},
+ addAmt: {},
+ renamingId: null,
+ renameVal: '',
+ collapsed: {},
+ TODAY: new Date(),
+};
+```
+
+Replaces all global `var` declarations at the top of `app.js`. Mutations like `goals = data` become `state.goals = data`.
+
+## Dependency Graph
+
+```
+state.js ← all modules
+i18n.js ← goals.js, ui.js, auth.js, sheets.js, render.js, app.js
+api.js ← auth.js, sheets.js, app.js
+goals.js ← render.js, sheets.js
+ui.js ← auth.js, sheets.js, render.js
+auth.js ← sheets.js, app.js
+render.js ← app.js
+```
+
+## Circular Import Solution
+
+`api.js` previously called `showLogin()` on 401 — creating a circular dependency with `auth.js`. Resolved via a Custom Event:
+
+```js
+// api.js — on 401:
+document.dispatchEvent(new CustomEvent('session-expired'));
+
+// app.js — on startup:
+document.addEventListener('session-expired', () => showLogin());
+```
+
+Events are used only for this cross-cutting concern. All other communication is via direct imports.
+
+## const/let Conversion
+
+- ES modules run in strict mode automatically
+- `var` → `const` where the binding is never reassigned
+- `var` → `let` for loop counters and mutated locals
+- State re-assignments (`goals = newArray`) → `state.goals = newArray`
+- `TODAY` moves into `state.TODAY`, updated in `scheduleMidnight()` and `visibilitychange`
+
+## HTML Change
+
+`templates/app.html.twig`:
+
+```html
+
+
+
+
+
+```
+
+`public/app.js` is deleted after the migration is complete.
+
+## File Size Estimates
+
+| File | Est. lines |
+|---|---|
+| state.js | ~15 |
+| i18n.js | ~185 |
+| api.js | ~35 |
+| goals.js | ~60 |
+| ui.js | ~35 |
+| auth.js | ~130 |
+| sheets.js | ~135 |
+| render.js | ~185 |
+| app.js | ~60 |
+| **Total** | **~840** |
+
+(Reduction due to removed comments and tighter const/let style.)
+
+## Migration Strategy
+
+One module at a time, keeping the app functional throughout:
+
+1. Create `public/js/` directory
+2. Write modules in dependency order: state → i18n → api → goals → ui → auth → sheets → render → app
+3. Switch HTML to `