M15-Karte "Setup-Bereitschaft" gebaut - Ersatz der Gesamtempfehlung (v=153)

Nach drei negativen M15-Messungen (Squeeze-Einstieg, VWAP-Baender, Exit-ATR)
steht fest: auf M15 traegt keine Richtungsquelle. Gebaut ist deshalb bewusst
KEINE Empfehlung, sondern eine sequenzielle Gate-Kette:

  [1 HTF-BIAS M30/H1] -> [2 KOSTEN Spread/ATR_M15] -> [3 LEVEL & KEGEL]
  -> Order AM LEVEL

Der Unterschied zur alten Karte ist grundsaetzlich: dort wurden Stimmen zu einem
Bias VERRECHNET (gemessen 48-51 % Treffer), hier muss jedes Gate EINZELN
passieren. Ein gefallenes Gate laesst sich nicht durch ein anderes ausgleichen.

Nur belegte Bausteine: HTF-Trend (einziger robuster Filter, Edge x2), Kosten
(M15 gemessen 0,104 gegen M5 0,193), naechstes stehendes Level + P(break),
Kegel (out-of-sample kalibriert, 80-%-Band trifft 77 %), Veto (-6,60 EUR/Lot).

Status-Banner: SETUP BEREIT / WARTEN AUF LEVEL / KEIN TRADE. Es bestaetigt
ausdruecklich auch das NICHT-Handeln - die gemessene Kern-Leckage sind
diskretionaere Abweichungen.

Die Order-Buttons werden BEWUSST NICHT gesperrt (gegen den Vorschlag
"ausgrauen"): hartes Blockieren wurde im Projekt verworfen, und
User-Uebersteuerungen liefen gemessen 68 % WR.

Umsetzung: engine._m15_setup() setzt nur zusammen, was der Snapshot ohnehin
erzeugt. Neu gerechnet wird allein das M15-Kosten-Verhaeltnis und P(break) fuer
das naechste Level. Dafuer veroeffentlicht wave_rec jetzt tf_atr (ADDITIV) - der
ATR je Zeitebene wurde im Turn-Fetch laengst berechnet, war aber nirgends
abrufbar.

P(break)-Merkmale bleiben auf M5 (Modell ist darauf trainiert, Fix 2026-07-13).

Mit 5 Szenarien getestet (alles passt, Bias uneinig, Kosten teuer, Level zu
weit, Kaltstart ohne ATR_M15 -> kein Absturz), live verifiziert ueber
deploy.py --feld m15_setup.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Axel Hocks
2026-08-08 15:18:29 +02:00
co-authored by Claude Opus 5
parent 010007f7a3
commit 44955bf70a
6 changed files with 259 additions and 3 deletions
+40 -1
View File
@@ -4182,7 +4182,7 @@ dort bereits nachvalidiert, ØR +0,305.) **Eine** Oberfläche:
(„⚠ die Welle (steuert die Order) schweigt").
Live gegengerechnet, beide Zeilen erscheinen wie beabsichtigt.
⚠ Reine Anzeige — an Gewichten, Gates und Order-Logik ist nichts geändert.
- Asset-Version aktuell **v=151** (in `web/index.html` hochzählen, siehe Workflows).
- Asset-Version aktuell **v=153** (in `web/index.html` hochzählen, siehe Workflows).
Schriftgrößen 2026-07-24 global **+4px** (2× je +2px auf User-Wunsch; Body-Basis
14→18px). ⚠ Betrifft in `style.css` sowohl `font-size:Npx` (71×) ALS AUCH die
`font:<weight> Npx/…`-**Shorthand** (3×: `.ms-chip`/`.ms-bos-lbl`/`.sqm-badge`
@@ -4942,6 +4942,45 @@ Modell schliesst keine Trades.
„die Rangfolge ist kaputt" ist **nicht** belegt; belegt ist nur die
Basisraten-/Kalibrierungslücke.
## ✅ M15-KARTE „SETUP-BEREITSCHAFT" GEBAUT (2026-08-08, v=153) — Ersatz der Gesamtempfehlung
Nach dem Entfernen der Gesamtempfehlung und drei negativen M15-Messungen
(Squeeze-Einstieg, VWAP-Bänder, Exit-ATR) steht fest: **auf M15 trägt keine
Richtungsquelle.** Gebaut ist deshalb bewusst **keine Empfehlung**, sondern eine
**sequenzielle Gate-Kette** nach dem Schema des Users:
```
[① HTF-BIAS M30/H1] → [② KOSTEN Spread/ATR_M15] → [③ LEVEL & KEGEL] → Order AM LEVEL
```
**Der Unterschied zur alten Karte ist grundsätzlich**: dort wurden Stimmen zu
einem Bias **verrechnet** (gemessen 4851 % Treffer über 1.071 Episoden), hier
muss jedes Gate **einzeln** passieren. Ein gefallenes Gate lässt sich nicht durch
ein anderes ausgleichen.
**Nur BELEGTE Bausteine:** ① HTF-Trend (einziger robuster Filter, Edge ×2) ·
② Kosten (M15 gemessen **0,104** gegen M5 0,193 — hier gewinnt M15 wirklich) ·
③ nächstes STEHENDES Level aus `_draw_levels` + P(break) · ④ Kegel
(out-of-sample kalibriert, 80 %-Band trifft 77 %) · ⑤ Veto (gegen das Signal
6,60 €/Lot, beidhälftig).
**Status-Banner:** 🟢 SETUP BEREIT @ Level · 🟡 WARTEN AUF LEVEL · 🔴 KEIN TRADE.
⚠ Das Banner bestätigt ausdrücklich auch das **Nicht**-Handeln — die gemessene
Kern-Leckage sind diskretionäre Abweichungen, nicht verpasste Gelegenheiten.
⚠⚠ **Die Order-Buttons werden BEWUSST NICHT gesperrt** (gegen den Vorschlag
„ausgrauen"): hartes Blockieren wurde im Projekt verworfen, und
User-Übersteuerungen liefen gemessen **68 % WR**. Sichtbar machen, nicht
bevormunden.
**Umsetzung:** `engine._m15_setup()` setzt nur zusammen, was der Snapshot ohnehin
erzeugt; NEU gerechnet wird allein das M15-Kosten-Verhältnis und P(break) für das
nächste Level. ⚠ Dafür veröffentlicht `wave_rec` jetzt **`tf_atr`** (ADDITIV) —
der ATR je Zeitebene wurde im Turn-Fetch längst berechnet, war aber nirgends
abrufbar; ihn neu zu holen wäre ein zweiter MT5-Fetch für vorhandene Zahlen
(derselbe Fehler wie beim Konsens-Pfeil).
⚠ P(break)-Merkmale bleiben auf **M5** — das Modell ist darauf trainiert (Fix
2026-07-13); auf M15 umzustellen würde es falsch skalieren.
Mit **5 Szenarien getestet** (alles passt · Bias uneinig · Kosten teuer · Level
zu weit · Kaltstart ohne ATR_M15 → kein Absturz), live verifiziert über
`deploy.py --feld m15_setup`.
## ⚠⚠ EXIT-ABSTAND AUS M15-ATR = FÄLLT DURCH (2026-08-08)
Zweiter Teil der M15-Idee: M5-Einstieg behalten, nur den EXIT gröber führen.
+84
View File
@@ -3795,6 +3795,12 @@ class TradingEngine:
if (b and a and atr) else None)(
(market or {}).get("bid"), (market or {}).get("ask"),
((wave_snap or {}).get("pb_feats") or {}).get("atr")),
# M15-SETUP-BEREITSCHAFT (2026-08-08) — Ersatz fuer die entfernte
# Gesamtempfehlung. ⚠ KEINE Richtungsaussage: auf M15 traegt gemessen
# keine (Squeeze n=33/Nachbarn kippen, VWAP-Baender ohne Edge).
# Zeigt Bedingungen, nicht Prognosen.
"m15_setup": self._m15_setup(market, wave_snap, _cone,
self._alignment_cached()),
"sr_close_hint": sr_close_hint,
"stop_approach": stop_approach,
"auto_sr_close": self._auto_sr_close,
@@ -3968,6 +3974,84 @@ class TradingEngine:
log.debug(f"sr_close_hint: {e}")
return None
def _m15_setup(self, market, wave_snap, cone, alignment) -> dict | None:
"""SETUP-BEREITSCHAFT auf M15 — der Ersatz für die entfernte Gesamtempfehlung.
⚠⚠ BEWUSST KEINE RICHTUNGS-EMPFEHLUNG. Gemessen (2026-08-08) trägt auf
M15 KEINE Richtungsquelle: der Squeeze fällt dort durch (n=33 in H1,
Nachbarn kippen), VWAP-Bänder ebenso (Zufallslinie hält gleich gut, alle
Konfidenzintervalle enthalten die Null), und 24 Signal-Eingriffe sind
insgesamt gescheitert. Was BELEGT ist, sind Bedingungen — nicht Prognosen.
Die Karte zeigt deshalb das Schema des Users (Bias → Zone → Reichweite →
Veto) und überlässt den Auslöser dem Menschen, dessen Auswahl-Vorteil
unabhängig belegt ist.
Zusammengesetzt aus bereits berechneten Teilen — NEU gerechnet wird nur
das Kosten-Verhältnis auf M15 und P(break) für das nächste Level. Der
Konsens-Pfeil hat einmal ein zweites `_verdict()` gezogen; das ist die
Lehre daraus.
"""
try:
bid = (market or {}).get("bid"); ask = (market or {}).get("ask")
ws = wave_snap or {}
atr15 = (ws.get("tf_atr") or {}).get("M15")
out: dict = {"atr_m15": round(atr15, 4) if atr15 else None}
# ── 1) BIAS — der einzige belegte Filter (M30-Gegen-Trend: Edge ×2)
h30 = ws.get("htf_trend"); h1 = ws.get("h1_trend")
einig = (h30 and h1 and h30 == h1)
out["bias"] = {"m30": h30, "h1": h1,
"richtung": h30 if einig else None,
"einig": bool(einig)}
# ── 2) KOSTEN auf M15 — hier gewinnt M15 wirklich (gemessen 0,104
# gegen M5 0,193). Schwellen wie beim M5-Chip (0,20 / 0,32).
kr = None
if bid and ask and atr15 and atr15 > 0:
kr = round((ask - bid) / atr15, 3)
out["kosten"] = {"ratio": kr,
"stufe": (None if kr is None else
"guenstig" if kr < 0.20 else
"normal" if kr < 0.32 else "teuer")}
# ── 3) ZONE — nächstes STEHENDES Level (M30-Pivots, dieselbe Quelle
# wie Chart und S/R-Close) + P(break). ⚠ Die P(break)-Merkmale
# kommen aus M5, weil das Modell darauf trainiert ist (Fix
# 2026-07-13) — NICHT auf M15 umstellen.
dl = self._draw_levels() or {}
res = (dl.get("res") or [None])[0]
sup = (dl.get("sup") or [None])[0]
pf = ws.get("pb_feats") or {}
a5 = pf.get("atr") or 0.2
zonen = []
for lvl, seite in ((res, "res"), (sup, "sup")):
if not lvl or not bid or not atr15:
continue
d = 1 if seite == "res" else -1
gap = abs(lvl - bid)
wt = 1.0 if (pf.get("ema_diff", 0.0) or 0.0) * d > 0 else 0.0
p = _p_break(pf.get("mom6", 0.0) * d, pf.get("mom3", 0.0) * d,
wt, gap / max(a5, 0.06))
zonen.append({"seite": seite, "level": round(lvl, 3),
"gap_atr15": round(gap / atr15, 2),
"p_break": round(100 * p)})
out["zonen"] = zonen
# ── 4) REICHWEITE — der Kegel (kalibriert: 80 %-Band trifft 77 %)
lv = (cone or {}).get("levels") or []
b80 = [x for x in lv if x.get("band") == 80]
out["reichweite"] = [{"minutes": x.get("minutes"),
"lo": x.get("lo"), "hi": x.get("hi")}
for x in b80] or None
# ── 5) VETO — die eigene Bilanz, je Lot (absolute Euro sind bei
# wechselnder Positionsgröße ungültig, Lehre vom 04.08.)
out["veto"] = alignment or None
return out
except Exception as e:
log.debug(f"m15_setup: {e}")
return None
def _stop_approach_hint(self, market, position, wave_snap) -> dict | None:
"""REINE ANZEIGE: P(Durchbruch) für das Level, dem sich der Kurs auf der
**Gegen-/Stop-Seite** der offenen Position nähert (LONG → Support darunter,
+9
View File
@@ -398,6 +398,14 @@ class WaveRecommender:
if turn_data: # frisch geholt → neu berechnen, sonst letzten Stand halten
self._last_tf_turns = {lbl: self._turn_state(c, a)
for lbl, (c, a) in turn_data.items()}
# ⚠ ADDITIV (2026-08-08): den ATR je Zeitebene MITNEHMEN. Er wird im
# Turn-Fetch ohnehin berechnet (`turn_data[lbl] = (closes, atr)`),
# war aber nirgends abrufbar — die M15-Setup-Karte braucht ihn fuer
# das Kosten-Gate (Spread/ATR_M15) und die Abstandsangaben. Ihn dort
# neu zu holen waere ein zweiter MT5-Fetch fuer Zahlen, die schon da
# sind (derselbe Fehler wie beim Konsens-Pfeil, der ein zweites
# `_verdict()` rechnete).
self._tf_atr = {lbl: a for lbl, (c, a) in turn_data.items() if a}
# P(break)-Features IMMER aus M5 (Modell wurde auf M5 trainiert/kalibriert
# — die Wave-TF wechselt per Heuristik bis M30; damit wäre mom6 ein 3-h-
# statt 30-min-Momentum und der ATR 23× größer → P(break) völlig falsch
@@ -484,6 +492,7 @@ class WaveRecommender:
else:
self._last_bounce = {"state": None, "dir": None, "tf": None}
snap["tf_turns"] = dict(self._last_tf_turns)
snap["tf_atr"] = dict(getattr(self, "_tf_atr", {}) or {})
snap["bounce"] = dict(self._last_bounce) # Multi-TF überschreibt Basis-TF
snap["pb_feats"] = dict(self._pb_feats) if getattr(self, "_pb_feats", None) else None
snap["pb_levels"] = dict(self._pb_levels) if getattr(self, "_pb_levels", None) else None
+79
View File
@@ -142,6 +142,84 @@ function beepRecommendation(sig, isNew) {
// · HL-Kontext (`#hl-note`) — schreibt in die Meldungen-Karte, nicht hierher
// Entfallen ist die Block-Grund-Zeile (`#vd-blocks`).
// ══════════════════════════════════════════════════════════════════════════
// ══════════════════════════════════════════════════════════════════════════
// M15 SETUP-BEREITSCHAFT — sequenzielle GATE-KETTE, keine Score-Aggregation.
// [HTF-BIAS] → [KOSTEN] → [LEVEL & KEGEL] → AUSFÜHRUNG NUR AM LEVEL
// ⚠ Bewusst KEINE Richtungsaussage. Auf M15 trägt gemessen keine (Squeeze
// n=33/Nachbarn kippen, VWAP-Bänder ohne Edge). Die Karte sagt, ob die
// BEDINGUNGEN stimmen — der Auslöser bleibt beim Menschen.
// ⚠ Der Unterschied zur entfernten Gesamtempfehlung ist grundsätzlich: dort
// wurden Stimmen zu einem Bias VERRECHNET (gemessen 4851 % Treffer), hier
// muss jedes Gate EINZELN passieren. Ein Gate, das fällt, lässt sich nicht
// durch ein anderes ausgleichen.
// ══════════════════════════════════════════════════════════════════════════
function renderM15(s) {
const st = $("m15-status");
if (!s) { st.className = "m15-status"; st.textContent = "— keine Daten"; return; }
const f = (v, d = 2) => (v == null ? "—" : (+v).toFixed(d));
// ── Gate 1: HTF-Bias (der einzige belegte Filter, Edge ×2) ───────────
const b = s.bias || {};
const bOk = !!b.einig;
$("m15-bias").className = "m15-item " + (bOk ? "ok" : "fail");
$("m15-bias").textContent = bOk
? `① Bias ${b.richtung === "LONG" ? "▲ LONG" : "▼ SHORT"} · M30+H1 einig`
: `① Bias unklar · M30 ${b.m30 || "—"} / H1 ${b.h1 || "—"}`;
// ── Gate 2: Kosten auf M15 ───────────────────────────────────────────
const k = s.kosten || {};
const kOk = k.stufe === "guenstig" || k.stufe === "normal";
$("m15-kosten").className = "m15-item " + (k.ratio == null ? "" : kOk ? "ok" : "fail");
$("m15-kosten").textContent = k.ratio == null
? "② Kosten —"
: `② Kosten ${f(k.ratio, 3)}×ATR · ${
k.stufe === "guenstig" ? "günstig" : k.stufe === "normal" ? "normal" : "TEUER"}`;
// ── Gate 3: Level in Reichweite ──────────────────────────────────────
const z = (s.zonen || []);
const nah = z.filter(x => x.gap_atr15 != null && x.gap_atr15 <= 0.5);
const zOk = nah.length > 0;
$("m15-zone").className = "m15-item " + (z.length ? (zOk ? "ok" : "warn") : "");
$("m15-zone").textContent = z.length
? "③ " + z.map(x => `${x.seite === "res" ? "Widerstand" : "Support"} ${f(x.level, 3)}`
+ ` (${f(x.gap_atr15)}×ATR, Bruch ${x.p_break} %)`).join(" · ")
: "③ kein Level in Sicht";
// ── Reichweite (Kegel) — sagt WIE WEIT, nicht wohin ──────────────────
const r = s.reichweite || [];
$("m15-reich").className = "m15-item";
$("m15-reich").textContent = r.length
? "④ erwartete Spanne · " + r.map(x => `${x.minutes} ${f(x.lo, 2)}${f(x.hi, 2)}`).join(" · ")
: "④ erwartete Spanne —";
// ── Veto: die eigene Bilanz, JE LOT ──────────────────────────────────
const v = s.veto || {};
const g = v.against || {};
$("m15-veto").className = "m15-item " + (g.n >= 5 ? "warn" : "");
$("m15-veto").textContent = g.n >= 5
? `⑤ gegen das Signal: ${g.n} Trades, ${f(g.pnl_lot, 2)} € je Lot`
: "⑤ Veto: noch zu wenige Trades";
// ── Status: die Kette wird der Reihe nach ausgewertet ────────────────
let cls, txt;
if (!bOk) { cls = "rot"; txt = "🔴 KEIN TRADE · Bias unklar (M30/H1 uneinig)"; }
else if (!kOk && k.ratio != null) { cls = "rot"; txt = "🔴 KEIN TRADE · Kosten zu hoch"; }
else if (!zOk) { cls = "gelb"; txt = "🟡 WARTEN AUF LEVEL · Bias und Kosten passen"; }
else {
const nn = nah[0];
cls = "gruen";
txt = `🟢 SETUP BEREIT @ ${f(nn.level, 3)} · ${b.richtung === "LONG" ? "▲" : "▼"} `
+ `${b.richtung} · Order ANS LEVEL legen, nicht zum Markt`;
}
st.className = "m15-status " + cls;
st.textContent = txt;
st.title = "Sequenzielle Kette: Bias → Kosten → Level. Ein gefallenes Gate lässt "
+ "sich NICHT durch ein anderes ausgleichen. ⚠ Die Order-Buttons bleiben "
+ "bewusst bedienbar — hartes Blockieren wurde im Projekt verworfen, und "
+ "Übersteuerungen des Users liefen gemessen 68 % Trefferquote.";
}
function renderKontext(snap) {
// ── Selbst-Kalibrierung: die Empfehlung zeigt ihre eigene Trefferquote ──
// ⚠ Gegen 50 % zu lesen, nicht gegen 0 — es ist eine Richtungsaussage.
@@ -609,6 +687,7 @@ function render(d) {
}
// ── Gesamtempfehlung (aggregiert) ───────
renderM15(d.m15_setup);
renderKontext(d);
renderCircuitBreaker(d.circuit_breaker);
renderPatterns(d.patterns);
+28 -2
View File
@@ -6,7 +6,7 @@
<meta name="theme-color" content="#0d1117">
<title>Oil · MT5</title>
<link rel="manifest" href="manifest.json">
<link rel="stylesheet" href="style.css?v=152">
<link rel="stylesheet" href="style.css?v=153">
</head>
<body>
<!-- Modul-Menü (ganz oben) -->
@@ -87,6 +87,32 @@
GEBLIEBEN sind drei Dinge, die dort nur geparkt waren und je eine
eigene, unabhaengige Beleglage haben (s. CLAUDE.md).
══════════════════════════════════════════════════════════════════ -->
<!-- ══════════════════════════════════════════════════════════════════
M15 SETUP-BEREITSCHAFT (2026-08-08) — Ersatz fuer die entfernte
Gesamtempfehlung. ⚠ KEINE Richtungsaussage: auf M15 traegt gemessen
KEINE (Squeeze n=33/Nachbarn kippen, VWAP-Baender ohne Edge, 24
Signal-Eingriffe gescheitert). Gezeigt werden BEDINGUNGEN nach dem
Schema Bias -> Zone -> Reichweite -> Veto; den Ausloeser gibt der
Mensch, dessen Auswahl-Vorteil unabhaengig belegt ist.
══════════════════════════════════════════════════════════════════ -->
<section class="card wide" id="card-m15">
<h2 title="Setup-Bereitschaft auf M15. Zeigt BEDINGUNGEN, keine Richtungsprognose - auf M15 traegt gemessen kein Richtungssignal. Der Ausloeser bleibt beim Menschen.">M15 · Setup-Bereitschaft</h2>
<!-- Status-Banner: die wichtigste Zeile. Sie bestaetigt ausdruecklich
auch das NICHT-Handeln - die gemessene Kern-Leckage sind
diskretionaere Abweichungen, nicht fehlende Gelegenheiten.
⚠ Die Order-Buttons werden BEWUSST NICHT gesperrt: hartes Blockieren
wurde im Projekt verworfen, und User-Uebersteuerungen liefen
gemessen 68 % WR. Sichtbar machen, nicht bevormunden. -->
<div class="m15-status" id="m15-status"></div>
<div class="m15-grid">
<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>
<div class="m15-item" id="m15-zone" title="Naechstes STEHENDES Level (M30-Pivots, dieselbe Quelle wie Chart und S/R-Close) mit Abstand und Durchbruchwahrscheinlichkeit. ⚠ P(break) ist live derzeit rund 7 Pp zu niedrig kalibriert."></div>
<div class="m15-item" id="m15-reich" title="Erwartete Spanne aus core/cone.py - out-of-sample geprueft, die 80-%-Baender treffen real 77 %. Sagt WIE WEIT, nicht wohin."></div>
<div class="m15-item" id="m15-veto" title="Deine eigene Bilanz mit/ohne/gegen Signal, je Lot gerechnet. Gegen das Signal zu handeln kostet gemessen 6,60 EUR je Lot - beidhaelftig robust."></div>
</div>
</section>
<section class="card wide" id="card-kontext">
<h2 title="Drei unabhaengige Kontext-Anzeigen. KEINE Empfehlung und keine Richtungsaussage - die Aggregation zu einer Gesamtempfehlung wurde am 08.08. entfernt.">Marktkontext</h2>
<!-- Zeitebenen-Ampel (M1/M5/M15/M30/H1) -->
@@ -314,6 +340,6 @@
</div>
<div id="toast" class="toast hidden"></div>
<script src="app.js?v=152"></script>
<script src="app.js?v=153"></script>
</body>
</html>
+19
View File
@@ -489,3 +489,22 @@ main{padding-bottom:74px} /* Platz für die fixe Leiste */
.hl-note{font-size:13px;color:var(--dim);font-family:var(--mono);
padding:5px 0;border-top:1px solid var(--line);margin-top:5px}
.hl-note.hidden{display:none}
/* ── M15 Setup-Bereitschaft (2026-08-08, Ersatz der Gesamtempfehlung) ──────
Sequenzielle Gate-Kette statt Score-Aggregation. Das Status-Banner ist
bewusst das groesste Element der Karte: die gemessene Kern-Leckage sind
diskretionaere Abweichungen, nicht verpasste Gelegenheiten - deshalb muss
auch das NICHT-Handeln ausdruecklich bestaetigt werden. */
#card-m15{border-color:var(--accent)}
.m15-status{font-size:19px;font-weight:700;letter-spacing:.3px;
padding:10px 12px;border-radius:var(--r);margin-bottom:10px;
background:var(--surface-2);border:1px solid var(--border);color:var(--dim)}
.m15-status.gruen{color:var(--up);border-color:#1d5c2a;background:#0e2413}
.m15-status.gelb {color:var(--accent);border-color:#4a3600;background:#241d05}
.m15-status.rot {color:var(--down);border-color:#5c1a1a;background:#240f0f}
.m15-grid{display:flex;flex-direction:column;gap:5px}
.m15-item{font-size:15px;color:var(--dim);padding:4px 8px;border-radius:var(--r-sm);
background:var(--surface-2);border:1px solid var(--line)}
.m15-item.ok {color:var(--bright);border-color:#1d5c2a}
.m15-item.warn{color:var(--accent);border-color:#4a3600}
.m15-item.fail{color:var(--down);border-color:#5c1a1a}