diff --git a/server/pruef-calls-rudel.mjs b/server/pruef-calls-rudel.mjs
new file mode 100644
index 00000000..213ac874
--- /dev/null
+++ b/server/pruef-calls-rudel.mjs
@@ -0,0 +1,312 @@
+/* =====================================================================
+ pruef-calls-rudel.mjs — Calls & Protokolle bei Team Dogi
+ =====================================================================
+
+ Filipe, 22.09.2026: „ich will diese kachel vom workspace auch im team
+ dogi seite. ich will dass jeder immer nur seine eigenen sachen von
+ seinem kalender sieht mehr nicht. perfektioniere das bitte. jeder der
+ einen kalender hat soll auch sowas haben danke."
+
+ Das sind ZWEI Aussagen, und beide werden hier gemessen:
+
+ (1) WER EINEN KALENDER HAT, HAT AUCH CALLS.
+ Nicht „vier Listen tragen die Kachel" -- das waere eine
+ Abschrift, die beim naechsten neuen Rollenzuschnitt
+ auseinanderlaeuft. Geprueft wird die Regel: In JEDER Liste
+ steht Calls genau dann, wenn dort auch ein Kalender steht --
+ und direkt dahinter, in derselben Gruppe.
+
+ Gemessen vor dem Bauen:
+ admin 31 Kacheln, Kalender ja, Calls NEIN
+ hand 31 ja NEIN
+ linke 30 ja NEIN
+ modi 26 ja NEIN
+ gast 12 nein nein
+ Die Community hat keinen Kalender -- sein Satz beschreibt die
+ Lage also genau, und „alle bekommen es" waere falsch gewesen.
+
+ (2) JEDER SIEHT NUR SEINE EIGENEN.
+ Die Regel dafuer gibt es seit dem 07.09. (`termineSichtbar`).
+ Sie wird hier aber nicht GELESEN, sondern am laufenden Server
+ ausprobiert -- mit zwei Leuten, die sich gegenseitig nicht
+ sehen duerfen, und einem dritten, der dabei ist und deshalb
+ schon.
+
+ Eine Regel, die im Quelltext richtig steht, kann trotzdem an
+ einer Stelle umgangen werden, die sie nicht aufruft. Das ist
+ genau der Grund, warum hier eine Messung steht und kein Verweis
+ auf den Kommentar dort.
+
+ WARUM node:http UND NICHT fetch: Team Dogi und die Agentur melden
+ sich an verschiedenen Waenden an, und welche gilt, entscheidet der
+ Host-Kopf. `fetch` in Node setzt den selbst und ignoriert einen
+ gesetzten.
+
+ AUFRUF: node server/pruef-calls-rudel.mjs
+ ===================================================================== */
+
+import { mkdtempSync, rmSync } from "node:fs";
+import { tmpdir } from "node:os";
+import { join } from "node:path";
+import { request as httpAnfrage } from "node:http";
+import { eigenerPort } from "./helfer-port.mjs";
+import { notbremse } from "./helfer-notbremse.mjs";
+
+const PORT = await eigenerPort(import.meta, "pruef-calls-rudel");
+const ordner = mkdtempSync(join(tmpdir(), "ws-calls-"));
+process.env.WORKSPACE_DB = join(ordner, "workspace.db");
+process.env.PORT = String(PORT);
+process.env.SITE_ACCESS_SECRET = "lokaler-test";
+process.env.SITE_PUBLIC_LAUNCH_AT = "2020-01-01T00:00:00+01:00";
+
+const express = (await import("express")).default;
+const ec = express.response.cookie;
+express.response.cookie = function (n, w, o) { return ec.call(this, n, w, { ...(o || {}), secure: false }); };
+await import("./index.js");
+notbremse(240_000, "pruef-calls-rudel");
+await new Promise((r) => setTimeout(r, 900));
+
+process.on("uncaughtException", (f) => { console.error("ABSTURZ:", f); process.exit(7); });
+process.on("unhandledRejection", (f) => { console.error("ABSTURZ (Versprechen):", f); process.exit(7); });
+
+let fehler = 0, geprueft = 0;
+const melde = (t) => console.log(t);
+const ok = (b, t) => { geprueft++; console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; };
+
+const CREW = "crew.dogfather-universe.com";
+
+/* =====================================================================
+ 1. DIE REGEL: Kalender und Calls gehoeren zusammen
+ ===================================================================== */
+melde("\n=== 1. Wer einen Kalender hat, hat auch Calls ===");
+const w = await import("./workspace.js");
+const { darfSeite } = await import("./rechte.js");
+
+const ROLLEN = ["admin", "hand", "linke", "modi", "gast"];
+{
+ let mitKalender = 0, ohneKalender = 0;
+ for (const rolle of ROLLEN) {
+ const liste = w.bereicheFuer({ rolle, haus: "crew" }) || [];
+ const iKal = liste.findIndex((k) => String(k.ziel || "").startsWith("kalender.html"));
+ const iCall = liste.findIndex((k) => String(k.ziel || "").startsWith("calls.html"));
+
+ if (iKal >= 0) {
+ mitKalender++;
+ ok(iCall === iKal + 1,
+ `${rolle}: Calls steht direkt hinter dem Kalender `
+ + `(Kalender #${iKal}, Calls ${iCall < 0 ? "fehlt" : "#" + iCall})`);
+ ok(iCall >= 0 && liste[iCall].gruppe === liste[iKal].gruppe,
+ `${rolle}: und in derselben Gruppe `
+ + `("${liste[iCall]?.gruppe}" / "${liste[iKal].gruppe}")`);
+ } else {
+ ohneKalender++;
+ /* DIE GEGENRICHTUNG IST GENAUSO WICHTIG. Eine Calls-Kachel ohne
+ Kalender waere eine Seite ueber Termine bei jemandem, der
+ keine hat. */
+ ok(iCall < 0,
+ `${rolle}: kein Kalender, also auch keine Calls `
+ + `(${iCall < 0 ? "stimmt" : "aber da ist eine"})`);
+ }
+ }
+ /* DIE ANZAHL GEHOERT IN DIE BEDINGUNG: Ohne Rollen mit Kalender
+ waere die Schleife leer und trotzdem gruen. */
+ ok(mitKalender >= 4 && ohneKalender >= 1,
+ `${mitKalender} Rollen mit Kalender, ${ohneKalender} ohne — beide Faelle kamen vor`);
+}
+
+melde("");
+melde("=== 1b. Und wer die Kachel sieht, darf die Seite auch oeffnen ===");
+{
+ for (const rolle of ROLLEN) {
+ const liste = w.bereicheFuer({ rolle, haus: "crew" }) || [];
+ const hatKachel = liste.some((k) => String(k.ziel || "").startsWith("calls.html"));
+ const darf = darfSeite({ rolle, haus: "crew" }, "/workspace/calls.html");
+ ok(hatKachel === darf,
+ `${rolle}: Kachel ${hatKachel ? "ja" : "nein"} und Zutritt ${darf ? "ja" : "nein"} `
+ + `— ${hatKachel === darf ? "passt zusammen" : "PASST NICHT"}`);
+ }
+ /* GEGENPROBE: Die Messung muss einen Unterschied auch sehen koennen.
+ Eine Seite, die es nicht gibt, darf niemand oeffnen. */
+ ok(!darfSeite({ rolle: "admin", haus: "crew" }, "/workspace/gibtsnicht.html"),
+ "Gegenprobe: eine erfundene Seite darf auch DogFather nicht");
+}
+
+/* =====================================================================
+ 2. AM LAUFENDEN SERVER: jeder sieht nur seine eigenen
+ ===================================================================== */
+const { DatabaseSync } = await import("node:sqlite");
+const { scryptSync, randomBytes, createHmac } = await import("node:crypto");
+const d = new DatabaseSync(process.env.WORKSPACE_DB);
+const jetzt = new Date().toISOString();
+const schluessel = randomBytes(32).toString("hex");
+d.prepare("INSERT INTO einstellungen (schluessel, wert, geaendert, von)"
+ + " VALUES (?,?,?,NULL) ON CONFLICT(schluessel) DO NOTHING")
+ .run("code_kennung_schluessel", schluessel, jetzt);
+function anlegen(name, rolle, code) {
+ const salt = randomBytes(16).toString("hex");
+ const hash = scryptSync(code, salt, 64, { N: 32768, r: 8, p: 1, maxmem: 96 * 1024 * 1024 }).toString("hex");
+ d.prepare("INSERT INTO personen (name, rolle, code_hash, code_salt, code_n, code_kennung, aktiv, erstellt)"
+ + " VALUES (?,?,?,?,?,?,1,?)")
+ .run(name, rolle, hash, salt, 32768, createHmac("sha256", schluessel).update(code).digest("hex"), jetzt);
+ return d.prepare("SELECT last_insert_rowid() AS id").get().id;
+}
+const idChef = anlegen("Filipe", "admin", "CODE-DOGI-0001");
+const idHand = anlegen("Rieke", "hand", "CODE-HAND-0001");
+const idLinke = anlegen("Lino", "linke", "CODE-LINK-0001");
+const idModi = anlegen("Frida", "modi", "CODE-MODI-0001");
+d.close();
+
+function roh(pfad, wie = {}) {
+ return new Promise((fertig, schief) => {
+ const daten = wie.body === undefined ? null : Buffer.from(JSON.stringify(wie.body));
+ const a = httpAnfrage({
+ host: "127.0.0.1", port: PORT, path: pfad,
+ method: wie.methode || (daten ? "POST" : "GET"),
+ headers: {
+ Host: CREW,
+ ...(daten ? { "Content-Type": "application/json", "Content-Length": daten.length } : {}),
+ ...(wie.keks ? { Cookie: wie.keks } : {}),
+ Origin: `https://${CREW}`, Referer: `https://${CREW}/workspace/`,
+ },
+ }, (antwort) => {
+ const st = [];
+ antwort.on("data", (x) => st.push(x));
+ antwort.on("end", () => {
+ const text = Buffer.concat(st).toString("utf8");
+ fertig({ code: antwort.statusCode, text,
+ json: (() => { try { return JSON.parse(text); } catch { return null; } })() });
+ });
+ });
+ a.on("error", schief);
+ if (daten) a.write(daten);
+ a.end();
+ });
+}
+async function anmelden(rolle, code) {
+ const r = await new Promise((fertig, schief) => {
+ const daten = Buffer.from(JSON.stringify({ rolle, code }));
+ const a = httpAnfrage({
+ host: "127.0.0.1", port: PORT, path: "/workspace/api/anmelden", method: "POST",
+ headers: { Host: CREW, "Content-Type": "application/json", "Content-Length": daten.length,
+ Origin: `https://${CREW}`, Referer: `https://${CREW}/workspace/` },
+ }, (antwort) => {
+ const st = [];
+ antwort.on("data", (x) => st.push(x));
+ antwort.on("end", () => fertig({ code: antwort.statusCode, kopf: antwort.headers }));
+ });
+ a.on("error", schief);
+ a.write(daten); a.end();
+ });
+ const kekse = [].concat(r.kopf?.["set-cookie"] || []);
+ return { keks: kekse.map((c) => c.split(";")[0]).join("; "), code: r.code };
+}
+
+const chef = await anmelden("admin", "CODE-DOGI-0001");
+const hand = await anmelden("hand", "CODE-HAND-0001");
+const linke = await anmelden("linke", "CODE-LINK-0001");
+const modi = await anmelden("modi", "CODE-MODI-0001");
+
+melde("");
+melde("=== 2. Alle vier kommen herein ===");
+for (const [wer, s] of [["DogFather", chef], ["rechte Hand", hand],
+ ["linke Hand", linke], ["Modi", modi]]) {
+ ok(s.code === 200 && s.keks.length > 0, `${wer} ist angemeldet (${s.code})`);
+}
+
+/* Ein Zeitpunkt, der nicht vom Kalender eingeholt wird. Ein festes
+ Datum waere eine Zeitbombe: Es ist richtig, bis es vorbei ist. */
+const inZweiTagen = new Date(Date.now() + 2 * 86400_000)
+ .toISOString().slice(0, 16);
+
+const callAnlegen = (keks, titel, teilnehmer) =>
+ roh("/workspace/api/termine", {
+ keks, body: { titel, beginn: inZweiTagen, art: "call", dauer_min: 30,
+ ...(teilnehmer ? { teilnehmer } : {}) },
+ });
+const callsVon = (keks) => roh("/workspace/api/calls", { keks });
+
+melde("");
+melde("=== 3. Jeder sieht nur seine eigenen Calls ===");
+{
+ const a = await callAnlegen(hand.keks, "Gespraech der rechten Hand");
+ const b = await callAnlegen(modi.keks, "Gespraech des Modis");
+ ok(a.code === 201 || a.code === 200, `die rechte Hand legt einen Call an (${a.code})`);
+ ok(b.code === 201 || b.code === 200, `der Modi auch (${b.code})`);
+
+ const titelVon = (r) => {
+ const j = r.json || {};
+ return [...(j.offen || []), ...(j.anstehend || []), ...(j.serien || []), ...(j.erledigt || [])]
+ .map((c) => c.titel);
+ };
+
+ const beiHand = titelVon(await callsVon(hand.keks));
+ const beiModi = titelVon(await callsVon(modi.keks));
+ const beiLinke = titelVon(await callsVon(linke.keks));
+
+ ok(beiHand.includes("Gespraech der rechten Hand"),
+ `die rechte Hand sieht ihren eigenen (${beiHand.length} insgesamt)`);
+ ok(!beiHand.includes("Gespraech des Modis"),
+ "und NICHT den des Modis");
+ ok(beiModi.includes("Gespraech des Modis"),
+ `der Modi sieht seinen eigenen (${beiModi.length} insgesamt)`);
+ ok(!beiModi.includes("Gespraech der rechten Hand"),
+ "und NICHT den der rechten Hand");
+ ok(beiLinke.length === 0,
+ `die linke Hand, die an keinem beteiligt ist, sieht keinen (${beiLinke.length})`);
+
+ /* AUCH DOGFATHER NICHT -- und das ist die eigentliche Aussage.
+ Bis zum 07.09. bekam er jeden Termin von jedem. Ein Kalender, in
+ dem alles steht, ist kein Kalender mehr. */
+ const beiChef = titelVon(await callsVon(chef.keks));
+ ok(!beiChef.includes("Gespraech des Modis")
+ && !beiChef.includes("Gespraech der rechten Hand"),
+ `auch DogFather sieht die fremden nicht (${beiChef.length} bei ihm)`);
+}
+
+melde("");
+melde("=== 4. Wer eingetragen ist, sieht ihn schon ===");
+{
+ /* GEGENPROBE ZU ABSCHNITT 3. Ohne sie waere „niemand sieht etwas"
+ genauso gruen, wenn die Liste schlicht kaputt waere. */
+ const a = await callAnlegen(hand.keks, "Gespraech zu zweit", [idLinke]);
+ ok(a.code === 201 || a.code === 200, `ein Call mit der linken Hand als Teilnehmerin (${a.code})`);
+
+ const titelVon = (r) => {
+ const j = r.json || {};
+ return [...(j.offen || []), ...(j.anstehend || []), ...(j.serien || []), ...(j.erledigt || [])]
+ .map((c) => c.titel);
+ };
+ const beiLinke = titelVon(await callsVon(linke.keks));
+ const beiModi = titelVon(await callsVon(modi.keks));
+
+ ok(beiLinke.includes("Gespraech zu zweit"),
+ `die linke Hand sieht ihn jetzt (${beiLinke.length})`);
+ ok(!beiModi.includes("Gespraech zu zweit"),
+ "der Modi, der nicht eingetragen ist, weiterhin nicht");
+}
+
+melde("");
+melde("=== 5. Die Community kommt gar nicht an die Seite ===");
+{
+ /* Sie hat keinen Kalender, also auch keine Calls -- geprueft nicht
+ an der Kachel (die kann man verstecken), sondern an der Tuer. */
+ const r = await roh("/workspace/api/calls", {});
+ ok(r.code === 401 || r.code === 403 || r.code === 404,
+ `ohne Anmeldung kommt nichts (${r.code})`);
+}
+
+/* =====================================================================
+ ERGEBNIS
+ ===================================================================== */
+melde("");
+/* Die Zahl gehoert in die Bedingung, nicht nur in den Text. */
+if (geprueft < 25) {
+ console.log(` FEHL nur ${geprueft} Pruefungen gelaufen -- erwartet waren 25+`);
+ fehler++;
+}
+melde(fehler === 0
+ ? `Alles in Ordnung. ${geprueft} Pruefungen, 0 Fehler.`
+ : `${fehler} FEHLER bei ${geprueft} Pruefungen.`);
+melde("");
+try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ }
+process.exit(fehler ? 1 : 0);
diff --git a/server/pruef-kachelfarben.mjs b/server/pruef-kachelfarben.mjs
index fa94ce65..bcc19643 100644
--- a/server/pruef-kachelfarben.mjs
+++ b/server/pruef-kachelfarben.mjs
@@ -39,21 +39,19 @@ import { dirname, join } from "node:path";
const HIER = dirname(fileURLToPath(import.meta.url));
const CSS = join(HIER, "..", "workspace", "assets", "css", "start.css");
-const GRUND = "#0a121e";
+/* DIE SCHWELLEN STEHEN SEIT DEM 22.09.2026 IN tools/kachelton-regeln.mjs.
-/* DIE SCHWELLE IST GEMESSEN, NICHT GERATEN.
- Vor dem Lauf vom 22.09.2026 lag das engste Paar der benutzten Töne
- bei 0,0741 („Was ansteht" neben dem neuen Rot von „Draußen") —
- das war der Zustand, den Filipe als „ähnlich" gemeldet hat.
- Danach: 0,1028. 0,090 liegt dazwischen: Der heutige Stand hat
- Luft, ein Rückfall auf „sieht gleich aus" schlägt an. */
-const MINDEST_ABSTAND = 0.090;
+ Sie standen vorher hier UND im Rechenwerkzeug — mit verschiedenen
+ Werten. Das Werkzeug suchte die Buntheit bis 0,04 herunter, diese
+ Prüfung verlangt 0,12; ein gerechneter Ton wurde deshalb hier
+ abgelehnt, obwohl er dort der beste war. Zwei Zahlen für dieselbe
+ Regel in zwei Dateien, die nichts voneinander wissen.
-/* Der Median der Buntheit aller Töne lag bei 0,168, die fünf
- blassesten zwischen 0,064 und 0,088. Geprüft werden nur die
- BENUTZTEN — unter den unbenutzten stehen noch blasse, die niemand
- sieht und die beim nächsten Gebrauch ohnehin neu gerechnet werden. */
-const MINDEST_BUNTHEIT = 0.12;
+ Geprüft werden nur die BENUTZTEN Töne — unter den unbenutzten
+ stehen noch blasse, die niemand sieht und die beim nächsten
+ Gebrauch ohnehin neu gerechnet werden. */
+const { GRUND, MINDEST_ABSTAND, MINDEST_BUNTHEIT } =
+ await import("../tools/kachelton-regeln.mjs");
/* Was festgelegt ist — mit dem Grund, aus dem es festgelegt wurde. */
const FESTGELEGT = [
diff --git a/server/workspace.js b/server/workspace.js
index 115723bc..c186d22ed 100644
--- a/server/workspace.js
+++ b/server/workspace.js
@@ -1588,11 +1588,99 @@ const MEINE_ENTWICKLUNG_KACHEL = {
workspace-werdegang.js steht `leitung ? alle : []`, in
workspace-entwicklungs-punkte.js dasselbe. Die Kachel oeffnet die
Seite -- was darauf steht, entscheidet der Server. */
-const MODI_MIT_TREFF = [...MODI_BEREICHE, BEFINDEN_KACHEL,
+/* =====================================================================
+ CALLS & PROTOKOLLE GEHOEREN NEBEN JEDEN KALENDER (22.09.2026)
+ =====================================================================
+ Filipe: „ich will diese kachel vom workspace auch im team dogi seite.
+ ich will dass jeder immer nur seine eigenen sachen von seinem
+ kalender sieht mehr nicht. perfektioniere das bitte. jeder der einen
+ kalender hat soll auch sowas haben danke."
+
+ GEMESSEN, BEVOR GEBAUT WURDE:
+
+ Rolle Kacheln Kalender Calls
+ admin 31 ja NEIN
+ hand 31 ja NEIN
+ linke 30 ja NEIN
+ modi 26 ja NEIN
+ gast 12 nein nein
+
+ Vier Listen mit Kalender, keine mit Calls -- und die Community hat
+ keinen Kalender und braucht deshalb auch keine Calls. Sein Satz
+ „jeder der einen kalender hat" beschreibt die Lage also genau.
+
+ DESHALB IST ES EINE REGEL UND KEINE VIERFACHE EINTRAGUNG. Vier
+ Stellen von Hand zu pflegen waere die Stelle, an der es beim
+ naechsten neuen Rollenzuschnitt auseinanderlaeuft: Einer bekommt
+ einen Kalender, und die Calls vergisst man.
+
+ DIE GRUPPE WIRD MITGENOMMEN, nicht danebengeschrieben: Die neue
+ Kachel erbt `gruppe` und `gruppeUnter` vom Kalender, neben dem sie
+ steht. Zieht der Kalender eines Tages in eine andere Gruppe, zieht
+ sie mit.
+
+ DER TON IST GERECHNET, NICHT UEBERNOMMEN -- und der erste Versuch
+ war falsch.
+
+ Naheliegend war Ton 15, derselbe wie im anderen Haus: dieselbe
+ Sache, dieselbe Farbe. Als Zahl war er frei. Auf dem BILDSCHIRM
+ nicht: pruef-kachelfarben misst den Abstand zwischen je zwei
+ Kacheln an echten Bildpunkten, und 15 lag bei 0,0139 zu „Was
+ ansteht" -- unter der Grenze von 0,02. Zwei Kacheln, die gleich
+ aussehen, sind eine Kachel zu viel.
+
+ Also ausgerechnet, welcher freie Ton am weitesten von ALLEN
+ benutzten wegliegt (OKLab-Abstand):
+
+ Ton 22 #fee0b2 0,1135 aber Buntheit nur 0,068 -> zu blass
+ Ton 7 #098f71 0,1067 Buntheit 0,112 -> zu blass
+ Ton 16 #19b2c2 0,0854 Abstand unter 0,090 -> zu nah
+ Ton 15 #01d7e3 0,0279 (der erste Versuch)
+
+ KEINER DER FREIEN TOENE ERFUELLTE ALLE HAUSREGELN. Die Palette
+ kannte 42 Toene, 31 davon im Einsatz -- und unter den elf uebrigen
+ war keiner, der gleichzeitig weit genug weg (>= 0,090) und bunt
+ genug (>= 0,12) ist. Eine 32. Kachel braucht einen 32. Ton.
+
+ Also einen NEUEN gerechnet statt einen vorhandenen umzufaerben:
+ Ton 16 gehoert im anderen Haus zu „Start-Check" -- ihn zu aendern
+ haette dort eine Kachel umgefaerbt, nach der niemand gefragt hat.
+ `tools/kachel-farbe-einzeln.mjs` haelt alle vorhandenen fest und
+ sucht fuer EINEN die Farbe, die am weitesten von allen wegliegt:
+
+ Ton 43 #027afb Abstand 0,0925 Buntheit 0,212 Kontrast 4,63:1
+
+ Alle drei Regeln erfuellt, und es bleibt ein Blau -- die Kachel ist
+ damit wiedererkennbar als dieselbe wie im anderen Haus.
+ ===================================================================== */
+const CALLS_KACHEL = {
+ name: "Calls & Protokolle",
+ unter: "Gespräche mit To-dos",
+ zeichen: "calls",
+ ton: 43,
+ ziel: "calls.html",
+ szene: "lounge",
+};
+
+/** Hinter jeden Kalender einer Liste eine Calls-Kachel setzen. */
+function mitCalls(liste) {
+ const raus = [];
+ for (const k of liste) {
+ raus.push(k);
+ /* Am ZIEL erkannt, nicht am Namen: Der Name einer Kachel ist im
+ Haus schon mehrfach gewandert, das Ziel nicht. */
+ if (String(k.ziel || "").startsWith("kalender.html")) {
+ raus.push({ gruppe: k.gruppe, gruppeUnter: k.gruppeUnter, ...CALLS_KACHEL });
+ }
+ }
+ return raus;
+}
+
+const MODI_MIT_TREFF = mitCalls([...MODI_BEREICHE, BEFINDEN_KACHEL,
ENTWICKLUNG_KACHEL, WERDEGANG_KACHEL, ...TREFF_BEREICHE,
- TREFF_MODERATION_KACHEL];
-const HAND_MIT_TREFF = [...HAND_BEREICHE, ...TREFF_BEREICHE,
- TREFF_MODERATION_KACHEL, HILFE_KACHEL];
+ TREFF_MODERATION_KACHEL]);
+const HAND_MIT_TREFF = mitCalls([...HAND_BEREICHE, ...TREFF_BEREICHE,
+ TREFF_MODERATION_KACHEL, HILFE_KACHEL]);
/* DIE LINKE HAND SIEHT "DEIN TEAM" NICHT (22.09.2026).
diff --git a/tools/kachel-farbe-einzeln.mjs b/tools/kachel-farbe-einzeln.mjs
index 31e97a6f..410d3176 100644
--- a/tools/kachel-farbe-einzeln.mjs
+++ b/tools/kachel-farbe-einzeln.mjs
@@ -60,9 +60,11 @@ import { readFileSync, writeFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { dirname, join } from "node:path";
+import { GRUND, MINDEST_BUNTHEIT, HOECHST_BUNTHEIT, MINDEST_KONTRAST }
+ from "./kachelton-regeln.mjs";
+
const HIER = dirname(fileURLToPath(import.meta.url));
const CSS = join(HIER, "..", "workspace", "assets", "css", "start.css");
-const GRUND = "#0a121e";
/* ---------- Farbrechnung (gleich wie in kachel-farben.mjs) ---------- */
const linear = (c) => (c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4);
@@ -156,7 +158,16 @@ function beste(andere) {
for (let L = 0.56; L <= 0.94; L += 0.01) {
/* Von oben herunter, damit die Farbe so satt wie erlaubt wird
und nicht blass -- Filipe: „richtig geil und speziell“. */
- for (let C = 0.30; C >= 0.04; C -= 0.004) {
+ /* DIE UNTERE GRENZE IST NICHT 0,04, SONDERN DIE DER PRUEFUNG.
+ Bis zum 22.09.2026 lief die Suche bis 0,04 herunter -- und
+ lieferte fuer Ton 43 ein blasses #bcdbff mit Buntheit 0,060.
+ Abstand 0,1339, der beste Wert weit und breit, und
+ pruef-kachelfarben hat ihn trotzdem abgelehnt: zu blass.
+ Ein Werkzeug, dessen Vorschlaege die Pruefung danach
+ verwirft, ist schlimmer als keines -- man haelt das
+ Ergebnis fuer fertig. Beide lesen die Zahl jetzt aus
+ kachelton-regeln.mjs. */
+ for (let C = HOECHST_BUNTHEIT; C >= MINDEST_BUNTHEIT; C -= 0.004) {
const hex = ausOklch(L, C, H);
if (!hex) continue;
if (kontrast(hex) < 4.5) break;
diff --git a/tools/kachelton-regeln.mjs b/tools/kachelton-regeln.mjs
new file mode 100644
index 00000000..cb2394ec
--- /dev/null
+++ b/tools/kachelton-regeln.mjs
@@ -0,0 +1,51 @@
+/* =====================================================================
+ DIE REGELN FÜR EINEN KACHELTON — an einer Stelle (22.09.2026)
+ =====================================================================
+
+ ENTSTANDEN AUS EINEM WIDERSPRUCH, den niemand bemerkt hätte, bis er
+ Arbeit gekostet hat.
+
+ Für die neue Kachel „Calls & Protokolle" musste ein Ton gerechnet
+ werden. `tools/kachel-farbe-einzeln.mjs` lieferte `#bcdbff` mit
+ einem Abstand von 0,1339 — dem besten Wert weit und breit.
+ `server/pruef-kachelfarben.mjs` lehnte ihn ab:
+
+ FEHL keiner ist blasser als 0.12 — Ton 43 #bcdbff (0.060)
+
+ Beide hatten recht. Das Werkzeug suchte die Buntheit von 0,30 bis
+ herunter auf 0,04, die Prüfung verlangt mindestens 0,12. Zwei
+ Zahlen für dieselbe Regel, in zwei Dateien, die nichts voneinander
+ wissen — die eine kannte die andere nicht.
+
+ Das ist dieselbe Krankheit wie eine abgeschriebene Spaltenliste:
+ Es wird nicht falsch, es wird UNZUSTÄNDIG. Ein Werkzeug, das
+ Vorschläge macht, die die Prüfung danach ablehnt, ist schlimmer als
+ keines — man hält das Ergebnis für fertig.
+
+ Deshalb stehen die Schwellen jetzt hier, und beide Seiten lesen sie
+ von hier. Wer eine ändert, ändert sie für alle.
+ ===================================================================== */
+
+/** Der dunkle Grund, gegen den jede Kachelfarbe gemessen wird. */
+export const GRUND = "#0a121e";
+
+/* DIE SCHWELLE IST GEMESSEN, NICHT GERATEN.
+ Vor dem Lauf vom 22.09.2026 lag das engste Paar der benutzten Töne
+ bei 0,0741 („Was ansteht" neben dem neuen Rot von „Draußen") — das
+ war der Zustand, den Filipe als „ähnlich" gemeldet hat. Danach:
+ 0,1028. 0,090 liegt dazwischen: Der heutige Stand hat Luft, ein
+ Rückfall auf „sieht gleich aus" schlägt an. */
+export const MINDEST_ABSTAND = 0.090;
+
+/* Der Median der Buntheit aller Töne lag bei 0,168, die fünf
+ blassesten zwischen 0,064 und 0,088. Filipe: „die farben sollen auch
+ richtig geil und speziell sein." Augenschonend heißt dunkles Schema
+ und kein Flackern — NICHT blass. */
+export const MINDEST_BUNTHEIT = 0.12;
+
+/* Die Obergrenze. Über 0,30 wird es auf einem dunklen Grund grell, und
+ grell ist das, was die Augen belastet. */
+export const HOECHST_BUNTHEIT = 0.30;
+
+/* Nicht verhandelbar: Eine Kachelfarbe trägt auch Text und Zahlen. */
+export const MINDEST_KONTRAST = 4.5;
diff --git a/workspace/anruf-probe.html b/workspace/anruf-probe.html
index 97e22552..99fd0a53 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`. -->
-
-
+
+
-
+