# ETW-Kaufpreisrechner bauen

Lässt Ihre KI einen geführten Kaufpreisrechner für Eigentumswohnungen programmieren: vier Schritte bis zur Obergrenze, Detailprüfung dahinter, Verkäuferansicht als Preisbrücke mit Korridor.

Portabler porthor-Skill aus der Kategorie Due Diligence.
Ergebnis: lauffähiger, geführter Kaufpreisrechner (HTML/CSS/JS) mit Verkäufer-Preisbrücke und Kaufpreisindikation. Zeitrahmen: ~30–45 Minuten Build, danach 5 Minuten je Objekt.

## So richten Sie den Skill als Gem ein

1. Legen Sie in Gemini ein neues Gem an.
2. Tragen Sie als Gem-Anweisung ein:

Befolge die hinterlegte Wissensdatei „ETW-Kaufpreisrechner bauen" als verbindliche Arbeitsanweisung.
Halte dich an die dort genannten Rechenregeln, das Ausgabeformat und das Startverhalten.
Fehlen dir Angaben, frage sie gebündelt nach, statt sie zu schätzen.
Weise Ergebnisse als Entwurf aus, den eine fachkundige Person prüft.

3. Laden Sie diese Datei beim Gem unter "Knowledge" als Wissensdatei hoch.

Ein Gem verbindet eigene Anweisungen mit hinterlegten Wissensdateien. So bleibt der
vollständige Skill erhalten, während die kurze Anweisung auf ihn verweist.

## Bevor Sie echte Unterlagen verwenden

Arbeiten Sie mit echten Namen, Adressen, Kontodaten oder Vertragsdaten? Dann prüfen Sie vor
der Übermittlung, ob diese Daten an den verwendeten KI-Anbieter übermittelt werden dürfen.
Der Skill selbst schützt diese Daten nicht, er beschreibt nur die Arbeitsweise.
Sensible Unterlagen lassen sich vorher lokal pseudonymisieren: https://porthor.de/download

Jedes Ergebnis ist ein Entwurf. Eine fachlich verantwortliche Person prüft Zahlen, Fristen
und rechtliche Aussagen, bevor etwas verwendet, versendet oder veröffentlicht wird.
Verwenden Sie dazu KI-generierte oder KI-bearbeitete Bilder, gilt seit dem 2. August 2026 die
Kennzeichnungspflicht des EU AI Act. Details: https://porthor.de/wissen/ai-act-immobilienprofis
Der Skill unterstützt Ihre Vorbereitung und ersetzt keine Rechtsberatung.

## System-Prompt

# SKILL · ETW-KAUFPREISRECHNER BAUEN
**Version 2.0 · Input = Auftrag (neue App, bestehende Variante anpassen oder nur Konzept) plus optionale Hauswerte · Output = lauffähige statische App in `dist/` (index.html, styles.css, app.js) mit Validierungsbericht**

> **Nutzung:** Diesen Skill laden, dann den Auftrag in einem Satz nennen („Baue die App", „Passe X an"). Der Skill arbeitet autonom nach den Referenzdateien in `references/`, baut die App vollständig und kennzeichnet jede Annahme und jeden Datenstand.

# Identität & Mission
Du bist ein Senior-Frontend-Entwickler mit Erfahrung als Immobilien-Investment-Analyst. Du programmierst einen geführten Kaufpreisrechner: eine Schnellrechnung in vier Schritten mit etwa zehn Eingaben, dahinter eine Detailprüfung mit allen ETW-Spezifika, und eine Verkäuferansicht, die den Preis nicht behauptet, sondern in fünf Stationen aus offengelegten Belastungen herleitet (Preisbrücke, Korridor, Einigungsoptionen). Du baust aus den Referenzen, nicht aus Erinnerung: Produktumfang aus `references/PRD.md`, Formeln aus `references/calculation-spec.md`, Oberfläche und Pflicht-IDs aus `references/ui-spec.md`, Datenmodell aus `references/data-schema.md`, Konstanten aus `references/data-tables.md`, Tests aus `references/acceptance-tests.md`. Fertig ist die App erst, wenn sie die Abnahmetests besteht.

# Eiserne Regeln
1. Faktenbindung: Jede Formel, jede Rundung und jede Reihenfolge stammt wörtlich aus `calculation-spec.md`. Erfinde keine Rechenwege, keine Schwellen, keine Kennzahlen.
2. Datenbindung: Steuersätze, Zinsreihe, Kostenbänder, II.-BV-Werte und Schwellen kommen ausschließlich aus `data-tables.md`. Aktualisierst du sie, dann nur aus der Primärquelle in `sources.md`, mit Kennzeichnung 🌐 RECHERCHIERT und neuem Datenstand in der Oberfläche. Ohne Webzugriff bleibt der Datenstand stehen und wird sichtbar als ⚠️ ANNAHME ausgewiesen.
3. Umfang = PRD. Du fügst keine Module hinzu (kein Eigennutzer-Modus, kein Backend, keine Steuerrechnung), es sei denn, der Nutzer verlangt es ausdrücklich; dann hältst du PRD, Rechenregeln, Schema, Validator und Tests gleichzeitig synchron.
4. Keine Rechts-, Steuer- oder Anlageberatung im Rechtssinn: Die Leitplankentexte aus PRD §25 gehören wörtlich in die Oberfläche; Mietspiegel, Kappungsgrenze, Mietpreisbremse, GEG-Marker und Kostenbänder sind Nutzerannahmen und Prüfpunkte, nie Rechtsaussagen.
5. Technik: kein Backend, kein Tracking, keine externen Skripte oder Fonts, keine `eval`/`new Function`, importierte oder eingegebene Texte nur als `textContent` oder Formularwert. Der Basispreis einer Sanierungskategorie wird nie von der Automatik überschrieben; die angepasste Schätzung steht daneben.
6. Im Zweifel konservativ: Fehlt eine Eingabe, zeigt die App „nicht berechenbar" und nennt das fehlende Feld. Unbezifferte Sonderumlagen erhalten nur die ⚠️-Bandbreite, nie einen Punktwert.

# Plattform-Kalibrierung (still für dich)
- Dateisystem und Ausführung vorhanden (Claude Code, Codex, Code Interpreter): Dateien direkt schreiben, danach `python scripts/validate_app.py ZIEL` und `node --check ZIEL/dist/app.js` ausführen und Fehler beheben, bevor du abgibst.
- Nur Chat ohne Dateisystem (claude.ai ohne Code-Ausführung, Gemini Gem, ChatGPT ohne Tools): Gib die drei Dateien vollständig und in fester Reihenfolge als Codeblöcke aus — `dist/index.html`, `dist/styles.css`, `dist/app.js` — jeweils in einem Stück, ohne Auslassungen wie „… Rest wie oben". Danach arbeitest du die manuelle Checkliste in `acceptance-tests.md` §1b ab und berichtest jeden Punkt. Bei Anpassungen bestehender Apps lieferst du ganze ersetzte Funktionen oder Blöcke, keine Diff-Fragmente.
- Kleines Kontextfenster: Lies die Referenzen in der Reihenfolge PRD → calculation-spec → ui-spec → data-schema; `data-tables.md` erst beim Schreiben von `app.js`, `acceptance-tests.md` in der Prüfphase.
- Webzugriff vorhanden: Bundesbank-Reihe und Grunderwerbsteuersätze dürfen gegen die Primärquellen geprüft werden. Kein Webzugriff: Datenstände aus `data-tables.md` beibehalten und nennen.
- Läuft der Skill in porthor, ist er dort nicht importierbar; dieser Skill ist für Claude, ChatGPT, Gemini und vergleichbare Werkzeuge gedacht.

# Arbeitsweise: Phasenmodell
Phase 1–3 sind Arbeit am Code, Phase 4 ist Prüfung; ausgegeben wird das Ergebnis nach dem Output-Vertrag.

## Phase 1 — Auftrag & Modus
Bestimme den Modus: (a) neue App: Zielverzeichnis, Standard `./etw-kaufpreisrechner/`; (b) bestehende Variante anpassen: erst deren Struktur lesen, nur die verlangten Punkte ändern, fremde Dateien nicht überschreiben; (c) nur Konzept oder PRD: die einschlägigen Referenzinhalte liefern, keine App bauen. Übernimm genannte Hauswerte (Bundesland, Zins, Hausgeld, MEA, Rücklage, Sonderumlagen) als Startwerte; enthält der Auftrag Klarnamen, bleiben sie außerhalb der ausgelieferten Defaults.

## Phase 2 — Referenzen lesen
Lies PRD, Rechenregeln, UI-Vertrag und Datenmodell vollständig. Notiere die Pflicht-IDs aus `ui-spec.md` §3, §5 und §7 als Arbeitsliste; jede fehlende ID lässt den Validator scheitern.

## Phase 3 — Bauen in fester Reihenfolge
1. `app.js`, Kopf: Konstanten aus `data-tables.md` (inkl. `renovationCategories`, `renovationPackages`, `levyCatalog`, `evidenceLevels`, `defaultLevies`), `globalDefaults`/`textDefaults` aus `data-schema.md`, `inspectionCatalog`, `wegCheckCatalog` (Namen wie in `ui-spec.md` §13).
2. Rechenkern als reine Funktionen: `getEffectiveCategoryPrice`, `calculateSpecialLevies`, `solvePrice`, `calculateScenario`, `computeMetrics`, `buildPriceBridge` — exakt nach `calculation-spec.md` inklusive §18 (ohne nicht anerkannte Positionen) und §19 (Korridor, Preisbrücke, Ampel, Übernahme-Delta).
3. `index.html`: Wizard mit vier Schritten Objekt → Miete → Kosten → Ergebnis (je Schritt Pflichtfelder, dann `details.advanced`, dann Weiter/Zurück), danach der Verkäuferdialog mit fünf Stationen und Zusammenfassungsblatt; jede ID genau einmal.
4. Rendering: Kategoriekarten, Maßnahmenliste („Maßnahme hinzufügen"), WEG-Checkliste und Hinweisliste werden von JS erzeugt; Wizard-Mechanik (`showStep`), Modusumschalter, „Mehr"-Menü.
5. `calculate()` schreibt in einem Durchlauf alle Käuferausgaben, Zusammenfassungszeilen, Ampel, Hinweise, die fünf Verkäuferstationen mit Preisbrücke (`buildPriceBridge`), das Zusammenfassungsblatt und `buildPurchaseOfferText`.
6. JSON-Export/-Import mit Whitelists und MFH-Ablehnung, Vollbild/Präsentationsmodus, Druck, Clipboard, `resetAll`.
7. `styles.css`: Design-DNA aus `ui-spec.md` §11, Präsentationsmodus, Druck A4 quer.
Sync-Pflicht: Jede Eingabe, die du anlegst, erscheint gleichzeitig in Käuferausgabe, Verkäuferansicht, Szenarien (soweit relevant), Indikation, JSON-Export und Importvalidierung.

## Phase 4 — Validieren
`validate_app.py` und `node --check` grün; danach die Abnahmegruppen S, O, W, H/R/L/F, K/Q, V, A/J und P aus `acceptance-tests.md` im Browser oder gedanklich am Code durchgehen. Startzustand mit Nordrhein-Westfalen muss 52.317 € Obergrenze, 98.404 € Gesamtinvestition und 640 € Monatsrate ergeben; die Preisbrücke muss rechnerisch schließen. Scheitert ein Test, korrigierst du den Code, nicht die Erwartung.

## Phase 5 — Abgabe
Nach dem Output-Vertrag.

# Rechenregeln (deterministisch)
Die Formeln stehen in `references/calculation-spec.md`; der Code muss diese Invarianten sichtbar erfüllen:
- Abzugsreihenfolge: erst absolute Abzüge (nicht umlagefähiges Hausgeld + Rücklagenzuführung + Verwaltung + Instandhaltung + Erbbauzins + monatliche Sonderumlagen-Anteile), dann der prozentuale Puffer.
- Sonderumlage anteilig = (Gesamtkosten − Rücklagendeckung) × Schlüssel × Gewicht (Evidenz belegt/wahrscheinlich 1, vorsorglich Ansatz-%); Verkäufer als Zahler → 0; „monatlich" fließt in die Abzüge, „einmalig" in die Investitionen; „nicht anerkannt" nur in der Zweitrechnung 0.
- Korridor = Kaufpreis mit Puffer bis ohne Puffer; Preisbrücke Budget − Nebenkosten − Sanierung − Sonderumlagen = Korridor oben − Risikopuffer = Korridor unten, rechnerisch geschlossen; Ampel nur als Lage des Angebots zum Korridor.
- Mietdeckel-Kaskade: vermietet → min(Ziel, Mietspiegel, Ist × Kappung); leer → min(Ziel, Mietspiegel × ggf. 1,10); Ist-Basis ohne Deckel.
- Kaufpreis: K = (Darlehen + EK − Investitionen + Inventar × GrESt-Satz) ÷ (1 + Nebenkostensatz), mit Fallunterscheidung K < Inventar.
- Prozentwerte werden als Prozent eingegeben, intern in voller Genauigkeit gerechnet, nur zur Anzeige gerundet; deutsches Zahlenformat; jeder Rechenschritt ist in der Oberfläche als Gleichung sichtbar.
- Kennzahlen mit den Rundungen aus Abschnitt 15 der Rechenregeln; fehlende Eingabe → „nicht berechenbar".

# Output-Vertrag
Gib nach der Umsetzung genau diese Punkte aus, knapp und ohne Marketing:
1. **Dateien** — Pfade der erzeugten oder geänderten Dateien.
2. **Funktionsumfang** — die Kernliste aus PRD §4 als abgehakte Liste; nicht umgesetzte Punkte offen benennen.
3. **Validierung** — Ausgabe von `validate_app.py` und `node --check` wörtlich, oder die Ergebnisse der manuellen Checkliste §1b.
4. **Datenstände und Annahmen** — Zinsreihe (Stand), Grunderwerbsteuer (Stand), Kostenbänder ⚠️; was der Nutzer vor einem realen Ankauf aktualisieren muss.
5. **Start** — wie die App geöffnet wird (`dist/index.html` lokal oder Sites-Workflow). Keine Zugangsdaten, Projekt-IDs oder temporären Archive nennen.

# Qualitäts-Gate (intern, vor Ausgabe)
- [ ] Vier Schritte Objekt → Miete → Kosten → Ergebnis mit Fortschritt, Zusammenfassungen und „Mehr"-Menü; Verkäuferdialog mit fünf Stationen und Zusammenfassungsblatt in dieser Reihenfolge?
- [ ] Schnellrechnung zeigt nur Pflichtfelder; Detailprüfung blendet alle `details.advanced` ein; alle Pflicht-IDs aus `ui-spec.md` vorhanden, keine doppelt?
- [ ] Startzustand: Paket mittel 35.000 €, Dach 120.000 / 40.000 → 6.640 €, Kaufpreis mit NRW 52.317 €, Preisbrücke geschlossen, Ampel bei 180.000 € „über dem Korridor"?
- [ ] JSON-Export → Import identisch; MFH-Datei wird mit Klartext abgelehnt; HTML in Notizen bleibt Text?
- [ ] Indikation enthält 14-Tage-Frist, § 12 WEG, § 311b BGB, Einsicht in Protokolle/Beschlusssammlung/Vermögensbericht, Stichtag Sonderumlagen und den Abschlusssatz; Ein-Seiten-Druck mit höchstens sechs Zahlen?
- [ ] Ohne Bundesland sichtbare Warnung; MEA-, Hausgeld- und II.-BV-Hinweise greifen?
- [ ] Präsentationsmodus ohne native Fullscreen nutzbar (Ausknopf, Escape); keine Schemata, keine Regler außer Paket und Szenarien?
- [ ] Keine externen Abhängigkeiten, kein `innerHTML` mit Nutzertext, Disclaimer aus PRD §25 wörtlich?

# Datenschutz
Die App speichert nichts außerhalb des Browsers und exportiert nur auf Nutzerwunsch als JSON. In ausgelieferten Startwerten stehen keine echten Personen-, Adress- oder Objektdaten; Beispielwerte sind neutral (Beispielstraße, Frau Beispiel). Nennt der Auftrag Klarnamen, Adressen oder Kontodaten, behandle sie vertraulich, fordere keine weiteren personenbezogenen Daten an und schreibe sie nicht in Code oder Defaults — sie gehören in eine JSON-Datei des Nutzers. Enthält ein bereitgestellter Datensatz Platzhalter (z. B. Name_Gruppe1), verwende sie wörtlich weiter und rekonstruiere niemals Klardaten.

# Reihenfolge am Ende der Ausgabe
Halte diese Reihenfolge immer ein: (1) das Ergebnis nach dem Output-Vertrag, (2) die Anschluss-Aufgaben als kurzes Angebot, (3) eine Trennlinie „---", (4) als letzte Zeile der Freigabe-Hinweis. Erzeugt der Skill einen versandfertigen Text, endet der kopierfertige Teil vor Schritt 2.

# Startverhalten
Liegt ein Auftrag vor, beginne direkt mit Phase 1 — kein Interview. Fehlen Angaben, baue die neue App im vollen Referenzumfang in `./etw-kaufpreisrechner/` und nenne am Ende, welche Startwerte der Nutzer anpassen kann. Nur der Modus „bestehende Variante anpassen" ohne mitgelieferten Code ist ein Grund für eine Rückfrage. Ergebnisse aus dem porthor-Skill „Due Diligence: Eigentumswohnung" (Hausgeld-Zerlegung, MEA, Rücklage, Sonderumlagen-Prognose) nimmst du als Startwerte an — nur belegte Zahlen, keine Bewertungen.

# Mögliche Anschluss-Aufgaben
Biete dem Nutzer nach dem Ergebnis an:
- Die größten Risiken bzw. offenen Punkte der Kalkulation zusammenzufassen
- Fehlende Unterlagen oder Angaben aufzulisten, die vor einem realen Ankauf zu beschaffen sind
- Hauswerte einzutragen (Bundesland, Zins, Hausgeld, Puffer, Kostenbänder) und als neue Startwerte zu setzen
- Zins- und Steuerstand aus den Primärquellen zu aktualisieren
- Farben, Logo oder Objektbezeichnung anzupassen; eine zweite Sprachfassung anzulegen
- Aus der Kaufpreisindikation ein formgerechtes Kundenanschreiben (DIN 5008) oder eine versandfertige E-Mail zu formulieren
Folge-Skills: „Due Diligence: Eigentumswohnung" (Unterlagenprüfung vor dem Kauf), „Finanzierungs-Analyse" (Bankfähigkeit), „Kaufvertragsentwurf-Check" (vor dem Notartermin).

# Anpassungszonen
| Bereich | Anpassung durch Nutzer | Risiko |
|---|---|---|
| `globalDefaults`, `textDefaults` | Startwerte des eigenen Objekts | gering |
| `renovationCategories`, `renovationPackages`, `levyCatalog`, `defaultLevies`, `levyCostBands` | Hauspreise, Pakete, Katalog, Bänder | gering |
| `:root`-Farben, Texte der Oberfläche, Objektbezeichnung | Erscheinungsbild | gering |
| `buildPurchaseOfferText` | Tonfall und Zusatzvorbehalte (Vorbehalte nie streichen) | mittel |
| `transferTaxRates`, `interestTimeline` | nur mit Primärquelle und neuem Datenstand | mittel |
| Rechenkern, Rundungen, JSON-Sicherheitsregeln, Rechtshinweise | **nicht verändern** | hoch |

# Rechtsrahmen
Alle Ergebnisse der App sind Orientierung, keine Bewertung, Finanzierungszusage, Rechts-, Steuer- oder Anlageberatung. Aussagen zu Teilungserklärung, Gemeinschaftsordnung, Beschlüssen, Mietverhältnissen und Grundbuchlasten sind Hinweise auf Prüfbedarf, keine Rechtsauskunft. Die Kaufpreisindikation ist unverbindlich; eine Frist beschreibt die Gültigkeit der Gesprächsgrundlage und begründet keine Bindung — ein Grundstückskauf entsteht erst durch notarielle Beurkundung (§ 311b BGB). Verwende in App und Ausgabe nie Formulierungen wie „garantiert", „rechtssicher", „vollkommen sicher" oder „geprüft konform".

# Status & Freigabe
Dein Ergebnis ist ein Entwurf zur Entscheidungsvorbereitung — keine Rechts-, Steuer- oder Anlageberatung. Eine fachlich verantwortliche Person prüft Zahlen, Fristen und rechtliche Aussagen und gibt das Ergebnis frei, bevor es verwendet, versendet oder veröffentlicht wird. Schließe jede finale Ausgabe mit einer kurzen Zeile ab, die darauf hinweist (eine Zeile genügt, z. B. „Entwurf — bitte fachlich prüfen und freigeben.").

# Anhang — Konservative Default-Annahmen (nur ohne bessere Daten; immer ⚠️ markieren)
- Grunderwerbsteuer: 16 Ländersätze, Stand September 2026 (`data-tables.md` §1). Bauzinsen: Bundesbank-Reihe bis Juli 2026, Stand 02.09.2026; Aug–Nov 2026 lineare Annahme bis 5,8 %.
- Hausgeld ⚠️ 2,50–4,50 €/m²/Monat; Rücklagenzuführung ⚠️ nach § 28 Abs. 2 II. BV 7,10 / 9,00 / 11,50 €/m²/Jahr nach Gebäudealter.
- Kostenbänder Gemeinschaftseigentum ⚠️ je m² Gesamtwohnfläche: Dach 80–150 · Fassade 120–250 · Fenster 60–120 · Stränge 90–180 · Heizung 40–120; Aufzug 60.000–120.000 € je Anlage.
- Sicherheits-/Mietpuffer 15 %; baujahrestypische Maßnahmen 50 % Ansatz; WEG-Darlehen 8 % Annuität; Kaufnebenkosten 2 % Notar/Grundbuch ohne Makler.

## Referenzdateien

Der Skill verweist auf diese Dateien. In der Ordner-Fassung liegen sie unter
`references/`, `scripts/` und `agents/`; hier stehen sie vollständig, in derselben
Reihenfolge, jeweils mit ihrem Pfad als Überschrift.

### agents/openai.yaml

`````
interface:
  display_name: "ETW-Kaufpreisrechner bauen"
  short_description: "Programmiert eine Kaufpreis-Rechner-App für Eigentumswohnungen."
  default_prompt: "Nutze $build-etw-kaufpreisrechner, um die vollständige ETW-Kaufpreisrechner-App gemäß den Referenzen zu programmieren und zu validieren."
`````

### references/acceptance-tests.md

`````
# Abnahme- und Regressionstests (Version 2.0)

## 1. Automatische Strukturprüfung

```bash
python3 SKILL_ROOT/scripts/validate_app.py APP_ROOT
node --check APP_ROOT/dist/app.js
```

Beide mit Exitcode 0.

## 1b. Manuelle Checkliste (ohne Python/Node)

1. Drei Dateien vorhanden, HTML verweist nur auf `./styles.css` und `./app.js`.
2. Reihenfolge `stepObject` < `stepRent` < `stepCosts` < `stepResult`; im Dialog `seller-start` < `seller-burdens` < `seller-uncertainty` < `seller-corridor` < `seller-options` < `sellerSummarySheet`.
3. Alle IDs aus `ui-spec.md` §5 und §7 existieren genau einmal.
4. `app.js` enthält `jsonVersion = 2`, `renovationPackages`, `levyCatalog`, `evidenceLevels`, `defaultLevies`, `calculateSpecialLevies(`, `getEffectiveCategoryPrice(`, `solvePrice(`, `buildPriceBridge(`, `buildPurchaseOfferText(`, `showStep(`, `renderInterestChart("buyerInterestChart"`, `§ 311b BGB`, `§ 12 WEG`, `GEG-Risiko`, `MFH-Kaufpreisrechner`.
5. Kein `eval`, kein `innerHTML` mit Nutzertext.
6. CSS enthält `presentation-mode`, `100dvh`, `@media print`, `print-seller`.
7. Startwerte: Paket mittel = 35.000 €; Maßnahme Dach 120.000 / 40.000 → 6.640 €; mit NRW 52.317 € Obergrenze, 98.404 € Gesamtinvestition, 640 € Monatsrate.
8. JSON-Export → Import identisch; Konsole ohne Fehler; keine horizontale Scrollbar bei 390 px.
9. Verkäuferansicht öffnet aus Schritt 4; Druckvorschau zeigt nur das Zusammenfassungsblatt.

## 2. Start und Wizard (S)

| ID | Aktion | Erwartung |
|---|---|---|
| S-01 | App öffnen | Schritt 1 sichtbar, „Schritt 1 von 4", Schnellrechnung, keine Detailbereiche, Header nur Marke/Fortschritt/Umschalter/„Mehr". |
| S-02 | Startwerte | 75 m², 3 Zi, 1975, MEA „83 / 1000", 900 m², 12 Einheiten; Miete 10/12, Basis erwartete Miete; Hausgeld 170/50/60; Paket mittel; eine Maßnahme Dach; Zins 4,8, Tilgung 3, EK 0, Provision 0. |
| S-03 | „Weiter" ohne Bundesland | Schritt 2 öffnet; Zusammenfassung „75 m² · 3 Zi · 1975 · MEA 83/1000 · Bundesland offen". |
| S-04 | Bis Schritt 4 durchklicken, NRW gesetzt | Obergrenze 52.317 €, Gesamtinvestition 98.404 €, Monatsrate 640 €; Rechenweg drei Zeilen. |
| S-05 | Detailprüfung einschalten | In jedem Schritt erscheint „Erweiterte Annahmen" (zugeklappt); Ausschalten versteckt sie wieder, Werte bleiben. |
| S-06 | Schritt-Leiste | Schritt 3 erst anklickbar, nachdem Schritt 2 mit „Weiter" verlassen wurde. |
| S-07 | „Mehr"-Menü | enthält Export, Import, Drucken, Vollbild, Startwerte; Escape schließt. |
| S-08 | Pflichtfeld leer (Wohnfläche löschen) | „Weiter" blockiert mit Hinweis am Feld. |

## 3. Objekt (O)

| ID | Aktion | Erwartung |
|---|---|---|
| O-01 | Bayern / Bremen / NRW | 3,5 % / 5,5 % / 6,5 %. |
| O-02 | MEA „120/1000" | Plausibilität 1,44, `meaHint` warnt; Hinweis in Schritt 4. |
| O-03 | MEA „abc" | Hinweis „Format 83 / 1000"; Rechnung mit letztem gültigen Wert. |
| O-04 | Detail: Flächenquelle Exposé | Hinweis am Feld, nicht in `calculationWarning`. |
| O-05 | Detail: Erbbaurecht ja, 80 €, Ende 2050 | Abzüge +80 €; Hinweis Restlaufzeit 24 Jahre. |
| O-06 | Detail: Gas-Zentral, Baujahr Wärmeerzeuger 1990 | Hinweis „Wärmeerzeuger 36 Jahre — GEG § 72 prüfen". |
| O-07 | Detail: Aufzug nein, Maßnahme Aufzug hinzufügen | Zeile zeigt Hinweis, zählt nicht. |
| O-08 | Detail: Kontakt „Frau Erika Beispiel" | Indikation mit persönlicher Anrede. |
| O-09 | Detail: Verkäufertyp Aufteiler / Bauträger | Indikation nennt § 577/§ 577a bzw. MaBV. |
| O-10 | Detail: WEG-Check „Rechtsstreit: Ja" | Hinweis in Schritt 4 (−), Verkäufer-Station 3; „Abt. II: Ja" → Warnbanner + Vorbehalt. |

## 4. Miete (W)

| ID | Aktion | Erwartung |
|---|---|---|
| W-01 | Erwartet 14, Detail Cap 12, Ist 10, vermietet | 900 €; `rentHint` nennt Kappungsgrenze und Mietspiegel. |
| W-02 | (Fortsetzung von W-01) Cap 0 | 900 € (Kappung 12,00 bindet). |
| W-03 | (Fortsetzung) angespannt ja | 862,50 €. |
| W-04 | (Fortsetzung, Zielmiete 14) leer, angespannt, Cap 12 | 990 €. |
| W-05 | Basis Ist-Miete | 750 €; Hinweis, dass Deckel nicht angewendet werden. |
| W-06 | Detail: Stellplatz 60 €, Sonstiges 20 € | Kaltmiete +80 €. |

## 5. Kosten (H, R, L, F)

| ID | Aktion | Erwartung |
|---|---|---|
| H-01 | 170/50/60 | Summe 280 €; Abzüge 147,50 € (110 + 37,50). |
| H-02 | nicht umlagefähig 250 | `houseFeeHint`: 6,40 €/m² außerhalb 2,50–4,50; Hinweis (−). |
| H-03 | Rücklage 75 | 12,00 €/m²/J. ≥ 11,50 → Hinweis (+). |
| H-04 | Detail: Nachzahlung 400 | Hinweis (−). |
| R-01 | Paket gering | 7.500 €; Label „gering". |
| R-02 | Paket umfassend | 55.000 €. |
| R-03 | Detail: Küche aktivieren bei Paket mittel | 47.000 €, Label „individuell", Regler `data-custom`. |
| R-04 | Detail: Bad drei positive Antworten | 20.000 → 12.800 → gerundet 13.000 €; „Basis → angepasst" mit Begründung. |
| R-05 | Detail: Bad GEG-Risiko | 14.000 €; Hinweis „GEG-Risiko". |
| R-06 | Wohnfläche 90 | Oberflächen/Elektro 90 m² (Paket bleibt). |
| R-07 | Detail: Bad „nicht anerkannt" | Hauptrechnung unverändert; Verkäufer-Station 3 zeigt Rechnung ohne Bad. |
| L-01 | Dach Evidenz vorsorglich | 3.320 € (50 %). |
| L-02 | Dach Deckung 120.000 | 0 €. |
| L-03 | Detail: Verkäufer zahlt | 0 €; Station 5 zeigt Übernahme; Station 1 Entlastung. |
| L-04 | „Maßnahme hinzufügen" Fassade, wahrscheinlich, Kosten 0 | Band 108.000–225.000 €; „Obergrenze übernehmen" setzt 225.000 → Anteil 18.675 €. |
| L-05 | Maßnahme entfernen | Liste leer, Hinweis „Keine Maßnahmen erfasst"; Kaufpreis steigt. |
| L-06 | MEA 166/1000 | Dach 13.280 €. |
| L-07 | Detail: Schlüssel Wohnfläche / Einheiten / abweichend 5 % | 6.667 / 6.667 / 4.000 €. |
| L-08 | Detail: Finanzierung monatlich 8 % | einmalig 0, monatlich 44,27 € in den Abzügen. |
| L-09 | Detail: Deckungen Σ 130.000 > Rücklage | `coverageWarning`. |
| L-10 | Detail: Rückstände 15.000 / Verkäuferrückstände 2.000 | Hinweise (−), Vorbehalt in der Indikation. |
| F-01 | Käuferprovision 3,57 % | Nebenkosten 12,07 % (NRW). |
| F-02 | Detail: Provisionsrechner 6/19/100 → übernehmen | `brokerBuyerRate` 7,14 %, `brokerHint` § 656c. |
| F-03 | Detail: Inventar 8.000 | Kaufpreis ca. +479 € gegenüber S-04. |
| F-04 | EK 20.000 | Kaufpreis +18.433 €. |
| F-05 | Zins + Tilgung 0 | Fehler, kein Unendlich. |
| F-06 | Kaufpreis < 100.000 | `notaryHint` (GNotKG) im Detail sichtbar. |

## 6. Ergebnis (K, Q)

| ID | Aktion | Erwartung |
|---|---|---|
| K-01 | Startwerte NRW | 52.317 € / 98.404 € / 640 €; `stepBudget` „98.404 €", `stepDeductions` „− 4.447 − 35.000 − 6.640", `stepPrice` „52.317 €". |
| K-02 | Szenario optimistisch | Primärzahlen ≈ 68.162 €; Note nennt +5 % Miete, −0,5 Pp., −10 %, −0 %. Konservativ ≈ 30.272 €. |
| K-03 | Zurück auf realistisch | 52.317 €. |
| K-04 | Detail: Korridor | 52.317 € bis 68.322 €; Risikopuffer 16.005 €. |
| K-05 | Detail: Zustand „nicht anerkannt" am Bad | Hauptergebnis unverändert; nur Station 3 zeigt die Rechnung ohne Bad. |
| Q-01 | Kennzahlen | Bruttorendite 20,64 %, Faktor 4,8, 698 €/m², Überschuss 113 €. |
| Q-02 | Angebot 180.000 | Ampel „über dem Korridor", Delta +127.683 €. |
| Q-03 | Angebot 60.000 | „nur ohne Risikopuffer tragfähig". |
| Q-04 | Angebot 50.000 | „im tragfähigen Bereich". |
| Q-05 | Angebot 0 | Karte „nicht berücksichtigt", keine Ampel. |
| Q-06 | Detail: Vergleichswert 2.600 €/m² | Delta −142.683 €. |
| Q-07 | Detail: Diagramm, Punkt „Nov 26" per Enter | Zins 5,8 % übernommen. |
| Q-08 | Aktionen | „Angebot vergleichen" fokussiert `askingPrice`; „Zusammenfassung teilen" öffnet Dialog; „Daten speichern" exportiert. |

## 7. Verkäuferansicht (V)

| ID | Aktion | Erwartung |
|---|---|---|
| V-01 | Öffnen ohne Angebot | Station 1: Zielsatz, positive Punkte (anteilige Rücklage 9.960 €), Ausgangspunkt = Korridor 52.317–68.322 €. |
| V-02 | Preisbrücke | 115.769 − 5.807 − 35.000 − 6.640 = 68.322 → − 16.005 = 52.317 (geschlossen). |
| V-03 | Belastungsliste | Sanierung „vorsorglich/wahrscheinlich" je Kategorie, Dach „wahrscheinlich" mit Beleg; Zwischenergebnis 41.640 €. |
| V-04 | Station 3 | Dach als vorsorglich → Bandbreite statt Betrag; „nicht anerkannt" → Rechnung ohne Position; Entlastungen; Satz zu positiven Unterlagen. |
| V-05 | Station 4 | Korridor, Tragfähigkeitssatz, Schlüsselsatz mit 98.404 € und 52.317 €. |
| V-06 | Angebot 180.000 | Station 1 Ausgangspunkt 180.000 €; Station 4 Ampel „über dem Korridor". |
| V-07 | Station 5 | Drei Optionen; Übernahme Dach → „+6.120 €"; Indikation mit Abschlusssatz. |
| V-08 | Drucken | Nur Zusammenfassungsblatt, sechs Zahlen, A4. |

## 8. Indikation (A) und JSON (J)

| ID | Aktion | Erwartung |
|---|---|---|
| A-01 | Frist 14 → 21 Tage | Datum +21. |
| A-02 | Rechtstexte | § 12 WEG, § 311b BGB, Stichtag Sonderumlagen, § 566/566a, Abschlusssatz. |
| A-03 | Kopieren | Zwischenablage + Meldung. |
| J-01 | Export | Schema v2, `renovation.package`, 5 Kategorien, Maßnahmenliste, 12 WEG-Antworten, `mode`. |
| J-02 | Re-Import | identisch inkl. Modus und Paket; alle Schritte freigeschaltet, Anzeige auf Schritt 4. |
| J-03 | MFH-Datei / v1-Datei | Ablehnung mit Klartext. |
| J-04 | Doppelte Maßnahmen-ID / unbekannter Typ | Ablehnung ohne Teilzustand. |
| J-05 | HTML im Beleg | erscheint als Text. |

## 9. iPad, Responsive, Druck (P)

| ID | Aktion | Erwartung |
|---|---|---|
| P-01 | 390 px | keine horizontale Seitenscrollbar; Maßnahmenzeile umbricht oder scrollt lokal. |
| P-02 | Vollbild ohne native API | Präsentationsmodus, `exitPresentationButton` sichtbar, Escape beendet. |
| P-03 | iPad quer | Verkäuferdialog ohne Außenränder im Präsentationsmodus. |

## 10. Freigabeblocker

Falsche Kaufpreisformel · falscher Steuersatz · Preisbrücke nicht geschlossen · Sonderumlagen-Formel falsch · Deckel-Kaskade falsch · Datenverlust im JSON · Verkäuferansicht ohne die fünf Stationen · bindend klingende Indikation · unbenutzbare iPad-Ansicht · JS-Syntaxfehler.
`````

### references/calculation-spec.md

`````
# Berechnungsspezifikation ETW-Kaufpreisrechner (Version 2.0)

Version 2.0 (08.09.2026): Rechenkern unverändert; neu sind Sanierungspakete (§7.2), Sonderumlagen als Liste mit Evidenzstufe (§8), Positionen „nicht anerkannt" (§18), Korridor, Preisbrücke, Übernahme-Delta, Ampel und monatlicher Überschuss (§19).

Alle Geldbeträge werden intern mit voller JavaScript-Genauigkeit geführt und nur für die Anzeige gerundet (Euro auf ganze Euro, Kennzahlen nach Abschnitt 15). Prozentsätze werden als Prozentwerte eingegeben (`4.8` für 4,8 %). Anzeige immer im deutschen Zahlenformat („52.317 €", „3,73 €/m²", „6,5 %"). Jede Kennzahl, deren Eingabe fehlt oder 0 ist, wo 0 fachlich „nicht erfasst" bedeutet, zeigt „nicht berechenbar" statt eines Werts.

## 1. Notation

| Symbol | Bedeutung |
|---|---|
| `A` | Wohnfläche der Wohnung in m² |
| `A_ges` | Gesamtwohnfläche der WEG in m² |
| `MEA`, `MEA_ges` | Miteigentumsanteil Zähler und Nenner |
| `q_MEA` | `MEA ÷ MEA_ges` |
| `N_WEG` | Zahl der Einheiten in der WEG |
| `r_ist`, `r_ziel`, `r_cap` | Ist-Miete, Zielmiete, Mietspiegelwert in €/m²/Monat |
| `E` | weitere monatliche Mieten (Stellplatz + Sonstiges) |
| `HG_u`, `HG_n`, `HG_r` | Hausgeld umlagefähig, nicht umlagefähig, Rücklagenzuführung (€/Monat) |
| `V` | Sondereigentums-/Mietverwaltung in €/Monat |
| `I` | Instandhaltung Sondereigentum in €/m²/Monat |
| `EZ` | Erbbauzins in €/Monat (nur bei Erbbaurecht) |
| `p` | Sicherheits-/Mietpuffer in % |
| `i`, `t` | Sollzins und anfängliche Tilgung in % p. a. |
| `R_SE` | Sanierung Sondereigentum inkl. Reserve |
| `S_ein`, `S_mon` | Sonderumlagen: einmaliger Anteil, monatlicher Anteil |
| `R` | `R_SE + S_ein` (Investitionen, die im Kaufpreis-Rahmen finanziert werden) |
| `EK` | Eigenkapital |
| `g` | Grunderwerbsteuersatz in % |
| `n_o` | Notar/Grundbuch + Käuferanteil Makler + weitere Nebenkosten in % |
| `INV` | mitverkauftes bewegliches Inventar in € |
| `K` | maximaler Kaufpreis |

## 2. Wohnraummiete (Deckel-Kaskade)

Mietbasis `current`:

```text
Wohnraummiete = A × r_ist
```

Mietbasis `target` — wirksame Zielmiete `r_w` in drei Schritten, alle Grenzen sind Nutzerannahmen:

```text
Grenze_Mietspiegel = r_cap > 0 ? r_cap : ∞
Wohnung vermietet (tenancyStatus = "rented"):
  Kappung% = angespannt ? 15 : 20                      (§ 558 Abs. 3 BGB)
  Grenze_Kappung = r_ist > 0 ? r_ist × (1 + Kappung%/100) : ∞
  r_w = min(r_ziel, Grenze_Mietspiegel, Grenze_Kappung)
Wohnung leer (tenancyStatus = "vacant"):
  Grenze_Bremse = angespannt ? Grenze_Mietspiegel × 1,10 : Grenze_Mietspiegel   (§ 556d BGB)
  r_w = min(r_ziel, Grenze_Bremse)
Wohnraummiete = A × r_w
```

Die App nennt die bindende Grenze im Klartext („Zielmiete durch Kappungsgrenze auf 12,00 €/m² begrenzt"). `r_cap = 0` heißt „kein Mietspiegelwert erfasst", nicht „keine Grenze im Rechtssinn".

## 3. Weitere Mieten und Kaltmiete

```text
E = Stellplatz + Sonstiges
Kaltmiete = Wohnraummiete + E
Jahreskaltmiete = Kaltmiete × 12
```

## 4. Laufende Abzüge und verfügbare Rate

Nur Kosten, die der Anleger nicht auf den Mieter umlegen kann, mindern die für die Rate verfügbare Miete. Der umlagefähige Hausgeldanteil `HG_u` wird angezeigt, aber nicht abgezogen.

```text
Hausgeld_gesamt = HG_u + HG_n + HG_r
Eigentümerlast_Hausgeld = HG_n + HG_r
laufende_Abzüge = Eigentümerlast_Hausgeld + V + A × I + EZ + S_mon
vor_Puffer = max(0, Kaltmiete − laufende_Abzüge)
Pufferbetrag = vor_Puffer × p / 100
verfügbare_Rate = max(0, vor_Puffer − Pufferbetrag)
```

Reihenfolge verbindlich: erst absolute Abzüge, dann der prozentuale Puffer.

## 5. Maklercourtage

```text
Makler_Käufer_% = m_netto × (1 + USt/100) × (Käuferanteil/100)
```

Beispiel 6,0 % × 1,19 × 0,50 = 3,57 %. Liegt der Käuferanteil über 50 %, zeigt die App den Hinweis auf § 656c/§ 656d BGB (Provisionsteilung beim Verkauf von Wohnungen an Verbraucher); sie rechnet trotzdem mit dem eingegebenen Wert.

## 6. Kaufnebenkosten mit Inventar-Regel

Mitverkauftes bewegliches Inventar (z. B. Einbauküche) mindert die Bemessungsgrundlage der Grunderwerbsteuer, nicht die übrigen Nebenkosten. Die anteilige Erhaltungsrücklage mindert die Bemessungsgrundlage **nicht** (BFH II R 49/17).

```text
n_o = Notar_Grundbuch_% + Makler_Käufer_% + Weitere_%
GrESt_Basis = max(0, K − INV)
Grunderwerbsteuer_€ = GrESt_Basis × g / 100
Kaufnebenkosten_€ = Grunderwerbsteuer_€ + K × n_o / 100
Nebenkosten_% (Anzeige) = g + n_o
```

## 7. Sanierung Sondereigentum

Fünf Kategorien statt neun Gewerke: `bathroom` (Bad), `surfaces` (Oberflächen: Boden, Putz, Maler), `electric` (Elektro), `kitchen` (Küche), `other` (Sonstiges). Für jede aktive Kategorie `j`:

```text
Positionskosten_j = Menge_j × Checkpreis_j
direkt_SE = Summe(aktive Positionskosten)
Reserve_SE = direkt_SE × Reserve_% / 100
R_SE = direkt_SE + Reserve_SE
```

### 7.2 Sanierungspakete (Schnellrechnung)

Der Schieberegler `renovationPackage` (0, 1, 2) aktiviert Kategorien mit ihren Standardmengen und -preisen:

```text
0 gering     = surfaces                                  → 75 × 100 = 7.500 €
1 mittel     = bathroom + surfaces + electric            → 20.000 + 7.500 + 7.500 = 35.000 €   (Standard)
2 umfassend  = bathroom + surfaces + electric + kitchen + other → 35.000 + 12.000 + 8.000 = 55.000 €
```

Flächenkategorien (`surfaces`, `electric`) folgen der Wohnfläche. Ändert der Nutzer in der Detailprüfung eine Kategorie manuell, wird `package = "custom"`; der Regler zeigt dann keine Stufe. Der Paketwechsel setzt Mengen und Basispreise der Kategorien auf die Standardwerte zurück.

### 7.1 Angepasste Schätzung (aus optionaler Zustandsschätzung)

Nur beantwortete Fragen zählen („Offen" zählt nicht).

```text
Faktor_roh = 1 − 0,12 × positive_Antworten + 0,08 × negative_Antworten
Faktor = min(1,30, max(0,55, Faktor_roh))
Checkpreis = Basispreis × Faktor
bei markiertem GEG-Risiko: Checkpreis = Checkpreis × 1,10
Rundung auf die Schrittweite des Basispreisreglers
```

Der Basispreis bleibt unverändert im Eingabefeld. Die UI nennt das Ergebnis „angepasste Schätzung" (kein Fachbegriff „Checkpreis") und zeigt „Basis → angepasst · Begründung". Es gibt keine gewerkspezifische Sonderregel (die Dach-Regel des MFH-Rechners entfällt, weil das Dach Gemeinschaftseigentum ist).

## 8. Sonderumlagen-Prognose (Gemeinschaftseigentum)

Die Sonderumlagen sind eine Liste von Maßnahmen (0 bis 30 Einträge, „Maßnahme hinzufügen"). Jede Maßnahme hat einen Katalogtyp `type` (`roof, facade, windows, pipes, heating, elevator, garage, balconies, other`) und eine Evidenzstufe `evidence`: `documented` (belegt 📄, z. B. beschlossen mit Betrag), `likely` (wahrscheinlich, angekündigt oder Angebot), `precautionary` (vorsorglich ⚠️, baujahrestypisch). Für jede Maßnahme `j`:

```text
offen_j = max(0, Gesamtkosten_j − Rücklagendeckung_j)

Schlüssel q_j:
  "mea"    → MEA ÷ MEA_ges
  "area"   → A ÷ A_ges
  "units"  → 1 ÷ N_WEG
  "custom" → abweichender Anteil_% ÷ 100

Gewicht w_j:
  evidence "documented" / "likely" → 1
  evidence "precautionary"         → Ansatz_vorsorglich_% ÷ 100   (Default 50 %)
  payer "seller"                   → 0 (fällig vor Übergang, zahlt der Verkäufer)
  disputed = true                  → 0 in der Rechnung „ohne nicht anerkannte Positionen" (§18), sonst normal

Anteil_j = offen_j × q_j × w_j

financing "levy" (Sonderumlage, einmalig):
  S_ein += Anteil_j
financing "loan" (Hausgelderhöhung / WEG-Darlehen, monatlich):
  S_mon += Anteil_j × Annuität_WEG_% / 100 / 12          (Default 8 % p. a.)
```

Eine Maßnahme vom Typ `elevator` ist nur bei `hasElevator = yes` zulässig; sonst zeigt die Zeile einen Hinweis und zählt in keine Summe. Zeilen mit `payer = "open"` werden wie Käuferzeilen gerechnet und im Verkäufertext als Stichtagsfrage markiert. Nicht bezifferte Maßnahmen (Gesamtkosten 0) liefern 0 und den Bandhinweis; die App setzt nie selbst einen Punktwert.

Kennzahlen der Gemeinschaft:

```text
anteilige_Rücklage = Rücklage_gesamt × q_MEA
Rücklage_je_m²_WEG = Rücklage_gesamt ÷ A_ges
bezifferte_Maßnahmen = Summe(Gesamtkosten_j für evidence documented oder likely)
Deckungsgrad_% = bezifferte_Maßnahmen > 0 ? Rücklage_gesamt ÷ bezifferte_Maßnahmen × 100 : "nicht berechenbar"
Summe_Deckung = Summe(Rücklagendeckung_j)          → Warnung, wenn > Rücklage_gesamt
Rückstandsquote_% = Rücklage_gesamt > 0 ? Rückstände_Gemeinschaft ÷ Rücklage_gesamt × 100 : "nicht berechenbar"
```

## 9. Annuität und Darlehensrahmen

```text
Annuität = (i + t) / 100
maximales_Darlehen = verfügbare_Rate × 12 ÷ Annuität
```

Zins + Tilgung = 0 → Darlehen 0, Kaufpreis 0, sichtbarer Fehler.

## 10. Maximaler Kaufpreis

Aus `maximales_Darlehen + EK = K + Kaufnebenkosten_€ + R` mit `R = R_SE + S_ein` folgt unter der Annahme `K ≥ INV`:

```text
Budget = maximales_Darlehen + EK − R
K = (Budget + INV × g/100) ÷ (1 + (g + n_o)/100)
```

Fallunterscheidung (nur bei `INV > 0`): Ist das Ergebnis `K < INV`, gilt `GrESt_Basis = 0` und

```text
K = Budget ÷ (1 + n_o/100)
```

In beiden Fällen `K = max(0, K)`. Interne Obergrenze ohne Puffer: dieselbe Rechnung mit `vor_Puffer` statt `verfügbare_Rate`.

## 11. Gesamtinvestition und Bankfinanzierung

```text
Gesamtinvestition = K + Kaufnebenkosten_€ + R
Bankfinanzierung = max(0, Gesamtinvestition − EK)
anfängliche_Monatsrate = Bankfinanzierung × Annuität ÷ 12
Ratenanteil_an_Kaltmiete_% = Kaltmiete > 0 ? anfängliche_Monatsrate ÷ Kaltmiete × 100 : 0
```

`S_mon` ist keine Bankrate, sondern laufende Objektlast; er ist Teil der `laufenden_Abzüge` und erscheint im Miet-Wasserfall der Verkäuferansicht als Unterzeile „davon Sonderumlagen monatlich“ (kein zusätzlicher Abzug).

## 12. Drei Szenarien

Jedes Szenario verändert nur Kaltmiete, Sollzins, Investitionen `R` und die Eigentümerlast aus dem Hausgeld. Puffer, Tilgung, Eigenkapital, Nebenkostensätze, Verwaltung, Instandhaltung, Erbbauzins und `S_mon` bleiben gleich.

```text
Optimistisch:  Miete × (1 + Mietplus/100) · Zins − Zinsabschlag (min 0) · R × (1 − Sanierungsabschlag/100) · Eigentümerlast_Hausgeld × (1 − Hausgeldabschlag/100)
Realistisch:   unverändert
Konservativ:   Miete × (1 − Mietabschlag/100) · Zins + Zinsaufschlag · R × (1 + Sanierungsaufschlag/100) · Eigentümerlast_Hausgeld × (1 + Hausgeldaufschlag/100)
```

Defaults: +5 % / −0,5 Pp. / −10 % / −0 % und −5 % / +1,0 Pp. / +15 % / +10 %. Die Investitionsänderung wirkt proportional auf `R_SE` und `S_ein`; `stepDeductions` zeigt die skalierten Teilbeträge. Für jedes Szenario werden Abschnitte 4, 9 und 10 erneut ausgeführt; Käufer- und Verkäuferansicht zeigen dieselben Werte.

## 13. Zinsdiagramm

```text
Monatsrate_Datenpunkt = D × ((z + t) / 100) ÷ 12
```

Käuferdiagramm: `D` = Vergleichsdarlehen (Default 150.000 €). Verkäuferdiagramm: `D` = Bankfinanzierung aus Abschnitt 11. Punkt aktivieren → `z` wird als Sollzins übernommen.

## 14. Angebotspreis und Vergleichswert

```text
Delta_Angebot = Angebotspreis − K                     (nur wenn Angebotspreis > 0)
Vergleichswert_€ = Vergleichswert_€/m² × A            (nur wenn > 0)
Delta_Vergleich = K − Vergleichswert_€
```

Kennzahlen aus Abschnitt 15 werden für `K` und zusätzlich für den Angebotspreis berechnet (Kaufnebenkosten dann auf den Angebotspreis).

## 15. Kennzahlen (Formeln und Rundungen wörtlich wie porthor „Due Diligence: Eigentumswohnung")

| Kennzahl | Formel | Rundung |
|---|---|---|
| Kaufpreis je m² | `Preis ÷ A` | ganze € |
| Hausgeld pro Jahr | `Hausgeld_gesamt × 12` | ganze € |
| Bruttorendite | `Kaltmiete × 12 ÷ Preis × 100` | 2 Nachkommastellen, % |
| Kaufpreisfaktor | `Preis ÷ (Kaltmiete × 12)` | 1 Nachkommastelle |
| Hausgeld je m² und Monat | `Hausgeld_gesamt ÷ A` | 2 Nachkommastellen |
| Hausgeldquote | `HG_n ÷ Kaltmiete × 100` | 1 Nachkommastelle, % |
| Anteilige Rücklage | `Rücklage_gesamt × MEA ÷ MEA_ges` | 2 Nachkommastellen |
| MEA-Plausibilität | `(MEA ÷ MEA_ges) ÷ (A ÷ A_ges)` | 2 Nachkommastellen |
| Nettomietrendite | `(Kaltmiete × 12 − HG_n × 12) ÷ (Preis + Kaufnebenkosten_€) × 100` | 2 Nachkommastellen, % |
| Rücklagenzuführung je m² und Jahr | `HG_r × 12 ÷ A` | 2 Nachkommastellen |
| Deckungsgrad | Abschnitt 8 | 0 Nachkommastellen, % |
| AfA-Satz (Orientierung) | Tabelle `afaRates` nach Baujahr | — |

„nicht berechenbar", wenn `Preis = 0`, `Kaltmiete = 0`, `A = 0`, `A_ges = 0` oder `MEA_ges = 0`.

## 16. Warnschwellen und automatische Befunde

| Prüfung | Bedingung | Wirkung |
|---|---|---|
| Bundesland fehlt | `federalState = ""` | Warnung: Obergrenze zu hoch |
| Zins + Tilgung = 0 | — | Fehler, kein Ergebnis |
| Budget negativ | `Budget < 0` | Warnung: Rahmen deckt Investitionen nicht |
| MEA-Plausibilität | außerhalb 0,9–1,1 | negativer Befund + Hinweis; innerhalb → positiver Befund |
| Hausgeld je m² | außerhalb 2,50–4,50 | negativer Befund; innerhalb → positiver Befund |
| Rücklagenzuführung | unter II.-BV-Richtwert des Gebäudealters | negativer Befund; darüber → positiver Befund |
| Deckungsgrad | < 100 % | negativer Befund; ≥ 100 % → positiver Befund; „nicht berechenbar“ → kein Befund |
| Σ Rücklagendeckung > Rücklage | — | Warnung in der Tabelle |
| Rückstände Gemeinschaft | > 10 % der Rücklage | negativer Befund |
| Rückstände Verkäufer | > 0 | negativer Befund + Vorbehalt |
| Nachzahlung letzte Abrechnung | > 10 % von `Hausgeld_gesamt × 12` | negativer Befund |
| Erbbaurecht | Restlaufzeit `Laufzeitende − aktuelles Jahr < 40` | negativer Befund + Vorbehalt |
| Zentraler Wärmeerzeuger | Alter > 30 Jahre bei Gas-/Öl-Zentralheizung; Alter aus `heatingYear`, bei 0 kein Befund (kein Rückgriff auf das Gebäudebaujahr) | negativer Befund „GEG § 72 prüfen"; Vorschlag Status „baujahrestypisch" für Zeile `heating` (nur Hinweis, keine automatische Änderung) |
| Käuferanteil Makler | > 50 % | Hinweis § 656c BGB |
| Notar/Grundbuch | `Korridor_unten < 100.000` | Hinweis GNotKG degressiv (`notaryHint` in der Detailprüfung und in der Hinweisliste) |
| Abt. II belastet | WEG-Check `section2_rights = yes` | Warnbanner + Vorbehalt in der Indikation |

## 17. Vollständiges Rechenbeispiel (Startwerte der App)

Annahmen: Nordrhein-Westfalen 6,5 %; Notar 2,0 %; kein Makler, keine weiteren Nebenkosten; Wohnfläche 75 m²; vermietet, Ist 10 €/m², Ziel 12 €/m², Mietspiegel 12 €/m², nicht angespannt; Hausgeld 170 / 50 / 60; Verwaltung 0; Instandhaltung 0,50 €/m²; Puffer 15 %; Zins 4,8 %; Tilgung 3,0 %; Sondereigentum 35.000 €; Dach angekündigt 120.000 € mit 40.000 € Deckung, MEA 83/1000, Käufer, Sonderumlage; Eigenkapital 0; Inventar 0.

```text
Grenze_Kappung = 10 × 1,20 = 12,00 → r_w = min(12; 12; 12) = 12,00
Wohnraummiete = 75 × 12 = 900,00 €
laufende_Abzüge = 50 + 60 + 0 + 75 × 0,50 + 0 + 0 = 147,50 €
vor_Puffer = 900 − 147,50 = 752,50 €
verfügbare_Rate = 752,50 × 0,85 = 639,625 €
Annuität = 7,8 % → maximales_Darlehen = 639,625 × 12 ÷ 0,078 = 98.403,85 €
Sonderumlage Dach = (120.000 − 40.000) × 0,083 × 1 = 6.640,00 €
R = 35.000 + 6.640 = 41.640,00 €
Budget = 98.403,85 − 41.640 = 56.763,85 €
K = 56.763,85 ÷ 1,085 = 52.316,91 €
Grunderwerbsteuer = 52.316,91 × 6,5 % = 3.400,60 €
Notar = 52.316,91 × 2 % = 1.046,34 € → Kaufnebenkosten = 4.446,94 €
Gesamtinvestition = 52.316,91 + 4.446,94 + 41.640 = 98.403,85 €
Bankfinanzierung = 98.403,85 € · anfängliche Monatsrate = 639,63 €
interne Obergrenze ohne Puffer = (752,50 × 12 ÷ 0,078 − 41.640) ÷ 1,085 = 68.321,87 €
Optimistisch ≈ 68.162 € · Konservativ ≈ 30.272 €
```

Kennzahlen: Kaufpreis je m² 698 €; Bruttorendite 20,64 %; Kaufpreisfaktor 4,8; Hausgeld je m² 3,73 €; Hausgeldquote 5,6 %; anteilige Rücklage 9.960,00 €; MEA-Plausibilität 1,00 (0,996); Nettomietrendite 17,97 %; Rücklagenzuführung 9,60 €/m²/Jahr (Richtwert bei 51 Jahren Gebäudealter 11,50 → Befund); Deckungsgrad 100 % (120.000 ÷ 120.000).

Die Oberfläche zeigt gerundet **52.317 €** maximalen Kaufpreis.

## 18. Rechnung ohne nicht anerkannte Positionen

Jede Sonderumlagen-Maßnahme und jede Sanierungskategorie trägt ein Feld `disputed` („vom Verkäufer nicht anerkannt"). Die Hauptrechnung berücksichtigt alle Positionen. Zusätzlich wird `K_ohne_Bestritten` berechnet, indem alle Positionen mit `disputed = true` mit 0 angesetzt werden (Abschnitte 7, 8, 10 erneut). Die Verkäuferansicht zeigt beide Werte nebeneinander; die Kaufpreisindikation verwendet die Hauptrechnung.

## 19. Korridor, Preisbrücke, Übernahme-Delta, Ampel, Überschuss

```text
Korridor_unten = K                       (mit Sicherheits-/Mietpuffer, Abschnitt 10)
Korridor_oben  = K_ohne_Puffer           (Abschnitt 10, mit vor_Puffer)
Risikopuffer_€ = Korridor_oben − Korridor_unten
```

Preisbrücke (Verkäuferansicht, Station 2; rechnerisch geschlossen):

```text
Budget_ohne_Puffer      = vor_Puffer × 12 ÷ Annuität + EK
− Kaufnebenkosten       = Kaufnebenkosten_€ bei Korridor_oben
− Sanierung Sondereigentum = R_SE
− Sonderumlagen (Anteil)   = S_ein
= Korridor_oben
− Risikopuffer_€
= Korridor_unten
```

Angebotspreis (optional): ist er > 0, zeigt Station 1 ihn als Ausgangspunkt und Station 4 die Lage zum Korridor; sonst zeigt Station 1 den Korridor.

```text
Ampel:  Angebot ≤ Korridor_unten → "im tragfähigen Bereich"
        Angebot ≤ Korridor_oben  → "nur ohne Risikopuffer tragfähig"
        Angebot >  Korridor_oben → "über dem Korridor"
Delta_Angebot = Angebot − Korridor_unten
```

Übernahme-Delta je Maßnahme `j` mit `payer ≠ seller` und `financing = levy`:

```text
ΔK_j = Anteil_j ÷ (1 + (g + n_o)/100)
```

(„Übernimmt der Verkäufer diese Maßnahme, steigt das Angebot um ΔK_j."). Bei `financing = loan`: ΔK_j aus der Rate: `ΔK_j = (S_mon_j × 12 ÷ Annuität × (1 − p/100)) ÷ (1 + (g + n_o)/100)` — vereinfacht, als ⚠️ kennzeichnen.

Monatlicher Überschuss bei Korridor_unten:

```text
Überschuss = Kaltmiete − laufende_Abzüge − anfängliche_Monatsrate   (= Pufferbetrag, wenn EK = 0 und K > 0)
```

Beispiel mit Startwerten: Korridor 52.317 € bis 68.322 €, Risikopuffer 16.005 €; Preisbrücke: Budget 115.769,23 − 5.807,36 − 35.000 − 6.640 = 68.321,87 → − 16.004,96 = 52.316,91. Angebot 180.000 € → „über dem Korridor", Delta +127.683 €. Übernahme Dach durch Verkäufer: +6.119,82 € (6.640 ÷ 1,085). Überschuss 112,88 €/Monat.
`````

### references/data-schema.md

`````
# JSON-Datenmodell (Version 2)

## 1. Format

- `.json`, UTF-8, `application/json`; Schema `etw-kaufpreisrechner`, Version `2`; Import ≤ 1 MB.
- Abgeleitete Werte werden nicht gespeichert, sondern neu berechnet.
- Ablehnungen mit Klartext: `schema: "kaufpreis-rechner"` → „Diese Datei stammt aus dem MFH-Kaufpreisrechner und passt nicht zum ETW-Rechner."; `version: 1` → „Diese Datei stammt aus Version 1 des ETW-Rechners; bitte in Version 2 neu erfassen."

## 2. Top-Level-Struktur

```json
{
  "schema": "etw-kaufpreisrechner",
  "version": 2,
  "exportedAt": "2026-09-08T12:00:00.000Z",
  "inputs": {},
  "renovation": { "package": "medium", "categories": [] },
  "specialLevies": [],
  "wegChecks": {}
}
```

## 3. `inputs` — Text- und Auswahlfelder

| Schlüssel | erlaubte Werte / Grenze | Standard |
|---|---|---|
| `mode` | `quick`, `detail` | `quick` |
| `scenarioSelected` | `realistic`, `optimistic`, `conservative` | `realistic` |
| `objectName` | max. 120 | `Eigentumswohnung` |
| `street` / `postalCode` / `city` | 160 / 12 / 100 | leer |
| `federalState` | leer oder 16 Bundesländer | leer |
| `areaSource` | `unknown`, `declaration`, `expose`, `survey` | `unknown` |
| `hasElevator` | `unknown`, `yes`, `no` | `unknown` |
| `managerName` | max. 160 | leer |
| `sellerType` | `unknown`, `private`, `developer`, `converter` | `unknown` |
| `specialRights` | max. 200 | leer |
| `parkingOwnership` | `none`, `special-property`, `special-use`, `separate` | `none` |
| `leasehold` | `no`, `yes` | `no` |
| `heatingType` | `unknown`, `gas-central`, `oil-central`, `district`, `heatpump`, `gas-unit`, `electric` | `unknown` |
| `energyCertificateStatus` | `unknown`, `available`, `missing` | `unknown` |
| `energyCertificateType` | `unknown`, `consumption`, `demand` | `unknown` |
| `energyClass` | `unknown`, `A+`, `A` … `H` | `unknown` |
| `energyCertificateValidUntil` | HTML-Datum oder leer | leer |
| `sellerSalutation` | leer, `Frau`, `Herr`, `Familie`, `Firma` | leer |
| `sellerFirstName`, `sellerLastName` | 100 | leer |
| `sellerCompany` | 160 | leer |
| `sellerPhone` | 80 | leer |
| `sellerEmail` | 160 | leer |
| `tenancyStatus` | `rented`, `vacant` | `rented` |
| `tightMarket` | `no`, `yes` | `no` |
| `rentContractType` | `standard`, `graduated`, `index` | `standard` |
| `rentBasis` | `current`, `target` | `target` |
| `otherRentLabel` | 100 | `Keller / Sonstiges` |

## 4. `inputs` — Zahlenfelder (endlich, ≥ 0)

| Schlüssel | Einheit | Standard |
|---|---|---:|
| `area` | m² | 75 |
| `rooms` | Anzahl | 3 |
| `floorLevel` | Etage, 0 = EG | 2 |
| `floors` | Geschosse | 4 |
| `yearBuilt` | Jahr | 1975 |
| `heatingYear` | Jahr, 0 = unbekannt | 0 |
| `wegUnits` | Einheiten | 12 |
| `wegTotalArea` | m² | 900 |
| `meaShare` / `meaTotal` | MEA Zähler / Nenner | 83 / 1000 |
| `reserveTotal` | € | 120000 |
| `communityArrears` / `sellerArrears` | € | 0 / 0 |
| `groundRentMonthly` | €/Monat | 0 |
| `leaseholdEndYear` | Jahr, 0 = nicht erfasst | 0 |
| `typicalLevyWeight` | % (vorsorgliche Maßnahmen) | 50 |
| `wegLoanAnnuity` | % p. a. | 8 |
| `unitReserve` | % | 0 |
| `currentRentPerSqm` / `rentPerSqm` / `rentCapPerSqm` | €/m²/Monat | 10 / 12 / 12 |
| `parkingRent` / `otherRent` | €/Monat | 0 / 0 |
| `houseFeeRecoverable` / `houseFeeNonRecoverable` / `houseFeeReserve` | €/Monat | 170 / 50 / 60 |
| `houseFeeBackPayment` | € | 0 |
| `managementCostMonthly` | €/Monat | 0 |
| `ownerMaintenancePerSqm` | €/m²/Monat | 0.5 |
| `rentBuffer` | % | 15 |
| `optimisticRentIncrease` / `optimisticInterestReduction` / `optimisticRenovationReduction` / `optimisticHouseFeeReduction` | % / Pp. / % / % | 5 / 0.5 / 10 / 0 |
| `conservativeRentReduction` / `conservativeInterestIncrease` / `conservativeRenovationIncrease` / `conservativeHouseFeeIncrease` | % / Pp. / % / % | 5 / 1 / 15 / 10 |
| `interest` / `repayment` | % p. a. | 4.8 / 3 |
| `chartLoan` | € | 150000 |
| `notaryLandRegistryRate` | % | 2 |
| `brokerBuyerRate` | % effektive Käuferprovision | 0 |
| `brokerCommissionNetRate` / `brokerVatRate` / `brokerBuyerShareRate` | % (Provisionsrechner) | 0 / 19 / 50 |
| `otherPurchaseCostsRate` | % | 0 |
| `equity` / `inventoryValue` / `askingPrice` | € (0 = nicht berücksichtigt) | 0 / 0 / 0 |
| `marketPricePerSqm` | €/m², 0 = nicht erfasst | 0 |
| `offerValidityDays` | Tage 1–60 | 14 |

`brokerBuyerRate` ist der rechnende Wert. Der Provisionsrechner (`brokerCommissionNetRate` × (1 + `brokerVatRate`/100) × `brokerBuyerShareRate`/100) schreibt ihn nur, wenn der Nutzer den Button „übernehmen“ betätigt.

## 5. `renovation`

```json
{
  "package": "medium",
  "categories": [
    { "id": "bathroom", "active": true, "quantity": 1, "unitPrice": 20000, "inspection": { "modernized": "unknown", "moisture": "unknown", "waterproofing": "unknown" }, "gegRisk": false, "source": "", "disputed": false },
    { "id": "surfaces", "active": true, "quantity": 75, "unitPrice": 100, "inspection": {}, "gegRisk": false, "source": "", "disputed": false },
    { "id": "electric", "active": true, "quantity": 75, "unitPrice": 100, "inspection": {}, "gegRisk": false, "source": "", "disputed": false },
    { "id": "kitchen", "active": false, "quantity": 1, "unitPrice": 12000, "inspection": {}, "gegRisk": false, "source": "", "disputed": false },
    { "id": "other", "active": false, "quantity": 1, "unitPrice": 8000, "inspection": {}, "gegRisk": false, "source": "", "disputed": false }
  ]
}
```

`package` ∈ `low`, `medium`, `high`, `custom`. Welche Antwort je Prüffrage positiv ist, steht im PRD §10. Genau die fünf IDs `bathroom`, `surfaces`, `electric`, `kitchen`, `other`, jede einmal. Prüffragen (Antworten `unknown`/`yes`/`no`; fehlend = `unknown`): bathroom `modernized`, `moisture`, `waterproofing` · surfaces `floors`, `plaster`, `pollutants` · electric `rcd`, `three_wire`, `circuits` · kitchen `included`, `usable`, `tenant_owned` · other `damage`, `backlog`, `documentation`. `source` ≤ 200 Zeichen.

## 6. `specialLevies[]`

0 bis 30 Einträge, `id` eindeutig (`^[a-z0-9-]{1,20}$`).

```json
{ "id": "m1", "type": "roof", "evidence": "likely", "totalCost": 120000, "reserveCoverage": 40000, "key": "mea", "customShare": 0, "horizon": "1-3", "payer": "buyer", "financing": "levy", "source": "", "disputed": false }
```

| Feld | erlaubte Werte |
|---|---|
| `type` | `roof`, `facade`, `windows`, `pipes`, `heating`, `elevator`, `garage`, `balconies`, `other` |
| `evidence` | `documented`, `likely`, `precautionary` |
| `totalCost`, `reserveCoverage` | Zahl ≥ 0 |
| `key` | `mea`, `area`, `units`, `custom`; `customShare` 0–100 |
| `horizon` | `0-1`, `1-3`, `3-10` |
| `payer` | `buyer`, `seller`, `open` |
| `financing` | `levy`, `loan` |
| `source` | ≤ 200 Zeichen |
| `disputed` | Boolean |

Standard: genau der Eintrag oben.

## 7. `wegChecks{}`

Schlüssel (`unknown`/`yes`/`no`, fehlend = `unknown`): `reserve_verified`, `minutes_complete`, `resolutions_complete`, `business_plan`, `community_arrears`, `litigation`, `purpose_rental`, `sale_consent`, `section2_rights`, `major_owner`, `tenant_rights`, `structural_changes`.

## 8. Import-Sicherheitsregeln

Nur `JSON.parse`; Zahlen endlich, ≥ 0, < `Number.MAX_SAFE_INTEGER`; Texte längenbegrenzt; Auswahlwerte gegen Whitelists; Nutzertexte nur als Formularwert/`textContent`; kein Teilzustand bei Fehlern; fehlende `inputs`-Schlüssel sind ein Fehler mit Feldnennung; `offerValidityDays` außerhalb 1–60 ist ein Fehler; Maßnahmen mit unbekanntem Typ sind ein Fehler; doppelte Maßnahmen-IDs sind ein Fehler.
`````

### references/data-tables.md

`````
# Datentabellen (kopierfertig für `dist/app.js`)

Jede Tabelle trägt einen Datenstand. Übernimm die Konstanten wörtlich. Aktualisiere sie nur aus der in `sources.md` genannten Primärquelle und ändere dann gleichzeitig den sichtbaren Standhinweis in `dist/index.html`, dieses Dokument und das Rechenbeispiel in `calculation-spec.md`.

## 1. Grunderwerbsteuer nach Bundesland (Stand September 2026)

```js
const transferTaxRates = {
  "Baden-Württemberg": 5.0,
  Bayern: 3.5,
  Berlin: 6.0,
  Brandenburg: 6.5,
  Bremen: 5.5,
  Hamburg: 5.5,
  Hessen: 6.0,
  "Mecklenburg-Vorpommern": 6.0,
  Niedersachsen: 5.0,
  "Nordrhein-Westfalen": 6.5,
  "Rheinland-Pfalz": 5.0,
  Saarland: 6.5,
  Sachsen: 5.5,
  "Sachsen-Anhalt": 5.0,
  "Schleswig-Holstein": 6.5,
  Thüringen: 5.0,
};
```

Kein Bundesland gewählt → rechnerisch 0 % und sichtbare Warnung (siehe PRD §8).

## 2. Bauzinsreihe für das Zinsdiagramm (Deutsche Bundesbank, Stand 02.09.2026)

Wohnungsbaukredite an private Haushalte, anfängliche Zinsbindung über 5 bis 10 Jahre. `phase` steuert Linienfarbe, Achsenbeschriftung und die Szenario-Zone.

```js
const interestTimeline = [
  { label: "2020 Ø", rate: 1.0908, phase: "annual" },
  { label: "2021 Ø", rate: 1.0917, phase: "annual" },
  { label: "2022 Ø", rate: 2.4092, phase: "annual" },
  { label: "2023 Ø", rate: 3.73, phase: "annual" },
  { label: "2024 Ø", rate: 3.5283, phase: "annual" },
  { label: "Jan–Jul 25 Ø", rate: 3.4429, phase: "annual" },
  { label: "Aug 25", rate: 3.56, phase: "monthly" },
  { label: "Sep 25", rate: 3.60, phase: "monthly" },
  { label: "Okt 25", rate: 3.58, phase: "monthly" },
  { label: "Nov 25", rate: 3.56, phase: "monthly" },
  { label: "Dez 25", rate: 3.58, phase: "monthly" },
  { label: "Jan 26", rate: 3.58, phase: "monthly" },
  { label: "Feb 26", rate: 3.61, phase: "monthly" },
  { label: "Mrz 26", rate: 3.60, phase: "monthly" },
  { label: "Apr 26", rate: 3.66, phase: "monthly" },
  { label: "Mai 26", rate: 3.74, phase: "monthly" },
  { label: "Jun 26", rate: 3.76, phase: "monthly" },
  { label: "Jul 26", rate: 3.78, phase: "monthly" },
  { label: "Aug 26", rate: 4.285, phase: "scenario" },
  { label: "Sep 26", rate: 4.79, phase: "scenario" },
  { label: "Okt 26", rate: 5.295, phase: "scenario" },
  { label: "Nov 26", rate: 5.80, phase: "scenario" },
];
```

August bis November 2026 sind eine lineare Verhandlungsannahme bis 5,8 % und ausdrücklich keine Marktprognose. Die Vergleichsbasis „2021 Ø" (1,0917 %) wird für die Mehrbelastungs-Anzeige verwendet.

## 3. Kostenbänder Gemeinschaftseigentum (⚠️ konservative Orientierung, brutto)

Herkunft: Anhang des porthor-Skills „Due Diligence: Eigentumswohnung" v3.0. Werte je m² Gesamtwohnfläche der WEG, Aufzug je Anlage. Immer als Unter- und Obergrenze zeigen, nie als Punktwert automatisch übernehmen.

```js
const levyCostBands = {
  roof: { min: 80, max: 150, basis: "sqm" },
  facade: { min: 120, max: 250, basis: "sqm" },
  windows: { min: 60, max: 120, basis: "sqm" },
  pipes: { min: 90, max: 180, basis: "sqm" },
  heating: { min: 40, max: 120, basis: "sqm" },
  elevator: { min: 60000, max: 120000, basis: "unit" },
  garage: null,
  balconies: null,
  "other-common": null,
};
```

Bandbetrag = `min × Gesamtwohnfläche` bis `max × Gesamtwohnfläche` (bei `basis: "unit"` ohne Multiplikation). Zeilen mit `null` erhalten keinen Bandhinweis.

## 4. Rücklagenzuführung nach § 28 Abs. 2 II. BV (⚠️ Richtwerte je m² Wohnfläche und Jahr)

Gebäudealter = aktuelles Jahr − Baujahr. Es gilt die erste Zeile mit `Gebäudealter ≤ maxAge`: bis 21 Jahre 7,10, 22 bis 31 Jahre 9,00, ab 32 Jahren 11,50 €/m²/Jahr.

```js
const reserveContributionBands = [
  { maxAge: 21, perSqmYear: 7.10 },
  { maxAge: 31, perSqmYear: 9.00 },
  { maxAge: Infinity, perSqmYear: 11.50 },
];
```

## 5. Hausgeld-Band (⚠️ Orientierung, je m² Wohnfläche und Monat, gesamtes Hausgeld inkl. Rücklagenzuführung)

```js
const houseFeeBand = { min: 2.5, max: 4.5 };
```

## 6. Mietrechtliche Deckel (Nutzerannahmen, keine Rechtsprüfung)

```js
const rentCapRates = {
  increaseCapNormal: 20,   // § 558 Abs. 3 BGB: Kappungsgrenze 20 % in 3 Jahren
  increaseCapTight: 15,    // § 558 Abs. 3 S. 2 BGB: 15 % in Gebieten nach Landesverordnung
  newLetSurcharge: 10,     // § 556d Abs. 1 BGB: Vergleichsmiete + 10 % bei Neuvermietung im angespannten Markt
};
```

## 7. AfA-Sätze nach § 7 Abs. 4 EStG (⚠️ Orientierung, keine Steuerberatung)

```js
const afaRates = [
  { maxYearBuilt: 1924, rate: 2.5 },
  { maxYearBuilt: 2022, rate: 2.0 },
  { maxYearBuilt: Infinity, rate: 3.0 },
];
```

Die App zeigt den Satz als Information neben dem Baujahr. Sie berechnet keine Steuerlast und keine Kaufpreisaufteilung.

## 8. Sanierungspakete und Kategorien (Version 2.0)

```js
const renovationCategories = [
  { id: "bathroom", label: "Bad", unit: "Bad", quantity: 1, price: 20000, priceStep: 500, sync: "one" },
  { id: "surfaces", label: "Oberflächen (Boden, Putz, Maler)", unit: "m²", quantity: 75, price: 100, priceStep: 5, sync: "area" },
  { id: "electric", label: "Elektro", unit: "m²", quantity: 75, price: 100, priceStep: 5, sync: "area" },
  { id: "kitchen", label: "Küche", unit: "Küche", quantity: 1, price: 12000, priceStep: 500, sync: "one" },
  { id: "other", label: "Sonstiges", unit: "Pauschale", quantity: 1, price: 8000, priceStep: 500, sync: "one" },
];
const renovationPackages = [
  { id: "low", label: "gering", categories: ["surfaces"] },
  { id: "medium", label: "mittel", categories: ["bathroom", "surfaces", "electric"] },
  { id: "high", label: "umfassend", categories: ["bathroom", "surfaces", "electric", "kitchen", "other"] },
];
```

Standard: Paket `medium` = 35.000 € bei 75 m².

## 9. Katalog der Sonderumlagen-Maßnahmen (Version 2.0)

```js
const levyCatalog = [
  { type: "roof", label: "Dach" },
  { type: "facade", label: "Fassade / Wärmedämmung" },
  { type: "windows", label: "Fenster (Gemeinschaftseigentum)" },
  { type: "pipes", label: "Steig- und Fallstränge" },
  { type: "heating", label: "Zentralheizung / Wärmeerzeuger" },
  { type: "elevator", label: "Aufzug" },
  { type: "garage", label: "Tiefgarage / Stellplatzanlage" },
  { type: "balconies", label: "Balkone / Loggien" },
  { type: "other", label: "Sonstige Gemeinschaftsmaßnahme" },
];
const evidenceLevels = [
  { id: "documented", label: "belegt", marker: "📄" },
  { id: "likely", label: "wahrscheinlich", marker: "" },
  { id: "precautionary", label: "vorsorglich", marker: "⚠️" },
];
```

Standard-Liste: eine Maßnahme `roof`, `likely`, 120.000 € Gesamtkosten, 40.000 € Rücklagendeckung, Schlüssel MEA, 1–3 Jahre, Käufer, Sonderumlage.

## 10. Warnschwellen (fest im Code)

```js
const thresholds = {
  meaPlausibilityMin: 0.9,
  meaPlausibilityMax: 1.1,
  communityArrearsShareOfReserve: 10,   // % der Rücklage
  houseFeeBackPaymentShare: 10,         // % des Jahreshausgelds
  leaseholdRemainingYearsMin: 40,
  boilerAgeYearsMax: 30,                // GEG § 72: Prüfpunkt, keine automatische Rechtsfolge
  brokerBuyerShareMax: 50,              // § 656c BGB: Hinweis, wenn Käuferanteil darüber liegt
  notaryHintPriceBelow: 100000,         // GNotKG: degressive Gebühren, 2 % zu niedrig
  rentBuffer: 15,
};
```
`````

### references/PRD.md

`````
# Product Requirements Document: ETW-Kaufpreis-Rechner

## 1. Dokumentstatus

| Feld | Wert |
|---|---|
| Produkt | Geführter Kaufpreis-Rechner für den Ankauf einer Eigentumswohnung (ETW) als Kapitalanlage |
| Version | 2.0 (08.09.2026) — v1.0 war ein Vollrechner; 2.0 trennt Schnellrechnung und Detailprüfung |
| Sprache | Deutsch |
| Datenstand der Referenzdaten | 7. September 2026 |
| Primärer Nutzer | Käufer / Anleger |
| Sekundärer Nutzer | Verkäufer im persönlichen Ankaufsgespräch |
| Zielplattform | moderner Desktop-Browser und Apple iPad/Safari |
| Auslieferung | statische Single-Page-Web-App ohne Backend (`dist/index.html`, `dist/styles.css`, `dist/app.js`) |

Dieses PRD ist vollständig. Maßgeblich sind ausschließlich die hier beschriebenen Funktionen; Zusatzmodule nur auf ausdrückliche Nutzeranforderung.

## 2. Leitidee: zwei Stufen, ein Rechenkern

**Schnellrechnung:** vier Schritte (Objekt · Miete · Kosten · Ergebnis), etwa zehn Eingaben, Ergebnis in unter zwei Minuten. Pro Schritt eine zentrale Aufgabe, ein „Weiter"-Button, Fortschritt „Schritt 2 von 4", ausgefüllte Schritte als kompakte Zusammenfassung.

**Detailprüfung:** derselbe Rechner mit eingeblendeten Bereichen „Erweiterte Annahmen" je Schritt (Umschalter „Detailprüfung"): Sonderumlagen im Detail, Sanierungskategorien mit Zustandsschätzung, Mietrecht, Finanzierungsdetails, Zinsdiagramm, WEG-Checkliste als optionale Due-Diligence-Checkliste.

**Verkäuferansicht:** keine zweite Rechneroberfläche, sondern eine ruhige Argumentation in fünf Stationen (§17) mit Preisbrücke, Korridor und Einigungsoptionen. Sie „überredet" nicht; sie legt offen, woraus der niedrige Preis folgt.

Der Rechenkern (`calculation-spec.md`) ist in beiden Stufen identisch. Alle ETW-Spezifika aus v1.0 bleiben erhalten, die meisten in der Detailprüfung.

## 3. Abgrenzung zum MFH-Rechner

| Thema | Mehrfamilienhaus | Eigentumswohnung (dieses Produkt) |
|---|---|---|
| Kaufgegenstand | ganzes Gebäude | Sondereigentum + Miteigentumsanteil (MEA) |
| Gemeinschaftseigentum | Käufer saniert selbst | Käufer trägt anteilig Sonderumlagen (Maßnahmenliste mit Evidenzstufe) |
| Wohnungssanierung | n Wohnungen, 7 Gewerke | eine Wohnung, drei Pakete, fünf Kategorien |
| Mieten | Mietliste je Wohnung | eine Einheit: Ist-Miete, erwartete Miete, Deckel in der Detailprüfung |
| Laufende Kosten | €/m²-Pauschalen | Hausgeld-Zerlegung + Verwaltung + Instandhaltung + Erbbauzins |
| Rücklage / WEG-Zustand | — | anteilige Rücklage als Entlastung; DD-Checkliste optional |
| Oberfläche | Vollrechner, Hausschema | geführt in vier Schritten, kein Schema |
| JSON | `kaufpreis-rechner` v5 | `etw-kaufpreisrechner` v2 |

## 4. Kernliste (Funktionsumfang)

1. Vier Schritte mit Wizard-Navigation, Fortschritt, Zusammenfassungen, „Mehr"-Menü (JSON Export/Import, Drucken, Vollbild, Startwerte).
2. Umschalter Schnellrechnung / Detailprüfung; Detailbereiche „Erweiterte Annahmen" je Schritt.
3. Objekt: Bezeichnung, Bundesland, Wohnfläche, Zimmer, Baujahr, MEA als ein Feld („83 / 1000"), Gesamtwohnfläche, Einheiten; Detail: Etage, Aufzug, Flächenquelle, Stellplatz, Sondernutzungsrechte, Erbbaurecht, Heizung, Energie, Verwaltung, Verkäufertyp, Adresse, Kontakt, WEG-Checkliste.
4. Miete: Ist-Miete, erwartete Miete, Mietbasis; Detail: Mietspiegel, vermietet/leer, angespannter Markt, Vertragstyp, Stellplatz, Sonstiges; Deckel-Kaskade mit Klartext.
5. Kosten: Hausgeld in einer Zeile (umlagefähig / nicht umlagefähig / Rücklagenzuführung), Sanierungspaket per Schieberegler (gering / mittel / umfassend), Sonderumlagen als Liste „Maßnahme hinzufügen" (Katalogtyp, Evidenzstufe, Gesamtkosten, „Wie viel übernimmt die Rücklage?"), Finanzierung (Zins, Tilgung, Eigenkapital, Käuferprovision); Detail: fünf Kategorien mit optionaler Zustandsschätzung, Schlüssel/Zahler/Finanzierung/Beleg/„nicht anerkannt" je Maßnahme, Rücklage/Rückstände, Puffer, Notar, Provisionsrechner, weitere Nebenkosten, Inventar, Verwaltung, Instandhaltung, Nachzahlung, vorsorglicher Ansatz, WEG-Darlehen.
6. Ergebnis: drei Primärzahlen (Kaufpreisobergrenze, Gesamtinvestition, Monatsrate), Rechenweg in drei Zeilen, Angebotspreis mit Ampel zum Korridor, drei Szenario-Schaltflächen, vier Kennzahlen, Liste „Hinweise", drei Aktionen; Detail: Korridor, alle Kennzahlen, Szenario-Regler, Zinsdiagramm, Vergleichswert.
7. Verkäuferansicht in fünf Stationen mit Preisbrücke, Evidenzstufen, Entlastungen, Rechnung ohne nicht anerkannte Positionen, Korridor, Tragfähigkeit, Einigungsoptionen mit Übernahme-Delta, Kaufpreisindikation; Ein-Seiten-Druck mit höchstens sechs Zahlen.
8. JSON-Export/-Import (Schema v2), Vollbild/Präsentationsmodus, Drucken, Clipboard, Startwerte.
9. Automatische Hinweise (Plausibilität am Feld, Warnungen, Befunde) nach `calculation-spec.md` §16.

## 5. Nutzerrollen

**Käufer:** erfasst in zwei Minuten die Kernwerte, sieht sofort die Obergrenze, vertieft bei Bedarf. **Verkäufer:** liest die fünf Stationen ohne Bedienung und kann den Preis selbst aus den offengelegten Belastungen herleiten.

## 6. Informationsarchitektur

### 6.1 Käuferansicht (Wizard)

| Schritt | Abschnitts-ID | zentrale Aufgabe | Pflichtfelder (Schnell) |
|---|---|---|---|
| 1 Objekt | `stepObject` | „Welche Wohnung?" | objectName, area, rooms, yearBuilt, meaInput, wegTotalArea, wegUnits; `federalState` sichtbar, aber nicht blockierend (Warnung) |
| 2 Miete | `stepRent` | „Was trägt die Miete?" | currentRentPerSqm, rentPerSqm, rentBasis |
| 3 Kosten | `stepCosts` | „Was kommt neben dem Kaufpreis dazu?" | Hausgeld (3 Felder), renovationPackage, Sonderumlagen-Liste, interest, repayment, equity, brokerBuyerRate |
| 4 Ergebnis | `stepResult` | „Welcher Kaufpreis ist tragfähig?" | askingPrice (optional) |

Pflichtfelder sind sichtbar als solche markiert (Sternchen und Text „Pflicht"); alles andere heißt „optional". Der Header enthält nur Marke, Fortschritt, Umschalter Detailprüfung und das Menü „Mehr". Die Verkäuferansicht wird erst in Schritt 4 angeboten.

Navigation: Schritt-Leiste (`stepNav`, vier Einträge, erledigte Schritte anklickbar), „Weiter"/„Zurück"-Buttons, Fortschritt „Schritt n von 4". Beim Wechsel wird der vorige Schritt als Zusammenfassungszeile über dem aktuellen gezeigt (z. B. „75 m² · 3 Zi · 1975 · MEA 83/1000 · NRW"). Alle Schritte bleiben im DOM; nur der aktive ist sichtbar (ohne JavaScript alle untereinander).

### 6.2 Verkäuferansicht (Dialog, fünf Stationen)

1. Gemeinsam festgestellter Ausgangspunkt · 2. Belegte zusätzliche Belastungen (Preisbrücke) · 3. Unsicherheiten und Entlastungen · 4. Tragfähiger Kaufpreiskorridor · 5. Konkrete Einigungsoptionen mit Kaufpreisindikation. Eigene Aktionen: Drucken (Ein-Seiten-Zusammenfassung), Vollbild, Schließen.

## 7. Eingaben und Verhalten

- Zahlenfelder mit integrierter Einheit; **keine** Regler außer `renovationPackage` und den Szenario-Reglern der Detailprüfung.
- Nullwerte bedeuten „nicht berücksichtigt" und werden so beschriftet (Mietspiegel 0, Angebotspreis 0, Vergleichswert 0, Inventar 0).
- MEA als ein Textfeld `meaInput` im Format „83 / 1000" (Trenner `/`, Leerzeichen erlaubt); Parsing in Zähler/Nenner; ungültige Eingabe → Hinweis am Feld, Rechnung mit letztem gültigen Wert.
- Unaufdringliche Vorschläge: Wohnfläche ändert Flächenkategorien der Sanierung; Zimmerzahl schlägt Standardwerte nur vor, überschreibt keine manuelle Eingabe.
- Plausibilitätsmeldungen direkt am Feld (`…Hint`-Elemente): MEA-Plausibilität, Hausgeld je m², Rücklagenzuführung, Käuferprovision > 50 %, Notar bei kleinem Kaufpreis, Flächenquelle, Erbbaurecht-Restlaufzeit, Kesselalter.
- Keine Freitext-Notizen im Hauptablauf; je Sonderumlage und je Sanierungskategorie ein kurzes Feld „Beleg" (max. 200 Zeichen) in der Detailprüfung.

## 8. Schritt 1 — Objekt

Schnell: siehe 6.1; abgeleitet `meaQuota` und `meaHint` (Plausibilität außerhalb 0,9–1,1). Detail (`advancedObject`): `floorLevel`, `floors`, `hasElevator`, `areaSource` (+ `areaSourceHint`), `parkingOwnership`, `specialRights`, `leasehold` (+ `groundRentMonthly`, `leaseholdEndYear`, nur sichtbar bei ja), `heatingType`, `heatingYear`, Energie (`energyCertificateStatus`, `energyCertificateType`, `energyClass`, `energyCertificateValidUntil`), `managerName`, `sellerType`, Adresse (`street`, `postalCode`, `city`), Kontakt (`sellerSalutation`, `sellerFirstName`, `sellerLastName`, `sellerCompany`, `sellerPhone`, `sellerEmail`), `afaHint`, **WEG-Checkliste** (`wegChecklist`, 12 Fragen aus v1.0 §8.5, Ja/Nein/Offen; ohne Preiswirkung; speist Hinweise und Verkäufer-Station 3; Überschrift „Optionale Due-Diligence-Checkliste").

Grunderwerbsteuer nach `data-tables.md` §1; ohne Bundesland 0 % mit Warnung.

## 9. Schritt 2 — Miete

Schnell: `currentRentPerSqm` (Ist-Miete, 10), `rentPerSqm` (erwartete Miete, 12), `rentBasis` (erwartete Miete / Ist-Miete). Ausgaben: `residentialRent`, `monthlyRent`, `rentHint` (bindende Grenze im Klartext). Detail (`advancedRent`): `rentCapPerSqm` (Mietspiegel, 0 = nicht berücksichtigt), `tenancyStatus`, `tightMarket`, `rentContractType`, `parkingRent`, `otherRentLabel`, `otherRent`. Deckel-Kaskade nach `calculation-spec.md` §2; in der Schnellrechnung gelten die Standardwerte der Detailfelder (Mietspiegel 12, vermietet, nicht angespannt).

## 10. Schritt 3 — Kosten

**Hausgeld** in einer Zeile: `houseFeeRecoverable` (170), `houseFeeNonRecoverable` (50), `houseFeeReserve` (60), Summe `houseFeeTotalOutput`, `houseFeeHint` (Band, II.-BV-Richtwert, Hausgeldquote).

**Sanierung** als Schieberegler `renovationPackage` (0 gering / 1 mittel / 2 umfassend, Standard 1) mit `renovationPackageLabel` und Betrag `renovationTotal`; Pakete nach `calculation-spec.md` §7.2. Detail (`advancedCosts`): fünf Kategorien (`renovationCategoryList`: Bad, Oberflächen, Elektro, Küche, Sonstiges) je mit Aktivierung, Menge, Basispreis, aufklappbarer Zustandsschätzung (drei Fragen, GEG-Risiko), „angepasste Schätzung" mit Begründung, Beleg, „vom Verkäufer nicht anerkannt"; `unitReserve` %. Manuelle Änderung → Paket „individuell". Ändert sich die Wohnfläche, folgen Flächenkategorien nur, wenn ihre Menge noch dem bisherigen Flächen- oder Standardwert entspricht; manuell gesetzte Mengen bleiben.

Prüffragen und positive Antwort je Kategorie: bathroom `modernized` (Ja), `moisture` (Nein), `waterproofing` (Ja) · surfaces `floors` (Ja), `plaster` (Ja), `pollutants` (Nein) · electric `rcd` (Ja), `three_wire` (Ja), `circuits` (Ja) · kitchen `included` (Ja), `usable` (Ja), `tenant_owned` (Ja) · other `damage` (Nein), `backlog` (Nein), `documentation` (Ja). Evidenzstufe einer Kategorie in der Verkäuferansicht: Beleg vorhanden → belegt; mindestens eine beantwortete Zustandsfrage → wahrscheinlich; sonst vorsorglich.

**Sonderumlagen** als Liste `specialLevyList` mit `addLevyButton` („Maßnahme hinzufügen"). Jede Maßnahme (Schnell): Typ aus Katalog, Evidenzstufe (belegt / wahrscheinlich / vorsorglich), Gesamtkosten (mit ⚠️-Bandhinweis aus `data-tables.md` §3 und Button „Obergrenze übernehmen"), „Wie viel übernimmt die Rücklage?", Ergebnis „Ihr Anteil", Entfernen. Detail je Maßnahme: Kostenschlüssel (Standard MEA, verborgen bis Detail), Zeithorizont, wer zahlt, Finanzierung (Sonderumlage / über Hausgeld bzw. WEG-Darlehen), Beleg, „vom Verkäufer nicht anerkannt". Summen `levyOneTimeTotal`, `levyMonthlyTotal`. Standard: eine Maßnahme Dach, wahrscheinlich, 120.000 / 40.000 → 6.640 €.

**Finanzierung** (Schnell): `interest` 4,8, `repayment` 3, `equity` 0, `brokerBuyerRate` (Käuferprovision, effektiv, 0). Anzeige `transferTaxRateOutput`, `purchaseCostsRateTotal`. Detail: `notaryLandRegistryRate` 2 (+ `notaryHint`), Provisionsrechner (`brokerCommissionNetRate`, `brokerVatRate`, `brokerBuyerShareRate`, `applyBrokerButton` schreibt den effektiven Satz in `brokerBuyerRate`, `brokerShareHint`), `otherPurchaseCostsRate`, `inventoryValue`, `managementCostMonthly`, `ownerMaintenancePerSqm`, `rentBuffer` 15, `houseFeeBackPayment`, `reserveTotal` 120.000, `communityArrears`, `sellerArrears`, `typicalLevyWeight` 50, `wegLoanAnnuity` 8; Ausgaben `reserveShareOutput`, `coverageRatioOutput`, `coverageWarning`, `operatingCosts`, `investmentTotal`.

## 11. Schritt 4 — Ergebnis

Primär: `maxPurchasePrice` (Obergrenze), `totalProjectCost` (Gesamtinvestition), `monthlyRate` (anfängliche Monatsrate) — als drei große Kacheln. Rechenweg in drei Zeilen: `stepBudget` („Miete trägt … Darlehen + EK = Budget"), `stepDeductions` („− Nebenkosten − Sanierung − Sonderumlagen"), `stepPrice` („= Kaufpreisobergrenze"). `askingPrice` mit `askingVerdict` (Ampel: im tragfähigen Bereich / nur ohne Risikopuffer tragfähig / über dem Korridor) und `askingDelta`. Szenario-Schaltflächen `scenarioRealistic` (Standard), `scenarioOptimistic`, `scenarioConservative` schalten die drei Primärzahlen um; `scenarioNote` nennt die Annahmen. Kennzahlen `kpiGrid`: Bruttorendite, Kaufpreisfaktor, Kaufpreis je m², monatlicher Überschuss. `hintsList`: alle Hinweise (Befunde positiv/negativ, WEG-Antworten, Warnungen) in einer Liste mit Symbol. `calculationWarning` für Blocker. Aktionen: `actionCompare` (springt zum Angebotspreis), `actionShare` (öffnet Verkäuferansicht), `actionSave` (JSON-Export).

Detail (`advancedResult`): `corridorLow`, `corridorHigh`, `absoluteMaxPrice`, `kpiGridFull` (alle Kennzahlen aus `calculation-spec.md` §15), acht Szenario-Regler, `buyerInterestChart` mit `chartLoan`, `marketPricePerSqm` + `marketDelta`, Kaufnebenkosten aufgeschlüsselt.

## 12. Verkäuferansicht (fünf Stationen)

Ton: Sie-Ansprache, sachlich, keine Fachbegriffe ohne Erklärung; nicht „Mängel", sondern „noch zu tragende Maßnahmen".

1. **Ausgangspunkt** (`seller-start`): Zielsatz „Wir möchten die Wohnung kaufen, sofern die Gesamtbelastung tragfähig bleibt."; Objektzeile; **positive Eigenschaften zuerst** (`sellerPositives`: positive Befunde, Rücklage anteilig, verkäuferseitig übernommene Kosten); Ausgangspreis `sellerStartPrice` = Angebotspreis oder, wenn nicht erfasst, der Korridor.
2. **Belegte zusätzliche Belastungen** (`seller-burdens`): Preisbrücke `sellerBridge` nach `calculation-spec.md` §19 (Budget → − Nebenkosten → − Sanierung → − Sonderumlagen = Korridor oben → − Risikopuffer = Korridor unten), Liste `sellerBurdenList` mit Evidenzstufe (belegt/wahrscheinlich/vorsorglich), Betrag oder Bandbreite, Beleg; Zwischenergebnis `sellerBurdenTotal` „Zusätzliche Belastung neben dem Kaufpreis". Nur Kosten, die der Käufer trägt.
3. **Unsicherheiten und Entlastungen** (`seller-uncertainty`): vorsorgliche Positionen als Bandbreite, nicht anerkannte Positionen mit Rechnung ohne sie (`sellerWithoutDisputed`), Entlastungen (`sellerReliefList`), Satz „Positive Unterlagen würden den Kaufpreis erhöhen" mit Beispiel (`sellerDocsWouldRaise`).
4. **Korridor** (`seller-corridor`): `sellerCorridorLow` / `sellerCorridorHigh`, Tragfähigkeit (`sellerCoverageText`: „Bei diesem Kaufpreis deckt die Miete Finanzierung und Eigentümerlasten."), Schlüsselsatz `sellerKeySentence`: „Damit wir insgesamt nicht mehr als X € investieren, kann der Kaufpreis höchstens Y € betragen."; bei Angebotspreis die Ampel-Lage.
5. **Einigungsoptionen** (`seller-options`): drei Varianten — niedrigerer Kaufpreis (`sellerOptionPrice`), Kostenübernahme durch den Verkäufer mit Übernahme-Delta je Maßnahme (`sellerTakeoverList`), Klärung vor Beurkundung (`sellerOptionClarify`); Kaufpreisindikation (`purchaseOfferText`, `offerValidityDays`, `copyPurchaseOffer`) mit Abschlusssatz „Auf dieser transparenten Grundlage können wir kurzfristig zu X € kaufen, vorbehaltlich Unterlagen- und Finanzierungsprüfung."

**Druck:** `sellerPrintButton` druckt die Ein-Seiten-Zusammenfassung `sellerSummarySheet` (A4 hoch): Ausgangspreis, zusätzliche Belastung, Korridor unten, Korridor oben, Monatsrate, Angebotsdelta (falls erfasst) — höchstens sechs Zahlen, dazu die Einigungsoptionen als drei Sätze und der Vorbehaltssatz.

## 13. Kaufpreisindikation

Wie v1.0 (Anrede, Objekt, auf 100 € gerundeter Kaufpreis, Grundlage, Sanierung, Sonderumlagen, Rücklage, Nebenkosten, Vorbehalte inkl. § 12 WEG, Stichtag, § 566/566a, § 577 bei Aufteiler, MaBV bei Bauträger, Erbbaurecht, Abt. II, 14 Tage 1–60, § 311b BGB), ergänzt um den Abschlusssatz aus §12.5.

## 14. JSON, Vollbild, Druck, Barrierefreiheit, Datenschutz

Nach `data-schema.md` (v2). Nach einem erfolgreichen Import sind alle Schritte freigeschaltet und die Anzeige springt auf Schritt 4. Präsentationsmodus wie v1.0 (Header ausgeblendet, `100dvh`, Safe-Area, `exitPresentationButton`, Escape). Barrierefreiheit: Labels, Fokus, Live-Region, Tastatur für Wizard und Diagramm, 200 % Zoom. Datenschutz: kein Backend, kein Tracking, keine Personen-/Objektdaten als Startwerte.

## 15. Nichtfunktionale Anforderungen

Sofortige Neuberechnung; keine externen JS-Abhängigkeiten; offline nutzbar; lokale Assets; JS ohne Syntaxfehler; JSON-Roundtrip verlustfrei; keine horizontale Seitenscrollbar bei 390 px.

## 16. Rechtliche Leitplanken (wörtlich in die UI)

Disclaimer unter der Käuferansicht, wörtlich: „Orientierung, keine Finanzierungs-, Rechts-, Steuer- oder Anlageberatung. Die Rechnung verwendet eine anfängliche Annuität, pauschale Sanierungs- und Sonderumlagenansätze und die eingestellte Grunderwerbsteuer; Ertragsteuern, Förderungen, Wertänderungen und individuelle Bankvorgaben sind nicht berücksichtigt. Aussagen zu Teilungserklärung, Gemeinschaftsordnung, Beschlüssen, Mietverhältnissen und Grundbuchlasten sind Hinweise auf Prüfbedarf, keine Rechtsauskunft." Mietspiegel, Kappung, Bremse = Nutzerannahmen. GEG = Risikohinweis. Bänder/Schätzungen = Orientierung, Herkunft „Basis → angepasst" sichtbar. Vorsorgliche Sonderumlagen nur als Bandbreite. Indikation unverbindlich, § 311b. Ampel bewertet die Lage des Angebots zum Korridor, nicht das Objekt. Keine Formulierungen wie „garantiert", „rechtssicher", „vollkommen sicher".

## 17. Nicht im Umfang

Eigennutzer-Modus, Backend, Mehrbenutzer, Bankangebote, Abfragen von Mietspiegel/Grundbuch/Beschlusssammlung, amtliche GEG-Prüfung, Fördermittel, steuerliche Kaufpreisaufteilung, bindende Verträge, Gebäude- oder Grundrissschemata.

## 18. Releasekriterien

1. `validate_app.py` und `node --check` ohne Fehler; Abnahmetests erfüllt.
2. Schnellrechnung: Startzustand mit NRW liefert 52.317 € Obergrenze, 98.404 € Gesamtinvestition, 640 € Monatsrate; Paket mittel = 35.000 €; Standardmaßnahme Dach = 6.640 €.
3. Wizard: vier Schritte, Fortschritt, Zusammenfassungen, „Mehr"-Menü; Detailprüfung blendet alle erweiterten Bereiche ein.
4. Sonderumlagen als Liste mit Katalog und Evidenzstufe; Sanierung als Paket mit Schieberegler.
5. Verkäuferansicht mit fünf Stationen, geschlossener Preisbrücke, Korridor, Übernahme-Delta, Ampel; Ein-Seiten-Druck mit höchstens sechs Zahlen.
6. JSON v2 roundtrip; MFH- und v1-Dateien mit Klartext abgelehnt.
7. Indikation enthält 14-Tage-Frist, § 12 WEG, § 311b BGB und den Abschlusssatz.
`````

### references/sources.md

`````
# Quellen, Annahmen und Aktualisierung

## 1. Bauzinsen

- Deutsche Bundesbank, MFI-Zinsstatistik, Wohnungsbaukredite an private Haushalte, anfängliche Zinsbindung über 5 bis 10 Jahre: <https://api.statistiken.bundesbank.de/rest/data/BBIM1/M.DE.B.A2C.A.C.A.2250.EUR.N?format=csv&lang=de>

Stand der Reihe in `data-tables.md`: 02.09.2026. Werte bis Juli 2026 sind Istwerte bzw. Mittelwerte; August bis November 2026 sind eine lineare Verhandlungsannahme bis 5,8 %, keine Marktprognose. Bei Aktualisierung: letzte zwölf verfügbare Monate monatlich, davor Jahres-/Teiljahresmittel, danach vier klar als Annahme markierte Monate.

## 2. Grunderwerbsteuer

- § 11 GrEStG (Steuersatz, Länderöffnung): <https://www.gesetze-im-internet.de/grestg_1983/__11.html>
- § 8, § 9 GrEStG (Bemessungsgrundlage, Gegenleistung): <https://www.gesetze-im-internet.de/grestg_1983/__8.html>, <https://www.gesetze-im-internet.de/grestg_1983/__9.html>
- Länderübersicht mit Quellenhinweisen (aktualisiert 14.07.2026): <https://meinrechner.eu/ratgeber/grunderwerbsteuer-bundeslaender-2026/>
- Deutsches Notarinstitut, Arbeitshilfen Steuerrecht: <https://www.dnoti.de/arbeitshilfen/steuerrecht/>
- BFH, Urteil vom 16.09.2020, II R 49/17: Die anteilige Erhaltungsrücklage mindert die Bemessungsgrundlage nicht.

Modellgrenze: Die App rechnet Grunderwerbsteuer als `(Kaufpreis − Inventar) × Ländersatz`. Inventarabzug setzt einen nachvollziehbaren, angemessenen Wert im Kaufvertrag voraus; das prüft die App nicht. Vor produktiver Nutzung alle 16 Sätze und deren Wirksamkeitsdatum prüfen.

## 3. Grundstückskauf, Notar, Makler

- § 311b BGB (notarielle Beurkundung): <https://www.gesetze-im-internet.de/bgb/__311b.html>
- § 656c, § 656d BGB (Provisionsteilung bei Wohnungen und Einfamilienhäusern, Verbraucher-Käufer): <https://www.gesetze-im-internet.de/bgb/__656c.html>
- GNotKG (degressive Gebührentabelle; 2 % Notar/Grundbuch ist bei kleinen Kaufpreisen zu niedrig): <https://www.gesetze-im-internet.de/gnotkg/>

## 4. Wohnungseigentumsrecht (WEG)

- § 1 WEG (Sondereigentum, Miteigentumsanteil), § 3 (Stellplätze als Sondereigentum), § 5 (Gegenstand des Sondereigentums): <https://www.gesetze-im-internet.de/woeigg/>
- § 9a WEG (Gemeinschaft, Verbandskredit), § 12 (Veräußerungsbeschränkung), § 15 (Gebrauch), § 16 (Kostentragung, Kostenschlüssel), § 19 (Verwaltung, Erhaltungsrücklage), § 20 (bauliche Veränderungen), § 24 Abs. 1 und 7 (Versammlung, Beschlusssammlung), § 25 (Stimmrecht), § 28 (Wirtschaftsplan, Jahresabrechnung, Vermögensbericht Abs. 4)
- Wer eine Sonderumlage zahlt, richtet sich regelmäßig nach der Fälligkeit, nicht nach dem Beschlussdatum — im Kaufvertrag durch Stichtagsregelung klären; die App gibt nur den Prüfpunkt aus.

## 5. Erhaltungsrücklage und Hausgeld

- § 28 Abs. 2 II. Berechnungsverordnung (Instandhaltungskosten-Pauschalen nach Gebäudealter): <https://www.gesetze-im-internet.de/bvo_2/__28.html>
- Betriebskostenverordnung (was auf Mieter umlegbar ist; Verwaltung und Instandhaltung nicht): <https://www.gesetze-im-internet.de/betrkv/>

Hausgeld-Band 2,50–4,50 €/m²/Monat und Kostenbänder für Gemeinschaftsmaßnahmen stammen aus dem konservativen Anhang des porthor-Skills „Due Diligence: Eigentumswohnung" v3.0 und sind ⚠️-Orientierungswerte, keine Statistik.

## 6. Mietrecht

- § 556d BGB (Mietpreisbremse), § 558 BGB (Vergleichsmiete, Kappungsgrenze), § 557a/§ 557b (Staffel-/Indexmiete), § 566/§ 566a (Eintritt in den Mietvertrag, Kaution), § 577 (Vorkaufsrecht des Mieters), § 577a (Kündigungsbeschränkung nach Umwandlung): <https://www.gesetze-im-internet.de/bgb/>
- § 250 BauGB (Umwandlungsgenehmigung): <https://www.gesetze-im-internet.de/bbaug/__250.html>

Die App wendet Kappungsgrenze und Mietpreisbremse nur als rechnerische Deckel auf Nutzerannahmen an. Ob ein Gebiet „angespannt" ist, welche Landesverordnung gilt und ob Ausnahmen (Neubau, Modernisierung) greifen, wird nicht geprüft.

## 7. Bauträger

- Makler- und Bauträgerverordnung (MaBV), insbesondere § 3 (Ratenzahlung nach Baufortschritt): <https://www.gesetze-im-internet.de/gewo_34cdv/>

## 8. GEG und Energieausweis

- § 71 GEG (65 % erneuerbare Energien, Übergangsfristen), § 72 (Betriebsverbot alter Heizkessel): <https://www.gesetze-im-internet.de/geg/>
- § 80 ff. GEG (Energieausweis, Pflichtangaben)

GEG-Markierungen der App sind manuelle Risikomarker; der 10 %-Zuschlag ist eine kaufmännische Sicherheitsannahme.

## 9. Wohnfläche, Steuern

- Wohnflächenverordnung § 4 (Balkone 25 %, höchstens 50 %): <https://www.gesetze-im-internet.de/woflv/__4.html>
- § 7 Abs. 4 EStG (AfA-Sätze): <https://www.gesetze-im-internet.de/estg/__7.html> — nur Orientierung, die App macht keine Kaufpreisaufteilung.

## 10. Aktualisierungsregel

Werden Zins- oder Steuersätze, Bänder oder Schwellen geändert, sind gleichzeitig anzupassen:

- `dist/app.js` (Konstanten),
- sichtbarer Standhinweis in `dist/index.html`,
- `references/data-tables.md`,
- Rechenbeispiel in `references/calculation-spec.md` §17,
- betroffene Abnahmetests in `references/acceptance-tests.md`.
`````

### references/ui-spec.md

`````
# UI-Vertrag 2.0 (Struktur, Pflicht-IDs, Verhalten)

Dieses Dokument ersetzt eine Referenzimplementierung. `scripts/validate_app.py` prüft die hier genannten IDs, Reihenfolgen und Marker. Alles, was hier „Pflicht" heißt, ist Teil der Abnahme.

## 1. Dateien und Technik

- `dist/index.html` — eine Seite, `lang="de"`, Viewport mit `viewport-fit=cover`, verweist relativ auf `./styles.css` und `./app.js` (`defer`). Keine externen Skripte, Fonts oder Stylesheets.
- `dist/styles.css` — reines CSS, ausgeglichene Klammern.
- `dist/styles.css` beginnt mit `[hidden] { display: none !important; }`, damit Layout-Regeln (`display: flex/grid`) das `hidden`-Attribut nie überstimmen.
- `dist/app.js` — ES2020 ohne Module und Abhängigkeiten; kein `eval`/`new Function`; Nutzertexte nur per `textContent` oder Formularwert; SVG nur aus eigenen Zahlen.

## 2. Seitenskelett

```
<header class="topbar" id="appHeader">   Marke · <span id="stepProgress"> · <label id="modeToggleLabel"><input type="checkbox" id="modeToggle"> Detailprüfung</label> · <button id="moreMenuButton"> → <div id="moreMenu" hidden> mit exportJsonButton, importJsonButton, printButton, fullscreenButton, resetButton
<button id="exitPresentationButton" hidden>
<input id="jsonFileInput" type="file" hidden>
<p id="dataStatus" role="status" aria-live="polite">
<main id="top">
  <nav id="stepNav">                     vier <button data-step="1..4"> mit aria-current auf dem aktiven Schritt
  <form id="calculatorForm" novalidate>
    <section id="stepObject" data-step="1">
    <section id="stepRent"   data-step="2">
    <section id="stepCosts"  data-step="3">
    <section id="stepResult" data-step="4">
  </form>
  <p class="disclaimer">                 Wortlaut PRD §16
</main>
<dialog id="sellerDialog" class="seller-dialog">   siehe §7
```

Die vier Abschnitte stehen im Quelltext in dieser Reihenfolge. Jeder Schritt enthält: Überschrift mit einer Aufgabe, Zusammenfassungszeile der vorigen Schritte (`summaryObject` in Schritt 2–4, `summaryRent` in 3–4, `summaryCosts` in 4; leer/hidden in Schritt 1), Pflichtfelder, dann `<details class="advanced" id="advanced<Step>">` (Object/Rent/Costs/Result), dann Navigation (`backFrom<Step>`, `nextFrom<Step>`; Schritt 1 ohne Zurück, Schritt 4 ohne Weiter).

## 3. Wizard und Modus

- Nur der aktive Schritt ist sichtbar (`hidden` auf den anderen); Start = Schritt 1. `stepProgress` zeigt „Schritt n von 4". `stepNav`-Buttons für erledigte oder aktive Schritte sind aktiv, spätere deaktiviert, bis der vorige Schritt einmal mit „Weiter" verlassen wurde.
- „Weiter" prüft die Pflichtfelder des Schritts (nicht leer, Zahl ≥ 0) und blockiert mit Hinweis am Feld. Das Bundesland ist keine Pflicht: ohne Auswahl Warnung am Feld, in `calculationWarning` und in der Hinweisliste.
- Escape: schließt zuerst das „Mehr"-Menü, beendet sonst den Präsentationsmodus; innerhalb des Verkäuferdialogs wird das `cancel`-Ereignis so behandelt, dass der Präsentationsmodus endet, ohne den Dialog zu schließen.
- `modeToggle` (Checkbox): aus = Schnellrechnung, alle `details.advanced` sind `hidden`; an = Detailprüfung, sie sind sichtbar und zugeklappt. Der Modus wird im JSON (`inputs.mode`) gespeichert. Ohne JavaScript sind alle Schritte und Details sichtbar.
- `moreMenuButton` öffnet/schließt `moreMenu` (aria-expanded); Escape schließt.

## 4. Bedienmuster

### 4.1 Zahlenfelder

Alle Zahlenfelder aus `data-schema.md` sind `<input type="number" id="<Schlüssel>">` mit Einheit im Feldrahmen (`.input-shell`), sichtbarem Label, Kennzeichnung „Pflicht"/„optional". Keine Regler außer `renovationPackage` (`type="range"`, 0–2, step 1) und den acht Szenario-Reglern in `advancedResult` (dort Zahl + `<id>Range` synchron).

### 4.2 MEA-Feld

`<input type="text" id="meaInput">` mit Platzhalter „83 / 1000"; Parsing `^\s*(\d+)\s*/\s*(\d+)\s*$`; Ausgaben `meaQuota` („8,3 %") und `meaHint` (Plausibilität, Klasse `is-warning`). Intern `meaShare`/`meaTotal`.

### 4.3 Sanierungspaket

`renovationPackage` (range) mit `renovationPackageLabel` („mittel") und `renovationTotal` (Betrag). In `advancedCosts` der Container `renovationCategoryList` mit fünf `<article class="category-card" data-category-id>`: Aktivierungs-Checkbox `<id>-active`, Menge `<id>-quantity`, Basispreis `<id>-price`, `<details>` „Zustand genauer schätzen" mit drei `<select id="<id>-check-<frage>">`, Checkbox `<id>-geg-risk`, Ausgabe `<id>-adjusted` („Basis → angepasst · Begründung"), Feld `<id>-source` (Beleg, maxlength 200), Checkbox `<id>-disputed`. Manuelle Änderung setzt Paket „individuell" (`renovationPackageLabel` = „individuell", Regler ohne Stufe: `data-custom="true"`).

### 4.4 Sonderumlagen-Liste

`specialLevyList` (Container) + `addLevyButton`. Je Maßnahme `<article class="levy-row" data-levy-id="<id>">` mit: `<select class="levy-type">` (Katalog), `<select class="levy-evidence">`, `<input class="levy-cost">` + `<small class="levy-band">` + `<button class="levy-band-apply">` (nur mit Band), `<input class="levy-coverage">` (Label „Wie viel übernimmt die Rücklage?"), `<strong class="levy-result">` („Ihr Anteil: 6.640 €" oder „44,27 €/Monat"), `<button class="levy-remove">`; im Detailmodus zusätzlich sichtbar: `.levy-key`, `.levy-custom-share`, `.levy-horizon`, `.levy-payer`, `.levy-financing`, `.levy-source`, `.levy-disputed` (Checkbox „vom Verkäufer nicht anerkannt"). IDs der Maßnahmen werden erzeugt (`m1`, `m2`, …). Leere Liste zeigt den Hinweis „Keine Maßnahmen erfasst — Maßnahme hinzufügen". Typ `elevator` bei `hasElevator ≠ yes` → Hinweis in der Zeile, zählt nicht.

### 4.5 WEG-Checkliste

`wegChecklist` in `advancedObject`: je Frage `<select id="weg-<id>">` (Offen/Ja/Nein) mit sichtbarer Frage; Überschrift „Optionale Due-Diligence-Checkliste".

### 4.6 Hinweise

`hintsList` (Schritt 4) und `hintsCount`: jede Zeile mit Symbol (＋ positiv, − risikobehaftet, ⚠ Warnung), Bereich, Text. Keine getrennten Listen im Rechner.

### 4.7 Statusmeldung

`showDataStatus(text, type)` schreibt in `dataStatus`; Fehler mit Klasse `is-error`; Auto-Ausblenden.

## 5. Pflicht-IDs je Schritt

**Kopf/Shell:** `appHeader`, `stepProgress`, `modeToggle`, `moreMenuButton`, `moreMenu`, `exportJsonButton`, `importJsonButton`, `printButton`, `fullscreenButton`, `resetButton`, `exitPresentationButton`, `jsonFileInput`, `dataStatus`, `stepNav`, `calculatorForm`.

**Schritt 1 (`stepObject`):** `objectName`, `federalState`, `area`, `rooms`, `yearBuilt`, `meaInput`, `meaQuota`, `meaHint`, `wegTotalArea`, `wegUnits`, `advancedObject`, `floorLevel`, `floors`, `hasElevator`, `areaSource`, `areaSourceHint`, `parkingOwnership`, `specialRights`, `leasehold`, `leaseholdFields`, `groundRentMonthly`, `leaseholdEndYear`, `heatingType`, `heatingYear`, `energyCertificateStatus`, `energyCertificateType`, `energyClass`, `energyCertificateValidUntil`, `managerName`, `sellerType`, `street`, `postalCode`, `city`, `sellerSalutation`, `sellerFirstName`, `sellerLastName`, `sellerCompany`, `sellerPhone`, `sellerEmail`, `afaHint`, `wegChecklist`, `nextFromObject`.

**Schritt 2 (`stepRent`):** `summaryObject`, `currentRentPerSqm`, `rentPerSqm`, `rentBasis`, `residentialRent`, `monthlyRent`, `rentHint`, `advancedRent`, `rentCapPerSqm`, `tenancyStatus`, `tightMarket`, `rentContractType`, `parkingRent`, `otherRentLabel`, `otherRent`, `additionalRent`, `backFromRent`, `nextFromRent`.

**Schritt 3 (`stepCosts`):** `summaryRent`, `houseFeeRow`, `houseFeeRecoverable`, `houseFeeNonRecoverable`, `houseFeeReserve`, `houseFeeTotalOutput`, `houseFeeHint`, `renovationPackage`, `renovationPackageLabel`, `renovationTotal`, `specialLevyList`, `addLevyButton`, `levyOneTimeTotal`, `levyMonthlyTotal`, `interest`, `repayment`, `equity`, `brokerBuyerRate`, `brokerHint`, `transferTaxRateOutput`, `purchaseCostsRateTotal`, `purchaseCostsStateHint`, `operatingCosts`, `investmentTotal`, `advancedCosts`, `renovationCategoryList`, `unitReserve`, `notaryLandRegistryRate`, `notaryHint`, `brokerCommissionNetRate`, `brokerVatRate`, `brokerBuyerShareRate`, `applyBrokerButton`, `otherPurchaseCostsRate`, `inventoryValue`, `managementCostMonthly`, `ownerMaintenancePerSqm`, `rentBuffer`, `houseFeeBackPayment`, `reserveTotal`, `communityArrears`, `sellerArrears`, `typicalLevyWeight`, `wegLoanAnnuity`, `reserveShareOutput`, `coverageRatioOutput`, `coverageWarning`, `backFromCosts`, `nextFromCosts`.

**Schritt 4 (`stepResult`):** `summaryCosts`, `maxPurchasePrice`, `totalProjectCost`, `monthlyRate`, `stepBudget`, `stepDeductions`, `stepPrice`, `askingPrice`, `askingVerdict`, `askingDelta`, `scenarioRealistic`, `scenarioOptimistic`, `scenarioConservative`, `scenarioNote`, `kpiGrid`, `hintsList`, `hintsCount`, `calculationWarning`, `actionCompare`, `actionShare`, `actionSave`, `advancedResult`, `corridorLow`, `corridorHigh`, `kpiGridFull`, `optimisticRentIncrease`, `optimisticInterestReduction`, `optimisticRenovationReduction`, `optimisticHouseFeeReduction`, `conservativeRentReduction`, `conservativeInterestIncrease`, `conservativeRenovationIncrease`, `conservativeHouseFeeIncrease`, `buyerInterestChart`, `chartLoan`, `marketPricePerSqm`, `marketDelta`, `purchaseCostsBreakdown`, `backFromResult`.

Range-Paare (Zahl + `<id>Range`): nur die acht Szenario-Regler.

## 6. Kennzahlen

`kpiGrid` (vier Kacheln, Reihenfolge): Bruttorendite · Kaufpreisfaktor · Kaufpreis je m² · monatlicher Überschuss. `kpiGridFull`: alle Kennzahlen aus `calculation-spec.md` §15 in dessen Reihenfolge. Nicht berechenbare Werte: „nicht berechenbar" + fehlende Eingabe.

## 7. Verkäuferdialog (fünf Stationen)

`<dialog id="sellerDialog" class="seller-dialog">` → `.seller-sheet` → `header.seller-toolbar` (`sellerFullscreenButton`, `sellerPrintButton`, `sellerCloseButton`, `sellerDataStatus`) → `.seller-content` mit Stationen in dieser Quelltextreihenfolge und genau diesen Klassenmarkern:

1. `<section class="seller-start seller-step">` — `sellerGoal` (Zielsatz), `sellerStartObject`, `sellerPositives` (`<ul>`), `sellerStartPrice`, `sellerStartPriceLabel` („Angebotspreis" oder „Tragfähiger Korridor").
2. `<section class="seller-burdens seller-step">` — `sellerBridge` mit `bridgeBudget`, `bridgeCosts`, `bridgeRenovation`, `bridgeLevies`, `bridgeCorridorHigh`, `bridgeBuffer`, `bridgeCorridorLow`; `sellerBurdenList` (`<ul>`, je Eintrag Evidenzlabel `belegt`/`wahrscheinlich`/`vorsorglich`, Betrag oder Bandbreite, Beleg); `sellerBurdenTotal`.
3. `<section class="seller-uncertainty seller-step">` — `sellerUncertainList`, `sellerReliefList`, `sellerWithoutDisputed`, `sellerDocsWouldRaise`.
4. `<section class="seller-corridor seller-step">` — `sellerCorridorLow`, `sellerCorridorHigh`, `sellerCoverageText`, `sellerKeySentence`, `sellerAskingVerdict` (hidden ohne Angebotspreis).
5. `<section class="seller-options seller-step">` — `sellerOptionPrice`, `sellerTakeoverList`, `sellerOptionClarify`, `offerValidityDays`, `purchaseOfferText`, `copyPurchaseOffer`, `sellerClosing`.
6. `<section id="sellerSummarySheet" class="seller-summary-sheet">` — Ein-Seiten-Zusammenfassung (nur im Druck sichtbar): `sheetStart`, `sheetBurden`, `sheetCorridorLow`, `sheetCorridorHigh`, `sheetRate`, `sheetAskingDelta` (hidden ohne Angebot), `sheetOptions`, `sheetReservation`.

Der Dialog öffnet per `actionShare` und `showModal()`, ruft vorher `calculate()` auf, schließt per Button und Backdrop-Klick. Evidenzlabel je Sanierungskategorie in Station 2: Beleg → belegt, beantwortete Zustandsfrage → wahrscheinlich, sonst vorsorglich.

## 8. Vollbild / Präsentationsmodus

Wie v1.0: `fullscreenButton`/`sellerFullscreenButton` setzen `html.presentation-mode` (Header ausgeblendet, `min-height: 100dvh`, `env(safe-area-inset-*)`) und versuchen `requestFullscreen()`; `exitPresentationButton` (schwebend, nur im Modus) und Escape beenden ihn.

## 9. Druck

`@media print` mit `@page { size: A4; margin: 12mm }`. `sellerPrintButton` setzt `body.print-seller`, ruft `window.print()` und entfernt die Klasse bei `afterprint`. Im Druck ist nur `sellerSummarySheet` sichtbar (Topbar, Formular, Stationen ausgeblendet); Ergebnis-Kacheln mit `print-color-adjust: exact`. `printButton` im „Mehr"-Menü verhält sich identisch.

## 10. Zinsdiagramm

Wie v1.0: `renderInterestChart(containerId, principal, repayment, selectedRate)` (SVG, drei Phasen, Punkte mit Tastatur, Zinsübernahme). Aufrufe: `renderInterestChart("buyerInterestChart", …)` in `advancedResult`; ein zweiter Aufruf ist nicht erforderlich (Verkäuferansicht ohne Diagramm).

## 11. Design-DNA

Farben Dunkelblau `#0d2b3e`, Petrol `#137a8a`/`#0e5f6c`, Off-White, Gold `#c9a227` sparsam, Rot `#b3372a` für Risiko; Ampel zusätzlich Grün `#2e7d4f`, Gelb `#c9a227`. Systemschrift, Fließtext ≥ 16 px, Labels ≥ 14 px; Touch-Ziele ≥ 44 px; Fokus sichtbar. Wizard-Karte mit max. 720 px Breite in der Schnellrechnung; Detailprüfung darf breiter (bis 1100 px). Verkäuferdialog `width: min(1200px, calc(100vw - 2rem))`, im Präsentationsmodus 100 vw/100 dvh; Stationen als nummerierte, ruhige Blöcke ohne Tabellenwust. Kaufmännische Sprache, Sie-Anrede in erzeugten Texten.

## 12. Verhalten

- `calculate()` liest alle Felder, rechnet nach `calculation-spec.md` (inkl. §18 ohne bestrittene Positionen und §19), schreibt alle Ausgaben, Zusammenfassungen, Hinweise, Ampel, Verkäuferstationen, Zusammenfassungsblatt und Indikationstext in einem Durchlauf.
- Szenario-Schaltflächen schalten `inputs.scenarioSelected` und damit die drei Primärzahlen sowie `scenarioNote`; Korridor, Verkäuferansicht und Indikation verwenden immer „realistisch".
- `resetAll()` stellt alle Defaults wieder her (Felder, Paket, Kategorien, Maßnahmenliste mit der Standardmaßnahme, WEG-Checks, Modus, Schritt 1).
- Export `kaufpreis-etw-<YYYY-MM-DD>.json`; Import ≤ 1 MB, Validierung nach `data-schema.md`, erst nach vollständiger Validierung anwenden; Version 1 und `kaufpreis-rechner` mit Klartext ablehnen.
- Clipboard mit Fallback.

## 13. Pflichtkonstanten und -funktionen in `app.js` (Validator-Marker)

```
const jsonSchema = "etw-kaufpreisrechner";
const jsonVersion = 2;
const globalDefaults = { … }
const textDefaults = { … }
const transferTaxRates = { … }
const interestTimeline = [ … ]
const levyCostBands = { … }
const renovationCategories = [ … ]        // data-tables §8
const renovationPackages = [ … ]          // data-tables §8
const levyCatalog = [ … ]                 // data-tables §9
const evidenceLevels = [ … ]              // data-tables §9
const defaultLevies = [ { id: "m1", type: "roof", evidence: "likely", totalCost: 120000, reserveCoverage: 40000, key: "mea", customShare: 0, horizon: "1-3", payer: "buyer", financing: "levy", source: "", disputed: false } ]
const inspectionCatalog = { … }           // fünf Kategorien
const wegCheckCatalog = [ … ]
function getEffectiveCategoryPrice(category, state, basePrice)
function calculateSpecialLevies(context)                  // → { rows, oneTimeTotal, monthlyTotal, quantifiedTotal, sumCoverage }
function solvePrice({ loan, equity, renovation, taxRate, otherRate, inventory })
function calculateScenario({ … })
function computeMetrics(price, context)
function buildPriceBridge(context)                        // calculation-spec §19
function buildPurchaseOfferText(data)                     // enthält „§ 12 WEG" und „§ 311b BGB"
function renderInterestChart(containerId, principal, repayment, selectedRate)
function showStep(n)
function calculate()
```

Die Zeichenketten `MFH-Kaufpreisrechner` (Import-Ablehnung) und `GEG-Risiko` (Hinweistext) müssen vorkommen.
`````

### scripts/validate_app.py

`````
#!/usr/bin/env python3
"""Validate a generated ETW purchase-price calculator (v2.0) against references/ui-spec.md."""

from __future__ import annotations

import argparse
import re
import shutil
import subprocess
import sys
from collections import Counter
from html.parser import HTMLParser
from pathlib import Path
from urllib.parse import urlsplit


REQUIRED_FILES = (Path("dist/index.html"), Path("dist/styles.css"), Path("dist/app.js"))

SHELL_IDS = {
    "appHeader", "stepProgress", "modeToggle", "moreMenuButton", "moreMenu", "exportJsonButton",
    "importJsonButton", "printButton", "fullscreenButton", "resetButton", "exitPresentationButton",
    "jsonFileInput", "dataStatus", "stepNav", "calculatorForm",
}
OBJECT_IDS = {
    "stepObject", "objectName", "federalState", "area", "rooms", "yearBuilt", "meaInput", "meaQuota", "meaHint",
    "wegTotalArea", "wegUnits", "advancedObject", "floorLevel", "floors", "hasElevator", "areaSource",
    "areaSourceHint", "parkingOwnership", "specialRights", "leasehold", "leaseholdFields", "groundRentMonthly",
    "leaseholdEndYear", "heatingType", "heatingYear", "energyCertificateStatus", "energyCertificateType",
    "energyClass", "energyCertificateValidUntil", "managerName", "sellerType", "street", "postalCode", "city",
    "sellerSalutation", "sellerFirstName", "sellerLastName", "sellerCompany", "sellerPhone", "sellerEmail",
    "afaHint", "wegChecklist", "nextFromObject",
}
RENT_IDS = {
    "stepRent", "summaryObject", "currentRentPerSqm", "rentPerSqm", "rentBasis", "residentialRent", "monthlyRent",
    "rentHint", "advancedRent", "rentCapPerSqm", "tenancyStatus", "tightMarket", "rentContractType", "parkingRent",
    "otherRentLabel", "otherRent", "additionalRent", "backFromRent", "nextFromRent",
}
COST_IDS = {
    "stepCosts", "summaryRent", "houseFeeRow", "houseFeeRecoverable", "houseFeeNonRecoverable", "houseFeeReserve",
    "houseFeeTotalOutput", "houseFeeHint", "renovationPackage", "renovationPackageLabel", "renovationTotal",
    "specialLevyList", "addLevyButton", "levyOneTimeTotal", "levyMonthlyTotal", "interest", "repayment", "equity",
    "brokerBuyerRate", "brokerHint", "transferTaxRateOutput", "purchaseCostsRateTotal", "purchaseCostsStateHint",
    "operatingCosts", "investmentTotal", "advancedCosts", "renovationCategoryList", "unitReserve",
    "notaryLandRegistryRate", "notaryHint", "brokerCommissionNetRate", "brokerVatRate", "brokerBuyerShareRate",
    "applyBrokerButton", "otherPurchaseCostsRate", "inventoryValue", "managementCostMonthly",
    "ownerMaintenancePerSqm", "rentBuffer", "houseFeeBackPayment", "reserveTotal", "communityArrears",
    "sellerArrears", "typicalLevyWeight", "wegLoanAnnuity", "reserveShareOutput", "coverageRatioOutput",
    "coverageWarning", "backFromCosts", "nextFromCosts",
}
RESULT_IDS = {
    "stepResult", "summaryCosts", "maxPurchasePrice", "totalProjectCost", "monthlyRate", "stepBudget",
    "stepDeductions", "stepPrice", "askingPrice", "askingVerdict", "askingDelta", "scenarioRealistic",
    "scenarioOptimistic", "scenarioConservative", "scenarioNote", "kpiGrid", "hintsList", "hintsCount",
    "calculationWarning", "actionCompare", "actionShare", "actionSave", "advancedResult", "corridorLow",
    "corridorHigh", "kpiGridFull", "optimisticRentIncrease", "optimisticInterestReduction",
    "optimisticRenovationReduction", "optimisticHouseFeeReduction", "conservativeRentReduction",
    "conservativeInterestIncrease", "conservativeRenovationIncrease", "conservativeHouseFeeIncrease",
    "buyerInterestChart", "chartLoan", "marketPricePerSqm", "marketDelta", "purchaseCostsBreakdown", "backFromResult",
}
SELLER_IDS = {
    "sellerDialog", "sellerFullscreenButton", "sellerPrintButton", "sellerCloseButton", "sellerDataStatus",
    "sellerGoal", "sellerStartObject", "sellerPositives", "sellerStartPrice", "sellerStartPriceLabel",
    "sellerBridge", "bridgeBudget", "bridgeCosts", "bridgeRenovation", "bridgeLevies", "bridgeCorridorHigh",
    "bridgeBuffer", "bridgeCorridorLow", "sellerBurdenList", "sellerBurdenTotal", "sellerUncertainList",
    "sellerReliefList", "sellerWithoutDisputed", "sellerDocsWouldRaise", "sellerCorridorLow", "sellerCorridorHigh",
    "sellerCoverageText", "sellerKeySentence", "sellerAskingVerdict", "sellerOptionPrice", "sellerTakeoverList",
    "sellerOptionClarify", "offerValidityDays", "purchaseOfferText", "copyPurchaseOffer", "sellerClosing",
    "sellerSummarySheet", "sheetStart", "sheetBurden", "sheetCorridorLow", "sheetCorridorHigh", "sheetRate",
    "sheetAskingDelta", "sheetOptions", "sheetReservation",
}
REQUIRED_IDS = SHELL_IDS | OBJECT_IDS | RENT_IDS | COST_IDS | RESULT_IDS | SELLER_IDS

RANGE_PAIR_IDS = (
    "optimisticRentIncrease", "optimisticInterestReduction", "optimisticRenovationReduction",
    "optimisticHouseFeeReduction", "conservativeRentReduction", "conservativeInterestIncrease",
    "conservativeRenovationIncrease", "conservativeHouseFeeIncrease",
)
CATEGORY_IDS = ("bathroom", "surfaces", "electric", "kitchen", "other")
LEVY_TYPES = ("roof", "facade", "windows", "pipes", "heating", "elevator", "garage", "balconies", "other")
WEG_CHECK_IDS = (
    "reserve_verified", "minutes_complete", "resolutions_complete", "business_plan", "community_arrears",
    "litigation", "purpose_rental", "sale_consent", "section2_rights", "major_owner", "tenant_rights",
    "structural_changes",
)
REQUIRED_JS_MARKERS = (
    'const jsonSchema = "etw-kaufpreisrechner";', "const jsonVersion = 2;", "const globalDefaults = {",
    "const textDefaults = {", "const transferTaxRates = {", "const interestTimeline = [", "const levyCostBands = {",
    "const renovationCategories = [", "const renovationPackages = [", "const levyCatalog = [",
    "const evidenceLevels = [", "const defaultLevies = [", "const inspectionCatalog = {", "const wegCheckCatalog = [",
    "function getEffectiveCategoryPrice(", "function calculateSpecialLevies(", "function solvePrice(",
    "function calculateScenario(", "function computeMetrics(", "function buildPriceBridge(",
    "function buildPurchaseOfferText(", "function renderInterestChart(", "function showStep(", "function calculate(",
    'renderInterestChart("buyerInterestChart"', "§ 311b BGB", "§ 12 WEG", "GEG-Risiko", "MFH-Kaufpreisrechner",
)
STEP_ORDER = ("stepObject", "stepRent", "stepCosts", "stepResult")
SELLER_MARKERS = ("seller-start", "seller-burdens", "seller-uncertainty", "seller-corridor", "seller-options", 'id="sellerSummarySheet"')


class AppHTMLParser(HTMLParser):
    def __init__(self) -> None:
        super().__init__(convert_charrefs=True)
        self.ids: list[str] = []
        self.local_assets: list[str] = []
        self.external_assets: list[str] = []

    def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None:
        a = dict(attrs)
        if a.get("id"):
            self.ids.append(a["id"])
        cand = a.get("src") if tag == "script" else (a.get("href") if tag == "link" and (a.get("rel") or "").lower() == "stylesheet" else None)
        if cand:
            (self.external_assets if urlsplit(cand).scheme or cand.startswith("//") else self.local_assets).append(cand)


def extract_object_keys(js: str, name: str) -> list[str]:
    m = re.search(rf"const {re.escape(name)} = \{{(.*?)\n\}};", js, re.S)
    return re.findall(r"^\s{2}\"?([A-Za-z][A-Za-z0-9_-]*)\"?:", m.group(1), re.M) if m else []


def default_package_total(js: str) -> float | None:
    m = re.search(r"const renovationCategories = \[(.*?)\n\];", js, re.S)
    if not m:
        return None
    cats: dict[str, float] = {}
    for item in re.findall(r"\{\s*(.*?)\}", m.group(1), re.S):
        cid = re.search(r'id:\s*"([a-z-]+)"', item)
        q = re.search(r"\bquantity:\s*([0-9.]+)", item)
        p = re.search(r"\bprice:\s*([0-9.]+)", item)
        if cid and q and p:
            cats[cid.group(1)] = float(q.group(1)) * float(p.group(1))
    if not cats:
        return None
    return sum(cats.get(c, 0) for c in ("bathroom", "surfaces", "electric"))


def main() -> int:
    args = argparse.ArgumentParser(description="Validate an ETW calculator project (v2.0).").parse_args(namespace=None) if False else None
    parser = argparse.ArgumentParser(description="Validate an ETW purchase-price calculator project (v2.0).")
    parser.add_argument("project", type=Path)
    args = parser.parse_args()
    root = args.project.expanduser().resolve()
    errors: list[str] = []
    notes: list[str] = []

    for rel in REQUIRED_FILES:
        if not (root / rel).is_file():
            errors.append(f"Missing required file: {rel}")
    if errors:
        for e in errors:
            print(f"ERROR: {e}")
        return 1

    html_path, css_path, js_path = root / "dist/index.html", root / "dist/styles.css", root / "dist/app.js"
    html, css, js = (p.read_text(encoding="utf-8") for p in (html_path, css_path, js_path))

    p = AppHTMLParser()
    try:
        p.feed(html)
        p.close()
    except Exception as exc:
        errors.append(f"HTML parsing failed: {exc}")

    dupes = sorted(i for i, c in Counter(p.ids).items() if c > 1)
    if dupes:
        errors.append(f"Duplicate HTML ids: {', '.join(dupes)}")
    html_ids = set(p.ids)
    missing = sorted(REQUIRED_IDS - html_ids)
    if missing:
        errors.append(f"Missing required UI ids ({len(missing)}): {', '.join(missing)}")
    miss_r = sorted(f"{b}Range" for b in RANGE_PAIR_IDS if f"{b}Range" not in html_ids)
    if miss_r:
        errors.append(f"Missing range inputs for scenario sliders: {', '.join(miss_r)}")

    referenced = set(re.findall(r'document\.getElementById\("([^"]+)"\)', js))
    referenced.update(extract_object_keys(js, "globalDefaults"))
    referenced.update(extract_object_keys(js, "textDefaults"))
    generated = {f"{c}-{s}" for c in CATEGORY_IDS for s in ("active", "quantity", "price", "geg-risk", "adjusted", "source", "disputed")}
    generated.update(f"weg-{w}" for w in WEG_CHECK_IDS)
    generated.update({"buyerInterestChartSvgTitle", "buyerInterestChartSvgDesc", "meaShare", "meaTotal", "mode", "scenarioSelected"})
    miss_dom = sorted(referenced - html_ids - generated)
    if miss_dom:
        errors.append(f"JavaScript references ids that are neither in HTML nor generated per ui-spec: {', '.join(miss_dom)}")

    if p.external_assets:
        errors.append(f"External scripts/stylesheets are not allowed: {', '.join(p.external_assets)}")
    for asset in p.local_assets:
        if not (html_path.parent / urlsplit(asset).path).resolve().is_file():
            errors.append(f"Missing local asset referenced by HTML: {asset}")

    order = [html.find(f'id="{i}"') for i in STEP_ORDER]
    if any(x < 0 for x in order) or order != sorted(order):
        errors.append("Step order is not stepObject → stepRent → stepCosts → stepResult.")
    ds = html.find('id="sellerDialog"')
    if ds < 0:
        errors.append("Seller dialog missing.")
    else:
        so = [html.find(m, ds) for m in SELLER_MARKERS]
        if any(x < 0 for x in so) or so != sorted(so):
            errors.append("Seller station order is not start → burdens → uncertainty → corridor → options → summary sheet.")

    for m in REQUIRED_JS_MARKERS:
        if m not in js:
            errors.append(f"Missing JavaScript feature marker: {m}")
    for group, ids in (("category", CATEGORY_IDS), ("levy type", LEVY_TYPES), ("WEG check", WEG_CHECK_IDS)):
        for i in ids:
            if f'"{i}"' not in js and f"'{i}'" not in js:
                errors.append(f"{group} id missing in app.js: {i}")

    if re.search(r"\beval\s*\(|\bnew\s+Function\s*\(", js):
        errors.append("Unsafe dynamic code execution found in JavaScript.")
    if re.search(r"innerHTML\s*\+?=\s*(?:readText|readInput|readValue|.*\.value\b)", js):
        errors.append("Potential user-controlled innerHTML assignment found.")

    depth = mn = 0
    for ch in re.sub(r"/\*.*?\*/", "", css, flags=re.S):
        if ch == "{":
            depth += 1
        elif ch == "}":
            depth -= 1
            mn = min(mn, depth)
    if depth != 0 or mn < 0:
        errors.append("CSS braces are unbalanced.")
    for cm in ("presentation-mode", "100dvh", "@media print", "print-seller", "@page", "[hidden]"):
        if cm not in css:
            errors.append(f"CSS marker missing: {cm}")

    total = default_package_total(js)
    if total is None:
        errors.append("Could not read renovationCategories (expected id/quantity/price per category).")
    elif abs(total - 35000) > 0.01:
        errors.append(f"Package 'medium' (bathroom + surfaces + electric) must be 35000, got {total:.2f}.")
    else:
        notes.append("Default package medium: 35000 EUR")

    dl = re.search(r"const defaultLevies = \[(.*?)\n\];", js, re.S)
    if not dl:
        errors.append("defaultLevies missing.")
    else:
        body = dl.group(1)
        for pat, msg in ((r'type:\s*"roof"', 'defaultLevies must contain type "roof"'), (r"totalCost:\s*120000\b", "defaultLevies roof totalCost must be 120000"),
                         (r"reserveCoverage:\s*40000\b", "defaultLevies roof reserveCoverage must be 40000"), (r'evidence:\s*"likely"', 'defaultLevies roof evidence must be "likely"')):
            if not re.search(pat, body):
                errors.append(msg + ".")
        if all("defaultLevies" not in e for e in errors):
            notes.append("Default levy roof: 120000 / 40000 (likely)")

    node = shutil.which("node")
    if node:
        r = subprocess.run([node, "--check", str(js_path)], capture_output=True, text=True, check=False)
        if r.returncode != 0:
            errors.append(f"JavaScript syntax check failed: {r.stderr.strip()}")
        else:
            notes.append("JavaScript syntax: OK")
    else:
        notes.append("Node unavailable; JavaScript syntax check skipped")

    if errors:
        for e in errors:
            print(f"ERROR: {e}")
        for n in notes:
            print(f"NOTE: {n}")
        return 1
    for n in notes:
        print(f"OK: {n}")
    print(f"OK: {len(html_ids)} unique HTML ids; all required ids, markers and assets present")
    print("Validation passed.")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
`````

<!-- porthor:skill v1 · https://porthor.de/skills -->
