summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMatthias Andreas Benkard <code@mail.matthias.benkard.de>2026-08-07 12:49:30 +0200
committerMatthias Andreas Benkard <code@mail.matthias.benkard.de>2026-08-22 19:18:03 +0200
commitbf8c6d7cf96ba8602f8b2c34ef5345f461d74f3a (patch)
tree4ae3c36fffd505c73b1cd7241dd91340d5a8bfbd
parent8a2862590bb76d90597fe02432ac2e01c70d26a2 (diff)
Windows-95-Design
-rw-r--r--app/README.md63
-rw-r--r--app/icon-192.pngbin2288 -> 2914 bytes
-rw-r--r--app/icon-512.pngbin6201 -> 5008 bytes
-rw-r--r--app/icon-maskable-512.pngbin5181 -> 4936 bytes
-rw-r--r--app/index.html610
-rw-r--r--app/manifest.json33
-rw-r--r--app/service-worker.js2
7 files changed, 498 insertions, 210 deletions
diff --git a/app/README.md b/app/README.md
index 9265909..dac29cf 100644
--- a/app/README.md
+++ b/app/README.md
@@ -61,6 +61,7 @@ selbst zu aktualisieren.
| 9 | Rechenweg |
| 10 | Render-Schleife |
| 11 | Installation und Offline-Betrieb |
+| 12 | Fensterrahmen: Titelleisten und Minimieren |
Der Rechenkern in Teil 1 ist **die einzige Stelle, an der gerechnet wird.** Er ist zeichengenau
aus `build_bav.py` übertragen; Änderungen an der Rechtslage gehören in beide Dateien.
@@ -119,9 +120,15 @@ Damit die beiden Kopien des Kerns nicht auseinanderlaufen, schneidet `extract.js
`index.html` heraus und erzeugt daraus das `model.js`, gegen das getestet wird. Der Testkern
*kann* also gar nicht vom ausgelieferten abweichen.
-Zusätzlich prüfen zwei Testskripte (nicht Teil der Auslieferung, siehe Sitzungsprotokoll):
+Die Testskripte liegen unter `outputs/tests/`, `./tests/alle.sh` fährt alles durch:
-- **DOM-Test:** 40 Zusicherungen über jsdom — Karten, Sichtbarkeitsregeln, Regler, Segment-
+- **Stilblatt-Test:** vergleicht die im Dokument benutzten Klassen mit denen im Stilblatt,
+ in beide Richtungen. Benutzt-ohne-Regel heißt: ein Element steht unformatiert da.
+ Regel-ohne-Nutzung heißt: tote Zeilen, meist eine vergessene Umbenennung. Der Test hat
+ nach dem Stilwechsel zwei echte Fehler gefunden — den unformatierten Wasserfall-Umschalter
+ und sechs verwaiste Hilfsklassen. Zusätzlich: jede per `var()` referenzierte Farbvariable
+ muss definiert sein.
+- **DOM-Test:** 34 Zusicherungen über jsdom — Karten, Sichtbarkeitsregeln, Regler, Segment-
umschalter, Brutto-Netto-Kopplung in beide Richtungen, Fazittexte, mitlaufende Leiste,
Rundung, Tabs, Kennzahlwechsel, Referenz merken, Zurücksetzen, `localStorage`.
Die beiden seltenen Zweige des Fazits — „zulasten“ und „die Reihenfolge kehrt sich um“ —
@@ -133,8 +140,8 @@ Zusätzlich prüfen zwei Testskripte (nicht Teil der Auslieferung, siehe Sitzung
Beitrag 0, entarteter Zeitraum bei Horizont = Rentenbeginn, und — der interessanteste —
nicht ganzzahlige Zwischenwerte in der Sensitivitätskurve über das Feld *Alter*, wodurch
`Z[6,2]` indiziert wurde.
-- **Canvas-Test, Stufe 2** (Sichtbarkeit): 208 Kombinationen aus 4 Breiten-/DPR-Paaren ×
- 13 Szenarien × 4 Diagrammen. Prüft zusätzlich, dass jede Koordinate **innerhalb der Bitmap**
+- **Canvas-Test, Stufe 2** (Sichtbarkeit): 400 Kombinationen aus 5 Breiten-/DPR-Paaren ×
+ 20 Szenarien × 4 Diagrammen. Prüft zusätzlich, dass jede Koordinate **innerhalb der Bitmap**
liegt. Diese Stufe entstand, nachdem Stufe 1 einen sichtbaren Fehler durchgelassen hatte:
`prep()` skalierte die Zeichenmatrix mit `devicePixelRatio`, setzte `canvas.height` aber in
logischen Pixeln — alles unterhalb von `h/dpr` wurde aus der Bitmap herausgezeichnet.
@@ -215,7 +222,53 @@ Die Sensitivitätskurve zeigt die Sättigung als waagerechten Ast.
---
-## 9. Bewusst nicht gebaut
+## 9. Das Aussehen: Windows 3.1 / NT 3.5
+
+Silber `#C0C0C0` auf Petrol `#008080`, Navy-Titelleisten, harte Ein-Pixel-Kanten, alles aus der
+16-Farben-VGA-Palette. **Es gibt nur ein Farbschema** — `prefers-color-scheme` ist entfernt,
+eine Dunkelvariante gäbe es hier nicht zu skinnen.
+
+Drei Kantenformen tragen die ganze Oberfläche:
+
+| Form | Aufbau | Wo |
+|---|---|---|
+| erhaben (3.1) | `1px solid #000` + `inset 1px 1px 0 #FFF` + `inset -1px -1px 0 #808080` | Schaltflächen |
+| erhaben (95) | zusätzlich `border-color:#DFDFDF #000 #000 #DFDFDF` | Fenster, Kacheln |
+| versenkt | `border-color:#808080 #FFF #FFF #808080` + `inset 1px 1px 0 #000` | Felder, Zeichenfläche |
+
+Gedrückte Umschalter bekommen zusätzlich das Rastermuster der eingerasteten Werkzeugleiste
+(`repeating-conic-gradient` mit 2 px Kachel). Das Fokusrechteck ist das originale gepunktete —
+zugleich das barrierefreiste, was Windows je hatte.
+
+### Was bewusst aus Windows 95 / NT 4 kommt
+
+Windows 3.1 kannte diese Steuerelemente schlicht nicht, und ohne sie wäre die App schlechter:
+
+- **Trackbar** — der Schieberegler ist ein Win95-Common-Control. Unter 3.1 hätte man Zahlen
+ eintippen müssen; die unmittelbare Rückkopplung wäre dahin.
+- **Registerkarten** — 3.1 löste Mehrseitigkeit über eigene Dialoge. Vier Diagramme in vier
+ Fenstern wären hier klar schlechter.
+- **Versenkter Rahmen** (`WS_EX_CLIENTEDGE`) statt 3.1s einfachem schwarzen Rahmen um
+ Eingabefelder — liest sich deutlich besser.
+
+### Was 3.1 besser konnte
+
+Die Segmentschalter sind zu **echten Optionsfeldgruppen** geworden. Das ist die authentische
+Entsprechung für eine Auswahl unter wenigen Alternativen, es ist das semantisch richtige
+Element, und die Pfeiltastennavigation der Radiogruppe gibt es geschenkt.
+
+### Fenster, die wirklich Fenster sind
+
+Jeder Abschnitt hat eine Titelleiste mit Systemmenü-Kasten und Minimieren-Knopf. Der Knopf ist
+kein Zierrat: er klappt den Inhalt tatsächlich ein, Doppelklick auf die Titelleiste ebenso, und
+der Zustand wird gespeichert. Auf dem Telefon ist das nützlich — man klappt „Ihre Angaben“ zu
+und hat das Diagramm ganzseitig.
+
+Grau ist unter Windows die Farbe des *Deaktivierten*. Lesbarer Text bleibt deshalb durchgehend
+schwarz und wird über Größe und Kursivstellung abgestuft, nicht über Aufhellung. Das ist
+zugleich authentisch und der bessere Kontrast: Schwarz auf `#C0C0C0` sind 9,9 : 1.
+
+## 10. Bewusst nicht gebaut
- **Onboarding-Assistent.** Elf Kernfelder tragen keinen Wizard; er verstellte nur den Blick
auf die Kopplung der Größen.
diff --git a/app/icon-192.png b/app/icon-192.png
index a782b2c..f8a1bd0 100644
--- a/app/icon-192.png
+++ b/app/icon-192.png
Binary files differ
diff --git a/app/icon-512.png b/app/icon-512.png
index 8512817..781a845 100644
--- a/app/icon-512.png
+++ b/app/icon-512.png
Binary files differ
diff --git a/app/icon-maskable-512.png b/app/icon-maskable-512.png
index 08606b1..6be1ffb 100644
--- a/app/icon-maskable-512.png
+++ b/app/icon-maskable-512.png
Binary files differ
diff --git a/app/index.html b/app/index.html
index 83ab1b1..22b14ee 100644
--- a/app/index.html
+++ b/app/index.html
@@ -3,163 +3,313 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
-<meta name="theme-color" content="#1f3864">
+<meta name="theme-color" content="#000080">
<meta name="description" content="Betriebliche Altersvorsorge, Altersvorsorgedepot und privates ETF-Depot bei gleichem Nettoaufwand vergleichen. Rechtsstand 2026.">
<title>Altersvorsorge-Vergleich</title>
<link rel="manifest" href="manifest.json">
<link rel="apple-touch-icon" href="icon-192.png">
<style>
+/* =====================================================================
+ Windows 3.1 / NT 3.5. 16-Farben-VGA-Palette, harte 1-Pixel-Kanten.
+ Wo 3.1 kein passendes Steuerelement kannte, wird das aus Windows 95 /
+ NT 4 genommen und als solches kenntlich gemacht:
+ - Schieberegler (Trackbar) -> Win95 common control, 3.1 hatte keinen
+ - Registerkarten (Tab control) -> Win95, 3.1 loeste das mit Dialogen
+ - versenkter Rahmen (CLIENTEDGE)-> Win95, liest sich besser als 3.1s
+ einfacher schwarzer Rahmen
+ Keine Dunkelvariante: das Silber IST das Thema.
+ ===================================================================== */
:root{
- --bg:#f6f7fa; --card:#ffffff; --ink:#15181d; --muted:#5c6470; --line:#dde1e8;
- --accent:#1f3864; --bav:#c9761d; --av:#2266b8; --priv:#2b8a5a;
- --good:#1e7a48; --bad:#b3261e; --chip:#eef1f6;
- --r:14px; --sh:0 1px 2px rgba(16,24,40,.06), 0 6px 20px rgba(16,24,40,.05);
- --mono:ui-monospace,SFMono-Regular,"SF Mono",Menlo,Consolas,monospace;
-}
-@media (prefers-color-scheme: dark){
- :root{ --bg:#0e1116; --card:#161a21; --ink:#e7eaf0; --muted:#97a0ae; --line:#262c36;
- --accent:#8fb0e8; --bav:#e29a4b; --av:#5c9ce8; --priv:#4fb583;
- --good:#4fb583; --bad:#e5726a; --chip:#1e242d;
- --sh:0 1px 2px rgba(0,0,0,.35), 0 6px 20px rgba(0,0,0,.25); }
+ /* die 16 VGA-Farben, aus denen alles gebaut ist */
+ --black:#000000; --maroon:#800000; --green:#008000; --olive:#808000;
+ --navy:#000080; --purple:#800080; --teal:#008080; --silver:#C0C0C0;
+ --gray:#808080; --red:#FF0000; --lime:#00FF00; --yellow:#FFFF00;
+ --blue:#0000FF; --fuchsia:#FF00FF;--aqua:#00FFFF; --white:#FFFFFF;
+ /* Win95 kannte zusaetzlich das helle Grau der aeusseren Kante */
+ --face-lt:#DFDFDF;
+
+ --desk:var(--teal); /* Programm-Manager-Hintergrund */
+ --face:var(--silver); /* Dialogflaeche */
+ --field:var(--white); /* Eingabefeld */
+ --ink:var(--black);
+ --dim:var(--gray); /* NUR fuer Deaktiviertes und Zierrat */
+ --title:var(--navy);
+ --title-fg:var(--white);
+
+ --bav:#800000; --av:#000080; --priv:#008000;
+ --good:#008000; --bad:#800000;
+
+ --ui:"MS Sans Serif","Microsoft Sans Serif",Tahoma,Geneva,Verdana,Arial,sans-serif;
+ --mono:"Fixedsys","Lucida Console","Courier New",monospace;
}
*{box-sizing:border-box}
html{-webkit-text-size-adjust:100%}
-body{margin:0;background:var(--bg);color:var(--ink);
- font:16px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
+body{margin:0;background:var(--desk);color:var(--ink);font:13px/1.45 var(--ui);
padding-bottom:env(safe-area-inset-bottom)}
-.wrap{max-width:920px;margin:0 auto;padding:16px 14px 64px}
-h1{font-size:1.25rem;margin:.2em 0 .1em;letter-spacing:-.01em}
-h2{font-size:1.02rem;margin:0 0 .1em;letter-spacing:-.01em}
-h3{font-size:.9rem;margin:1.2em 0 .5em;color:var(--muted);text-transform:uppercase;letter-spacing:.06em}
-p{margin:.4em 0}
-.sub{color:var(--muted);font-size:.85rem}
-.card{background:var(--card);border:1px solid var(--line);border-radius:var(--r);padding:14px;box-shadow:var(--sh);margin:12px 0}
-.card>h2{margin-bottom:.35em}
+.wrap{max-width:940px;margin:0 auto;padding:10px 8px 40px}
+
+/* ---------- Die drei Kantenformen ----------
+ Sie sind unten ueberall ausgeschrieben statt als Hilfsklassen vergeben,
+ damit jede Regel fuer sich lesbar bleibt. Zum Nachschlagen:
+
+ erhaben (Schaltflaeche, Win 3.1):
+ border:1px solid #000 + inset 1px 1px 0 #FFF + inset -1px -1px 0 #808080
+ erhaben (Fenster/Kachel, Win 95, aeussere Kante zusaetzlich):
+ border-color:#DFDFDF #000 #000 #DFDFDF + dieselben inset-Schatten
+ versenkt (Eingabefeld, Klientenbereich):
+ border-color:#808080 #FFF #FFF #808080 + inset 1px 1px 0 #000
+ + inset -1px -1px 0 #DFDFDF
+ geaetzt (Gruppenrahmen): nur border-color:#808080 #FFF #FFF #808080
+*/
+
+/* ---------- Fenster ---------- */
+.win{background:var(--face);margin:0 0 8px;
+ border:1px solid;border-color:var(--face-lt) var(--black) var(--black) var(--face-lt);
+ box-shadow:inset 1px 1px 0 var(--white), inset -1px -1px 0 var(--gray)}
+.titlebar{display:flex;align-items:center;gap:3px;background:var(--title);color:var(--title-fg);
+ padding:2px 2px 2px 3px;margin:2px;font-weight:700;font-size:12px;user-select:none}
+.tbtext{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
+ letter-spacing:.01em;padding-left:2px}
+/* Systemmenue-Kasten links: unter 3.1 ein Kasten mit waagerechtem Strich */
+.sysbox{width:17px;height:15px;flex:0 0 auto;background:var(--face);position:relative;
+ border:1px solid;border-color:var(--white) var(--black) var(--black) var(--white)}
+.sysbox::after{content:"";position:absolute;left:2px;right:2px;top:6px;height:2px;
+ background:var(--black);border-top:1px solid var(--gray)}
+.tbbtn{width:17px;height:15px;flex:0 0 auto;padding:0;background:var(--face);color:var(--black);
+ font:9px/13px var(--ui);cursor:pointer;
+ border:1px solid;border-color:var(--white) var(--black) var(--black) var(--white)}
+.tbbtn:active{border-color:var(--black) var(--white) var(--white) var(--black)}
+.winbody{padding:9px 10px 11px}
+.win.rolled .winbody{display:none}
+
+/* ---------- Schrift ---------- */
+h1{font-size:15px;margin:0 0 2px;font-weight:700}
+h2{font-size:13px;margin:0 0 3px;font-weight:700}
+h3{font-size:12px;margin:12px 0 5px;font-weight:700}
+p{margin:4px 0}
+/* Grau ist unter Windows die Farbe des Deaktivierten. Lesbarer Text bleibt
+ schwarz und wird ueber Groesse und Kursivstellung abgestuft. */
+.sub{font-size:12px;font-style:italic}
+.pill{display:inline-block;background:var(--face);padding:0 5px;font-size:11px;
+ border:1px solid;border-color:var(--gray) var(--white) var(--white) var(--gray)}
+
+/* ---------- Schaltflaechen (Win 3.1) ---------- */
+.btn,.qm,.tabs button,.sb-up{font-family:var(--ui)}
+.btn{appearance:none;background:var(--face);color:var(--ink);font-size:12px;padding:4px 12px;
+ min-height:25px;cursor:pointer;border:1px solid var(--black);
+ box-shadow:inset 1px 1px 0 var(--white), inset -1px -1px 0 var(--gray)}
+.btn:active{box-shadow:inset 1px 1px 0 var(--gray), inset -1px -1px 0 var(--white);
+ padding:5px 11px 3px 13px}
+.btn.pri{font-weight:700;outline:1px solid var(--black);outline-offset:1px}
+.btn[disabled]{color:var(--gray);text-shadow:1px 1px 0 var(--white);cursor:default}
+.qm{appearance:none;width:17px;height:16px;padding:0;background:var(--face);color:var(--ink);
+ font-size:11px;font-weight:700;line-height:14px;text-align:center;cursor:pointer;
+ margin-left:5px;vertical-align:-2px;border:1px solid var(--black);
+ box-shadow:inset 1px 1px 0 var(--white), inset -1px -1px 0 var(--gray)}
+.qm:active{box-shadow:inset 1px 1px 0 var(--gray), inset -1px -1px 0 var(--white)}
+.row{display:flex;gap:6px;flex-wrap:wrap;align-items:center}
+
+/* ---------- Fokusrechteck ---------- *
+ Das gepunktete Rechteck ist das Original und zugleich barrierefrei. */
+:focus-visible{outline:1px dotted var(--black);outline-offset:-4px}
+input[type=text]:focus-visible,input[type=number]:focus-visible{outline-offset:-3px}
+
/* ---------- Ergebniskarten ---------- */
-#metricbar{display:flex;gap:6px;flex-wrap:wrap;margin:10px 0 4px}
-.seg{display:inline-flex;background:var(--chip);border-radius:10px;padding:3px;gap:2px;flex-wrap:wrap}
-.seg button{appearance:none;border:0;background:transparent;color:var(--muted);font:inherit;font-size:.84rem;
- padding:7px 11px;border-radius:8px;cursor:pointer;white-space:nowrap;min-height:36px}
-.seg button[aria-pressed=true]{background:var(--card);color:var(--ink);font-weight:600;box-shadow:var(--sh)}
-#cards{display:grid;grid-template-columns:repeat(3,1fr);gap:10px}
+#metricbar{display:flex;gap:0;flex-wrap:wrap;margin:2px 0 5px}
+.seg{display:inline-flex;gap:0;flex-wrap:wrap}
+#metricbar button,.seg.btnseg button{appearance:none;background:var(--face);color:var(--ink);
+ font:12px var(--ui);padding:4px 11px;min-height:25px;cursor:pointer;white-space:nowrap;
+ border:1px solid var(--black);margin-right:-1px;
+ box-shadow:inset 1px 1px 0 var(--white), inset -1px -1px 0 var(--gray)}
+/* Gedrueckt = versenkt, dazu das Rastermuster der eingerasteten Werkzeugleiste
+ aus Windows 3.1. */
+#metricbar button[aria-pressed=true],.seg.btnseg button[aria-pressed=true]{font-weight:700;
+ box-shadow:inset 1px 1px 0 var(--gray), inset -1px -1px 0 var(--white);
+ background:repeating-conic-gradient(var(--face) 0% 25%, var(--face-lt) 0% 50%) 0 0/2px 2px}
+.seg.btnseg{margin-left:auto;align-self:flex-end;padding-bottom:2px}
+.seg.btnseg button{font-size:11px;padding:3px 9px;min-height:22px}
+#cards{display:grid;grid-template-columns:repeat(3,1fr);gap:6px}
@media(max-width:640px){#cards{grid-template-columns:1fr}}
-.opt{border:1px solid var(--line);border-radius:12px;padding:12px;position:relative;overflow:hidden;
- background:var(--card);transition:border-color .15s}
-.opt::before{content:"";position:absolute;inset:0 auto 0 0;width:4px;background:var(--c)}
-.opt.win{border-color:var(--c)}
-.opt .nm{font-size:.8rem;color:var(--muted);font-weight:600;display:flex;align-items:center;gap:6px}
-.opt .big{font-size:1.5rem;font-weight:700;font-variant-numeric:tabular-nums;letter-spacing:-.02em;margin:.12em 0}
-@media(max-width:640px){.opt .big{font-size:1.35rem}}
-.opt .sec{font-size:.78rem;color:var(--muted);font-variant-numeric:tabular-nums}
-.delta{font-size:.78rem;font-weight:600;font-variant-numeric:tabular-nums;display:inline-block;
- padding:1px 7px;border-radius:99px;background:var(--chip);margin-top:4px}
-.delta.up{color:var(--good)} .delta.dn{color:var(--bad)} .delta.eq{color:var(--muted)}
-.crown{font-size:.68rem;background:var(--c);color:#fff;border-radius:99px;padding:1px 7px;font-weight:700}
-@media (prefers-color-scheme: dark){.crown{color:#0e1116}}
-#baserow{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-top:10px;font-size:.8rem;color:var(--muted)}
-
-/* ---------- Eingaben ---------- */
-.field{display:grid;grid-template-columns:1fr auto;gap:2px 10px;align-items:center;padding:9px 0;border-top:1px solid var(--line)}
-.field:first-of-type{border-top:0}
-.field .lab{font-size:.9rem;grid-column:1}
+.opt{background:var(--face);padding:7px 9px 8px;position:relative;
+ border:1px solid;border-color:var(--white) var(--gray) var(--gray) var(--white)}
+.opt.lead{border-color:var(--gray) var(--white) var(--white) var(--gray);background:var(--face-lt)}
+.opt .nm{font-size:11px;font-weight:700;display:flex;align-items:center;gap:5px}
+.opt .nm::before{content:"";width:9px;height:9px;flex:0 0 auto;background:var(--c);
+ border:1px solid var(--black)}
+.opt .big{font-size:20px;font-weight:700;font-family:var(--mono);letter-spacing:-.02em;margin:3px 0 1px}
+@media(max-width:640px){.opt .big{font-size:18px}}
+.opt .sec{font-size:11px;font-family:var(--mono)}
+.delta{font-size:11px;font-weight:700;font-family:var(--mono);display:inline-block;
+ padding:0 5px;background:var(--face);margin-top:2px;
+ border:1px solid;border-color:var(--gray) var(--white) var(--white) var(--gray)}
+.delta.up{color:var(--good)} .delta.dn{color:var(--bad)} .delta.eq{color:var(--dim)}
+.crown{font-size:10px;background:var(--title);color:var(--white);padding:0 5px;font-weight:700}
+#baserow{display:flex;align-items:center;gap:7px;flex-wrap:wrap;margin-top:9px;font-size:11px}
+
+/* ---------- Meldungsfelder ---------- *
+ Nachgebaut nach der 3.1-Meldungsbox: Symbol links, Text rechts. */
+.msg{display:flex;gap:8px;align-items:flex-start;background:var(--face);
+ font-size:12px;line-height:1.45;padding:7px 9px;margin:9px 0 0;
+ border:1px solid;border-color:var(--white) var(--gray) var(--gray) var(--white)}
+.msg::before{content:attr(data-ico);flex:0 0 auto;width:20px;height:20px;line-height:19px;
+ text-align:center;font-weight:700;font-size:14px;color:var(--white);background:var(--title);
+ border:1px solid var(--black)}
+.msg.warn::before{background:var(--maroon)}
+.msg.note::before{background:var(--navy)}
+.msg.demo::before{background:var(--olive)}
+.verdict{margin:9px 0 0;padding:8px 10px;font-size:12.5px;line-height:1.5;background:var(--face-lt);
+ border:1px solid;border-color:var(--gray) var(--white) var(--white) var(--gray)}
+.verdict b{font-weight:700}
+.verdict .band{display:block;margin-top:6px;padding-top:6px;font-size:11.5px;font-style:italic;
+ border-top:1px solid var(--gray)}
+.warn{color:var(--maroon);font-size:12px;font-weight:700}
+
+/* ---------- Eingabefelder ---------- */
+/* Geaetzte Trennlinie: eine graue Zeile mit einer weissen darunter — genau so
+ trennte Windows 3.1 Dialogbereiche. #DFDFDF allein waere auf Silber unsichtbar. */
+.field{display:grid;grid-template-columns:1fr auto;gap:1px 9px;align-items:center;
+ padding:6px 0 7px;border-bottom:1px solid var(--gray);box-shadow:0 1px 0 var(--white)}
+.field:last-child{border-bottom:0;box-shadow:none}
+.field .lab{font-size:12px;grid-column:1}
.field .val{grid-column:2;text-align:right}
.field .rng{grid-column:1/-1}
-.field .hint{grid-column:1/-1;font-size:.76rem;color:var(--muted);margin-top:2px;display:none}
+.field .hint{grid-column:1/-1;font-size:11.5px;line-height:1.45;margin-top:4px;display:none;
+ background:var(--white);padding:6px 8px;
+ border:1px solid;border-color:var(--gray) var(--white) var(--white) var(--gray)}
.field.open .hint{display:block}
-.qm{appearance:none;border:0;background:var(--chip);color:var(--muted);width:19px;height:19px;border-radius:99px;
- font-size:.72rem;line-height:19px;text-align:center;padding:0;cursor:pointer;margin-left:5px;vertical-align:1px}
-input[type=number],input[type=text].num{width:8.2em;font:inherit;font-size:.92rem;font-variant-numeric:tabular-nums;
- text-align:right;padding:6px 8px;border:1px solid var(--line);border-radius:8px;background:var(--bg);color:var(--ink);min-height:34px}
-input[type=range]{width:100%;margin:6px 0 0;accent-color:var(--accent);height:26px}
-.suffix{color:var(--muted);font-size:.85rem;margin-left:4px}
-.field.pair-top{border-bottom:0;padding-bottom:4px}
-.field.pair-bottom{border-top:0;padding-top:4px;position:relative}
-.field.pair-bottom::before{content:"";position:absolute;left:-8px;top:-6px;bottom:14px;width:3px;
- border-radius:2px;background:var(--accent);opacity:.28}
-.field.pair-top::after{content:"";position:absolute;left:-8px;width:3px}
-.couple{grid-column:1/-1;font-size:.74rem;color:var(--muted);margin-top:4px;display:flex;gap:5px;align-items:baseline}
-.couple span{color:var(--accent);font-weight:700}
-.livenote{grid-column:1/-1;font-size:.75rem;line-height:1.45;color:var(--bad);margin-top:5px;
- background:var(--chip);border-radius:8px;padding:6px 9px}
-details{border-top:1px solid var(--line)}
-details>summary{cursor:pointer;padding:11px 2px;font-weight:600;font-size:.92rem;list-style:none;display:flex;
- justify-content:space-between;align-items:center;gap:8px;min-height:44px}
+input[type=text].num,input[type=number]{width:8.6em;font:13px var(--mono);text-align:right;
+ padding:2px 4px;min-height:23px;background:var(--field);color:var(--ink);
+ border:1px solid;border-color:var(--gray) var(--white) var(--white) var(--gray);
+ box-shadow:inset 1px 1px 0 var(--black), inset -1px -1px 0 var(--face-lt)}
+.suffix{font-size:11px;margin-left:4px}
+
+/* ---------- Trackbar (Windows 95) ---------- */
+input[type=range]{-webkit-appearance:none;appearance:none;width:100%;margin:7px 0 3px;
+ height:24px;background:transparent;cursor:pointer}
+input[type=range]::-webkit-slider-runnable-track{height:4px;background:var(--face);
+ border:1px solid;border-color:var(--gray) var(--white) var(--white) var(--gray);
+ box-shadow:inset 1px 1px 0 var(--black)}
+input[type=range]::-moz-range-track{height:4px;background:var(--face);
+ border:1px solid;border-color:var(--gray) var(--white) var(--white) var(--gray);
+ box-shadow:inset 1px 1px 0 var(--black)}
+input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;width:11px;height:20px;
+ margin-top:-8px;background:var(--face);border:1px solid var(--black);
+ box-shadow:inset 1px 1px 0 var(--white), inset -1px -1px 0 var(--gray)}
+input[type=range]::-moz-range-thumb{width:11px;height:20px;border-radius:0;
+ background:var(--face);border:1px solid var(--black);
+ box-shadow:inset 1px 1px 0 var(--white), inset -1px -1px 0 var(--gray)}
+
+/* ---------- Optionsfelder (Windows 3.1) ---------- */
+.radios{grid-column:1/-1;display:flex;flex-wrap:wrap;gap:3px 16px;margin-top:3px}
+.radios label{display:inline-flex;align-items:center;gap:6px;font-size:12px;cursor:pointer;
+ min-height:22px;padding:1px 2px}
+.radios input{appearance:none;-webkit-appearance:none;margin:0;width:13px;height:13px;flex:0 0 auto;
+ border-radius:50%;background:var(--white);position:relative;cursor:pointer;
+ border:1px solid;border-color:var(--gray) var(--white) var(--white) var(--gray);
+ box-shadow:inset 1px 1px 0 var(--black), inset -1px -1px 0 var(--face-lt)}
+.radios input:checked::after{content:"";position:absolute;inset:3px;border-radius:50%;
+ background:var(--black)}
+.radios label:has(input:checked){font-weight:700}
+.radios input:focus-visible{outline:none}
+.radios label:has(input:focus-visible){outline:1px dotted var(--black);outline-offset:0}
+
+/* ---------- Kopplungsband und Live-Notiz ---------- */
+.field.pair-top{border-bottom:0;box-shadow:none;padding-bottom:2px}
+.field.pair-bottom{border-top:0;padding-top:2px;position:relative}
+.couple{grid-column:1/-1;font-size:11px;margin-top:3px;display:flex;gap:5px;align-items:baseline;
+ font-style:italic}
+.couple span{font-weight:700;font-style:normal}
+.livenote{grid-column:1/-1;font-size:11.5px;line-height:1.45;margin-top:5px;padding:5px 8px;
+ background:var(--face);color:var(--maroon);
+ border:1px solid;border-color:var(--white) var(--gray) var(--gray) var(--white)}
+
+/* ---------- Aufklappbares (3.1 kannte das nicht; Schaltflaeche wie „Optionen >>“) ---------- */
+details{margin:0 0 3px}
+details>summary{cursor:pointer;list-style:none;display:flex;align-items:center;gap:7px;
+ font-size:12px;font-weight:700;padding:4px 9px;min-height:25px;background:var(--face);
+ border:1px solid var(--black);
+ box-shadow:inset 1px 1px 0 var(--white), inset -1px -1px 0 var(--gray)}
details>summary::-webkit-details-marker{display:none}
-details>summary::after{content:"\25BE";color:var(--muted);transition:transform .15s;font-size:.8rem}
-details[open]>summary::after{transform:rotate(180deg)}
-summary .cnt{font-weight:400;color:var(--muted);font-size:.8rem;margin-left:auto;margin-right:4px}
-.dbody{padding:0 0 8px}
-
-/* ---------- Diagramme ---------- */
-.tabs{display:flex;gap:4px;overflow-x:auto;-webkit-overflow-scrolling:touch;margin-bottom:8px;padding-bottom:2px}
-.tabs button{appearance:none;border:1px solid var(--line);background:var(--card);color:var(--muted);font:inherit;
- font-size:.82rem;padding:7px 11px;border-radius:99px;cursor:pointer;white-space:nowrap;min-height:36px}
-.tabs button[aria-pressed=true]{background:var(--accent);border-color:var(--accent);color:#fff;font-weight:600}
-@media (prefers-color-scheme: dark){.tabs button[aria-pressed=true]{color:#0e1116}}
-canvas{width:100%;display:block;touch-action:pan-y}
-.legend{display:flex;gap:12px;flex-wrap:wrap;font-size:.78rem;color:var(--muted);margin-top:8px}
-.legend i{display:inline-block;width:11px;height:3px;border-radius:2px;vertical-align:3px;margin-right:5px}
-.readout{font-size:.8rem;color:var(--muted);min-height:1.4em;margin-top:6px;font-variant-numeric:tabular-nums}
+details>summary::before{content:"\25B8";font-size:10px;width:9px}
+details[open]>summary::before{content:"\25BE"}
+summary .cnt{font-weight:400;margin-left:auto;font-size:11px;font-style:italic}
+.dbody{padding:7px 9px 4px;background:var(--face);
+ border:1px solid;border-color:var(--white) var(--gray) var(--gray) var(--white);
+ border-top:0}
+
+/* ---------- Registerkarten (Windows 95) ---------- */
+.tabs{display:flex;gap:1px;overflow-x:auto;-webkit-overflow-scrolling:touch;
+ margin:0 0 -1px;padding:2px 0 0;position:relative;z-index:2}
+.tabs button{appearance:none;background:var(--face);color:var(--ink);font-size:12px;
+ padding:4px 11px 5px;min-height:25px;cursor:pointer;white-space:nowrap;
+ border:1px solid;border-color:var(--white) var(--gray) transparent var(--white);
+ border-bottom:0;position:relative;top:2px}
+.tabs button[aria-pressed=true]{font-weight:700;top:0;padding:5px 13px 6px;background:var(--face);
+ border-color:var(--white) var(--black) transparent var(--white)}
+#chartwrap{background:var(--face);padding:8px 9px 9px;position:relative;
+ border:1px solid;border-color:var(--white) var(--black) var(--black) var(--white);
+ box-shadow:inset -1px -1px 0 var(--gray)}
+canvas{width:100%;display:block;touch-action:pan-y;background:var(--white);
+ border:1px solid;border-color:var(--gray) var(--white) var(--white) var(--gray);
+ box-shadow:inset 1px 1px 0 var(--black), inset -1px -1px 0 var(--face-lt)}
+.legend{display:flex;gap:11px;flex-wrap:wrap;font-size:11px;margin-top:7px}
+.legend i{display:inline-block;width:11px;height:9px;vertical-align:-1px;margin-right:5px;
+ border:1px solid var(--black)}
+.readout{font-size:11.5px;min-height:1.4em;margin-top:6px;font-family:var(--mono);
+ background:var(--face);padding:3px 6px;
+ border:1px solid;border-color:var(--gray) var(--white) var(--white) var(--gray)}
+.note{font-size:11.5px;line-height:1.45;margin:7px 0 0;font-style:italic}
/* ---------- Tabellen ---------- */
-.scroll{overflow-x:auto;-webkit-overflow-scrolling:touch}
-table{border-collapse:collapse;width:100%;font-size:.8rem;font-variant-numeric:tabular-nums}
-th,td{padding:5px 7px;text-align:right;border-bottom:1px solid var(--line);white-space:nowrap}
-th:first-child,td:first-child{text-align:left;white-space:normal;min-width:12em}
-thead th{color:var(--muted);font-weight:600;font-size:.74rem;text-transform:uppercase;letter-spacing:.04em;
- position:sticky;top:0;background:var(--card)}
-tr.hl td{font-weight:700}
-.note{font-size:.78rem;color:var(--muted);margin:.5em 0 0}
-.btn{appearance:none;border:1px solid var(--line);background:var(--card);color:var(--ink);font:inherit;font-size:.84rem;
- padding:8px 12px;border-radius:9px;cursor:pointer;min-height:38px}
-.btn:hover{border-color:var(--accent)}
-.btn.pri{background:var(--accent);border-color:var(--accent);color:#fff}
-@media (prefers-color-scheme: dark){.btn.pri{color:#0e1116}}
-.row{display:flex;gap:8px;flex-wrap:wrap;align-items:center}
-.warn{color:var(--bad);font-size:.82rem;font-weight:600}
-.hintbox{font-size:.8rem;line-height:1.55;background:var(--chip);border-left:3px solid var(--accent);
- border-radius:0 9px 9px 0;padding:9px 12px;margin-top:12px;color:var(--muted)}
-.hintbox b{color:var(--ink)}
-footer{color:var(--muted);font-size:.76rem;margin-top:26px;line-height:1.6}
-kbd{font:inherit;font-family:var(--mono);font-size:.85em;background:var(--chip);padding:1px 5px;border-radius:5px}
-a{color:var(--accent)}
-.pill{display:inline-block;background:var(--chip);border-radius:99px;padding:1px 8px;font-size:.75rem;color:var(--muted)}
-
-/* ---------- mitlaufende Ergebnisleiste ---------- */
-#stickybar{position:fixed;top:0;left:0;right:0;z-index:50;background:var(--card);
- border-bottom:1px solid var(--line);box-shadow:0 2px 12px rgba(16,24,40,.10);
- padding-top:env(safe-area-inset-top)}
-@media (prefers-color-scheme: dark){#stickybar{box-shadow:0 2px 12px rgba(0,0,0,.4)}}
-.sb-inner{max-width:920px;margin:0 auto;padding:6px 14px;display:flex;align-items:center;gap:10px}
+.scroll{overflow:auto;-webkit-overflow-scrolling:touch;max-height:60vh;background:var(--white);
+ border:1px solid;border-color:var(--gray) var(--white) var(--white) var(--gray);
+ box-shadow:inset 1px 1px 0 var(--black)}
+table{border-collapse:collapse;width:100%;font:11.5px var(--mono);background:var(--white)}
+th,td{padding:2px 6px;text-align:right;white-space:nowrap;border-right:1px solid var(--face-lt);
+ border-bottom:1px solid var(--face-lt)}
+th:first-child,td:first-child{text-align:left;white-space:normal;min-width:13em}
+thead th{font:700 11px var(--ui);position:sticky;top:0;z-index:1;background:var(--face);
+ border:1px solid;border-color:var(--white) var(--gray) var(--gray) var(--white)}
+tr.hl td{font-weight:700;background:var(--face-lt)}
+
+/* ---------- Mitlaufende Leiste = Statuszeile ---------- */
+#stickybar{position:fixed;top:0;left:0;right:0;z-index:50;background:var(--face);
+ padding-top:env(safe-area-inset-top);
+ border-bottom:1px solid var(--black);box-shadow:0 1px 0 var(--gray), inset 0 1px 0 var(--white)}
+.sb-inner{max-width:940px;margin:0 auto;padding:3px 8px 4px;display:flex;align-items:center;gap:6px}
.sb-col{flex:1;min-width:0}
-.sb-cap{font-size:.64rem;color:var(--muted);letter-spacing:.05em;text-transform:uppercase;
- margin-bottom:1px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
-#sb-vals{display:flex;gap:14px;min-width:0;overflow:hidden}
-.sb-i{min-width:0;flex:1}
-.sb-i .n{font-size:.66rem;color:var(--muted);font-weight:600;white-space:nowrap;
- overflow:hidden;text-overflow:ellipsis;display:block}
-.sb-i .v{font-size:.98rem;font-weight:700;font-variant-numeric:tabular-nums;letter-spacing:-.01em;
- white-space:nowrap;display:flex;align-items:baseline;gap:5px}
-.sb-i .v b{width:7px;height:7px;border-radius:99px;background:var(--c);flex:0 0 auto}
-.sb-i .d{font-size:.68rem;font-weight:600;font-variant-numeric:tabular-nums}
-.sb-i .d.up{color:var(--good)} .sb-i .d.dn{color:var(--bad)} .sb-i .d.eq{color:var(--muted)}
-.sb-up{appearance:none;border:1px solid var(--line);background:var(--bg);color:var(--muted);
- width:34px;height:34px;border-radius:99px;flex:0 0 auto;cursor:pointer;font-size:.95rem;line-height:1}
-@media(max-width:480px){ .sb-inner{padding:6px 10px;gap:6px} #sb-vals{gap:8px}
- .sb-i .v{font-size:.88rem} .sb-i .n{font-size:.62rem} }
-
-/* ---------- Klartext-Fazit und Beispielwert-Hinweis ---------- */
-.verdict{font-size:.88rem;line-height:1.55;margin:12px 0 0;padding:11px 13px;border-radius:11px;
- background:var(--chip);border-left:3px solid var(--accent)}
-.verdict b{font-weight:700}
-.verdict .band{display:block;margin-top:5px;font-size:.78rem;color:var(--muted)}
-.demobox{font-size:.82rem;line-height:1.5;margin:0 0 10px;padding:9px 12px;border-radius:10px;
- background:var(--chip);border-left:3px solid var(--bav);color:var(--muted)}
-.demobox b{color:var(--ink)}
-
-/* ---------- Tastaturbedienung sichtbar machen ---------- */
-:focus-visible{outline:2px solid var(--accent);outline-offset:2px;border-radius:6px}
+.sb-cap{font-size:10px;font-weight:700;margin-bottom:2px;white-space:nowrap;overflow:hidden;
+ text-overflow:ellipsis}
+#sb-vals{display:flex;gap:4px;min-width:0}
+.sb-i{min-width:0;flex:1;background:var(--face);padding:1px 5px;
+ border:1px solid;border-color:var(--gray) var(--white) var(--white) var(--gray)}
+.sb-i .n{font-size:10px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;display:block}
+.sb-i .v{font:700 13px var(--mono);white-space:nowrap;display:flex;align-items:baseline;gap:4px}
+.sb-i .v b{width:8px;height:8px;background:var(--c);flex:0 0 auto;border:1px solid var(--black)}
+.sb-i .d{font:700 10px var(--mono)}
+.sb-i .d.up{color:var(--good)} .sb-i .d.dn{color:var(--bad)} .sb-i .d.eq{color:var(--dim)}
+.sb-up{appearance:none;width:25px;height:25px;flex:0 0 auto;padding:0;background:var(--face);
+ color:var(--ink);font-size:11px;cursor:pointer;border:1px solid var(--black);
+ box-shadow:inset 1px 1px 0 var(--white), inset -1px -1px 0 var(--gray)}
+.sb-up:active{box-shadow:inset 1px 1px 0 var(--gray), inset -1px -1px 0 var(--white)}
+@media(max-width:480px){ .sb-inner{padding:3px 5px 4px;gap:4px} #sb-vals{gap:3px}
+ .sb-i{padding:1px 3px} .sb-i .v{font-size:11.5px} .sb-i .n{font-size:9px} }
+
+/* ---------- Bildlaufleisten ---------- */
+*{scrollbar-width:auto;scrollbar-color:var(--face) var(--face-lt)}
+::-webkit-scrollbar{width:16px;height:16px;background:
+ repeating-conic-gradient(var(--white) 0% 25%, var(--face) 0% 50%) 0 0/2px 2px}
+::-webkit-scrollbar-thumb{background:var(--face);
+ border:1px solid;border-color:var(--white) var(--black) var(--black) var(--white);
+ box-shadow:inset 1px 1px 0 var(--white), inset -1px -1px 0 var(--gray)}
+::-webkit-scrollbar-corner{background:var(--face)}
+
+footer{font-size:11px;line-height:1.5;margin-top:12px;background:var(--face);padding:8px 10px;
+ border:1px solid;border-color:var(--white) var(--gray) var(--gray) var(--white)}
+kbd{font:11px var(--mono);background:var(--face);padding:0 4px;
+ border:1px solid;border-color:var(--white) var(--gray) var(--gray) var(--white)}
+a{color:var(--navy)}
@media (prefers-reduced-motion: reduce){ *{transition:none !important;scroll-behavior:auto !important} }
</style>
</head>
@@ -186,23 +336,23 @@ a{color:var(--accent)}
</header>
<!-- ======================= ERGEBNIS ======================= -->
-<section class="card" id="result">
+<section class="win" id="w-result">
<h2>Ergebnis</h2>
<div id="metricbar" class="seg" role="group" aria-label="Kennzahl"></div>
<p class="sub" id="metricdesc" style="margin:2px 0 10px"></p>
- <p id="demo" class="demobox" hidden></p>
+ <p id="demo" class="msg demo" data-ico="?" hidden></p>
<div id="cards"></div>
<p id="verdict" class="verdict"></p>
<div id="baserow">
<span id="basetext"></span>
<button class="btn" id="pin" style="padding:4px 9px;min-height:30px;font-size:.78rem">Als Referenz merken</button>
</div>
- <p id="rankhint" class="hintbox" hidden></p>
- <p id="bisektwarn" class="warn" hidden></p>
+ <p id="rankhint" class="msg note" data-ico="i" hidden></p>
+ <p id="bisektwarn" class="msg warn" data-ico="!" hidden></p>
</section>
<!-- ======================= KERNANGABEN ======================= -->
-<section class="card">
+<section class="win">
<h2>Ihre Angaben</h2>
<p class="sub" style="margin-bottom:6px">Die Zahlen rechnen sich bei jeder Änderung sofort neu.
Mit <span class="qm" style="cursor:default">?</span> erklärt sich jedes Feld selbst.</p>
@@ -210,7 +360,7 @@ a{color:var(--accent)}
</section>
<!-- ======================= DIAGRAMME ======================= -->
-<section class="card">
+<section class="win" data-title="Diagramme">
<div class="tabs" id="tabs" role="tablist"></div>
<div id="chartwrap">
<canvas id="chart" height="260"></canvas>
@@ -221,21 +371,21 @@ a{color:var(--accent)}
</section>
<!-- ======================= WEITERE ANGABEN ======================= -->
-<section class="card">
+<section class="win">
<h2 style="margin-bottom:6px">Weitere Angaben</h2>
<p class="sub" style="margin-bottom:2px">Alles hier hat sinnvolle Voreinstellungen. Nur anfassen, wenn es auf Sie zutrifft.</p>
<div id="groups"></div>
</section>
<!-- ======================= RECHENWEG ======================= -->
-<section class="card">
+<section class="win">
<h2 style="margin-bottom:6px">Rechenweg</h2>
<p class="sub" style="margin-bottom:2px">Für alle, die es nachvollziehen wollen.</p>
<div id="details"></div>
</section>
<!-- ======================= WEITERGEBEN ======================= -->
-<section class="card">
+<section class="win">
<h2>Speichern und weitergeben</h2>
<p class="sub">Ihre Eingaben liegen ausschließlich in diesem Browser (<kbd>localStorage</kbd>). Nichts wird übertragen,
es gibt keinen Server und keine Tracker.</p>
@@ -247,7 +397,7 @@ a{color:var(--accent)}
</div>
<input type="file" id="file" accept=".json,application/json" hidden>
<p class="sub" id="sharemsg" style="margin-top:8px"></p>
- <p class="hintbox" id="installhint" hidden></p>
+ <p class="msg note" data-ico="i" id="installhint" hidden></p>
</section>
<footer>
@@ -659,11 +809,11 @@ root.AVModell = { rechne:rechne, waterfall:waterfall, bruttoVonNetto:bruttoVonNe
/* nom/satz/gen: das Fazit ist ein deutscher Satz, kein Etikettensalat.
„mehr als privates ETF-Depot“ liest sich wie eine Tabellenzeile. */
var OPTS = [
- { k:'bav', name:'bAV (Entgeltumwandlung)', short:'bAV', color:'var(--bav)', raw:'#c9761d',
+ { k:'bav', name:'bAV (Entgeltumwandlung)', short:'bAV', color:'var(--bav)', raw:'#800000',
nom:'die betriebliche Altersvorsorge', satz:'Die betriebliche Altersvorsorge', gen:'der bAV' },
- { k:'av', name:'Altersvorsorgedepot', short:'AV-Depot', color:'var(--av)', raw:'#2266b8',
+ { k:'av', name:'Altersvorsorgedepot', short:'AV-Depot', color:'var(--av)', raw:'#000080',
nom:'das Altersvorsorgedepot', satz:'Das Altersvorsorgedepot', gen:'des Altersvorsorgedepots' },
- { k:'priv', name:'privates ETF-Depot', short:'privat', color:'var(--priv)', raw:'#2b8a5a',
+ { k:'priv', name:'privates ETF-Depot', short:'privat', color:'var(--priv)', raw:'#008000',
nom:'das private ETF-Depot', satz:'Das private ETF-Depot', gen:'des privaten ETF-Depots' }
];
function opt(k){ return OPTS.filter(function(o){return o.k===k;})[0]; }
@@ -989,14 +1139,26 @@ function buildField(f){
wrap.appendChild(lab);
if (f.kind === 'seg'){
- var seg = el('div','seg'); seg.style.gridColumn = '1/-1'; seg.style.marginTop = '4px';
+ /* Windows 3.1 kannte keine Segmentschalter — die richtige Entsprechung
+ fuer eine Auswahl unter wenigen Alternativen ist die Optionsfeldgruppe.
+ Sie ist zugleich das semantisch korrekte Element und bringt die
+ Pfeiltastennavigation der Radiogruppe geschenkt mit. */
+ var box = el('div','radios');
+ var gname = 'r_' + f.src + '_' + f.k;
f.opts.forEach(function(o){
- var b = el('button', null, o[1]); b.type='button';
- b.setAttribute('aria-pressed', fieldValue(f) === o[0]);
- b.onclick = function(){ arm(f.k); setFieldValue(f, o[0]); refresh(); };
- seg.appendChild(b);
+ var lb = document.createElement('label');
+ var rb = document.createElement('input');
+ rb.type = 'radio'; rb.name = gname; rb.value = String(o[0]);
+ rb.checked = fieldValue(f) === o[0];
+ rb.addEventListener('change', function(){
+ if (!rb.checked) return;
+ arm(f.k); setFieldValue(f, o[0]); refresh();
+ });
+ lb.appendChild(rb);
+ lb.appendChild(document.createTextNode(' ' + o[1]));
+ box.appendChild(lb);
});
- wrap.appendChild(seg);
+ wrap.appendChild(box);
} else {
var val = el('div','val');
var inp = document.createElement('input');
@@ -1091,8 +1253,8 @@ function syncVisibility(){
if (w._sync) w._sync();
if (w._live && R) w._live(R);
if (f.kind === 'seg'){
- var bs = w.querySelectorAll('.seg button');
- for (var i=0;i<bs.length;i++) bs[i].setAttribute('aria-pressed', fieldValue(f) === f.opts[i][0]);
+ var bs = w.querySelectorAll('.radios input');
+ for (var i=0;i<bs.length;i++) bs[i].checked = (fieldValue(f) === f.opts[i][0]);
}
});
}
@@ -1124,7 +1286,7 @@ function renderCards(){
vals.forEach(function(v,i){ if (isFinite(v) && v > best){ best = v; bi = i; } });
OPTS.forEach(function(o,i){
- var c = el('div','opt' + (i===bi?' win':''));
+ var c = el('div','opt' + (i===bi?' lead':''));
c.style.setProperty('--c', o.raw);
var nm = el('div','nm'); nm.appendChild(document.createTextNode(o.name));
if (i===bi){ var cr = el('span','crown','vorn'); cr.style.setProperty('--c', o.raw); nm.appendChild(cr); }
@@ -1340,22 +1502,26 @@ function niceTicks(lo, hi, n){
for(var v=s; v<=hi+step*1e-9; v+=step) t.push(Math.abs(v)<step*1e-9?0:v);
return t;
}
+var UIFONT = '11px "MS Sans Serif","Microsoft Sans Serif",Tahoma,Verdana,sans-serif';
function axes(g,w,h,pad,xr,yr,fmtY,fmtX,xt){
- var ink=css('--ink'), muted=css('--muted'), line=css('--line');
+ var ink=css('--ink'), grid=css('--gray');
var yt = niceTicks(yr[0], yr[1], 5);
- g.font = '11px system-ui, sans-serif'; g.textBaseline='middle';
+ g.font = UIFONT; g.textBaseline='middle';
yt.forEach(function(v){
- var y = pad.t + (1-(v-yr[0])/(yr[1]-yr[0]))*(h-pad.t-pad.b);
- g.strokeStyle = Math.abs(v)<1e-9 ? muted : line;
- g.lineWidth = Math.abs(v)<1e-9 ? 1 : 1;
+ var y = Math.round(pad.t + (1-(v-yr[0])/(yr[1]-yr[0]))*(h-pad.t-pad.b));
+ var null0 = Math.abs(v) < 1e-9;
+ g.strokeStyle = null0 ? ink : grid;
+ g.lineWidth = 1;
+ if (!null0) g.setLineDash([1,2]); // gepunktetes Raster wie in Excel 4.0
g.beginPath(); g.moveTo(pad.l, y+.5); g.lineTo(w-pad.r, y+.5); g.stroke();
- g.fillStyle = muted; g.textAlign='right';
+ g.setLineDash([]);
+ g.fillStyle = ink; g.textAlign='right';
g.fillText(fmtY(v), pad.l-6, y);
});
if (xt){
- g.textAlign='center'; g.textBaseline='top'; g.fillStyle=muted;
+ g.textAlign='center'; g.textBaseline='top'; g.fillStyle=ink;
xt.forEach(function(v){
- var x = pad.l + (v-xr[0])/(xr[1]-xr[0])*(w-pad.l-pad.r);
+ var x = Math.round(pad.l + (v-xr[0])/(xr[1]-xr[0])*(w-pad.l-pad.r));
g.fillText(fmtX(v), x, h-pad.b+6);
});
}
@@ -1467,11 +1633,11 @@ function drawSens(){
var Y = function(v){ return pad.t + (1-(v-yr[0])/(yr[1]-yr[0]))*(h-pad.t-pad.b); };
// Marke am aktuellen Wert
- g.strokeStyle = css('--muted'); g.setLineDash([4,4]); g.lineWidth = 1;
+ g.strokeStyle = css('--ink'); g.setLineDash([3,3]); g.lineWidth = 1;
g.beginPath(); g.moveTo(X(s.cur), pad.t); g.lineTo(X(s.cur), h-pad.b); g.stroke(); g.setLineDash([]);
OPTS.forEach(function(o){
- g.strokeStyle = o.raw; g.lineWidth = 2.2; g.lineJoin='round'; g.beginPath();
+ g.strokeStyle = o.raw; g.lineWidth = 2; g.lineJoin='miter'; g.beginPath();
s.pts[o.k].forEach(function(p,i){ var x=X(p[0]), y=Y(p[1]); if(i) g.lineTo(x,y); else g.moveTo(x,y); });
g.stroke();
var cp = nearest(s.pts[o.k], s.cur);
@@ -1488,7 +1654,7 @@ function drawSens(){
var yv = s.pts[k][i-1][1] + t*(s.pts[k][i][1]-s.pts[k][i-1][1]);
g.strokeStyle = css('--ink'); g.lineWidth=1.5;
g.beginPath(); g.arc(X(xv), Y(yv), 5.5, 0, 6.284); g.stroke();
- g.fillStyle = css('--ink'); g.font='600 10px system-ui,sans-serif'; g.textAlign='center'; g.textBaseline='bottom';
+ g.fillStyle = css('--ink'); g.font='bold '+UIFONT; g.textAlign='center'; g.textBaseline='bottom';
g.fillText(fmtX(xv), X(xv), Y(yv)-8);
}
}
@@ -1496,8 +1662,8 @@ function drawSens(){
if (hoverX != null){
var xv2 = xr[0] + clamp((hoverX-pad.l)/(w-pad.l-pad.r),0,1)*(xr[1]-xr[0]);
- g.strokeStyle = css('--ink'); g.globalAlpha=.35; g.lineWidth=1;
- g.beginPath(); g.moveTo(X(xv2), pad.t); g.lineTo(X(xv2), h-pad.b); g.stroke(); g.globalAlpha=1;
+ g.strokeStyle = css('--ink'); g.globalAlpha=1; g.lineWidth=1; g.setLineDash([1,1]);
+ g.beginPath(); g.moveTo(X(xv2), pad.t); g.lineTo(X(xv2), h-pad.b); g.stroke(); g.setLineDash([]);
var out = [f.label + ' = ' + formatViewNumber(f, vw(xv2)) + ' ' + fieldUnit(f) + ' → '];
OPTS.forEach(function(o){ out.push(o.short + ' ' + M.fmt(nearest(s.pts[o.k], xv2)[1])); });
$('#readout').textContent = out[0] + out.slice(1).join(' · ');
@@ -1559,9 +1725,9 @@ function drawWaterfall(){
var barL = 8, barR = w-8, avail = barR-barL;
var X = function(v){ return barL + (v-lo)/(hi-lo)*avail; };
- g.font = '11px system-ui,sans-serif';
+ g.font = UIFONT;
var zero = X(0);
- g.strokeStyle = css('--muted'); g.globalAlpha=.5;
+ g.strokeStyle = css('--gray'); g.globalAlpha=1;
g.beginPath(); g.moveTo(zero+.5, pad.t); g.lineTo(zero+.5, h-pad.b); g.stroke(); g.globalAlpha=1;
run = 0;
@@ -1575,13 +1741,13 @@ function drawWaterfall(){
g.fillStyle = col; g.globalAlpha = it.type==='abs'?1:.85;
g.fillRect(xa, y, Math.max(2, xb-xa), bh); g.globalAlpha=1;
if (it.type==='rel' && i<items.length-1){
- g.strokeStyle = css('--line'); g.setLineDash([2,3]);
+ g.strokeStyle = css('--gray'); g.setLineDash([1,2]);
g.beginPath(); g.moveTo(X(b), y+bh); g.lineTo(X(b), y+rowH); g.stroke(); g.setLineDash([]);
}
g.fillStyle = css('--ink'); g.textAlign='left'; g.textBaseline='bottom';
- g.font = it.type==='abs' ? '600 11.5px system-ui,sans-serif' : '11.5px system-ui,sans-serif';
+ g.font = it.type==='abs' ? 'bold '+UIFONT : UIFONT;
g.fillText(it.label, 8, y-2);
- g.textAlign='right'; g.font='600 11.5px system-ui,sans-serif';
+ g.textAlign='right'; g.font='bold '+UIFONT;
g.fillStyle = it.type==='abs' ? css('--ink') : (it.wert>=0?css('--good'):css('--bad'));
var t = (it.type==='rel'?(it.wert>=0?'+':'−'):'') + nf0.format(Math.round(Math.abs(it.wert))) + ' €';
g.fillText(t, w-8, y-2);
@@ -1631,13 +1797,13 @@ function drawKapital(){
axes(g,w,h,pad,xr,yr,function(v){return num(v/1000,0)+'k';}, function(v){return num(v,0);},
niceTicks(a0, xhi, 6).filter(function(v){return v>=a0&&v<=xhi;}));
- g.strokeStyle = css('--muted'); g.setLineDash([4,4]);
+ g.strokeStyle = css('--ink'); g.setLineDash([3,3]);
g.beginPath(); g.moveTo(X(S.I.rentbeg), pad.t); g.lineTo(X(S.I.rentbeg), h-pad.b); g.stroke(); g.setLineDash([]);
- g.fillStyle = css('--muted'); g.font='10px system-ui,sans-serif'; g.textAlign='left'; g.textBaseline='top';
+ g.fillStyle = css('--ink'); g.font=UIFONT; g.textAlign='left'; g.textBaseline='top';
g.fillText('Rentenbeginn', X(S.I.rentbeg)+4, pad.t+2);
OPTS.forEach(function(o){
- g.strokeStyle=o.raw; g.lineWidth=2.2; g.lineJoin='round'; g.beginPath();
+ g.strokeStyle=o.raw; g.lineWidth=2; g.lineJoin='miter'; g.beginPath();
series[o.k].forEach(function(p,i){ var x=X(p[0]), y=Y(p[1]); if(i) g.lineTo(x,y); else g.moveTo(x,y); });
g.stroke();
});
@@ -1666,20 +1832,20 @@ function drawZerlegung(){
if (!(maxV > 0) || !isFinite(maxV)) maxV = 1;
var pad = { l:8, r:8, t:22, b:8 };
var colW = (w-16)/3, barH = 26;
- g.font='11px system-ui,sans-serif';
+ g.font=UIFONT;
OPTS.forEach(function(o,i){
var x0 = 8 + i*colW + 6, bw = colW-12;
var y = pad.t;
- g.fillStyle=css('--muted'); g.textAlign='center'; g.textBaseline='alphabetic';
- g.font='600 11px system-ui,sans-serif';
+ g.fillStyle=css('--ink'); g.textAlign='center'; g.textBaseline='alphabetic';
+ g.font='bold '+UIFONT;
g.fillText(o.short, x0+bw/2, 14);
// gestapelte Abzüge vom Brutto
var brutto = R.monatBrutto[o.k];
var segs = [
{ v: brutto + R.monatSteuer[o.k] + R.monatKV[o.k] + R.monatEP[o.k], c:o.raw, l:'netto' },
- { v: -R.monatSteuer[o.k], c:'#8a8f98', l:'Steuer' },
- { v: -R.monatKV[o.k], c:'#c0574f', l:'KV/PV' },
- { v: -R.monatEP[o.k], c:'#7a6ab0', l:'Rente' }
+ { v: -R.monatSteuer[o.k], c:'#808080', l:'Steuer' },
+ { v: -R.monatKV[o.k], c:'#800000', l:'KV/PV' },
+ { v: -R.monatEP[o.k], c:'#800080', l:'Rente' }
];
var acc = 0;
var scale = (h-pad.t-pad.b-24)/(maxV||1);
@@ -1688,17 +1854,17 @@ function drawZerlegung(){
var hgt = s.v*scale;
g.fillStyle = s.c; g.fillRect(x0, h-pad.b-24-acc-hgt, bw, hgt);
if (hgt > 15){
- g.fillStyle='#fff'; g.font='10px system-ui,sans-serif'; g.textAlign='center';
+ g.fillStyle='#fff'; g.font=UIFONT; g.textAlign='center';
g.fillText(s.l + ' ' + nf0.format(Math.round(s.v)), x0+bw/2, h-pad.b-24-acc-hgt/2+3);
}
acc += hgt;
});
- g.fillStyle=css('--ink'); g.font='700 12px system-ui,sans-serif'; g.textAlign='center';
+ g.fillStyle=css('--ink'); g.font='bold 12px "MS Sans Serif",Tahoma,Verdana,sans-serif'; g.textAlign='center';
g.fillText(eur2(R.monat[o.k]), x0+bw/2, h-pad.b-8);
});
- $('#legend').innerHTML = '<span><i style="background:#8a8f98"></i>Steuer</span>'
- + '<span><i style="background:#c0574f"></i>KV/PV</span>'
- + '<span><i style="background:#7a6ab0"></i>entgangene gesetzliche Rente</span>'
+ $('#legend').innerHTML = '<span><i style="background:#808080"></i>Steuer</span>'
+ + '<span><i style="background:#800000"></i>KV/PV</span>'
+ + '<span><i style="background:#800080"></i>entgangene gesetzliche Rente</span>'
+ '<span style="margin-left:auto">farbiger Sockel = das, was übrig bleibt</span>';
$('#readout').textContent = 'Alles als konstante Monatsleistung in heutiger Kaufkraft. Weil Annuitisierung linear ist, '
+ 'addieren sich die Blöcke exakt.';
@@ -1722,7 +1888,7 @@ function buildTabs(){
host.appendChild(b);
});
if (S.tab==='wf'){
- var sw = el('div','seg'); sw.style.marginLeft='auto';
+ var sw = el('div','seg btnseg'); sw.style.marginLeft='auto';
[['bav','bAV'],['av','AV-Depot']].forEach(function(o){
var b = el('button', null, o[1]); b.type='button';
b.setAttribute('aria-pressed', S.wfZiel===o[0]);
@@ -1972,7 +2138,7 @@ $('#file').onchange = function(e){
var o = JSON.parse(rd.result);
if (o.I) for (var k in AVModell.DEFAULT_INPUT) if (typeof o.I[k]==='number') S.I[k]=o.I[k];
if (o.P) for (var p in AVModell.DEFAULT_PARAM) if (typeof o.P[p]==='number') S.P[p]=o.P[p];
- BASE = null; PINNED = false; armed = null;
+ BASE = null; PINNED = false; armed = null; koppelWarn = null;
buildInputs(); refresh();
$('#sharemsg').textContent = 'Geladen.';
} catch(err){ $('#sharemsg').textContent = 'Die Datei ließ sich nicht lesen: ' + err.message; }
@@ -1982,22 +2148,72 @@ $('#file').onchange = function(e){
$('#reset').onclick = function(){
S.I = Object.assign({}, AVModell.DEFAULT_INPUT);
S.P = Object.assign({}, AVModell.DEFAULT_PARAM);
- BASE = null; PINNED = false; armed = null;
+ BASE = null; PINNED = false; armed = null; koppelWarn = null;
try { localStorage.removeItem(LSKEY); } catch(e){}
history.replaceState(null,'',location.pathname);
buildInputs(); refresh();
$('#sharemsg').textContent = 'Zurückgesetzt auf den Basisfall.';
};
+/* Es gibt nur ein Farbschema. Silber auf Petrol ist hier nicht Dekoration,
+ sondern die Sache selbst — eine Dunkelvariante gäbe es nicht zu skinnen. */
var rt = null;
window.addEventListener('resize', function(){ clearTimeout(rt); rt = setTimeout(drawChart, 120); });
-if (window.matchMedia) {
- var mq = window.matchMedia('(prefers-color-scheme: dark)');
- (mq.addEventListener ? mq.addEventListener.bind(mq,'change') : mq.addListener.bind(mq))(function(){ drawChart(); });
+
+/* =====================================================================
+ TEIL 12 — FENSTERRAHMEN
+ Jeder Abschnitt bekommt eine Titelleiste im Stil von Windows 3.1:
+ Systemmenue-Kasten links, Titel, Minimieren rechts. Der Knopf ist
+ nicht Zierrat — er klappt den Fensterinhalt tatsaechlich ein, was auf
+ dem Telefon nuetzlich ist. Der Zustand wird mitgespeichert.
+ ===================================================================== */
+function dressWindows(){
+ var host = document.querySelector('.wrap');
+ var wins = [].slice.call(host.querySelectorAll(':scope > header, :scope > section.win'));
+ wins.forEach(function(sec, i){
+ if (sec.querySelector(':scope > .titlebar')) return;
+ var isHeader = sec.tagName === 'HEADER';
+ if (isHeader) sec.className = 'win';
+
+ var h = sec.querySelector('h1, h2');
+ var title = sec.getAttribute('data-title')
+ || (h ? h.textContent.replace(/‑/g, '-') : 'Fenster ' + (i+1));
+ if (h) h.remove();
+
+ var key = 'w_' + (sec.id || ('n' + i));
+ var body = el('div','winbody');
+ while (sec.firstChild) body.appendChild(sec.firstChild);
+
+ var bar = el('div','titlebar');
+ bar.appendChild(el('span','sysbox'));
+ bar.appendChild(el('span','tbtext', title));
+ var mn = el('button','tbbtn','▬');
+ mn.type = 'button';
+ mn.title = 'Minimieren';
+ mn.setAttribute('aria-expanded', 'true');
+ bar.appendChild(mn);
+
+ sec.appendChild(bar); sec.appendChild(body);
+
+ var setRolled = function(r, save){
+ sec.classList.toggle('rolled', r);
+ mn.textContent = r ? '■' : '▬';
+ mn.title = r ? 'Wiederherstellen' : 'Minimieren';
+ mn.setAttribute('aria-expanded', String(!r));
+ S.open[key] = !r;
+ if (save) saveState();
+ if (!r) drawChart(); // eingeklapptes Canvas hat Breite 0
+ };
+ mn.onclick = function(){ setRolled(!sec.classList.contains('rolled'), true); };
+ /* Doppelklick auf die Titelleiste klappt ein - wie unter Windows */
+ bar.ondblclick = function(e){ if (e.target !== mn) mn.onclick(); };
+ if (S.open[key] === false) setRolled(true, false);
+ });
}
/* --- Start --- */
loadState();
+dressWindows();
buildMetricBar(); buildTabs(); buildInputs();
R = AVModell.rechne(S.I, S.P);
refresh();
diff --git a/app/manifest.json b/app/manifest.json
index fd416dd..28baa45 100644
--- a/app/manifest.json
+++ b/app/manifest.json
@@ -8,12 +8,31 @@
"scope": ".",
"display": "standalone",
"orientation": "portrait-primary",
- "background_color": "#f6f7fa",
- "theme_color": "#1f3864",
- "categories": ["finance", "utilities", "productivity"],
+ "background_color": "#008080",
+ "theme_color": "#000080",
+ "categories": [
+ "finance",
+ "utilities",
+ "productivity"
+ ],
"icons": [
- { "src": "icon-192.png", "sizes": "192x192", "type": "image/png", "purpose": "any" },
- { "src": "icon-512.png", "sizes": "512x512", "type": "image/png", "purpose": "any" },
- { "src": "icon-maskable-512.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" }
+ {
+ "src": "icon-192.png",
+ "sizes": "192x192",
+ "type": "image/png",
+ "purpose": "any"
+ },
+ {
+ "src": "icon-512.png",
+ "sizes": "512x512",
+ "type": "image/png",
+ "purpose": "any"
+ },
+ {
+ "src": "icon-maskable-512.png",
+ "sizes": "512x512",
+ "type": "image/png",
+ "purpose": "maskable"
+ }
]
-}
+} \ No newline at end of file
diff --git a/app/service-worker.js b/app/service-worker.js
index 18c0348..103ea88 100644
--- a/app/service-worker.js
+++ b/app/service-worker.js
@@ -9,7 +9,7 @@
* Beim Ändern der App die Version hochzählen — sonst sieht ein bereits
* installierter Client die Änderung erst nach zwei Aufrufen.
*/
-const VERSION = 'av-v3';
+const VERSION = 'av-v4';
const ASSETS = [
'./',
'./index.html',