diff --git a/docs/superpowers/specs/2026-04-16-hardening-v2.0.1-design.md b/docs/superpowers/specs/2026-04-16-hardening-v2.0.1-design.md
new file mode 100644
index 0000000..08c4e48
--- /dev/null
+++ b/docs/superpowers/specs/2026-04-16-hardening-v2.0.1-design.md
@@ -0,0 +1,400 @@
+# Hellion NewTab v2.0.1 — Hardening Release Design
+
+**Datum:** 2026-04-16
+**Autor:** Florian Wathling / Claude Code
+**Status:** Approved
+**Scope:** Security, Stability, i18n, Code Quality
+**Strategie:** Foundation First (Event-System zuerst, dann darauf aufbauen)
+
+---
+
+## Kontext
+
+Umfassender Audit von v2.0.0 hat Findings in vier Kategorien ergeben:
+- 3 Sicherheitslücken (HOCH)
+- 2 Stabilitätsprobleme (Race Conditions)
+- 8 fehlende i18n-Attribute
+- 3 Code-Qualität-Items
+
+Dieses Design beschreibt alle Fixes als zusammenhängendes Hardening-Release.
+
+---
+
+## Sektion 1: Widget Event-System
+
+### Problem
+
+Calculator (`calculator.js:692-728`), Timer (`timer.js:723-758`) und ImageRef (`image-ref.js:463-498`) überschreiben `WidgetManager.close`, `.minimize` und `.openWidget` durch Monkey-Patching in ihrer `init()`. Das erzeugt eine 3-stufige Closure-Kette pro Methode. Funktional korrekt, aber fragil und schwer debugbar.
+
+### Lösung
+
+WidgetManager bekommt ein internes Event-System basierend auf `EventTarget`.
+
+**Neue API in `widgets.js`:**
+
+```javascript
+_emitter: new EventTarget(),
+
+on(event, handler) {
+ this._emitter.addEventListener(event, handler);
+},
+
+off(event, handler) {
+ this._emitter.removeEventListener(event, handler);
+},
+```
+
+**Events:**
+
+| Event | Feuert nach | Detail |
+|---|---|---|
+| `widget:close` | `entry.el.remove()` + `_widgets.delete(id)` | `{ id }` | **Achtung:** Element bereits entfernt, Listener dürfen nicht auf Widget-Entry zugreifen |
+| `widget:minimize` | State-Änderung + Animation + Save | `{ id }` |
+| `widget:open` | State-Änderung + Display-Reset + Save | `{ id }` |
+
+**Migration der Widget-Module:**
+
+Das gesamte Monkey-Patching wird ersetzt durch `WidgetManager.on()` Aufrufe:
+
+```javascript
+// Beispiel: Calculator.init()
+WidgetManager.on('widget:close', (e) => {
+ if (e.detail.id === self.WIDGET_ID) self.onClose();
+});
+WidgetManager.on('widget:minimize', (e) => {
+ if (e.detail.id === self.WIDGET_ID) {
+ self._isOpen = false;
+ self.save();
+ }
+});
+WidgetManager.on('widget:open', (e) => {
+ if (e.detail.id === self.WIDGET_ID) {
+ self._isOpen = true;
+ const body = WidgetManager.getBody(self.WIDGET_ID);
+ if (body && body.children.length === 0) self.renderBody(body);
+ self.save();
+ }
+});
+```
+
+ImageRef folgt dem gleichen Pattern, prüft aber per `self._images.some(img => img.id === id)` statt gegen eine feste WIDGET_ID.
+
+**Load-Order:** Kein Problem. `widgets.js` wird vor allen Widget-Modulen geladen. Die Module rufen `WidgetManager.on()` in ihrer `init()` auf, die erst in `app.js` aufgerufen wird.
+
+### Betroffene Dateien
+
+- `src/js/widgets.js` — Event-System hinzufügen, Events in close/minimize/openWidget dispatchen
+- `src/js/calculator.js` — Monkey-Patching (Z. 692-728) durch Event-Listener ersetzen
+- `src/js/timer.js` — Monkey-Patching (Z. 723-758) durch Event-Listener ersetzen
+- `src/js/image-ref.js` — Monkey-Patching (Z. 463-498) durch Event-Listener ersetzen
+
+---
+
+## Sektion 2: Minimize-Animation mit `transitionend`
+
+### Problem
+
+`WidgetManager.minimize()` (`widgets.js:154-163`) setzt `display: none` nach 250ms `setTimeout`. Wenn `openWidget()` in diesen 250ms aufgerufen wird, überschreibt der Timeout das `display: flex` wieder (Race Condition).
+
+### Lösung
+
+`setTimeout` wird durch `transitionend` Event ersetzt. Eine `_minimizing` Flag verhindert die Race Condition.
+
+```javascript
+async minimize(id) {
+ const entry = this._widgets.get(id);
+ if (!entry) return;
+ entry.state.open = false;
+ entry._minimizing = true;
+ entry.el.classList.add('widget-minimized');
+
+ entry.el.addEventListener('transitionend', function onEnd(e) {
+ if (e.target !== entry.el) return;
+ entry.el.removeEventListener('transitionend', onEnd);
+ if (entry._minimizing) {
+ entry.el.style.display = 'none';
+ }
+ entry._minimizing = false;
+ }, { once: false });
+
+ this._emitter.dispatchEvent(new CustomEvent('widget:minimize', { detail: { id } }));
+ await this.save();
+},
+
+async openWidget(id) {
+ const entry = this._widgets.get(id);
+ if (!entry) return;
+ entry._minimizing = false; // Race Condition verhindert
+ entry.state.open = true;
+ entry.el.style.display = 'flex';
+ requestAnimationFrame(() => {
+ entry.el.classList.remove('widget-minimized');
+ });
+ this.bringToFront(id);
+ this._emitter.dispatchEvent(new CustomEvent('widget:open', { detail: { id } }));
+ await this.save();
+},
+```
+
+**Warum `_minimizing` Flag:** Robuster als `clearTimeout`, weil sie unabhängig von der CSS-Transition-Duration funktioniert.
+
+**Fallback:** Falls `transitionend` nicht feuert (kein Transition definiert), bleibt das Widget sichtbar mit der Klasse. Akzeptabel, da alle Widgets in `main.css` eine Transition haben.
+
+### Betroffene Dateien
+
+- `src/js/widgets.js` — `minimize()` und `openWidget()` umschreiben
+
+---
+
+## Sektion 3: Security Fixes
+
+### 3a: URL-Injection in backgroundImage
+
+**Datei:** `src/js/settings.js:93`
+**Problem:** `settings.bgUrl` wird unvalidiert in CSS-Template-Literal eingefügt.
+
+**Fix:** Protokoll-Whitelist. Nur `blob:` und `data:image/` erlauben (die einzigen Protokolle die der Upload erzeugt).
+
+```javascript
+function isValidBgUrl(url) {
+ return typeof url === 'string' &&
+ (url.startsWith('blob:') || url.startsWith('data:image/'));
+}
+```
+
+Validierung an zwei Stellen: `applySettings()` und beim Speichern nach Upload.
+
+### 3b: URL-Validierung beim JSON-Import
+
+**Datei:** `src/js/data.js:45-49`
+**Problem:** Importierte Bookmark-URLs werden nicht auf Protokoll geprüft. `javascript:` oder `data:` URLs kommen durch.
+
+**Fix:** Protokoll-Whitelist für importierte URLs.
+
+```javascript
+function isSafeUrl(url) {
+ try {
+ const u = new URL(url);
+ return ['http:', 'https:', 'ftp:'].includes(u.protocol);
+ } catch {
+ return false;
+ }
+}
+```
+
+Integration in die Bookmark-Filter-Logik: `if (!bm || typeof bm.title !== 'string' || !isSafeUrl(bm.url)) return false;`
+
+Ungültige Bookmarks werden still übersprungen.
+
+### 3c: Objekt-Mutation im Import
+
+**Datei:** `src/js/data.js:43-48`
+**Problem:** `b.id = b.id || uid()` mutiert das geparste JSON-Objekt direkt. Keine Längenvalidierung.
+
+**Fix:** Immutable Mapping mit expliziter Feldauswahl und String-Längen-Limits.
+
+```javascript
+.map(bm => ({
+ id: bm.id || uid(),
+ title: String(bm.title).slice(0, 200),
+ url: bm.url,
+ desc: String(bm.desc || '').slice(0, 500)
+}));
+```
+
+Analog für Boards:
+
+```javascript
+.map(b => ({
+ id: b.id || uid(),
+ title: String(b.title).slice(0, 100),
+ blurred: !!b.blurred,
+ bookmarks: /* bereits sanitized, siehe oben */
+}));
+```
+
+Notes-Felder beim Import werden ebenfalls sanitized:
+
+```javascript
+.filter(n => n && n.id && n.template)
+.map(n => ({
+ id: n.id,
+ template: ['note', 'checklist'].includes(n.template) ? n.template : 'note',
+ title: String(n.title || '').slice(0, 200),
+ content: String(n.content || '').slice(0, 5000),
+ checklistItems: Array.isArray(n.checklistItems) ? n.checklistItems : []
+}));
+```
+
+### Betroffene Dateien
+
+- `src/js/settings.js` — `isValidBgUrl()` + Validierung in `applySettings()`
+- `src/js/data.js` — `isSafeUrl()` + immutable Mapping + Längen-Limits
+
+---
+
+## Sektion 4: Lokale Favicons
+
+### Problem
+
+`getFaviconUrl()` (`state.js:36-43`) ruft Google Favicons API auf. Brave Shields blockiert das. Jeder Bookmark erzeugt einen fehlgeschlagenen Netzwerk-Request. Zusätzlich leakt jeder Hostname an Google.
+
+### Lösung
+
+Kein externer Request mehr. `getFaviconUrl()` wird entfernt. Bookmarks zeigen ein farbiges Buchstaben-Icon (erster Buchstabe des Titels).
+
+**state.js:** `getFaviconUrl()` löschen.
+
+**boards.js:** Statt `` + Error-Fallback nur noch ein `