Meldungen entschlackt, Bild-Modalbox, neues Muster, kleinere Trade-Schrift, M15 eingeklappt (v=207)

Fuenf User-Vorgaben in einem Durchgang.

(1) HYPERLIQUID + MUSTER + TRAILING AUS DER MELDUNGEN-KARTE. Die drei Divs sind
ENTFERNT, nicht nur versteckt; die Render-Bloecke bleiben stehen (null-sicher),
damit ein Zurueckholen eine reine HTML-Aenderung ist. Backend rechnet
unveraendert weiter (hl_ctx, patterns_tf, trail_info stehen im Snapshot).
Stufe F bekam eine dokumentierte Ausnahmeliste - sonst haette sie drei
Dauer-Treffer gemeldet, und eine Pruefung mit Dauer-Treffern wird ignoriert.

⚠⚠ DABEI ZWEI ECHTE ABSTUERZE GEFUNDEN - und der Weg dahin ist der Punkt.
`hl-note` wurde ueber das Auffang-`$` geholt; nach dem Entfernen des Elements
griffen ZWEI Zweige ungeschuetzt auf `hlEl.classList` zu. Im Browser ein
TypeError mitten im Render = das GESAMTE Dashboard friert auf Altwerten ein.
Die Render-Probe blieb zunaechst GRUEN - ihr eigener blinder Fleck: die
DOM-Attrappe lieferte fuer JEDE ID ein Element, `$streng` konnte darin also NIE
`null` zurueckgeben. Genau der Fall, fuer den es `$streng` gibt, war ungeprueft.
Behoben: die Attrappe kennt jetzt die echten IDs aus index.html
(`getElementById: id => HTML_IDS.has(id) ? el() : null`) - und meldete die
beiden Zweige im selben Lauf. Danach beide Waechter ergaenzt.

(2) BILD-MODALBOX. Klick auf ein Chartmuster oeffnet es gross.
⚠⚠ BEWUSST OHNE BIBLIOTHEK: ein Lightbox-Paket waere 15-40 KB plus ein zweites
Bedienkonzept fuer ~30 Zeilen Code. Das Dashboard laeuft nur ueber WireGuard,
ein CDN ist gar nicht erreichbar; das Projekt hat aus demselben Grund Tailwind
abgelehnt und am 05.08. die vendorte lightweight-charts (164 KB) entfernt.
Delegierter Klick auf `document`, weil die Kacheln bei JEDEM Render neu erzeugt
werden - ein Handler am <img> waere nach dem naechsten Snapshot weg.
Grosse Fassung (1408 px, ~133 KB) liegt getrennt unter muster/gross/ und wird
ERST beim Klick geladen; die Kachel bleibt bei 25 KB. Esc und Klick auf den
Hintergrund schliessen.

(3) NEUES BILD gescannt: "Inverse Tasse+Henkel.jpg". `inv_cup` hatte bis dahin
ausdruecklich KEIN Bild ("lieber keins als ein falsches") - jetzt zugeordnet.
Damit 16 Muster, keins ohne Datei, keine Datei ohne Zuordnung.

(4) SCHRIFT IN DER TRADE-LEISTE um 2 px (bezogen auf die 18-px-Referenz)
verkleinert - 4 Regeln: Ueberschrift, Feldbeschriftungen, Werte/Eingaben,
Knoepfe.
⚠ Erster Versuch nahm alles zwischen `.tb-title{` und `.mg-row` und traf 80
Regeln statt 4 - die halbe Oberflaeche. Zurueckgesetzt und namentlich
aufgezaehlt. Ein Zeilenbereich ist kein Selektor.

(5) M15-KARTE PER DEFAULT EINGEKLAPPT. Das `open` am ersten <details> ist weg.
⚠ Zusaetzlich der localStorage-Schluessel gebumpt (oil_m15_akk -> oil_m15_akk2):
bei jedem, der die Karte je aufgeklappt hatte, stand dort `true`, und das
gewinnt gegen den neuen Default. Ein neuer Schluessel setzt genau EINMAL
zurueck; danach merkt sich die Karte die eigene Wahl wie bisher.

Pipeline A-G gruen, 97 Tests, 14 render*-Funktionen (Fixture UND leerer
Snapshot), deployt und am ausgelieferten HTML/JS verifiziert.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Axel Hocks
2026-08-20 22:27:18 +02:00
co-authored by Claude Opus 5
parent 06024dd0c1
commit 71abb49cf7
23 changed files with 155 additions and 21 deletions
+5 -1
View File
@@ -321,12 +321,16 @@ def _detect(piv, cur, atr, last_idx):
# Verkleinert: 7,5 MB -> 379 KB fuer alle 15.
# ⚠ Doppeltop und Doppelboden teilen sich ein Bild (die Vorlage zeigt beide),
# ebenso Dreifach-Top/-Boden und Flagge bullisch/baerisch.
# ⚠ `inv_cup` hat KEIN eigenes Bild — lieber keins als ein falsches.
# ⚠ `inv_cup` hat seit dem 20.08. ein eigenes Bild (vorher bewusst keins).
BILD = {
"double_top": "double", "double_bottom": "double",
"triple_top": "triple", "triple_bottom": "triple",
"hs": "hs", "inv_hs": "inv_hs",
"cup": "cup",
# ⚠ Am 2026-08-20 nachgereicht (User: "scanne neue Bilder"). Bis dahin
# stand hier ausdruecklich KEIN Eintrag — lieber kein Bild als ein
# falsches; die normale Tasse zu zeigen waere die falsche Richtung gewesen.
"inv_cup": "inv_cup",
"flag": "flag", "pennant": "pennant",
"wedge_rise": "wedge_rise", "wedge_fall": "wedge_fall",
"tri_asc": "tri_asc", "tri_desc": "tri_desc", "tri_sym": "tri_sym",
+9 -1
View File
@@ -478,7 +478,15 @@ def _stufe_ids():
js = re.sub(r"(?m)^\s*//.*$", "", js)
html_ids = re.findall(r'id="([^"]+)"', html)
doppelt = sorted({i for i in html_ids if html_ids.count(i) > 1})
fehlt = sorted(set(re.findall(r'\$(?:streng)?\("([^"]+)"\)', js)) - set(html_ids))
# ⚠⚠ ABSICHTLICH ausgeblendete Elemente. Der JS-Block bleibt stehen —
# er ist null-sicher (`$streng` + Waechter) — damit ein Zurueckholen eine
# reine HTML-Aenderung ist. Ohne diese Liste meldete die Stufe drei
# Dauer-Treffer, und eine Pruefung mit Dauer-Treffern wird ignoriert (die
# Lehre vom Linter). ⚠ Wer hier etwas eintraegt, MUSS den Zugriff im JS
# auf `$streng` + Waechter umstellen — sonst bricht der GESAMTE Render.
AUSGEBLENDET = {"hl-note", "pat-note", "trail-note"} # User-Wunsch 2026-08-20
fehlt = sorted(set(re.findall(r'\$(?:streng)?\("([^"]+)"\)', js))
- set(html_ids) - AUSGEBLENDET)
for i in fehlt:
befunde.append(f"ID verwaist: $(\"{i}\") ohne Element in index.html")
print(f" FEHLER verwaiste ID: {i}")
+17 -1
View File
@@ -26,6 +26,14 @@ import vm from "node:vm";
const pfad = process.argv[2] || "tests/fixtures/snapshot.json";
const snap = JSON.parse(fs.readFileSync(pfad, "utf-8"));
const src = fs.readFileSync("web/app.js", "utf-8");
// Die real vorhandenen Element-IDs aus der ausgelieferten HTML.
const HTML_IDS = new Set(
[...fs.readFileSync("web/index.html", "utf-8").matchAll(/id="([^"]+)"/g)]
.map((m) => m[1]));
if (HTML_IDS.size < 50) {
console.log(` ❌ nur ${HTML_IDS.size} IDs aus index.html gelesen — Probe greift nicht`);
process.exit(1);
}
function el() {
const o = {
@@ -44,7 +52,15 @@ function el() {
const ctx = {
console,
document: {
getElementById: () => el(), querySelector: () => el(),
// ⚠⚠ NUR IDs, die es in `index.html` wirklich gibt (Fix 2026-08-20).
// Vorher lieferte die Attrappe fuer JEDE ID ein Element — damit konnte
// `$streng()` in der Probe NIE `null` zurueckgeben, und genau der Fall,
// fuer den es `$streng` gibt, war ungeprueft. Real aufgefallen beim
// Ausblenden von `#hl-note`: der Hauptzweig griff ungeschuetzt auf
// `hlEl.classList` zu, die Probe blieb gruen, im Browser haette es den
// GESAMTEN Render abgebrochen.
getElementById: (id) => (HTML_IDS.has(id) ? el() : null),
querySelector: () => el(),
querySelectorAll: () => [], createElement: () => el(),
addEventListener() {}, body: el(), documentElement: el(),
activeElement: null, hidden: false, visibilityState: "visible",
+66 -6
View File
@@ -748,9 +748,19 @@ function renderKontext(snap) {
trN.textContent = trTxt || "";
trN.classList.toggle("hidden", !trTxt);
}
const hlEl = $("hl-note");
// ⚠ `$streng`, nicht das Auffang-`$`: das Element ist am 2026-08-20 auf
// User-Wunsch aus der Meldungen-Karte ENTFERNT. Mit dem Auffang-`$` liefe
// der Block ins Leere und meldete bei jedem Laden eine Konsolen-Warnung —
// ein Dauer-Fehlalarm fuer einen gewollten Zustand.
const hlEl = $streng("hl-note");
const h = snap && snap.hl_ctx;
if (h && h.premium != null) {
// ⚠⚠ `hlEl` MUSS mitgeprueft werden. Das Element ist am 2026-08-20 auf
// User-Wunsch entfernt worden; ohne diese Bedingung wirft `hlEl.classList`
// einen TypeError — und der bricht den GESAMTEN Render ab (Dashboard
// friert auf Altwerten ein). Die else-Zweige hatten den Waechter, der
// Hauptzweig nicht. Gefunden von der Render-Probe, nachdem deren eigener
// blinder Fleck behoben war (die Attrappe gab fuer JEDE ID ein Element).
if (hlEl && h && h.premium != null) {
const fr = (h.funding != null) ? (h.funding * 100).toFixed(4) + " %" : "—";
const oi = (h.open_interest != null)
? (h.open_interest / 1e6).toFixed(2) + " Mio" : "—";
@@ -761,7 +771,7 @@ function renderKontext(snap) {
+ "der Perp notiert über dem Index, Longs zahlen. ⚠ Gemessen über 5.095 "
+ "Funding-Stundenwerte (7 Monate) gegen den CFD: kein belastbarer "
+ "Prognosewert.";
} else {
} else if (hlEl) {
hlEl.classList.add("hidden");
}
}
@@ -1013,6 +1023,49 @@ function renderCone(cn) {
// ⚠ Bilder mit `loading="lazy"`: die 15 Vorlagen liegen verkleinert bei ~25 KB
// (Originale 436611 KB), aber über WireGuard soll nur geladen werden, was
// wirklich sichtbar ist.
// ══ BILD-MODALBOX für die Chartmuster (User-Wunsch 2026-08-20) ═════════════
// ⚠⚠ BEWUSST OHNE BIBLIOTHEK — Begründung im Markup (`#bildbox`). Kurz: ein
// Lightbox-Paket wäre 1540 KB plus ein zweites Bedienkonzept, das Dashboard
// läuft nur über WireGuard (kein CDN erreichbar), und das Projekt hat aus
// demselben Grund Tailwind abgelehnt und die vendorte Chart-Bibliothek
// entfernt. Was hier fehlt, sind ~30 Zeilen.
// ⚠ Delegierter Klick auf `document`: die Kacheln werden bei JEDEM Render neu
// erzeugt (`innerHTML = …`). Ein Handler direkt am `<img>` wäre nach dem
// nächsten Snapshot weg — genau die Sorte Fehler, die nur sporadisch auffällt.
function bildboxOeffnen(bild, text) {
const box = $streng("bildbox"), img = $streng("bildbox-img");
if (!box || !img) return;
// Grosse Fassung (~133 KB) erst JETZT laden, nicht mit dem Dashboard.
img.src = `muster/gross/${bild}.jpg`;
img.alt = text || "";
const t = $streng("bildbox-txt");
if (t) t.textContent = text || "";
box.classList.remove("hidden");
}
function bildboxSchliessen() {
const box = $streng("bildbox");
if (box) box.classList.add("hidden");
}
document.addEventListener("click", (e) => {
const t = e.target;
if (t && t.classList && t.classList.contains("pt-img") && t.dataset
&& t.dataset.bild) {
bildboxOeffnen(t.dataset.bild, t.dataset.txt || t.alt || "");
return;
}
// Klick auf den Hintergrund ODER den Schliessen-Knopf schliesst.
const box = $streng("bildbox");
if (box && !box.classList.contains("hidden")
&& (t === box || (t && t.id === "bildbox-zu"))) {
bildboxSchliessen();
}
});
document.addEventListener("keydown", (e) => {
if (e.key === "Escape") bildboxSchliessen();
});
function renderPatternsTF(d) {
const el = $streng("pt-tfgrid");
if (!el) return;
@@ -1028,7 +1081,9 @@ function renderPatternsTF(d) {
const arr = m.dir === "bull" ? "▲" : m.dir === "bear" ? "▼" : "◆";
const best = m.status === "confirmed";
const bild = m.bild
? `<img class="pt-img" src="muster/${m.bild}.jpg" alt="${m.name}" loading="lazy">`
? `<img class="pt-img" src="muster/${m.bild}.jpg" alt="${m.name}" `
+ `data-bild="${m.bild}" data-txt="${tf} · ${m.name}" loading="lazy" `
+ `title="Klicken öffnet das Bild in voller Größe.">`
: `<div class="pt-img leer">kein Bild</div>`;
const q = (m.quality != null ? m.quality : 0).toFixed(2);
return `<div class="pt-tfcell${best ? " best" : ""}">`
@@ -2405,7 +2460,12 @@ async function setMgGlobal() {
// Ein zugeklapptes <details> ist nur nicht sichtbar - `textContent` wird
// weiter gesetzt, und die naechste Oeffnung zeigt den aktuellen Stand.
{
const KEY = "oil_m15_akk";
// ⚠⚠ SCHLUESSEL GEBUMPT (2026-08-20). Der Default ist jetzt
// "eingeklappt" — aber im `localStorage` steht bei jedem, der die Karte je
// aufgeklappt hatte, noch `true`, und das gewinnt gegen den neuen Default.
// Ein neuer Schluessel setzt genau EINMAL zurueck; danach merkt sich die
// Karte die eigene Wahl wie bisher.
const KEY = "oil_m15_akk2";
let stand = {};
try { stand = JSON.parse(localStorage.getItem(KEY) || "{}"); } catch (e) { stand = {}; }
document.querySelectorAll("details.m15-akk").forEach(el => {
@@ -2459,7 +2519,7 @@ async function closeSlot(slot, label, pnl) {
// und der Browser zeigte Altes. Statt das jedes Mal von Hand zu diagnostizieren,
// sagt die Seite es jetzt selbst.
// ⚠ Bewusst SICHTBAR statt nur Konsole: die Konsole sieht am Handy niemand.
const _JS_V = "205";
const _JS_V = "207";
{
const htmlV = document.body && document.body.dataset
? document.body.dataset.htmlV : null;
+35 -7
View File
@@ -12,9 +12,9 @@
⚠ LOKAL eingebunden, kein CDN: die PWA laeuft ueber WireGuard
und muss offline funktionieren. -->
<link rel="stylesheet" href="pico.min.css?v=1">
<link rel="stylesheet" href="style.css?v=205">
<link rel="stylesheet" href="style.css?v=207">
</head>
<body data-html-v="205">
<body data-html-v="207">
<!-- Modul-Menü (ganz oben) -->
<nav id="tabs">
<button class="tab active" data-view="dash">Dashboard</button>
@@ -139,7 +139,11 @@
dann zu diesen Preisen. -->
<div class="m15-setup" id="m15-setupblock" hidden></div>
<div class="m15-grid">
<details class="m15-akk" data-akk="prüfkette" open>
<!-- ⚠ KEIN `open` mehr (User-Wunsch 2026-08-20): alle Punkte der
M15-Karte sind per Default EINGEKLAPPT. Der zuletzt vom
Nutzer gewaehlte Zustand wird weiterhin gemerkt — nur der
Ausgangszustand ist jetzt zu. -->
<details class="m15-akk" data-akk="prüfkette">
<summary> ④ Prüfkette</summary>
<div class="m15-item" id="m15-bias" title="Uebergeordneter Trend M30 + H1. Der EINZIGE belegte Filter: gegen den HTF zu handeln halbiert den Edge (gemessen ×2)."></div>
<div class="m15-item" id="m15-kosten" title="Spread geteilt durch ATR_M15. Auf M15 gemessen 0,104 gegen 0,193 auf M5 - hier ist die groebere Zeitebene echt im Vorteil. Schwellen 0,20 / 0,32 aus backtest_realcosts.py."></div>
@@ -174,14 +178,20 @@
gemessen (`analyze_hl_funding.py`, 5.095 Stundenwerte, 7 Monate)
hält entüberlappt KEIN Bucket die 2-Stichproben-Regel. Kein
Verdict-Gewicht, kein Trigger. -->
<div class="hl-note hidden" id="hl-note"></div>
<!-- ⚠ AUSGEBLENDET am 2026-08-20 (User-Wunsch): Hyperliquid-Kontext
(`#hl-note`), M1/M5-Muster (`#pat-note`) und Trailing-Zustand
(`#trail-note`). Die Elemente sind ENTFERNT, nicht nur versteckt —
die drei Render-Bloecke greifen ueber `$streng` zu und ueberspringen
sich damit sauber. Das Backend rechnet unveraendert weiter
(`hl_ctx`, `patterns_tf`, `trail_info` stehen im Snapshot), die
Muster-Bilder haben ihre eigene Karte im Dashboard.
Zurueckholen = die drei <div class="hl-note hidden" id="..."> hier
wieder einsetzen; am JS ist nichts zu aendern. -->
<!-- Trailing-Zustand im Klartext. Die Trailing-KARTE ist seit dem
22.07. ausgeblendet; damit gab es keine Stelle mehr, an der man
sah, WARUM der Stop nicht nachgezogen wird (User-Frage 20.08.). -->
<!-- M1/M5-Muster kompakt (User 2026-08-20). Bewusst OHNE Bild:
die Meldungen-Karte ist der Live-Textstrom zum laufenden Trade. -->
<div class="hl-note hidden" id="pat-note" title="Das gerade entstehende Muster auf M1 und M5. Reine Anzeige — die Muster-Klasse ist als Signal gemessen und verworfen (Ziel-Trefferquote 1338 %), auf M1 am schwaechsten."></div>
<div class="hl-note hidden" id="trail-note" title="Was das Trailing gerade tut. Zwei Gruende, warum es scheinbar nichts tut und beide richtig sind: es startet erst ab 1,3×ATR Gewinn (seit 20.08., vorher 0,3×), und es lockert einen bereits engeren Stop niemals."></div>
<!-- Auto-Flip-Close: schließt eine Position, wenn die Empfehlung dreht.
⚠ GEMESSEN in BEIDEN Halbjahren negativ (92/160 R,
backtest_flipclose2.py), läuft auf ausdrücklichen User-Wunsch. Hatte
@@ -431,6 +441,24 @@
</nav>
<!-- Modal (Bestätigung / Token-Eingabe) -->
<!-- ══ BILD-MODALBOX (User-Wunsch 2026-08-20) ═══════════════════════════════
Klick auf ein Chartmuster-Bild öffnet es in voller Größe.
⚠⚠ BEWUSST OHNE BIBLIOTHEK. Ein Lightbox-Paket (GLightbox, PhotoSwipe …)
wäre 1540 KB plus ein zweites Bedienkonzept — für ~40 Zeilen Markup und
CSS. Das Projekt hat aus demselben Grund Tailwind abgelehnt (WireGuard,
offline, kein npm) und am 05.08. die vendorte lightweight-charts (164 KB)
wieder entfernt. Ein CDN scheidet ohnehin aus: das Dashboard läuft nur
über WireGuard, ohne Weg ins Internet.
⚠ Die große Fassung (`muster/gross/…`, ~133 KB) wird ERST beim Klick
geladen — die Kachel zeigt die 25-KB-Fassung. -->
<div id="bildbox" class="bildbox hidden">
<button id="bildbox-zu" class="bildbox-zu" title="Schließen (Esc)"></button>
<figure class="bildbox-inner">
<img id="bildbox-img" alt="">
<figcaption id="bildbox-txt"></figcaption>
</figure>
</div>
<div id="modal" class="modal hidden">
<div class="box">
<div class="box-title" id="modal-title"></div>
@@ -445,6 +473,6 @@
</div>
<div id="toast" class="toast hidden"></div>
<script src="app.js?v=205"></script>
<script src="app.js?v=207"></script>
</body>
</html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 131 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 161 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 139 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 160 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

+22 -4
View File
@@ -215,7 +215,7 @@ main{display:grid;grid-template-columns:repeat(auto-fit,minmax(320px,1fr));
.trade-bar{padding:0.4444rem 0.6667rem;border-color:#2d4a2d}
.trade-bar.hidden{display:none}
#card-trail.hidden{display:none} /* Trailing-Kachel ausgeblendet, 2026-07-22 */
.tb-title{margin:0 0 0.4444rem;font-size:0.7778rem;letter-spacing:1.5px;text-transform:uppercase;
.tb-title{margin:0 0 0.4444rem;font-size:0.6667rem;letter-spacing:1.5px;text-transform:uppercase;
color:var(--dim);font-weight:600}
/* Laufzeit-Uhr: im Titel mitlaufend (kompakt) + eigenes Feld in der Zeile */
.tb-runtime{margin-left:0.4444rem;color:var(--bright);font-family:var(--mono);
@@ -225,7 +225,7 @@ main{display:grid;grid-template-columns:repeat(auto-fit,minmax(320px,1fr));
/* EINHEITLICHES Feld-Design: Anzeige-Boxen (Richtung/Lots/Einsatz) und Eingabe-
Felder (SL/TP/Gewinn-Close/Notfall-Close) sehen identisch aus (Label über Box). */
.tb-field{display:flex;flex-direction:column;gap:2px}
.tb-field label{font-size:0.7222rem;color:var(--dim);letter-spacing:.5px}
.tb-field label{font-size:0.6111rem;color:var(--dim);letter-spacing:.5px}
/* ⚠ Feste 88px waren zu schmal (User 2026-08-19: „das Einsatz-Feld ist nicht
breit genug"). Der Einsatz zeigt Betraege wie „712.84 €" — bei fett und
0,94rem passt das nicht in 88px. `ch` = Breite einer Ziffer, haengt also
@@ -236,7 +236,7 @@ main{display:grid;grid-template-columns:repeat(auto-fit,minmax(320px,1fr));
/* ⚠ font-weight 700 -> 400 (User-Wunsch 2026-08-20): alle Felder der
Trade-Leiste normal, nicht fett — Eingaben UND Anzeige-Boxen, damit
die Leiste einheitlich bleibt. */
font-size:0.9444rem;font-weight:400;padding:0.3333rem 0.3889rem;text-align:center;
font-size:0.8333rem;font-weight:400;padding:0.3333rem 0.3889rem;text-align:center;
transition:border-color .15s, box-shadow .15s}
.tb-field .tb-val{display:block;white-space:nowrap}
.tb-field input{width:8ch} /* Eingaben: feste, aber mitskalierende Basis */ /* nur Anzeige, kein Input */
@@ -253,7 +253,7 @@ main{display:grid;grid-template-columns:repeat(auto-fit,minmax(320px,1fr));
/* ── Gesamtempfehlung ───────────────────── */
/* Circuit Breaker Banner (Tagesverlust-Stopp) — ganz oben, rot bei ausgelöst */
.circuit{margin:0 0 0.6111rem;padding:0.6111rem 0.7778rem;border-radius:var(--r);font-weight:700;
font-size:0.8889rem;text-align:center;letter-spacing:.2px}
font-size:0.7778rem;text-align:center;letter-spacing:.2px}
.circuit.hidden{display:none}
.circuit.tripped{background:#3d1a1a;color:#ff6b6b;border:1px solid var(--down);
box-shadow:0 0 0 1px var(--down)}
@@ -734,3 +734,21 @@ main{padding-bottom:4.1111rem} /* Platz für die fixe Leiste */
justify-content:center; font-size:0.6667rem; color:var(--dim)}
.pt-tfname{font-size:0.7778rem; font-weight:600; line-height:1.2}
.pt-tfst{font-size:0.6667rem; color:var(--dim); font-family:var(--mono)}
/* ── Bild-Modalbox fuer die Chartmuster (User 2026-08-20) ──────────────────
⚠ Ohne Bibliothek, s. Begruendung im Markup. `dvh` statt `vh`, damit die
Adressleiste am Handy die Box nicht abschneidet; `vh` als Rueckfall zuerst. */
.bildbox{position:fixed; inset:0; z-index:60; display:flex;
align-items:center; justify-content:center; padding:0.8889rem;
background:#000000e6; cursor:zoom-out}
.bildbox.hidden{display:none}
.bildbox-inner{margin:0; max-width:100%; cursor:default; text-align:center}
.bildbox-inner img{max-width:100%; max-height:80vh; max-height:80dvh;
border-radius:var(--r); display:block; background:#0d1117}
.bildbox-inner figcaption{margin-top:0.4444rem; color:var(--dim);
font-size:0.7778rem; font-family:var(--mono)}
.bildbox-zu{position:absolute; top:0.6667rem; right:0.8889rem; z-index:61;
background:var(--surface-2); border:1px solid var(--border);
border-radius:var(--r-sm); color:var(--bright); cursor:pointer;
font-size:1.1111rem; line-height:1; padding:0.3333rem 0.5556rem}
.pt-img[data-bild]{cursor:zoom-in}