`
-```
-
-- [ ] **Step 4: meals.js — Empty State ergänzen (fehlte bisher)**
-
-In `public/pages/meals.js`, in der Funktion die den Tages-Slot rendert, wenn keine Mahlzeit vorhanden:
-
-```js
-// Wenn kein Meal für diesen Slot: Leeren Slot mit Empty-State rendern
-if (!meal) {
- return `
-
-`;
-```
-
-- [ ] **Step 7: Modul-eigene Empty-State-CSS-Klassen entfernen**
-
-In den folgenden CSS-Dateien die modul-eigenen Klassen entfernen (werden durch `.empty-state` aus `layout.css` ersetzt):
-- `public/styles/tasks.css`: `.tasks-empty`, `.tasks-empty__icon`, `.tasks-empty__title`, `.tasks-empty__desc`
-- `public/styles/shopping.css`: `.shopping-empty`, `.shopping-empty__icon`, `.shopping-empty__title`, `.shopping-empty__desc`
-- `public/styles/notes.css`: `.notes-empty`, `.notes-empty__icon`
-- `public/styles/contacts.css`: `.contacts-empty`
-- `public/styles/budget.css`: `.budget-empty`
-
-- [ ] **Step 8: Visuell prüfen**
-
-Alle sechs Module ohne Einträge öffnen. Empty States sollen einheitlich aussehen (zentriert, Icon, Titel, Beschreibung). Meals soll leere Slots kompakt darstellen.
-
-- [ ] **Step 9: Commit**
-
-```bash
-git add public/pages/tasks.js public/pages/shopping.js public/pages/notes.js public/pages/meals.js public/pages/contacts.js public/pages/budget.js public/styles/tasks.css public/styles/shopping.css public/styles/notes.css public/styles/contacts.css public/styles/budget.css public/styles/layout.css
-git commit -m "style: unify all empty states to shared .empty-state class across all modules"
-```
-
----
-
-### Task 7: Swipe-Reveal proportionale Opacity (Tasks)
-
-**Files:**
-- Modify: `public/pages/tasks.js`
-
-- [ ] **Step 1: wireSwipeGestures() in tasks.js finden**
-
-```bash
-grep -n "wireSwipeGestures\|swipe-reveal" public/pages/tasks.js | head -15
-```
-
-- [ ] **Step 2: Reveal-Opacity proportional zur Swipe-Distanz machen**
-
-In `public/pages/tasks.js`, in der `wireSwipeGestures()`-Funktion (ca. ab Zeile 680), im `touchmove`-Event-Handler:
-
-Den Block, der die swipe-reveal-Elemente steuert, ergänzen:
-
-```js
-// Proportionale Opacity des Reveal-Bereichs
-const doneReveal = row.querySelector('.swipe-reveal--done');
-const editReveal = row.querySelector('.swipe-reveal--edit');
-const progress = Math.min(Math.abs(deltaX) / SWIPE_THRESHOLD, 1);
-
-if (deltaX < 0 && doneReveal) {
- doneReveal.style.opacity = String(progress);
-} else if (deltaX > 0 && editReveal) {
- editReveal.style.opacity = String(progress);
-}
-```
-
-Im `touchend`-Handler: Opacity zurücksetzen:
-
-```js
-if (doneReveal) doneReveal.style.opacity = '';
-if (editReveal) editReveal.style.opacity = '';
-```
-
-- [ ] **Step 3: Visuell prüfen (mobiler Viewport)**
-
-Im Browser DevTools auf mobilen Viewport wechseln. Eine Aufgabe nach links wischen. Der grüne Bereich soll proportional zur Wischweite einblenden, nicht abrupt erscheinen.
-
-- [ ] **Step 4: Commit**
-
-```bash
-git add public/pages/tasks.js
-git commit -m "feat: proportional opacity on swipe-reveal action areas"
-```
-
----
-
-## Schicht 3 — Mobile PWA & Natives Gefühl
-
-### Task 8: Install-Prompt Timing und Dismiss-Dauer anpassen
-
-**Files:**
-- Modify: `public/components/oikos-install-prompt.js`
-
-- [ ] **Step 1: Dismiss-Dauer von 30d auf 7d ändern**
-
-In `public/components/oikos-install-prompt.js`, Zeile 14:
-
-```js
-// Vorher
-const DISMISS_DURATION_MS = 30 * 24 * 60 * 60 * 1000; // 30 Tage
-
-// Nachher
-const DISMISS_DURATION_MS = 7 * 24 * 60 * 60 * 1000; // 7 Tage
-```
-
-- [ ] **Step 2: Interaktions-Zähler einführen**
-
-Vor der Klassen-Definition:
-
-```js
-const INTERACTION_KEY = 'oikos-install-interactions';
-const INTERACTION_THRESHOLD = 2; // Anzahl Interaktionen vor Prompt
-```
-
-- [ ] **Step 3: connectedCallback anpassen — erst nach Interaktionen zeigen**
-
-Die `connectedCallback()`-Methode um Interaktions-Check erweitern:
-
-```js
-connectedCallback() {
- if (
- window.matchMedia('(display-mode: standalone)').matches ||
- navigator.standalone === true
- ) return;
-
- const dismissed = localStorage.getItem(DISMISS_KEY);
- if (dismissed && Date.now() - Number(dismissed) < DISMISS_DURATION_MS) return;
-
- // Noch nicht genug Interaktionen
- const interactions = Number(localStorage.getItem(INTERACTION_KEY) || '0');
- if (interactions < INTERACTION_THRESHOLD) {
- this._waitForInteractions();
- return;
- }
-
- if (this._isIOS()) {
- this._showIOSPrompt();
- } else {
- this._listenForInstallPrompt();
- }
-}
-```
-
-- [ ] **Step 4: `_waitForInteractions()` implementieren**
-
-```js
-_waitForInteractions() {
- const onInteraction = () => {
- const count = Number(localStorage.getItem(INTERACTION_KEY) || '0') + 1;
- localStorage.setItem(INTERACTION_KEY, String(count));
-
- if (count >= INTERACTION_THRESHOLD) {
- document.removeEventListener('click', onInteraction);
- if (this._isIOS()) {
- this._showIOSPrompt();
- } else {
- this._listenForInstallPrompt();
- }
- }
- };
- document.addEventListener('click', onInteraction);
- this._offInteraction = () => document.removeEventListener('click', onInteraction);
-}
-```
-
-- [ ] **Step 5: disconnectedCallback cleanup**
-
-```js
-disconnectedCallback() {
- window.removeEventListener('beforeinstallprompt', this._onBeforeInstall);
- if (this._offInteraction) this._offInteraction();
-}
-```
-
-- [ ] **Step 6: Visuell prüfen**
-
-`localStorage.removeItem('oikos-install-interactions')` in DevTools Console ausführen. App neu laden. Banner soll NICHT sofort erscheinen. Zweimal auf Buttons klicken → Banner soll einblenden.
-
-- [ ] **Step 7: Commit**
-
-```bash
-git add public/components/oikos-install-prompt.js
-git commit -m "feat: delay install prompt until 2 interactions, reduce dismiss to 7 days"
-```
-
----
-
-### Task 9: Virtual-Keyboard scrollIntoView in modal.js
-
-**Files:**
-- Modify: `public/components/modal.js`
-
-- [ ] **Step 1: scrollIntoView-Listener in trapFocus() hinzufügen**
-
-In `public/components/modal.js`, in der `trapFocus()`-Funktion nach dem `container.addEventListener('keydown', ...)`:
-
-```js
-// Virtual Keyboard: Focused Input in sichtbaren Bereich scrollen
-function onInputFocus(e) {
- const tag = e.target.tagName;
- if (tag !== 'INPUT' && tag !== 'TEXTAREA' && tag !== 'SELECT') return;
- setTimeout(() => {
- e.target.scrollIntoView({ behavior: 'smooth', block: 'center' });
- }, 300);
-}
-container.addEventListener('focusin', onInputFocus);
-
-// Cleanup beim Schließen: Referenz speichern
-container._onInputFocus = onInputFocus;
-```
-
-- [ ] **Step 2: Listener in closeModal() entfernen**
-
-In `closeModal()`, vor `activeOverlay.remove()`:
-
-```js
-const panel = activeOverlay.querySelector('.modal-panel');
-if (panel?._onInputFocus) {
- panel.removeEventListener('focusin', panel._onInputFocus);
-}
-```
-
-- [ ] **Step 3: Prüfen im mobilen Viewport**
-
-DevTools: iPhone-Viewport. Ein Modal öffnen. Auf ein unteres Eingabefeld tippen. Das Feld soll sanft in die Mitte des sichtbaren Bereichs scrollen, wenn die Tastatur erscheint.
-
-- [ ] **Step 4: Commit**
-
-```bash
-git add public/components/modal.js
-git commit -m "feat: scroll focused input into view when virtual keyboard opens"
-```
-
----
-
-### Task 10: Vibrations-Feedback konsistenzieren
-
-**Files:**
-- Modify: `public/pages/tasks.js`
-- Modify: `public/pages/shopping.js`
-- Modify: `public/pages/contacts.js`
-- Modify: `public/pages/budget.js`
-- Modify: `public/pages/notes.js`
-
-- [ ] **Step 1: Import in allen Modulen ergänzen**
-
-In jedem der o.g. Module:
-
-```js
-import { vibrate } from '/utils/ux.js';
-```
-
-- [ ] **Step 2: tasks.js — bestehende `navigator.vibrate()`-Calls ersetzen**
-
-```bash
-grep -n "navigator.vibrate" public/pages/tasks.js
-```
-
-Jeden Fund ersetzen:
-```js
-// Vorher
-if (navigator.vibrate) navigator.vibrate(40);
-
-// Nachher
-vibrate(40);
-```
-
-- [ ] **Step 3: shopping.js — Vibration bei Artikel abhaken**
-
-In `public/pages/shopping.js`, im Checkbox-Toggle-Handler:
-
-```js
-// Nach erfolgreichem API-Call beim Abhaken:
-vibrate(10);
-```
-
-- [ ] **Step 4: contacts.js — Vibration bei Löschen**
-
-In `public/pages/contacts.js`, im Delete-Handler (nach API-Aufruf):
-
-```js
-vibrate([30, 50, 30]);
-```
-
-- [ ] **Step 5: budget.js — Vibration bei Löschen**
-
-In `public/pages/budget.js`, im Delete-Handler:
-
-```js
-vibrate([30, 50, 30]);
-```
-
-- [ ] **Step 6: notes.js — Vibration bei Löschen**
-
-In `public/pages/notes.js`, im Delete-Handler:
-
-```js
-vibrate([30, 50, 30]);
-```
-
-- [ ] **Step 7: Commit**
-
-```bash
-git add public/pages/tasks.js public/pages/shopping.js public/pages/contacts.js public/pages/budget.js public/pages/notes.js
-git commit -m "feat: consistent vibration feedback via vibrate() utility across modules"
-```
-
----
-
-## Schicht 4 — Formulare & Modals
-
-### Task 11: Bottom Sheet Modal auf Mobile
-
-**Files:**
-- Modify: `public/components/modal.js`
-- Modify: `public/styles/layout.css`
-
-- [ ] **Step 1: Bottom-Sheet-Styles in layout.css ergänzen**
-
-In `public/styles/layout.css`, nach den bestehenden Modal-Styles:
-
-```css
-/* ── Bottom Sheet (Mobile < 768px) ── */
-@media (max-width: 767px) {
- .modal-overlay {
- align-items: flex-end;
- }
-
- .modal-panel {
- width: 100%;
- max-width: 100%;
- max-height: 90dvh;
- border-radius: var(--radius-xl) var(--radius-xl) 0 0;
- overflow-y: auto;
- -webkit-overflow-scrolling: touch;
- /* Sheet-Handle */
- padding-top: calc(var(--space-4) + 20px);
- animation: sheet-in 0.25s var(--ease-out) forwards;
- }
-
- .modal-panel::before {
- content: '';
- position: absolute;
- top: var(--space-3);
- left: 50%;
- transform: translateX(-50%);
- width: 36px;
- height: 4px;
- background: var(--color-border);
- border-radius: var(--radius-full);
- }
-
- .modal-panel--closing {
- animation: sheet-out 0.2s ease forwards;
- }
-}
-
-@keyframes sheet-in {
- from { transform: translateY(100%); }
- to { transform: translateY(0); }
-}
-
-@keyframes sheet-out {
- from { transform: translateY(0); }
- to { transform: translateY(100%); }
-}
-
-@media (prefers-reduced-motion: reduce) {
- .modal-panel {
- animation: none;
- }
- .modal-panel--closing {
- animation: none;
- }
-}
-```
-
-- [ ] **Step 2: Animierte Schließung in closeModal() ergänzen**
-
-In `public/components/modal.js`, die `closeModal()`-Funktion anpassen:
-
-```js
-export function closeModal() {
- if (!activeOverlay) return;
-
- document.removeEventListener('keydown', onEscape);
-
- if (focusTrapHandler) {
- const panel = activeOverlay.querySelector('.modal-panel');
- if (panel) {
- panel.removeEventListener('keydown', focusTrapHandler);
- if (panel._onInputFocus) panel.removeEventListener('focusin', panel._onInputFocus);
- // Sheet-Out-Animation auf Mobile
- const isMobile = window.innerWidth < 768;
- if (isMobile) {
- panel.classList.add('modal-panel--closing');
- panel.addEventListener('animationend', () => {
- _doClose();
- }, { once: true });
- return;
- }
- }
- focusTrapHandler = null;
- }
-
- _doClose();
-}
-
-function _doClose() {
- if (!activeOverlay) return;
- activeOverlay.remove();
- activeOverlay = null;
- document.body.style.overflow = '';
- if (previouslyFocused?.focus) {
- previouslyFocused.focus();
- previouslyFocused = null;
- }
- if (window.oikos?.restoreThemeColor) window.oikos.restoreThemeColor();
-}
-```
-
-- [ ] **Step 3: Swipe-to-Close für Bottom Sheet**
-
-In `public/components/modal.js`, in `openModal()` nach `trapFocus(panel)`:
-
-```js
-// Swipe-to-Close auf Mobile
-if (window.innerWidth < 768) {
- _wireSheetSwipe(panel);
-}
-```
-
-Neue Funktion:
-
-```js
-function _wireSheetSwipe(panel) {
- let startY = 0;
- let dragging = false;
-
- panel.addEventListener('touchstart', (e) => {
- startY = e.touches[0].clientY;
- dragging = true;
- }, { passive: true });
-
- panel.addEventListener('touchmove', (e) => {
- if (!dragging) return;
- const dy = e.touches[0].clientY - startY;
- if (dy < 0) return; // Kein Swipe nach oben
- panel.style.transform = `translateY(${dy * 0.6}px)`;
- }, { passive: true });
-
- panel.addEventListener('touchend', (e) => {
- if (!dragging) return;
- dragging = false;
- const dy = e.changedTouches[0].clientY - startY;
- panel.style.transform = '';
- if (dy > 80) {
- closeModal();
- }
- });
-}
-```
-
-- [ ] **Step 4: Visuell prüfen (Mobile Viewport)**
-
-Modal öffnen. Auf Mobile: Sheet fährt von unten ein, Sheet-Handle sichtbar. Nach unten wischen → Modal schließt sich.
-
-- [ ] **Step 5: Commit**
-
-```bash
-git add public/components/modal.js public/styles/layout.css
-git commit -m "feat: bottom sheet modal on mobile with swipe-to-close"
-```
-
----
-
-### Task 12: Enter-Tastennavigation zwischen Formularfeldern
-
-**Files:**
-- Modify: `public/components/modal.js`
-
-- [ ] **Step 1: Enter-Navigation in trapFocus() einbauen**
-
-In `public/components/modal.js`, in der `trapFocus()`-Funktion, den `focusTrapHandler` erweitern:
-
-```js
-focusTrapHandler = (e) => {
- // Tab-Trap (bestehend)
- if (e.key === 'Tab') {
- const focusable = container.querySelectorAll(FOCUSABLE);
- if (!focusable.length) return;
- const first = focusable[0];
- const last = focusable[focusable.length - 1];
- if (e.shiftKey && document.activeElement === first) {
- e.preventDefault(); last.focus();
- } else if (!e.shiftKey && document.activeElement === last) {
- e.preventDefault(); first.focus();
- }
- }
-
- // Enter in einzeiligen Inputs → zum nächsten Feld springen
- if (e.key === 'Enter') {
- const active = document.activeElement;
- const isInput = active.tagName === 'INPUT' && active.type !== 'submit' && active.type !== 'button';
- const isSelect = active.tagName === 'SELECT';
-
- if (isInput || isSelect) {
- const focusable = Array.from(container.querySelectorAll(FOCUSABLE));
- const idx = focusable.indexOf(active);
- const next = focusable[idx + 1];
-
- if (next && next.tagName !== 'BUTTON') {
- e.preventDefault();
- next.focus();
- }
- // Beim letzten Feld oder wenn Next ein Button ist: Submit auslösen
- if (!next || next.tagName === 'BUTTON') {
- const submitBtn = container.querySelector('button[type="submit"], .btn--primary');
- if (submitBtn && !submitBtn.disabled) {
- e.preventDefault();
- submitBtn.click();
- }
- }
- }
- }
-};
-```
-
-- [ ] **Step 2: Prüfen in einem Modal**
-
-Ein beliebiges Formular öffnen (z.B. neue Aufgabe). Mit Enter durch die Felder navigieren. Letztes Feld + Enter soll den Speichern-Button auslösen.
-
-- [ ] **Step 3: Commit**
-
-```bash
-git add public/components/modal.js
-git commit -m "feat: Enter-key advances focus between form fields and triggers submit on last field"
-```
-
----
-
-### Task 13: Inline Blur-Validierung in Formularfeldern
-
-**Files:**
-- Modify: `public/styles/layout.css`
-- Modify: `public/components/modal.js`
-
-- [ ] **Step 1: Validierungs-CSS in layout.css ergänzen**
-
-In `public/styles/layout.css`, nach den Input-Styles:
-
-```css
-/* ── Inline-Validierung ── */
-.form-group {
- display: flex;
- flex-direction: column;
- gap: var(--space-1);
-}
-
-.form-field--error .form-control {
- border-color: var(--color-danger);
-}
-
-.form-field--valid .form-control {
- border-color: var(--color-success);
-}
-
-.form-field__error {
- display: none;
- font-size: var(--text-sm);
- color: var(--color-danger);
- gap: var(--space-1);
- align-items: center;
-}
-
-.form-field--error .form-field__error {
- display: flex;
-}
-```
-
-- [ ] **Step 2: Blur-Validierung als Utility in modal.js einbauen**
-
-In `public/components/modal.js`, neue Hilfsfunktion:
-
-```js
-/**
- * Aktiviert Blur-Validierung für alle required-Inputs in einem Container.
- * Aufgerufen von onSave-Callbacks der einzelnen Seiten-Module.
- *
- * @param {HTMLElement} formContainer
- */
-export function wireBlurValidation(formContainer) {
- formContainer.querySelectorAll('input[required], select[required], textarea[required]').forEach((input) => {
- input.addEventListener('blur', () => {
- const group = input.closest('.form-field') ?? input.parentElement;
- const hasValue = input.value.trim().length > 0;
- group?.classList.toggle('form-field--error', !hasValue);
- group?.classList.toggle('form-field--valid', hasValue);
- });
- });
-}
-```
-
-- [ ] **Step 3: Fehleranzeige-HTML-Struktur in tasks.js einbauen (als Beispiel)**
-
-In `public/pages/tasks.js`, im Formular-HTML der Aufgaben-Modal, Titelfeld anpassen:
-
-```html
-
-
-
-
-
-
-
-
-
-
- Dieses Feld ist erforderlich.
-
-
-```
-
-- [ ] **Step 4: wireBlurValidation() im onSave-Callback von tasks.js aufrufen**
-
-In `public/pages/tasks.js`, im `openModal()`-Aufruf, innerhalb von `onSave`:
-
-```js
-import { openModal, closeModal, wireBlurValidation } from '/components/modal.js';
-
-// ...
-onSave: (panel) => {
- wireBlurValidation(panel);
- // ... bestehender Event-Binding-Code
-}
-```
-
-- [ ] **Step 5: Visuell prüfen**
-
-Neue Aufgabe → Titel-Feld anklicken → wieder verlassen ohne Eingabe → roter Rand + Fehlermeldung. Etwas eingeben → grüner Rand.
-
-- [ ] **Step 6: Commit**
-
-```bash
-git add public/components/modal.js public/styles/layout.css public/pages/tasks.js
-git commit -m "feat: blur-triggered inline validation for required form fields"
-```
-
----
-
-### Task 14: Submit-Feedback (Checkmark-Erfolg, Shake-Fehler)
-
-**Files:**
-- Modify: `public/styles/layout.css`
-- Modify: `public/components/modal.js`
-
-- [ ] **Step 1: Animationen in layout.css ergänzen**
-
-In `public/styles/layout.css`:
-
-```css
-/* ── Submit-Feedback Animationen ── */
-@keyframes btn-shake {
- 0%, 100% { transform: translateX(0); }
- 20% { transform: translateX(-4px); }
- 40% { transform: translateX(4px); }
- 60% { transform: translateX(-4px); }
- 80% { transform: translateX(4px); }
-}
-
-.btn--shaking {
- animation: btn-shake 0.3s ease;
-}
-
-.btn--success {
- background-color: var(--color-success) !important;
- color: #fff !important;
- pointer-events: none;
-}
-```
-
-- [ ] **Step 2: Submit-Feedback Utility in modal.js**
-
-In `public/components/modal.js`, neue exportierte Funktionen:
-
-```js
-/**
- * Zeigt Erfolgs-Feedback auf einem Button (Checkmark für 600ms).
- * @param {HTMLButtonElement} btn
- * @param {string} [originalLabel]
- */
-export function btnSuccess(btn, originalLabel) {
- const label = originalLabel ?? btn.textContent;
- btn.classList.add('btn--success');
- btn.innerHTML = `
-
- `;
- setTimeout(() => {
- btn.classList.remove('btn--success');
- btn.textContent = label;
- }, 700);
-}
-
-/**
- * Zeigt Fehler-Feedback auf einem Button (Shake-Animation).
- * @param {HTMLButtonElement} btn
- */
-export function btnError(btn) {
- btn.classList.remove('btn--shaking');
- // Reflow erzwingen damit Animation neu startet
- void btn.offsetWidth;
- btn.classList.add('btn--shaking');
- btn.addEventListener('animationend', () => btn.classList.remove('btn--shaking'), { once: true });
-}
-```
-
-- [ ] **Step 3: btnSuccess/btnError in tasks.js verwenden**
-
-In `public/pages/tasks.js`, Import erweitern:
-
-```js
-import { openModal, closeModal, wireBlurValidation, btnSuccess, btnError } from '/components/modal.js';
-```
-
-Im Form-Submit-Handler der Aufgaben:
-
-```js
-// Nach erfolgreichem API-Call:
-btnSuccess(saveBtn, 'Speichern');
-setTimeout(() => closeModal(), 700);
-
-// Bei API-Fehler:
-btnError(saveBtn);
-showToast('Fehler beim Speichern.', 'danger');
-```
-
-- [ ] **Step 4: Visuell prüfen**
-
-Neue Aufgabe erstellen → Speichern → Button soll kurz grün mit Checkmark werden → Modal schließt sich. Absichtlich Netzwerkfehler simulieren (DevTools → Offline) → Button soll schütteln.
-
-- [ ] **Step 5: Commit**
-
-```bash
-git add public/styles/layout.css public/components/modal.js public/pages/tasks.js
-git commit -m "feat: checkmark success and shake error feedback on form submit buttons"
-```
-
----
-
-## Abschluss
-
-- [ ] **Alle Tests ausführen**
-
-```bash
-npm test
-```
-
-Erwartung: Alle bisherigen Tests + neue `test-ux-utils.js`-Tests grün.
-
-- [ ] **Vollständige Durchsicht im Browser (Mobile + Desktop)**
-
-Jede Seite öffnen und prüfen:
-- Cards haben einheitliche Abstände ✓
-- FAB-Farben entsprechen dem Modul ✓
-- Seitenübergänge sind direktional (links/rechts) ✓
-- Listen blenden gestaffelt ein ✓
-- Empty States sind einheitlich ✓
-- Swipe-Reveal in Aufgaben blendet proportional ein ✓
-- Install-Prompt erscheint erst nach 2 Interaktionen ✓
-- Virtuelles Keyboard scrollt Inputs in den sichtbaren Bereich ✓
-- Vibration bei Aktionen (Mobile) ✓
-- Modals öffnen als Bottom Sheet auf Mobile ✓
-- Swipe-to-Close funktioniert ✓
-- Enter navigiert durch Formularfelder ✓
-- Blur-Validierung zeigt Fehler unmittelbar ✓
-- Submit-Button gibt Erfolg/Fehler-Feedback ✓
-
-- [ ] **CHANGELOG.md aktualisieren**
-
-Im `[Unreleased]`-Block:
-
-```markdown
-### Changed
-- Directional slide-x page transitions (forward = right, backward = left)
-- PWA install prompt delayed until 2 user interactions, dismiss window reduced to 7 days
-- Unified card padding to 16px across all modules
-
-### Added
-- Staggered fade-in animation for list items on page load
-- Unified empty states using shared `.empty-state` class in contacts and budget
-- Proportional opacity on swipe-reveal action areas (tasks)
-- scrollIntoView for focused inputs when virtual keyboard opens
-- Consistent vibration feedback via `vibrate()` utility across modules
-- Bottom sheet modal on mobile (< 768px) with swipe-to-close and sheet handle
-- Enter-key navigation between form fields; Enter on last field triggers submit
-- Blur-triggered inline validation for required fields with error/success states
-- Submit button checkmark-success (700ms) and shake-error feedback animations
-- FAB colors tied to per-module accent tokens
-```
-
-- [ ] **Final Commit**
-
-```bash
-git add CHANGELOG.md
-git commit -m "docs: update CHANGELOG for UX polish (all 4 layers)"
-```
diff --git a/docs/superpowers/plans/2026-03-31-shopping-swipe-gestures.md b/docs/superpowers/plans/2026-03-31-shopping-swipe-gestures.md
deleted file mode 100644
index 4bc423f..0000000
--- a/docs/superpowers/plans/2026-03-31-shopping-swipe-gestures.md
+++ /dev/null
@@ -1,476 +0,0 @@
-# Shopping Swipe Gestures 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:** Add left-swipe-to-toggle and right-swipe-to-delete touch gestures to Shopping list items on mobile, replacing the visible × delete button on small screens.
-
-**Architecture:** Shared swipe base CSS is moved from `tasks.css` to `layout.css` so both modules can use it. Shopping-specific styles (delete reveal, mobile button hiding) go in `shopping.css`. `shopping.js` wraps each item in a `swipe-row` and registers touch handlers via a new `wireSwipeGestures()` function, called after every list re-render.
-
-**Tech Stack:** Vanilla JS (ES modules), Touch Events API, CSS custom properties, Node.js built-in test runner (regression tests only — no DOM test framework in this project).
-
----
-
-## File Map
-
-| File | Change |
-|------|--------|
-| `public/styles/tasks.css` | Remove `.swipe-row`, `.swipe-reveal`, `.swipe-reveal--done` (moved to layout.css) |
-| `public/styles/layout.css` | Add moved shared swipe styles + `.swipe-reveal--done` |
-| `public/styles/shopping.css` | Add `.swipe-reveal--delete`, `.swipe-row .shopping-item`, `.swipe-row--swiping .shopping-item`, mobile hide for `.item-delete` |
-| `public/pages/shopping.js` | Wrap `renderItem()` output in swipe-row, add `wireSwipeGestures(container)`, call it from `updateItemsList()` |
-
----
-
-### Task 1: Move shared swipe CSS from tasks.css to layout.css
-
-**Files:**
-- Modify: `public/styles/tasks.css` (lines 170–229)
-- Modify: `public/styles/layout.css` (append before Print section)
-
-- [ ] **Step 1: Remove base swipe styles from tasks.css**
-
-In `public/styles/tasks.css`, delete the block from `/* Swipe-Wrapper (Mobil-Gesten) */` through `.swipe-reveal--done { ... }` (lines 170–216). Keep only the task-specific rules that follow:
-
-```css
-/* Kein Margin mehr am Task-Card selbst (übernimmt swipe-row) */
-.swipe-row .task-card {
- margin-bottom: 0;
- border-radius: var(--radius-md);
- position: relative;
- z-index: 1;
- will-change: transform;
-}
-
-/* Rechts hinter der Karte = Bearbeiten (Swipe nach rechts) */
-.swipe-reveal--edit {
- left: 0;
- background-color: var(--color-accent);
- color: #fff;
- border-radius: var(--radius-md) 0 0 var(--radius-md);
-}
-
-/* Touch-Feedback: leichte Hervorhebung während Swipe */
-.swipe-row--swiping .task-card {
- box-shadow: var(--shadow-lg);
-}
-```
-
-Add a comment header so the section remains clear:
-
-```css
-/* --------------------------------------------------------
- * Swipe-Wrapper — Task-spezifische Styles
- * Basis-Styles (.swipe-row, .swipe-reveal, .swipe-reveal--done)
- * liegen in layout.css
- * -------------------------------------------------------- */
-```
-
-- [ ] **Step 2: Add shared swipe styles to layout.css**
-
-In `public/styles/layout.css`, find the Print section (`/* Print-Styles */`) and insert the following block **directly before it**:
-
-```css
-/* --------------------------------------------------------
- * Swipe-Wrapper — Gemeinsame Basis (Tasks + Shopping)
- * Modul-spezifische Styles (.swipe-reveal--edit, .swipe-reveal--delete,
- * .swipe-row .task-card, .swipe-row .shopping-item) liegen in den Modul-CSS.
- * -------------------------------------------------------- */
-.swipe-row {
- position: relative;
- overflow: hidden;
- border-radius: var(--radius-md);
- margin-bottom: var(--space-2);
- /* Verhindert ungewolltes Flackern auf iOS */
- -webkit-backface-visibility: hidden;
-}
-
-/* Reveal-Panels hinter der Karte */
-.swipe-reveal {
- position: absolute;
- top: 0;
- bottom: 0;
- width: 50%;
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- gap: var(--space-1);
- font-size: var(--text-xs);
- font-weight: var(--font-weight-semibold);
- opacity: 0;
- pointer-events: none;
- z-index: 0;
- transition: opacity 0.05s linear;
-}
-
-/* Gemeinsam: Erledigt / Abhaken (Swipe nach links) */
-.swipe-reveal--done {
- right: 0;
- background-color: var(--color-success);
- color: #fff;
- border-radius: 0 var(--radius-md) var(--radius-md) 0;
-}
-```
-
-- [ ] **Step 3: Run regression tests**
-
-```bash
-npm test
-```
-
-Expected: `# pass 12` and `# fail 0`
-
-- [ ] **Step 4: Verify tasks swipe still works visually**
-
-Open the app in a browser (or mobile DevTools touch emulation), go to Tasks list, swipe an item left and right. The green "Erledigt" and blue "Bearbeiten" panels must still appear correctly.
-
-- [ ] **Step 5: Commit**
-
-```bash
-git add public/styles/tasks.css public/styles/layout.css
-git commit -m "refactor: move shared swipe CSS from tasks.css to layout.css"
-```
-
----
-
-### Task 2: Add shopping-specific swipe CSS
-
-**Files:**
-- Modify: `public/styles/shopping.css`
-
-- [ ] **Step 1: Find the correct insertion point in shopping.css**
-
-In `public/styles/shopping.css`, locate the section for `.shopping-item` styles. The new swipe styles go **after** the existing `.shopping-item` block.
-
-- [ ] **Step 2: Add the styles**
-
-Append the following after the existing `.shopping-item` rules in `public/styles/shopping.css`:
-
-```css
-/* --------------------------------------------------------
- * Swipe-Wrapper — Shopping-spezifische Styles
- * -------------------------------------------------------- */
-
-/* Kein Margin mehr am shopping-item selbst (übernimmt swipe-row) */
-.swipe-row .shopping-item {
- margin-bottom: 0;
- border-radius: var(--radius-md);
- position: relative;
- z-index: 1;
- will-change: transform;
-}
-
-/* Rechts hinter der Karte = Löschen (Swipe nach rechts) */
-.swipe-reveal--delete {
- left: 0;
- background-color: var(--color-danger);
- color: #fff;
- border-radius: var(--radius-md) 0 0 var(--radius-md);
-}
-
-/* Touch-Feedback: leichte Hervorhebung während Swipe */
-.swipe-row--swiping .shopping-item {
- box-shadow: var(--shadow-lg);
-}
-
-/* × Löschen-Button auf Mobile ausblenden — Swipe übernimmt */
-@media (max-width: 1023px) {
- .item-delete {
- display: none;
- }
-}
-```
-
-- [ ] **Step 3: Run regression tests**
-
-```bash
-npm test
-```
-
-Expected: `# pass 12` and `# fail 0`
-
-- [ ] **Step 4: Commit**
-
-```bash
-git add public/styles/shopping.css
-git commit -m "feat: add shopping swipe CSS (delete reveal, mobile button hide)"
-```
-
----
-
-### Task 3: Wrap renderItem with swipe-row in shopping.js
-
-**Files:**
-- Modify: `public/pages/shopping.js`
-
-- [ ] **Step 1: Update renderItem()**
-
-Find `function renderItem(item)` in `public/pages/shopping.js` (currently returns a `.shopping-item` div). Replace the entire function with:
-
-```js
-function renderItem(item) {
- const isDone = Boolean(item.is_checked);
- return `
-
-
-
- ${isDone ? 'Zurück' : 'Abhaken'}
-
-
-
- Löschen
-
-
-
-
-
${item.name}
- ${item.quantity ? `
${item.quantity}
` : ''}
-
-
-
-
`;
-}
-```
-
-- [ ] **Step 2: Run regression tests**
-
-```bash
-npm test
-```
-
-Expected: `# pass 12` and `# fail 0`
-
-- [ ] **Step 3: Verify shopping list renders without errors**
-
-Open the app in a browser, navigate to Shopping. Items must render with the swipe-row wrapper. The × button must be hidden on mobile (visible on desktop ≥ 1024px). The existing checkbox toggle must still work.
-
-- [ ] **Step 4: Commit**
-
-```bash
-git add public/pages/shopping.js
-git commit -m "feat: wrap shopping items in swipe-row"
-```
-
----
-
-### Task 4: Add wireSwipeGestures and wire into updateItemsList
-
-**Files:**
-- Modify: `public/pages/shopping.js`
-
-- [ ] **Step 1: Add constants at the top of shopping.js**
-
-After the `import` statements (before the `state` declaration), add:
-
-```js
-// Swipe-Gesten Konstanten (identisch zu tasks.js)
-const SWIPE_THRESHOLD = 80; // px — Mindestweg für Aktion
-const SWIPE_MAX_VERT = 12; // px — vertikaler Toleranzbereich
-const SWIPE_LOCK_VERT = 30; // px — ab diesem Weg gilt es als Scroll
-```
-
-- [ ] **Step 2: Add wireSwipeGestures() function**
-
-Add the following function **before** `updateItemsList()` in `shopping.js`:
-
-```js
-function wireSwipeGestures(container) {
- const listEl = container.querySelector('#items-list');
- if (!listEl) return;
-
- listEl.querySelectorAll('.swipe-row').forEach((row) => {
- let startX = 0, startY = 0;
- let dx = 0;
- let locked = false; // false | 'swipe' | 'scroll'
- const card = row.querySelector('.shopping-item');
- if (!card) return;
-
- function resetCard(animate = true) {
- card.style.transition = animate ? 'transform 0.25s ease' : '';
- card.style.transform = '';
- row.classList.remove('swipe-row--swiping');
- row.querySelector('.swipe-reveal--done').style.opacity = '0';
- row.querySelector('.swipe-reveal--delete').style.opacity = '0';
- }
-
- row.addEventListener('touchstart', (e) => {
- if (document.getElementById('shared-modal-overlay')) return;
- startX = e.touches[0].clientX;
- startY = e.touches[0].clientY;
- dx = 0;
- locked = false;
- card.style.transition = '';
- }, { passive: true });
-
- row.addEventListener('touchmove', (e) => {
- if (locked === 'scroll') return;
-
- const currentX = e.touches[0].clientX;
- const currentY = e.touches[0].clientY;
- dx = currentX - startX;
- const dy = Math.abs(currentY - startY);
-
- if (locked === false) {
- if (dy > SWIPE_MAX_VERT && Math.abs(dx) < dy) {
- locked = 'scroll';
- resetCard(false);
- return;
- }
- if (Math.abs(dx) > SWIPE_MAX_VERT) {
- locked = 'swipe';
- }
- }
-
- if (locked !== 'swipe') return;
-
- if (dy < SWIPE_LOCK_VERT) e.preventDefault();
-
- const dampened = dx > 0
- ? Math.min(dx, SWIPE_THRESHOLD + (dx - SWIPE_THRESHOLD) * 0.2)
- : Math.max(dx, -(SWIPE_THRESHOLD + (-dx - SWIPE_THRESHOLD) * 0.2));
-
- card.style.transform = `translateX(${dampened}px)`;
- row.classList.add('swipe-row--swiping');
-
- const progress = Math.min(Math.abs(dx) / SWIPE_THRESHOLD, 1);
- if (dx < 0) {
- row.querySelector('.swipe-reveal--done').style.opacity = String(progress);
- row.querySelector('.swipe-reveal--delete').style.opacity = '0';
- } else {
- row.querySelector('.swipe-reveal--delete').style.opacity = String(progress);
- row.querySelector('.swipe-reveal--done').style.opacity = '0';
- }
- }, { passive: false });
-
- row.addEventListener('touchend', async () => {
- if (locked !== 'swipe') { resetCard(false); return; }
-
- const itemId = Number(row.dataset.swipeId);
- const checked = Number(row.dataset.swipeChecked);
-
- if (dx < -SWIPE_THRESHOLD) {
- // Swipe links → abhaken / zurück
- card.style.transition = 'transform 0.2s ease';
- card.style.transform = 'translateX(-110%)';
- vibrate(40);
- setTimeout(async () => {
- resetCard(false);
- const newVal = checked ? 0 : 1;
- const item = state.items.find((i) => i.id === itemId);
- if (item) {
- item.is_checked = newVal;
- updateItemsList(container);
- updateListCounter(state.activeListId, 0, newVal ? 1 : -1);
- renderTabs(container);
- }
- try {
- await api.patch(`/shopping/items/${itemId}`, { is_checked: newVal });
- vibrate(10);
- } catch (err) {
- if (item) item.is_checked = checked;
- updateItemsList(container);
- window.oikos.showToast(err.message, 'danger');
- }
- }, 200);
-
- } else if (dx > SWIPE_THRESHOLD) {
- // Swipe rechts → löschen
- card.style.transition = 'transform 0.2s ease';
- card.style.transform = 'translateX(110%)';
- vibrate(40);
- setTimeout(async () => {
- const item = state.items.find((i) => i.id === itemId);
- try {
- await api.delete(`/shopping/items/${itemId}`);
- state.items = state.items.filter((i) => i.id !== itemId);
- updateItemsList(container);
- updateListCounter(state.activeListId, -1, item?.is_checked ? -1 : 0);
- renderTabs(container);
- } catch (err) {
- resetCard(true);
- window.oikos.showToast(err.message, 'danger');
- }
- }, 200);
-
- } else {
- resetCard(true);
- }
- });
- });
-}
-```
-
-- [ ] **Step 3: Call wireSwipeGestures from updateItemsList()**
-
-In `updateItemsList(container)`, after the `stagger(...)` call, add:
-
-```js
-wireSwipeGestures(container);
-```
-
-The updated block looks like this:
-
-```js
-function updateItemsList(container) {
- const listEl = container.querySelector('#items-list');
- if (listEl) {
- listEl.innerHTML = renderItems();
- if (window.lucide) window.lucide.createIcons();
- stagger(listEl.querySelectorAll('.shopping-item'));
- wireSwipeGestures(container); // ← new
- }
- // ... rest of function unchanged
-}
-```
-
-- [ ] **Step 4: Run regression tests**
-
-```bash
-npm test
-```
-
-Expected: `# pass 12` and `# fail 0`
-
-- [ ] **Step 5: Verify swipe gestures work end-to-end**
-
-Test in mobile DevTools (Chrome → Toggle device toolbar → touch emulation):
-
-1. Open Shopping, add 3 test items
-2. Swipe an item **left** past 80 px → green panel appears → item toggles to checked (strikethrough) → swipe again to uncheck
-3. Swipe an item **right** past 80 px → red panel appears → item is removed from list
-4. Swipe < 80 px in either direction → card springs back, no action
-5. Scroll the list vertically → no swipe triggers
-6. On desktop (≥ 1024px): × delete button is visible, swipe not triggered by mouse
-
-- [ ] **Step 6: Update CHANGELOG.md**
-
-Add to `## [Unreleased]` → `### Added`:
-
-```
-- Shopping: swipe-left to toggle checked/unchecked, swipe-right to delete items on mobile; × delete button hidden on mobile in favour of swipe gesture
-```
-
-- [ ] **Step 7: Commit**
-
-```bash
-git add public/pages/shopping.js CHANGELOG.md
-git commit -m "feat: swipe gestures on shopping list items (toggle + delete)"
-```
-
----
-
-### Task 5: Push
-
-- [ ] **Push to remote**
-
-```bash
-git push
-```
diff --git a/docs/superpowers/specs/2026-03-30-ux-polish-design.md b/docs/superpowers/specs/2026-03-30-ux-polish-design.md
deleted file mode 100644
index b349919..0000000
--- a/docs/superpowers/specs/2026-03-30-ux-polish-design.md
+++ /dev/null
@@ -1,224 +0,0 @@
-# UX Polish — Design-Spezifikation
-
-**Datum:** 2026-03-30
-**Status:** Genehmigt
-**Scope:** UX-Verbesserungen (Phase 1 vor Featureerweiterungen)
-
----
-
-## Ausgangslage
-
-Oikos v0.1.0 ist funktional vollständig und alle 146 Tests sind grün. Das UI wirkt jedoch steril und beliebig — es fehlt eine eigene Persönlichkeit. Zusätzlich gibt es Konsistenzlücken zwischen Modulen, abrupte Übergänge, ein suboptimales mobiles Erlebnis und verbesserungswürdige Formular-UX.
-
-Die Verbesserungen erfolgen in vier aufeinander aufbauenden Schichten (Layer for Layer), sodass jede Schicht das Fundament der nächsten bildet.
-
----
-
-## Schicht 1 — Design-Sprache & Konsistenz
-
-### Ziel
-Der App eine eigene, wiedererkennbare Persönlichkeit geben — klar und präzise als Hauptrichtung, mit einem Hauch Wärme und Familiarität.
-
-### Typografie-Skala
-Vier klar unterscheidbare Stufen ersetzen die aktuelle flache Hierarchie:
-
-| Stufe | Größe | Gewicht | Einsatz |
-|-------|-------|---------|---------|
-| Display | 24px | 700 | Seitentitel, Modal-Titel |
-| Title | 18px | 600 | Widget-Überschriften, Gruppen-Header |
-| Body | 15px | 400 | Fließtext, Listeneinträge |
-| Caption | 13px | 400 | Metadaten, Zeitstempel, Labels |
-
-Überschriften (Display, Title) erhalten `letter-spacing: -0.3px` für einen modernen, knappen Look.
-
-### Farb-Tokens
-Die Grautöne erhalten einen minimalen Warmton-Shift, um den Charakter von "Tech-App" zu "Familien-App" zu verschieben:
-
-```css
-/* Vorher → Nachher */
---color-bg: #F5F5F7 → #F6F5F3 /* ganz leicht warmer Tint */
---color-surface: #FFFFFF → #FFFFFF /* bleibt rein */
---color-text-primary:#1C1C1E → #1A1A1F /* minimal wärmer */
-```
-
-Der Accent-Blau (`#007AFF`) bleibt unverändert. Dunkel-Modus erhält analoge Anpassungen.
-
-### Komponenten-Konsistenz
-Alle Module erhalten identische Card-Tokens:
-- Padding: `16px` überall (aktuell variiert zwischen 12px–20px je Modul)
-- Schatten: `shadow-sm` im Ruhezustand, `shadow-md` bei Hover
-- Buttons: `:hover` = leichter Helligkeitsshift, `:active` = `scale(0.97)` (haptisches Feedback-Gefühl)
-
-### Modul-Akzentfarben
-Jedes Modul erhält eine dezente, eigene Akzentfarbe für Page-Header und FAB. Die Farben sind bereits in der Architektur vorgesehen (theme-color), werden aber vervollständigt und konsequent eingesetzt:
-
-| Modul | Akzent |
-|-------|--------|
-| Dashboard | `#007AFF` (Standard-Blau) |
-| Aufgaben | `#FF9500` (Orange) |
-| Einkauf | `#34C759` (Grün) |
-| Essensplan | `#FF6B35` (Warm-Orange) |
-| Kalender | `#5AC8FA` (Hellblau) |
-| Notizen | `#FFCC00` (Gelb) |
-| Kontakte | `#AF52DE` (Violett) |
-| Budget | `#30B0C7` (Teal) |
-
----
-
-## Schicht 2 — Animationen & Übergänge
-
-### Ziel
-Die App fühlt sich lebendig an. Alle Animationen respektieren `prefers-reduced-motion: reduce`.
-
-### Seitenübergänge
-Neue Seite fährt von rechts ein, alte geht nach links raus:
-- Transform: `translateX(24px) → translateX(0)`
-- Opacity: `0 → 1`
-- Dauer: `200ms`, Easing: `ease-out`
-- Zurück-Navigation: gespiegelte Richtung
-
-Implementierung im zentralen `router.js` — kein Modul-Code nötig.
-
-### Gestaffelte Listen-Einblendung (Staggered Fade-In)
-Beim initialen Laden einer Seite erscheinen Listenelemente und Cards nacheinander:
-- Jedes Item: `opacity 0 → 1` + `translateY(8px) → 0`
-- Verzögerung: 30ms pro Item, maximal 5 Items gestaffelt (danach sofort)
-- Dauer pro Item: `180ms`
-
-### Micro-Interactions
-
-**Checkbox (Aufgaben erledigt):**
-Das SVG-Häkchen zeichnet sich per `stroke-dashoffset`-Animation ein (60ms). Die Karten-Zeile bekommt einen `text-decoration: line-through`-Transition (100ms).
-
-**FAB:**
-`scale(0.92)` beim `:active` + Ripple-Effekt (radial expandierender Kreis, 300ms, opacity 0→1→0).
-
-**Swipe-Reveal (Aufgaben):**
-Aktuell erscheint der farbige Hintergrund abrupt. Neu: Hintergrundfarbe und Icon blenden proportional zur Swipe-Distanz ein (`opacity: swipeDistance / SWIPE_THRESHOLD`).
-
-### Skeleton-Loading
-Dashboard-Widgets zeigen beim Laden animierte Skeleton-Platzhalter:
-- Shimmer-Animation via `@keyframes` (linearer Gradient läuft durch, 1.4s, unendlich)
-- Schematische Rechtecke in Card-Form, passend zur jeweiligen Widget-Größe
-- Ersetzt leere Flächen während des API-Calls
-
-### Empty States
-Jede leere Liste erhält einen Inline-SVG-Platzhalter und einen kontextuellen CTA:
-
-| Modul | Text | CTA |
-|-------|------|-----|
-| Aufgaben | "Keine Aufgaben — alles erledigt?" | "+ Aufgabe erstellen" |
-| Einkauf | "Die Liste ist leer" | "+ Artikel hinzufügen" |
-| Essensplan | "Kein Essen geplant" | "Mahlzeit eintragen" |
-| Notizen | "Noch keine Notizen" | "+ Notiz erstellen" |
-| Kontakte | "Noch keine Kontakte" | "+ Kontakt hinzufügen" |
-| Budget | "Keine Buchungen diesen Monat" | "+ Buchung eintragen" |
-
-SVGs sind kleine, themenbezogene Illustrationen (Linien-Icons, kein Clipart), inline im HTML, kein externer Fetch.
-
----
-
-## Schicht 3 — Mobile PWA & Natives Gefühl
-
-### Ziel
-Die installierte App fühlt sich auf dem Handy nativ an.
-
-### PWA-Install-Prompt
-**Timing:** Prompt erscheint erst nach 2–3 Benutzerinteraktionen (z.B. nach dem ersten erfolgreich erstellten Eintrag), nicht sofort beim ersten Seitenaufruf.
-
-**Darstellung:** Bottom Sheet von unten einfahrend (nicht abruptes Banner). Enthält App-Icon, Name "Oikos", kurzen Nutzentext.
-
-**Wiederholung:** Einmal abgelehnt → 7 Tage nicht erneut zeigen (via `localStorage` mit Timestamp).
-
-**Plattformspezifisch:**
-- Android: natives `beforeinstallprompt`-Event
-- iOS: eigene Anleitung ("Teilen → Zum Home-Bildschirm") da kein natives Event
-
-### Scroll & Overscroll
-Auf allen scrollbaren Containern:
-- `overscroll-behavior: contain` — verhindert Browser-Pull-to-Refresh innerhalb der App
-- `-webkit-overflow-scrolling: touch` — Momentum-Scrolling auf iOS
-- Bottom Nav und Header: `position: sticky` mit `env(safe-area-inset-bottom)` — kein Layout-Shift durch dynamische Viewport-Höhe (iOS Safari)
-
-### Vibrations-Feedback
-`navigator.vibrate()` bei bedeutsamen Aktionen, nur wenn API verfügbar und `prefers-reduced-motion` nicht gesetzt:
-
-| Aktion | Muster |
-|--------|--------|
-| Aufgabe erledigt | `10ms` |
-| Swipe-Aktion ausgelöst | `15ms` |
-| Eintrag gelöscht | `[30, 50, 30]ms` |
-| Fehlermeldung | `[20, 40, 20]ms` |
-
-### Keyboard-Verhalten (Virtuelles Keyboard)
-Beim Tippen in ein Eingabefeld springt dieses automatisch in den sichtbaren Bereich:
-```js
-input.addEventListener('focus', () => {
- setTimeout(() => input.scrollIntoView({ behavior: 'smooth', block: 'center' }), 300);
-});
-```
-300ms Verzögerung gibt dem Keyboard Zeit, sich zu öffnen.
-
-### Theme-Color
-Dynamische `theme-color` Meta-Tag-Aktualisierung beim Modulwechsel wird vervollständigt — jedes Modul übergibt beim Rendern seine Akzentfarbe, die Browser-Chrome-Farbe wechselt entsprechend.
-
----
-
-## Schicht 4 — Formulare & Modals
-
-### Ziel
-Eingaben sind schnell, klar und fehlertolerant — besonders auf Mobil.
-
-### Auto-Fokus & Tastaturnavigation
-- Beim Öffnen eines Modals: Fokus springt automatisch auf erstes Eingabefeld (`setTimeout(0)` nach Modal-Render)
-- `Tab`: logische Feldreihenfolge (entspricht DOM-Reihenfolge)
-- `Enter` in einzeiligen Inputs: springt zum nächsten Feld
-- `Enter` im letzten Feld (oder Textarea + `Ctrl+Enter`): löst Submit aus
-- `Escape`: schließt Modal
-
-### Inline-Validierung
-- Trigger: `blur`-Event auf jedem Feld (nicht erst bei Submit)
-- Fehlermeldung: direkt unter dem Feld, `color: var(--color-danger)`, mit Warn-Icon
-- Erfolgreiche Pflichtfelder: dezenter grüner Rand (`border-color: var(--color-success)`)
-- Submit-Button: deaktiviert solange Pflichtfelder leer, aktiv sobald Minimalanforderungen erfüllt
-
-### Modal-UX auf Mobil
-Auf Screens < 768px werden Modals als **Bottom Sheet** dargestellt:
-- Einfähranimation: `translateY(100%) → translateY(0)`, 250ms, `ease-out`
-- Maximalhöhe: `90dvh`, intern scrollbar
-- Swipe-to-Close: Swipe nach unten > 80px schließt Modal; zwischen 0–80px gibt es gummibandartigen Widerstand (`transform: translateY(distance * 0.4)`)
-- Backdrop-Klick: schließt Modal
-- Schließanimation: `translateY(0) → translateY(100%)`, 200ms
-
-Auf Desktop (≥ 768px): zentriertes Modal bleibt unverändert (Backdrop-Klick + Escape schließen).
-
-### Submit-Feedback
-**Erfolg:**
-1. Submit-Button: Label wird durch Checkmark-Icon ersetzt (600ms)
-2. Modal schließt sich mit Slide-Down-Animation
-3. Liste aktualisiert sich (optimistisch oder via Re-Fetch)
-
-**Fehler:**
-1. Submit-Button: `shake`-Animation (300ms, ±4px horizontal)
-2. Fehlermeldung erscheint unter dem betreffenden Feld oder als Banner oben im Modal
-3. Kein Datenverlust — alle eingegebenen Werte bleiben erhalten
-
----
-
-## Nicht in Scope
-
-- Neue Features (Meal Drag&Drop, Budget-Recurrence, Kalender-Auto-Sync) — diese kommen erst nach UX + Code-Qualität
-- Backend-Änderungen — alle vier Schichten sind rein frontend-seitig
-- Push-Benachrichtigungen — explizit v1.1 (BACKLOG)
-- Grundlegende Architekturänderungen am Router oder API-Layer
-
----
-
-## Reihenfolge der Implementierung
-
-1. Schicht 1: `tokens.css`, `reset.css`, `layout.css`, alle Modul-CSS-Dateien
-2. Schicht 2: `router.js` (Seitenübergänge), alle Page-Module (Staggering, Micro-Interactions), `dashboard.js` (Skeleton)
-3. Schicht 3: `oikos-install-prompt.js`, `sw.js`, alle Page-Module (Scroll, Keyboard, Vibration)
-4. Schicht 4: `components/modal.js`, alle Page-Module (Formulare, Validierung)
-
-Jede Schicht ist ein eigener Commit-Block und kann unabhängig reviewt werden.
diff --git a/docs/superpowers/specs/2026-03-31-module-accent-colors-design.md b/docs/superpowers/specs/2026-03-31-module-accent-colors-design.md
deleted file mode 100644
index e6dc844..0000000
--- a/docs/superpowers/specs/2026-03-31-module-accent-colors-design.md
+++ /dev/null
@@ -1,73 +0,0 @@
-# Modul-Akzentfarben stärker nutzen — Design Spec
-
-**Date:** 2026-03-31
-**Status:** Approved
-
-## Ziel
-
-Die vorhandenen `--module-*` CSS-Tokens (Dashboard=Blau, Tasks=Grün, Kalender=Violett, Mahlzeiten=Orange, Einkauf=Rot-Orange, Notizen=Gold, Kontakte=Kräftiges Blau, Budget=Teal) werden aktuell nur für die FAB-Hintergrundfarbe genutzt. Ziel: drei weitere visuelle Ebenen mit Modul-Akzenten versehen, damit der Nutzer sofort erkennt, in welchem Modul er sich befindet.
-
-## Drei Änderungsbereiche
-
-### A — Aktiver Tab in Navigation (Bottom-Nav + Sidebar)
-
-**Problem:** `--module-accent` ist auf dem Page-Wrapper gesetzt (z. B. `.tasks-page`), aber die Nav-Bar liegt im App-Shell außerhalb dieser Wrapper. CSS-Kaskade funktioniert nicht.
-
-**Lösung — JS:** In `updateNav(path)` (router.js) wird nach dem Setzen von `aria-current` zusätzlich `--active-module-accent` als CSS Custom Property auf `document.documentElement` geschrieben:
-
-```js
-const module = ROUTES.find(r => r.path === path)?.module;
-const accent = module ? getCSSToken(`--module-${module}`) : '';
-document.documentElement.style.setProperty('--active-module-accent', accent || '');
-```
-
-**Lösung — CSS** (layout.css): Alle drei Stellen, die aktuell `var(--color-accent)` für den aktiven Nav-State nutzen, werden auf `var(--active-module-accent, var(--color-accent))` umgestellt:
-
-1. `.nav-item[aria-current="page"] { color: ... }` — Bottom-Nav Icon + Label
-2. `.nav-sidebar .nav-item[aria-current="page"] { color: ...; background-color: ... }` — Sidebar Highlight
-3. `.nav-sidebar .nav-item[aria-current="page"]::before { background: ... }` — Sidebar linker Akzentstreifen
-
-Das Fallback `var(--color-accent)` stellt sicher, dass Login-Screen und Fehlerseiten ohne Modul-Kontext korrekt dargestellt werden.
-
----
-
-### B — Seitenkopf-Streifen (3px border-top)
-
-`border-top: 3px solid var(--module-accent)` wird auf den Toolbar/Header-Selektor jedes Moduls gesetzt. Da diese Elemente innerhalb des Page-Wrappers liegen, erben sie `--module-accent` direkt.
-
-| Modul | Selektor | CSS-Datei |
-|-------|----------|-----------|
-| Tasks | `.tasks-toolbar` | tasks.css |
-| Notizen | `.notes-toolbar` | notes.css |
-| Kontakte | `.contacts-toolbar` | contacts.css |
-| Kalender | `.cal-toolbar` | calendar.css |
-| Einkauf | `.list-header` | shopping.css |
-| Budget | `.budget-list-header` | budget.css |
-| Mahlzeiten | — | entfällt (kein Toolbar) |
-| Dashboard | — | entfällt (Widget-Grid, kein Toolbar) |
-
----
-
-### C — Karten-Randstreifen (3px border-left)
-
-`border-left: 3px solid var(--module-accent)` auf den Hauptkarten-/Zeilen-Elementen. Der linke `border-radius` wird auf `0` gesetzt damit der Streifen sauber anliegt (`border-radius: 0 var(--radius-md) var(--radius-md) 0`).
-
-| Modul | Selektor | CSS-Datei | Bemerkung |
-|-------|----------|-----------|-----------|
-| Tasks | `.task-card` | tasks.css | |
-| Einkauf | `.shopping-item` | shopping.css | |
-| Kontakte | `.contact-item` | contacts.css | |
-| Budget | `.budget-entry` | budget.css | Hat bereits Einnahmen/Ausgaben-Dot — kein Konflikt |
-| Kalender | ❌ | — | Eigene Event-Farblogik |
-| Notizen | ❌ | — | Eigene Karten-Hintergrundfarben |
-| Mahlzeiten | ❌ | — | Slot-Layout, keine klassische Liste |
-| Dashboard | ❌ | — | Widget-Struktur |
-
----
-
-## Out of Scope
-
-- Kein Umbau des Farbsystems oder der Token-Namen
-- Kein Dark-Mode-spezifisches Anpassen (Dark-Mode-Tokens für `--module-*` existieren bereits in tokens.css)
-- Keine neuen Modul-Farben
-- Keine Änderungen an Meals, Dashboard, Calendar-Events, Notes-Karten
diff --git a/docs/superpowers/specs/2026-03-31-shopping-swipe-gestures-design.md b/docs/superpowers/specs/2026-03-31-shopping-swipe-gestures-design.md
deleted file mode 100644
index 1e1acbd..0000000
--- a/docs/superpowers/specs/2026-03-31-shopping-swipe-gestures-design.md
+++ /dev/null
@@ -1,79 +0,0 @@
-# Shopping Swipe Gestures — Design Spec
-
-**Date:** 2026-03-31
-**Status:** Approved
-
-## Scope
-
-Add swipe gestures to Shopping list items on mobile. Notes and other modules are explicitly out of scope.
-
-## Behaviour
-
-| Gesture | Action | Reveal colour |
-|---------|--------|---------------|
-| Swipe left (> threshold) | Toggle checked/unchecked | Green (`--color-success`) |
-| Swipe right (> threshold) | Delete item | Red (`--color-danger`) |
-
-- Reveal label for left swipe: "Abhaken" (unchecked) or "Zurück" (already checked)
-- Reveal label for right swipe: "Löschen" (always)
-- Threshold, damping, and scroll-lock logic identical to `tasks.js`
-- On swipe-right delete: optimistic DOM removal → `DELETE /api/v1/shopping/items/:id` → on error restore item and show danger toast
-- On swipe-left toggle: optimistic DOM update (class toggle) → `PATCH /api/v1/shopping/items/:id` → on error revert and show danger toast
-
-## CSS Changes
-
-**`layout.css`** — receives shared swipe infrastructure (moved from `tasks.css`):
-- `.swipe-row` base styles
-- `.swipe-reveal` base styles
-- `.swipe-reveal--done` (green, used by tasks and shopping)
-
-**`tasks.css`** — retains only task-specific styles:
-- `.swipe-row .task-card`
-- `.swipe-reveal--edit` (blue, tasks only)
-- `.swipe-row--swiping .task-card`
-
-**`shopping.css`** — new shopping-specific styles:
-- `.swipe-row .shopping-item`
-- `.swipe-row--swiping .shopping-item`
-- `.swipe-reveal--delete` (red, `--color-danger`)
-- `@media (max-width: 1023px) .item-delete { display: none }` — × button hidden on mobile, swipe replaces it
-
-## JavaScript Changes (`shopping.js`)
-
-### `renderItem(item)` → wrapped in swipe-row
-
-```html
-
-
-
- Abhaken|Zurück
-
-
-
- Löschen
-
-
-
-```
-
-### New `wireSwipeGestures(container)`
-
-Registers `touchstart` / `touchmove` (passive: false) / `touchend` on each `.swipe-row` inside `#items-list`. Logic mirrors tasks.js:
-
-1. `touchstart`: record `startX`, `startY`, clear `locked` flag
-2. `touchmove`: determine swipe vs. vertical scroll via angle; once locked to swipe, translate card, fade-in appropriate reveal panel proportionally
-3. `touchend`: if locked and `|dx| > SWIPE_THRESHOLD` trigger action; otherwise spring back
-
-Constants (same as tasks.js):
-- `SWIPE_THRESHOLD = 80` px
-- `SWIPE_LOCK_VERT = 8` px
-- `SWIPE_MAX_VERT = 10` px
-
-Called from `renderContent()` after DOM update, alongside existing `wireAutocomplete` and `wireQuickAdd` calls. Also called from `rerenderItems()` after any state change that re-renders the list.
-
-## Out of Scope
-
-- Notes swipe gestures
-- Any other module
-- Undo toast after delete (delete is immediate; existing × button provided undo-less delete already)
-- Desktop swipe via mouse/pointer events