diff --git a/server/pruef-tippziele.mjs b/server/pruef-tippziele.mjs
new file mode 100644
index 00000000..90240535
--- /dev/null
+++ b/server/pruef-tippziele.mjs
@@ -0,0 +1,265 @@
+/* =====================================================================
+ pruef-tippziele.mjs — Was man antippen soll, muss man treffen können.
+
+ Filipe, 19.09.2026: „jeder button jeder knopf jede kachel jeder text
+ einfach alles muss klappen weil die modis haben schon probleme."
+
+ ---------------------------------------------------------------------
+ WARUM ES DIESE PRÜFUNG GIBT, OBWOHL ES DEN GROSSEN RUNDGANG GIBT
+
+ `pruef-handy-teamdogi` misst das Richtige -- an echten Bildpunkten,
+ über 206 Seitenansichten. Nur dauert sie Minuten und gehört damit zu
+ den Läufen, die nicht nebenbei laufen.
+
+ Diese hier braucht keinen Browser und keine Sekunde: Sie liest das
+ CSS und fragt, ob ein Bedienelement überhaupt eine Chance hat, 44
+ Pixel groß zu sein. Das ist weniger, als die große kann -- aber es
+ läuft nach JEDER Änderung, und genau daran ist die alte Lösung
+ gescheitert.
+
+ ---------------------------------------------------------------------
+ DIE EIGENTLICHE AUFGABE: EINE LISTE ABSCHAFFEN
+
+ In `module.css` steht ein Block `@media (pointer: coarse)`, der ein
+ Dutzend Klassen auf 44 Pixel hebt. Diese Liste wird von Hand
+ gepflegt -- und ist damit genau die Falle, vor der die Hausregeln
+ warnen: Wer morgen einen Knopf mit `width: 34px` baut, trägt ihn
+ dort nicht nach. Er merkt es auch nicht.
+
+ Gemessen am 20.09.2026: `.chat__weg` (34×34) stand nicht drin. Das
+ ist der Knopf, der ein Gespräch wegräumt -- und direkt daneben
+ sitzt der, der es FÜR ALLE auflöst. Zwei 34-Pixel-Ziele
+ nebeneinander, von denen eines unwiderruflich ist.
+
+ Die Liste bleibt (CSS kann nicht rechnen). Aber sie wird ab jetzt
+ BEWACHT: Diese Prüfung leitet die Bedienelemente aus dem CSS ab und
+ verlangt für jedes eine Abdeckung. Eine Liste, die überwacht wird,
+ kann nicht mehr still veralten.
+
+ ---------------------------------------------------------------------
+ WAS ALS BEDIENELEMENT ZÄHLT — UND WAS NICHT
+
+ Ein Bedienelement ist, was man antippt. Ein Zeichen DARIN ist es
+ nicht: `.chat__weg svg` ist 17 Pixel groß und soll das auch sein --
+ getroffen wird der Knopf darum herum. Ebenso `::before`, `::after`
+ und Innenteile wie `__punkt`, `__lupe`, `__pfeil`.
+
+ Der erste Anlauf hat genau das nicht unterschieden und 21 Treffer
+ gemeldet, von denen 15 richtig waren. Eine Prüfung, die zu viel
+ meldet, wird abgeschaltet -- das ist kein besseres Ergebnis als
+ eine, die zu wenig meldet.
+
+ AUFRUF: node server/pruef-tippziele.mjs
+ Läuft in unter einer Sekunde, ohne Server, ohne Browser.
+ ===================================================================== */
+
+import { readFileSync, readdirSync } from "node:fs";
+import { join, dirname } from "node:path";
+import { fileURLToPath } from "node:url";
+
+const HIER = dirname(fileURLToPath(import.meta.url));
+const CSS = join(HIER, "..", "workspace", "assets", "css");
+
+/** Was ein Daumen sicher trifft. Keine Hausmeinung, sondern die Zahl,
+ * die Apple, Google und die WCAG (2.5.8) gemeinsam nennen. */
+const ZIEL = 44;
+
+let ok = 0, fehl = 0;
+const pruef = (name, bedingung, zusatz = "") => {
+ if (bedingung) { ok++; console.log(` OK ${name}`); }
+ else { fehl++; console.log(` FEHL ${name}${zusatz ? "\n " + zusatz : ""}`); }
+};
+
+/** Zerlegt CSS klammergenau. Ein Muster „bis zur nächsten }" greift bei
+ * jedem `@media` daneben. */
+function regeln(css, inMedia = null) {
+ const out = [];
+ let i = 0; const n = css.length;
+ while (i < n) {
+ if (css.startsWith("/*", i)) { const j = css.indexOf("*/", i + 2); i = j < 0 ? n : j + 2; continue; }
+ if (" \t\r\n".includes(css[i])) { i++; continue; }
+ const s = i; let j = i;
+ while (j < n && css[j] !== "{") {
+ if (css.startsWith("/*", j)) { const k = css.indexOf("*/", j + 2); j = k < 0 ? n : k + 2; continue; }
+ j++;
+ }
+ if (j >= n) break;
+ const kopf = css.slice(s, j).trim();
+ let t = 0, k = j;
+ for (; k < n; k++) {
+ if (css.startsWith("/*", k)) { const m = css.indexOf("*/", k + 2); k = m < 0 ? n : m + 1; continue; }
+ if (css[k] === "{") t++;
+ else if (css[k] === "}") { t--; if (!t) { k++; break; } }
+ }
+ const koerper = css.slice(j + 1, k - 1);
+ if (kopf.startsWith("@")) {
+ /* In einen @media hineingehen -- und merken, unter welcher
+ Bedingung die Regeln darin stehen. */
+ out.push(...regeln(koerper, inMedia ? `${inMedia} && ${kopf}` : kopf));
+ } else {
+ out.push({ sel: kopf, koerper, media: inMedia });
+ }
+ i = k;
+ }
+ return out;
+}
+
+/** Ist dieser Selektor ein Ding, das man antippt? */
+function istBedienelement(sel) {
+ /* Nur der LETZTE Teil zählt: `.chat__weg svg` ist das Zeichen im
+ Knopf, nicht der Knopf. */
+ const letzter = sel.split(",")[0].trim().split(/\s+|>/).filter(Boolean).pop() || "";
+ if (/::(before|after|marker|backdrop)/.test(letzter)) return false;
+ /* Ein nacktes Element am Ende (svg, span, path) ist ein Innenteil. */
+ if (/^(svg|path|span|i|b|em|circle|rect|line)\b/.test(letzter)) return false;
+ /* Innenteile tragen im Haus eigene Endungen. */
+ if (/__(svg|zeichen|punkt|lupe|pfeil|gleis|funke|laden|zahl|text|name)\b/.test(letzter)) return false;
+ return /knopf|__weg\b|schalter|pille|chip|tag-knopf|toggle|schritt\b/i.test(letzter);
+}
+
+/** Die kleinste feste Größe, die eine Regel festschreibt. */
+function festeGroesse(koerper) {
+ const zahl = (name) => {
+ const m = new RegExp(`(?:^|[;{\\s])${name}:\\s*(\\d+)px`).exec(koerper);
+ return m ? Number(m[1]) : null;
+ };
+ return { h: zahl("height"), w: zahl("width") };
+}
+
+console.log("\n=== Tippziele: kann man jeden Knopf mit dem Daumen treffen? ===\n");
+
+const dateien = readdirSync(CSS).filter((f) => f.endsWith(".css"));
+const alle = [];
+for (const f of dateien) {
+ for (const r of regeln(readFileSync(join(CSS, f), "utf8"))) alle.push({ ...r, datei: f });
+}
+pruef("das CSS ließ sich zerlegen", alle.length > 500, `${alle.length} Regeln in ${dateien.length} Dateien`);
+
+/* ---- 1. Wer ist zu klein, ohne Rücksicht auf den Zeiger? ----------- */
+const zuKlein = [];
+for (const r of alle) {
+ if (r.media) continue; /* gleich gesondert */
+ if (!istBedienelement(r.sel)) continue;
+ const { h, w } = festeGroesse(r.koerper);
+ if ((h !== null && h < ZIEL) || (w !== null && w < ZIEL)) {
+ zuKlein.push({ ...r, h, w });
+ }
+}
+
+/* ---- 2. Wird es auf Fingergeräten angehoben? ----------------------- */
+/** Alle Selektoren, die in einem `pointer: coarse`-Block auf mindestens
+ * 44 gehoben werden. Abgeleitet, nicht abgeschrieben. */
+const gehoben = new Set();
+for (const r of alle) {
+ if (!r.media || !/pointer:\s*coarse/.test(r.media)) continue;
+ const minH = /min-height:\s*(\d+)px/.exec(r.koerper);
+ const minW = /min-width:\s*(\d+)px/.exec(r.koerper);
+ if (!minH && !minW) continue;
+ for (const teil of r.sel.split(",")) {
+ const t = teil.trim();
+ if (!t) continue;
+ gehoben.add(t);
+ /* Auch die kurze Form merken, damit `.a .b` und `.b` sich finden. */
+ const letzter = t.split(/\s+|>/).filter(Boolean).pop();
+ if (letzter) gehoben.add(letzter);
+ }
+}
+pruef("es gibt überhaupt einen Block für Fingerbedienung", gehoben.size > 0,
+ `${gehoben.size} Selektoren werden angehoben`);
+
+/** Der zweite Weg: eine unsichtbare Trefferfläche um ein kleines
+ * Zeichen herum (`::after { inset: -10px }`). In dichten Zeilen ist
+ * das die richtige Lösung -- ein 44er-Knopf würde dort jede Zeile
+ * aufblähen.
+ *
+ * NACHGERECHNET, NICHT GEGLAUBT: Die Fläche zählt nur, wenn
+ * Grundgröße + 2 x Rand wirklich 44 erreicht. Sonst hätte man eine
+ * Regel, die gut aussieht und nichts löst. */
+const flaeche = new Map();
+for (const r of alle) {
+ if (!r.media || !/pointer:\s*coarse/.test(r.media)) continue;
+ const m = /inset:\s*-(\d+)px/.exec(r.koerper);
+ if (!m) continue;
+ for (const teil of r.sel.split(",")) {
+ const t = teil.trim();
+ if (!/::(after|before)\s*$/.test(t)) continue;
+ const grund = t.replace(/::(after|before)\s*$/, "").trim();
+ flaeche.set(grund, Math.max(flaeche.get(grund) || 0, Number(m[1])));
+ const letzter = grund.split(/\s+|>/).filter(Boolean).pop();
+ if (letzter) flaeche.set(letzter, Math.max(flaeche.get(letzter) || 0, Number(m[1])));
+ }
+}
+/* KEINE PRUEFUNG DARAUF, DASS ES SIE GIBT. Am 20.09.2026 stand hier
+ `pruef("und es gibt den zweiten Weg", flaeche.size > 0)` -- und
+ genau dann fiel sie um, als die Trefferfläche wieder verschwand,
+ weil sich die schlichte Lösung als richtig erwies. Eine Prüfung,
+ die eine BAUWEISE erzwingt statt eines Ergebnisses, steht dem
+ Aufräumen im Weg. Sie wird anerkannt, wenn sie benutzt wird --
+ verlangt wird sie nicht. */
+
+const ungedeckt = [];
+for (const r of zuKlein) {
+ const teile = r.sel.split(",").map((t) => t.trim()).filter(Boolean);
+ /* Gedeckt ist es nur, wenn JEDER Teil des Selektors gedeckt ist --
+ sonst bliebe die Hälfte klein. */
+ const alleGedeckt = teile.every((t) => {
+ const letzter = t.split(/\s+|>/).filter(Boolean).pop();
+ if (gehoben.has(t) || (letzter && gehoben.has(letzter))) return true;
+ /* Trefferfläche: nachrechnen, nicht glauben. */
+ const rand = flaeche.get(t) ?? (letzter ? flaeche.get(letzter) : 0) ?? 0;
+ if (!rand) return false;
+ const klein = Math.min(r.h ?? Infinity, r.w ?? Infinity);
+ return klein + 2 * rand >= ZIEL;
+ });
+ if (!alleGedeckt) ungedeckt.push(r);
+}
+
+/* DIE ZAHL GEHOERT IN DIE BEDINGUNG: Fände die Prüfung gar keine
+ Bedienelemente, wäre „keins ungedeckt" trivialerweise wahr. */
+pruef("jedes zu kleine Bedienelement wird auf Fingergeräten angehoben",
+ zuKlein.length > 0 && ungedeckt.length === 0,
+ `${zuKlein.length} fest kleiner als ${ZIEL}px, davon ungedeckt: ${ungedeckt.length}\n `
+ + ungedeckt.map((r) => `${r.datei}: ${r.sel.replace(/\s+/g, " ").slice(0, 46)}`
+ + ` (${[r.h && r.h + "h", r.w && r.w + "w"].filter(Boolean).join(" ")})`).join("\n "));
+
+/* ---- 3. Und der Block hebt wirklich auf 44, nicht auf 40 ----------- */
+const zuNiedrig = [];
+for (const r of alle) {
+ if (!r.media || !/pointer:\s*coarse/.test(r.media)) continue;
+ for (const name of ["min-height", "min-width"]) {
+ const m = new RegExp(`${name}:\\s*(\\d+)px`).exec(r.koerper);
+ if (m && Number(m[1]) < ZIEL) zuNiedrig.push(`${r.sel.slice(0, 40)} ${name}: ${m[1]}px`);
+ }
+}
+pruef(`und angehoben wird auf mindestens ${ZIEL}px`, zuNiedrig.length === 0,
+ zuNiedrig.join("; "));
+
+/* ---- DIE GEGENPROBEN ----------------------------------------------- */
+console.log("\n --- Gegenproben (jede MUSS anschlagen) ---");
+
+pruef("Gegenprobe: ein Zeichen IM Knopf zählt nicht als Tippziel",
+ !istBedienelement(".chat__weg svg") && !istBedienelement(".zurueck-knopf::before"));
+pruef("Gegenprobe: der Knopf selbst zählt schon",
+ istBedienelement(".chat__weg") && istBedienelement(".wahl2__knopf"));
+pruef("Gegenprobe: eine feste Größe wird erkannt",
+ festeGroesse("width: 34px; height: 34px;").w === 34);
+pruef("Gegenprobe: eine min-Größe ist KEINE feste Größe",
+ festeGroesse("min-width: 34px;").w === null,
+ "sonst gälte ein Knopf, der nach unten begrenzt ist, als zu klein");
+pruef("Gegenprobe: eine zu kleine Trefferfläche rettet nichts",
+ (() => {
+ /* 24 + 2x5 = 34 -- das reicht nicht, und die Prüfung muss das
+ merken. Sonst wäre jede Fläche ein Freibrief. */
+ const klein = 24, rand = 5;
+ return klein + 2 * rand < ZIEL;
+ })());
+pruef("Gegenprobe: eine ausreichende schon",
+ 24 + 2 * 10 >= ZIEL);
+
+pruef("Gegenprobe: @media wird betreten, nicht überlesen",
+ regeln("@media (pointer: coarse) { .x { min-height: 44px } }")[0]?.media
+ ?.includes("coarse"));
+
+console.log(`\n ${ok} bestanden, ${fehl} fehlgeschlagen`);
+console.log(` (${alle.length} CSS-Regeln, ${zuKlein.length} feste Tippziele unter ${ZIEL}px)\n`);
+process.exit(fehl ? 1 : 0);
diff --git a/workspace/anruf-probe.html b/workspace/anruf-probe.html
index 113be0f8..4c81e8b9 100644
--- a/workspace/anruf-probe.html
+++ b/workspace/anruf-probe.html
@@ -16,15 +16,15 @@
verdeckt. Gefunden hat es pruef-struktur.
Richtig sind `gate.css` (die Farbwerte des Hauses) und `haus.css`. -->
-
-
+
+
-
+