diff --git a/docs/superpowers/plans/2026-04-16-calculator-upgrade.md b/docs/superpowers/plans/2026-04-16-calculator-upgrade.md
new file mode 100644
index 0000000..8dea1c0
--- /dev/null
+++ b/docs/superpowers/plans/2026-04-16-calculator-upgrade.md
@@ -0,0 +1,3068 @@
+# Calculator Upgrade v2.1.0 — 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:** Extend the Calculator widget with 5 new modes — Scientific, Unit Converter, Satisfactory, Factorio, Stationeers — using a tab-based architecture with mode registration pattern.
+
+**Architecture:** Each mode lives in its own IIFE file that calls `Calculator.registerMode()`. The core `calculator.js` gains a `_modes` Map, tab-bar rendering, and `switchMode()` logic. The Shunting-Yard parser is extended with `^` (power) and `sqrt()` (function). All new files load between `calculator.js` and `timer.js` in newtab.html.
+
+**Tech Stack:** Vanilla JS ES2020, CSS Custom Properties, chrome.storage.local via Store, i18n via t() helper
+
+**Spec:** `docs/superpowers/specs/2026-04-16-calculator-upgrade-design.md`
+
+---
+
+### Task 1: Tab-System im Calculator Core
+
+**Files:**
+- Modify: `src/js/calculator.js` (全 Datei — Tab-System, registerMode, switchMode, Standard-Mode-Extraktion)
+- Modify: `src/css/main.css:1284` (neue CSS-Klassen nach bestehenden Calculator-Styles)
+
+**Kontext:** Aktuell rendert `Calculator.renderBody()` direkt das Standard-UI (Display + Buttons + History). Wir refactoren das zu einem Tab-System: `renderBody()` baut Tab-Bar + Mode-Container, dann delegiert an den aktiven Modus. Der Standard-Modus wird als interner Modus registriert. Externe Modi registrieren sich per `Calculator.registerMode()`.
+
+- [ ] **Step 1: Neue Properties und registerMode() hinzufügen**
+
+In `calculator.js`, nach `_keydownHandler: null,` (Zeile 19) folgende Properties ergänzen:
+
+```javascript
+_modes: new Map(),
+_activeMode: 'standard',
+_tabBarEl: null,
+```
+
+Neue Methode nach `_keydownHandler` Block:
+
+```javascript
+/**
+ * Modus registrieren (wird von externen Mode-Dateien aufgerufen)
+ * @param {string} name - Eindeutiger Modus-Name
+ * @param {Object} config - { label, shortName, titleKey, render(bodyEl), destroy() }
+ */
+registerMode(name, config) {
+ this._modes.set(name, config);
+ // Tab-Bar aktualisieren falls Widget bereits offen
+ if (this._tabBarEl) this._renderTabBar();
+},
+```
+
+- [ ] **Step 2: Standard-Modus als internen Modus registrieren**
+
+Am Ende der `init()` Methode (vor dem Schließen des Objekts), nach den Event-Listener-Registrierungen, den Standard-Modus registrieren:
+
+```javascript
+// Standard-Modus intern registrieren
+this._modes.set('standard', {
+ label: '🔢',
+ shortName: 'Std',
+ titleKey: 'calculator.tab.standard',
+ render: (bodyEl) => this._renderStandardMode(bodyEl),
+ destroy: () => {
+ // Standard-Modus hat keinen speziellen Cleanup
+ this._displayExprEl = null;
+ this._displayResultEl = null;
+ }
+});
+```
+
+- [ ] **Step 3: renderBody() refactoren — Tab-Bar + Mode-Container**
+
+Die bestehende `renderBody()` Methode komplett ersetzen:
+
+```javascript
+/**
+ * Calculator-Body rendern: Tab-Bar + aktiver Modus
+ * @param {HTMLElement} bodyEl
+ */
+renderBody(bodyEl) {
+ bodyEl.textContent = '';
+ bodyEl.style.padding = '0';
+ bodyEl.style.display = 'flex';
+ bodyEl.style.flexDirection = 'column';
+ bodyEl.style.height = '100%';
+
+ // Tab-Bar
+ const tabBar = document.createElement('div');
+ tabBar.className = 'calc-tab-bar';
+ this._tabBarEl = tabBar;
+ this._renderTabBar();
+
+ // Mode-Body Container
+ const modeBody = document.createElement('div');
+ modeBody.className = 'calc-mode-body';
+
+ bodyEl.append(tabBar, modeBody);
+
+ // Aktiven Modus rendern
+ const mode = this._modes.get(this._activeMode);
+ if (mode) {
+ mode.render(modeBody);
+ }
+},
+```
+
+- [ ] **Step 4: _renderTabBar() und _updateTabBar() erstellen**
+
+```javascript
+/**
+ * Tab-Bar mit Buttons aus _modes Map befüllen
+ */
+_renderTabBar() {
+ if (!this._tabBarEl) return;
+ // Alle bisherigen Tabs entfernen
+ while (this._tabBarEl.firstChild) {
+ this._tabBarEl.removeChild(this._tabBarEl.firstChild);
+ }
+
+ this._modes.forEach((config, name) => {
+ const tab = document.createElement('button');
+ tab.type = 'button';
+ tab.className = 'calc-tab' + (name === this._activeMode ? ' active' : '');
+ tab.dataset.mode = name;
+
+ const icon = document.createElement('span');
+ icon.className = 'calc-tab-icon';
+ icon.textContent = config.label;
+
+ const label = document.createElement('span');
+ label.className = 'calc-tab-label';
+ label.textContent = config.shortName;
+
+ tab.append(icon, label);
+ tab.addEventListener('click', () => this.switchMode(name));
+ this._tabBarEl.appendChild(tab);
+ });
+},
+
+/**
+ * Aktiven Tab visuell markieren (ohne Neuaufbau)
+ */
+_updateTabBar() {
+ if (!this._tabBarEl) return;
+ const tabs = this._tabBarEl.querySelectorAll('.calc-tab');
+ tabs.forEach(tab => {
+ tab.classList.toggle('active', tab.dataset.mode === this._activeMode);
+ });
+},
+```
+
+- [ ] **Step 5: switchMode() erstellen**
+
+```javascript
+/**
+ * Modus wechseln
+ * @param {string} name - Ziel-Modus
+ */
+async switchMode(name) {
+ if (name === this._activeMode) return;
+ const mode = this._modes.get(name);
+ if (!mode) return;
+
+ // Alten Modus aufräumen
+ const oldMode = this._modes.get(this._activeMode);
+ if (oldMode && oldMode.destroy) oldMode.destroy();
+
+ this._activeMode = name;
+
+ // Mode-Body leeren und neu rendern
+ const entry = WidgetManager._widgets.get(this.WIDGET_ID);
+ if (!entry) return;
+ const modeBody = entry.el.querySelector('.calc-mode-body');
+ if (!modeBody) return;
+ modeBody.textContent = '';
+ mode.render(modeBody);
+
+ // Tab-UI aktualisieren
+ this._updateTabBar();
+
+ // Auto-Resize für komplexe Modi
+ const isComplex = name !== 'standard';
+ if (isComplex) {
+ const state = WidgetManager.getState(this.WIDGET_ID);
+ if (state) {
+ const newW = Math.max(state.width, 320);
+ const newH = Math.max(state.height, 480);
+ if (newW !== state.width || newH !== state.height) {
+ WidgetManager.resize(this.WIDGET_ID, newW, newH);
+ }
+ }
+ }
+
+ // Keyboard neu binden
+ this._unbindKeyboard();
+ if (name === 'standard' || name === 'scientific') {
+ if (entry) this._bindKeyboard(entry.el);
+ }
+
+ await this.save();
+},
+```
+
+- [ ] **Step 6: _renderStandardMode() extrahieren**
+
+Neue Methode, die den bisherigen `renderBody()` Inhalt enthält (Display + Buttons + History):
+
+```javascript
+/**
+ * Standard-Modus UI rendern
+ * @param {HTMLElement} bodyEl
+ */
+_renderStandardMode(bodyEl) {
+ bodyEl.style.padding = '8px';
+ bodyEl.style.display = 'flex';
+ bodyEl.style.flexDirection = 'column';
+ bodyEl.style.flex = '1';
+ bodyEl.style.overflow = 'hidden';
+
+ // Display
+ const display = document.createElement('div');
+ display.className = 'calc-display';
+
+ const exprEl = document.createElement('div');
+ exprEl.className = 'calc-expression';
+ this._displayExprEl = exprEl;
+
+ const resultEl = document.createElement('div');
+ resultEl.className = 'calc-result';
+ resultEl.textContent = '0';
+ this._displayResultEl = resultEl;
+
+ display.append(exprEl, resultEl);
+
+ // Buttons
+ const buttonsEl = this._createButtons();
+
+ // History
+ const historyEl = this._createHistoryPanel();
+
+ bodyEl.append(display, buttonsEl, historyEl);
+
+ // Aktuellen State anzeigen
+ this._updateDisplay();
+},
+```
+
+- [ ] **Step 7: save() und load() um activeMode erweitern**
+
+In `save()`, `calcData` um `activeMode` erweitern:
+
+```javascript
+const calcData = {
+ x: widgetState ? widgetState.x : 400,
+ y: widgetState ? widgetState.y : 120,
+ width: widgetState ? widgetState.width : 280,
+ height: widgetState ? widgetState.height : 400,
+ open: this._isOpen,
+ activeMode: this._activeMode,
+ history: this._history.slice(0, this.MAX_HISTORY)
+};
+```
+
+In `load()`, activeMode wiederherstellen:
+
+```javascript
+async load() {
+ const data = await Store.get(this.STORAGE_KEY);
+ if (data && data.calculator) {
+ this._history = Array.isArray(data.calculator.history) ? data.calculator.history : [];
+ if (data.calculator.activeMode) {
+ this._activeMode = data.calculator.activeMode;
+ }
+ }
+},
+```
+
+In `onClose()`, alten Modus aufräumen:
+
+```javascript
+async onClose() {
+ // Aktiven Modus aufräumen
+ const mode = this._modes.get(this._activeMode);
+ if (mode && mode.destroy) mode.destroy();
+
+ this._isOpen = false;
+ this._unbindKeyboard();
+ this._tabBarEl = null;
+ this._displayExprEl = null;
+ this._displayResultEl = null;
+ await this.save();
+},
+```
+
+- [ ] **Step 8: CSS für Tab-Bar und Mode-Body**
+
+In `src/css/main.css`, nach dem `.calc-history-item .calc-h-result` Block (Zeile 1283), vor den Timer-Styles einfügen:
+
+```css
+/* Calculator Tab System */
+.calc-tab-bar {
+ display: flex;
+ background: rgba(0,0,0,0.2);
+ border-bottom: 1px solid var(--border);
+ overflow-x: auto;
+ scrollbar-width: none;
+ flex-shrink: 0;
+}
+.calc-tab-bar::-webkit-scrollbar {
+ display: none;
+}
+.calc-tab {
+ display: flex;
+ align-items: center;
+ gap: 3px;
+ padding: 6px 8px;
+ border: none;
+ border-bottom: 2px solid transparent;
+ background: none;
+ color: var(--text-muted);
+ font-size: 11px;
+ font-family: 'Rajdhani', sans-serif;
+ cursor: pointer;
+ white-space: nowrap;
+ transition: color 0.15s, border-color 0.15s;
+ flex-shrink: 0;
+}
+.calc-tab:hover {
+ color: var(--text-secondary);
+}
+.calc-tab.active {
+ color: var(--accent);
+ border-bottom-color: var(--accent);
+ font-weight: 600;
+}
+.calc-tab-icon {
+ font-size: 12px;
+}
+.calc-tab-label {
+ font-size: 10px;
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+}
+.calc-mode-body {
+ flex: 1;
+ overflow-y: auto;
+ overflow-x: hidden;
+}
+```
+
+- [ ] **Step 9: i18n-Key für Standard-Tab**
+
+In `src/js/i18n.js`, nach `'calculator.error'` in beiden Sprachen:
+
+DE:
+```javascript
+'calculator.tab.standard': 'Standard',
+```
+
+EN:
+```javascript
+'calculator.tab.standard': 'Standard',
+```
+
+- [ ] **Step 10: Testen**
+
+1. Extension in Chrome laden
+2. Calculator öffnen → Tab-Bar mit einem Tab "🔢 Std" sichtbar
+3. Calculator schließen und wieder öffnen → State bleibt erhalten
+4. Standard-Rechner funktioniert wie vorher (Buttons, History, Keyboard)
+
+- [ ] **Step 11: Commit**
+
+```bash
+git add src/js/calculator.js src/css/main.css src/js/i18n.js
+git commit -m "feat(calculator): Tab-System mit registerMode() und switchMode()"
+```
+
+---
+
+### Task 2: Parser-Erweiterung — ^ und sqrt
+
+**Files:**
+- Modify: `src/js/calculator.js` — `_evaluate()`, `_tokenize()`, `_parseExpression()` innere Funktionen
+
+**Kontext:** Der Shunting-Yard-Parser kennt aktuell nur `+`, `-`, `*`, `/`, `%`. Wir fügen `^` (Potenz, rechts-assoziativ) und `sqrt` (unäre Funktion) hinzu. Die neue Hierarchie: `parseExpr(+,-)` → `parseTerm(*,/,%)` → `parsePower(^)` → `parseFactor(number | parens | func)`.
+
+- [ ] **Step 1: Sanitizer erweitern**
+
+In `_evaluate()`, die Sanitizer-Regex erweitern um `^` und Buchstaben (für `sqrt`):
+
+```javascript
+_evaluate(expr) {
+ try {
+ // Nur erlaubte Zeichen (inkl. ^ und sqrt-Buchstaben)
+ const sanitized = expr.replace(/[^0-9+\-*/.%()^a-z]/g, '');
+ if (!sanitized) return null;
+
+ const tokens = this._tokenize(sanitized);
+ if (!tokens) return null;
+
+ return this._parseExpression(tokens);
+ } catch {
+ return null;
+ }
+},
+```
+
+- [ ] **Step 2: Tokenizer um ^ und sqrt erweitern**
+
+In `_tokenize()`, nach dem Operator-Block (`if (/[+\-*/%]/.test(ch))`) und vor dem Klammern-Block, `^` als Operator erkennen. Vor dem Zahlen-Block `sqrt` als Funktion erkennen:
+
+```javascript
+_tokenize(expr) {
+ const tokens = [];
+ let i = 0;
+
+ while (i < expr.length) {
+ const ch = expr[i];
+
+ // Funktion: sqrt
+ if (expr.substring(i, i + 4) === 'sqrt') {
+ tokens.push({ type: 'func', value: 'sqrt' });
+ i += 4;
+ continue;
+ }
+
+ // Zahl (inkl. Dezimal)
+ if (/[0-9.]/.test(ch)) {
+ let num = '';
+ while (i < expr.length && /[0-9.]/.test(expr[i])) {
+ num += expr[i];
+ i++;
+ }
+ const parsed = parseFloat(num);
+ if (isNaN(parsed)) return null;
+ tokens.push({ type: 'number', value: parsed });
+ continue;
+ }
+
+ // Potenz-Operator
+ if (ch === '^') {
+ tokens.push({ type: 'op', value: '^' });
+ i++;
+ continue;
+ }
+
+ // Operator
+ if (/[+\-*/%]/.test(ch)) {
+ // Negativer Vorzeichen-Check: am Anfang oder nach Operator/oeffnender Klammer
+ if (ch === '-') {
+ const prev = tokens[tokens.length - 1];
+ if (!prev || prev.type === 'op' || (prev.type === 'paren' && prev.value === '(')) {
+ let num = '-';
+ i++;
+ while (i < expr.length && /[0-9.]/.test(expr[i])) {
+ num += expr[i];
+ i++;
+ }
+ if (num === '-') return null;
+ const parsed = parseFloat(num);
+ if (isNaN(parsed)) return null;
+ tokens.push({ type: 'number', value: parsed });
+ continue;
+ }
+ }
+ tokens.push({ type: 'op', value: ch });
+ i++;
+ continue;
+ }
+
+ // Klammern
+ if (ch === '(' || ch === ')') {
+ tokens.push({ type: 'paren', value: ch });
+ i++;
+ continue;
+ }
+
+ // Unbekannte Buchstaben überspringen (können Reste von Funktionsnamen sein)
+ if (/[a-z]/.test(ch)) {
+ return null;
+ }
+
+ // Unbekanntes Zeichen
+ return null;
+ }
+
+ return tokens;
+},
+```
+
+- [ ] **Step 3: Parser um parsePower() und Funktions-Support erweitern**
+
+Die `_parseExpression()` Methode komplett ersetzen. `parseTerm()` ruft jetzt `parsePower()` statt `parseFactor()` auf. `parsePower()` ist rechts-assoziativ. `parseFactor()` prüft auf Funktionen:
+
+```javascript
+_parseExpression(tokens) {
+ let pos = 0;
+
+ function peek() { return tokens[pos]; }
+ function consume() { return tokens[pos++]; }
+
+ // Expression: Term (('+' | '-') Term)*
+ function parseExpr() {
+ let left = parseTerm();
+ if (left === null) return null;
+
+ while (pos < tokens.length) {
+ const tk = peek();
+ if (!tk || tk.type !== 'op' || (tk.value !== '+' && tk.value !== '-')) break;
+ consume();
+ const right = parseTerm();
+ if (right === null) return null;
+ left = tk.value === '+' ? left + right : left - right;
+ }
+ return left;
+ }
+
+ // Term: Power (('*' | '/' | '%') Power)*
+ function parseTerm() {
+ let left = parsePower();
+ if (left === null) return null;
+
+ while (pos < tokens.length) {
+ const tk = peek();
+ if (!tk || tk.type !== 'op' || (tk.value !== '*' && tk.value !== '/' && tk.value !== '%')) break;
+ consume();
+ const right = parsePower();
+ if (right === null) return null;
+ if (tk.value === '*') {
+ left = left * right;
+ } else if (tk.value === '/') {
+ if (right === 0) return null;
+ left = left / right;
+ } else {
+ left = left % right;
+ }
+ }
+ return left;
+ }
+
+ // Power: Factor ('^' Power)? — rechts-assoziativ durch Rekursion
+ function parsePower() {
+ let base = parseFactor();
+ if (base === null) return null;
+
+ const tk = peek();
+ if (tk && tk.type === 'op' && tk.value === '^') {
+ consume();
+ const exp = parsePower(); // Rechts-assoziativ!
+ if (exp === null) return null;
+ return Math.pow(base, exp);
+ }
+ return base;
+ }
+
+ // Factor: func '(' Expression ')' | Number | '(' Expression ')'
+ function parseFactor() {
+ const tk = peek();
+ if (!tk) return null;
+
+ // Funktion: sqrt(...)
+ if (tk.type === 'func') {
+ const funcName = tk.value;
+ consume();
+ // Erwarte öffnende Klammer
+ const open = peek();
+ if (!open || open.type !== 'paren' || open.value !== '(') return null;
+ consume();
+ const val = parseExpr();
+ if (val === null) return null;
+ const close = peek();
+ if (close && close.type === 'paren' && close.value === ')') {
+ consume();
+ }
+ if (funcName === 'sqrt') {
+ if (val < 0) return null; // Keine negativen Wurzeln
+ return Math.sqrt(val);
+ }
+ return null;
+ }
+
+ if (tk.type === 'number') {
+ consume();
+ return tk.value;
+ }
+
+ if (tk.type === 'paren' && tk.value === '(') {
+ consume();
+ const val = parseExpr();
+ if (val === null) return null;
+ const closing = peek();
+ if (closing && closing.type === 'paren' && closing.value === ')') {
+ consume();
+ }
+ return val;
+ }
+
+ return null;
+ }
+
+ const result = parseExpr();
+
+ // Alle Tokens muessen verbraucht sein
+ if (pos < tokens.length) return null;
+
+ if (result === null || !isFinite(result)) return null;
+ return result;
+},
+```
+
+- [ ] **Step 4: _handleKey() um ^ erweitern**
+
+Im `default`-case der `_handleKey()` Methode, nach dem Ziffern-Check, `^` als Operator behandeln. In den Operator-Case (die bestehende `case '%': case '+': ...` Kette), `'^'` hinzufügen:
+
+```javascript
+case '%':
+case '+':
+case '-':
+case '*':
+case '/':
+case '^': {
+ // Wenn gerade ein Ergebnis angezeigt wird, damit weiterrechnen
+ if (this._lastResult && this._currentExpr === '') {
+ this._currentExpr = this._lastResult;
+ this._lastResult = '';
+ }
+ // Doppelte Operatoren verhindern (letzten ersetzen)
+ const last = this._currentExpr.slice(-1);
+ if (/[+\-*/%^]/.test(last)) {
+ this._currentExpr = this._currentExpr.slice(0, -1) + key;
+ } else {
+ this._currentExpr += key;
+ }
+ break;
+}
+```
+
+- [ ] **Step 5: _formatExpression() um ^ erweitern**
+
+```javascript
+_formatExpression(expr) {
+ return expr
+ .replace(/\*/g, '\u00D7')
+ .replace(/\//g, '\u00F7')
+ .replace(/sqrt\(/g, '√(');
+},
+```
+
+- [ ] **Step 6: Testen**
+
+1. Calculator öffnen
+2. `2^10` eingeben → Ergebnis: 1024
+3. `2^3^2` eingeben → Ergebnis: 512 (rechts-assoziativ)
+4. In Browser-Console: Calculator._evaluate('sqrt(144)') → 12
+5. `3+sqrt(16)` → 7
+6. `sqrt(-4)` → Fehler
+
+- [ ] **Step 7: Commit**
+
+```bash
+git add src/js/calculator.js
+git commit -m "feat(calculator): Parser um ^ (Potenz) und sqrt() erweitern"
+```
+
+---
+
+### Task 3: Scientific-Modus
+
+**Files:**
+- Create: `src/js/calc-scientific.js`
+- Modify: `newtab.html:509` (Script-Tag nach calculator.js einfügen)
+- Modify: `src/js/i18n.js` (neue Keys)
+- Modify: `src/css/main.css` (Scientific-spezifische Styles)
+
+**Kontext:** Der Scientific-Modus erweitert den Standard-Rechner um 6 wissenschaftliche Buttons (√, x², xⁿ, π, e, ±) und einen Formel-Helfer mit 6 vorgefertigten Formeln. Er nutzt den gleichen `_handleKey()`/`_calculate()` Flow des Cores.
+
+- [ ] **Step 1: calc-scientific.js erstellen**
+
+```javascript
+/* =============================================
+ HELLION NEWTAB — calc-scientific.js
+ Scientific-Modus für Calculator Widget
+ ============================================= */
+
+(function() {
+ 'use strict';
+
+ /** Formel-Definitionen */
+ const FORMULAS = [
+ {
+ key: 'circle_area',
+ fields: [{ key: 'radius', default: '' }],
+ calc: (vals) => Math.PI * vals.radius * vals.radius
+ },
+ {
+ key: 'circle_circumference',
+ fields: [{ key: 'radius', default: '' }],
+ calc: (vals) => 2 * Math.PI * vals.radius
+ },
+ {
+ key: 'celsius_to_fahrenheit',
+ fields: [{ key: 'temp', default: '' }],
+ calc: (vals) => (vals.temp * 9 / 5) + 32
+ },
+ {
+ key: 'fahrenheit_to_celsius',
+ fields: [{ key: 'temp', default: '' }],
+ calc: (vals) => (vals.temp - 32) * 5 / 9
+ },
+ {
+ key: 'pythagoras',
+ fields: [{ key: 'a', default: '' }, { key: 'b', default: '' }],
+ calc: (vals) => Math.sqrt(vals.a * vals.a + vals.b * vals.b)
+ },
+ {
+ key: 'percentage',
+ fields: [{ key: 'value', default: '' }, { key: 'percent', default: '' }],
+ calc: (vals) => vals.value * vals.percent / 100
+ }
+ ];
+
+ /** Keyboard-Handler Referenz für Cleanup */
+ let _keyboardExtHandler = null;
+
+ /**
+ * Scientific-Buttons rendern
+ * @param {HTMLElement} container
+ */
+ function renderSciButtons(container) {
+ const grid = document.createElement('div');
+ grid.className = 'calc-buttons calc-sci-buttons';
+
+ const buttons = [
+ ['√', 'sqrt', 'operator'],
+ ['x²', 'square', 'operator'],
+ ['xⁿ', 'power', 'operator'],
+ ['π', 'pi', 'operator'],
+ ['e', 'euler', 'operator'],
+ ['±', 'negate', 'operator']
+ ];
+
+ buttons.forEach(([label, value, cls]) => {
+ const btn = document.createElement('button');
+ btn.className = 'calc-btn' + (cls ? ' ' + cls : '');
+ btn.textContent = label;
+ btn.type = 'button';
+ btn.addEventListener('click', () => handleSciKey(value));
+ grid.appendChild(btn);
+ });
+
+ container.appendChild(grid);
+ }
+
+ /**
+ * Scientific-Taste verarbeiten
+ * @param {string} key
+ */
+ function handleSciKey(key) {
+ switch (key) {
+ case 'sqrt':
+ Calculator._currentExpr += 'sqrt(';
+ Calculator._updateDisplay();
+ break;
+ case 'square':
+ Calculator._currentExpr += '^2';
+ Calculator._updateDisplay();
+ break;
+ case 'power':
+ Calculator._handleKey('^');
+ break;
+ case 'pi':
+ Calculator._currentExpr += '3.14159265359';
+ Calculator._updateDisplay();
+ break;
+ case 'euler':
+ Calculator._currentExpr += '2.71828182846';
+ Calculator._updateDisplay();
+ break;
+ case 'negate':
+ handleNegate();
+ break;
+ }
+ }
+
+ /**
+ * Vorzeichen-Wechsel: letzten numerischen Wert negieren
+ */
+ function handleNegate() {
+ const expr = Calculator._currentExpr;
+ if (!expr && Calculator._lastResult) {
+ // Ergebnis negieren
+ const num = parseFloat(Calculator._lastResult);
+ if (!isNaN(num)) {
+ Calculator._currentExpr = String(-num);
+ Calculator._lastResult = '';
+ Calculator._updateDisplay();
+ }
+ return;
+ }
+ // Letzte Zahl in der Expression finden und negieren
+ const match = expr.match(/(-?\d+\.?\d*)$/);
+ if (match) {
+ const num = parseFloat(match[1]);
+ const negated = String(-num);
+ Calculator._currentExpr = expr.slice(0, expr.length - match[1].length) + negated;
+ Calculator._updateDisplay();
+ }
+ }
+
+ /**
+ * Formel-Helfer UI rendern
+ * @param {HTMLElement} container
+ */
+ function renderFormulaHelper(container) {
+ const wrapper = document.createElement('div');
+ wrapper.className = 'calc-formula-helper';
+
+ const label = document.createElement('div');
+ label.className = 'calc-formula-label';
+ label.textContent = t('calculator.sci.formulas');
+
+ const select = document.createElement('select');
+ select.className = 'calc-formula-select';
+
+ // Leer-Option
+ const emptyOpt = document.createElement('option');
+ emptyOpt.value = '';
+ emptyOpt.textContent = t('calculator.sci.select_formula');
+ select.appendChild(emptyOpt);
+
+ FORMULAS.forEach((f, i) => {
+ const opt = document.createElement('option');
+ opt.value = String(i);
+ opt.textContent = t('calculator.sci.formula.' + f.key);
+ select.appendChild(opt);
+ });
+
+ const inputsContainer = document.createElement('div');
+ inputsContainer.className = 'calc-formula-inputs';
+
+ const resultContainer = document.createElement('div');
+ resultContainer.className = 'calc-formula-result';
+
+ select.addEventListener('change', () => {
+ // Inputs leeren
+ while (inputsContainer.firstChild) {
+ inputsContainer.removeChild(inputsContainer.firstChild);
+ }
+ resultContainer.textContent = '';
+
+ const idx = parseInt(select.value, 10);
+ if (isNaN(idx)) return;
+
+ const formula = FORMULAS[idx];
+ renderFormulaInputs(formula, inputsContainer, resultContainer);
+ });
+
+ wrapper.append(label, select, inputsContainer, resultContainer);
+ container.appendChild(wrapper);
+ }
+
+ /**
+ * Formel-Eingabefelder und Live-Ergebnis rendern
+ * @param {Object} formula
+ * @param {HTMLElement} inputsEl
+ * @param {HTMLElement} resultEl
+ */
+ function renderFormulaInputs(formula, inputsEl, resultEl) {
+ const inputs = {};
+
+ formula.fields.forEach(field => {
+ const row = document.createElement('div');
+ row.className = 'calc-formula-row';
+
+ const lbl = document.createElement('label');
+ lbl.textContent = t('calculator.sci.field.' + field.key);
+
+ const inp = document.createElement('input');
+ inp.type = 'number';
+ inp.className = 'calc-formula-input';
+ inp.placeholder = '0';
+ inp.step = 'any';
+ inputs[field.key] = inp;
+
+ inp.addEventListener('input', () => {
+ recalcFormula(formula, inputs, resultEl);
+ });
+
+ row.append(lbl, inp);
+ inputsEl.appendChild(row);
+ });
+ }
+
+ /**
+ * Formel-Ergebnis live berechnen
+ * @param {Object} formula
+ * @param {Object} inputs - { key: HTMLInputElement }
+ * @param {HTMLElement} resultEl
+ */
+ function recalcFormula(formula, inputs, resultEl) {
+ const vals = {};
+ let allValid = true;
+
+ for (const field of formula.fields) {
+ const v = parseFloat(inputs[field.key].value);
+ if (isNaN(v)) { allValid = false; break; }
+ vals[field.key] = v;
+ }
+
+ if (!allValid) {
+ resultEl.textContent = '';
+ return;
+ }
+
+ const result = formula.calc(vals);
+ if (result === null || !isFinite(result)) {
+ resultEl.textContent = t('calculator.error');
+ return;
+ }
+
+ resultEl.textContent = '= ' + Calculator._formatResult(result);
+ }
+
+ /**
+ * Keyboard-Erweiterung für Scientific-Modus
+ * @param {HTMLElement} widgetEl
+ */
+ function bindSciKeyboard(widgetEl) {
+ _keyboardExtHandler = (e) => {
+ if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
+ if (e.target.contentEditable === 'true') return;
+
+ if (e.key === 'p') {
+ handleSciKey('pi');
+ e.preventDefault();
+ e.stopPropagation();
+ } else if (e.key === '^') {
+ handleSciKey('power');
+ e.preventDefault();
+ e.stopPropagation();
+ }
+ };
+ widgetEl.addEventListener('keydown', _keyboardExtHandler);
+ }
+
+ // Modus registrieren
+ Calculator.registerMode('scientific', {
+ label: '📐',
+ shortName: 'Sci',
+ titleKey: 'calculator.tab.scientific',
+
+ render(bodyEl) {
+ bodyEl.style.padding = '8px';
+ bodyEl.style.display = 'flex';
+ bodyEl.style.flexDirection = 'column';
+ bodyEl.style.flex = '1';
+ bodyEl.style.overflow = 'hidden';
+
+ // Display (gemeinsam mit Standard)
+ const display = document.createElement('div');
+ display.className = 'calc-display';
+
+ const exprEl = document.createElement('div');
+ exprEl.className = 'calc-expression';
+ Calculator._displayExprEl = exprEl;
+
+ const resultEl = document.createElement('div');
+ resultEl.className = 'calc-result';
+ resultEl.textContent = Calculator._lastResult || '0';
+ Calculator._displayResultEl = resultEl;
+
+ display.append(exprEl, resultEl);
+
+ // Scientific-Buttons (2x3 Grid)
+ const sciSection = document.createElement('div');
+ renderSciButtons(sciSection);
+
+ // Standard-Buttons (4x5 Grid)
+ const stdButtons = Calculator._createButtons();
+
+ // History
+ const historyEl = Calculator._createHistoryPanel();
+
+ // Formel-Helfer
+ const formulaSection = document.createElement('div');
+ renderFormulaHelper(formulaSection);
+
+ bodyEl.append(display, sciSection, stdButtons, historyEl, formulaSection);
+ Calculator._updateDisplay();
+
+ // Keyboard erweitern
+ const entry = WidgetManager._widgets.get(Calculator.WIDGET_ID);
+ if (entry) bindSciKeyboard(entry.el);
+ },
+
+ destroy() {
+ // Keyboard-Extension entfernen
+ if (_keyboardExtHandler) {
+ const entry = WidgetManager._widgets.get(Calculator.WIDGET_ID);
+ if (entry) {
+ entry.el.removeEventListener('keydown', _keyboardExtHandler);
+ }
+ _keyboardExtHandler = null;
+ }
+ Calculator._displayExprEl = null;
+ Calculator._displayResultEl = null;
+ }
+ });
+})();
+```
+
+- [ ] **Step 2: Script-Tag in newtab.html einfügen**
+
+Nach Zeile 509 (``):
+
+```html
+
+```
+
+- [ ] **Step 3: CSS für Scientific-Modus**
+
+In `src/css/main.css`, nach den Tab-System Styles:
+
+```css
+/* Calculator Scientific Mode */
+.calc-sci-buttons {
+ grid-template-columns: repeat(3, 1fr);
+ margin-bottom: 4px;
+}
+.calc-formula-helper {
+ border-top: 1px solid var(--border);
+ padding-top: 8px;
+ margin-top: 4px;
+}
+.calc-formula-label {
+ font-size: 9px;
+ color: var(--text-muted);
+ text-transform: uppercase;
+ letter-spacing: 1px;
+ margin-bottom: 4px;
+}
+.calc-formula-select {
+ width: 100%;
+ padding: 4px 6px;
+ background: rgba(0,0,0,0.3);
+ border: 1px solid var(--border);
+ border-radius: var(--radius-sm);
+ color: var(--text-primary);
+ font-size: 12px;
+ font-family: 'Rajdhani', sans-serif;
+ margin-bottom: 6px;
+}
+.calc-formula-row {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ margin-bottom: 4px;
+}
+.calc-formula-row label {
+ font-size: 11px;
+ color: var(--text-secondary);
+ min-width: 50px;
+}
+.calc-formula-input {
+ flex: 1;
+ padding: 4px 6px;
+ background: rgba(0,0,0,0.3);
+ border: 1px solid var(--border);
+ border-radius: var(--radius-sm);
+ color: var(--text-primary);
+ font-size: 12px;
+ font-family: 'Rajdhani', sans-serif;
+}
+.calc-formula-result {
+ font-size: 14px;
+ color: var(--accent);
+ font-weight: 600;
+ font-family: 'Rajdhani', monospace;
+ text-align: right;
+ min-height: 20px;
+ padding: 2px 0;
+}
+```
+
+- [ ] **Step 4: i18n-Keys für Scientific**
+
+In `src/js/i18n.js`, DE-Block nach `'calculator.tab.standard'`:
+
+```javascript
+'calculator.tab.scientific': 'Wissenschaftlich',
+'calculator.sci.formulas': 'Formel-Helfer',
+'calculator.sci.select_formula': 'Formel wählen…',
+'calculator.sci.formula.circle_area': 'Kreisfläche (π×r²)',
+'calculator.sci.formula.circle_circumference':'Kreisumfang (2πr)',
+'calculator.sci.formula.celsius_to_fahrenheit':'°C → °F',
+'calculator.sci.formula.fahrenheit_to_celsius':'°F → °C',
+'calculator.sci.formula.pythagoras': 'Pythagoras (√(a²+b²))',
+'calculator.sci.formula.percentage': 'Prozentwert',
+'calculator.sci.field.radius': 'Radius',
+'calculator.sci.field.temp': 'Temperatur',
+'calculator.sci.field.a': 'Seite a',
+'calculator.sci.field.b': 'Seite b',
+'calculator.sci.field.value': 'Wert',
+'calculator.sci.field.percent': 'Prozent',
+```
+
+EN-Block nach `'calculator.tab.standard'`:
+
+```javascript
+'calculator.tab.scientific': 'Scientific',
+'calculator.sci.formulas': 'Formula Helper',
+'calculator.sci.select_formula': 'Choose formula…',
+'calculator.sci.formula.circle_area': 'Circle Area (π×r²)',
+'calculator.sci.formula.circle_circumference':'Circle Circumference (2πr)',
+'calculator.sci.formula.celsius_to_fahrenheit':'°C → °F',
+'calculator.sci.formula.fahrenheit_to_celsius':'°F → °C',
+'calculator.sci.formula.pythagoras': 'Pythagoras (√(a²+b²))',
+'calculator.sci.formula.percentage': 'Percentage',
+'calculator.sci.field.radius': 'Radius',
+'calculator.sci.field.temp': 'Temperature',
+'calculator.sci.field.a': 'Side a',
+'calculator.sci.field.b': 'Side b',
+'calculator.sci.field.value': 'Value',
+'calculator.sci.field.percent': 'Percent',
+```
+
+- [ ] **Step 5: Testen**
+
+1. Calculator öffnen → 2 Tabs sichtbar (Std, Sci)
+2. Sci-Tab klicken → 6 Scientific-Buttons + Standard-Grid + Formel-Helfer
+3. √ klicken → `sqrt(` im Display, 9 eingeben, `)` eingeben, = → 3
+4. π klicken → 3.14159265359 im Display
+5. Formel-Dropdown "Kreisfläche" wählen → Radius-Feld, r=5 → = 78.5398...
+6. `p`-Taste → Pi einfügen
+7. `^`-Taste → Potenz-Operator
+
+- [ ] **Step 6: Commit**
+
+```bash
+git add src/js/calc-scientific.js newtab.html src/css/main.css src/js/i18n.js
+git commit -m "feat(calculator): Scientific-Modus mit Formel-Helfer"
+```
+
+---
+
+### Task 4: Unit-Converter
+
+**Files:**
+- Create: `src/js/calc-converter.js`
+- Modify: `newtab.html` (Script-Tag nach calc-scientific.js)
+- Modify: `src/js/i18n.js` (neue Keys)
+- Modify: `src/css/main.css` (Converter-spezifische Styles)
+
+**Kontext:** 6 Kategorien (Länge, Gewicht, Temperatur, Volumen, Geschwindigkeit, Fläche) mit toBase/fromBase Pattern. Temperatur als Spezialfall. Live-Update bei Eingabe, Swap-Button, Schnellreferenz.
+
+- [ ] **Step 1: calc-converter.js erstellen**
+
+```javascript
+/* =============================================
+ HELLION NEWTAB — calc-converter.js
+ Unit-Converter Modus für Calculator Widget
+ ============================================= */
+
+(function() {
+ 'use strict';
+
+ // ---- UNIT DEFINITIONS ----
+
+ const CATEGORIES = {
+ length: {
+ titleKey: 'calculator.conv.cat.length',
+ baseUnit: 'm',
+ units: {
+ mm: { toBase: v => v / 1000, fromBase: v => v * 1000 },
+ cm: { toBase: v => v / 100, fromBase: v => v * 100 },
+ m: { toBase: v => v, fromBase: v => v },
+ km: { toBase: v => v * 1000, fromBase: v => v / 1000 },
+ in: { toBase: v => v * 0.0254, fromBase: v => v / 0.0254 },
+ ft: { toBase: v => v * 0.3048, fromBase: v => v / 0.3048 },
+ yd: { toBase: v => v * 0.9144, fromBase: v => v / 0.9144 },
+ mi: { toBase: v => v * 1609.344, fromBase: v => v / 1609.344 }
+ }
+ },
+ weight: {
+ titleKey: 'calculator.conv.cat.weight',
+ baseUnit: 'g',
+ units: {
+ mg: { toBase: v => v / 1000, fromBase: v => v * 1000 },
+ g: { toBase: v => v, fromBase: v => v },
+ kg: { toBase: v => v * 1000, fromBase: v => v / 1000 },
+ t: { toBase: v => v * 1000000, fromBase: v => v / 1000000 },
+ oz: { toBase: v => v * 28.3495, fromBase: v => v / 28.3495 },
+ lb: { toBase: v => v * 453.592, fromBase: v => v / 453.592 }
+ }
+ },
+ temperature: {
+ titleKey: 'calculator.conv.cat.temperature',
+ baseUnit: null, // Spezialfall
+ units: { '\u00B0C': null, '\u00B0F': null, 'K': null },
+ convert(value, from, to) {
+ if (from === to) return value;
+ const key = from + '_' + to;
+ const conversions = {
+ '\u00B0C_\u00B0F': v => (v * 9 / 5) + 32,
+ '\u00B0C_K': v => v + 273.15,
+ '\u00B0F_\u00B0C': v => (v - 32) * 5 / 9,
+ '\u00B0F_K': v => (v - 32) * 5 / 9 + 273.15,
+ 'K_\u00B0C': v => v - 273.15,
+ 'K_\u00B0F': v => (v - 273.15) * 9 / 5 + 32
+ };
+ const fn = conversions[key];
+ return fn ? fn(value) : null;
+ }
+ },
+ volume: {
+ titleKey: 'calculator.conv.cat.volume',
+ baseUnit: 'ml',
+ units: {
+ ml: { toBase: v => v, fromBase: v => v },
+ L: { toBase: v => v * 1000, fromBase: v => v / 1000 },
+ 'm\u00B3':{ toBase: v => v * 1000000, fromBase: v => v / 1000000 },
+ 'gal(US)':{ toBase: v => v * 3785.41, fromBase: v => v / 3785.41 },
+ 'gal(UK)':{ toBase: v => v * 4546.09, fromBase: v => v / 4546.09 },
+ 'ft\u00B3':{ toBase: v => v * 28316.8, fromBase: v => v / 28316.8 }
+ }
+ },
+ speed: {
+ titleKey: 'calculator.conv.cat.speed',
+ baseUnit: 'm/s',
+ units: {
+ 'm/s': { toBase: v => v, fromBase: v => v },
+ 'km/h': { toBase: v => v / 3.6, fromBase: v => v * 3.6 },
+ 'mph': { toBase: v => v * 0.44704, fromBase: v => v / 0.44704 },
+ 'kn': { toBase: v => v * 0.514444, fromBase: v => v / 0.514444 }
+ }
+ },
+ area: {
+ titleKey: 'calculator.conv.cat.area',
+ baseUnit: 'm\u00B2',
+ units: {
+ 'mm\u00B2': { toBase: v => v / 1000000, fromBase: v => v * 1000000 },
+ 'cm\u00B2': { toBase: v => v / 10000, fromBase: v => v * 10000 },
+ 'm\u00B2': { toBase: v => v, fromBase: v => v },
+ 'km\u00B2': { toBase: v => v * 1000000, fromBase: v => v / 1000000 },
+ 'ha': { toBase: v => v * 10000, fromBase: v => v / 10000 },
+ 'acre': { toBase: v => v * 4046.86, fromBase: v => v / 4046.86 },
+ 'ft\u00B2': { toBase: v => v * 0.092903, fromBase: v => v / 0.092903 },
+ 'in\u00B2': { toBase: v => v * 0.00064516, fromBase: v => v / 0.00064516 }
+ }
+ }
+ };
+
+ const CATEGORY_ORDER = ['length', 'weight', 'temperature', 'volume', 'speed', 'area'];
+
+ // ---- STATE ----
+
+ let _currentCategory = 'length';
+ let _fromUnit = 'cm';
+ let _toUnit = 'in';
+ let _fromInput = null;
+ let _toInput = null;
+ let _refEl = null;
+
+ // ---- CONVERSION ----
+
+ /**
+ * Wert konvertieren
+ * @param {number} value
+ * @param {string} from - Quelleinheit
+ * @param {string} to - Zieleinheit
+ * @returns {number|null}
+ */
+ function convert(value, from, to) {
+ const cat = CATEGORIES[_currentCategory];
+ if (!cat) return null;
+
+ if (cat.convert) {
+ return cat.convert(value, from, to);
+ }
+
+ const fromDef = cat.units[from];
+ const toDef = cat.units[to];
+ if (!fromDef || !toDef) return null;
+
+ const base = fromDef.toBase(value);
+ return toDef.fromBase(base);
+ }
+
+ /**
+ * Live-Berechnung auslösen
+ */
+ function recalc() {
+ if (!_fromInput || !_toInput) return;
+ const val = parseFloat(_fromInput.value);
+ if (isNaN(val)) {
+ _toInput.value = '';
+ updateReference();
+ return;
+ }
+ const result = convert(val, _fromUnit, _toUnit);
+ if (result === null) {
+ _toInput.value = '';
+ } else {
+ _toInput.value = Calculator._formatResult(result);
+ }
+ updateReference();
+ }
+
+ /**
+ * Schnellreferenz aktualisieren
+ */
+ function updateReference() {
+ if (!_refEl) return;
+ _refEl.textContent = '';
+
+ const r1 = convert(1, _fromUnit, _toUnit);
+ const r2 = convert(1, _toUnit, _fromUnit);
+
+ if (r1 !== null) {
+ const line1 = document.createElement('div');
+ line1.textContent = '1 ' + _fromUnit + ' = ' + Calculator._formatResult(r1) + ' ' + _toUnit;
+ _refEl.appendChild(line1);
+ }
+ if (r2 !== null) {
+ const line2 = document.createElement('div');
+ line2.textContent = '1 ' + _toUnit + ' = ' + Calculator._formatResult(r2) + ' ' + _fromUnit;
+ _refEl.appendChild(line2);
+ }
+ }
+
+ /**
+ * Unit-Selects für aktuelle Kategorie befüllen
+ * @param {HTMLSelectElement} selectEl
+ * @param {string} selectedUnit
+ */
+ function populateUnitSelect(selectEl, selectedUnit) {
+ while (selectEl.firstChild) {
+ selectEl.removeChild(selectEl.firstChild);
+ }
+ const cat = CATEGORIES[_currentCategory];
+ if (!cat) return;
+
+ const units = Object.keys(cat.units);
+ units.forEach(unit => {
+ const opt = document.createElement('option');
+ opt.value = unit;
+ opt.textContent = unit;
+ if (unit === selectedUnit) opt.selected = true;
+ selectEl.appendChild(opt);
+ });
+ }
+
+ /**
+ * Kategorie-spezifische Defaults für Units
+ * @param {string} catKey
+ * @returns {{ from: string, to: string }}
+ */
+ function getDefaultUnits(catKey) {
+ const defaults = {
+ length: { from: 'cm', to: 'in' },
+ weight: { from: 'kg', to: 'lb' },
+ temperature: { from: '\u00B0C', to: '\u00B0F' },
+ volume: { from: 'L', to: 'gal(US)' },
+ speed: { from: 'km/h', to: 'mph' },
+ area: { from: 'm\u00B2', to: 'ft\u00B2' }
+ };
+ return defaults[catKey] || { from: Object.keys(CATEGORIES[catKey].units)[0], to: Object.keys(CATEGORIES[catKey].units)[1] };
+ }
+
+ /**
+ * State aus Storage laden
+ */
+ async function loadState() {
+ const data = await Store.get(Calculator.STORAGE_KEY);
+ if (data && data.calculator && data.calculator.converter) {
+ const s = data.calculator.converter;
+ if (s.lastCategory && CATEGORIES[s.lastCategory]) _currentCategory = s.lastCategory;
+ if (s.fromUnit) _fromUnit = s.fromUnit;
+ if (s.toUnit) _toUnit = s.toUnit;
+ }
+ }
+
+ /**
+ * State in Storage speichern
+ */
+ async function saveState() {
+ const data = await Store.get(Calculator.STORAGE_KEY) || {};
+ if (!data.calculator) data.calculator = {};
+ data.calculator.converter = {
+ lastCategory: _currentCategory,
+ fromUnit: _fromUnit,
+ toUnit: _toUnit
+ };
+ await Store.set(Calculator.STORAGE_KEY, data);
+ }
+
+ // ---- REGISTRATION ----
+
+ Calculator.registerMode('converter', {
+ label: '⚖️',
+ shortName: 'Unit',
+ titleKey: 'calculator.tab.converter',
+
+ render(bodyEl) {
+ bodyEl.style.padding = '8px';
+ bodyEl.style.display = 'flex';
+ bodyEl.style.flexDirection = 'column';
+ bodyEl.style.gap = '8px';
+
+ loadState().then(() => {
+ buildUI(bodyEl);
+ });
+ },
+
+ destroy() {
+ _fromInput = null;
+ _toInput = null;
+ _refEl = null;
+ saveState();
+ }
+ });
+
+ /**
+ * Converter-UI aufbauen
+ * @param {HTMLElement} bodyEl
+ */
+ function buildUI(bodyEl) {
+ // Kategorie-Dropdown
+ const catSelect = document.createElement('select');
+ catSelect.className = 'calc-conv-select';
+
+ CATEGORY_ORDER.forEach(catKey => {
+ const opt = document.createElement('option');
+ opt.value = catKey;
+ opt.textContent = t(CATEGORIES[catKey].titleKey);
+ if (catKey === _currentCategory) opt.selected = true;
+ catSelect.appendChild(opt);
+ });
+
+ // From-Zeile
+ const fromRow = document.createElement('div');
+ fromRow.className = 'calc-conv-row';
+
+ _fromInput = document.createElement('input');
+ _fromInput.type = 'number';
+ _fromInput.className = 'calc-conv-input';
+ _fromInput.placeholder = '0';
+ _fromInput.step = 'any';
+
+ const fromSelect = document.createElement('select');
+ fromSelect.className = 'calc-conv-unit';
+ populateUnitSelect(fromSelect, _fromUnit);
+
+ fromRow.append(_fromInput, fromSelect);
+
+ // Swap-Button
+ const swapBtn = document.createElement('button');
+ swapBtn.type = 'button';
+ swapBtn.className = 'calc-conv-swap';
+ swapBtn.textContent = '\u21C5';
+ swapBtn.title = t('calculator.conv.swap');
+
+ // To-Zeile
+ const toRow = document.createElement('div');
+ toRow.className = 'calc-conv-row';
+
+ _toInput = document.createElement('input');
+ _toInput.type = 'text';
+ _toInput.className = 'calc-conv-input';
+ _toInput.readOnly = true;
+ _toInput.placeholder = '0';
+
+ const toSelect = document.createElement('select');
+ toSelect.className = 'calc-conv-unit';
+ populateUnitSelect(toSelect, _toUnit);
+
+ toRow.append(_toInput, toSelect);
+
+ // Schnellreferenz
+ _refEl = document.createElement('div');
+ _refEl.className = 'calc-conv-ref';
+
+ // Event Listeners
+ _fromInput.addEventListener('input', () => recalc());
+
+ fromSelect.addEventListener('change', () => {
+ _fromUnit = fromSelect.value;
+ recalc();
+ saveState();
+ });
+
+ toSelect.addEventListener('change', () => {
+ _toUnit = toSelect.value;
+ recalc();
+ saveState();
+ });
+
+ swapBtn.addEventListener('click', () => {
+ const tmpUnit = _fromUnit;
+ _fromUnit = _toUnit;
+ _toUnit = tmpUnit;
+ populateUnitSelect(fromSelect, _fromUnit);
+ populateUnitSelect(toSelect, _toUnit);
+ // Swap-Wert übernehmen
+ const currentVal = _toInput.value;
+ if (currentVal) {
+ _fromInput.value = currentVal;
+ }
+ recalc();
+ saveState();
+ });
+
+ catSelect.addEventListener('change', () => {
+ _currentCategory = catSelect.value;
+ const defaults = getDefaultUnits(_currentCategory);
+ _fromUnit = defaults.from;
+ _toUnit = defaults.to;
+ populateUnitSelect(fromSelect, _fromUnit);
+ populateUnitSelect(toSelect, _toUnit);
+ _fromInput.value = '';
+ _toInput.value = '';
+ updateReference();
+ saveState();
+ });
+
+ bodyEl.append(catSelect, fromRow, swapBtn, toRow, _refEl);
+
+ // Initiale Referenz anzeigen
+ updateReference();
+ }
+})();
+```
+
+- [ ] **Step 2: Script-Tag in newtab.html einfügen**
+
+Nach dem `calc-scientific.js` Script-Tag:
+
+```html
+
+```
+
+- [ ] **Step 3: CSS für Converter**
+
+In `src/css/main.css`, nach den Scientific-Styles:
+
+```css
+/* Calculator Converter Mode */
+.calc-conv-select {
+ width: 100%;
+ padding: 6px 8px;
+ background: rgba(0,0,0,0.3);
+ border: 1px solid var(--border);
+ border-radius: var(--radius-sm);
+ color: var(--text-primary);
+ font-size: 13px;
+ font-family: 'Rajdhani', sans-serif;
+}
+.calc-conv-row {
+ display: flex;
+ gap: 6px;
+}
+.calc-conv-input {
+ flex: 1;
+ padding: 8px;
+ background: rgba(0,0,0,0.3);
+ border: 1px solid var(--border);
+ border-radius: var(--radius-sm);
+ color: var(--text-primary);
+ font-size: 16px;
+ font-family: 'Rajdhani', monospace;
+}
+.calc-conv-input:read-only {
+ color: var(--accent);
+ font-weight: 600;
+}
+.calc-conv-unit {
+ width: 80px;
+ padding: 4px 6px;
+ background: rgba(0,0,0,0.3);
+ border: 1px solid var(--border);
+ border-radius: var(--radius-sm);
+ color: var(--text-primary);
+ font-size: 12px;
+ font-family: 'Rajdhani', sans-serif;
+}
+.calc-conv-swap {
+ align-self: center;
+ width: 36px;
+ height: 28px;
+ background: rgba(255,255,255,0.04);
+ border: 1px solid var(--border);
+ border-radius: var(--radius-sm);
+ color: var(--accent);
+ font-size: 16px;
+ cursor: pointer;
+ transition: all 0.15s;
+}
+.calc-conv-swap:hover {
+ background: var(--accent-dim);
+}
+.calc-conv-ref {
+ font-size: 11px;
+ color: var(--text-muted);
+ padding: 4px 0;
+ border-top: 1px solid var(--border);
+}
+```
+
+- [ ] **Step 4: i18n-Keys für Converter**
+
+DE-Block:
+
+```javascript
+'calculator.tab.converter': 'Umrechner',
+'calculator.conv.swap': 'Einheiten tauschen',
+'calculator.conv.cat.length': 'Länge',
+'calculator.conv.cat.weight': 'Gewicht',
+'calculator.conv.cat.temperature': 'Temperatur',
+'calculator.conv.cat.volume': 'Volumen',
+'calculator.conv.cat.speed': 'Geschwindigkeit',
+'calculator.conv.cat.area': 'Fläche',
+```
+
+EN-Block:
+
+```javascript
+'calculator.tab.converter': 'Converter',
+'calculator.conv.swap': 'Swap units',
+'calculator.conv.cat.length': 'Length',
+'calculator.conv.cat.weight': 'Weight',
+'calculator.conv.cat.temperature': 'Temperature',
+'calculator.conv.cat.volume': 'Volume',
+'calculator.conv.cat.speed': 'Speed',
+'calculator.conv.cat.area': 'Area',
+```
+
+- [ ] **Step 5: Testen**
+
+1. Calculator öffnen → 3 Tabs sichtbar (Std, Sci, Unit)
+2. Unit-Tab klicken → Kategorie-Dropdown, Eingabe-Feld, Ergebnis
+3. 100 cm eingeben → 39.3701 in
+4. Swap-Button → cm und in tauschen, Wert übernehmen
+5. Kategorie zu Temperatur wechseln → °C und °F als Units
+6. 100 °C → 212 °F
+7. 0 °F → -17.7778 °C
+
+- [ ] **Step 6: Commit**
+
+```bash
+git add src/js/calc-converter.js newtab.html src/css/main.css src/js/i18n.js
+git commit -m "feat(calculator): Unit-Converter mit 6 Kategorien"
+```
+
+---
+
+### Task 5: Satisfactory Calculator
+
+**Files:**
+- Create: `src/js/calc-satisfactory.js`
+- Modify: `newtab.html` (Script-Tag nach calc-converter.js)
+- Modify: `src/js/i18n.js` (neue Keys)
+- Modify: `src/css/main.css` (Game-Calculator shared Styles)
+
+**Kontext:** 3 Sub-Modi: Items/Min, Overclock Power, Machines. Power-Exponent: 1.321928 (Update 8 Wert). Shared CSS-Klassen `.calc-game-*` werden hier definiert und von Factorio/Stationeers wiederverwendet.
+
+- [ ] **Step 1: calc-satisfactory.js erstellen**
+
+```javascript
+/* =============================================
+ HELLION NEWTAB — calc-satisfactory.js
+ Satisfactory Calculator Modus
+ ============================================= */
+
+(function() {
+ 'use strict';
+
+ const POWER_EXPONENT = 1.321928;
+
+ const SUB_MODES = ['itemsPerMin', 'power', 'machines'];
+ let _activeSubMode = 'itemsPerMin';
+
+ // ---- SHARED HELPERS ----
+
+ /**
+ * Sub-Tab-Leiste erstellen
+ * @param {HTMLElement} container
+ * @param {Array} modes
+ * @param {string} activeMode
+ * @param {string} i18nPrefix
+ * @param {Function} onSwitch
+ */
+ function createSubTabs(container, modes, activeMode, i18nPrefix, onSwitch) {
+ const bar = document.createElement('div');
+ bar.className = 'calc-game-subtabs';
+
+ modes.forEach(mode => {
+ const btn = document.createElement('button');
+ btn.type = 'button';
+ btn.className = 'calc-game-subtab' + (mode === activeMode ? ' active' : '');
+ btn.textContent = t(i18nPrefix + mode);
+ btn.dataset.mode = mode;
+ btn.addEventListener('click', () => {
+ bar.querySelectorAll('.calc-game-subtab').forEach(b => b.classList.remove('active'));
+ btn.classList.add('active');
+ onSwitch(mode);
+ });
+ bar.appendChild(btn);
+ });
+
+ container.appendChild(bar);
+ }
+
+ /**
+ * Eingabefeld mit Label erstellen
+ * @param {string} labelKey - i18n-Key
+ * @param {number} defaultVal
+ * @param {Object} opts - { step, min, max }
+ * @returns {{ row: HTMLElement, input: HTMLInputElement }}
+ */
+ function createField(labelKey, defaultVal, opts) {
+ opts = opts || {};
+ const row = document.createElement('div');
+ row.className = 'calc-game-field';
+
+ const label = document.createElement('label');
+ label.textContent = t(labelKey);
+
+ const input = document.createElement('input');
+ input.type = 'number';
+ input.className = 'calc-game-input';
+ input.value = defaultVal;
+ if (opts.step) input.step = opts.step;
+ if (opts.min !== undefined) input.min = opts.min;
+ if (opts.max !== undefined) input.max = opts.max;
+
+ row.append(label, input);
+ return { row, input };
+ }
+
+ /**
+ * Ergebnis-Zeile erstellen
+ * @param {string} labelKey
+ * @returns {{ row: HTMLElement, value: HTMLElement }}
+ */
+ function createOutput(labelKey) {
+ const row = document.createElement('div');
+ row.className = 'calc-game-output';
+
+ const label = document.createElement('span');
+ label.textContent = t(labelKey);
+
+ const value = document.createElement('span');
+ value.className = 'calc-game-value';
+
+ row.append(label, value);
+ return { row, value };
+ }
+
+ // ---- SUB-MODE RENDERERS ----
+
+ function renderItemsPerMin(container) {
+ const itemsField = createField('calculator.sat.items_per_craft', 1, { step: 1, min: 1 });
+ const timeField = createField('calculator.sat.craft_time', 4, { step: 0.1, min: 0.1 });
+ const clockField = createField('calculator.sat.clock_speed', 100, { step: 1, min: 1, max: 250 });
+
+ const output = createOutput('calculator.sat.output_per_min');
+
+ function calc() {
+ const items = parseFloat(itemsField.input.value) || 0;
+ const time = parseFloat(timeField.input.value) || 1;
+ const clock = parseFloat(clockField.input.value) || 100;
+ const result = (items * 60) / time * (clock / 100);
+ output.value.textContent = Calculator._formatResult(result) + ' items/min';
+ }
+
+ [itemsField, timeField, clockField].forEach(f => {
+ f.input.addEventListener('input', calc);
+ });
+
+ container.append(itemsField.row, timeField.row, clockField.row, output.row);
+ calc();
+ }
+
+ function renderPower(container) {
+ const basePowerField = createField('calculator.sat.base_power', 30, { step: 1, min: 0.1 });
+ const clockField = createField('calculator.sat.clock_speed', 100, { step: 1, min: 1, max: 250 });
+
+ const powerOutput = createOutput('calculator.sat.power_usage');
+ const effOutput = createOutput('calculator.sat.efficiency');
+
+ function calc() {
+ const basePower = parseFloat(basePowerField.input.value) || 0;
+ const clock = parseFloat(clockField.input.value) || 100;
+ const ratio = clock / 100;
+ const power = basePower * Math.pow(ratio, POWER_EXPONENT);
+ const effPerItem = Math.pow(ratio, POWER_EXPONENT - 1);
+
+ powerOutput.value.textContent = Calculator._formatResult(power) + ' MW';
+
+ if (clock > 100) {
+ const overhead = (effPerItem - 1) * 100;
+ effOutput.value.textContent = '+' + Calculator._formatResult(overhead) + '% ' + t('calculator.sat.per_item');
+ effOutput.row.style.display = '';
+ } else {
+ effOutput.row.style.display = 'none';
+ }
+ }
+
+ [basePowerField, clockField].forEach(f => {
+ f.input.addEventListener('input', calc);
+ });
+
+ container.append(basePowerField.row, clockField.row, powerOutput.row, effOutput.row);
+ calc();
+ }
+
+ function renderMachines(container) {
+ const targetField = createField('calculator.sat.target_output', 60, { step: 1, min: 1 });
+ const itemsField = createField('calculator.sat.items_per_craft', 1, { step: 1, min: 1 });
+ const timeField = createField('calculator.sat.craft_time', 4, { step: 0.1, min: 0.1 });
+ const clockField = createField('calculator.sat.clock_speed', 100, { step: 1, min: 1, max: 250 });
+ const basePowerField = createField('calculator.sat.base_power', 30, { step: 1, min: 0.1 });
+
+ const machinesOutput = createOutput('calculator.sat.machines_needed');
+ const totalPowerOutput = createOutput('calculator.sat.total_power');
+
+ function calc() {
+ const target = parseFloat(targetField.input.value) || 0;
+ const items = parseFloat(itemsField.input.value) || 1;
+ const time = parseFloat(timeField.input.value) || 1;
+ const clock = parseFloat(clockField.input.value) || 100;
+ const basePower = parseFloat(basePowerField.input.value) || 0;
+
+ const ratio = clock / 100;
+ const itemsPerMin = (items * 60) / time * ratio;
+ const machines = itemsPerMin > 0 ? Math.ceil(target / itemsPerMin) : 0;
+ const totalPower = machines * basePower * Math.pow(ratio, POWER_EXPONENT);
+
+ machinesOutput.value.textContent = machines;
+ totalPowerOutput.value.textContent = Calculator._formatResult(totalPower) + ' MW';
+ }
+
+ [targetField, itemsField, timeField, clockField, basePowerField].forEach(f => {
+ f.input.addEventListener('input', calc);
+ });
+
+ container.append(targetField.row, itemsField.row, timeField.row, clockField.row, basePowerField.row, machinesOutput.row, totalPowerOutput.row);
+ calc();
+ }
+
+ // ---- STATE ----
+
+ async function loadState() {
+ const data = await Store.get(Calculator.STORAGE_KEY);
+ if (data && data.calculator && data.calculator.satisfactory) {
+ const s = data.calculator.satisfactory;
+ if (s.lastSubMode && SUB_MODES.includes(s.lastSubMode)) _activeSubMode = s.lastSubMode;
+ }
+ }
+
+ async function saveState() {
+ const data = await Store.get(Calculator.STORAGE_KEY) || {};
+ if (!data.calculator) data.calculator = {};
+ data.calculator.satisfactory = { lastSubMode: _activeSubMode };
+ await Store.set(Calculator.STORAGE_KEY, data);
+ }
+
+ // ---- RENDER ----
+
+ function renderSubMode(container) {
+ container.textContent = '';
+ switch (_activeSubMode) {
+ case 'itemsPerMin': renderItemsPerMin(container); break;
+ case 'power': renderPower(container); break;
+ case 'machines': renderMachines(container); break;
+ }
+ }
+
+ // ---- REGISTRATION ----
+
+ Calculator.registerMode('satisfactory', {
+ label: '⚙️',
+ shortName: 'SAT',
+ titleKey: 'calculator.tab.satisfactory',
+
+ render(bodyEl) {
+ bodyEl.style.padding = '8px';
+ bodyEl.style.display = 'flex';
+ bodyEl.style.flexDirection = 'column';
+ bodyEl.style.gap = '8px';
+
+ loadState().then(() => {
+ const subContent = document.createElement('div');
+ subContent.className = 'calc-game-content';
+
+ createSubTabs(bodyEl, SUB_MODES, _activeSubMode, 'calculator.sat.tab.', (mode) => {
+ _activeSubMode = mode;
+ renderSubMode(subContent);
+ saveState();
+ });
+
+ bodyEl.appendChild(subContent);
+ renderSubMode(subContent);
+ });
+ },
+
+ destroy() {
+ saveState();
+ }
+ });
+})();
+```
+
+- [ ] **Step 2: Script-Tag in newtab.html**
+
+Nach dem `calc-converter.js` Script-Tag:
+
+```html
+
+```
+
+- [ ] **Step 3: Shared Game-Calculator CSS**
+
+In `src/css/main.css`, nach den Converter-Styles:
+
+```css
+/* Calculator Game Modes (shared) */
+.calc-game-subtabs {
+ display: flex;
+ gap: 4px;
+ margin-bottom: 8px;
+}
+.calc-game-subtab {
+ flex: 1;
+ padding: 5px 4px;
+ background: rgba(255,255,255,0.04);
+ border: 1px solid var(--border);
+ border-radius: var(--radius-sm);
+ color: var(--text-muted);
+ font-size: 10px;
+ font-family: 'Rajdhani', sans-serif;
+ cursor: pointer;
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+ transition: all 0.15s;
+}
+.calc-game-subtab:hover {
+ color: var(--text-secondary);
+}
+.calc-game-subtab.active {
+ background: var(--accent-dim);
+ border-color: var(--accent);
+ color: var(--accent);
+ font-weight: 600;
+}
+.calc-game-field {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ margin-bottom: 4px;
+}
+.calc-game-field label {
+ font-size: 11px;
+ color: var(--text-secondary);
+ min-width: 90px;
+ flex-shrink: 0;
+}
+.calc-game-input {
+ flex: 1;
+ padding: 5px 8px;
+ background: rgba(0,0,0,0.3);
+ border: 1px solid var(--border);
+ border-radius: var(--radius-sm);
+ color: var(--text-primary);
+ font-size: 13px;
+ font-family: 'Rajdhani', monospace;
+}
+.calc-game-output {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 6px 8px;
+ background: rgba(0,0,0,0.2);
+ border: 1px solid var(--border);
+ border-radius: var(--radius-sm);
+ margin-top: 4px;
+}
+.calc-game-output span:first-child {
+ font-size: 11px;
+ color: var(--text-muted);
+}
+.calc-game-value {
+ font-size: 14px;
+ color: var(--accent);
+ font-weight: 600;
+ font-family: 'Rajdhani', monospace;
+}
+.calc-game-warning {
+ font-size: 10px;
+ color: var(--danger);
+ padding: 2px 0;
+}
+.calc-game-content {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+}
+```
+
+- [ ] **Step 4: i18n-Keys für Satisfactory**
+
+DE-Block:
+
+```javascript
+'calculator.tab.satisfactory': 'Satisfactory',
+'calculator.sat.tab.itemsPerMin': 'Items/Min',
+'calculator.sat.tab.power': 'Strom',
+'calculator.sat.tab.machines': 'Maschinen',
+'calculator.sat.items_per_craft': 'Items/Craft',
+'calculator.sat.craft_time': 'Craftzeit (s)',
+'calculator.sat.clock_speed': 'Taktrate (%)',
+'calculator.sat.base_power': 'Grundleistung (MW)',
+'calculator.sat.target_output': 'Ziel Output/Min',
+'calculator.sat.output_per_min': 'Output',
+'calculator.sat.power_usage': 'Stromverbrauch',
+'calculator.sat.efficiency': 'Effizienz',
+'calculator.sat.per_item': 'pro Item',
+'calculator.sat.machines_needed': 'Maschinen benötigt',
+'calculator.sat.total_power': 'Gesamtleistung',
+```
+
+EN-Block:
+
+```javascript
+'calculator.tab.satisfactory': 'Satisfactory',
+'calculator.sat.tab.itemsPerMin': 'Items/Min',
+'calculator.sat.tab.power': 'Power',
+'calculator.sat.tab.machines': 'Machines',
+'calculator.sat.items_per_craft': 'Items/Craft',
+'calculator.sat.craft_time': 'Craft Time (s)',
+'calculator.sat.clock_speed': 'Clock Speed (%)',
+'calculator.sat.base_power': 'Base Power (MW)',
+'calculator.sat.target_output': 'Target Output/Min',
+'calculator.sat.output_per_min': 'Output',
+'calculator.sat.power_usage': 'Power Usage',
+'calculator.sat.efficiency': 'Efficiency',
+'calculator.sat.per_item': 'per item',
+'calculator.sat.machines_needed': 'Machines needed',
+'calculator.sat.total_power': 'Total Power',
+```
+
+- [ ] **Step 5: Testen**
+
+1. SAT-Tab klicken → 3 Sub-Tabs (Items/Min, Strom, Maschinen)
+2. Items/Min: 1 Item, 4s Craft, 100% → 15 items/min
+3. Power: 30 MW Base, 250% Clock → ~115 MW (wegen Exponent 1.321928)
+4. Machines: 60 Target, 1 Item, 4s, 100%, 30 MW → 4 Maschinen, 120 MW
+
+- [ ] **Step 6: Commit**
+
+```bash
+git add src/js/calc-satisfactory.js newtab.html src/css/main.css src/js/i18n.js
+git commit -m "feat(calculator): Satisfactory Calculator mit Overclock-Power"
+```
+
+---
+
+### Task 6: Factorio Calculator
+
+**Files:**
+- Create: `src/js/calc-factorio.js`
+- Modify: `newtab.html` (Script-Tag nach calc-satisfactory.js)
+- Modify: `src/js/i18n.js` (neue Keys)
+
+**Kontext:** 3 Sub-Modi: Ratio, Belt, Machines. Assembler-Speeds: Asm1=0.5, Asm2=0.75, Asm3=1.25. Belt-Throughput: Yellow=15/s, Red=30/s, Blue=45/s. Nutzt die shared `.calc-game-*` CSS-Klassen aus Task 5.
+
+- [ ] **Step 1: calc-factorio.js erstellen**
+
+```javascript
+/* =============================================
+ HELLION NEWTAB — calc-factorio.js
+ Factorio Calculator Modus
+ ============================================= */
+
+(function() {
+ 'use strict';
+
+ const ASSEMBLERS = [
+ { key: 'asm1', speed: 0.5 },
+ { key: 'asm2', speed: 0.75 },
+ { key: 'asm3', speed: 1.25 }
+ ];
+
+ const BELTS = [
+ { key: 'yellow', throughput: 15, perSide: 7.5 },
+ { key: 'red', throughput: 30, perSide: 15 },
+ { key: 'blue', throughput: 45, perSide: 22.5 }
+ ];
+
+ const SUB_MODES = ['ratio', 'belt', 'machines'];
+ let _activeSubMode = 'ratio';
+
+ /**
+ * Assembler-Dropdown erstellen
+ * @param {string} selectedKey
+ * @returns {{ row: HTMLElement, select: HTMLSelectElement }}
+ */
+ function createAssemblerSelect(selectedKey) {
+ const row = document.createElement('div');
+ row.className = 'calc-game-field';
+
+ const label = document.createElement('label');
+ label.textContent = t('calculator.fac.assembler');
+
+ const select = document.createElement('select');
+ select.className = 'calc-game-input';
+
+ ASSEMBLERS.forEach(asm => {
+ const opt = document.createElement('option');
+ opt.value = asm.key;
+ opt.textContent = t('calculator.fac.asm.' + asm.key) + ' (' + asm.speed + 'x)';
+ if (asm.key === selectedKey) opt.selected = true;
+ select.appendChild(opt);
+ });
+
+ row.append(label, select);
+ return { row, select };
+ }
+
+ /**
+ * Belt-Dropdown erstellen
+ * @param {string} selectedKey
+ * @returns {{ row: HTMLElement, select: HTMLSelectElement }}
+ */
+ function createBeltSelect(selectedKey) {
+ const row = document.createElement('div');
+ row.className = 'calc-game-field';
+
+ const label = document.createElement('label');
+ label.textContent = t('calculator.fac.belt');
+
+ const select = document.createElement('select');
+ select.className = 'calc-game-input';
+
+ BELTS.forEach(belt => {
+ const opt = document.createElement('option');
+ opt.value = belt.key;
+ opt.textContent = t('calculator.fac.belt.' + belt.key) + ' (' + belt.throughput + '/s)';
+ if (belt.key === selectedKey) opt.selected = true;
+ select.appendChild(opt);
+ });
+
+ row.append(label, select);
+ return { row, select };
+ }
+
+ function getAssemblerSpeed(key) {
+ const asm = ASSEMBLERS.find(a => a.key === key);
+ return asm ? asm.speed : 1;
+ }
+
+ function getBelt(key) {
+ return BELTS.find(b => b.key === key) || BELTS[0];
+ }
+
+ /**
+ * Findet den kleinsten Belt der den Throughput schafft
+ * @param {number} throughput
+ * @returns {Object|null}
+ */
+ function findSmallestBelt(throughput) {
+ for (const belt of BELTS) {
+ if (belt.throughput >= throughput) return belt;
+ }
+ return null;
+ }
+
+ // ---- Field/Output helpers (reuse pattern from Satisfactory) ----
+
+ function createField(labelKey, defaultVal, opts) {
+ opts = opts || {};
+ const row = document.createElement('div');
+ row.className = 'calc-game-field';
+ const label = document.createElement('label');
+ label.textContent = t(labelKey);
+ const input = document.createElement('input');
+ input.type = 'number';
+ input.className = 'calc-game-input';
+ input.value = defaultVal;
+ if (opts.step) input.step = opts.step;
+ if (opts.min !== undefined) input.min = opts.min;
+ row.append(label, input);
+ return { row, input };
+ }
+
+ function createOutput(labelKey) {
+ const row = document.createElement('div');
+ row.className = 'calc-game-output';
+ const label = document.createElement('span');
+ label.textContent = t(labelKey);
+ const value = document.createElement('span');
+ value.className = 'calc-game-value';
+ row.append(label, value);
+ return { row, value };
+ }
+
+ // ---- SUB-MODE RENDERERS ----
+
+ function renderRatio(container) {
+ const asmSelect = createAssemblerSelect('asm3');
+ const outputField = createField('calculator.fac.recipe_output', 1, { step: 1, min: 1 });
+ const timeField = createField('calculator.fac.recipe_time', 1, { step: 0.1, min: 0.1 });
+
+ const perSecOutput = createOutput('calculator.fac.items_per_sec');
+ const perMinOutput = createOutput('calculator.fac.items_per_min');
+
+ function calc() {
+ const speed = getAssemblerSpeed(asmSelect.select.value);
+ const output = parseFloat(outputField.input.value) || 0;
+ const time = parseFloat(timeField.input.value) || 1;
+ const perSec = output * speed / time;
+ const perMin = perSec * 60;
+ perSecOutput.value.textContent = Calculator._formatResult(perSec) + ' /s';
+ perMinOutput.value.textContent = Calculator._formatResult(perMin) + ' /min';
+ }
+
+ [outputField, timeField].forEach(f => f.input.addEventListener('input', calc));
+ asmSelect.select.addEventListener('change', calc);
+
+ container.append(asmSelect.row, outputField.row, timeField.row, perSecOutput.row, perMinOutput.row);
+ calc();
+ }
+
+ function renderBelt(container) {
+ const beltSelect = createBeltSelect('yellow');
+ const consumeField = createField('calculator.fac.consume_per_sec', 1, { step: 0.1, min: 0.1 });
+
+ const machinesOutput = createOutput('calculator.fac.machines_per_belt');
+ const utilOutput = createOutput('calculator.fac.belt_utilization');
+
+ function calc() {
+ const belt = getBelt(beltSelect.select.value);
+ const consume = parseFloat(consumeField.input.value) || 1;
+ const machines = Math.floor(belt.throughput / consume);
+ const util = (consume * machines) / belt.throughput * 100;
+ machinesOutput.value.textContent = machines;
+ utilOutput.value.textContent = Calculator._formatResult(util) + '%';
+ }
+
+ consumeField.input.addEventListener('input', calc);
+ beltSelect.select.addEventListener('change', calc);
+
+ container.append(beltSelect.row, consumeField.row, machinesOutput.row, utilOutput.row);
+ calc();
+ }
+
+ function renderMachines(container) {
+ const asmSelect = createAssemblerSelect('asm3');
+ const targetField = createField('calculator.fac.target_output_sec', 10, { step: 0.1, min: 0.1 });
+ const outputField = createField('calculator.fac.recipe_output', 1, { step: 1, min: 1 });
+ const timeField = createField('calculator.fac.recipe_time', 1, { step: 0.1, min: 0.1 });
+
+ const machinesOutput = createOutput('calculator.fac.machines_needed');
+ const beltOutput = createOutput('calculator.fac.belt_needed');
+
+ function calc() {
+ const speed = getAssemblerSpeed(asmSelect.select.value);
+ const target = parseFloat(targetField.input.value) || 0;
+ const output = parseFloat(outputField.input.value) || 1;
+ const time = parseFloat(timeField.input.value) || 1;
+
+ const perMachine = output * speed / time;
+ const machines = perMachine > 0 ? Math.ceil(target / perMachine) : 0;
+ const totalThroughput = machines * perMachine;
+ const belt = findSmallestBelt(totalThroughput);
+
+ machinesOutput.value.textContent = machines;
+ if (belt) {
+ const util = (totalThroughput / belt.throughput) * 100;
+ beltOutput.value.textContent = t('calculator.fac.belt.' + belt.key) + ' (' + Calculator._formatResult(util) + '%)';
+ } else {
+ beltOutput.value.textContent = t('calculator.fac.exceeds_belt');
+ }
+ }
+
+ [targetField, outputField, timeField].forEach(f => f.input.addEventListener('input', calc));
+ asmSelect.select.addEventListener('change', calc);
+
+ container.append(asmSelect.row, targetField.row, outputField.row, timeField.row, machinesOutput.row, beltOutput.row);
+ calc();
+ }
+
+ // ---- STATE ----
+
+ async function loadState() {
+ const data = await Store.get(Calculator.STORAGE_KEY);
+ if (data && data.calculator && data.calculator.factorio) {
+ const s = data.calculator.factorio;
+ if (s.lastSubMode && SUB_MODES.includes(s.lastSubMode)) _activeSubMode = s.lastSubMode;
+ }
+ }
+
+ async function saveState() {
+ const data = await Store.get(Calculator.STORAGE_KEY) || {};
+ if (!data.calculator) data.calculator = {};
+ data.calculator.factorio = { lastSubMode: _activeSubMode };
+ await Store.set(Calculator.STORAGE_KEY, data);
+ }
+
+ function renderSubMode(container) {
+ container.textContent = '';
+ switch (_activeSubMode) {
+ case 'ratio': renderRatio(container); break;
+ case 'belt': renderBelt(container); break;
+ case 'machines': renderMachines(container); break;
+ }
+ }
+
+ // ---- REGISTRATION ----
+
+ Calculator.registerMode('factorio', {
+ label: '🏭',
+ shortName: 'FAC',
+ titleKey: 'calculator.tab.factorio',
+
+ render(bodyEl) {
+ bodyEl.style.padding = '8px';
+ bodyEl.style.display = 'flex';
+ bodyEl.style.flexDirection = 'column';
+ bodyEl.style.gap = '8px';
+
+ loadState().then(() => {
+ const subContent = document.createElement('div');
+ subContent.className = 'calc-game-content';
+
+ // Sub-Tab helper aus Satisfactory ist nicht verfügbar (IIFE-Scope),
+ // daher lokale Implementierung
+ const bar = document.createElement('div');
+ bar.className = 'calc-game-subtabs';
+
+ SUB_MODES.forEach(mode => {
+ const btn = document.createElement('button');
+ btn.type = 'button';
+ btn.className = 'calc-game-subtab' + (mode === _activeSubMode ? ' active' : '');
+ btn.textContent = t('calculator.fac.tab.' + mode);
+ btn.dataset.mode = mode;
+ btn.addEventListener('click', () => {
+ bar.querySelectorAll('.calc-game-subtab').forEach(b => b.classList.remove('active'));
+ btn.classList.add('active');
+ _activeSubMode = mode;
+ renderSubMode(subContent);
+ saveState();
+ });
+ bar.appendChild(btn);
+ });
+
+ bodyEl.append(bar, subContent);
+ renderSubMode(subContent);
+ });
+ },
+
+ destroy() {
+ saveState();
+ }
+ });
+})();
+```
+
+- [ ] **Step 2: Script-Tag in newtab.html**
+
+Nach dem `calc-satisfactory.js` Script-Tag:
+
+```html
+
+```
+
+- [ ] **Step 3: i18n-Keys für Factorio**
+
+DE-Block:
+
+```javascript
+'calculator.tab.factorio': 'Factorio',
+'calculator.fac.tab.ratio': 'Ratio',
+'calculator.fac.tab.belt': 'Belt',
+'calculator.fac.tab.machines': 'Maschinen',
+'calculator.fac.assembler': 'Assembler',
+'calculator.fac.asm.asm1': 'Assembler 1',
+'calculator.fac.asm.asm2': 'Assembler 2',
+'calculator.fac.asm.asm3': 'Assembler 3',
+'calculator.fac.belt': 'Belt-Typ',
+'calculator.fac.belt.yellow': 'Gelb',
+'calculator.fac.belt.red': 'Rot',
+'calculator.fac.belt.blue': 'Blau',
+'calculator.fac.recipe_output': 'Rezept-Output',
+'calculator.fac.recipe_time': 'Rezeptzeit (s)',
+'calculator.fac.consume_per_sec': 'Verbrauch/s',
+'calculator.fac.target_output_sec': 'Ziel Output/s',
+'calculator.fac.items_per_sec': 'Items/s',
+'calculator.fac.items_per_min': 'Items/min',
+'calculator.fac.machines_per_belt': 'Maschinen/Belt',
+'calculator.fac.belt_utilization': 'Belt-Auslastung',
+'calculator.fac.machines_needed': 'Maschinen benötigt',
+'calculator.fac.belt_needed': 'Belt benötigt',
+'calculator.fac.exceeds_belt': 'Übersteigt max. Belt',
+```
+
+EN-Block:
+
+```javascript
+'calculator.tab.factorio': 'Factorio',
+'calculator.fac.tab.ratio': 'Ratio',
+'calculator.fac.tab.belt': 'Belt',
+'calculator.fac.tab.machines': 'Machines',
+'calculator.fac.assembler': 'Assembler',
+'calculator.fac.asm.asm1': 'Assembler 1',
+'calculator.fac.asm.asm2': 'Assembler 2',
+'calculator.fac.asm.asm3': 'Assembler 3',
+'calculator.fac.belt': 'Belt Type',
+'calculator.fac.belt.yellow': 'Yellow',
+'calculator.fac.belt.red': 'Red',
+'calculator.fac.belt.blue': 'Blue',
+'calculator.fac.recipe_output': 'Recipe Output',
+'calculator.fac.recipe_time': 'Recipe Time (s)',
+'calculator.fac.consume_per_sec': 'Consume/s',
+'calculator.fac.target_output_sec': 'Target Output/s',
+'calculator.fac.items_per_sec': 'Items/s',
+'calculator.fac.items_per_min': 'Items/min',
+'calculator.fac.machines_per_belt': 'Machines/Belt',
+'calculator.fac.belt_utilization': 'Belt Utilization',
+'calculator.fac.machines_needed': 'Machines needed',
+'calculator.fac.belt_needed': 'Belt needed',
+'calculator.fac.exceeds_belt': 'Exceeds max belt',
+```
+
+- [ ] **Step 4: Testen**
+
+1. FAC-Tab klicken → 3 Sub-Tabs (Ratio, Belt, Maschinen)
+2. Ratio: Asm3 (1.25x), 1 Output, 1s → 1.25/s, 75/min
+3. Belt: Yellow (15/s), 1/s consume → 15 Machines, 100%
+4. Machines: Asm3, 10/s target, 1 output, 1s → 8 machines, Blue (22.2%)
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add src/js/calc-factorio.js newtab.html src/js/i18n.js
+git commit -m "feat(calculator): Factorio Calculator mit Assembler-Ratios"
+```
+
+---
+
+### Task 7: Stationeers Calculator
+
+**Files:**
+- Create: `src/js/calc-stationeers.js`
+- Modify: `newtab.html` (Script-Tag nach calc-factorio.js)
+- Modify: `src/js/i18n.js` (neue Keys)
+- Modify: `src/css/main.css` (Stationeers-spezifische Styles)
+
+**Kontext:** 4 Sub-Modi: Gas (PV=nRT), Furnace, Solar/Battery, Atmosphere. R=8314.46261815324, Combustion Energy=563452 J/mol. Komplexester Modus. Nutzt shared `.calc-game-*` CSS.
+
+- [ ] **Step 1: calc-stationeers.js erstellen**
+
+```javascript
+/* =============================================
+ HELLION NEWTAB — calc-stationeers.js
+ Stationeers Calculator Modus
+ ============================================= */
+
+(function() {
+ 'use strict';
+
+ // ---- CONSTANTS ----
+ const R = 8314.46261815324; // L·Pa / (mol·K)
+ const COMBUSTION_ENERGY = 563452; // J/mol bei 95% Effizienz
+ const HEAT_CAP_PURE_FUEL = 61.9; // J/(mol·K) für 1:2 O₂:H₂
+ const HEAT_CAP_DELTA = 172.615; // 0.95 × (243.6 − 61.9)
+ const BATTERY_CAPACITY = 50000; // Ws (Station Battery)
+
+ const HEAT_CAPS = [
+ { gas: 'O\u2082', cp: 21.1 },
+ { gas: 'H\u2082', cp: 20.4 },
+ { gas: 'CO\u2082', cp: 28.2 },
+ { gas: 'N\u2082', cp: 20.6 },
+ { gas: 'H\u2082O', cp: 72.0 },
+ { gas: 'N\u2082O', cp: 23.0 },
+ { gas: 'Pollutant', cp: 24.8 }
+ ];
+
+ const GAS_VARS = ['P', 'V', 'n', 'T'];
+ const SUB_MODES = ['gas', 'furnace', 'solar', 'atmo'];
+ let _activeSubMode = 'gas';
+
+ // ---- Field/Output helpers ----
+
+ function createField(labelKey, defaultVal, opts) {
+ opts = opts || {};
+ const row = document.createElement('div');
+ row.className = 'calc-game-field';
+ const label = document.createElement('label');
+ label.textContent = t(labelKey);
+ const input = document.createElement('input');
+ input.type = 'number';
+ input.className = 'calc-game-input';
+ input.value = defaultVal;
+ if (opts.step) input.step = opts.step;
+ if (opts.min !== undefined) input.min = opts.min;
+ if (opts.max !== undefined) input.max = opts.max;
+ if (opts.disabled) input.disabled = true;
+ row.append(label, input);
+ return { row, input };
+ }
+
+ function createOutput(labelKey) {
+ const row = document.createElement('div');
+ row.className = 'calc-game-output';
+ const label = document.createElement('span');
+ label.textContent = t(labelKey);
+ const value = document.createElement('span');
+ value.className = 'calc-game-value';
+ row.append(label, value);
+ return { row, value };
+ }
+
+ // ---- GAS (PV=nRT) ----
+
+ function renderGas(container) {
+ // Solve-for Dropdown
+ const solveRow = document.createElement('div');
+ solveRow.className = 'calc-game-field';
+ const solveLabel = document.createElement('label');
+ solveLabel.textContent = t('calculator.sta.solve_for');
+ const solveSelect = document.createElement('select');
+ solveSelect.className = 'calc-game-input';
+
+ GAS_VARS.forEach(v => {
+ const opt = document.createElement('option');
+ opt.value = v;
+ opt.textContent = t('calculator.sta.var.' + v);
+ solveSelect.appendChild(opt);
+ });
+
+ solveRow.append(solveLabel, solveSelect);
+ container.appendChild(solveRow);
+
+ // Eingabefelder für alle 4 Variablen
+ const fields = {};
+ const units = { P: 'kPa', V: 'L', n: 'mol', T: 'K' };
+ const defaults = { P: 101.325, V: 1000, n: 1, T: 293.15 };
+
+ GAS_VARS.forEach(v => {
+ const f = createField(
+ 'calculator.sta.var.' + v + '_label',
+ defaults[v],
+ { step: 'any' }
+ );
+ fields[v] = f;
+ container.appendChild(f.row);
+ });
+
+ // Hilfstext für Temperatur
+ const tempHelper = document.createElement('div');
+ tempHelper.className = 'calc-game-hint';
+ container.appendChild(tempHelper);
+
+ const resultOutput = createOutput('calculator.sta.result');
+ container.appendChild(resultOutput.row);
+
+ function calc() {
+ const solveFor = solveSelect.value;
+
+ // Felder aktivieren/deaktivieren
+ GAS_VARS.forEach(v => {
+ fields[v].input.disabled = (v === solveFor);
+ fields[v].input.style.opacity = (v === solveFor) ? '0.5' : '1';
+ });
+
+ const P_kPa = parseFloat(fields.P.input.value) || 0;
+ const P = P_kPa * 1000; // kPa → Pa
+ const V = parseFloat(fields.V.input.value) || 0;
+ const n = parseFloat(fields.n.input.value) || 0;
+ const T = parseFloat(fields.T.input.value) || 0;
+
+ let result = null;
+ let unit = '';
+
+ switch (solveFor) {
+ case 'P':
+ if (V > 0) { result = (n * R * T) / V; result /= 1000; unit = 'kPa'; }
+ break;
+ case 'V':
+ if (P > 0) { result = (n * R * T) / P; unit = 'L'; }
+ break;
+ case 'n':
+ if (R * T > 0) { result = (P * V) / (R * T); unit = 'mol'; }
+ break;
+ case 'T':
+ if (n * R > 0) { result = (P * V) / (n * R); unit = 'K'; }
+ break;
+ }
+
+ if (result !== null && isFinite(result)) {
+ fields[solveFor].input.value = Calculator._formatResult(result);
+ resultOutput.value.textContent = Calculator._formatResult(result) + ' ' + unit;
+ } else {
+ resultOutput.value.textContent = '-';
+ }
+
+ // Temperatur-Hilfstext
+ const tempVal = parseFloat(fields.T.input.value) || 0;
+ tempHelper.textContent = Calculator._formatResult(tempVal - 273.15) + ' \u00B0C';
+ }
+
+ // Events
+ GAS_VARS.forEach(v => {
+ fields[v].input.addEventListener('input', calc);
+ });
+ solveSelect.addEventListener('change', calc);
+ calc();
+ }
+
+ // ---- FURNACE ----
+
+ function renderFurnace(container) {
+ const fuelField = createField('calculator.sta.fuel_ratio', 0.5, { step: 0.01, min: 0, max: 1 });
+ const tempField = createField('calculator.sta.start_temp', 293.15, { step: 1, min: 0 });
+ const pressField = createField('calculator.sta.start_pressure', 101.325, { step: 0.1, min: 0 });
+
+ const tempOutput = createOutput('calculator.sta.temp_after');
+ const pressOutput = createOutput('calculator.sta.pressure_after');
+ const warningEl = document.createElement('div');
+ warningEl.className = 'calc-game-warning';
+
+ function calc() {
+ const fuel = parseFloat(fuelField.input.value) || 0;
+ const T_vor = parseFloat(tempField.input.value) || 293.15;
+ const P_vor = parseFloat(pressField.input.value) || 101.325;
+
+ warningEl.textContent = '';
+ if (fuel < 0.05) {
+ warningEl.textContent = t('calculator.sta.warn_low_fuel');
+ }
+ if (P_vor < 10) {
+ warningEl.textContent += (warningEl.textContent ? ' ' : '') + t('calculator.sta.warn_low_pressure');
+ }
+
+ const specificHeat = HEAT_CAP_PURE_FUEL;
+ const T_nach = (T_vor * specificHeat + fuel * COMBUSTION_ENERGY) / (specificHeat + fuel * HEAT_CAP_DELTA);
+ const P_nach = P_vor * T_nach * (1 + 5.7 * fuel) / T_vor;
+
+ tempOutput.value.textContent = Calculator._formatResult(T_nach) + ' K (' + Calculator._formatResult(T_nach - 273.15) + ' \u00B0C)';
+ pressOutput.value.textContent = Calculator._formatResult(P_nach) + ' kPa';
+ }
+
+ [fuelField, tempField, pressField].forEach(f => f.input.addEventListener('input', calc));
+
+ container.append(fuelField.row, tempField.row, pressField.row, warningEl, tempOutput.row, pressOutput.row);
+ calc();
+ }
+
+ // ---- SOLAR / BATTERY ----
+
+ function renderSolar(container) {
+ const panelField = createField('calculator.sta.panels', 12, { step: 1, min: 1 });
+ const wattField = createField('calculator.sta.watts_per_panel', 500, { step: 10, min: 1 });
+ const dayField = createField('calculator.sta.day_length', 600, { step: 1, min: 1 });
+ const nightField = createField('calculator.sta.night_length', 600, { step: 1, min: 1 });
+ const consumeField = createField('calculator.sta.consumption', 2000, { step: 10, min: 0 });
+
+ const genOutput = createOutput('calculator.sta.generation');
+ const surplusOutput = createOutput('calculator.sta.surplus');
+ const nightOutput = createOutput('calculator.sta.night_energy');
+ const battOutput = createOutput('calculator.sta.batteries_needed');
+
+ function calc() {
+ const panels = parseFloat(panelField.input.value) || 0;
+ const wpp = parseFloat(wattField.input.value) || 0;
+ const nightLen = parseFloat(nightField.input.value) || 0;
+ const consume = parseFloat(consumeField.input.value) || 0;
+
+ const generation = panels * wpp;
+ const surplus = generation - consume;
+ const nightEnergy = consume * nightLen;
+ const batteries = nightEnergy > 0 ? Math.ceil(nightEnergy / BATTERY_CAPACITY) : 0;
+
+ genOutput.value.textContent = Calculator._formatResult(generation) + ' W';
+
+ surplusOutput.value.textContent = Calculator._formatResult(surplus) + ' W';
+ if (surplus < 0) {
+ surplusOutput.value.style.color = 'var(--danger)';
+ } else {
+ surplusOutput.value.style.color = '';
+ }
+
+ nightOutput.value.textContent = Calculator._formatResult(nightEnergy) + ' Ws';
+ battOutput.value.textContent = batteries;
+ }
+
+ [panelField, wattField, dayField, nightField, consumeField].forEach(f => f.input.addEventListener('input', calc));
+
+ container.append(panelField.row, wattField.row, dayField.row, nightField.row, consumeField.row,
+ genOutput.row, surplusOutput.row, nightOutput.row, battOutput.row);
+ calc();
+ }
+
+ // ---- ATMOSPHERE / MIXER ----
+
+ function renderAtmo(container) {
+ const targetField = createField('calculator.sta.target_temp', 293.15, { step: 1 });
+ const gas1Field = createField('calculator.sta.gas1_temp', 200, { step: 1 });
+ const gas2Field = createField('calculator.sta.gas2_temp', 400, { step: 1 });
+
+ const m1Output = createOutput('calculator.sta.mixer_input1');
+ const m2Output = createOutput('calculator.sta.mixer_input2');
+
+ function calc() {
+ const T0 = parseFloat(targetField.input.value) || 0;
+ const T1 = parseFloat(gas1Field.input.value) || 0;
+ const T2 = parseFloat(gas2Field.input.value) || 0;
+
+ const denom = Math.abs(T1 - T0) + Math.abs(T2 - T0);
+ if (denom === 0) {
+ m1Output.value.textContent = '50%';
+ m2Output.value.textContent = '50%';
+ return;
+ }
+
+ const M1 = Math.abs(T2 - T0) / denom;
+ const M2 = 1 - M1;
+
+ m1Output.value.textContent = Calculator._formatResult(M1 * 100) + '%';
+ m2Output.value.textContent = Calculator._formatResult(M2 * 100) + '%';
+ }
+
+ [targetField, gas1Field, gas2Field].forEach(f => f.input.addEventListener('input', calc));
+
+ container.append(targetField.row, gas1Field.row, gas2Field.row, m1Output.row, m2Output.row);
+ calc();
+
+ // Aufklappbare Wärmekapazität-Referenz
+ const details = document.createElement('details');
+ details.className = 'calc-game-details';
+
+ const summary = document.createElement('summary');
+ summary.textContent = t('calculator.sta.heat_cap_ref');
+ details.appendChild(summary);
+
+ const table = document.createElement('table');
+ table.className = 'calc-game-table';
+
+ const thead = document.createElement('thead');
+ const headerRow = document.createElement('tr');
+ const thGas = document.createElement('th');
+ thGas.textContent = t('calculator.sta.gas');
+ const thCp = document.createElement('th');
+ thCp.textContent = 'Cp (J/mol\u00B7K)';
+ headerRow.append(thGas, thCp);
+ thead.appendChild(headerRow);
+
+ const tbody = document.createElement('tbody');
+ HEAT_CAPS.forEach(entry => {
+ const tr = document.createElement('tr');
+ const tdGas = document.createElement('td');
+ tdGas.textContent = entry.gas;
+ const tdCp = document.createElement('td');
+ tdCp.textContent = entry.cp;
+ tr.append(tdGas, tdCp);
+ tbody.appendChild(tr);
+ });
+
+ table.append(thead, tbody);
+ details.appendChild(table);
+ container.appendChild(details);
+ }
+
+ // ---- STATE ----
+
+ async function loadState() {
+ const data = await Store.get(Calculator.STORAGE_KEY);
+ if (data && data.calculator && data.calculator.stationeers) {
+ const s = data.calculator.stationeers;
+ if (s.lastSubMode && SUB_MODES.includes(s.lastSubMode)) _activeSubMode = s.lastSubMode;
+ }
+ }
+
+ async function saveState() {
+ const data = await Store.get(Calculator.STORAGE_KEY) || {};
+ if (!data.calculator) data.calculator = {};
+ data.calculator.stationeers = { lastSubMode: _activeSubMode };
+ await Store.set(Calculator.STORAGE_KEY, data);
+ }
+
+ function renderSubMode(container) {
+ container.textContent = '';
+ switch (_activeSubMode) {
+ case 'gas': renderGas(container); break;
+ case 'furnace': renderFurnace(container); break;
+ case 'solar': renderSolar(container); break;
+ case 'atmo': renderAtmo(container); break;
+ }
+ }
+
+ // ---- REGISTRATION ----
+
+ Calculator.registerMode('stationeers', {
+ label: '🚀',
+ shortName: 'STA',
+ titleKey: 'calculator.tab.stationeers',
+
+ render(bodyEl) {
+ bodyEl.style.padding = '8px';
+ bodyEl.style.display = 'flex';
+ bodyEl.style.flexDirection = 'column';
+ bodyEl.style.gap = '8px';
+
+ loadState().then(() => {
+ const subContent = document.createElement('div');
+ subContent.className = 'calc-game-content';
+
+ const bar = document.createElement('div');
+ bar.className = 'calc-game-subtabs';
+
+ SUB_MODES.forEach(mode => {
+ const btn = document.createElement('button');
+ btn.type = 'button';
+ btn.className = 'calc-game-subtab' + (mode === _activeSubMode ? ' active' : '');
+ btn.textContent = t('calculator.sta.tab.' + mode);
+ btn.dataset.mode = mode;
+ btn.addEventListener('click', () => {
+ bar.querySelectorAll('.calc-game-subtab').forEach(b => b.classList.remove('active'));
+ btn.classList.add('active');
+ _activeSubMode = mode;
+ renderSubMode(subContent);
+ saveState();
+ });
+ bar.appendChild(btn);
+ });
+
+ bodyEl.append(bar, subContent);
+ renderSubMode(subContent);
+ });
+ },
+
+ destroy() {
+ saveState();
+ }
+ });
+})();
+```
+
+- [ ] **Step 2: Script-Tag in newtab.html**
+
+Nach dem `calc-factorio.js` Script-Tag:
+
+```html
+
+```
+
+- [ ] **Step 3: CSS für Stationeers-spezifische Elemente**
+
+In `src/css/main.css`, nach den Game-Calculator shared Styles:
+
+```css
+/* Calculator Stationeers specifics */
+.calc-game-hint {
+ font-size: 10px;
+ color: var(--text-muted);
+ font-style: italic;
+ margin-top: -4px;
+ text-align: right;
+}
+.calc-game-details {
+ border-top: 1px solid var(--border);
+ padding-top: 6px;
+ margin-top: 4px;
+}
+.calc-game-details summary {
+ font-size: 10px;
+ color: var(--text-muted);
+ cursor: pointer;
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+}
+.calc-game-table {
+ width: 100%;
+ font-size: 11px;
+ border-collapse: collapse;
+ margin-top: 4px;
+}
+.calc-game-table th {
+ text-align: left;
+ color: var(--text-muted);
+ font-weight: 600;
+ padding: 2px 6px;
+ border-bottom: 1px solid var(--border);
+}
+.calc-game-table td {
+ padding: 2px 6px;
+ color: var(--text-secondary);
+}
+.calc-game-table tr:nth-child(even) td {
+ background: rgba(0,0,0,0.1);
+}
+```
+
+- [ ] **Step 4: i18n-Keys für Stationeers**
+
+DE-Block:
+
+```javascript
+'calculator.tab.stationeers': 'Stationeers',
+'calculator.sta.tab.gas': 'Gas',
+'calculator.sta.tab.furnace': 'Ofen',
+'calculator.sta.tab.solar': 'Solar',
+'calculator.sta.tab.atmo': 'Atmo',
+'calculator.sta.solve_for': 'Gesucht',
+'calculator.sta.var.P': 'Druck (P)',
+'calculator.sta.var.V': 'Volumen (V)',
+'calculator.sta.var.n': 'Stoffmenge (n)',
+'calculator.sta.var.T': 'Temperatur (T)',
+'calculator.sta.var.P_label': 'Druck (kPa)',
+'calculator.sta.var.V_label': 'Volumen (L)',
+'calculator.sta.var.n_label': 'Stoffmenge (mol)',
+'calculator.sta.var.T_label': 'Temperatur (K)',
+'calculator.sta.result': 'Ergebnis',
+'calculator.sta.fuel_ratio': 'Fuel-Anteil (0-1)',
+'calculator.sta.start_temp': 'Start-Temperatur (K)',
+'calculator.sta.start_pressure': 'Start-Druck (kPa)',
+'calculator.sta.temp_after': 'T nach Zündung',
+'calculator.sta.pressure_after': 'P nach Zündung',
+'calculator.sta.warn_low_fuel': '⚠ Fuel unter 5%',
+'calculator.sta.warn_low_pressure': '⚠ Druck unter 10 kPa',
+'calculator.sta.panels': 'Anzahl Panels',
+'calculator.sta.watts_per_panel': 'Watt/Panel',
+'calculator.sta.day_length': 'Taglänge (s)',
+'calculator.sta.night_length': 'Nachtlänge (s)',
+'calculator.sta.consumption': 'Verbrauch (W)',
+'calculator.sta.generation': 'Erzeugung',
+'calculator.sta.surplus': 'Überschuss',
+'calculator.sta.night_energy': 'Nacht-Energie',
+'calculator.sta.batteries_needed': 'Batterien benötigt',
+'calculator.sta.target_temp': 'Ziel-Temperatur (K)',
+'calculator.sta.gas1_temp': 'Gas 1 Temperatur (K)',
+'calculator.sta.gas2_temp': 'Gas 2 Temperatur (K)',
+'calculator.sta.mixer_input1': 'Mixer Input 1',
+'calculator.sta.mixer_input2': 'Mixer Input 2',
+'calculator.sta.heat_cap_ref': 'Wärmekapazitäten (Referenz)',
+'calculator.sta.gas': 'Gas',
+```
+
+EN-Block:
+
+```javascript
+'calculator.tab.stationeers': 'Stationeers',
+'calculator.sta.tab.gas': 'Gas',
+'calculator.sta.tab.furnace': 'Furnace',
+'calculator.sta.tab.solar': 'Solar',
+'calculator.sta.tab.atmo': 'Atmo',
+'calculator.sta.solve_for': 'Solve for',
+'calculator.sta.var.P': 'Pressure (P)',
+'calculator.sta.var.V': 'Volume (V)',
+'calculator.sta.var.n': 'Amount (n)',
+'calculator.sta.var.T': 'Temperature (T)',
+'calculator.sta.var.P_label': 'Pressure (kPa)',
+'calculator.sta.var.V_label': 'Volume (L)',
+'calculator.sta.var.n_label': 'Amount (mol)',
+'calculator.sta.var.T_label': 'Temperature (K)',
+'calculator.sta.result': 'Result',
+'calculator.sta.fuel_ratio': 'Fuel Ratio (0-1)',
+'calculator.sta.start_temp': 'Start Temperature (K)',
+'calculator.sta.start_pressure': 'Start Pressure (kPa)',
+'calculator.sta.temp_after': 'T after ignition',
+'calculator.sta.pressure_after': 'P after ignition',
+'calculator.sta.warn_low_fuel': '⚠ Fuel below 5%',
+'calculator.sta.warn_low_pressure': '⚠ Pressure below 10 kPa',
+'calculator.sta.panels': 'Panel Count',
+'calculator.sta.watts_per_panel': 'Watts/Panel',
+'calculator.sta.day_length': 'Day Length (s)',
+'calculator.sta.night_length': 'Night Length (s)',
+'calculator.sta.consumption': 'Consumption (W)',
+'calculator.sta.generation': 'Generation',
+'calculator.sta.surplus': 'Surplus',
+'calculator.sta.night_energy': 'Night Energy',
+'calculator.sta.batteries_needed': 'Batteries needed',
+'calculator.sta.target_temp': 'Target Temperature (K)',
+'calculator.sta.gas1_temp': 'Gas 1 Temperature (K)',
+'calculator.sta.gas2_temp': 'Gas 2 Temperature (K)',
+'calculator.sta.mixer_input1': 'Mixer Input 1',
+'calculator.sta.mixer_input2': 'Mixer Input 2',
+'calculator.sta.heat_cap_ref': 'Heat Capacities (Reference)',
+'calculator.sta.gas': 'Gas',
+```
+
+- [ ] **Step 5: Testen**
+
+1. STA-Tab klicken → 4 Sub-Tabs (Gas, Ofen, Solar, Atmo)
+2. Gas: P gesucht, V=1000, n=1, T=293.15 → P=2437.66 kPa (Stationeers-R ist groß!)
+3. Furnace: Fuel=0.5, T=293K, P=101kPa → hohe Temperatur nach Zündung
+4. Solar: 12 Panels, 500W, 600s Tag/Nacht, 2000W → 6000W Gen, 4000W Surplus, 24 Batteries
+5. Atmo: Target=300K, Gas1=200K, Gas2=400K → M1=50%, M2=50%
+6. Wärmekapazität-Tabelle aufklappbar
+
+- [ ] **Step 6: Commit**
+
+```bash
+git add src/js/calc-stationeers.js newtab.html src/css/main.css src/js/i18n.js
+git commit -m "feat(calculator): Stationeers Calculator mit Gas/Furnace/Solar/Atmo"
+```
+
+---
+
+### Task 8: Version Bump und CHANGELOG
+
+**Files:**
+- Modify: `manifest.json` — Version → 2.1.0
+- Modify: `manifest.firefox.json` — Version → 2.1.0
+- Modify: `manifest.opera.json` — Version → 2.1.0
+- Modify: `newtab.html` — Version-String → 2.1.0
+- Modify: `src/js/data.js` — Export-Version → 2.1.0
+- Modify: `src/js/app.js` — Backup-Version → 2.1.0
+- Modify: `CHANGELOG.md` — v2.1.0 Eintrag
+
+- [ ] **Step 1: Manifests aktualisieren**
+
+In allen drei Manifest-Dateien:
+```json
+"version": "2.1.0"
+```
+
+- [ ] **Step 2: newtab.html Version-String**
+
+Den bestehenden Version-String von "2.0.1" auf "2.1.0" ändern.
+
+- [ ] **Step 3: data.js Export-Version**
+
+```javascript
+version: '2.1.0',
+```
+
+- [ ] **Step 4: app.js Backup-Version**
+
+```javascript
+version: '2.1.0',
+```
+
+- [ ] **Step 5: CHANGELOG.md Eintrag**
+
+Am Anfang der Datei (nach dem Header):
+
+```markdown
+## [2.1.0] — 2026-04-16
+
+### Added
+- **Calculator Tab-System:** 6 Modi über Tab-Leiste erreichbar (Standard, Scientific, Unit, SAT, FAC, STA)
+- **Scientific-Modus:** Wurzel, Potenz, Pi, Euler, Vorzeichen-Wechsel + Formel-Helfer (Kreis, Pythagoras, Prozent, Temperatur)
+- **Unit-Converter:** 6 Kategorien (Länge, Gewicht, Temperatur, Volumen, Geschwindigkeit, Fläche) mit Live-Konvertierung und Swap
+- **Satisfactory Calculator:** Items/Min, Overclock-Power (Exponent 1.321928), Maschinen-Rechner
+- **Factorio Calculator:** Assembler-Ratios, Belt-Throughput, Maschinen-Rechner mit Belt-Empfehlung
+- **Stationeers Calculator:** Idealgas (PV=nRT), Furnace/Verbrennung, Solar/Batterie-Dimensionierung, Atmosphären-Mixer
+
+### Changed
+- Parser um `^` (Potenz, rechts-assoziativ) und `sqrt()` erweitert
+- Calculator-Widget Auto-Resize auf 320×480 für komplexe Modi
+- ~110 neue i18n-Keys (DE + EN)
+```
+
+- [ ] **Step 6: Commit**
+
+```bash
+git add manifest.json manifest.firefox.json manifest.opera.json newtab.html src/js/data.js src/js/app.js CHANGELOG.md
+git commit -m "feat(release): Version auf v2.1.0 bumpen — Calculator Upgrade"
+```